added pixels2objects

This commit is contained in:
Mario Voigt 2021-10-11 20:24:10 +02:00
parent 2d4fb92dde
commit bc69b6fdb7
2 changed files with 214 additions and 0 deletions

View 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
(width1, height1). 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>

View 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()