1 | // A "mesh" is a set of points ("anchors") connected through curves. |
2 | // An anchor is either |
3 | // -the end of a curve |
4 | // -an intersection of curves |
5 | // -or a randomly chosen point along a curve (type 3) |
6 | |
7 | // A type 3 anchor can be necessary to define an "o" shape |
8 | // (because such a shape doesn't have an anchor point per se, |
9 | // so we just choose one point along the ring). |
10 | |
11 | // Sometimes however, a type 3 anchor is an artifact of the mesh |
12 | // finding process and can be eliminated, reducing the anchor and |
13 | // curve count by 1. |
14 | |
15 | srecord noeq G22SkeletonToMesh<Img extends WidthAndHeight>(IImageRegion<Img> region) is Steppable { |
16 | settable bool debug; |
17 | settable bool debugDroppingUnnecessaryAnchors; |
18 | settable bool autoSimplify = true; |
19 | |
20 | new LinkedHashMap<Pt, Anchor> anchorMap; // sorted by index |
21 | //gettable new L<Anchor> anchors; |
22 | new Set<Curve> curves; // in no particular order |
23 | |
24 | sclass Curve { |
25 | Anchor start, end; |
26 | OnePathWithOrigin path; // origin = start.pt |
27 | |
28 | *(Anchor *start, Anchor *end, L<Pt> points) { |
29 | path = new OnePathWithOrigin(points, false); |
30 | start.outgoingCurves.add(this); |
31 | end.incomingCurves.add(this); |
32 | } |
33 | |
34 | toString { |
35 | ret "Curve of length " + n2(path.nSteps()) |
36 | + " connecting " + start?.shortToString() + " and " + end?.shortToString(); |
37 | } |
38 | |
39 | // This counts one of the anchors, but not the other. |
40 | // So if the anchors are right next to each other, |
41 | // the curve's length is 1. |
42 | int length() { ret path.nSteps(); } |
43 | |
44 | L<Anchor> anchors() { ret ll(start, end); } |
45 | } |
46 | |
47 | sclass Anchor { |
48 | int index; // every anchor gets a number starting from one |
49 | Pt pt; |
50 | new L<Curve> outgoingCurves; |
51 | new L<Curve> incomingCurves; |
52 | |
53 | *(int *index, Pt *pt) {} |
54 | |
55 | int arity() { ret l(outgoingCurves) + l(incomingCurves); } |
56 | |
57 | S shortToString() { |
58 | ret "Anchor " + index + " at " + pt; |
59 | } |
60 | |
61 | toString { |
62 | L<Int> connections = sorted(map(connectedToAnchors(), a -> a.index)); |
63 | ret shortToString() + ", arity " + arity() |
64 | + stringIf(isRingAnchor(), " [arbitrarily chosen ring anchor]") |
65 | + (empty(connections) ? "" : ", connected to " + joinWithComma(connections)); |
66 | } |
67 | |
68 | Set<Anchor> connectedToAnchors() { |
69 | ret joinSets( |
70 | map(outgoingCurves, c -> c.end), |
71 | map(incomingCurves, c -> c.start)); |
72 | } |
73 | |
74 | // The ring case where we just randomly select an anchor |
75 | bool isRingAnchor() { |
76 | ret l(outgoingCurves) == 1 && l(incomingCurves) == 1 |
77 | && first(outgoingCurves) == first(incomingCurves); |
78 | } |
79 | } |
80 | |
81 | // TEMPORARY DATA |
82 | |
83 | ItIt<Pt> regionIterator; |
84 | new L<WorkItem> queue; |
85 | new PtSet pointsSeen; |
86 | |
87 | // p is a point in 3x3 neighborhood of anchor |
88 | srecord noeq WorkItem(Anchor anchor, Pt p) {} |
89 | |
90 | public bool step() { |
91 | // first, initialize iterator |
92 | if (regionIterator == null) |
93 | regionIterator = region.pixelIterator(); |
94 | |
95 | // then check queue for work |
96 | if (nempty(queue)) { |
97 | WorkItem work = popLast(queue); |
98 | |
99 | Pt p = work.p; |
100 | |
101 | new PtBuffer curvePoints; |
102 | curvePoints.add(work.anchor.pt); // add anchor to path |
103 | Anchor endAnchor = null; |
104 | |
105 | if (debug) print("Exploring from " + work.anchor.pt + " / " + p); |
106 | |
107 | while ping (true) { |
108 | if (debug) print("Curve point " + p); |
109 | curvePoints.add(p); |
110 | |
111 | // did we hit another anchor? |
112 | endAnchor = anchorMap.get(p); |
113 | if (endAnchor != null) { |
114 | if (debug) print("Hit anchor " + endAnchor); |
115 | break; |
116 | } |
117 | |
118 | // check if we visited this point already |
119 | if (!pointsSeen.add(p)) { |
120 | if (l(curvePoints) == 2) |
121 | true; // It's a 1-pixel curve anyway, just forget it |
122 | else |
123 | // If this is an actual thing, we should make a proper anchor here (splitting existing curves). |
124 | // I don't think it's actually a thing though |
125 | warn("Surprising seen point (" + curvePoints + "). Adding as anchor"); |
126 | break; |
127 | } |
128 | |
129 | PtBuffer branches = unexploredPointsAround(p); |
130 | |
131 | // line ends or branches - same thing for us, it means our curve ends and we create a new anchor here |
132 | if (l(branches) != 1) { |
133 | if (debug) print("Branch count: " + l(branches)); |
134 | break; |
135 | } else |
136 | p = first(branches); // curve continues |
137 | } |
138 | |
139 | if (endAnchor == null) |
140 | endAnchor = newAnchor(p); |
141 | |
142 | addCurve(new Curve(work.anchor, endAnchor, curvePoints)); |
143 | true; |
144 | } |
145 | |
146 | // finally look for next point in region |
147 | if (regionIterator.hasNext()) { |
148 | Pt p = regionIterator.next(); |
149 | |
150 | // check if point already seen, otherwise mark as seen |
151 | if (!pointsSeen.add(p)) true; |
152 | |
153 | // new point - save as anchor |
154 | newAnchor(p); |
155 | true; |
156 | } |
157 | |
158 | // last step, simplify |
159 | |
160 | if (autoSimplify) simplify(); |
161 | false; |
162 | } |
163 | |
164 | Anchor newAnchor(Pt p) { |
165 | Anchor anchor = new(l(anchorMap)+1, p); |
166 | anchorMap.put(p, anchor); |
167 | //anchors.add(anchor); |
168 | |
169 | // explore in all directions |
170 | for (Pt p2 : unexploredPointsAround(p)) |
171 | queue.add(new WorkItem(anchor, p2)); |
172 | |
173 | ret anchor; |
174 | } |
175 | |
176 | PtBuffer unexploredPointsAround(Pt p) { |
177 | new PtBuffer out; |
178 | for (int dir = 1; dir <= 8; dir++) { |
179 | Pt p2 = onePathDirection(p, dir); |
180 | if (region.contains(p2) && !pointsSeen.contains(p2)) |
181 | out.add(p2); |
182 | } |
183 | ret out; |
184 | } |
185 | |
186 | Cl<Pt> anchorPts() { ret keys(anchorMap); } |
187 | Cl<Anchor> anchors() { ret values(anchorMap); } |
188 | |
189 | bool simplify() { |
190 | ret 0 != stepAll(combineSteppables_dontDropEnded( |
191 | l0 dropLengthOneCurves, |
192 | l0 dropShortLoops, |
193 | l0 dropUnnecessaryAnchors)); |
194 | } |
195 | |
196 | // drops very short curves connecting a node to itself |
197 | // returns true if anything changed |
198 | bool dropShortLoops() { |
199 | int n = l(curves); |
200 | for (Curve curve : cloneList(curves)) { |
201 | if (curve.start != curve.end) continue; |
202 | if (curve.length() > 3) continue; |
203 | |
204 | if (debug) print("Removing short loop: " + curve); |
205 | removeCurve(curve); |
206 | } |
207 | |
208 | ret l(curves) < n; |
209 | } |
210 | |
211 | // returns true if anything changed |
212 | bool dropLengthOneCurves() { |
213 | int n = l(anchorMap); |
214 | for (Curve curve : cloneList(curves)) { |
215 | if (curve.length() != 1) continue; |
216 | |
217 | // Decide which anchor to keep |
218 | bool keepStartAnchor = curve.start.arity() >= curve.end.arity(); |
219 | |
220 | // Forget curve |
221 | removeCurve(curve); |
222 | |
223 | // Merge the less important (defined as: less connected) anchor into the other one |
224 | if (keepStartAnchor) |
225 | mergeAnchor(curve.end, curve.start); |
226 | else |
227 | mergeAnchor(curve.start, curve.end); |
228 | } |
229 | ret l(anchorMap) < n; |
230 | } |
231 | |
232 | void removeCurve(Curve curve) { |
233 | curves.remove(curve); |
234 | curve.start.outgoingCurves.remove(curve); |
235 | curve.end.incomingCurves.remove(curve); |
236 | } |
237 | |
238 | // moves a1's connections to a2, deletes a1 |
239 | void mergeAnchor(Anchor a1, Anchor a2) { |
240 | if (debug) printVarsInMultipleLines("mergeAnchor", +a1, +a2); |
241 | |
242 | for (Curve curve : a1.outgoingCurves) { |
243 | // first step is now going from a2 to a1 |
244 | curve.path.insertStep(0, ptMinus(a1.pt, a2.pt)); |
245 | curve.path.origin(a2.pt); |
246 | curve.start = a2; |
247 | a2.outgoingCurves.add(curve); |
248 | } |
249 | |
250 | for (Curve curve : a1.incomingCurves) { |
251 | // last step is now going from a1 to a2 |
252 | curve.path.addStep(ptMinus(a2.pt, a1.pt)); |
253 | curve.end = a2; |
254 | a2.incomingCurves.add(curve); |
255 | } |
256 | |
257 | anchorMap.remove(a1.pt); |
258 | } |
259 | |
260 | // returns true if anything changed |
261 | bool dropUnnecessaryAnchors() { |
262 | int n = l(anchorMap); |
263 | for (Anchor anchor : cloneValues(anchorMap)) { |
264 | if (anchor.arity() != 2) continue; |
265 | if (anchor.isRingAnchor()) continue; |
266 | |
267 | // It's a non-branching anchor sitting on a curve - we can drop it. |
268 | if (debugDroppingUnnecessaryAnchors) print("Dropping " + anchor); |
269 | |
270 | // This list will have length 2 |
271 | L<Curve> curves = concatLists(anchor.incomingCurves, anchor.outgoingCurves); |
272 | |
273 | // Find the endpoints of the new curve (either 1 or 2 anchors) |
274 | Set<Anchor> anchors = concatMapToSet(curves, c -> c.anchors()); |
275 | anchors.remove(this); |
276 | |
277 | if (debugDroppingUnnecessaryAnchors) printVarsInMultipleLines(+curves, +anchors); |
278 | |
279 | // Collect points for merged curve |
280 | |
281 | // Process curve 1, optionally reverse it so it does NOT start at the middle anchor |
282 | Curve curve1 = first(curves); |
283 | removeCurve(curve1); |
284 | L<Pt> points1 = curve1.path.pointList(); |
285 | if (eq(first(points1), anchor.pt)) reverseInPlace(points1); |
286 | |
287 | // Process curve 2, optionally reverse it so it DOES start at the middle anchor |
288 | Curve curve2 = second(curves); |
289 | removeCurve(curve2); |
290 | L<Pt> points2 = curve2.path.pointList(); |
291 | if (!eq(first(points2), anchor.pt)) reverseInPlace(points2); |
292 | |
293 | // Drop overlapping point, append points2 to points1 |
294 | assertEquals(anchor.pt, popLast(points1)); |
295 | L<Pt> points = points1; |
296 | addAll(points, points2); |
297 | |
298 | Anchor anchor1 = assertNotNull(anchorMap.get(first(points))); |
299 | Anchor anchor2 = assertNotNull(anchorMap.get(last(points))); |
300 | |
301 | if (debugDroppingUnnecessaryAnchors) printVarsInMultipleLines(+anchor, +anchor1, +anchor2); |
302 | |
303 | anchorMap.remove(anchor.pt); |
304 | addCurve(new Curve(anchor1, anchor2, points)); |
305 | } |
306 | |
307 | ret l(anchorMap) < n; |
308 | } |
309 | |
310 | void addCurve(Curve curve) { |
311 | curves.add(curve); |
312 | } |
313 | |
314 | // Internal structure sanity check |
315 | void checkArities { |
316 | new MultiMap<Anchor, Curve> outgoing; |
317 | new MultiMap<Anchor, Curve> incoming; |
318 | |
319 | for (Curve curve : curves) { |
320 | outgoing.add(curve.start, curve); |
321 | if (!anchorMap.containsKey(curve.start.pt)) |
322 | fail("Start of curve not found: " + curve); |
323 | incoming.add(curve.end, curve); |
324 | if (!anchorMap.containsKey(curve.end.pt)) |
325 | fail("End of curve not found: " + curve); |
326 | } |
327 | |
328 | for (Anchor anchor : anchors()) { |
329 | assertSetEquals(anchor + " outgoing", outgoing.get(anchor), anchor.outgoingCurves); |
330 | assertSetEquals(anchor + " incoming", incoming.get(anchor), anchor.incomingCurves); |
331 | } |
332 | } |
333 | } |
Began life as a copy of #1035010
download show line numbers debug dex old transpilations
Travelled to 3 computer(s): bhatertpkbcr, mowyntqkapby, mqqgnosmbjvj
No comments. add comment
Snippet ID: | #1035028 |
Snippet name: | G22SkeletonToMesh [backup] |
Eternal ID of this version: | #1035028/1 |
Text MD5: | 4d2c45c0938ec2321bc9c57a87c7095b |
Author: | stefan |
Category: | javax |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-03-24 11:50:40 |
Source code size: | 10635 bytes / 333 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 171 / 183 |
Referenced in: | [show references] |