Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/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 simplepath
- import gettext
- _ = gettext.gettext
- import numpy
- numpy.set_printoptions(precision=3)
- # *************************************************************
- # 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
- """
- interpretCommand = {
- 'C' : lambda x, prevL : x[-2:], # bezier curve. Ignore the curve.
- 'L' : lambda x, prevL : x[0:2],
- 'M' : lambda x, prevL : x[0:2],
- 'Z' : lambda x, prevL : prevL[0],
- }
- points =[]
- for i,(c, arg) in enumerate(parsedList):
- #debug('toArray ', i, c , arg)
- newp = interpretCommand[c](arg, points)
- points.append( newp)
- a=numpy.array( points )
- # 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[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.matrix( [[1,-1],[1,1]] )/numpy.sqrt(2)
- unrotMat = numpy.matrix( [[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)
- from numpy import sqrt
- def D2(p1, p2):
- return ((p1-p2)**2).sum()
- def D(p1, p2):
- return sqrt(D2(p1,p2) )
- def norm(p):
- return 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 sqrt( D2(p1,p2) )
- def barycenter(points):
- """
- """
- return points.sum(axis=0)/len(points)
- # *************************************************************
- # 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 = 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 = 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( 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(rotMat.dot(nu))[0]
- nv = numpy.array(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 = 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=', 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*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(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 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 = 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 self.next is None or not self.next.isSegment():
- # move the last point (no intersect with next)
- pN = self.projectPoint(self.pointN)
- dirN = pN - self.point1
- lN = 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
- # *************************************************************
- # *************************************************************
- # 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 = reformatList( self.listOfPaths)
- ele = addPath( newList , node)
- debug("PathGroup ", newList)
- return ele
- def setNodeStyle(self,ele, node):
- style = node.get('style')
- ele.set('style', style)
- @staticmethod
- def toSegments(points, refSVGPathList, refNode, isClosing=False):
- """
- """
- segs = [ Segment.from2Points(p, points[i+1], points[i:i+2] ) for (i,p) in enumerate(points[:-1]) ]
- 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 = addPath( newList , node)
- return ele
- def setNodeStyle(self,ele, node):
- style = node.get('style')+';marker-end:url(#Arrow1Lend)'
- style
- 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 = inkex.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 = inkex.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 length(a.point1, d.pointN)> 0.2*(a.length+d.length)*0.5:
- debug('rectangle test failed closing ', length(a.point1, d.pointN), a.length, d.length)
- return None
- Aac , Abd = closeAngleAbs(a.angle,c.angle), 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 = 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
- # *************************************************************
- # 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 = 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 = 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 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=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=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)
- ##**************************************
- ##
- 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 = 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
- 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
- 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
- # 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 seg.next and seg.next.isSegment() :
- debug('merging segs ', seg.angle, ' with : ' ,seg.next.point1, seg.next.pointN, ' ang=',seg.next.angle)
- if 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 = inkex.etree.Element('{http://www.w3.org/2000/svg}path')
- ele.set('d', simplepath.formatPath(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]) ]
- 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 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
- 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
- # *************************************************************
- # The inkscape extension
- # *************************************************************
- class ShapeReco(inkex.Effect):
- def __init__(self):
- inkex.Effect.__init__(self)
- self.OptionParser.add_option("--title")
- self.OptionParser.add_option("-k", "--keepOrigin", dest="keepOrigin", default=False,
- action="store", type="inkbool",
- help="Do not replace path")
- self.OptionParser.add_option( "--MainTabs")
- #self.OptionParser.add_option( "--Basic")
- self.OptionParser.add_option( "--segExtensionDtoSeg", dest="segExtensionDtoSeg", default=0.03,
- action="store", type="float",
- help="max distance from point to segment")
- self.OptionParser.add_option( "--segExtensionQual", dest="segExtensionQual", default=0.5,
- action="store", type="float",
- help="segment extension fit quality")
- self.OptionParser.add_option( "--segExtensionEnable", dest="segExtensionEnable", default=True,
- action="store", type="inkbool",
- help="Enable segment extension")
- self.OptionParser.add_option( "--segAngleMergeEnable", dest="segAngleMergeEnable", default=True,
- action="store", type="inkbool",
- help="Enable merging of almost aligned consecutive segments")
- self.OptionParser.add_option( "--segAngleMergeTol1", dest="segAngleMergeTol1", default=0.2,
- action="store", type="float",
- help="merging with tollarance 1")
- self.OptionParser.add_option( "--segAngleMergeTol2", dest="segAngleMergeTol2", default=0.35,
- action="store", type="float",
- help="merging with tollarance 2")
- self.OptionParser.add_option( "--segAngleMergePara", dest="segAngleMergePara", default=0.001,
- action="store", type="float",
- help="merge lines as parralels if they fit")
- self.OptionParser.add_option( "--segRemoveSmallEdge", dest="segRemoveSmallEdge", default=True,
- action="store", type="inkbool",
- help="Enable removing very small segments")
- self.OptionParser.add_option( "--doUniformization", dest="doUniformization", default=True,
- action="store", type="inkbool",
- help="Preform angles and distances uniformization")
- for opt in ["doParrallelize", "doKnownAngle", "doEqualizeDist" , "doEqualizeRadius" , "doCenterCircOnSeg"]:
- self.OptionParser.add_option( "--"+opt, dest=opt, default=True,
- action="store", type="inkbool",
- help=opt)
- #0.3
- self.OptionParser.add_option( "--shapeDistLocal", dest="shapeDistLocal", default=0.3,
- action="store", type="float",
- help="Pthe percentage of difference at which we make lengths equal, locally")
- #0.025
- self.OptionParser.add_option( "--shapeDistGlobal", dest="shapeDistGlobal", default=0.025,
- action="store", 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 self.selected.iteritems():
- if node.tag == '{http://www.w3.org/2000/svg}path' and rej not in node.keys():
- paths.append(node)
- shapes = self.extractShapes(paths)
- # add new shapes in SVG document
- self.addShapesToDoc( shapes )
- def removeSmallEdge(self, 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 = computeBox(points)
- return sqrt(w**2+h**2), w, h
- removeSeg=[]
- def remove(p):
- removeSeg.append(p)
- if p.next : p.next.prev = p.prev
- if p.prev: p.prev.next = p.next
- 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, p.prev
- if next is None: next = prev
- if prev is None: prev = next
- if not next.isSegment() or not 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(self,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)] += _pi # we care about direction, not angle orientation
- clList = clusterValues(angles, 0.30, refScaleAbs='abs')#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(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-_pi
- def prepareDistanceEqualization(self,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 = 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]=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(self, 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( range(ncircles+len(otherDists) ) )
- clusters = 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(self, 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(self, 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(vec_in_mPi_pPi(knownAngle - a) )).argmin()
- seg.newAngle = knownAngle[i]
- debug( ' Known angle ', seg, seg.tempAngle(),' -> ', knownAngle[i])
- ## if abs(knownAngle[i] - a) < 0.08:
- def checkForCircle(self, 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 = computeBox( points)
- diag2=(w*w+h*h)
- diag = 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([ [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 = 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)
- self.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,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(self, svgCommandsList, refNode):
- a, svgCommandsList = toArray(svgCommandsList)
- tangents = buildTangents(a)
- newSegs = [ Segment.fromCenterAndDir( p, t ) for (p,t) in zip(a,tangents) ]
- debug("build envelop ", newSegs[0].point1 , newSegs[0].pointN)
- clustersInd = clusterAngles( [s.angle for s in newSegs] )
- debug("build envelop cluster: ", clustersInd)
- return TangentEnvelop( newSegs, svgCommandsList, refNode)
- def segsFromTangents(self,svgCommandsList, refNode):
- """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 = toArray(svgCommandsList)
- d = D(sourcepoints[0],sourcepoints[-1])
- x,y,wTot,hTot = computeBox(sourcepoints)
- aR = min(wTot/hTot, hTot/wTot)
- maxDim = max(wTot, hTot)
- # was 0.2
- isClosing = aR*0.5 > d/maxDim
- debug('isClosing ', isClosing, maxDim, d)
- if d==0:
- # then we remove the last point to avoid null distance
- # in other calculations
- sourcepoints = sourcepoints[:-1]
- svgCommandsList = svgCommandsList[:-1]
- if len(sourcepoints) < 4:
- return PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=isClosing)
- tangents = buildTangents(sourcepoints, isClosing=isClosing)
- # global quantities :
- # Check if circle -----------------------
- if isClosing:
- if len(sourcepoints)<9:
- return PathGroup.toSegments(sourcepoints, svgCommandsList, refNode, isClosing=True)
- isCircle, res = self.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 = Circle((x,y),0.5*(rmin+rmax), refNode )
- else:
- circ = Circle((x,y),rmin, refNode, rmax=rmax, angle=angle)
- circ.points = sourcepoints
- return circ
- # -----------------------
- # cluster points by angle of their tangents -------------
- tgSegs = [ Segment.fromCenterAndDir( p, t ) for (p,t) in zip(sourcepoints,tangents) ]
- clustersInd = clusterAngles( [s.angle for s in tgSegs] )
- clustersInd.sort( )
- 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 = fitSingleSegment(sourcepoints[imin:imax+1])
- elif imin+1==imax: # 2 point path : we build a segment
- seg = Segment.from2Points(sourcepoints[imin], sourcepoints[imax] , sourcepoints[imin:imax+1])
- else:
- seg = Path( sourcepoints[imin:imax+1] )
- seg.sourcepoints = sourcepoints
- newSegs.append( seg )
- resetPrevNextSegment( newSegs )
- debug(newSegs)
- # -----------------------
- # -----------------------
- # Merge consecutive Path objects
- updatedSegs=[]
- def toMerge(p):
- l=[p]
- setattr(p, 'merged', True)
- if 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 = 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 = resetPrevNextSegment( updatedSegs )
- # Extend segments -----------------------------------
- if self.options.segExtensionEnable:
- newSegs = SegmentExtender.extendSegments( newSegs, self.options.segExtensionDtoSeg, self.options.segExtensionQual )
- debug("extended segs", newSegs)
- newSegs = resetPrevNextSegment( newSegs )
- debug("extended segs", newSegs)
- # ----------------------------------------
- # ---------------------------------------
- # merge consecutive segments with close angle
- updatedSegs=[]
- if self.options.segAngleMergeEnable:
- newSegs = mergeConsecutiveCloseAngles( newSegs , mangle=self.options.segAngleMergeTol1 )
- newSegs=resetPrevNextSegment(newSegs)
- debug(' __ 2nd angle merge')
- newSegs = mergeConsecutiveCloseAngles( newSegs, mangle=self.options.segAngleMergeTol2 ) # 2nd pass
- newSegs=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 closeAngleAbs( first.angle, last.angle) < 0.1:
- # force merge
- points= numpy.concatenate( [ last.points, first.points] )
- newseg = fitSingleSegment(points)
- newseg.next = first.next
- last.prev.next = None
- newSegs[0]=newseg
- newSegs.pop()
- # -----------------------------------------------------
- # remove negligible Path/Segments between 2 large Segments
- if self.options.segRemoveSmallEdge:
- self.removeSmallEdge(newSegs , wTot, hTot)
- newSegs=resetPrevNextSegment(newSegs)
- debug('after remove small ', len(newSegs),newSegs)
- # -----------------------------------------------------
- # -----------------------------------------------------
- # Extend segments to their intersections
- for p in newSegs:
- if p.isSegment() and p.next:
- p.setIntersectWithNext()
- # -----------------------------------------------------
- return PathGroup(newSegs, svgCommandsList, refNode, isClosing)
- 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'):
- parsedSVGCommands = node.get('d')
- g = self.segsFromTangents(simplepath.parsePath(parsedSVGCommands), node)
- #g = self.tangentEnvelop(simplepath.parsePath(parsedSVGCommands), node)
- 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 = Rectangle( recSize, recCenter, 0 , [], node)
- elif node.tag.endswith('circle'):
- g = 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 = 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 (PathGroup) or Circle
- for n in nodes :
- g = self.buildShape(n)
- if g :
- analyzedNodes.append( g )
- # uniformize shapes
- if self.options.doUniformization:
- analyzedNodes = self.uniformizeShapes(analyzedNodes)
- return analyzedNodes
- def mergeConsecutiveParralels(self, segments):
- ignoreNext=False
- newList=[]
- for s in segments:
- if ignoreNext:
- ignoreNext=False
- continue
- if not s.isSegment():
- newList.append(s)
- continue
- if s.next is None:
- newList.append(s)
- continue
- if not s.next.isSegment():
- newList.append(s)
- continue
- d = closeAngleAbs(s.angle ,s.next.angle)
- if d<self.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(self, pathGroupList):
- allSegs = [ p for g in pathGroupList for p in g.listOfPaths if p.isSegment() ]
- if self.options.doParrallelize:
- self.prepareParrallelize(allSegs)
- if self.options.doKnownAngle:
- self.adjustToKnownAngle(allSegs)
- adjustAng = self.options.doKnownAngle or self.options.doParrallelize
- allShapeDist = []
- for g in [ group for group in pathGroupList if not isinstance(group, Circle)]:
- # first pass : independently per path
- if adjustAng:
- adjustAllAngles(g.listOfPaths)
- g.listOfPaths[:] = self.mergeConsecutiveParralels(g.listOfPaths)
- if self.options.doEqualizeDist:
- allShapeDist=allShapeDist + self.prepareDistanceEqualization([p for p in g.listOfPaths if p.isSegment()],self.options.shapeDistLocal ) ##0.30
- ##
- adjustAllDistances([p for p in group.listOfPaths if p.isSegment()])
- ## # then 2nd global pass, with tighter criteria
- if self.options.doEqualizeDist:
- allShapeDist=self.prepareDistanceEqualization(allSegs, self.options.shapeDistGlobal) ##0.08
- for g in [ group for group in pathGroupList if not isinstance(group, Circle)]:
- 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,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, Circle)]
- if self.options.doEqualizeRadius:
- self.prepareRadiusEqualization(circles, allShapeDist)
- if self.options.doCenterCircOnSeg:
- self.centerCircOnSeg(circles, allSegs)
- pathGroupList = [toRemarkableShape(g) for g in pathGroupList]
- return pathGroupList
- def addShapesToDoc(self, pathGroupList):
- for group in pathGroupList:
- debug("final ", group.listOfPaths, group.refNode )
- # change to Rectangle if possible :
- #finalshape = 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__':
- e = ShapeReco()
- e.affect()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement