253 lines
11 KiB
Python
253 lines
11 KiB
Python
#!/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 inkex
|
|
import urllib.parse
|
|
import urllib.request
|
|
import os
|
|
import random
|
|
from PIL import Image
|
|
from lxml import etree
|
|
from inkex import Transform
|
|
import numpy as np
|
|
from scipy.spatial import Delaunay
|
|
from scipy.cluster.vq import kmeans2
|
|
import cv2
|
|
|
|
class Triangulation(inkex.Effect):
|
|
def __init__(self):
|
|
# Call base class construtor.
|
|
inkex.Effect.__init__(self)
|
|
# Option parser:
|
|
# -n, --num_points
|
|
# -m, --edge_thresh_min
|
|
# -M, --edge_thresh_max
|
|
# -c, --add_corners
|
|
# -g, --gradient_fill
|
|
# -b, --tab
|
|
self.arg_parser.add_argument("-n", "--num_points", type=int, default=100, help="Number of points to be sampled")
|
|
self.arg_parser.add_argument("-m", "--edge_thresh_min", type=int, default=200, help="Minimum threshold for edge detection")
|
|
self.arg_parser.add_argument("-M", "--edge_thresh_max", type=int, default=255, help="Maximum threshold for edge detection")
|
|
self.arg_parser.add_argument("-c", "--add_corners", type=inkex.Boolean, default=0, help="Use corners for triangulation?")
|
|
self.arg_parser.add_argument("-g", "--gradient_fill", type=inkex.Boolean, default=0, help="Fill triangles with gradient?")
|
|
self.arg_parser.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 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, errcode) = self.checkImagePath(obj) # This also ensures the file exists
|
|
if errcode==1:
|
|
inkex.errormsg("Embedded images are not (yet?) supported, please use a linked image. Skipping.")
|
|
continue
|
|
elif errcode==2:
|
|
inkex.errormsg("The image points to a file, which seems to be missing: "+self.path+". Skipping.")
|
|
continue
|
|
|
|
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"))
|
|
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(grp)
|
|
|
|
# Check file exists and returns its path
|
|
def checkImagePath(self, obj):
|
|
xlink = obj.get(inkex.addNS('href','xlink'))
|
|
if xlink[:5] == 'data:': # Embedded image
|
|
return (None, 1)
|
|
|
|
# Code shamelessly copied from the Embed image extension :)
|
|
if xlink is None or xlink[:5] != 'data:':
|
|
absref = obj.get(inkex.addNS('absref','sodipodi'))
|
|
url = urllib.parse.urlparse(xlink)
|
|
href = urllib.request.url2pathname(url.path)
|
|
|
|
path=''
|
|
#path selection strategy:
|
|
# 1. href if absolute
|
|
# 2. realpath-ified href
|
|
# 3. absref, only if the above does not point to a file
|
|
if (href != None):
|
|
path = os.path.realpath(href)
|
|
if (not os.path.isfile(path)):
|
|
if (absref != None):
|
|
path=absref
|
|
if (not os.path.isfile(path)):
|
|
return (path, 2)
|
|
|
|
return (path, 0)
|
|
|
|
# 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, grp):
|
|
#inkex.utils.debug(self.path)
|
|
# Read image with OpenCV
|
|
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]
|
|
#pt = random.sample(coords, self.options.num_points)
|
|
pt, idx = kmeans2(np.array(coords), self.options.num_points, minit="points")
|
|
|
|
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)
|
|
|
|
Triangulation().run() |