def onStrokeAdded(self, stroke): "Tags 1's and 0's as letters (TextAnnotation)" closedDistRatio = 0.22 circularityThresh_0 = 0.80 circularityThresh_1 = 0.20 strokeLen = max(GeomUtils.strokeLength(stroke), 1) normDist = max(3, strokeLen / 5) head, tail = stroke.Points[0], stroke.Points[-1] endDist = GeomUtils.pointDistance(head.X, head.Y, tail.X, tail.Y) #If the endpoints are 1/thresh apart, actually close the thing isClosedShape = GeomUtils.pointDistanceSquared(head.X, head.Y, tail.X, tail.Y) \ < (strokeLen * closedDistRatio) ** 2 if isClosedShape: #Close the shape back up s_norm = GeomUtils.strokeNormalizeSpacing( Stroke(stroke.Points + [stroke.Points[0]]) , normDist ) else: s_norm = GeomUtils.strokeNormalizeSpacing( stroke , normDist ) curvatures = GeomUtils.strokeGetPointsCurvature(s_norm) circularity = GeomUtils.strokeCircularity( s_norm ) if isClosedShape and circularity > circularityThresh_0: height = stroke.BoundTopLeft.Y - stroke.BoundBottomRight.Y oAnnotation = TextAnnotation("0", height) l_logger.debug("Annotating %s with %s" % ( stroke, oAnnotation)) BoardSingleton().AnnotateStrokes( [stroke], oAnnotation) l_logger.debug(" Afterward: %s.annotations is %s" % ( stroke, stroke.Annotations)) elif len(stroke.Points) >= 2 \ and max(curvatures) < 0.5 \ and circularity < circularityThresh_1: if stroke.Points[0].X < stroke.Points[-1].X + strokeLen / 2.0 \ and stroke.Points[0].X > stroke.Points[-1].X - strokeLen / 2.0: height = stroke.BoundTopLeft.Y - stroke.BoundBottomRight.Y oneAnnotation = TextAnnotation("1", height) l_logger.debug("Annotating %s with %s" % ( stroke, oneAnnotation.text)) BoardSingleton().AnnotateStrokes( [stroke], oneAnnotation) l_logger.debug(" Afterward: %s.annotations is %s" % ( stroke, stroke.Annotations)) elif stroke.Points[0].Y < stroke.Points[-1].Y + strokeLen / 2.0 \ and stroke.Points[0].Y > stroke.Points[-1].Y - strokeLen / 2.0: width = stroke.BoundBottomRight.X - stroke.BoundTopLeft.X dashAnnotation = TextAnnotation("-", width * 1.5) #Treat the dash's (boosted) width as its scale l_logger.debug("Annotating %s with %s" % ( stroke, dashAnnotation.text)) BoardSingleton().AnnotateStrokes( [stroke], dashAnnotation) else: if not isClosedShape: l_logger.debug("0: Not a closed shape") if not (circularity > circularityThresh_0): l_logger.debug("0: Not circular enough: %s" % (circularity)) if not len(stroke.Points) >= 2: l_logger.debug("1: Not enough points") if not (circularity < circularityThresh_1): l_logger.debug("1: Too circular") if not (max(curvatures) < 0.5): l_logger.debug("1: Max curvature too big %s" % max(curvatures)) if not ( stroke.Points[0].X < stroke.Points[-1].X + strokeLen / 3 \ and stroke.Points[0].X > stroke.Points[-1].X - strokeLen / 3): l_logger.debug("1: Not vertical enough: \nX1 %s, \nX2 %s, \nLen %s" % (stroke.Points[0].X, stroke.Points[-1].X, strokeLen))
def classifyArrowhead(board, stroke): """Following the class 1 semantics, this classifies arrowheads""" if _isArrowHead(stroke, arrowHeadMatcher): # * (tip-point) # o o # o o # o o # o o #Get the endpoints/tip point as max curvature strokeNorm = GeomUtils.strokeNormalizeSpacing(stroke, numpoints = 7) curvatures = GeomUtils.strokeGetPointsCurvature(strokeNorm) ptIdx = curvatures.index(max(curvatures)) tip = strokeNorm.Points[ptIdx] #Middle is the point of max curvature annotation = ArrowHeadAnnotation(stroke.Points[0], tip, stroke.Points[-1]) return annotation return None
def _isArrowHead(stroke, matcher): numPts = 11 sNorm = GeomUtils.strokeNormalizeSpacing(stroke, numpoints = numPts) curvatures = GeomUtils.strokeGetPointsCurvature(sNorm) maxCurv = max(curvatures) maxCurvIdx = curvatures.index(maxCurv) #Make sure the max curvature is roughly in the middle of the stroke before even bothering # with more complicated checks if maxCurvIdx > (numPts / 5.0) and maxCurvIdx < ( 4 * numPts / 5.0): strkLen = GeomUtils.strokeLength(stroke) arrowHeadStroke = GeomUtils.strokeNormalizeSpacing(Stroke([sNorm.Points[0], sNorm.Points[maxCurvIdx], sNorm.Points[-1]]), numpoints = strkLen) #What would the approximated arrowhead look like? origStroke = GeomUtils.strokeNormalizeSpacing(stroke, numpoints = strkLen) approxAcc = GeomUtils.strokeDTWDist(sNorm, arrowHeadStroke) #logger.debug("Stroke approximates arrowhead with %s accuracy" % (approxAcc)) return approxAcc < 500000 #_isArrowHead_Template(stroke, matcher) or _isArrowHead_Template(Stroke(list(reversed(stroke.Points))), matcher) return False
def strokeCurvatureHistogram(stroke, norm_len=None, gran=10): # points = GeomUtils.strokeNormalizeSpacing( stroke, numpoints=50).Points rad2deg = 57.295 if norm_len == None: norm_len = len(stroke.Points) norm_stroke = GeomUtils.strokeNormalizeSpacing(stroke, numpoints=norm_len) # find the first 90 degree turn in the stroke curvatures = GeomUtils.strokeGetPointsCurvature(norm_stroke) for idx, ori in enumerate(curvatures): print "%s:\t|" % (idx), quantity = ori * rad2deg while quantity > 0: quantity -= gran print "X", print "\t\t%s" % (ori * rad2deg) print "_______________________________" print "Max:%s, Avg%s" % (max(curvatures), sum(curvatures) / float(len(curvatures))) print "_______________________________"
def tagBox(self, stroke): endPointDistPct = 0.10 #How close (as % of length) the points have to be to each other boxApproxThresh = 50000 #The DTW distance between the stroke and how it best fits a box stkLen = GeomUtils.strokeLength(stroke) ep1, ep2 = stroke.Points[0], stroke.Points[-1] epDistSqr = GeomUtils.pointDistanceSquared(ep1.X, ep1.Y, ep2.X, ep2.Y) if epDistSqr > (endPointDistPct * stkLen) ** 2: print "Endpoints aren't close enough to be a box" return overshoot = max(1, len(stroke.Points)/10) norm_stroke = GeomUtils.strokeSmooth(GeomUtils.strokeNormalizeSpacing(Stroke(stroke.Points + stroke.Points[0:overshoot]), numpoints = 70)) #D.strokeCurvatureHistogram(norm_stroke) curvatures = GeomUtils.strokeGetPointsCurvature(norm_stroke) corners = set([]) curvatures_cpy = list(curvatures) while len(corners) < 4: crnr_idx = curvatures_cpy.index(max(curvatures_cpy)) crnr = curvatures_cpy[crnr_idx] * 57.295 for nBor in range(crnr_idx -2, crnr_idx + 3): if nBor < len(curvatures_cpy) and nBor > 0: curvatures_cpy[nBor] = 0 if crnr > 0: #30 and crnr < 150: #Have a curvature, and we haven't already classified its immed neighbors as a corner corners.add(crnr_idx) else: break if len(corners) != 4: return else: c_list = [norm_stroke.Points[c_idx] for c_idx in sorted(list(corners))] cornerStroke = Stroke(c_list + c_list[:2]) boxStroke = GeomUtils.strokeNormalizeSpacing(Stroke(c_list + [c_list[0]])) origStroke = GeomUtils.strokeNormalizeSpacing(Stroke(stroke.Points + [stroke.Points[0]])) approxAcc = GeomUtils.strokeDTWDist(boxStroke, origStroke) print "Box approximates original with %s accuracy" % (approxAcc) if approxAcc < boxApproxThresh: self.getBoard().AnnotateStrokes([stroke], BoxAnnotation(c_list))
def onStrokeAdded( self, stroke ): "Watches for Strokes that look like an arrow to Annotate" smoothedStroke = GeomUtils.strokeSmooth(stroke) ep1 = stroke.Points[0] ep2 = stroke.Points[-1] #ep1 = smoothedStroke.Points[0] #ep2 = smoothedStroke.Points[-1] isArrowHead = False #GeomUtils.ellipseAxisRatio(stroke) #Match single-stroke arrows #DISABLED logger.debug("**Warning: Single-stroke arrows disabled**") tip, tail = None, None tip, tail = _isSingleStrokeArrow(smoothedStroke) #if tip is None or tail is None: #revpts = list(smoothedStroke.Points) #revpts.reverse() #tip, tail = _isSingleStrokeArrow(Stroke(revpts)) if tip is not None and tail is not None: isArrowHead = False anno = ArrowAnnotation( tip, tail, headstroke= stroke, tailstroke = stroke ) self.getBoard().AnnotateStrokes( [stroke], anno) #/DISABLED else: if _isArrowHead(smoothedStroke, self.arrowHeadMatcher): logger.debug(" ARROWHEAD") #head = smoothedStroke head = stroke isArrowHead = True # * (tip-point) # o o # o o # o o # o o #Get the endpoints/tip point as max curvature strokeNorm = GeomUtils.strokeNormalizeSpacing(smoothedStroke, numpoints = 7) curvatures = GeomUtils.strokeGetPointsCurvature(strokeNorm) ptIdx = curvatures.index(max(curvatures)) tip = strokeNorm.Points[ptIdx] #Middle is the point of max curvature #Match it to any tails we have matchedTails = self._matchHeadtoTail(head = stroke, point = tip) for headpoint, tail in matchedTails: #Orient the tail correctly if tail.Points[0] == headpoint: endpoint = tail.Points[-1] direction = 'head2tail' elif tail.Points[-1] == headpoint: endpoint = tail.Points[0] direction = 'tail2head' logger.debug("Stroke is head of arrow, drawn %s" % (direction)) anno = ArrowAnnotation(tip, endpoint, headstroke = stroke, tailstroke = tail, direction = direction) self.getBoard().AnnotateStrokes([head, tail],anno) #Match it like a tail even if we think it's an arrowhead. Oh ambiguity! matchedHeads = self._matchHeadtoTail(tail = stroke, point = ep1) tail = stroke for tip, head in matchedHeads: logger.debug("Stroke is tail of arrow, drawn head2tail") anno = ArrowAnnotation(tip, ep2, headstroke = head, tailstroke = tail, direction='head2tail') #Arrow is from the back endpoint to the tip of the arrowhead self.getBoard().AnnotateStrokes([head, tail],anno) matchedHeads = self._matchHeadtoTail(tail = stroke, point = ep2) for tip, head in matchedHeads: logger.debug("Stroke is tail of arrow, drawn tail2head") anno = ArrowAnnotation(tip, ep1, headstroke = head, tailstroke =tail, direction='tail2head') self.getBoard().AnnotateStrokes([head, tail],anno) #Add this stroke to the pool for future evaluation sNorm = GeomUtils.strokeNormalizeSpacing(stroke, numpoints = max(GeomUtils.strokeLength(stroke) / 3, 1)) self._endpoints.append( (ep1, stroke, sNorm) ) self._endpoints.append( (ep2, stroke, sNorm) ) if isArrowHead: self._arrowHeads.append( (tip, stroke, sNorm) )
def _scoreStrokesForLetter(strokelist, letter): """Get the confidence score for a group of strokes matching a letter, normalized [0.0-1.0]""" retConfidence = 0.0 #Recognition thresholds closedDistRatio = 0.22 circularityThresh_0 = 0.80 circularityThresh_1 = 0.20 maxStraightCurvature = 0.6 strokesBB = GeomUtils.strokelistBoundingBox(strokelist) if len(strokelist) == 0: return 0.0 #The case of a single point if strokesBB[0].X == strokesBB[1].X and strokesBB[0].Y == strokesBB[1].Y: return 0.0 #Recognize a zero if letter.upper() == "0": stroke = strokelist[0] strokeLen = max(GeomUtils.strokeLength(stroke), 1) normDist = max(3, strokeLen / 5) #granularity of point spacing -- at least 3 head, tail = stroke.Points[0], stroke.Points[-1] endDist = GeomUtils.pointDistance(head.X, head.Y, tail.X, tail.Y) #If the endpoints are 1/thresh apart, actually close the thing isClosedShape = GeomUtils.pointDistanceSquared(head.X, head.Y, tail.X, tail.Y) \ < ( (strokeLen * closedDistRatio) ** 2 ) if isClosedShape: #Close the shape back up s_norm = GeomUtils.strokeNormalizeSpacing( Stroke(stroke.Points + [stroke.Points[0]]) , normDist ) else: s_norm = GeomUtils.strokeNormalizeSpacing( stroke , normDist ) #curvatures = GeomUtils.strokeGetPointsCurvature(s_norm) circularity = GeomUtils.strokeCircularity( s_norm ) if isClosedShape: retConfidence += 0.5 if circularity > circularityThresh_0: retConfidence += 0.5 return retConfidence #Recognize a one elif letter.upper() == "1": stroke = strokelist[0] strokeLen = max(GeomUtils.strokeLength(stroke), 1) normDist = max(3, strokeLen / 5) #granularity of point spacing -- at least 3 s_norm = GeomUtils.strokeNormalizeSpacing( stroke , normDist ) circularity = GeomUtils.strokeCircularity( s_norm ) curvatures = GeomUtils.strokeGetPointsCurvature(s_norm) if max(curvatures) < maxStraightCurvature: retConfidence += 0.30 if circularity < circularityThresh_1: retConfidence += 0.5 if stroke.Points[0].X < stroke.Points[-1].X + strokeLen / 2.0 \ and stroke.Points[0].X > stroke.Points[-1].X - strokeLen / 2.0: retConfidence += 0.2 return retConfidence #Recognize a dash elif letter.upper() == "-": stroke = strokelist[0] strokeLen = max(GeomUtils.strokeLength(stroke), 1) normDist = max(3, strokeLen / 5) #granularity of point spacing -- at least 3 s_norm = GeomUtils.strokeNormalizeSpacing( stroke , normDist ) circularity = GeomUtils.strokeCircularity( s_norm ) curvatures = GeomUtils.strokeGetPointsCurvature(s_norm) if max(curvatures) < maxStraightCurvature: retConfidence += 0.30 if circularity < circularityThresh_1: retConfidence += 0.5 if stroke.Points[0].Y < stroke.Points[-1].Y + strokeLen / 2.0 \ and stroke.Points[0].Y > stroke.Points[-1].Y - strokeLen / 2.0: retConfidence += 0.2 return retConfidence else: return 0.0