added shape recognition. does not work for most things yet. please help bugfixing

This commit is contained in:
leyghisbb 2021-04-28 01:13:00 +02:00
parent 1bc2f73d30
commit 6a1ca669fa
8 changed files with 2348 additions and 0 deletions

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Shape Recognition</name>
<id>fablabchemnitz.de.shape_recognition</id>
<param name="MainTabs" type="notebook">
<page name="Basic" gui-text="Basic options">
<label>Simple shape
recognition. From a selection of one or many path, find straight
lines, recognizes those parallel or with similar angles or
lenght. Recognizes rectangles, circle and ellipses.</label>
<param name="keepOrigin" type="bool" gui-text="Keep origin path">false</param>
<param name="doUniformization" type="bool" gui-text="Enable uniformization">true</param>
</page>
<page name="page_segments" gui-text="Segments finding">
<label appearance="header">Segment extension</label>
<param name="segExtensionEnable" type="bool" gui-text="Enable">true</param>
<param name="segExtensionDtoSeg" type="float" gui-description="Distance (relative to segment length) from point to segment below wich point is merged to segment" gui-text="Relative distance to segment" precision="3" min="0" max="20">10.0</param>
<param name="segExtensionQual" type="float" gui-description="Fit quality for which a candidate point is actually merged" gui-text="Fit quality" precision="2" max="1.">0.2</param>
<label appearance="header">Remove very small segments/sub-paths</label>
<param name="segRemoveSmallEdge" type="bool" gui-text="Enable">true</param>
</page>
<page name="page_merging" gui-text="Segments merging">
<label appearance="header">Merge aligned consectutive segments</label>
<param name="segAngleMergeEnable" type="bool" gui-text="Enable">true</param>
<param name="segAngleMergePara" type="float" gui-description="How close to parraell do segments need to be to fit" gui-text="Fit parrell at" precision="3" max="1.">0.500</param>
<param name="segAngleMergeTol1" type="float" gui-description="Merge line if angles are less than tolarane 1" gui-text="merge lines with angles within tolerance1" precision="2" max="1.">0.48</param>
<param name="segAngleMergeTol2" type="float" gui-description="Merge line if angles are less than tolarane 2" gui-text="merge lines with angles within tolerance2" precision="3" max="1.">0.50</param>
</page>
<page name="page_normalization" gui-text="length normalization">
<param name="doEqualizeDist" type="bool" gui-text="Equalize segments of similar length">true</param>
<param name="shapeDistLocal" type="float" gui-description="Make lengths the mean of the lengths if they are within this threashold" gui-text="make lengths equal locally" precision="3" max="50.">0.3</param>
<param name="shapeDistGlobal" type="float" gui-description="Make lengths the mean of the lengths if they are within this threashold, globally" gui-text="make lengths equal globally" precision="4" max="50.">0.025</param>
</page>
<page name="page_unif" gui-text="Uniformization">
<param name="doParrallelize" type="bool" gui-text="Parallelize segments">true</param>
<param name="doKnownAngle" type="bool" gui-text="Set segment angles to closest remarkable angles">true</param>
<param name="doEqualizeRadius" type="bool" gui-text="Equalize circle radius of similar length">true</param>
<param name="doCenterCircOnSeg" type="bool" gui-text="Center circle center on nearby segment">true</param>
</page>
</param>
<effect>
<object-type>path</object-type>
<effects-menu>
<submenu name="FabLab Chemnitz">
<submenu name="Tracing/Edge Detection"/>
</submenu>
</effects-menu>
</effect>
<script>
<command location="inx" interpreter="python">shapereco.py</command>
</script>
</inkscape-extension>

View File

