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