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