@ -0,0 +1,836 @@
#!/usr/bin/env python
'''
Copyright (C) 2017 , Pierre-Antoine Delsart
This file is part of InkscapeShapeReco.
InkscapeShapeReco 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 InkscapeShapeReco; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Quick description:
This extension uses all selected path, ignoring all other selected objects.
It tries to regularize hand drawn paths BY :
- evaluating if the path is a full circle or ellipse
- else finding sequences of aligned points and replacing them by a simple segment.
- changing the segments angles to the closest remarkable angle (pi/2, pi/3, pi/6, etc...)
- eqalizing all segments lengths which are close to each other
- replacing 4 segments paths by a rectangle object if this makes sens (giving the correct rotation to the rectangle).
Requires numpy.
'''
import sys
sys.path.append('/usr/share/inkscape/extensions')
import inkex
import gettext
_ = gettext.gettext
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
from shaperrec import geometric
from shaperrec import internal
from shaperrec import groups
from shaperrec import manipulation
from shaperrec import extenders
from shaperrec import miscellaneous
import numpy
numpy.set_printoptions(precision=3)
class PreProcess():
def removeSmallEdge(paths, wTot, hTot):
"""Remove small Path objects which stand between 2 Segments (or at the ends of the sequence).
Small means the bbox of the path is less then 5% of the mean of the 2 segments."""
if len(paths)<2:
return
def getdiag(points):
xmin, ymin, w, h = geometric.computeBox(points)
return numpy.sqrt(w**2+h**2), w, h
removeSeg=[]
def remove(p):
removeSeg.append(p)
if hasattr(p, "__next__") : p.next.prev = p.prev
if p.prev: p.prev.next = p.__next__ if hasattr(p, "__next__") else None
p.effectiveNPoints =0
debug(' --> remove !', p, p.length, len(p.points))
for p in paths:
if len(p.points)==0 :
remove(p)
continue
# select only path between 2 segments
next, prev = p.__next__ if hasattr(p, "__next__") else None, p.prev
if next is None: next = prev
if prev is None: prev = next
if not (False if next == None else next.isSegment()) or not (False if prev == None else prev.isSegment()) : continue
#diag = getdiag(p.points)
diag, w, h = getdiag(p.points)
debug(p, p.pointN, ' removing edge diag = ', diag, p.length, ' l=', next.length+prev.length, 'totDim ', (wTot, hTot))
debug( ' ---> ', prev, next)
#t TODO: his needs to be parameterized
# remove last or first very small in anycase
doRemove = prev==next and (diag < 0.05*(wTot+hTot)*0.5 )
if not doRemove:
# check if this small
isLarge = diag > (next.length+prev.length)*0.1 # check size relative to neighbour
isLarge = isLarge or w > 0.2*wTot or h > 0.2*hTot # check size w.r.t total size
# is it the small side of a long rectangle ?
dd = prev.distanceTo(next.pointN)
rect = abs(prev.unitv.dot(next.unitv))>0.98 and diag > dd*0.5
doRemove = not( isLarge or rect )
if doRemove:
remove(p)
if next != prev:
prev.setIntersectWithNext(next)
debug('removed Segments ', removeSeg)
for p in removeSeg:
paths.remove(p)
def prepareParrallelize( segs):
"""Group Segment by their angles (segments are grouped together if their deltAangle is within 0.15 rad)
The 'newAngle' member of segments in a group are then set to the mean angle of the group (where angles are all
considered in [-pi, pi])
segs : list of segments
"""
angles = numpy.array([s.angle for s in segs ])
angles[numpy.where(angles<0)] += geometric._pi # we care about direction, not angle orientation
clList = miscellaneous.clusterValues(angles, 0.30, refScaleAbs='abs')#was 15
pi = numpy.pi
for cl in clList:
anglecount = {}
for angle in angles[list(cl)]:
# #angleDeg = int(angle * 360.0 / (2.0*pi))
if not angle in anglecount:
anglecount[angle] = 1
else:
anglecount[angle] += 1
anglecount = {k: v for k, v in sorted(list(anglecount.items()), key=lambda item: item[1], reverse=True)}
meanA = anglecount.popitem()[0]#.items()[1]#sorted(anglecount.items(), key = lambda kv:(kv[1], kv[0]), reverse=True)[1][1]
#meanA = float(meanA) * (2.0*pi) / 360.0
#meanA = angles[list(cl)].mean()
for i in cl:
seg = segs[i]
seg.newAngle = meanA if seg.angle>=0. else meanA-geometric._pi
def prepareDistanceEqualization(segs, relDelta=0.1):
""" Input segments are grouped according to their length :
- for each length L, find all other lengths within L*relDelta. of L.
- Find the larger of such subgroup.
- repeat the procedure on remaining lengths until none is left.
Each length in a group is set to the mean length of the group
segs : a list of segments
relDelta : float, minimum relative distance.
"""
lengths = numpy.array( [x.tempLength() for x in segs] )
clusters = miscellaneous.clusterValues(lengths, relDelta)
if len(clusters)==1:
# deal with special case with low num of segments
# --> don't let a single segment alone
if len(clusters[0])+1==len(segs):
clusters[0]=list(range(len(segs))) # all
allDist = []
for cl in clusters:
dmean = sum( lengths[i] for i in cl ) / len(cl)
allDist.append(dmean)
for i in cl:
segs[i].setNewLength(dmean)
debug( i, ' set newLength ', dmean, segs[i].length, segs[i].dumpShort())
return allDist
def prepareRadiusEqualization(circles, otherDists, relSize=0.2):
"""group circles radius and distances into cluster.
Then set circles radius according to the mean of the clusters they belong to."""
ncircles = len(circles)
lengths = numpy.array( [c.radius for c in circles]+otherDists )
indices = numpy.array( list(range(ncircles+len(otherDists))) )
clusters = miscellaneous.clusterValues(numpy.stack([ lengths, indices ], 1 ), relSize, refScaleAbs='local' )
debug('prepareRadiusEqualization radius ', repr(lengths))
debug('prepareRadiusEqualization clusters ', clusters)
allDist = []
for cl in clusters:
dmean = sum( lengths[i] for i in cl ) / len(cl)
#print cl , dmean ,
allDist.append(dmean)
if len(cl)==1:
continue
for i in cl:
if i< ncircles:
circles[i].radius = dmean
debug(' post radius ', [c.radius for c in circles] )
return allDist
def centerCircOnSeg(circles, segments, relSize=0.18):
""" move centers of circles onto the segments if close enough"""
for circ in circles:
circ.moved = False
for seg in segments:
for circ in circles:
d = seg.distanceTo(circ.center)
#debug( ' ', seg.projectPoint(circ.center))
if d < circ.radius*relSize and not circ.moved :
circ.center = seg.projectPoint(circ.center)
circ.moved = True
def adjustToKnownAngle( paths):
""" Check current angle against remarkable angles. If close enough, change it
paths : a list of segments"""
for seg in paths:
a = seg.tempAngle()
i = (abs(geometric.vec_in_mPi_pPi(geometric.knownAngle - a) )).argmin()
seg.newAngle = geometric.knownAngle[i]
debug( ' Known angle ', seg, seg.tempAngle(), ' -> ', geometric.knownAngle[i])
## if abs(geometric.knownAngle[i] - a) < 0.08:
class PostProcess():
def mergeConsecutiveParralels(segments, options):
ignoreNext=False
newList=[]
for s in segments:
if ignoreNext:
ignoreNext=False
continue
if not s.isSegment():
newList.append(s)
continue
if not hasattr(s, "__next__"):
newList.append(s)
continue
if not s.next.isSegment():
newList.append(s)
continue
d = geometric.closeAngleAbs(s.angle, s.next.angle)
if d < options.segAngleMergePara:
debug("merging ", s.angle, s.next.angle )
snew = s.mergedWithNext(doRefit=False)
ignoreNext=True
newList.append(snew)
else:
debug("notmerging ", s.angle, s.next.angle )
newList.append(s)
if len(segments)>len(newList):
debug("merged parallel ", segments, '-->', newList)
return newList
def uniformizeShapes(pathGroupList, options):
allSegs = [ p for g in pathGroupList for p in g.listOfPaths if p.isSegment() ]
if options.doParrallelize:
PreProcess.prepareParrallelize(allSegs)
if options.doKnownAngle:
PreProcess.adjustToKnownAngle(allSegs)
adjustAng = options.doKnownAngle or options.doParrallelize
allShapeDist = []
for g in [ group for group in pathGroupList if not isinstance(group, groups.Circle)]:
# first pass : independently per path
if adjustAng:
manipulation.adjustAllAngles(g.listOfPaths)
g.listOfPaths[:] = PostProcess.mergeConsecutiveParralels(g.listOfPaths, options)
if options.doEqualizeDist:
allShapeDist=allShapeDist + PreProcess.prepareDistanceEqualization([p for p in g.listOfPaths if p.isSegment()], options.shapeDistLocal ) ##0.30
manipulation.adjustAllDistances([p for p in g.listOfPaths if p.isSegment()]) #findme was group.li..
## # then 2nd global pass, with tighter criteria
if options.doEqualizeDist:
allShapeDist=PreProcess.prepareDistanceEqualization(allSegs, options.shapeDistGlobal) ##0.08
for g in [ group for group in pathGroupList if not isinstance(group, groups.Circle)]:
manipulation.adjustAllDistances([p for p in g.listOfPaths if p.isSegment()])
#TODO: I think this is supposed to close thje paths and it is failing
for g in pathGroupList:
if g.isClosing and not isinstance(g, groups.Circle):
debug('Closing intersec ', g.listOfPaths[0].point1, g.listOfPaths[0].pointN )
g.listOfPaths[-1].setIntersectWithNext(g.listOfPaths[0])
circles=[ group for group in pathGroupList if isinstance(group, groups.Circle)]
if options.doEqualizeRadius:
PreProcess.prepareRadiusEqualization(circles, allShapeDist)
if options.doCenterCircOnSeg:
PreProcess.centerCircOnSeg(circles, allSegs)
pathGroupList = [manipulation.toRemarkableShape(g) for g in pathGroupList]
return pathGroupList
class FitShapes():
def checkForCircle(points, tangents):
"""Determine if the points and their tangents represent a circle
The difficulty is to be able to recognize ellipse while avoiding paths small fluctuations a
nd false positive due to badly drawn rectangle or non-convex closed curves.
Method : we consider angle of tangent as function of lenght on path.
For circles these are : angle = c1 x lenght + c0. (c1 ~1)
We calculate dadl = d(angle)/d(length) and compare to c1.
We use 3 criteria :
* num(dadl > 6) : number of sharp angles
* length(dadl<0.3)/totalLength : lengths of straight lines within the path.
* totalLength/(2pi x radius) : fraction of lenght vs a plain circle
Still failing to recognize elongated ellipses...
"""
if len(points)<10:
return False, 0
if all(points[0]==points[-1]): # last exactly equals the first.
# Ignore last point for this check
points = points[:-1]
tangents = tangents[:-1]
#print 'Removed last ', points
xmin, ymin, w, h = geometric.computeBox( points)
diag2=(w*w+h*h)
diag = numpy.sqrt(diag2)*0.5
norms = numpy.sqrt(numpy.sum( tangents**2, 1 ))
angles = numpy.arctan2( tangents[:, 1], tangents[:, 0] )
#debug( 'angle = ', repr(angles))
N = len(angles)
deltas = points[1:] - points[:-1]
deltasD = numpy.concatenate([ [geometric.D(points[0], points[-1])/diag], numpy.sqrt(numpy.sum( deltas**2, 1 )) / diag] )
# locate and avoid the point when swicthing
# from -pi to +pi. The point is around the minimum
imin = numpy.argmin(angles)
debug(' imin ', imin)
angles = numpy.roll(angles, -imin)
deltasD = numpy.roll(deltasD, -imin)
n=int(N*0.1)
# avoid fluctuations by removing points around the min
angles=angles[n:-n]
deltasD=deltasD[n:-n]
deltasD = deltasD.cumsum()
N = len(angles)
# smooth angles to avoid artificial bumps
angles = manipulation.smoothArray(angles, n=max(int(N*0.03), 2) )
deltaA = angles[1:] - angles[:-1]
deltasDD = (deltasD[1:] -deltasD[:-1])
deltasDD[numpy.where(deltasDD==0.)] = 1e-5*deltasD[0]
dAdD = abs(deltaA/deltasDD)
belowT, count = True, 0
for v in dAdD:
if v>6 and belowT:
count+=1
belowT = False
belowT= (v<6)
temp = (deltasD, angles, tangents, dAdD )
fracStraight = numpy.sum(deltasDD[numpy.where(dAdD<0.3)])/(deltasD[-1]-deltasD[0])
curveLength = deltasD[-1]/3.14
#print "SSS ",count , fracStraight
if curveLength> 1.4 or fracStraight>0.4 or count > 6:
isCircle =False
else:
isCircle= (count < 4 and fracStraight<=0.3) or \
(fracStraight<=0.1 and count<5)
if not isCircle:
return False, 0
# It's a circle !
radius = points - numpy.array([xmin+w*0.5, ymin+h*0.5])
radius_n = numpy.sqrt(numpy.sum( radius**2, 1 )) # normalize
mini = numpy.argmin(radius_n)
rmin = radius_n[mini]
maxi = numpy.argmax(radius_n)
rmax = radius_n[maxi]
# void points around maxi and mini to make sure the 2nd max is found
# on the "other" side
n = len(radius_n)
radius_n[maxi]=0
radius_n[mini]=0
for i in range(1, int(n/8+1)):
radius_n[(maxi+i)%n]=0
radius_n[(maxi-i)%n]=0
radius_n[(mini+i)%n]=0
radius_n[(mini-i)%n]=0
radius_n_2 = [ r for r in radius_n if r>0]
rmax_2 = max(radius_n_2)
rmin_2 = min(radius_n_2) # not good !!
anglemax = numpy.arccos( radius[maxi][0]/rmax)*numpy.sign(radius[maxi][1])
return True, (xmin+w*0.5, ymin+h*0.5, 0.5*(rmin+rmin_2), 0.5*(rmax+rmax_2), anglemax)
def checkForArcs(points, tangents):
"""Determine if the points and their tangents represent a circle
The difficulty is to be able to recognize ellipse while avoiding paths small fluctuations a
nd false positive due to badly drawn rectangle or non-convex closed curves.
Method : we consider angle of tangent as function of lenght on path.
For circles these are : angle = c1 x lenght + c0. (c1 ~1)
We calculate dadl = d(angle)/d(length) and compare to c1.
We use 3 criteria :
* num(dadl > 6) : number of sharp angles
* length(dadl<0.3)/totalLength : lengths of straight lines within the path.
* totalLength/(2pi x radius) : fraction of lenght vs a plain circle
Still failing to recognize elongated ellipses...
"""
if len(points)<10:
return False, 0
if all(points[0]==points[-1]): # last exactly equals the first.
# Ignore last point for this check
points = points[:-1]
tangents = tangents[:-1]
print(('Removed last ', points))
xmin, ymin, w, h = geometric.computeBox( points)
diag2=(w*w+h*h)
diag = numpy.sqrt(diag2)*0.5
norms = numpy.sqrt(numpy.sum( tangents**2, 1 ))
angles = numpy.arctan2( tangents[:, 1], tangents[:, 0] )
#debug( 'angle = ', repr(angles))
N = len(angles)
deltas = points[1:] - points[:-1]
deltasD = numpy.concatenate([ [geometric.D(points[0], points[-1])/diag], numpy.sqrt(numpy.sum( deltas**2, 1 )) / diag] )
# locate and avoid the point when swicthing
# from -pi to +pi. The point is around the minimum
imin = numpy.argmin(angles)
debug(' imin ', imin)
angles = numpy.roll(angles, -imin)
deltasD = numpy.roll(deltasD, -imin)
n=int(N*0.1)
# avoid fluctuations by removing points around the min
angles=angles[n:-n]
deltasD=deltasD[n:-n]
deltasD = deltasD.cumsum()
N = len(angles)
# smooth angles to avoid artificial bumps
angles = manipulation.smoothArray(angles, n=max(int(N*0.03), 2) )
deltaA = angles[1:] - angles[:-1]
deltasDD = (deltasD[1:] -deltasD[:-1])
deltasDD[numpy.where(deltasDD==0.)] = 1e-5*deltasD[0]
dAdD = abs(deltaA/deltasDD)
belowT, count = True, 0
self.temp = (deltasD, angles, tangents, dAdD )
#TODO: Loop over deltasDD searching for curved segments, no sharp bumps and a curve of at least 1/4 pi
curveStart = 0
curveToTest= numpy.array([deltasDD[curveStart]]);
dAdDd = numpy.array([dAdD[curveStart]])
v = dAdD[curveStart]
belowT= (v<6)
for i in range(1, deltasDD.size):
curveToTest = numpy.append(curveToTest, deltasDD[i])
dAdDd = numpy.append(dAdDd, dAdD[i])
fracStraight = numpy.sum(curveToTest[numpy.where(dAdDd<0.3)])/(deltasD[i]-deltasD[curveStart])
curveLength = (deltasD[i]-deltasD[curveStart])/3.14
v = dAdD[i]
if v>6 and belowT:
count+=1
belowT = False
belowT= (v<6)
inkex.debug("SSS "+str(count) +":"+ str(fracStraight))
if curveLength> 1.4 or fracStraight>0.4 or count > 8:
inkex.debug("curveLengtha:" + str(curveLength) +"fracStraight:"+str(fracStraight)+"count:"+str(count))
isArc=False
curveStart=int(i)
curveToTest= numpy.array([deltasDD[curveStart]]);
v = dAdD[curveStart]
dAdDd = numpy.array([dAdD[curveStart]])
belowT= (v<6)
count = 0
continue
else:
inkex.debug("curveLengthb:" + str(curveLength) +"fracStraight:"+str(fracStraight)+"count:"+str(count))
isArc= (count < 4 and fracStraight<=0.3) or \
(fracStraight<=0.1 and count<5)
if not isArc:
return False, 0
# It's a circle !
radius = points - numpy.array([xmin+w*0.5, ymin+h*0.5])
radius_n = numpy.sqrt(numpy.sum( radius**2, 1 )) # normalize
mini = numpy.argmin(radius_n)
rmin = radius_n[mini]
maxi = numpy.argmax(radius_n)
rmax = radius_n[maxi]
# void points around maxi and mini to make sure the 2nd max is found
# on the "other" side
n = len(radius_n)
radius_n[maxi]=0
radius_n[mini]=0
for i in range(1, int(n/8+1)):
radius_n[(maxi+i)%n]=0
radius_n[(maxi-i)%n]=0
radius_n[(mini+i)%n]=0
radius_n[(mini-i)%n]=0
radius_n_2 = [ r for r in radius_n if r>0]
rmax_2 = max(radius_n_2)
rmin_2 = min(radius_n_2) # not good !!
anglemax = numpy.arccos( radius[maxi][0]/rmax)*numpy.sign(radius[maxi][1])
return True, (xmin+w*0.5, ymin+h*0.5, 0.5*(rmin+rmin_2), 0.5*(rmax+rmax_2), anglemax)
def tangentEnvelop(svgCommandsList, refNode, options):
a, svgCommandsList = geometric.toArray(svgCommandsList)
tangents = manipulation.buildTangents(a)
newSegs = [ internal.Segment.fromCenterAndDir( p, t ) for (p, t) in zip(a, tangents) ]
debug("build envelop ", newSegs[0].point1, newSegs[0].pointN)
clustersInd = manipulation.clusterAngles( [s.angle for s in newSegs] )
debug("build envelop cluster: ", clustersInd)
return TangentEnvelop( newSegs, svgCommandsList, refNode)
def isClosing(wTot, hTot, d):
aR = min(wTot/hTot, hTot/wTot)
maxDim = max(wTot, hTot)
# was 0.2
return aR*0.5 > d/maxDim
def curvedFromTangents(svgCommandsList, refNode, x, y, wTot, hTot, d, isClosing, sourcepoints, tangents, options):
# debug('isClosing ', isClosing, maxDim, d)
# global quantities :
hasArcs = False
res = ()
# Check if circle -----------------------
if isClosing:
if len(sourcepoints)<9:
return groups.PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=True)
isCircle, res = FitShapes.checkForCircle( sourcepoints, tangents)
debug("Is Circle = ", isCircle )
if isCircle:
x, y, rmin, rmax, angle = res
debug("Circle -> ", rmin, rmax, angle )
if rmin/rmax>0.7:
circ = groups.Circle((x, y), 0.5*(rmin+rmax), refNode )
else:
circ = groups.Circle((x, y), rmin, refNode, rmax=rmax, angle=angle)
circ.points = sourcepoints
return circ
#else:
# hasArcs, res = FitShapes.checkForArcs( sourcepoints, tangents)
#else:
#hasArcs, res = FitShapes.checkForArcs( sourcepoints, tangents)
# -----------------------
if hasArcs:
x, y, rmin, rmax, angle = res
debug("Circle -> ", rmin, rmax, angle )
if rmin/rmax>0.7:
circ = groups.Circle((x, y), 0.5*(rmin+rmax), refNode )
else:
circ = groups.Circle((x, y), rmin, refNode, rmax=rmax, angle=angle)
circ.points = sourcepoints
return circ
return None
def segsFromTangents(svgCommandsList, refNode, options):
"""Finds segments part in a list of points represented by svgCommandsList.
The method is to build the (averaged) tangent vectors to the curve.
Aligned points will have tangent with similar angle, so we cluster consecutive angles together
to define segments.
Then we extend segments to connected points not already part of other segments.
Then we merge consecutive segments with similar angles.
"""
sourcepoints, svgCommandsList = geometric.toArray(svgCommandsList)
d = geometric.D(sourcepoints[0], sourcepoints[-1])
x, y, wTot, hTot = geometric.computeBox(sourcepoints)
if wTot == 0: wTot = 0.001
if hTot == 0: hTot = 0.001
if d==0:
# then we remove the last point to avoid null distance
# in other calculations
sourcepoints = sourcepoints[:-1]
svgCommandsList = svgCommandsList[:-1]
isClosing = FitShapes.isClosing(wTot, hTot, d)
if len(sourcepoints) < 4:
return groups.PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=isClosing)
tangents = manipulation.buildTangents(sourcepoints, isClosing=isClosing)
aCurvedSegment = FitShapes.curvedFromTangents(svgCommandsList, refNode, x, y, wTot, hTot, d, isClosing, sourcepoints, tangents, options)
if not aCurvedSegment == None:
return aCurvedSegment
# cluster points by angle of their tangents -------------
tgSegs = [ internal.Segment.fromCenterAndDir( p, t ) for (p, t) in zip(sourcepoints, tangents) ]
clustersInd = sorted(manipulation.clusterAngles( [s.angle for s in tgSegs] ))
debug("build envelop cluster: ", clustersInd)
# build Segments from clusters
newSegs = []
for imin, imax in clustersInd:
if imin+1< imax: # consider clusters with more than 3 points
seg = manipulation.fitSingleSegment(sourcepoints[imin:imax+1])
elif imin+1==imax: # 2 point path : we build a segment
seg = internal.Segment.from2Points(sourcepoints[imin], sourcepoints[imax], sourcepoints[imin:imax+1])
else:
seg = internal.Path( sourcepoints[imin:imax+1] )
seg.sourcepoints = sourcepoints
newSegs.append( seg )
manipulation.resetPrevNextSegment( newSegs )
debug(newSegs)
# -----------------------
# -----------------------
# Merge consecutive Path objects
updatedSegs=[]
def toMerge(p):
l=[p]
setattr(p, 'merged', True)
if hasattr(p, "__next__") and not p.next.isSegment():
l += toMerge(p.next)
return l
for i, seg in enumerate(newSegs[:-1]):
if seg.isSegment():
updatedSegs.append( seg)
continue
if hasattr(seg, 'merged'): continue
mergeList = toMerge(seg)
debug('merging ', mergeList)
p = internal.Path(numpy.concatenate([ p.points for p in mergeList]) )
debug('merged == ', p.points)
updatedSegs.append(p)
if not hasattr(newSegs[-1], 'merged'): updatedSegs.append( newSegs[-1])
debug("merged path", updatedSegs)
newSegs = manipulation.resetPrevNextSegment( updatedSegs )
# Extend segments -----------------------------------
if options.segExtensionEnable:
newSegs = extenders.SegmentExtender.extendSegments( newSegs, options.segExtensionDtoSeg, options.segExtensionQual )
debug("extended segs", newSegs)
newSegs = manipulation.resetPrevNextSegment( newSegs )
debug("extended segs", newSegs)
# ----------------------------------------
# ---------------------------------------
# merge consecutive segments with close angle
updatedSegs=[]
if options.segAngleMergeEnable:
newSegs = miscellaneous.mergeConsecutiveCloseAngles( newSegs, mangle=options.segAngleMergeTol1 )
newSegs=manipulation.resetPrevNextSegment(newSegs)
debug(' __ 2nd angle merge')
newSegs = miscellaneous.mergeConsecutiveCloseAngles( newSegs, mangle=options.segAngleMergeTol2 ) # 2nd pass
newSegs=manipulation.resetPrevNextSegment(newSegs)
debug('after merge ', len(newSegs), newSegs)
# Check if first and last also have close angles.
if isClosing and len(newSegs)>2 :
first, last = newSegs[0], newSegs[-1]
if first.isSegment() and last.isSegment():
if geometric.closeAngleAbs( first.angle, last.angle) < 0.1:
# force merge
points= numpy.concatenate( [ last.points, first.points] )
newseg = manipulation.fitSingleSegment(points)
newseg.next = first.__next__ if hasattr(first, "__next__") else None
last.prev.next = None
newSegs[0]=newseg
newSegs.pop()
# -----------------------------------------------------
# remove negligible Path/Segments between 2 large Segments
if options.segRemoveSmallEdge:
PreProcess.removeSmallEdge(newSegs, wTot, hTot)
newSegs=manipulation.resetPrevNextSegment(newSegs)
debug('after remove small ', len(newSegs), newSegs)
# -----------------------------------------------------
# -----------------------------------------------------
# Extend segments to their intersections
for p in newSegs:
if p.isSegment() and hasattr(p, "__next__"):
p.setIntersectWithNext()
# -----------------------------------------------------
return groups.PathGroup(newSegs, svgCommandsList, refNode, isClosing)
# *************************************************************
# The inkscape extension
# *************************************************************
class ShapeReco(inkex.EffectExtension):
def add_arguments(self, pars):
pars.add_argument("--title")
pars.add_argument("--keepOrigin", dest="keepOrigin", default=False, type=inkex.Boolean, help="Do not replace path")
pars.add_argument("--MainTabs")
pars.add_argument("--segExtensionDtoSeg", dest="segExtensionDtoSeg", default=0.03, type=float, help="max distance from point to segment")
pars.add_argument("--segExtensionQual", dest="segExtensionQual", default=0.5, type=float, help="segment extension fit quality")
pars.add_argument("--segExtensionEnable", dest="segExtensionEnable", default=True, type=inkex.Boolean, help="Enable segment extension")
pars.add_argument("--segAngleMergeEnable", dest="segAngleMergeEnable", default=True, type=inkex.Boolean, help="Enable merging of almost aligned consecutive segments")
pars.add_argument("--segAngleMergeTol1", dest="segAngleMergeTol1", default=0.2, type=float, help="merging with tollarance 1")
pars.add_argument("--segAngleMergeTol2", dest="segAngleMergeTol2", default=0.35, type=float, help="merging with tollarance 2")
pars.add_argument("--segAngleMergePara", dest="segAngleMergePara", default=0.001, type=float, help="merge lines as parralels if they fit")
pars.add_argument("--segRemoveSmallEdge", dest="segRemoveSmallEdge", default=True, type=inkex.Boolean, help="Enable removing very small segments")
pars.add_argument("--doUniformization", dest="doUniformization", default=True, type=inkex.Boolean, help="Preform angles and distances uniformization")
for opt in ["doParrallelize", "doKnownAngle", "doEqualizeDist", "doEqualizeRadius", "doCenterCircOnSeg"]:
pars.add_argument( "--"+opt, dest=opt, default=True, type=inkex.Boolean, help=opt)
pars.add_argument("--shapeDistLocal", dest="shapeDistLocal", default=0.3, type=float, help="Pthe percentage of difference at which we make lengths equal, locally")
pars.add_argument("--shapeDistGlobal", dest="shapeDistGlobal", default=0.025, type=float, help="Pthe percentage of difference at which we make lengths equal, globally")
def effect(self):
rej='{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}type'
paths = []
for id, node in list(self.svg.selected.items()):
if node.tag == '{http://www.w3.org/2000/svg}path' and rej not in list(node.keys()):
paths.append(node)
shapes = self.extractShapes(paths)
# add new shapes in SVG document
self.addShapesToDoc( shapes )
def extractShapesFromID( self, *nids, **options ):
"""for debugging purpose """
eList = []
for nid in nids:
el = self.getElementById(nid)
if el is None:
print(("Cant find ", nid))
return
eList.append(el)
class tmp:
pass
self.options = self.OptionParser.parse_args()[0]
self.options._update_careful(options)
nodes=self.extractShapes(eList)
self.shape = nodes[0]
def buildShape(self, node):
def rotationAngle(tr):
if tr and tr.startswith('rotate'):
# retrieve the angle :
return float(tr[7:-1].split(','))
else:
return 0.
if node.tag.endswith('path'):
g = FitShapes.segsFromTangents(node.path.to_arrays(), node, self.options)
elif node.tag.endswith('rect'):
tr = node.get('transform', None)
if tr and tr.startswith('matrix'):
return None # can't deal with scaling
recSize = numpy.array([node.get('width'), node.get('height')])
recCenter = numpy.array([node.get('x'), node.get('y')]) + recSize/2
angle=rotationAngle(tr)
g = groups.Rectangle( recSize, recCenter, 0, [], node)
elif node.tag.endswith('circle'):
g = groups.Circle(node.get('cx'), node.get('cy'), node.get('r'), [], node )
elif node.tag.endswith('ellipse'):
if tr and tr.startswith('matrix'):
return None # can't deal with scaling
angle=rotationAngle(tr)
rx = node.get('rx')
ry = node.get('ry')
g = groups.Circle(node.get('cx'), node.get('cy'), ry, rmax=rx, angle=angle, refNode=node)
return g
def extractShapes( self, nodes ):
"""The main function.
nodes : a list of nodes"""
analyzedNodes = []
# convert nodes to list of segments (groups.PathGroup) or Circle
for n in nodes :
g = self.buildShape(n)
if g :
analyzedNodes.append( g )
# uniformize shapes
if self.options.doUniformization:
analyzedNodes = PostProcess.uniformizeShapes(analyzedNodes, self.options)
return analyzedNodes
def addShapesToDoc(self, pathGroupList):
for group in pathGroupList:
debug("final ", group.listOfPaths, group.refNode )
debug("final-style ", group.refNode.get('style'))
# change to Rectangle if possible :
finalshape = manipulation.toRemarkableShape( group )
ele = group.addToNode( group.refNode)
group.setNodeStyle(ele, group.refNode)
if not self.options.keepOrigin:
group.refNode.xpath('..')[0].remove(group.refNode)
if __name__ == '__main__':
ShapeReco().run()

