// G22SkeletonToMesh converts a skeleton (thinned image region) // to a mesh. // A "mesh" is a set of points ("anchors") connected through curves. // More docs at G22Mesh srecord noeq G22SkeletonToMesh_v2(IImageRegion region) is Steppable { settable bool debug; settable bool debugDroppingUnnecessaryAnchors; settable bool autoSimplify = true; // the generated mesh gettable G22Mesh mesh; // TEMPORARY DATA ItIt regionIterator; new L queue; new PtSet pointsSeen; delegate Anchor, Curve to G22Mesh. delegate getAnchor, addCurve, removeAnchor, removeCurve, mergeAnchorInto to mesh. // p is a point in 3x3 neighborhood of anchor srecord noeq WorkItem(Anchor anchor, Pt p) {} G22Mesh get() { if (mesh == null) run(); ret mesh; } void run { stepAll(this); } public bool step() { // first, initialize iterator & mesh if (mesh == null) { regionIterator = region.pixelIterator(); mesh = new G22Mesh; setMetaSrc(mesh, this); } // then check queue for work if (nempty(queue)) { WorkItem work = popLast(queue); Pt p = work.p; new PtBuffer curvePoints; curvePoints.add(work.anchor.pt); // add anchor to path Anchor endAnchor = null; if (debug) print("Exploring from " + work.anchor.pt + " / " + p); while ping (true) { if (debug) print("Curve point " + p); curvePoints.add(p); // did we hit another anchor? endAnchor = getAnchor(p); if (endAnchor != null) { if (debug) print("Hit anchor " + endAnchor); break; } // check if we visited this point already if (!pointsSeen.add(p)) { if (l(curvePoints) == 2) true; // It's a 1-pixel curve anyway, just forget it else // If this is an actual thing, we should make a proper anchor here (splitting existing curves). // I don't think it's actually a thing though warn("Surprising seen point (" + curvePoints + "). Adding as anchor"); break; } PtBuffer branches = unexploredPointsAround(p); // line ends or branches - same thing for us, it means our curve ends and we create a new anchor here if (l(branches) != 1) { if (debug) print("Branch count: " + l(branches)); break; } else p = first(branches); // curve continues } if (endAnchor == null) endAnchor = newAnchor(p); mesh.addCurve(new Curve(work.anchor, endAnchor, curvePoints)); true; } // finally look for next point in region if (regionIterator.hasNext()) { Pt p = regionIterator.next(); // check if point already seen, otherwise mark as seen if (!pointsSeen.add(p)) true; // new point - save as anchor newAnchor(p); true; } // last step, simplify if (autoSimplify) simplify(); false; } Anchor newAnchor(Pt p) { Anchor anchor = mesh.newAnchor(p); // explore in all directions for (Pt p2 : unexploredPointsAround(p)) queue.add(new WorkItem(anchor, p2)); ret anchor; } PtBuffer unexploredPointsAround(Pt p) { new PtBuffer out; for (int dir = 1; dir <= 8; dir++) { Pt p2 = onePathDirection(p, dir); if (region.contains(p2) && !pointsSeen.contains(p2)) out.add(p2); } ret out; } bool simplify() { ret 0 != stepAll(combineSteppables_dontDropEnded( l0 dropLengthOneCurves, l0 dropShortLoops, l0 dropUnnecessaryAnchors)); } // drops very short curves connecting a node to itself // returns true if anything changed bool dropShortLoops() { int n = l(mesh.curves()); for (Curve curve : cloneList(mesh.curves())) { if (curve.start != curve.end) continue; if (curve.length() > 3) continue; if (debug) print("Removing short loop: " + curve); removeCurve(curve); } ret l(mesh.curves()) < n; } // returns true if anything changed bool dropLengthOneCurves() { int n = l(mesh.anchors()); for (Curve curve : cloneList(mesh.curves())) { if (curve.length() != 1) continue; // Decide which anchor to keep bool keepStartAnchor = curve.start.arity() >= curve.end.arity(); // Forget curve removeCurve(curve); // Merge the less important (defined as: less connected) anchor into the other one if (keepStartAnchor) mergeAnchorInto(curve.end, curve.start); else mergeAnchorInto(curve.start, curve.end); } ret l(mesh.anchors()) < n; } // returns true if anything changed bool dropUnnecessaryAnchors() { int n = l(mesh.anchors()); for (Anchor anchor : cloneList(mesh.anchors())) { if (anchor.arity() != 2) continue; if (anchor.isRingAnchor()) continue; // It's a non-branching anchor sitting on a curve - we can drop it. if (debugDroppingUnnecessaryAnchors) print("Dropping " + anchor); // This list will have length 2 L curves = concatLists(anchor.incomingCurves, anchor.outgoingCurves); if (debugDroppingUnnecessaryAnchors) printVars(+curves); // Collect points for merged curve // Process curve 1, optionally reverse it so it does NOT start at the middle anchor Curve curve1 = first(curves); removeCurve(curve1); L points1 = curve1.path.pointList(); if (eq(first(points1), anchor.pt)) reverseInPlace(points1); // Process curve 2, optionally reverse it so it DOES start at the middle anchor Curve curve2 = second(curves); removeCurve(curve2); L points2 = curve2.path.pointList(); if (!eq(first(points2), anchor.pt)) reverseInPlace(points2); // Drop overlapping point, append points2 to points1 assertEquals(anchor.pt, popLast(points1)); L points = points1; addAll(points, points2); Anchor anchor1 = assertNotNull(getAnchor(first(points))); Anchor anchor2 = assertNotNull(getAnchor(last(points))); if (debugDroppingUnnecessaryAnchors) printVarsInMultipleLines(+anchor, +anchor1, +anchor2); removeAnchor(anchor); addCurve(new Curve(anchor1, anchor2, points)); } ret l(mesh.anchors()) < n; } }