Skip to content

Add contour angle to contours report #915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -5,17 +5,21 @@
import edu.wpi.grip.core.operations.network.Publishable;
import edu.wpi.grip.core.sockets.NoSocketTypeLabel;
import edu.wpi.grip.core.sockets.Socket;
import edu.wpi.grip.core.util.LazyInit;
import edu.wpi.grip.core.util.PointerStream;

import com.google.auto.value.AutoValue;

import org.bytedeco.javacpp.opencv_core.RotatedRect;
import org.bytedeco.javacpp.opencv_imgproc;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static org.bytedeco.javacpp.opencv_core.Mat;
import static org.bytedeco.javacpp.opencv_core.MatVector;
import static org.bytedeco.javacpp.opencv_core.Rect;
import static org.bytedeco.javacpp.opencv_imgproc.boundingRect;
import static org.bytedeco.javacpp.opencv_imgproc.contourArea;
import static org.bytedeco.javacpp.opencv_imgproc.convexHull;

@@ -31,7 +35,9 @@ public final class ContoursReport implements Publishable {
private final int rows;
private final int cols;
private final MatVector contours;
private Optional<Rect[]> boundingBoxes = Optional.empty();
private final LazyInit<Rect[]> boundingBoxes = new LazyInit<>(this::computeBoundingBoxes);
private final LazyInit<RotatedRect[]> rotatedBoundingBoxes =
new LazyInit<>(this::computeMinAreaBoundingBoxes);

/**
* Construct an empty report. This is used as a default value for {@link Socket}s containing
@@ -70,78 +76,66 @@ public List<Contour> getProcessedContours() {
double[] width = getWidth();
double[] height = getHeights();
double[] solidity = getSolidity();
double[] angles = getAngles();
for (int i = 0; i < contours.size(); i++) {
processedContours.add(Contour.create(area[i], centerX[i], centerY[i], width[i], height[i],
solidity[i]));
solidity[i], angles[i]));
}
return processedContours;
}

/**
* Compute the bounding boxes of all contours (if they haven't already been computed). Bounding
* boxes are used to compute several different properties, so it's probably not a good idea to
* compute them over and over again.
* Compute the bounding boxes of all contours. Called lazily and cached by {@link #boundingBoxes}.
*/
private synchronized Rect[] computeBoundingBoxes() {
if (!boundingBoxes.isPresent()) {
Rect[] bb = new Rect[(int) contours.size()];
for (int i = 0; i < contours.size(); i++) {
bb[i] = boundingRect(contours.get(i));
}

boundingBoxes = Optional.of(bb);
}
private Rect[] computeBoundingBoxes() {
return PointerStream.ofMatVector(contours)
.map(opencv_imgproc::boundingRect)
.toArray(Rect[]::new);
}

return boundingBoxes.get();
/**
* Computes the minimum-area bounding boxes of all contours. Called lazily and cached by
* {@link #rotatedBoundingBoxes}.
*/
private RotatedRect[] computeMinAreaBoundingBoxes() {
return PointerStream.ofMatVector(contours)
.map(opencv_imgproc::minAreaRect)
.toArray(RotatedRect[]::new);
}

@PublishValue(key = "area", weight = 0)
public double[] getArea() {
final double[] areas = new double[(int) contours.size()];
for (int i = 0; i < contours.size(); i++) {
areas[i] = contourArea(contours.get(i));
}
return areas;
return PointerStream.ofMatVector(contours)
.mapToDouble(opencv_imgproc::contourArea)
.toArray();
}

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

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

@PublishValue(key = "width", weight = 3)
public synchronized double[] getWidth() {
final double[] widths = new double[(int) contours.size()];
final Rect[] boundingBoxes = computeBoundingBoxes();
for (int i = 0; i < contours.size(); i++) {
widths[i] = boundingBoxes[i].width();
}
return widths;
return Stream.of(boundingBoxes.get())
.mapToDouble(Rect::width)
.toArray();
}

@PublishValue(key = "height", weight = 4)
public synchronized double[] getHeights() {
final double[] heights = new double[(int) contours.size()];
final Rect[] boundingBoxes = computeBoundingBoxes();
for (int i = 0; i < contours.size(); i++) {
heights[i] = boundingBoxes[i].height();
}
return heights;
return Stream.of(boundingBoxes.get())
.mapToDouble(Rect::height)
.toArray();
}

@PublishValue(key = "solidity", weight = 5)
@@ -156,11 +150,19 @@ public synchronized double[] getSolidity() {
return solidities;
}

@PublishValue(key = "angle", weight = 6)
public synchronized double[] getAngles() {
return Stream.of(rotatedBoundingBoxes.get())
.mapToDouble(RotatedRect::angle)
.toArray();
}

@AutoValue
public abstract static class Contour {
public static Contour create(double area, double centerX, double centerY, double width, double
height, double solidity) {
return new AutoValue_ContoursReport_Contour(area, centerX, centerY, width, height, solidity);
height, double solidity, double angle) {
return new AutoValue_ContoursReport_Contour(area, centerX, centerY, width, height, solidity,
angle);
}

public abstract double area();
@@ -174,5 +176,7 @@ public static Contour create(double area, double centerX, double centerY, double
public abstract double height();

public abstract double solidity();

public abstract double angle();
}
}
Original file line number Diff line number Diff line change
@@ -15,9 +15,7 @@

import static org.bytedeco.javacpp.opencv_core.Mat;
import static org.bytedeco.javacpp.opencv_core.MatVector;
import static org.bytedeco.javacpp.opencv_core.Rect;
import static org.bytedeco.javacpp.opencv_imgproc.arcLength;
import static org.bytedeco.javacpp.opencv_imgproc.boundingRect;
import static org.bytedeco.javacpp.opencv_imgproc.contourArea;
import static org.bytedeco.javacpp.opencv_imgproc.convexHull;

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

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

private final InputSocket<ContoursReport> contoursSocket;
private final InputSocket<Number> minAreaSocket;
@@ -87,6 +87,7 @@ public class FilterContoursOperation implements Operation {
private final InputSocket<Number> maxVertexSocket;
private final InputSocket<Number> minRatioSocket;
private final InputSocket<Number> maxRatioSocket;
private final InputSocket<List<Number>> angleSocket;

private final OutputSocket<ContoursReport> outputSocket;

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

this.outputSocket = outputSocketFactory.create(contoursHint);
}
@@ -124,7 +126,8 @@ public List<InputSocket> getInputSockets() {
maxVertexSocket,
minVertexSocket,
minRatioSocket,
maxRatioSocket
maxRatioSocket,
angleSocket
);
}

@@ -139,6 +142,7 @@ public List<OutputSocket> getOutputSockets() {
@SuppressWarnings("unchecked")
public void perform() {
final InputSocket<ContoursReport> inputSocket = contoursSocket;
final ContoursReport report = inputSocket.getValue().get();
final double minArea = minAreaSocket.getValue().get().doubleValue();
final double minPerimeter = minPerimeterSocket.getValue().get().doubleValue();
final double minWidth = minWidthSocket.getValue().get().doubleValue();
@@ -151,9 +155,10 @@ public void perform() {
final double maxVertexCount = maxVertexSocket.getValue().get().doubleValue();
final double minRatio = minRatioSocket.getValue().get().doubleValue();
final double maxRatio = maxRatioSocket.getValue().get().doubleValue();
final double minAngle = angleSocket.getValue().get().get(0).doubleValue();
final double maxAngle = angleSocket.getValue().get().get(1).doubleValue();


final MatVector inputContours = inputSocket.getValue().get().getContours();
final MatVector inputContours = report.getContours();
final MatVector outputContours = new MatVector(inputContours.size());
final Mat hull = new Mat();

@@ -164,15 +169,14 @@ public void perform() {
for (int i = 0; i < inputContours.size(); i++) {
final Mat contour = inputContours.get(i);

final Rect bb = boundingRect(contour);
if (bb.width() < minWidth || bb.width() > maxWidth) {
if (report.getWidth()[i] < minWidth || report.getWidth()[i] > maxWidth) {
continue;
}
if (bb.height() < minHeight || bb.height() > maxHeight) {
if (report.getHeights()[i] < minHeight || report.getHeights()[i] > maxHeight) {
continue;
}

final double area = contourArea(contour);
final double area = report.getArea()[i];
if (area < minArea) {
continue;
}
@@ -191,11 +195,16 @@ public void perform() {
continue;
}

final double ratio = (double) bb.width() / (double) bb.height();
final double ratio = report.getWidth()[i] / report.getHeights()[i];
if (ratio < minRatio || ratio > maxRatio) {
continue;
}

final double angle = report.getAngles()[i];
if (angle < minAngle || angle > maxAngle) {
continue;
}

outputContours.put(filteredContourCount++, contour);
}

44 changes: 44 additions & 0 deletions core/src/main/java/edu/wpi/grip/core/util/LazyInit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package edu.wpi.grip.core.util;

import java.util.Objects;
import java.util.function.Supplier;

/**
* A holder for data that gets lazily initialized.
*
* @param <T> the type of held data
*/
public class LazyInit<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are no thread safety guarantees offered? Is this intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping!


private T value = null;
private final Supplier<? extends T> factory;

/**
* Creates a new lazily initialized data holder.
*
* @param factory the factory to use to create the held value
*/
public LazyInit(Supplier<? extends T> factory) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't make this expose a constructor, it doesn't tightly couple this to needing to be newed.

this.factory = Objects.requireNonNull(factory, "factory");
}

/**
* Gets the value, initializing it if it has not yet been created.
*
* @return the held value
*/
public T get() {
if (value == null) {
value = factory.get();
}
return value;
}

/**
* Clears the held value. The next call to {@link #get()} will re-instantiate the held value.
*/
public void clear() {
value = null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should you be able to clear a lazy?


}
30 changes: 30 additions & 0 deletions core/src/main/java/edu/wpi/grip/core/util/PointerStream.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package edu.wpi.grip.core.util;

import java.util.stream.LongStream;
import java.util.stream.Stream;

import static org.bytedeco.javacpp.opencv_core.Mat;
import static org.bytedeco.javacpp.opencv_core.MatVector;

/**
* Utility class for streaming native vector wrappers like {@code MatVector}
* ({@code std::vector<T>}) with the Java {@link Stream} API.
*/
public final class PointerStream {

private PointerStream() {
throw new UnsupportedOperationException("This is a utility class!");
}

/**
* Creates a stream of {@code Mat} objects in a {@code MatVector}.
*
* @param vector the vector of {@code Mats} to stream
*
* @return a new stream object for the contents of the vector
*/
public static Stream<Mat> ofMatVector(MatVector vector) {
return LongStream.range(0, vector.size())
.mapToObj(vector::get);
}
}
45 changes: 45 additions & 0 deletions core/src/test/java/edu/wpi/grip/core/util/LazyInitTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package edu.wpi.grip.core.util;

import org.junit.Test;

import java.util.function.Supplier;

import static org.junit.Assert.assertEquals;

public class LazyInitTest {

@Test
public void testFactoryIsOnlyCalledOnce() {
final String output = "foo";
final int[] count = {0};
final Supplier<String> factory = () -> {
count[0]++;
return output;
};

LazyInit<String> lazyInit = new LazyInit<>(factory);
lazyInit.get();
assertEquals(1, count[0]);

lazyInit.get();
assertEquals("Calling get() more than once should only call the factory once", 1, count[0]);
}

@Test
public void testClear() {
final String output = "foo";
final int[] count = {0};
final Supplier<String> factory = () -> {
count[0]++;
return output;
};
LazyInit<String> lazyInit = new LazyInit<>(factory);
lazyInit.get();
assertEquals(1, count[0]);

lazyInit.clear();
lazyInit.get();
assertEquals(2, count[0]);
}

}
38 changes: 38 additions & 0 deletions core/src/test/java/edu/wpi/grip/core/util/PointerStreamTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package edu.wpi.grip.core.util;

import org.bytedeco.javacpp.opencv_core.MatVector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class PointerStreamTest {

private MatVector vector;

@Before
public void setupVector() {
vector = new MatVector();
}

@After
public void freeVector() {
vector.deallocate();
}

@Test
public void testStreamEmptyMatVector() {
vector.resize(0);
long size = PointerStream.ofMatVector(vector).count();
assertEquals("MatVector of size 0 should result in an empty stream", 0, size);
}

@Test
public void testStreamMatVectorWithContents() {
final int size = 4;
vector.resize(size);
long actual = PointerStream.ofMatVector(vector).count();
assertEquals("MatVector of size 4 should have 4 stream elements", size, actual);
}
}
Original file line number Diff line number Diff line change
@@ -15,9 +15,10 @@ This creates the C++ OpenCV Filter Contours function
* @param maxVertexCount maximum vertex Count.
* @param minRatio minimum ratio of width to height.
* @param maxRatio maximum ratio of width to height.
* @param angle the minimum and maximum angle of a contour
* @param output vector of filtered contours.
*/
void $className::#func($step ["inputContours", "minArea", "minPerimeter", "minWidth", "maxWidth", "minHeight", "maxHeight", "solidity", "maxVertexCount", "minVertexCount", "minRatio", "maxRatio", "output"]) {
void $className::#func($step ["inputContours", "minArea", "minPerimeter", "minWidth", "maxWidth", "minHeight", "maxHeight", "solidity", "maxVertexCount", "minVertexCount", "minRatio", "maxRatio", "angle", "output"]) {
std::vector<cv::Point> hull;
output.clear();
for (std::vector<cv::Point> contour: inputContours) {
@@ -33,6 +34,8 @@ This creates the C++ OpenCV Filter Contours function
if (contour.size() < minVertexCount || contour.size() > maxVertexCount) continue;
double ratio = (double) bb.width / (double) bb.height;
if (ratio < minRatio || ratio > maxRatio) continue;
double contourAngle = cv::minAreaRect(contour).angle;
if (contourAngle < angle[0] || contourAngle > angle[1]) continue;
output.push_back(contour);
}
}
Original file line number Diff line number Diff line change
@@ -15,12 +15,13 @@ This creates the java OpenCV Filter Contours function
* @param minVertexCount minimum vertex Count of the contours
* @param maxVertexCount maximum vertex Count
* @param minRatio minimum ratio of width to height
* @param angle the minimum and maximum angle of a contour
* @param maxRatio maximum ratio of width to height
*/
private void $tMeth.name($step.name())(List<MatOfPoint> inputContours, double minArea,
double minPerimeter, double minWidth, double maxWidth, double minHeight, double
maxHeight, double[] solidity, double maxVertexCount, double minVertexCount, double
minRatio, double maxRatio, List<MatOfPoint> output) {
minRatio, double maxRatio, double[] angle, List<MatOfPoint> output) {
final MatOfInt hull = new MatOfInt();
output.clear();
//operation
@@ -45,6 +46,10 @@ This creates the java OpenCV Filter Contours function
if (contour.rows() < minVertexCount || contour.rows() > maxVertexCount) continue;
final double ratio = bb.width / (double)bb.height;
if (ratio < minRatio || ratio > maxRatio) continue;
final MatOfPoint2f copy = new MatOfPoint2f(contour);
final RotatedRect rr = Imgproc.minAreaRect(copy);
copy.release();
if (rr.angle < angle[0] || rr.angle > angle[1]) continue;
output.add(contour);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@staticmethod
def $tMeth.name($step.name())(input_contours, min_area, min_perimeter, min_width, max_width,
min_height, max_height, solidity, max_vertex_count, min_vertex_count,
min_ratio, max_ratio):
min_ratio, max_ratio, angle):
"""Filters out contours that do not meet certain criteria.
Args:
input_contours: Contours as a list of numpy.ndarray.
@@ -16,6 +16,7 @@
max_vertex_count: Maximum vertex Count.
min_ratio: Minimum ratio of width to height.
max_ratio: Maximum ratio of width to height.
angle: The minimum and maximum allowable angles of a contour.
Returns:
Contours as a list of numpy.ndarray.
"""
@@ -40,5 +41,8 @@
ratio = (float)(w) / h
if (ratio < min_ratio or ratio > max_ratio):
continue
contourAngle = cv2.minAreaRect(contour).angle
if (contourAngle < angle[0] or contourAngle > angle[1]):
continue
output.append(contour)
return output