View File

@ -0,0 +1,130 @@
import numpy
import sys
from shaperrec import manipulation
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
##**************************************
##
class SegmentExtender:
"""Extend Segments part of a list of Path by aggregating points from neighbouring Path objects.
There are 2 concrete subclasses for extending forward and backward (due to technical reasons).
"""
def __init__(self, relD, fitQ):
self.relD = relD
self.fitQ = fitQ
def nextPaths(self, seg):
pL = []
p = self.getNext(seg) # prev or next
while p :
if p.isSegment(): break
if p.mergedObj is None: break
pL.append(p)
p = self.getNext(p)
if pL==[]:
return []
return pL
def extend(self, seg):
nextPathL = self.nextPaths(seg)
debug('extend ', self.extDir, seg, nextPathL, seg.length, len(nextPathL))
if nextPathL==[]: return seg
pointsToTest = numpy.concatenate( [p.points for p in nextPathL] )
mergeD = seg.length*self.relD
#print seg.point1 , seg.pointN, pointsToTest
pointsToFit, addedPoints = self.pointsToFit(seg, pointsToTest, mergeD)
if len(pointsToFit)==0:
return seg
newseg = manipulation.fitSingleSegment(pointsToFit)
if newseg.quality()>self.fitQ: # fit failed
return seg
debug( ' EXTENDING ! ', len(seg.points), len(addedPoints) )
self.removePath(seg, newseg, nextPathL, addedPoints )
newseg.points = pointsToFit
seg.mergedObj= newseg
newseg.sourcepoints = seg.sourcepoints
return newseg
@staticmethod
def extendSegments(segmentList, relD=0.03, qual=0.5):
"""Perform Segment extension from list of Path segmentList
returns the updated list of Path objects"""
fwdExt = FwdExtender(relD, qual)
bwdExt = BwdExtender(relD, qual)
# tag all objects with an attribute pointing to the extended object
for seg in segmentList:
seg.mergedObj = seg # by default the extended object is self
# extend each segments, starting by the longest
for seg in sorted(segmentList, key = lambda s : s.length, reverse=True):
if seg.isSegment():
newseg=fwdExt.extend(seg)
seg.mergedObj = bwdExt.extend(newseg)
# the extension procedure has marked as None the mergedObj
# which have been swallowed by an extension.
# filter them out :
updatedSegs=[seg.mergedObj for seg in segmentList if seg.mergedObj]
return updatedSegs
class FwdExtender(SegmentExtender):
extDir='Fwd'
def getNext(self, seg):
return seg.__next__ if hasattr(seg, "__next__") else None
def pointsToFit(self, seg, pointsToTest, mergeD):
distancesToLine =abs(seg.a*pointsToTest[:, 0]+seg.b*pointsToTest[:, 1]+seg.c)
goodInd=len(pointsToTest)
for i, d in reversed(list(enumerate(distancesToLine))):
if d<mergeD: goodInd=i;break
addedPoints = pointsToTest[:len(pointsToTest-goodInd)]
#debug( ' ++ pointsToFit ' , mergeD, i ,len(pointsToTest), addedPoints , seg.points )
return numpy.concatenate([seg.points, addedPoints]), addedPoints
def removePath(self, seg, newseg, nextPathL, addedPoints):
npoints = len(addedPoints)
acc=0
newseg.prev = seg.prev
for p in nextPathL:
if (acc+len(p.points))<=npoints:
p.mergedObj = None
acc += len(p.points)
else:
newseg.next = p
p.points = p.points[:(npoints-acc-len(p.points))]
break
class BwdExtender(SegmentExtender):
extDir='Bwd'
def getNext(self, seg):
return seg.prev
def pointsToFit(self, seg, pointsToTest, mergeD):
# TODO: shouldn't the distances be sorted cclosest to furthest
distancesToLine =abs(seg.a*pointsToTest[:, 0]+seg.b*pointsToTest[:, 1]+seg.c)
goodInd=len(pointsToTest)
for i, d in enumerate(distancesToLine):
if d<mergeD: goodInd=i; break
addedPoints = pointsToTest[goodInd:]
#debug( ' ++ pointsToFit ' , mergeD, i ,len(pointsToTest), addedPoints , seg.points )
return numpy.concatenate([addedPoints, seg.points]), addedPoints
def removePath(self, seg, newseg, nextPathL, addedPoints):
npoints = len(addedPoints)
acc=0
newseg.next = seg.__next__ if hasattr(seg, "__next__") else None
for p in reversed(nextPathL):
if (acc+len(p.points))<=npoints:
p.mergedObj = None
acc += len(p.points)
else:
newseg.prev = p
p.points = p.points[(npoints-acc-len(p.points)):]
break

