Skip to content

Commit 8d9dc08

Browse files
committed
Add contour angle to contours report
Angle is from minAreaRect, and is in the range [-90, 0) Rework contours operations to be a bit more functional Make filter contours a bit more optimized
1 parent b97819a commit 8d9dc08

File tree

6 files changed

+219
-55
lines changed

6 files changed

+219
-55
lines changed

core/src/main/java/edu/wpi/grip/core/operations/composite/ContoursReport.java

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import edu.wpi.grip.core.operations.network.Publishable;
55
import edu.wpi.grip.core.sockets.NoSocketTypeLabel;
66
import edu.wpi.grip.core.sockets.Socket;
7+
import edu.wpi.grip.core.util.LazyInit;
8+
import edu.wpi.grip.core.util.PointerStream;
79

810
import com.google.auto.value.AutoValue;
911

12+
import org.bytedeco.javacpp.opencv_core.RotatedRect;
13+
import org.bytedeco.javacpp.opencv_imgproc;
14+
1015
import java.util.ArrayList;
1116
import java.util.List;
12-
import java.util.Optional;
17+
import java.util.stream.Stream;
1318

1419
import static org.bytedeco.javacpp.opencv_core.Mat;
1520
import static org.bytedeco.javacpp.opencv_core.MatVector;
1621
import static org.bytedeco.javacpp.opencv_core.Rect;
17-
import static org.bytedeco.javacpp.opencv_imgproc.boundingRect;
1822
import static org.bytedeco.javacpp.opencv_imgproc.contourArea;
1923
import static org.bytedeco.javacpp.opencv_imgproc.convexHull;
2024

