added shape recognition. does not work for most things yet. please help bugfixing
This commit is contained in:
parent
1bc2f73d30
commit
6a1ca669fa
52
extensions/fablabchemnitz/shapereco/shapereco.inx
Normal file
52
extensions/fablabchemnitz/shapereco/shapereco.inx
Normal 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>
|
836
extensions/fablabchemnitz/shapereco/shapereco.py
Normal file
836
extensions/fablabchemnitz/shapereco/shapereco.py
Normal 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()
|
130
extensions/fablabchemnitz/shapereco/shaperrec/extenders.py
Normal file
130
extensions/fablabchemnitz/shapereco/shaperrec/extenders.py
Normal 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
|
||||||
|
|
305
extensions/fablabchemnitz/shapereco/shaperrec/geometric.py
Normal file
305
extensions/fablabchemnitz/shapereco/shaperrec/geometric.py
Normal 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)
|
278
extensions/fablabchemnitz/shapereco/shaperrec/groups.py
Normal file
278
extensions/fablabchemnitz/shapereco/shaperrec/groups.py
Normal 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
|
351
extensions/fablabchemnitz/shapereco/shaperrec/internal.py
Normal file
351
extensions/fablabchemnitz/shapereco/shaperrec/internal.py
Normal 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
|
187
extensions/fablabchemnitz/shapereco/shaperrec/manipulation.py
Normal file
187
extensions/fablabchemnitz/shapereco/shaperrec/manipulation.py
Normal 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)
|
209
extensions/fablabchemnitz/shapereco/shaperrec/miscellaneous.py
Normal file
209
extensions/fablabchemnitz/shapereco/shaperrec/miscellaneous.py
Normal 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
|
||||||
|
|
Reference in New Issue
Block a user