Skip to content

Commit

Permalink
Add contour angle to contours report
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SamCarlberg authored and azure-pipelines committed Apr 18, 2019
1 parent d4d5d5e commit eaf2e4d
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 55 deletions.
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,9 +76,10 @@ 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;
}
Expand All @@ -82,66 +89,51 @@ public List<Contour> getProcessedContours() {
* boxes are used to compute several different properties, so it's probably not a good idea to
* compute them over and over again.
*/
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();
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 +148,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 +174,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> {

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) {
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;
}

}
26 changes: 26 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,26 @@
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 {

/**
* 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

0 comments on commit eaf2e4d

Please sign in to comment.