Skip to content

Commit eb22c65

Browse files
committed
allow for simplify and complicate
1 parent 62644cc commit eb22c65

File tree

3 files changed

+291
-2
lines changed

3 files changed

+291
-2
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*-
2+
* #%L
3+
* Library to call models of the family of SAM (Segment Anything Model) from Java
4+
* %%
5+
* Copyright (C) 2024 SAMJ developers.
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package ai.nets.samj.annotation;
21+
22+
import java.awt.geom.Point2D;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
public class DouglasPeucker {
27+
/**
28+
* Simplify a polyline using the Ramer–Douglas–Peucker algorithm.
29+
*
30+
* @param points Original list of points (ordered, endpoints included).
31+
* @param epsilon Max allowed perpendicular distance (in same units as your coords).
32+
* @return A new List<Point2D> containing only the “significant” vertices.
33+
*/
34+
public static List<Point2D> simplify(List<Point2D> points, double epsilon) {
35+
if (points == null || points.size() < 3) {
36+
// Nothing to simplify if fewer than 3 points
37+
return new ArrayList<>(points);
38+
}
39+
// Find the point with the maximum distance from the line between endpoints
40+
double maxDist = 0;
41+
int index = 0;
42+
Point2D start = points.get(0);
43+
Point2D end = points.get(points.size() - 1);
44+
for (int i = 1; i < points.size() - 1; i++) {
45+
double dist = perpendicularDistance(points.get(i), start, end);
46+
if (dist > maxDist) {
47+
maxDist = dist;
48+
index = i;
49+
}
50+
}
51+
52+
// If max distance is greater than epsilon, recursively simplify
53+
if (maxDist > epsilon) {
54+
// Recursive calls
55+
List<Point2D> left = simplify(points.subList(0, index + 1), epsilon);
56+
List<Point2D> right = simplify(points.subList(index, points.size()), epsilon);
57+
58+
// Combine, removing duplicate at the join
59+
List<Point2D> result = new ArrayList<>(left);
60+
result.remove(result.size() - 1);
61+
result.addAll(right);
62+
return result;
63+
} else {
64+
// Otherwise, just return the two endpoints
65+
List<Point2D> result = new ArrayList<>();
66+
result.add(start);
67+
result.add(end);
68+
return result;
69+
}
70+
}
71+
72+
/**
73+
* Compute perpendicular distance from point p to the line through p0–p1.
74+
*/
75+
private static double perpendicularDistance(Point2D p, Point2D p0, Point2D p1) {
76+
double dx = p1.getX() - p0.getX();
77+
double dy = p1.getY() - p0.getY();
78+
// Normalize (to avoid dividing by zero)
79+
double mag = Math.hypot(dx, dy);
80+
if (mag == 0) {
81+
// p0 and p1 are the same point
82+
return p.distance(p0);
83+
}
84+
// Area of parallelogram / base length = height
85+
double cross = Math.abs(dy * (p.getX() - p0.getX())
86+
- dx * (p.getY() - p0.getY()));
87+
return cross / mag;
88+
}
89+
90+
// Example usage
91+
public static void main(String[] args) {
92+
List<Point2D> contour = new ArrayList<>();
93+
// ... populate contour with one Point2D per pixel along your ROI boundary ...
94+
95+
double epsilon = 2.0; // e.g., remove detail smaller than 2 pixels
96+
List<Point2D> simplified = simplify(contour, epsilon);
97+
98+
System.out.println("Original vertex count: " + contour.size());
99+
System.out.println("Simplified vertex count: " + simplified.size());
100+
}
101+
}

src/main/java/ai/nets/samj/annotation/Mask.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
package ai.nets.samj.annotation;
2121

