#!/usr/bin/env python3 ''' Copyright (C) 2014 Nicola Romano', romano.nicola@gmail.com version 0.1 0.1: first working version ------------------------------------------------------------------------ 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 2 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ------------------------------------------------------------------------ ''' import base64 from io import BytesIO import inkex import os from PIL import Image from lxml import etree import numpy as np from scipy.spatial import Delaunay from scipy.cluster.vq import kmeans2 import cv2 import urllib.request as urllib class ImageTriangulation(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument("-n", "--num_points", type=int, default=100, help="Number of points to be sampled") pars.add_argument("-m", "--edge_thresh_min", type=int, default=200, help="Minimum threshold for edge detection") pars.add_argument("-M", "--edge_thresh_max", type=int, default=255, help="Maximum threshold for edge detection") pars.add_argument("-c", "--add_corners", type=inkex.Boolean, default=0, help="Use corners for triangulation?") pars.add_argument("-g", "--gradient_fill", type=inkex.Boolean, default=0, help="Fill triangles with gradient?") pars.add_argument("-b", "--tab", default='', help="The tab of the interface") def draw_SVG_path(self, points, closed, style, parent): pathdesc = "M " for p in points: pathdesc = pathdesc + str(p[0]) + "," + str(p[1]) + " " if closed == 1: pathdesc = pathdesc + "Z" path = etree.SubElement(parent, inkex.addNS('path','svg'), {'style' : str(inkex.Style(style)), 'd' : pathdesc}) return path def checkImagePath(self, node): """Embed the data of the selected Image Tag element""" xlink = node.get('xlink:href') if xlink and xlink[:5] == 'data:': # No need, data alread embedded return url = urllib.urlparse(xlink) href = urllib.url2pathname(url.path) # Primary location always the filename itself. path = self.absolute_href(href or '') # Backup directory where we can find the image if not os.path.isfile(path): path = node.get('sodipodi:absref', path) if not os.path.isfile(path): inkex.errormsg('File not found "{}". Unable to embed image.').format(path) return if (os.path.isfile(path)): return path def effect(self): # Check we have something selected if len(self.svg.selected) == 0: inkex.errormsg("Please select an image.") exit() else: # Check it is an image for id, obj in self.svg.selected.items(): if obj.tag[len(obj.tag)-5:] != "image": inkex.errormsg("The selected object (" + id + ") is not an image, skipping.") continue else: self.path = self.checkImagePath(obj) # This also ensures the file exists grpname = 'img_triangles' # Make sure that the id/name is unique index = 0 while (str(self.svg.get_ids()) in grpname): grpname = 'axis' + str(index) index = index + 1 grp_name = grpname grp_attribs = {inkex.addNS('label','inkscape'):grp_name} # The group to put everything in grp = etree.SubElement(self.svg.get_current_layer(), 'g', grp_attribs) # Find image size and position in Inkscape try: self.img_x_pos = float(obj.get("x")) self.img_y_pos = float(obj.get("y")) except: self.img_x_pos = 0 self.img_y_pos = 0 self.img_width = float(obj.get("width")) self.img_height = float(obj.get("height")) if self.path is None: #check if image is embedded or linked image_string = obj.get('{http://www.w3.org/1999/xlink}href') # find comma position i = 0 while i < 40: if image_string[i] == ',': break i = i + 1 im = Image.open(BytesIO(base64.b64decode(image_string[i + 1:len(image_string)]))) else: im = Image.open(self.path) # IMPORTANT! # The numpy array is accessed as im.data[row,column], that is data[y_coord, x_coord] # Be careful not to pass coordinates as (x,y): rather use (y,x)! im.data = np.asarray(im) # The RGB components of all the pixels in the image self.red, self.green, self.blue = im.data[:,:,0], im.data[:,:,1], im.data[:,:,2] # Find real image size (self.img_real_width, self.img_real_height) = im.size self.doTriangulation(im, grp) # Converts image coordinates to screen coordinates def imgToScreen(self, x, y): newx = x / (self.img_real_width/self.img_width) + self.img_x_pos newy = y / (self.img_real_height/self.img_height) + self.img_y_pos return (newx, newy) def createLinearGradient(self, x1, y1, x2, y2, color1, color2, gradID): attribs = { 'x1' : str(x1), 'y1' : str(y1), 'x2' : str(x2), 'y2' : str(y2), 'id' : gradID, 'gradientUnits' : "userSpaceOnUse", '{'+inkex.NSS[u'xlink']+'}href': "#"+gradID } svgdefs = self.document.getroot().find(inkex.addNS('defs', 'svg')) gradient = etree.SubElement(svgdefs, inkex.addNS('linearGradient','svg'), attribs) attribs = { 'offset' : "0%", 'style' : "stop-color:"+color1+"; stop-opacity:1" } stop1 = etree.SubElement(gradient, inkex.addNS('stop','svg'), attribs) attribs = { 'offset' : "100%", 'style' : "stop-color:"+color2+"; stop-opacity:1" } stop2 = etree.SubElement(gradient, inkex.addNS('stop','svg'), attribs) return gradient def doTriangulation (self, im, grp): # Read image with OpenCV imcv = np.array(im) #imcv = cv2.imread(self.path) # Convert to grayscale gray = cv2.cvtColor(imcv,cv2.COLOR_RGB2GRAY) gray = np.float32(gray) # Find edges edges = cv2.Canny(imcv, self.options.edge_thresh_min, self.options.edge_thresh_max, 100) # Find coordinates of the edges coords = [(float(x),float(y)) for y, row in enumerate(edges) for x, col in enumerate(row) if col>0] try: pt, idx = kmeans2(np.array(coords), self.options.num_points, minit="points") except ValueError: inkex.utils.debug("Too much points. Reduce sampled points and try again!") exit(1) if self.options.add_corners: # Add the four corners corners = [(0, 0), (self.img_real_width-1, 0), (0, self.img_real_height-1), (self.img_real_width-1, self.img_real_height-1)] pt = np.vstack((pt, corners)) # Perform Delaunay triangulation tri = Delaunay(pt) tri_coord = [(pt[t[0]], pt[t[1]], pt[t[2]]) for t in tri.simplices] tri_colors = [( (self.red[int(t[0][1]),int(t[0][0])], self.green[int(t[0][1]),int(t[0][0])], self.blue[int(t[0][1]),int(t[0][0])]), (self.red[int(t[1][1]),int(t[1][0])], self.green[int(t[1][1]),int(t[1][0])], self.blue[int(t[1][1]),int(t[1][0])]), (self.red[int(t[2][1]),int(t[2][0])], self.green[int(t[2][1]),int(t[2][0])], self.blue[int(t[2][1]),int(t[2][0])]) ) for t in tri_coord] for i, c in enumerate(tri_coord): # Convert to screen coordinates v0 = self.imgToScreen(c[0][0], c[0][1]) v1 = self.imgToScreen(c[1][0], c[1][1]) v2 = self.imgToScreen(c[2][0], c[2][1]) col = tri_colors[i] fill = "" if self.options.gradient_fill: color1 = "rgb("+str(col[0][0])+","+str(col[0][1])+","+str(col[0][2])+")" color2 = "rgb("+str(0.5*col[1][0]+0.5*col[2][0])+","+ \ str(0.5*col[1][1]+0.5*col[2][1])+","+ \ str(0.5*col[1][2]+0.5*col[2][2])+")" gradID = 'linearGradient' # Make sure that the id is inique index = 0 while (str(self.svg.get_ids()) in gradID): gradID = 'linearGradient' + str(index) index = index + 1 #self.doc_ids[gradID]=1 gradient = self.createLinearGradient(v0[0], v0[1], 0.5*(v1[0]+v2[0]), 0.5*(v1[1]+v2[1]), color1, color2, gradID) fill = "url(#"+gradient.get("id")+")" else: fill = "rgb("+str(col[0][0])+","+str(col[0][1])+","+str(col[0][2])+")" tri_style = { 'stroke-width' : '1px', 'stroke-linecap' : 'round', 'stroke-opacity' : '1', 'fill' : fill, 'fill-opacity' : '1', 'stroke' : fill } self.draw_SVG_path([v0, v1, v2], 1, tri_style, grp) if __name__ == '__main__': ImageTriangulation().run()