diff --git a/core/src/main/java/edu/wpi/grip/core/operations/Operations.java b/core/src/main/java/edu/wpi/grip/core/operations/Operations.java index fee67bfc93..19ca783640 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/Operations.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/Operations.java @@ -3,13 +3,13 @@ import edu.wpi.grip.core.FileManager; import edu.wpi.grip.core.OperationMetaData; import edu.wpi.grip.core.events.OperationAddedEvent; +import edu.wpi.grip.core.operations.composite.AdvancedFilterContoursOperation; import edu.wpi.grip.core.operations.composite.BlobsReport; import edu.wpi.grip.core.operations.composite.BlurOperation; import edu.wpi.grip.core.operations.composite.ContoursReport; import edu.wpi.grip.core.operations.composite.ConvexHullsOperation; import edu.wpi.grip.core.operations.composite.DesaturateOperation; import edu.wpi.grip.core.operations.composite.DistanceTransformOperation; -import edu.wpi.grip.core.operations.composite.FilterContoursOperation; import edu.wpi.grip.core.operations.composite.FilterLinesOperation; import edu.wpi.grip.core.operations.composite.FindBlobsOperation; import edu.wpi.grip.core.operations.composite.FindContoursOperation; @@ -23,6 +23,7 @@ import edu.wpi.grip.core.operations.composite.RGBThresholdOperation; import edu.wpi.grip.core.operations.composite.ResizeOperation; import edu.wpi.grip.core.operations.composite.SaveImageOperation; +import edu.wpi.grip.core.operations.composite.SimpleFilterContoursOperation; import edu.wpi.grip.core.operations.composite.SwitchOperation; import edu.wpi.grip.core.operations.composite.ThresholdMoving; import edu.wpi.grip.core.operations.composite.ValveOperation; @@ -85,8 +86,10 @@ public class Operations { () -> new DesaturateOperation(isf, osf)), new OperationMetaData(DistanceTransformOperation.DESCRIPTION, () -> new DistanceTransformOperation(isf, osf)), - new OperationMetaData(FilterContoursOperation.DESCRIPTION, - () -> new FilterContoursOperation(isf, osf)), + new OperationMetaData(SimpleFilterContoursOperation.DESCRIPTION, + () -> new SimpleFilterContoursOperation(isf, osf)), + new OperationMetaData(AdvancedFilterContoursOperation.DESCRIPTION, + () -> new AdvancedFilterContoursOperation(isf, osf)), new OperationMetaData(FilterLinesOperation.DESCRIPTION, () -> new FilterLinesOperation(isf, osf)), new OperationMetaData(FindBlobsOperation.DESCRIPTION, diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/AdvancedFilterContoursOperation.java similarity index 73% rename from core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java rename to core/src/main/java/edu/wpi/grip/core/operations/composite/AdvancedFilterContoursOperation.java index 433ccd06cb..734862c11c 100644 --- a/core/src/main/java/edu/wpi/grip/core/operations/composite/FilterContoursOperation.java +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/AdvancedFilterContoursOperation.java @@ -14,11 +14,14 @@ import static org.bytedeco.javacpp.opencv_core.Mat; import static org.bytedeco.javacpp.opencv_core.MatVector; +import static org.bytedeco.javacpp.opencv_core.Point2f; import static org.bytedeco.javacpp.opencv_core.Rect; +import static org.bytedeco.javacpp.opencv_core.RotatedRect; 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; +import static org.bytedeco.javacpp.opencv_imgproc.minAreaRect; /** * An {@link Operation} that takes in a list of contours and outputs a list of any contours in the @@ -28,14 +31,15 @@ * small objects, as well as contours that do not meet the expected characteristics of the feature * we're actually looking for. So, this operation can help narrow them down. */ -public class FilterContoursOperation implements Operation { +public class AdvancedFilterContoursOperation implements Operation { public static final OperationDescription DESCRIPTION = OperationDescription.builder() - .name("Filter Contours") + .name("Advanced Filter Contours") .summary("Find contours matching certain criteria") .category(OperationDescription.Category.FEATURE_DETECTION) .icon(Icon.iconStream("find-contours")) + .aliases("Filter Contours") .build(); private final SocketHint contoursHint = new SocketHint.Builder<>(ContoursReport @@ -45,9 +49,19 @@ public class FilterContoursOperation implements Operation { private final SocketHint minAreaHint = SocketHints.Inputs.createNumberSpinnerSocketHint("Min Area", 0, 0, Integer.MAX_VALUE); + private final SocketHint maxAreaHint = + SocketHints.Inputs.createNumberSpinnerSocketHint("Max Area", 10000, 0, Integer.MAX_VALUE); + private final SocketHint minPerimeterHint = SocketHints.Inputs.createNumberSpinnerSocketHint("Min Perimeter", 0, 0, Integer.MAX_VALUE); + private final SocketHint maxPerimeterHint = + SocketHints.Inputs.createNumberSpinnerSocketHint("Max Perimeter", 10000, 0, + Integer.MAX_VALUE); + + private final SocketHint rotatedRectHint = + SocketHints.createBooleanSocketHint("Rotated Rectangles", false); + private final SocketHint minWidthHint = SocketHints.Inputs.createNumberSpinnerSocketHint("Min Width", 0, 0, Integer.MAX_VALUE); @@ -79,34 +93,40 @@ public class FilterContoursOperation implements Operation { private final InputSocket contoursSocket; private final InputSocket minAreaSocket; + private final InputSocket maxAreaSocket; private final InputSocket minPerimeterSocket; + private final InputSocket maxPerimeterSocket; + private final InputSocket rotatedRectSocket; private final InputSocket minWidthSocket; private final InputSocket maxWidthSocket; private final InputSocket minHeightSocket; private final InputSocket maxHeightSocket; - private final InputSocket> soliditySocket; private final InputSocket minVertexSocket; private final InputSocket maxVertexSocket; private final InputSocket minRatioSocket; private final InputSocket maxRatioSocket; + private final InputSocket> soliditySocket; private final OutputSocket outputSocket; @SuppressWarnings("JavadocMethod") - public FilterContoursOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory - outputSocketFactory) { + public AdvancedFilterContoursOperation(InputSocket.Factory inputSocketFactory, + OutputSocket.Factory outputSocketFactory) { this.contoursSocket = inputSocketFactory.create(contoursHint); this.minAreaSocket = inputSocketFactory.create(minAreaHint); + this.maxAreaSocket = inputSocketFactory.create(maxAreaHint); this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint); + this.maxPerimeterSocket = inputSocketFactory.create(maxPerimeterHint); + this.rotatedRectSocket = inputSocketFactory.create(rotatedRectHint); this.minWidthSocket = inputSocketFactory.create(minWidthHint); this.maxWidthSocket = inputSocketFactory.create(maxWidthHint); this.minHeightSocket = inputSocketFactory.create(minHeightHint); this.maxHeightSocket = inputSocketFactory.create(maxHeightHint); - this.soliditySocket = inputSocketFactory.create(solidityHint); this.minVertexSocket = inputSocketFactory.create(minVertexHint); this.maxVertexSocket = inputSocketFactory.create(maxVertexHint); this.minRatioSocket = inputSocketFactory.create(minRatioHint); this.maxRatioSocket = inputSocketFactory.create(maxRatioHint); + this.soliditySocket = inputSocketFactory.create(solidityHint); this.outputSocket = outputSocketFactory.create(contoursHint); } @@ -116,16 +136,19 @@ public List getInputSockets() { return ImmutableList.of( contoursSocket, minAreaSocket, + maxAreaSocket, minPerimeterSocket, + maxPerimeterSocket, + rotatedRectSocket, minWidthSocket, maxWidthSocket, minHeightSocket, maxHeightSocket, - soliditySocket, - maxVertexSocket, minVertexSocket, + maxVertexSocket, minRatioSocket, - maxRatioSocket + maxRatioSocket, + soliditySocket ); } @@ -141,7 +164,10 @@ public List getOutputSockets() { public void perform() { final InputSocket inputSocket = contoursSocket; final double minArea = minAreaSocket.getValue().get().doubleValue(); + final double maxArea = maxAreaSocket.getValue().get().doubleValue(); final double minPerimeter = minPerimeterSocket.getValue().get().doubleValue(); + final double maxPerimeter = maxPerimeterSocket.getValue().get().doubleValue(); + final boolean rotatedRect = rotatedRectSocket.getValue().get().booleanValue(); final double minWidth = minWidthSocket.getValue().get().doubleValue(); final double maxWidth = maxWidthSocket.getValue().get().doubleValue(); final double minHeight = minHeightSocket.getValue().get().doubleValue(); @@ -165,19 +191,43 @@ 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) { + double width; + double height; + if (rotatedRect) { + final RotatedRect bb = minAreaRect(contour); + Point2f points = new Point2f(4); + bb.points(points); + final double rotatedWidth = Math.sqrt(Math.pow(points.position(0).x() + - points.position(1).x(), 2) + + Math.pow(points.position(0).y() - points.position(1).y(), 2)); + final double rotatedHeight = Math.sqrt( Math.pow(points.position(1).x() + - points.position(2).x(), 2) + + Math.pow(points.position(1).y() - points.position(2).y(), 2)); + if (Math.abs(bb.angle()) >= 45) { + width = rotatedWidth; + height = rotatedHeight; + } else { + width = rotatedHeight; + height = rotatedWidth; + } + } else { + final Rect normbb = boundingRect(contour); + width = normbb.width(); + height = normbb.height(); + } + + if (width < minWidth || width > maxWidth) { continue; } - if (bb.height() < minHeight || bb.height() > maxHeight) { + if (width < minHeight || width > maxHeight) { continue; } final double area = contourArea(contour); - if (area < minArea) { + if (area < minArea || area > maxArea) { continue; } - if (arcLength(contour, true) < minPerimeter) { + if (arcLength(contour, true) < minPerimeter || arcLength(contour, true) > maxPerimeter) { continue; } @@ -191,7 +241,7 @@ public void perform() { continue; } - final double ratio = (double) bb.width() / (double) bb.height(); + final double ratio = width / height; if (ratio < minRatio || ratio > maxRatio) { continue; } diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/SimpleFilterContoursOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/SimpleFilterContoursOperation.java new file mode 100644 index 0000000000..37fff85d6c --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/SimpleFilterContoursOperation.java @@ -0,0 +1,115 @@ +package edu.wpi.grip.core.operations.composite; + +import edu.wpi.grip.core.Operation; +import edu.wpi.grip.core.OperationDescription; +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.SocketHint; +import edu.wpi.grip.core.sockets.SocketHints; +import edu.wpi.grip.core.util.Icon; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +import static org.bytedeco.javacpp.opencv_core.Mat; +import static org.bytedeco.javacpp.opencv_core.MatVector; +import static org.bytedeco.javacpp.opencv_imgproc.arcLength; +import static org.bytedeco.javacpp.opencv_imgproc.contourArea; + +/** + * An {@link Operation} that takes in a list of contours and outputs a list of any contours in the + * input that match all of several criteria. The user can specify a minimum area and perimeter. + * This is useful because running a FindContours on a real-life image typically leads to many small + * undesirable contours from noise and small objects, as well as contours that do not meet the + * expected characteristics of the feature we're actually looking for. So, this operation can + * help narrow them down. + */ +public class SimpleFilterContoursOperation implements Operation { + + public static final OperationDescription DESCRIPTION = + OperationDescription.builder() + .name("Simple Filter Contours") + .summary("Find contours matching certain criteria") + .category(OperationDescription.Category.FEATURE_DETECTION) + .icon(Icon.iconStream("find-contours")) + .build(); + + private final SocketHint contoursHint = + new SocketHint.Builder<>(ContoursReport.class) + .identifier("Contours").initialValueSupplier(ContoursReport::new).build(); + + private final SocketHint minAreaHint = + SocketHints.Inputs.createNumberSpinnerSocketHint("Min Area", 0, 0, Integer.MAX_VALUE); + + private final SocketHint minPerimeterHint = + SocketHints.Inputs.createNumberSpinnerSocketHint("Min Perimeter", 0, 0, Integer.MAX_VALUE); + + + private final InputSocket contoursSocket; + private final InputSocket minAreaSocket; + private final InputSocket minPerimeterSocket; + + private final OutputSocket outputSocket; + + @SuppressWarnings("JavadocMethod") + public SimpleFilterContoursOperation( + InputSocket.Factory inputSocketFactory, OutputSocket.Factory outputSocketFactory) { + this.contoursSocket = inputSocketFactory.create(contoursHint); + this.minAreaSocket = inputSocketFactory.create(minAreaHint); + this.minPerimeterSocket = inputSocketFactory.create(minPerimeterHint); + + this.outputSocket = outputSocketFactory.create(contoursHint); + } + + @Override + public List getInputSockets() { + return ImmutableList.of( + contoursSocket, + minAreaSocket, + minPerimeterSocket + ); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of( + outputSocket + ); + } + + @Override + @SuppressWarnings("unchecked") + public void perform() { + final InputSocket inputSocket = contoursSocket; + final double minArea = minAreaSocket.getValue().get().doubleValue(); + final double minPerimeter = minPerimeterSocket.getValue().get().doubleValue(); + + + final MatVector inputContours = inputSocket.getValue().get().getContours(); + final MatVector outputContours = new MatVector(inputContours.size()); + + // Add contours from the input vector to the output vector only if they pass all of the + // criteria (minimum area, minimum perimeter) + int filteredContourCount = 0; + for (int i = 0; i < inputContours.size(); i++) { + final Mat contour = inputContours.get(i); + + final double area = contourArea(contour); + if (area < minArea) { + continue; + } + if (arcLength(contour, true) < minPerimeter) { + continue; + } + outputContours.put(filteredContourCount++, contour); + } + + outputContours.resize(filteredContourCount); + + outputSocket.setValue(new ContoursReport( + outputContours, + inputSocket.getValue().get().getRows(), + inputSocket.getValue().get().getCols())); + } +} diff --git a/core/src/test/resources/edu/wpi/grip/projects/testALL.grip b/core/src/test/resources/edu/wpi/grip/projects/testALL.grip index 092c0c3ee9..8269b09ac1 100644 --- a/core/src/test/resources/edu/wpi/grip/projects/testALL.grip +++ b/core/src/test/resources/edu/wpi/grip/projects/testALL.grip @@ -108,27 +108,48 @@ - + 2.0 - 3.0 + 10000.0 - 1.0 + 3.0 1000.0 - - 3.0 + + true - 20.0 + 1.0 + 10000.0 + + + 3.0 + + + 10000.0 + + + 20.0 + + + 20.0 + + + 20.0 + + + 20.0 + + 0 100