Skip to content
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

Add contour angle to contours report #915

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -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
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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);
}
Expand All @@ -124,7 +126,8 @@ public List<InputSocket> getInputSockets() {
maxVertexSocket,
minVertexSocket,
minRatioSocket,
maxRatioSocket
maxRatioSocket,
angleSocket
);
}

Expand All @@ -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();
Expand All @@ -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();

Expand All @@ -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;
}
Expand All @@ -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);
}

Expand Down
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]);
}

}
Loading