View File

@ -0,0 +1,305 @@
import numpy
import sys
import collections
numpy.set_printoptions(precision=3)
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
curveFragments = 10
def qudSmRelBezCurFrag(ctrlPts, startPoint):
#just call the normal one with adjusted coordinates
ctrlPts[0] = startPoint[-2] + ctrlPts[0]
ctrlPts[1] = startPoint[-1] + ctrlPts[1]
return qudSmBezCurFrag(ctrlPts, startPoint)
def qudSmBezCurFrag(ctrlPts, startPoint):
#There are no control points
debug("startPoint: '", startPoint, "'")
debug("shouldbethehandle: '", [startPoint[2] + (startPoint[2] - startPoint[0]), startPoint[3] + (startPoint[3] - startPoint[1])], "'")
#ctrlPtsExt = [startPoint[2] + (startPoint[2] - startPoint[0]), startPoint[3] + (startPoint[3] - startPoint[1])] + ctrlPts
ctrlPtsExt = [startPoint[-2] + (startPoint[-2] - startPoint[-4]), startPoint[-1] + (startPoint[-1] - startPoint[-3])] + ctrlPts
debug("startPoint: '", startPoint, "'")
debug("ctrlPts: '", ctrlPts, "'")
debug("startPoint[-2:]: '", startPoint[-2:], "'")
debug("ctrlPtsExt: '", ctrlPtsExt, "'")
debug("shound be the same as non smooth: '", ctrlPtsExt)
return cubBezCurFrag(ctrlPtsExt, startPoint[-2:])
def qudRelBezCurFrag(ctrlPts, startPoint):
#just call the normal one with adjusted coordinates
ctrlPts[0] = startPoint[-2] + ctrlPts[0]
ctrlPts[1] = startPoint[-1] + ctrlPts[1]
return qudBezCurFrag(ctrlPts, startPoint)
def qudBezCurFrag(ctrlPts, startPoint):
#tested working
debug("startPoint: '", startPoint, "'")
return cubBezCurFrag(ctrlPts, startPoint[-2:])
def cubSmRelBezCurFrag(ctrlPts, startPoint):
#just call the normal one with adjusted coordinates
#[prevL[-1][0] + x[0:1][0] , prevL[1] + x[1:2][1]]
ctrlPts[0] = startPoint[-2] + ctrlPts[0]
ctrlPts[1] = startPoint[-1] + ctrlPts[1]
return cubSmBezCurFrag(ctrlPts, startPoint)
def cubSmBezCurFrag(ctrlPts, startPoint):
#tested working
#just call the normal one with adjusted coordinates
ctrlPtsExt = [startPoint[-2] + (startPoint[-2] - startPoint[-4]), startPoint[-1] + (startPoint[-1] - startPoint[-3])] + ctrlPts
return cubBezCurFrag(ctrlPtsExt, startPoint[-2:])
def cubRelBezCurFrag(ctrlPts, startPoint):
#just call the normal one with adjusted coordinates
ctrlPts[0] = startPoint[-2] + ctrlPts[0]
ctrlPts[1] = startPoint[-1] + ctrlPts[1]
return cubBezCurFrag(ctrlPts, startPoint)
def compute(t, points): #, _3d
#// shortcuts
if (t == 0) :
#points[0].t = 0;
return points[0:2]
order = int((int(len(points)) / 2)) - 1;
if (t == 1) :
#points[order].t = 1;
return points[order * 2:order * 2 + 2]
mt = 1 - t
p = points
# // constant?
if (order == 0):
#points[0].t = t;
return points[0:2]
#// linear?
if (order == 1):
ret = [
mt * p[0] + t * p[2],
mt * p[1] + t * p[3]
# t: t,
]
# if (_3d) {
# ret.z = mt * p[0].z + t * p[1].z;
# }
return ret
#// quadratic/cubic curve?
mt2 = 0
a = b = c = d = 0
if (order < 4):
mt2 = mt * mt
t2 = t * t
else:
sys.stderr.write("Order :'" + str(order) +"' beyond limits of function")
return [0.0, 0.0]
if (order == 2):
p = p + [0,0]
a = mt2
b = mt * t * 2
c = t2
elif (order == 3):
a = mt2 * mt
b = mt2 * t * 3
c = mt * t2 * 3
d = t * t2
ret = [
a * p[0] + b * p[2] + c * p[4] + d * p[6],
a * p[1] + b * p[3] + c * p[5] + d * p[7]
]
# if (_3d) {
# ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z;
# }
return ret
#TODO: Number of fragments should possibly be based upon the length of the segment.
def cubBezCurFrag(ctrlPts, startPoint):
#tested working
points =[]
rng = range(0, curveFragments + 1)
#sx = startPoint[0]
#sy = startPoint[1]
debug("control ", startPoint + ctrlPts)
inputPoints = startPoint + ctrlPts
for i in rng:
t = (float(i) / float(len(rng) - 1))
debug("t ", t, ":", i)
newp = compute(t, inputPoints)
#newp= [(1-t) ** 3 * startPoint[0] + 3 * ((1-t) ** 2) * t* ctrlPts[0] +3 * (1-t) * (t ** 2) * ctrlPts[2] + (t ** 3) * ctrlPts[4],
#(1-t) ** 3 * startPoint[1] + 3 * ((1-t) ** 2) * t* ctrlPts[1] +3 * (1-t) * (t ** 2) * ctrlPts[3] + (t ** 3) * ctrlPts[5]]
points.append(newp)
debug("result ", points)
return points
# *************************************************************
# a list of geometric helper functions
def toArray(parsedList):
"""Interprets a list of [(command, args),...]
where command is a letter coding for a svg path command
args are the argument of the command
"""
# The set of commands is now complete, all absolute positioning has been tested, relative positioning still neds some more testing.
# Curved parts of the path need fragmenting instead of just being taken as a straight line.
interpretCommand = {
'C': lambda x, prevL : cubBezCurFrag(x, prevL[-2:]), # cubic bezier curve. Ignore the curve. #TODO, fragment
'c': lambda x, prevL : cubRelBezCurFrag(x, prevL[-2:]), # cubic bezier curve, relative. Ignore the curve. #TODO, fragment
'S': lambda x, prevL : cubSmBezCurFrag(x, prevL), # cubic bezier curve, smooth. TODO, fragment
's': lambda x, prevL : cubSmRelBezCurFrag(x, prevL), # cubic bezier curve, smooth, relative. TODO, fragment
'Q': lambda x, prevL : qudBezCurFrag(x, prevL), # quadratic bezier curve. TODO, fragment
'q': lambda x, prevL : qudRelBezCurFrag(x, prevL), # quadratic bezier curve, relative TODO, fragment
#[[prevL[-1][0] + x[0:1][0] , prevL[1] + x[1:2][1]]], # quadratic bezier curve, relative TODO, fragment
'T': lambda x, prevL : qudSmBezCurFrag(x, prevL), # quadratic bezier curve, smooth. TODO, fragment
't': lambda x, prevL : qudSmRelBezCurFrag(x, prevL), # quadratic bezier curve, smooth, relative. TODO, fragment
'L': lambda x, prevL : [x[0:2]], # Line
'l': lambda x, prevL : [[prevL[0] + x[0:1][0] , prevL[1] + x[1:2][1]]], # Line, relative
'M': lambda x, prevL : [x[0:2]], # Move
'm': lambda x, prevL : [[prevL[0] + x[0:1][0] , prevL[1] + x[1:2][1]]], # Move, Relative
'H': lambda x, prevL : [[x[0:1][0],prevL[1]]], # Horizontal
'h': lambda x, prevL : [[prevL[0] + x[0:1][0], prevL[1]]], # Horizontal , relative
'V': lambda x, prevL : [[prevL[0], x[0:1][0]]], # Verticle
'v': lambda x, prevL : [[prevL[0], prevL[1] + x[0:1][0]]], # Verticle, relative
'A': lambda x, prevL : [x[5:7]], # Arc segment
'a': lambda x, prevL : [[prevL[0] + x[5:6][0] , prevL[1] + x[6:7][1]]], # Arc segment, relative
'Z': lambda x, prevL : [prevL[0]], # Close path
'z': lambda x, prevL : [prevL[0]], # Close path
}
#append the last set of attributes of the first element to the lookBack for cases where smooth or smooth quad segments appear at the begining of a path.
lookBack = parsedList[0][1] + parsedList[0][1]
points =[]
for i, (c, arg) in enumerate(parsedList):
debug('toArray ', i, c , arg)
debug('lookBack: ', lookBack)
newp = interpretCommand[c](arg, lookBack)
#double up if there are only two point entries to support transition into smooth beziers or arrays of smooth beziers etc..
if len(arg) == 2:
arg = arg + arg
#we only need to keep the last element in the lookBack, so remove any elemenmts in front.
lookBack = arg
debug('newPoints ', newp)
points = points + newp
a = numpy.array(points, dtype="object")
# Some times we have points *very* close to each other
# these do not bring any meaning full info, so we remove them
#
x, y, w, h = computeBox(a)
sizeC = 0.5*(w+h)
#deltas = numpy.zeros((len(a),2) )
deltas = a[1:] - a[:-1]
#deltas[-1] = a[0] - a[-1]
deltaD = numpy.sqrt(numpy.sum( deltas**2, 1 ))
sortedDind = numpy.argsort(deltaD)
# # expand longuest segments
nexp = int(len(deltaD)*0.9)
newpoints=[ None ]*len(a)
medDelta = deltaD[sortedDind[int(len(deltaD)/2)] ]
for i, ind in enumerate(sortedDind):
if deltaD[ind]/sizeC<0.005: continue
if i>nexp:
np = int(deltaD[ind]/medDelta)
pL = [a[ind]]
#print i,'=',ind,'adding ', np,' _ ', deltaD[ind], a[ind], a[ind+1]
for j in range(np-1):
f = float(j+1)/np
#print '------> ', (1-f)*a[ind]+f*a[ind+1]
pL.append( (1-f)*a[ind]+f*a[ind+1] )
newpoints[ind] = pL
else:
newpoints[ind]=[a[ind]]
if(D(a[0], a[-1])/sizeC > 0.005 ) :
newpoints[-1]=[a[-1]]
points = numpy.concatenate([p for p in newpoints if p!=None] )
# ## print ' medDelta ', medDelta, deltaD[sortedDind[-1]]
# ## print len(a) ,' ------> ', len(points)
rel_norms = numpy.sqrt(numpy.sum( deltas**2, 1 )) / sizeC
keep = numpy.concatenate([numpy.where( rel_norms >0.005 )[0], numpy.array([len(a)-1])])
#return a[keep] , [ parsedList[i] for i in keep]
#print len(a),' ',len(points)
return points, []
rotMat = numpy.array( [[1, -1], [1, 1]] )/numpy.sqrt(2)
unrotMat = numpy.array( [[1, 1], [-1, 1]] )/numpy.sqrt(2)
def setupKnownAngles():
pi = numpy.pi
#l = [ i*pi/8 for i in range(0, 9)] +[ i*pi/6 for i in [1,2,4,5,] ]
l = [ i*pi/8 for i in range(0, 9)] +[ i*pi/6 for i in [1, 2, 4, 5,] ] + [i*pi/12 for i in (1, 5, 7, 11)]
knownAngle = numpy.array( l )
return numpy.concatenate( [-knownAngle[:0:-1], knownAngle ])
knownAngle = setupKnownAngles()
_twopi = 2*numpy.pi
_pi = numpy.pi
def deltaAngle(a1, a2):
d = a1 - a2
return d if d > -_pi else d+_twopi
def closeAngleAbs(a1, a2):
d = abs(a1 - a2 )
return min( abs(d-_pi), abs( _twopi - d), d)
def deltaAngleAbs(a1, a2):
return abs(in_mPi_pPi(a1 - a2 ))
def in_mPi_pPi(a):
if(a>_pi): return a-_twopi
if(a<-_pi): return a+_twopi
return a
vec_in_mPi_pPi = numpy.vectorize(in_mPi_pPi)
def D2(p1, p2):
return ((p1-p2)**2).sum()
def D(p1, p2):
return numpy.sqrt(D2(p1, p2) )
def norm(p):
return numpy.sqrt( (p**2).sum() )
def computeBox(a):
"""returns the bounding box enclosing the array of points a
in the form (x,y, width, height) """
xmin, ymin = a[:, 0].min(), a[:, 1].min()
xmax, ymax = a[:, 0].max(), a[:, 1].max()
return xmin, ymin, xmax-xmin, ymax-ymin
def dirAndLength(p1, p2):
#l = max(D(p1, p2) ,1e-4)
l = D(p1, p2)
uv = (p1-p2)/l
return l, uv
def length(p1, p2):
return numpy.sqrt( D2(p1, p2) )
def barycenter(points):
"""
"""
return points.sum(axis=0)/len(points)

