diff --git a/core/src/main/java/edu/wpi/grip/core/operations/composite/CropOperation.java b/core/src/main/java/edu/wpi/grip/core/operations/composite/CropOperation.java new file mode 100644 index 0000000000..d432526273 --- /dev/null +++ b/core/src/main/java/edu/wpi/grip/core/operations/composite/CropOperation.java @@ -0,0 +1,129 @@ +package edu.wpi.grip.core.operations.composite; + +import edu.wpi.grip.annotation.operation.Description; +import edu.wpi.grip.annotation.operation.OperationCategory; +import edu.wpi.grip.core.MatWrapper; +import edu.wpi.grip.core.Operation; +import edu.wpi.grip.core.sockets.InputSocket; +import edu.wpi.grip.core.sockets.OutputSocket; +import edu.wpi.grip.core.sockets.SocketHints; + +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; + +import java.util.List; + +import static org.bytedeco.javacpp.opencv_core.Rect; + + +/** + * Crop an image to an exact width and height using one of several origin modes. Cropping + * images down can be a useful optimization. + */ +@Description(name = "Crop", + summary = "Crop an image to an exact size", + category = OperationCategory.IMAGE_PROCESSING, + iconName = "crop") +public class CropOperation implements Operation { + + private final InputSocket inputSocket; + private final InputSocket xSocket; + private final InputSocket ySocket; + + private final InputSocket widthSocket; + private final InputSocket heightSocket; + private final InputSocket originSocket; + + private final OutputSocket outputSocket; + + @Inject + @SuppressWarnings("JavadocMethod") + public CropOperation(InputSocket.Factory inputSocketFactory, OutputSocket.Factory + outputSocketFactory) { + this.inputSocket = inputSocketFactory.create(SocketHints + .createImageSocketHint("Input")); + this.xSocket = inputSocketFactory.create(SocketHints.Inputs + .createNumberSpinnerSocketHint("X", 100)); + this.ySocket = inputSocketFactory.create(SocketHints.Inputs + .createNumberSpinnerSocketHint("Y", 100)); + this.widthSocket = inputSocketFactory.create(SocketHints.Inputs + .createNumberSpinnerSocketHint("Width", 50)); + this.heightSocket = inputSocketFactory.create(SocketHints.Inputs + .createNumberSpinnerSocketHint("Height", 50)); + this.originSocket = inputSocketFactory + .create(SocketHints.createEnumSocketHint("Origin", Origin.CENTER)); + + this.outputSocket = outputSocketFactory.create(SocketHints + .createImageSocketHint("Output")); + } + + @Override + public List getInputSockets() { + return ImmutableList.of( + inputSocket, + xSocket, + ySocket, + widthSocket, + heightSocket, + originSocket + ); + } + + @Override + public List getOutputSockets() { + return ImmutableList.of( + outputSocket + ); + } + + @Override + public void perform() { + final MatWrapper input = inputSocket.getValue().get(); + final MatWrapper output = outputSocket.getValue().get(); + final Number x = xSocket.getValue().get(); + final Number y = ySocket.getValue().get(); + final Number width = widthSocket.getValue().get(); + final Number height = heightSocket.getValue().get(); + + final Origin origin = originSocket.getValue().get(); + + final Rect regionOfInterest = new Rect( + x.intValue() + (int) (origin.xOffsetMultiplier * width.intValue()), + y.intValue() + (int) (origin.yOffsetMultiplier * height.intValue()), + width.intValue(), + height.intValue() + ); + + //apply() returns a sub-matrix; It does not modify the input Mat: https://github.com/WPIRoboticsProjects/GRIP/pull/926 + if (input.isCpu()) { + output.set(input.getCpu().apply(regionOfInterest)); + } else { + output.set(input.getGpu().apply(regionOfInterest)); + } + + outputSocket.setValue(output); + } + + private enum Origin { + TOP_LEFT("Top Left", 0, 0), + TOP_RIGHT("Top Right", -1, 0), + BOTTOM_LEFT("Bottom Left", 0, -1), + BOTTOM_RIGHT("Bottom Right", -1, -1), + CENTER("Center", -.5, -.5); + + final String label; + final double xOffsetMultiplier; + final double yOffsetMultiplier; + + Origin(String label, double xOffsetMultiplier, double yOffsetMultiplier) { + this.label = label; + this.xOffsetMultiplier = xOffsetMultiplier; + this.yOffsetMultiplier = yOffsetMultiplier; + } + + @Override + public String toString() { + return label; + } + } +} diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/macros.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/macros.vm index aa0660d0b3..c7e74cdf86 100644 --- a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/macros.vm +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/macros.vm @@ -14,7 +14,8 @@ int#elseif($type.equalsIgnoreCase("MaskSize")) std::string#elseif($type.equalsIgnoreCase("Interpolation")) int#elseif($type.equalsIgnoreCase("FlipCode")) FlipCode#elseif($type.equalsIgnoreCase("List")) -List#else +List#elseif($type.equalsIgnoreCase("Origin")) +Origin#else cv::$type#end#end #macro(funPassType $baseType) @@ -68,7 +69,7 @@ $name(#foreach($inp in $step.getInputs())#param($inp $names[$count])#set($count int ${tMeth.name($input.name())} = $input.value(); // ENUM $input.type() #elseif ($input.type().equals("MaskSize") ) std::string ${tMeth.name($input.name())} = "$input.value()"; -#elseif ($input.type().equals("FlipCode") || $input.type().equals("BlurType") ) +#elseif ($input.type().equals("FlipCode") || $input.type().equals("BlurType") || $input.type().equals("Origin")) $input.type() ${tMeth.name($input.name())} = $input.type()::#cvVal($input.value()); #elseif ($input.type().contains("Type") || $input.type().equals("Interpolation")) int ${tMeth.name($input.name())} = #cvVal($input.value()); @@ -100,7 +101,12 @@ cv::INTER_AREA#elseif($value.equals("Box Blur")) BOX#elseif($value.equals("Gaussian Blur")) GAUSSIAN#elseif($value.equals("Median Filter")) MEDIAN#elseif($value.equals("Bilateral Filter")) -BILATERAL#else +BILATERAL#elseif($value.equals("Top Left")) +TOP_LEFT#elseif($value.equals("Top Right")) +TOP_RIGHT#elseif($value.equals("Bottom Left")) +BOTTOM_LEFT#elseif($value.equals("Bottom Right")) +BOTTOM_RIGHT#elseif($value.equals("Center")) +CENTER#else $value#end#end #macro(enumType $uniStep) @@ -113,6 +119,14 @@ $value#end#end enum BlurType { BOX, GAUSSIAN, MEDIAN, BILATERAL }; +#elseif($uniStep.name().equalsIgnoreCase("Crop")) +/** +* A representation of the different origins that can be used. +* +*/ +enum Origin { + TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER +}; #elseif($name.contains("Flip")) /** * Code used for CV_flip. diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/operations/Crop.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/operations/Crop.vm new file mode 100644 index 0000000000..9facbc2a8a --- /dev/null +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/cpp/operations/Crop.vm @@ -0,0 +1,40 @@ + /** + * Crops an image. + * + * @param input The image on which to perform the crop. + * @param x The x (horiontal) location of the crop. + * @param y The y (vertical) location of the crop. + * @param width The width(horizontal length) of the crop. + * @param height The height(vertical length) of the crop. + * @param origin The Origin of the crop. + * @param output The image in which to store the output. + */ + void $className::#func($step, ["input", "x", "y", "width", "height", "origin", "output"]) { + double xOffsetMultiplier = 0; + double yOffsetMultiplier = 0; + switch(origin) { + case TOP_RIGHT: + xOffsetMultiplier = -1; + break; + case BOTTOM_LEFT: + yOffsetMultiplier = -1; + break; + case BOTTOM_RIGHT: + xOffsetMultiplier = -1; + yOffsetMultiplier = -1; + break; + case CENTER: + xOffsetMultiplier = -.5; + yOffsetMultiplier = -.5; + break; + default: //origin == TOP_LEFT + break; + } + cv::Rect regionOfInterest = cv::Rect( + (int) (x + xOffsetMultiplier * width), + (int) (y + yOffsetMultiplier * height), + (int) width, + (int) height + ); + output = input(regionOfInterest); + } \ No newline at end of file diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/macros.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/macros.vm index 918731f12e..d84c6caf68 100644 --- a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/macros.vm +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/macros.vm @@ -24,7 +24,7 @@ Ref<$obj>#end int ${tMeth.name($input.name())} = Imgproc.$input.value(); #elseif ($input.type().equals("MaskSize")) int ${tMeth.name($input.name())} = $input.value().substring(0,1); -#elseif ($input.type().equals("BlurType")) +#elseif ($input.type().equals("BlurType") || $input.type().equals("Origin")) $input.type() ${tMeth.name($input.name())} = ${input.type()}.get("${input.value()}"); #elseif ($input.type().contains("Type") || $input.type().equals("Interpolation")) int ${tMeth.name($input.name())} = #enum($input.value()); diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/operations/Crop.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/operations/Crop.vm new file mode 100644 index 0000000000..a17d16a5c6 --- /dev/null +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/java/operations/Crop.vm @@ -0,0 +1,62 @@ + /** + * An indication of which type of which point on the box is used as the origin. + * Choices are TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER + */ + enum Origin { + TOP_LEFT("Top Left", 0, 0), + TOP_RIGHT("Top Right", -1, 0), + BOTTOM_LEFT("Bottom Left", 0, -1), + BOTTOM_RIGHT("Bottom Right", -1, -1), + CENTER("Center", -.5, -.5); + + private final String label; + private final double xOffsetMultiplier; + private final double yOffsetMultiplier; + + Origin(String label, double xOffsetMultiplier, double yOffsetMultiplier) { + this.label = label; + this.xOffsetMultiplier = xOffsetMultiplier; + this.yOffsetMultiplier = yOffsetMultiplier; + } + + public static Origin get(String label){ + switch(label){ + case "Top Left": + return TOP_LEFT; + case "Top Right": + return TOP_RIGHT; + case "Bottom Left": + return BOTTOM_LEFT; + case "Bottom Right": + return BOTTOM_RIGHT; + default: + return CENTER; + } + } + + @Override + public String toString() { + return label; + } + } + + /** + * Crops an image. + * @param input The image on which to perform the crop. + * @param x The x (horiontal) location of the crop. + * @param y The y (vertical) location of the crop. + * @param width The width(horizontal length) of the crop. + * @param height The height(vertical length) of the crop. + * @param origin The Origin of the crop. + * @param output The image in which to store the output. + */ + private void $tMeth.name($step.name())(Mat input, double x, double y, double width, double height, Origin origin, Mat output) { + + Rect regionOfInterest = new Rect( + (int) (x + origin.xOffsetMultiplier * width), + (int) (y + origin.yOffsetMultiplier * height), + (int) width, + (int) height + ); + input.submat(regionOfInterest).copyTo(output); + } \ No newline at end of file diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/enums/Origin.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/enums/Origin.vm new file mode 100644 index 0000000000..fe39261d86 --- /dev/null +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/enums/Origin.vm @@ -0,0 +1 @@ +Origin = Enum('Origin', 'Top_Left Top_Right Bottom_Left Bottom_Right Center') diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/macros.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/macros.vm index 691963d192..8fb88d3037 100644 --- a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/macros.vm +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/macros.vm @@ -4,7 +4,7 @@ self.$tMeth.name($inp.name())#end #macro(newInput $inp) #if($inp.hasValue()) -#if($inp.type().equals("BlurType") || $inp.type().equals("FlipCode")) +#if($inp.type().equals("BlurType") || $inp.type().equals("FlipCode") || $inp.type().equals("Origin")) #input($inp) = ${inp.type()}.$inp.value().replaceAll(' ','_')#elseif($inp.type().equals("MaskSize")) #input($inp) = $inp.value().substring(0,1)#elseif($inp.value().contains("source")) #input($inp) = $inp.value()#elseif ($inp.type().contains("Enum") || diff --git a/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/operations/Crop.vm b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/operations/Crop.vm new file mode 100644 index 0000000000..00bad13c09 --- /dev/null +++ b/ui/src/main/resources/edu/wpi/grip/ui/codegeneration/python/operations/Crop.vm @@ -0,0 +1,38 @@ +#needs("Origin") + @staticmethod + def $tMeth.name($step.name())(src, x, y, width, height, origin): + """Crops an image. + Args: + src: The source mat (numpy.ndarray). + x: The x (horiontal) location of the crop. + y: The y (vertical) location of the crop. + width: The width(horizontal length) of the crop. + height: The height(vertical length) of the crop. + origin: The Origin of the crop. + Returns: + A numpy.ndarray that has been cropped. + """ + + # origin is Top_Left + x_offset_multiplier = 0 + y_offset_multiplier = 0 + + if origin is Origin.Top_Right: + x_offset_multiplier = -1 + elif origin is Origin.Bottom_Left: + y_offset_multiplier = -1 + elif origin is Origin.Bottom_Right: + x_offset_multiplier = -1 + y_offset_multiplier = -1 + elif origin is Origin.Center: + x_offset_multiplier = -.5 + y_offset_multiplier = -.5 + + # Flooring to keep consistency with Java + x_start = int(x + x_offset_multiplier * width) + x_end = int(x_start + width) + + y_start = int(y + y_offset_multiplier * height) + y_end = int(y_start + height) + + return src[y_start: y_end, x_start : x_end] diff --git a/ui/src/main/resources/edu/wpi/grip/ui/icons/crop.png b/ui/src/main/resources/edu/wpi/grip/ui/icons/crop.png new file mode 100644 index 0000000000..beb3a10101 Binary files /dev/null and b/ui/src/main/resources/edu/wpi/grip/ui/icons/crop.png differ