2222
import java.awt.Polygon;
23+
import java.awt.geom.Point2D;
24+
import java.util.ArrayList;
2325
import java.util.Arrays;
26+
import java.util.HashMap;
2427
import java.util.List;
2528
import java.util.Objects;
2629
import java.util.UUID;
@@ -37,24 +40,62 @@
3740
*/
3841
public class Mask {
3942

40-
private final Polygon contour;
43+
private Polygon contour;
4144

4245
private String name;
4346

4447
private final String uuid = UUID.randomUUID().toString();
4548

46-
// TODO private final long[] rleEncoding;
4749
public long[] rleEncoding;
4850

51+
// TODO does it make sense to measure the level of simplification once a polygon is give?
52+
// TODO at the moment we assume it is always one vertix per pixel
53+
private double simplification = 0;
54+
55+
private HashMap<Double, Polygon> memory = new HashMap<Double, Polygon>();
56+
57+
private boolean rleValid = true;
58+
59+
private static final double COMPLEXITY_DELTA = 0.5;
60+
4961
private Mask(Polygon contour, long[] rleEncoding) {
5062
this.contour = contour;
5163
this.rleEncoding = rleEncoding;
64+
memory.put(simplification, contour);
5265
}
5366

5467
public static Mask build(Polygon contour, long[] rleEncoding) {
5568
return new Mask(contour, rleEncoding);
5669
}
5770

71+
public void simplify() {
72+
if (this.memory.get((this.simplification + COMPLEXITY_DELTA)) != null) {
73+
simplification += COMPLEXITY_DELTA;
74+
this.rleValid = simplification == 0;
75+
return;
76+
}
77+
List<Point2D> points = new ArrayList<Point2D>();
78+
for (int i = 0; i < getContour().npoints; i ++) {
79+
points.add(new Point2D.Double(getContour().xpoints[i], getContour().ypoints[i]));
80+
}
81+
List<Point2D> simple = DouglasPeucker.simplify(points, (this.simplification + COMPLEXITY_DELTA));
82+
simplification += COMPLEXITY_DELTA;
83+
this.rleValid = simplification == 0;
84+
85+
int[] xArr = simple.stream().mapToInt(pp -> (int) pp.getX()).toArray();
86+
int[] yArr = simple.stream().mapToInt(pp -> (int) pp.getY()).toArray();
87+
this.contour = new Polygon(xArr, yArr, xArr.length);
88+
memory.put(simplification, contour);
89+
}
90+
91+
public void complicate() {
92+
if (this.memory.get((this.simplification - COMPLEXITY_DELTA)) != null) {
93+
simplification -= COMPLEXITY_DELTA;
94+
this.rleValid = simplification == 0;
95+
return;
96+
}
97+
}
98+
5899
public String getName() {
59100
return name;
60101
}
@@ -73,6 +114,10 @@ public Polygon getContour() {
73114
}
74115

75116
public long[] getRLEMask() {
117+
if (this.rleValid)
118+
return this.rleEncoding;
119+
// TODO implement
120+
List<Integer> rle = PolygonToRLE.contourToRLE(null, 200, 200);
76121
return this.rleEncoding;
77122
}
78123

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*-
2+
* #%L
3+
* Library to call models of the family of SAM (Segment Anything Model) from Java
4+
* %%
5+
* Copyright (C) 2024 SAMJ developers.
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package ai.nets.samj.annotation;
21+
22+
import java.awt.geom.Point2D;
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.Comparator;
26+
import java.util.List;
27+
28+
public class PolygonToRLE {
29+
static class Edge {
30+
double x0, y0, x1, y1;
31+
double ymin, ymax;
32+
// slope‐inverse: dx/dy
33+
double dxdy;
34+
public Edge(Point2D a, Point2D b) {
35+
// ensure y0 <= y1
36+
if (a.getY() <= b.getY()) {
37+
x0 = a.getX(); y0 = a.getY();
38+
x1 = b.getX(); y1 = b.getY();
39+
} else {
40+
x0 = b.getX(); y0 = b.getY();
41+
x1 = a.getX(); y1 = a.getY();
42+
}
43+
ymin = y0; ymax = y1;
44+
dxdy = (x1 - x0) / (y1 - y0);
45+
}
46+
/** x‐intersection at scanline y+0.5 */
47+
public double intersectX(double scanY) {
48+
return x0 + dxdy * (scanY - y0);
49+
}
50+
}
51+
52+
/**
53+
* @param contour Must be closed (first==last) or will be treated as such.
54+
* @param width Image width in pixels.
55+
* @param height Image height in pixels.
56+
* @return COCO‐style RLE: alternating background/foreground run lengths.
57+
*/
58+
public static List<Integer> contourToRLE(
59+
List<Point2D> contour, int width, int height) {
60+
// 1) Build edge table
61+
List<Edge> edges = new ArrayList<>();
62+
for (int i = 0; i < contour.size() - 1; i++) {
63+
Point2D a = contour.get(i), b = contour.get(i+1);
64+
if (!a.equals(b) && a.getY() != b.getY()) {
65+
edges.add(new Edge(a, b));
66+
}
67+
}
68+
// Sort edges by ymin for activation
69+
edges.sort(Comparator.comparingDouble(e -> e.ymin));
70+
71+
List<Integer> runs = new ArrayList<>();
72+
int currentVal = 0, currentRun = 0;
73+
// Active edges for this scan‐line
74+
List<Edge> active = new ArrayList<>();
75+
int ei = 0; // pointer into edges
76+
77+
// 2) For each scan‐line
78+
for (int y = 0; y < height; y++) {
79+
double scanY = y + 0.5;
80+
81+
// Activate edges whose ymin <= scanY < ymax
82+
while (ei < edges.size() && edges.get(ei).ymin <= scanY) {
83+
if (edges.get(ei).ymax > scanY) {
84+
active.add(edges.get(ei));
85+
}
86+
ei++;
87+
}
88+
// Remove edges that end at or below scanY
89+
active.removeIf(e -> e.ymax <= scanY);
90+
91+
// 3) Compute intersections
92+
List<Double> xs = new ArrayList<>(active.size());
93+
for (Edge e : active) {
94+
xs.add(e.intersectX(scanY));
95+
}
96+
Collections.sort(xs);
97+
98+
// 4) Walk spans and accumulate RLE
99+
int prevX = 0;
100+
for (int i = 0; i+1 < xs.size(); i += 2) {
101+
int x0 = (int)Math.ceil(xs.get(i));
102+
int x1 = (int)Math.floor(xs.get(i+1));
103+
if (x1 < x0) continue;
104+
// background run [prevX, x0)
105+
currentRun = appendRun(runs, currentVal, currentRun, x0 - prevX);
106+
currentVal = 1 - currentVal;
107+
// foreground run [x0, x1+1)
108+
currentRun = appendRun(runs, currentVal, currentRun, (x1+1) - x0);
109+
currentVal = 1 - currentVal;
110+
prevX = x1 + 1;
111+
}
112+
// final background run to end of row
113+
currentRun = appendRun(runs, currentVal, currentRun, width - prevX);
114+
// ensure we’re back to background for next row
115+
if (currentVal != 0) {
116+
currentVal = 0;
117+
if (currentRun > 0) {
118+
runs.add(currentRun);
119+
currentRun = 0;
120+
}
121+
}
122+
}
123+
// 5) Flush last run
124+
if (currentRun > 0) {
125+
runs.add(currentRun);
126+
}
127+
return runs;
128+
}
129+
130+
/** Helper: either extend or flush & start new run. */
131+
private static int appendRun(List<Integer> runs, int currVal, int currRun, int length) {
132+
if (length <= 0) return currRun;
133+
if (currVal == 0) {
134+
// extending current background run
135+
return currRun + length;
136+
} else {
137+
// flush previous foreground run
138+
runs.add(currRun);
139+
// start new background run of size `length`
140+
return length;
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)