View File

@ -0,0 +1,278 @@
import numpy
import sys
import inkex
from lxml import etree
from shaperrec import geometric
from shaperrec import miscellaneous
from shaperrec import internal
from shaperrec import manipulation
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
# *************************************************************
# *************************************************************
# Groups of Path
#
class PathGroup(object):
"""A group of Path representing one SVG node.
- a list of Path
- a list of SVG commands describe the full node (=SVG path element)
- a reference to the inkscape node object
"""
listOfPaths = []
refSVGPathList = []
isClosing = False
refNode = None
def __init__(self, listOfPaths, refSVGPathList, refNode=None, isClosing=False):
self.refNode = refNode
self.listOfPaths = listOfPaths
self.refSVGPathList = refSVGPathList
self.isClosing=isClosing
def addToNode(self, node):
newList = miscellaneous.reformatList( self.listOfPaths)
ele = miscellaneous.addPath( newList, node)
debug("PathGroup ", newList)
return ele
def setNodeStyle(self, ele, node):
style = node.get('style')
cssClass = node.get('class')
debug("style ", style)
debug("class ", cssClass)
if style == None and cssClass == None :
style = 'fill:none; stroke:red; stroke-width:1'
if not cssClass == None:
ele.set('class', cssClass)
if not style == None:
ele.set('style', style)
@staticmethod
def toSegments(points, refSVGPathList, refNode, isClosing=False):
"""
"""
segs = [ internal.Segment.from2Points(p, points[i+1], points[i:i+2] ) for (i, p) in enumerate(points[:-1]) ]
manipulation.resetPrevNextSegment(segs)
return PathGroup( segs, refSVGPathList, refNode, isClosing)
class TangentEnvelop(PathGroup):
"""Specialization where the Path objects are all Segments and represent tangents to a curve """
def addToNode(self, node):
newList = [ ]
for s in self.listOfPaths:
newList += s.asSVGCommand(firstP=True)
debug("TangentEnvelop ", newList)
ele = miscellaneous.addPath( newList, node)
return ele
def setNodeStyle(self, ele, node):
style = node.get('style')+';marker-end:url(#Arrow1Lend)'
ele.set('style', style)
class Circle(PathGroup):
"""Specialization where the list of Path objects
is to be replaced by a Circle specified by a center and a radius.
If an other radius 'rmax' is given than the object represents an ellipse.
"""
isClosing= True
def __init__(self, center, rad, refNode=None, rmax=None, angle=0.):
self.listOfPaths = []
self.refNode = refNode
self.center = numpy.array(center)
self.radius = rad
if rmax:
self.type ='ellipse'
else:
self.type = 'circle'
self.rmax = rmax
self.angle = angle
def addToNode(self, refnode):
"""Add a node in the xml structure corresponding to this rect
refnode : xml node used as a reference, new point will be inserted a same level"""
ele = etree.Element('{http://www.w3.org/2000/svg}'+self.type)
ele.set('cx', str(self.center[0]))
ele.set('cy', str(self.center[1]))
if self.rmax:
ele.set('ry', str(self.radius))
ele.set('rx', str(self.rmax))
ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
else:
ele.set('r', str(self.radius))
refnode.xpath('..')[0].append(ele)
return ele
class Rectangle(PathGroup):
"""Specialization where the list of Path objects
is to be replaced by a Rectangle specified by a center and size (w,h) and a rotation angle.
"""
def __init__(self, center, size, angle, listOfPaths, refNode=None):
self.listOfPaths = listOfPaths
self.refNode = refNode
self.center = center
self.size = size
self.bbox = size
self.angle = angle
pos = self.center - numpy.array( size )/2
if angle != 0. :
cosa = numpy.cos(angle)
sina = numpy.sin(angle)
self.rotMat = numpy.matrix( [ [ cosa, sina], [-sina, cosa] ] )
self.rotMatstr = 'matrix(%1.7f,%1.7f,%1.7f,%1.7f,0,0)'%(cosa, sina, -sina, cosa)
#debug(' !!!!! Rotated rectangle !!', self.size, self.bbox, ' angles ', a, self.angle ,' center',self.center)
else :
self.rotMatstr = None
self.pos = pos
debug(' !!!!! Rectangle !!', self.size, self.bbox, ' angles ', self.angle, ' center', self.center)
def addToNode(self, refnode):
"""Add a node in the xml structure corresponding to this rect
refnode : xml node used as a reference, new point will be inserted a same level"""
ele = etree.Element('{http://www.w3.org/2000/svg}rect')
self.fill(ele)
refnode.xpath('..')[0].append(ele)
return ele
def fill(self, ele):
w, h = self.size
ele.set('width', str(w))
ele.set('height', str(h))
w, h = self.bbox
ele.set('x', str(self.pos[0]))
ele.set('y', str(self.pos[1]))
if self.rotMatstr:
ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
#ele.set('transform', self.rotMatstr)
@staticmethod
def isRectangle( pathGroup):
"""Check if the segments in pathGroups can form a rectangle.
Returns a Rectangle or None"""
#print 'xxxxxxxx isRectangle',pathGroups
if isinstance(pathGroup, Circle ): return None
segmentList = [p for p in pathGroup.listOfPaths if p.isSegment() ]#or p.effectiveNPoints >0]
if len(segmentList) != 4:
debug( 'rectangle Failed at length ', len(segmentList))
return None
a, b, c, d = segmentList
if geometric.length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
debug('rectangle test failed closing ', geometric.length(a.point1, d.pointN), a.length, d.length)
return None
Aac, Abd = geometric.closeAngleAbs(a.angle, c.angle), geometric.closeAngleAbs(b.angle, d.angle)
if min(Aac, Abd) > 0.07 or max(Aac, Abd) >0.27 :
debug( 'rectangle Failed at angles', Aac, Abd)
return None
notsimilarL = lambda d1, d2: abs(d1-d2)>0.20*min(d1, d2)
pi, twopi = numpy.pi, 2*numpy.pi
angles = numpy.array( [p.angle for p in segmentList] )
minAngleInd = numpy.argmin( numpy.minimum( abs(angles), abs( abs(angles)-pi), abs( abs(angles)-twopi) ) )
rotAngle = angles[minAngleInd]
width = (segmentList[minAngleInd].length + segmentList[(minAngleInd+2)%4].length)*0.5
height = (segmentList[(minAngleInd+1)%4].length + segmentList[(minAngleInd+3)%4].length)*0.5
# set rectangle center as the bbox center
x, y, w, h = geometric.computeBox( numpy.concatenate( [ p.points for p in segmentList]) )
r = Rectangle( numpy.array( [x+w/2, y+h/2]), (width, height), rotAngle, pathGroup.listOfPaths, pathGroup.refNode)
debug( ' found a rectangle !! ', a.length, b.length, c.length, d.length )
return r
class CurveGroup(PathGroup):
"""Specialization where the list of Path objects
is to be replaced by a Rectangle specified by a center and size (w,h) and a rotation angle.
"""
def __init__(self, center, size, angle, listOfPaths, refNode=None):
self.listOfPaths = listOfPaths
self.refNode = refNode
self.center = center
self.size = size
self.bbox = size
self.angle = angle
pos = self.center - numpy.array( size )/2
if angle != 0. :
cosa = numpy.cos(angle)
sina = numpy.sin(angle)
self.rotMat = numpy.matrix( [ [ cosa, sina], [-sina, cosa] ] )
self.rotMatstr = 'matrix(%1.7f,%1.7f,%1.7f,%1.7f,0,0)'%(cosa, sina, -sina, cosa)
#debug(' !!!!! Rotated rectangle !!', self.size, self.bbox, ' angles ', a, self.angle ,' center',self.center)
else :
self.rotMatstr = None
self.pos = pos
debug(' !!!!! Rectangle !!', self.size, self.bbox, ' angles ', self.angle, ' center', self.center)
def addToNode(self, refnode):
"""Add a node in the xml structure corresponding to this rect
refnode : xml node used as a reference, new point will be inserted a same level"""
ele = etree.Element('{http://www.w3.org/2000/svg}rect')
self.fill(ele)
refnode.xpath('..')[0].append(ele)
return ele
# def fill(self, ele):
# w, h = self.size
# ele.set('width', str(w))
# ele.set('height', str(h))
# w, h = self.bbox
# ele.set('x', str(self.pos[0]))
# ele.set('y', str(self.pos[1]))
# if self.rotMatstr:
# ele.set('transform', 'rotate(%3.2f,%f,%f)'%(numpy.degrees(self.angle), self.center[0], self.center[1]))
# #ele.set('transform', self.rotMatstr)
@staticmethod
def isCurvedSegment( pathGroup):
"""Check if the segments in pathGroups can form a rectangle.
Returns a Rectangle or None"""
#print 'xxxxxxxx isRectangle',pathGroups
if isinstance(pathGroup, Circle ): return None
segmentList = [p for p in pathGroup.listOfPaths if p.isSegment() ]#or p.effectiveNPoints >0]
if len(segmentList) != 4:
debug( 'rectangle Failed at length ', len(segmentList))
return None
a, b, c, d = segmentList
if geometric.length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
debug('rectangle test failed closing ', geometric.length(a.point1, d.pointN), a.length, d.length)
return None
Aac, Abd = geometric.closeAngleAbs(a.angle, c.angle), geometric.closeAngleAbs(b.angle, d.angle)
if min(Aac, Abd) > 0.07 or max(Aac, Abd) >0.27 :
debug( 'rectangle Failed at angles', Aac, Abd)
return None
notsimilarL = lambda d1, d2: abs(d1-d2)>0.20*min(d1, d2)
pi, twopi = numpy.pi, 2*numpy.pi
angles = numpy.array( [p.angle for p in segmentList] )
minAngleInd = numpy.argmin( numpy.minimum( abs(angles), abs( abs(angles)-pi), abs( abs(angles)-twopi) ) )
rotAngle = angles[minAngleInd]
width = (segmentList[minAngleInd].length + segmentList[(minAngleInd+2)%4].length)*0.5
height = (segmentList[(minAngleInd+1)%4].length + segmentList[(minAngleInd+3)%4].length)*0.5
# set rectangle center as the bbox center
x, y, w, h = geometric.computeBox( numpy.concatenate( [ p.points for p in segmentList]) )
r = Rectangle( numpy.array( [x+w/2, y+h/2]), (width, height), rotAngle, pathGroup.listOfPaths, pathGroup.refNode)
debug( ' found a rectangle !! ', a.length, b.length, c.length, d.length )
return r

