Transpiled version (8195L) is out of date.
1 | // G22SkeletonToMesh converts a skeleton (thinned image region) |
2 | // to a mesh. |
3 | |
4 | // A "mesh" is a set of points ("anchors") connected through curves. |
5 | // More docs at G22Mesh |
6 | |
7 | srecord noeq G22SkeletonToMesh<Img extends WidthAndHeight>(IImageRegion<Img> region) extends G22Mesh is Steppable { |
8 | settable bool debug; |
9 | settable bool debugDroppingUnnecessaryAnchors; |
10 | settable bool autoSimplify = true; |
11 | |
12 | // TEMPORARY DATA |
13 | |
14 | ItIt<Pt> regionIterator; |
15 | new L<WorkItem> queue; |
16 | new PtSet pointsSeen; |
17 | |
18 | // p is a point in 3x3 neighborhood of anchor |
19 | srecord noeq WorkItem(Anchor anchor, Pt p) {} |
20 | |
21 | public bool step() { |
22 | // first, initialize iterator |
23 | if (regionIterator == null) |
24 | regionIterator = region.pixelIterator(); |
25 | |
26 | // then check queue for work |
27 | if (nempty(queue)) { |
28 | WorkItem work = popLast(queue); |
29 | |
30 | Pt p = work.p; |
31 | |
32 | new PtBuffer curvePoints; |
33 | curvePoints.add(work.anchor.pt); // add anchor to path |
34 | Anchor endAnchor = null; |
35 | |
36 | if (debug) print("Exploring from " + work.anchor.pt + " / " + p); |
37 | |
38 | while ping (true) { |
39 | if (debug) print("Curve point " + p); |
40 | curvePoints.add(p); |
41 | |
42 | // did we hit another anchor? |
43 | endAnchor = anchorMap.get(p); |
44 | if (endAnchor != null) { |
45 | if (debug) print("Hit anchor " + endAnchor); |
46 | break; |
47 | } |
48 | |
49 | // check if we visited this point already |
50 | if (!pointsSeen.add(p)) { |
51 | if (l(curvePoints) == 2) |
52 | true; // It's a 1-pixel curve anyway, just forget it |
53 | else |
54 | // If this is an actual thing, we should make a proper anchor here (splitting existing curves). |
55 | // I don't think it's actually a thing though |
56 | warn("Surprising seen point (" + curvePoints + "). Adding as anchor"); |
57 | break; |
58 | } |
59 | |
60 | PtBuffer branches = unexploredPointsAround(p); |
61 | |
62 | // line ends or branches - same thing for us, it means our curve ends and we create a new anchor here |
63 | if (l(branches) != 1) { |
64 | if (debug) print("Branch count: " + l(branches)); |
65 | break; |
66 | } else |
67 | p = first(branches); // curve continues |
68 | } |
69 | |
70 | if (endAnchor == null) |
71 | endAnchor = newAnchor(p); |
72 | |
73 | addCurve(new Curve(work.anchor, endAnchor, curvePoints)); |
74 | true; |
75 | } |
76 | |
77 | // finally look for next point in region |
78 | if (regionIterator.hasNext()) { |
79 | Pt p = regionIterator.next(); |
80 | |
81 | // check if point already seen, otherwise mark as seen |
82 | if (!pointsSeen.add(p)) true; |
83 | |
84 | // new point - save as anchor |
85 | newAnchor(p); |
86 | true; |
87 | } |
88 | |
89 | // last step, simplify |
90 | |
91 | if (autoSimplify) simplify(); |
92 | false; |
93 | } |
94 | |
95 | Anchor newAnchor(Pt p) { |
96 | Anchor anchor = new(l(anchorMap)+1, p); |
97 | anchorMap.put(p, anchor); |
98 | //anchors.add(anchor); |
99 | |
100 | // explore in all directions |
101 | for (Pt p2 : unexploredPointsAround(p)) |
102 | queue.add(new WorkItem(anchor, p2)); |
103 | |
104 | ret anchor; |
105 | } |
106 | |
107 | PtBuffer unexploredPointsAround(Pt p) { |
108 | new PtBuffer out; |
109 | for (int dir = 1; dir <= 8; dir++) { |
110 | Pt p2 = onePathDirection(p, dir); |
111 | if (region.contains(p2) && !pointsSeen.contains(p2)) |
112 | out.add(p2); |
113 | } |
114 | ret out; |
115 | } |
116 | |
117 | bool simplify() { |
118 | ret 0 != stepAll(combineSteppables_dontDropEnded( |
119 | l0 dropLengthOneCurves, |
120 | l0 dropShortLoops, |
121 | l0 dropUnnecessaryAnchors)); |
122 | } |
123 | |
124 | // drops very short curves connecting a node to itself |
125 | // returns true if anything changed |
126 | bool dropShortLoops() { |
127 | int n = l(curves); |
128 | for (Curve curve : cloneList(curves)) { |
129 | if (curve.start != curve.end) continue; |
130 | if (curve.length() > 3) continue; |
131 | |
132 | if (debug) print("Removing short loop: " + curve); |
133 | removeCurve(curve); |
134 | } |
135 | |
136 | ret l(curves) < n; |
137 | } |
138 | |
139 | // returns true if anything changed |
140 | bool dropLengthOneCurves() { |
141 | int n = l(anchorMap); |
142 | for (Curve curve : cloneList(curves)) { |
143 | if (curve.length() != 1) continue; |
144 | |
145 | // Decide which anchor to keep |
146 | bool keepStartAnchor = curve.start.arity() >= curve.end.arity(); |
147 | |
148 | // Forget curve |
149 | removeCurve(curve); |
150 | |
151 | // Merge the less important (defined as: less connected) anchor into the other one |
152 | if (keepStartAnchor) |
153 | mergeAnchor(curve.end, curve.start); |
154 | else |
155 | mergeAnchor(curve.start, curve.end); |
156 | } |
157 | ret l(anchorMap) < n; |
158 | } |
159 | |
160 | void removeCurve(Curve curve) { |
161 | curves.remove(curve); |
162 | curve.start.outgoingCurves.remove(curve); |
163 | curve.end.incomingCurves.remove(curve); |
164 | } |
165 | |
166 | // moves a1's connections to a2, deletes a1 |
167 | void mergeAnchor(Anchor a1, Anchor a2) { |
168 | if (debug) printVarsInMultipleLines("mergeAnchor", +a1, +a2); |
169 | |
170 | for (Curve curve : a1.outgoingCurves) { |
171 | // first step is now going from a2 to a1 |
172 | curve.path.insertStep(0, ptMinus(a1.pt, a2.pt)); |
173 | curve.path.origin(a2.pt); |
174 | curve.start = a2; |
175 | a2.outgoingCurves.add(curve); |
176 | } |
177 | |
178 | for (Curve curve : a1.incomingCurves) { |
179 | // last step is now going from a1 to a2 |
180 | curve.path.addStep(ptMinus(a2.pt, a1.pt)); |
181 | curve.end = a2; |
182 | a2.incomingCurves.add(curve); |
183 | } |
184 | |
185 | anchorMap.remove(a1.pt); |
186 | } |
187 | |
188 | // returns true if anything changed |
189 | bool dropUnnecessaryAnchors() { |
190 | int n = l(anchorMap); |
191 | for (Anchor anchor : cloneValues(anchorMap)) { |
192 | if (anchor.arity() != 2) continue; |
193 | if (anchor.isRingAnchor()) continue; |
194 | |
195 | // It's a non-branching anchor sitting on a curve - we can drop it. |
196 | if (debugDroppingUnnecessaryAnchors) print("Dropping " + anchor); |
197 | |
198 | // This list will have length 2 |
199 | L<Curve> curves = concatLists(anchor.incomingCurves, anchor.outgoingCurves); |
200 | |
201 | // Find the endpoints of the new curve (either 1 or 2 anchors) |
202 | Set<Anchor> anchors = concatMapToSet(curves, c -> c.anchors()); |
203 | anchors.remove(this); |
204 | |
205 | if (debugDroppingUnnecessaryAnchors) printVarsInMultipleLines(+curves, +anchors); |
206 | |
207 | // Collect points for merged curve |
208 | |
209 | // Process curve 1, optionally reverse it so it does NOT start at the middle anchor |
210 | Curve curve1 = first(curves); |
211 | removeCurve(curve1); |
212 | L<Pt> points1 = curve1.path.pointList(); |
213 | if (eq(first(points1), anchor.pt)) reverseInPlace(points1); |
214 | |
215 | // Process curve 2, optionally reverse it so it DOES start at the middle anchor |
216 | Curve curve2 = second(curves); |
217 | removeCurve(curve2); |
218 | L<Pt> points2 = curve2.path.pointList(); |
219 | if (!eq(first(points2), anchor.pt)) reverseInPlace(points2); |
220 | |
221 | // Drop overlapping point, append points2 to points1 |
222 | assertEquals(anchor.pt, popLast(points1)); |
223 | L<Pt> points = points1; |
224 | addAll(points, points2); |
225 | |
226 | Anchor anchor1 = assertNotNull(anchorMap.get(first(points))); |
227 | Anchor anchor2 = assertNotNull(anchorMap.get(last(points))); |
228 | |
229 | if (debugDroppingUnnecessaryAnchors) printVarsInMultipleLines(+anchor, +anchor1, +anchor2); |
230 | |
231 | anchorMap.remove(anchor.pt); |
232 | addCurve(new Curve(anchor1, anchor2, points)); |
233 | } |
234 | |
235 | ret l(anchorMap) < n; |
236 | } |
237 | |
238 | void addCurve(Curve curve) { |
239 | curves.add(curve); |
240 | } |
241 | } |
Began life as a copy of #1034998
download show line numbers debug dex old transpilations
Travelled to 4 computer(s): bhatertpkbcr, ekrmjmnbrukm, mowyntqkapby, mqqgnosmbjvj
No comments. add comment
Snippet ID: | #1035010 |
Snippet name: | G22SkeletonToMesh [old, superceded by G22SkeletonToMesh_v2] |
Eternal ID of this version: | #1035010/63 |
Text MD5: | e2d240043f868cc84b1c0f11d4dcafed |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-05-07 02:49:08 |
Source code size: | 7547 bytes / 241 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 259 / 626 |
Version history: | 62 change(s) |
Referenced in: | [show references] |