added pixels2objects
This commit is contained in:
parent
2d4fb92dde
commit
bc69b6fdb7
40
extensions/fablabchemnitz/pixels2objects/pixels2objects.inx
Normal file
40
extensions/fablabchemnitz/pixels2objects/pixels2objects.inx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||||
|
<name>Pixels To Objects</name>
|
||||||
|
<id>fablabchemnitz.de.pixels2objects</id>
|
||||||
|
<param name="tab" type="notebook">
|
||||||
|
<page name="Options" gui-text="Options">
|
||||||
|
<param name="color-stroke" type="bool" gui-text="Apply color to stroke">false</param>
|
||||||
|
<param name="color-fill" type="bool" gui-text="Apply color to fill">false</param>
|
||||||
|
<param name="ignore-bg" type="bool" gui-text="Ignore background pixels">true</param>
|
||||||
|
<param name="obj-select" type="optiongroup" appearance="combo" gui-text="Instantiation of multiple objects">
|
||||||
|
<option value="coords">By image coordinates</option>
|
||||||
|
<option value="rr">Round robin</option>
|
||||||
|
<option value="random">Random</option>
|
||||||
|
</param>
|
||||||
|
<param name="scale" type="float" min="0.001" max="1000" precision="3" gui-text="Image coordinate scaling">1</param>
|
||||||
|
</page>
|
||||||
|
<page name="Help" gui-text="Help">
|
||||||
|
<label>Select a bitmapped image and one or more other objects. The
|
||||||
|
Pixels to Objects effect will place one copy of an object at
|
||||||
|
each coordinate in the bitmapped image, from (0, 0) through
|
||||||
|
(width−1, height−1). Options enable objects to have their
|
||||||
|
stroke and/or fill color adjusted to match the corresponding
|
||||||
|
image pixel; background-colored pixels to be either considered
|
||||||
|
or ignored; image coordinates to be scaled up or down; and
|
||||||
|
multiple objects to be assigned to coordinates either randomly
|
||||||
|
or deterministically.</label>
|
||||||
|
</page>
|
||||||
|
</param>
|
||||||
|
<effect>
|
||||||
|
<object-type>all</object-type>
|
||||||
|
<effects-menu>
|
||||||
|
<submenu name="FabLab Chemnitz">
|
||||||
|
<submenu name="Tracing/Edge Detection"/>
|
||||||
|
</submenu>
|
||||||
|
</effects-menu>
|
||||||
|
</effect>
|
||||||
|
<script>
|
||||||
|
<command location="inx" interpreter="python">pixels2objects.py</command>
|
||||||
|
</script>
|
||||||
|
</inkscape-extension>
|
174
extensions/fablabchemnitz/pixels2objects/pixels2objects.py
Executable file
174
extensions/fablabchemnitz/pixels2objects/pixels2objects.py
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
#! /usr/bin/python3
|
||||||
|
|
||||||
|
'''
|
||||||
|
Copyright (C) 2021 Scott Pakin, scott-ink@pakin.org
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
02110-1301, USA.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import inkex
|
||||||
|
import io
|
||||||
|
import PIL.Image
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class PixelsToObjects(inkex.EffectExtension):
|
||||||
|
"Copy an object to the coordinates of each pixel in an image."
|
||||||
|
|
||||||
|
def add_arguments(self, pars):
|
||||||
|
'Process program parameters passed in from the UI.'
|
||||||
|
pars.add_argument('--tab', help='The selected UI tab when OK was pressed')
|
||||||
|
pars.add_argument('--scale', type=float, help='Factor by which to scale image coordinates')
|
||||||
|
pars.add_argument('--color-stroke', type=inkex.Boolean, help="Color the object's stroke to match the pixel color")
|
||||||
|
pars.add_argument('--color-fill', type=inkex.Boolean, help='Fill the object with the pixel color')
|
||||||
|
pars.add_argument('--ignore-bg', type=inkex.Boolean, help='Ignore background-colored pixels')
|
||||||
|
pars.add_argument('--obj-select', choices=['coords', 'rr', 'random'], help='Specify how to iterate among multiple selected objects')
|
||||||
|
|
||||||
|
def _get_selection(self):
|
||||||
|
'Return an image and an object.'
|
||||||
|
# Bin the objects in the current selection.
|
||||||
|
img = None
|
||||||
|
objs = []
|
||||||
|
for s in self.svg.selection.values():
|
||||||
|
if isinstance(s, inkex.Image):
|
||||||
|
if img == None:
|
||||||
|
img = s # First image encountered
|
||||||
|
else:
|
||||||
|
objs.append(s) # All remaining images are consider "other" objects
|
||||||
|
else:
|
||||||
|
objs.append(s) # All non-images are "other" objects
|
||||||
|
|
||||||
|
# Ensure the selection is valid.
|
||||||
|
if img == None or objs == []:
|
||||||
|
inkex.utils.errormsg(_('Pixels to Objects requires that one image and at least one additional object be selected.'))
|
||||||
|
sys.exit(1)
|
||||||
|
return img, objs
|
||||||
|
|
||||||
|
def _generate_coords2obj(self, objs):
|
||||||
|
'''Return a function that maps pixel coordinates to an object in a
|
||||||
|
given list of SVG objects.'''
|
||||||
|
n_objs = len(objs)
|
||||||
|
obj_select = self.options.obj_select
|
||||||
|
if obj_select == 'coords':
|
||||||
|
return lambda x, y: objs[(x + y)%n_objs]
|
||||||
|
if obj_select == 'rr':
|
||||||
|
idx = 0
|
||||||
|
def coords2obj_rr(x, y):
|
||||||
|
nonlocal idx
|
||||||
|
obj = objs[idx]
|
||||||
|
idx = (idx + 1)%len(objs)
|
||||||
|
return obj
|
||||||
|
return coords2obj_rr
|
||||||
|
if obj_select == 'random':
|
||||||
|
return lambda x, y: objs[random.randint(0, n_objs - 1)]
|
||||||
|
inkex.errormessage(_('internal error: unhandled object-selection type "%s"' % obj_select))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _copy_object_to(self, obj, xy):
|
||||||
|
'''Copy an object and center the copy on the given coordinates,
|
||||||
|
optionally recoloring its stroke and/or fill'''
|
||||||
|
# Create a transform that moves the object to the origin.
|
||||||
|
xform = inkex.Transform(obj.get('transform'))
|
||||||
|
pos = obj.bounding_box(obj.composed_transform()).center
|
||||||
|
xform.add_translate(-pos)
|
||||||
|
|
||||||
|
# Modify the transform to move the object to the target coordinates.
|
||||||
|
xform.add_translate(xy)
|
||||||
|
|
||||||
|
# Duplicate the object and apply the transform.
|
||||||
|
new_obj = obj.duplicate()
|
||||||
|
new_obj.update(transform=xform)
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
def _read_image(self, img_elt):
|
||||||
|
'Read an image from either the SVG file itself or an external file.'
|
||||||
|
# Read the image from an external file.
|
||||||
|
fname = img_elt.get('sodipodi:absref')
|
||||||
|
if fname != None:
|
||||||
|
# Fully qualified filename. Read it directly.
|
||||||
|
return PIL.Image.open(fname)
|
||||||
|
xlink = img_elt.get('xlink:href')
|
||||||
|
if not xlink.startswith('data:'):
|
||||||
|
# Unqualified filename. Try reading it directly although there's a
|
||||||
|
# good chance this will fail.
|
||||||
|
return PIL.Image.open(fname)
|
||||||
|
|
||||||
|
# Read an image embedded in the SVG file itself.
|
||||||
|
try:
|
||||||
|
mime, dtype_data = xlink[5:].split(';', 1)
|
||||||
|
dtype, data64 = dtype_data.split(',', 1)
|
||||||
|
except ValueError:
|
||||||
|
inkex.errormsg('failed to parse embedded image data')
|
||||||
|
sys.exit(1)
|
||||||
|
if dtype != 'base64':
|
||||||
|
inkex.errormsg('embedded image is encoded as %s, but this plugin supports only base64' % dtype)
|
||||||
|
sys.exit(1)
|
||||||
|
raw_data = base64.decodebytes(data64.encode('utf-8'))
|
||||||
|
return PIL.Image.open(io.BytesIO(raw_data))
|
||||||
|
|
||||||
|
def _guess_background_color(self, img):
|
||||||
|
'Return the most commonly occurring color, assuming it represents the background.'
|
||||||
|
wd, ht = img.size
|
||||||
|
tally, bg_color = max(img.getcolors(wd*ht))
|
||||||
|
return bg_color
|
||||||
|
|
||||||
|
def _pixels_to_objects(self, img, coords2obj, bg_color, scale, change_stroke, change_fill):
|
||||||
|
'''Perform the core functionality of this extension, using pixel
|
||||||
|
coordinates to place object replicas.'''
|
||||||
|
# Create a new group for all the objects we'll create.
|
||||||
|
group = inkex.Group()
|
||||||
|
parent = self.svg.selection.first().getparent()
|
||||||
|
parent.add(group)
|
||||||
|
|
||||||
|
# Convert each pixel to a replica of the object, optionally
|
||||||
|
# recoloring it.
|
||||||
|
wd, ht = img.size
|
||||||
|
for y in range(ht):
|
||||||
|
for x in range(wd):
|
||||||
|
xy = (x, y)
|
||||||
|
pix = img.getpixel(xy)
|
||||||
|
if pix == bg_color:
|
||||||
|
continue
|
||||||
|
obj = coords2obj(x, y)
|
||||||
|
new_obj = self._copy_object_to(obj, inkex.Vector2d(xy)*scale)
|
||||||
|
if change_stroke:
|
||||||
|
new_obj.style['stroke'] = '#%02x%02x%02x' % (pix[0], pix[1], pix[2])
|
||||||
|
new_obj.style['stroke-opacity'] = '%.10g' % (pix[3]/255.0)
|
||||||
|
if change_fill:
|
||||||
|
new_obj.style['fill'] = '#%02x%02x%02x' % (pix[0], pix[1], pix[2])
|
||||||
|
new_obj.style['fill-opacity'] = '%.10g' % (pix[3]/255.0)
|
||||||
|
group.add(new_obj)
|
||||||
|
|
||||||
|
def effect(self):
|
||||||
|
'''Given a bitmap image and an object, copy the object to each
|
||||||
|
coordinate of the image that contains a colored pixel.'''
|
||||||
|
img_elt, objs = self._get_selection()
|
||||||
|
img = self._read_image(img_elt).convert('RGBA')
|
||||||
|
if self.options.ignore_bg:
|
||||||
|
bg_color = self._guess_background_color(img)
|
||||||
|
else:
|
||||||
|
bg_color = None
|
||||||
|
scale = self.options.scale
|
||||||
|
change_stroke = self.options.color_stroke
|
||||||
|
change_fill = self.options.color_fill
|
||||||
|
coords2obj = self._generate_coords2obj(objs)
|
||||||
|
self._pixels_to_objects(img, coords2obj, bg_color, scale, change_stroke, change_fill)
|
||||||
|
img.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
PixelsToObjects().run()
|
Reference in New Issue
Block a user