View File

@ -0,0 +1,351 @@
import numpy
import sys
from shaperrec import geometric
from shaperrec import miscellaneous
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
# *************************************************************
# Internal Objects
class Path(object):
"""Private representation of a sequence of points.
A SVG node of type 'path' is splitted in several of these Path objects.
"""
next = None # next Path in the sequence of path corresponding to a SVG node
prev = None # previous Path in the sequence of path corresponding to a SVG node
sourcepoints = None # the full list of points from which this path is a subset
normalv = None # normal vector to this Path
def __init__(self, points):
"""points an array of points """
self.points = points
self.init()
def init(self):
self.effectiveNPoints = len(self.points)
if self.effectiveNPoints>1:
self.length, self.univ = geometric.dirAndLength(self.points[0], self.points[-1])
else:
self.length, self.univ = 0, numpy.array([0, 0])
if self.effectiveNPoints>0:
self.pointN=self.points[-1]
self.point1=self.points[0]
def isSegment(self):
return False
def quality(self):
return 1000
def dump(self):
n = len(self.points)
if n>0:
return 'path at '+str(self.points[0])+ ' to '+ str(self.points[-1])+' npoints=%d / %d (eff)'%(n, self.effectiveNPoints)
else:
return 'path Void !'
def setNewLength(self, l):
self.newLength = l
def removeLastPoints(self, n):
self.points = self.points[:-n]
self.init()
def removeFirstPoints(self, n):
self.points = self.points[n:]
self.init()
def costheta(self, seg):
return self.unitv.dot(seg.unitv)
def translate(self, tr):
"""Translate this path by tr"""
self.points = self.points + tr
def asSVGCommand(self, firstP=False):
svgCommands = []
com = 'M' if firstP else 'L'
for p in self.points:
svgCommands.append( [com, [p[0], p[1]] ] )
com='L'
return svgCommands
def setIntersectWithNext(self, next=None):
pass
def mergedWithNext(self, newPath=None):
""" Returns the combination of self and self.next.
sourcepoints has to be set
"""
if newPath is None: newPath = Path( numpy.concatenate([self.points, self.next.points]) )
newPath.sourcepoints = self.sourcepoints
newPath.prev = self.prev
if self.prev : newPath.prev.next = newPath
newPath.next = self.next.__next__
if newPath.__next__:
newPath.next.prev = newPath
return newPath
# *************************************************************
#
class Segment(Path):
""" A segment. Defined by its line equation ax+by+c=0 and the points from orignal paths
it is ensured that a**2+b**2 = 1
"""
QUALITYCUT = 0.9
newAngle = None # temporary angle set during the "parralelization" step
newLength = None # temporary lenght set during the "parralelization" step
# Segment Builders
@staticmethod
def from2Points( p1, p2, refPoints = None):
dirV = p2-p1
center = 0.5*(p2+p1)
return Segment.fromCenterAndDir(center, dirV, refPoints)
@staticmethod
def fromCenterAndDir( center, dirV, refPoints=None):
b = dirV[0]
a = -dirV[1]
c = - (a*center[0]+b*center[1])
if refPoints is None:
refPoints = numpy.array([ center-0.5*dirV, center+0.5*dirV] )
s = Segment( a, b, c, refPoints)
return s
def __init__(self, a,b,c, points, doinit=True):
"""a,b,c: the line parameters.
points : the array of 2D points represented by this Segment
doinit : if true will compute additionnal parameters to this Segment (first/last points, unit vector,...)
"""
self.a = a
self.b = b
self.c = c
self.points = points
d = numpy.sqrt(a**2+b**2)
if d != 1. :
self.a /= d
self.b /= d
self.c /= d
if doinit :
self.init()
def init(self):
a, b, c = self.a, self.b, self.c
x, y = self.points[0]
self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
x, y = self.points[-1]
self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
uv = self.computeDirLength()
self.distancesToLine = self.computeDistancesToLine(self.points)
self.normalv = numpy.array( [ a, b ])
self.angle = numpy.arccos( uv[0] )*numpy.sign(uv[1] )
def computeDirLength(self):
"""re-compute and set unit vector and length """
self.length, uv = geometric.dirAndLength(self.pointN, self.point1)
self.unitv = uv
return uv
def isSegment(self):
return True
def recomputeEndPoints(self):
a, b, c = self.a, self.b, self.c
x, y = self.points[0]
self.point1 = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
x, y = self.points[-1]
self.pointN = numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
self.length = numpy.sqrt( geometric.D2(self.pointN, self.point1) )
def projectPoint(self, p):
""" return the point projection of p onto this segment"""
a, b, c = self.a, self.b, self.c
x, y = p
return numpy.array( [ b*(x*b-y*a) - c*a, a*(y*a-x*b) - c*b ] )
def intersect(self, seg):
"""Returns the intersection of this line with the line seg"""
nu, nv = self.normalv, seg.normalv
u = numpy.array([[-self.c], [-seg.c]])
doRotation = min(nu.min(), nv.min()) <1e-4
if doRotation:
# rotate to avoid numerical issues
nu = numpy.array(geometric.rotMat.dot(nu))[0]
nv = numpy.array(geometric.rotMat.dot(nv))[0]
m = numpy.matrix( (nu, nv) )
i = (m**-1).dot(u)
i=numpy.array( i).swapaxes(0, 1)[0]
debug(' intersection ', nu, nv, self.angle, seg.angle, ' --> ', i)
if doRotation:
i = geometric.unrotMat.dot(i).A1
debug(' ', i)
return i
def setIntersectWithNext(self, next=None):
"""Modify self such as self.pointN is the intersection with next segment """
if next is None:
next = self.__next__
if next and next.isSegment():
if abs(self.normalv.dot(next.unitv)) < 1e-3:
return
debug(' Intersect', self, next, ' from ', self.point1, self.pointN, ' to ', next.point1, next.pointN,)
inter = self.intersect(next)
debug(' --> ', inter, ' d=', geometric.D(self.pointN, inter) )
next.point1 = inter
self.pointN = inter
self.computeDirLength()
next.computeDirLength()
def computeDistancesToLine(self, points):
"""points: array of points.
returns the array of distances to this segment"""
return abs(self.a*points[:, 0]+self.b*points[:, 1]+self.c)
def distanceTo(self, point):
return abs(self.a*point[0]+self.b*point[1]+self.c)
def inverse(self):
"""swap all x and y values. """
def inv(v):
v[0], v[1] = v[1], v[0]
for v in [self.point1, self.pointN, self.unitv, self.normalv]:
inv(v)
self.points = numpy.roll(self.points, 1, axis=1)
self.a, self.b = self.b, self.a
self.angle = numpy.arccos( self.unitv[0] )*numpy.sign(self.unitv[1] )
return
def dumpShort(self):
return 'seg '+' '+str(self.point1 )+'to '+str(self.pointN)+ ' npoints=%d | angle,offset=(%.2f,%.2f )'%(len(self.points), self.angle, self.c)+' ', self.normalv
def dump(self):
v = self.variance()
n = len(self.points)
return 'seg '+str(self.point1 )+' , '+str(self.pointN)+ ' v/l=%.2f / %.2f = %.2f r*numpy.sqrt(n)=%.2f npoints=%d | angle,offset=(%.2f,%.2f )'%(v, self.length, v/self.length, v/self.length*numpy.sqrt(n), n, self.angle, self.c)
def variance(self):
d = self.distancesToLine
return numpy.sqrt( (d**2).sum()/len(d) )
def quality(self):
n = len(self.points)
return min(self.variance()/self.length*numpy.sqrt(n), 1000)
def formatedSegment(self, firstP=False):
return self.asSVGCommand(firstP)
def asSVGCommand(self, firstP=False):
if firstP:
segment = [ ['M', [self.point1[0], self.point1[1] ] ],
['L', [self.pointN[0], self.pointN[1] ] ]
]
else:
segment = [ ['L', [self.pointN[0], self.pointN[1] ] ] ]
#debug("Segment, format : ", segment)
return segment
def replaceInList(self, startPos, fullList):
code0 = fullList[startPos][0]
segment = [ [code0, [self.point1[0], self.point1[1] ] ],
['L', [self.pointN[0], self.pointN[1] ] ]
]
l = fullList[:startPos]+segment+fullList[startPos+len(self.points):]
return l
def mergedWithNext(self, doRefit=True):
""" Returns the combination of self and self.next.
sourcepoints has to be set
"""
spoints = numpy.concatenate([self.points, self.next.points])
if doRefit:
newSeg = fitSingleSegment(spoints)
else:
newSeg = Segment.fromCenterAndDir(geometric.barycenter(spoints), self.unitv, spoints)
newSeg = Path.mergedWithNext(self, newSeg)
return newSeg
def center(self):
return 0.5*(self.point1+self.pointN)
def box(self):
return geometric.computeBox(self.points)
def translate(self, tr):
"""Translate this segment by tr """
c = self.c -self.a*tr[0] -self.b*tr[1]
self.c =c
self.pointN = self.pointN+tr
self.point1 = self.point1+tr
self.points +=tr
def adjustToNewAngle(self):
"""reset all parameters so that self.angle is change to self.newAngle """
self.a, self.b, self.c = miscellaneous.parametersFromPointAngle( 0.5*(self.point1+self.pointN), self.newAngle)
#print 'adjustToNewAngle ', self, self.angle, self.newAngle
self.angle = self.newAngle
self.normalv = numpy.array( [ self.a, self.b ])
self.unitv = numpy.array( [ self.b, -self.a ])
if abs(self.angle) > numpy.pi/2 :
if self.b > 0: self.unitv *= -1
elif self.b<0 : self.unitv *= -1
self.point1 = self.projectPoint(self.point1) # reset point1
if not hasattr(self, "__next__") or not self.next.isSegment():
# move the last point (no intersect with next)
pN = self.projectPoint(self.pointN)
dirN = pN - self.point1
lN = geometric.length(pN, self.point1)
self.pointN = dirN/lN*self.length + self.point1
#print ' ... adjusting last seg angle ',p.dump() , ' normalv=', p.normalv, 'unitv ', p.unitv
else:
self.setIntersectWithNext()
def adjustToNewDistance(self):
self.pointN = self.newLength* self.unitv + self.point1
self.length = self.newLength
def tempLength(self):
if self.newLength : return self.newLength
else : return self.length
def tempAngle(self):
if self.newAngle: return self.newAngle
return self.angle