@@ -29,7 +33,9 @@ public final class ContoursReport implements Publishable {
2933
private final int rows;
3034
private final int cols;
3135
private final MatVector contours;
32-
private Optional<Rect[]> boundingBoxes = Optional.empty();
36+
private final LazyInit<Rect[]> boundingBoxes = new LazyInit<>(this::computeBoundingBoxes);
37+
private final LazyInit<RotatedRect[]> rotatedBoundingBoxes =
38+
new LazyInit<>(this::computeMinAreaBoundingBoxes);
3339

3440
/**
3541
* Construct an empty report. This is used as a default value for {@link Socket}s containing
@@ -68,9 +74,10 @@ public List<Contour> getProcessedContours() {
6874
double[] width = getWidth();
6975
double[] height = getHeights();
7076
double[] solidity = getSolidity();
77+
double[] angles = getAngles();
7178
for (int i = 0; i < contours.size(); i++) {
7279
processedContours.add(Contour.create(area[i], centerX[i], centerY[i], width[i], height[i],
73-
solidity[i]));
80+
solidity[i], angles[i]));
7481
}
7582
return processedContours;
7683
}
@@ -80,66 +87,51 @@ public List<Contour> getProcessedContours() {
8087
* boxes are used to compute several different properties, so it's probably not a good idea to
8188
* compute them over and over again.
8289
*/
83-
private synchronized Rect[] computeBoundingBoxes() {
84-
if (!boundingBoxes.isPresent()) {
85-
Rect[] bb = new Rect[(int) contours.size()];
86-
for (int i = 0; i < contours.size(); i++) {
87-
bb[i] = boundingRect(contours.get(i));
88-
}
89-
90-
boundingBoxes = Optional.of(bb);
91-
}
90+
private Rect[] computeBoundingBoxes() {
91+
return PointerStream.ofMatVector(contours)
92+
.map(opencv_imgproc::boundingRect)
93+
.toArray(Rect[]::new);
94+
}
9295

93-
return boundingBoxes.get();
96+
private RotatedRect[] computeMinAreaBoundingBoxes() {
97+
return PointerStream.ofMatVector(contours)
98+
.map(opencv_imgproc::minAreaRect)
99+
.toArray(RotatedRect[]::new);
94100
}
95101

96102
@PublishValue(key = "area", weight = 0)
97103
public double[] getArea() {
98-
final double[] areas = new double[(int) contours.size()];
99-
for (int i = 0; i < contours.size(); i++) {
100-
areas[i] = contourArea(contours.get(i));
101-
}
102-
return areas;
104+
return PointerStream.ofMatVector(contours)
105+
.mapToDouble(opencv_imgproc::contourArea)
106+
.toArray();
103107
}
104108

105109
@PublishValue(key = "centerX", weight = 1)
106110
public double[] getCenterX() {
107-
final double[] centers = new double[(int) contours.size()];
108-
final Rect[] boundingBoxes = computeBoundingBoxes();
109-
for (int i = 0; i < contours.size(); i++) {
110-
centers[i] = boundingBoxes[i].x() + boundingBoxes[i].width() / 2;
111-
}
112-
return centers;
111+
return Stream.of(boundingBoxes.get())
112+
.mapToDouble(r -> r.x() + r.width() / 2)
113+
.toArray();
113114
}
114115

115116
@PublishValue(key = "centerY", weight = 2)
116117
public double[] getCenterY() {
117-
final double[] centers = new double[(int) contours.size()];
118-
final Rect[] boundingBoxes = computeBoundingBoxes();
119-
for (int i = 0; i < contours.size(); i++) {
120-
centers[i] = boundingBoxes[i].y() + boundingBoxes[i].height() / 2;
121-
}
122-
return centers;
118+
return Stream.of(boundingBoxes.get())
119+
.mapToDouble(r -> r.y() + r.height() / 2)
120+
.toArray();
123121
}
124122

125123
@PublishValue(key = "width", weight = 3)
126124
public synchronized double[] getWidth() {
127-
final double[] widths = new double[(int) contours.size()];
128-
final Rect[] boundingBoxes = computeBoundingBoxes();
129-
for (int i = 0; i < contours.size(); i++) {
130-
widths[i] = boundingBoxes[i].width();
131-
}
132-
return widths;
125+
return Stream.of(boundingBoxes.get())
126+
.mapToDouble(Rect::width)
127+
.toArray();
133128
}
134129

135130
@PublishValue(key = "height", weight = 4)
136131
public synchronized double[] getHeights() {
137-
final double[] heights = new double[(int) contours.size()];
138-
final Rect[] boundingBoxes = computeBoundingBoxes();
139-
for (int i = 0; i < contours.size(); i++) {
140-
heights[i] = boundingBoxes[i].height();
141-
}
142-
return heights;
132+
return Stream.of(boundingBoxes.get())
133+
.mapToDouble(Rect::height)
134+
.toArray();
143135
}
144136

145137
@PublishValue(key = "solidity", weight = 5)
@@ -154,11 +146,19 @@ public synchronized double[] getSolidity() {
154146
return solidities;
155147
}
156148

149+
@PublishValue(key = "angle", weight = 6)
150+
public synchronized double[] getAngles() {
151+
return Stream.of(rotatedBoundingBoxes.get())
152+
.mapToDouble(RotatedRect::angle)
153+
.toArray();
154+
}
155+
157156
@AutoValue
158157
public abstract static class Contour {
159158
static Contour create(double area, double centerX, double centerY, double width, double
160-
height, double solidity) {
161-
return new AutoValue_ContoursReport_Contour(area, centerX, centerY, width, height, solidity);
159+
height, double solidity, double angle) {
160+
return new AutoValue_ContoursReport_Contour(area, centerX, centerY, width, height, solidity,
161+
angle);
162162
}
163163

164164
public abstract double area();
@@ -172,5 +172,7 @@ static Contour create(double area, double centerX, double centerY, double width,
172172
public abstract double height();
173173

174174
public abstract double solidity();
175+
176+
public abstract double angle();
175177
}
176178
}

core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515

1616
import static org.bytedeco.javacpp.opencv_core.Mat;
1717
import static org.bytedeco.javacpp.opencv_core.MatVector;
18-
import static org.bytedeco.javacpp.opencv_core.Rect;
1918
import static org.bytedeco.javacpp.opencv_imgproc.arcLength;
20-
import static org.bytedeco.javacpp.opencv_imgproc.boundingRect;
2119
import static org.bytedeco.javacpp.opencv_imgproc.contourArea;
2220
import static org.bytedeco.javacpp.opencv_imgproc.convexHull;
2321

@@ -73,6 +71,8 @@ public class FilterContoursOperation implements Operation {
7371
private final SocketHint<Number> maxRatioHint =
7472
SocketHints.Inputs.createNumberSpinnerSocketHint("Max Ratio", 1000, 0, Integer.MAX_VALUE);
7573

74+
private final SocketHint<List<Number>> angleHint =
75+
SocketHints.Inputs.createNumberListRangeSocketHint("Angle", -90, 0);
7676

7777
private final InputSocket<ContoursReport> contoursSocket;
7878
private final InputSocket<Number> minAreaSocket;
@@ -86,6 +86,7 @@ public class FilterContoursOperation implements Operation {
8686
private final InputSocket<Number> maxVertexSocket;
8787
private final InputSocket<Number> minRatioSocket;
8888
private final InputSocket<Number> maxRatioSocket;
89+
private final InputSocket<List<Number>> angleSocket;
8990

9091
private final OutputSocket<ContoursReport> outputSocket;
9192

@@ -105,6 +106,7 @@ public FilterContoursOperation(InputSocket.Factory inputSocketFactory, OutputSoc
105106
this.maxVertexSocket = inputSocketFactory.create(maxVertexHint);
106107
this.minRatioSocket = inputSocketFactory.create(minRatioHint);
107108
this.maxRatioSocket = inputSocketFactory.create(maxRatioHint);
109+
this.angleSocket = inputSocketFactory.create(angleHint);
108110

109111
this.outputSocket = outputSocketFactory.create(contoursHint);
110112
}
@@ -123,7 +125,8 @@ public List<InputSocket> getInputSockets() {
123125
maxVertexSocket,
124126
minVertexSocket,
125127
minRatioSocket,
126-
maxRatioSocket
128+
maxRatioSocket,
129+
angleSocket
127130
);
128131
}
129132

@@ -138,6 +141,7 @@ public List<OutputSocket> getOutputSockets() {
138141
@SuppressWarnings("unchecked")
139142
public void perform() {
140143
final InputSocket<ContoursReport> inputSocket = contoursSocket;
144+
final ContoursReport report = inputSocket.getValue().get();
141145
final double minArea = minAreaSocket.getValue().get().doubleValue();
142146
final double minPerimeter = minPerimeterSocket.getValue().get().doubleValue();
143147
final double minWidth = minWidthSocket.getValue().get().doubleValue();
@@ -150,9 +154,10 @@ public void perform() {
150154
final double maxVertexCount = maxVertexSocket.getValue().get().doubleValue();
151155
final double minRatio = minRatioSocket.getValue().get().doubleValue();
152156
final double maxRatio = maxRatioSocket.getValue().get().doubleValue();
157+
final double minAngle = angleSocket.getValue().get().get(0).doubleValue();
158+
final double maxAngle = angleSocket.getValue().get().get(1).doubleValue();
153159

154-
155-
final MatVector inputContours = inputSocket.getValue().get().getContours();
160+
final MatVector inputContours = report.getContours();
156161
final MatVector outputContours = new MatVector(inputContours.size());
157162
final Mat hull = new Mat();
158163

@@ -163,15 +168,14 @@ public void perform() {
163168
for (int i = 0; i < inputContours.size(); i++) {
164169
final Mat contour = inputContours.get(i);
165170

166-
final Rect bb = boundingRect(contour);
167-
if (bb.width() < minWidth || bb.width() > maxWidth) {
171+
if (report.getWidth()[i] < minWidth || report.getWidth()[i] > maxWidth) {
168172
continue;
169173
}
170-
if (bb.height() < minHeight || bb.height() > maxHeight) {
174+
if (report.getHeights()[i] < minHeight || report.getHeights()[i] > maxHeight) {
171175
continue;
172176
}
173177

174-
final double area = contourArea(contour);
178+
final double area = report.getArea()[i];
175179
if (area < minArea) {
176180
continue;
177181
}
@@ -190,11 +194,16 @@ public void perform() {
190194
continue;
191195
}
192196

193-
final double ratio = (double) bb.width() / (double) bb.height();
197+
final double ratio = report.getWidth()[i] / report.getHeights()[i];
194198
if (ratio < minRatio || ratio > maxRatio) {
195199
continue;
196200
}
197201

202+
final double angle = report.getAngles()[i];
203+
if (angle < minAngle || angle > maxAngle) {
204+
continue;
205+
}
206+
198207
outputContours.put(filteredContourCount++, contour);
199208
}
200209

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package edu.wpi.grip.core.util;
2+
3+
import java.util.Objects;
4+
import java.util.function.Supplier;
5+
6+
/**
7+
* A holder for data that gets lazily initialized.
8+
*
9+
* @param <T> the type of held data
10+
*/
11+
public class LazyInit<T> {
12+
13+
private T value = null;
14+
private final Supplier<? extends T> factory;
15+
16+
/**
17+
* Creates a new lazily initialized data holder.
18+
*
19+
* @param factory the factory to use to create the held value
20+
*/
21+
public LazyInit(Supplier<? extends T> factory) {
22+
this.factory = Objects.requireNonNull(factory, "factory");
23+
}
24+
25+
/**
26+
* Gets the value, initializing it if it has not yet been created.
27+
*
28+
* @return the held value
29+
*/
30+
public T get() {
31+
if (value == null) {
32+
value = factory.get();
33+
}
34+
return value;
35+
}
36+
37+
/**
38+
* Clears the held value. The next call to {@link #get()} will re-instantiate the held value.
39+
*/
40+
public void clear() {
41+
value = null;
42+
}
43+
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package edu.wpi.grip.core.util;
2+
3+
import java.util.stream.LongStream;
4+
import java.util.stream.Stream;
5+
6+
import static org.bytedeco.javacpp.opencv_core.Mat;
7+
import static org.bytedeco.javacpp.opencv_core.MatVector;
8+
9+
/**
10+
* Utility class for streaming native vector wrappers like {@code MatVector}
11+
* ({@code std::vector<T>}) with the Java {@link Stream} API.
12+
*/
13+
public final class PointerStream {
14+
15+
/**
16+
* Creates a stream of {@code Mat} objects in a {@code MatVector}.
17+
*
18+
* @param vector the vector of {@code Mats} to stream
19+
*
20+
* @return a new stream object for the contents of the vector
21+
*/
22+
public static Stream<Mat> ofMatVector(MatVector vector) {
23+
return LongStream.range(0, vector.size())
24+
.mapToObj(vector::get);
25+
}
26+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package edu.wpi.grip.core.util;
2+
3+
import org.junit.Test;
4+
5+
import java.util.function.Supplier;
6+
7+
import static org.junit.Assert.assertEquals;
8+
9+
public class LazyInitTest {
10+
11+
@Test
12+
public void testFactoryIsOnlyCalledOnce() {
13+
final String output = "foo";
14+
final int[] count = {0};
15+
final Supplier<String> factory = () -> {
16+
count[0]++;
17+
return output;
18+
};
19+
20+
LazyInit<String> lazyInit = new LazyInit<>(factory);
21+
lazyInit.get();
22+
assertEquals(1, count[0]);
23+
24+
lazyInit.get();
25+
assertEquals("Calling get() more than once should only call the factory once", 1, count[0]);
26+
}
27+
28+
@Test
29+
public void testClear() {
30+
final String output = "foo";
31+
final int[] count = {0};
32+
final Supplier<String> factory = () -> {
33+
count[0]++;
34+
return output;
35+
};
36+
LazyInit<String> lazyInit = new LazyInit<>(factory);
37+
lazyInit.get();
38+
assertEquals(1, count[0]);
39+
40+
lazyInit.clear();
41+
lazyInit.get();
42+
assertEquals(2, count[0]);
43+
}
44+
45+
}

0 commit comments

Comments
 (0)