Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

333
LINES

< > BotCompany Repo | #1035028 // G22SkeletonToMesh [backup]

JavaX fragment (include)

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  
}

Author comment

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: 65 / 73
Referenced in: [show references]