View File

@ -0,0 +1,187 @@
import numpy
import sys
from shaperrec import groups
from shaperrec import geometric
from shaperrec import internal
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
# *************************************************************
# Object manipulation functions
def toRemarkableShape( group ):
"""Test if PathGroup instance 'group' looks like a remarkable shape (ex: Rectangle).
if so returns a new shape instance else returns group unchanged"""
r = groups.Rectangle.isRectangle( group )
if r : return r
return group
def resetPrevNextSegment(segs):
for i, seg in enumerate(segs[:-1]):
s = segs[i+1]
seg.next = s
s.prev = seg
return segs
def fitSingleSegment(a):
xmin, ymin, w, h = geometric.computeBox(a)
inverse = w<h
if inverse:
a = numpy.roll(a, 1, axis=1)
seg = regLin(a)
if inverse:
seg.inverse()
#a = numpy.roll(a,1,axis=0)
return seg
def regLin(a , returnOnlyPars=False):
"""perform a linear regression on 2dim array a. Creates a segment object in return """
sumX = a[:, 0].sum()
sumY = a[:, 1].sum()
sumXY = (a[:, 1]*a[:, 0]).sum()
a2 = a*a
sumX2 = a2[:, 0].sum()
sumY2 = a2[:, 1].sum()
N = a.shape[0]
pa = (N*sumXY - sumX*sumY)/ ( N*sumX2 - sumX*sumX)
pb = (sumY - pa*sumX) /N
if returnOnlyPars:
return pa, -1, pb
return internal.Segment(pa, -1, pb, a)
def smoothArray(a, n=2):
count = numpy.zeros(a.shape)
smootha = numpy.array(a)
for i in range(n):
count[i]=n+i+1
count[-i-1] = n+i+1
count[n:-n] = n+n+1
#debug('smooth ', len(smooth[:-2]) [)
for i in range(1, n+1):
smootha[:-i] += a[i:]
smootha[i:] += a[:-i]
return smootha/count
def buildTangents( points , averaged=True, isClosing=False):
"""build tangent vectors to the curve 'points'.
if averaged==True, the tangents are averaged with their direct neighbours (use case : smoother tangents)"""
tangents = numpy.zeros( (len(points), 2) )
i=1
tangents[:-i] += points[i:] - points[:-i] # i <- p_i+1 - p_i
tangents[i:] += points[i:] - points[:-i] # i <- p_i - p_i-1
if isClosing:
tangents[0] += tangents[0] - tangents[-1]
tangents[-1] += tangents[0] - tangents[-1]
tangents *= 0.5
if not isClosing:
tangents[0] *=2
tangents[-1] *=2
## debug('points ', points)
## debug('buildTangents --> ', tangents )
if averaged:
# average over neighbours
avTan = numpy.array(tangents)
avTan[:-1] += tangents[1:]
avTan[1:] += tangents[:-1]
if isClosing:
tangents[0]+=tangents[-1]
tangents[1]+=tangents[0]
avTan *= 1./3
if not isClosing:
avTan[0] *=1.5
avTan[-1] *=1.5
return avTan
def clusterAngles(array, dAng=0.15):
"""Cluster together consecutive angles with similar values (within 'dAng').
array : flat array of angles
returns [ ..., (indi_0, indi_1),...] where each tuple are indices of cluster i
"""
N = len(array)
closebyAng = numpy.zeros( (N, 4), dtype=int)
for i, a in enumerate(array):
cb = closebyAng[i]
cb[0] =i
cb[2]=i
cb[3]=i
c=i-1
# find number of angles within dAng in nearby positions
while c>-1: # indices below i
d=geometric.closeAngleAbs(a, array[c])
if d>dAng:
break
cb[1]+=1
cb[2]=c
c-=1
c=i+1
while c<N-1:# indices above i
d=geometric.closeAngleAbs(a, array[c])
if d>dAng:
break
cb[1]+=1
cb[3]=c
c+=1
closebyAng= closebyAng[numpy.argsort(closebyAng[:, 1]) ]
clusteredPos = numpy.zeros(N, dtype=int)
clusters = []
for cb in reversed(closebyAng):
if clusteredPos[cb[0]]==1:
continue
# try to build a cluster
minI = cb[2]
while clusteredPos[minI]==1:
minI+=1
maxI = cb[3]
while clusteredPos[maxI]==1:
maxI-=1
for i in range(minI, maxI+1):
clusteredPos[i] = 1
clusters.append( (minI, maxI) )
return clusters
def adjustAllAngles(paths):
for p in paths:
if p.isSegment() and p.newAngle is not None:
p.adjustToNewAngle()
# next translate to fit end points
tr = numpy.zeros(2)
for p in paths[1:]:
if p.isSegment() and p.prev.isSegment():
tr = p.prev.pointN - p.point1
debug(' translating ', p, ' prev is', p.prev, ' ', tr, )
p.translate(tr)
def adjustAllDistances(paths):
for p in paths:
if p.isSegment() and p.newLength is not None:
p.adjustToNewDistance()
# next translate to fit end points
tr = numpy.zeros(2)
for p in paths[1:]:
if p.isSegment() and p.prev.isSegment():
tr = p.prev.pointN - p.point1
p.translate(tr)

