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