View File

@ -0,0 +1,209 @@
import sys
import inkex
from inkex import Path
import numpy
from shaperrec import manipulation
from lxml import etree
# *************************************************************
# debugging
def void(*l):
pass
def debug_on(*l):
sys.stderr.write(' '.join(str(i) for i in l) +'\n')
debug = void
#debug = debug_on
errwrite = void
# miscellaneous helper functions to sort
# merge consecutive segments with close angle
def mergeConsecutiveCloseAngles( segList , mangle =0.25 , q=0.5):
def toMerge(seg):
l=[seg]
setattr(seg, 'merged', True)
if hasattr(seg, "__next__") and seg.next.isSegment() :
debug('merging segs ', seg.angle, ' with : ', seg.next.point1, seg.next.pointN, ' ang=', seg.next.angle)
if geometric.deltaAngleAbs( seg.angle, seg.next.angle) < mangle:
l += toMerge(seg.next)
return l
updatedSegs = []
for i, seg in enumerate(segList[:-1]):
if not seg.isSegment() :
updatedSegs.append(seg)
continue
if hasattr(seg, 'merged'):
continue
debug(i, ' inspect merge : ', seg.point1, '-', seg.pointN, seg.angle, ' q=', seg.quality())
mList = toMerge(seg)
debug(' --> tomerge ', len(mList))
if len(mList)<2:
delattr(seg, 'merged')
updatedSegs.append(seg)
continue
points= numpy.concatenate( [p.points for p in mList] )
newseg = fitSingleSegment(points)
if newseg.quality()>q:
delattr(seg, 'merged')
updatedSegs.append(seg)
continue
for p in mList:
setattr(seg, 'merged', True)
newseg.sourcepoints = seg.sourcepoints
debug(' --> post merge qual = ', newseg.quality(), seg.pointN, ' --> ', newseg.pointN, newseg.angle)
newseg.prev = mList[0].prev
newseg.next = mList[-1].__next__
updatedSegs.append(newseg)
if not hasattr(segList[-1], 'merged') : updatedSegs.append( segList[-1])
return updatedSegs
def parametersFromPointAngle(point, angle):
unitv = numpy.array([ numpy.cos(angle), numpy.sin(angle) ])
ortangle = angle+numpy.pi/2
normal = numpy.array([ numpy.cos(ortangle), numpy.sin(ortangle) ])
genOffset = -normal.dot(point)
a, b = normal
return a, b, genOffset
def addPath(newList, refnode):
"""Add a node in the xml structure corresponding to the content of newList
newList : list of Segment or Path
refnode : xml node used as a reference, new point will be inserted a same level"""
ele = etree.Element('{http://www.w3.org/2000/svg}path')
errwrite("newList = " + str(newList) + "\n")
ele.set('d', str(Path(newList)))
refnode.xpath('..')[0].append(ele)
return ele
def reformatList( listOfPaths):
""" Returns a SVG paths list (same format as simplepath.parsePath) from a list of Path objects
- Segments in paths are added in the new list
- simple Path are retrieved from the original refSVGPathList and put in the new list (thus preserving original bezier curves)
"""
newList = []
first = True
for seg in listOfPaths:
newList += seg.asSVGCommand(first)
first = False
return newList
def clusterValues( values, relS=0.1 , refScaleAbs='range' ):
"""form clusters of similar quantities from input 'values'.
Clustered values are not necessarily contiguous in the input array.
Clusters size (that is max-min) is < relS*cluster_average """
if len(values)==0:
return []
if len(values.shape)==1:
sortedV = numpy.stack([ values, numpy.arange(len(values))], 1)
else:
# Assume value.shape = (N,2) and index are ok
sortedV = values
sortedV = sortedV[ numpy.argsort(sortedV[:, 0]) ]
sortedVV = sortedV[:, 0]
refScale = sortedVV[-1]-sortedVV[0]
#sortedVV += 2*min(sortedVV)) # shift to avoid numerical issues around 0
#print sortedVV
class Cluster:
def __init__(self, delta, sum, indices):
self.delta = delta
self.sum = sum
self.N=len(indices)
self.indices = indices
def size(self):
return self.delta/refScale
def combine(self, c):
#print ' combine ', self.indices[0], c.indices[-1], ' -> ', sortedVV[c.indices[-1]] - sortedVV[self.indices[0]]
newC = Cluster(sortedVV[c.indices[-1]] - sortedVV[self.indices[0]],
self.sum+c.sum,
self.indices+c.indices)
return newC
def originIndices(self):
return tuple(int(sortedV[i][1]) for i in self.indices)
def size_local(self):
return self.delta / sum( sortedVV[i] for i in self.indices) *len(self.indices)
def size_range(self):
return self.delta/refScale
def size_abs(self):
return self.delta
if refScaleAbs=='range':
Cluster.size = size_range
elif refScaleAbs=='local':
Cluster.size = size_local
elif refScaleAbs=='abs':
Cluster.size = size_abs
class ClusterPair:
next=None
prev=None
def __init__(self, c1, c2 ):
self.c1=c1
self.c2=c2
self.refresh()
def refresh(self):
self.potentialC =self.c1.combine(self.c2)
self.size = self.potentialC.size()
def setC1(self, c1):
self.c1=c1
self.refresh()
def setC2(self, c2):
self.c2=c2
self.refresh()
#ave = 0.5*(sortedVV[1:,0]+sortedV[:-1,0])
#deltaR = (sortedV[1:,0]-sortedV[:-1,0])/ave
cList = [Cluster(0, v, (i,)) for (i, v) in enumerate(sortedVV) ]
cpList = [ ClusterPair( c, cList[i+1] ) for (i, c) in enumerate(cList[:-1]) ]
manipulation.resetPrevNextSegment( cpList )
#print cpList
def reduceCL( cList ):
if len(cList)<=1:
return cList
cp = min(cList, key=lambda cp:cp.size)
#print '==', cp.size , relS, cp.c1.indices , cp.c2.indices, cp.potentialC.indices
while cp.size < relS:
if hasattr(cp, "__next__"):
cp.next.setC1(cp.potentialC)
cp.next.prev = cp.prev
if cp.prev:
cp.prev.setC2(cp.potentialC)
cp.prev.next = cp.__next__ if hasattr(cp, "__next__") else None
cList.remove(cp)
if len(cList)<2:
break
cp = min(cList, key=lambda cp:cp.size)
#print ' -----> ', [ (cp.c1.indices , cp.c2.indices) for cp in cList]
return cList
cpList = reduceCL(cpList)
if len(cpList)==1:
cp = cpList[0]
if cp.potentialC.size()<relS:
return [ cp.potentialC.originIndices() ]
#print cpList
if cpList==[]:
return []
finalCL = [ cp.c1.originIndices() for cp in cpList ]+[ cpList[-1].c2.originIndices() ]
return finalCL