diff --git a/.gitattributes b/.gitattributes index 8af972c..6331761 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /gradlew text eol=lf *.bat text eol=crlf *.jar binary +* text=auto diff --git a/build.gradle.kts b/build.gradle.kts index d393c7f..13ee80b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,10 +7,7 @@ plugins { } group = "com.chromascape" -version = "0.4.0-SNAPSHOT" - -// Customize build directories - put DLLs in build/dist -layout.buildDirectory.set(file("build")) +version = "0.4.0" java { toolchain { @@ -83,76 +80,3 @@ spotless { tasks.named("check") { dependsOn("spotlessApply", "spotlessCheck", "checkstyleMain") } - -// Windows-only native build configuration -val isWindows = org.gradle.internal.os.OperatingSystem.current().isWindows - -// Copy prebuilt DLLs to build/dist folder -val copyNativeLibraries by tasks.registering(Copy::class) { - group = "native" - description = "Copy prebuilt native libraries to build/dist" - - // Only run on Windows - onlyIf { - isWindows - } - - // Check if we have prebuilt libraries - onlyIf { - val kInputExists = file("third_party/KInput/KInput/KInput/bin/Release/KInput.dll").exists() - val kInputCtrlExists = file("third_party/KInput/KInput/KInputCtrl/bin/Release/KInputCtrl.dll").exists() - - kInputExists && kInputCtrlExists - } - - doFirst { - // Ensure build/dist directory exists - file("build/dist").mkdirs() - } - - // Copy from prebuilt libraries - from("third_party/KInput/KInput/KInput/bin/Release") - from("third_party/KInput/KInput/KInputCtrl/bin/Release") - into("build/dist") - - include("*.dll") -} - -// Note: Removed copyNativeToResources task as the application loads DLLs directly from build/dist -// and doesn't use classpath fallback mechanism - -// Make build depend on native library copying and quality checks -tasks.named("processResources") { - dependsOn(copyNativeLibraries) -} - -tasks.named("build") { - dependsOn(copyNativeLibraries, "check") -} - -tasks.named("jar") { - dependsOn(copyNativeLibraries) -} - -// Custom task to clean .chromascape directory -tasks.register("cleanChromascape") { - group = "cleanup" - description = "Remove the .chromascape directory" - - doLast { - val chromascapeDir = file(".chromascape") - if (chromascapeDir.exists()) { - delete(chromascapeDir) - println("Removed .chromascape directory") - } else { - println(".chromascape directory does not exist") - } - } -} - -// Task to clean everything including .chromascape -tasks.register("cleanAll") { - group = "cleanup" - description = "Clean build artifacts and .chromascape directory" - dependsOn("clean", "cleanChromascape") -} diff --git a/src/main/java/com/chromascape/utils/actions/MouseOver.java b/src/main/java/com/chromascape/utils/actions/MouseOver.java index d6c121c..b0384e6 100644 --- a/src/main/java/com/chromascape/utils/actions/MouseOver.java +++ b/src/main/java/com/chromascape/utils/actions/MouseOver.java @@ -1,85 +1,212 @@ package com.chromascape.utils.actions; -import static org.bytedeco.opencv.global.opencv_core.bitwise_or; -import static org.bytedeco.opencv.global.opencv_core.inRange; -import static org.bytedeco.opencv.global.opencv_imgproc.COLOR_BGR2HSV; -import static org.bytedeco.opencv.global.opencv_imgproc.cvtColor; -import static org.opencv.core.CvType.CV_8UC1; +import static org.bytedeco.opencv.global.opencv_core.CV_8UC1; +import static org.bytedeco.opencv.global.opencv_core.CV_8UC3; import com.chromascape.base.BaseScript; import com.chromascape.utils.core.screen.colour.ColourObj; +import com.chromascape.utils.core.screen.topology.ColourContours; +import com.chromascape.utils.core.screen.topology.TemplateMatching; import com.chromascape.utils.core.screen.window.ScreenManager; import com.chromascape.utils.domain.ocr.Ocr; import java.awt.Rectangle; import java.awt.image.BufferedImage; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; -import org.bytedeco.javacv.Java2DFrameUtils; +import java.util.Set; +import org.bytedeco.javacpp.indexer.UByteIndexer; import org.bytedeco.opencv.opencv_core.Mat; import org.bytedeco.opencv.opencv_core.Scalar; /** - * An actions utility to provide a high level API for MouseOverText. + * An actions utility to provide a high level API for MouseOverText. Allows the user to get the text + * of the MouseOverText zone regardless of colour. * - *
Uses OpenCV to iterate over a list of colours, and collates the resulting image into one - * overall mask. This allows the user to get the text of the whole MouseOverText zone regardless of - * colour. - * - *
Allows the user to grab the MouseOverText immediately as a string, excluding spaces. + *
Allows the user to grab the MouseOverText as a string, excluding spaces.
*/
public class MouseOver {
- /** Colours that exist within the MouseOverText zone. */
- private static final List This method is primarily intended for testing and debugging purposes during development.
*
* @param image The image to display. If the source is an OpenCV {@code Mat}, convert it first
- * using {@link Java2DFrameUtils#toBufferedImage(org.bytedeco.opencv.opencv_core.Mat)}.
+ * using {@code TemplateMatching.matToBufferedImage(Mat)}.
*/
public static void display(BufferedImage image) {
if (frame == null) {
diff --git a/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java b/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
index 2c679ff..8bf69ac 100644
--- a/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
+++ b/src/main/java/com/chromascape/utils/core/screen/topology/ColourContours.java
@@ -14,7 +14,6 @@
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
-import org.bytedeco.javacv.Java2DFrameUtils;
import org.bytedeco.opencv.opencv_core.*;
/**
@@ -96,7 +95,7 @@ public static ChromaObj getChromaObjClosestToCentre(List The method requires both images to have 4 channels (BGRA). If they do not, they are
- * converted internally. The matching ignores fully transparent pixels in the template by applying
- * a mask based on its alpha channel.
- *
* The method returns the bounding rectangle of the best match if its matching score is below
* the given threshold. If no match satisfies the threshold, the method returns {@code null}.
*
- * @param templateImg The template image (smaller), expected as a {@link BufferedImage} in BGRA
- * format or convertible to it.
+ * @param templateImg The template image (smaller), expected as a {@link BufferedImage}.
* @param baseImg The base image (larger) where the template is searched, expected as a {@link
- * BufferedImage} in BGRA format or convertible to it.
- * @param threshold The maximum allowed normalized squared difference score for a valid match.
+ * BufferedImage} in RGB format.
+ * @param threshold The maximum allowed normalised squared difference score for a valid match.
* Lower values mean better matches.
* @return A {@link MatchResult} representing the position and size of the matching area in the
* base image, or {@code null} if no match meets the threshold criteria.
@@ -83,7 +80,7 @@ public static MatchResult match(String templateImg, BufferedImage baseImg, doubl
} else {
cvtColor(template, view, COLOR_BGR2RGB);
}
- ViewportManager.getInstance().updateState(view);
+ ViewportManager.getInstance().updateState(template);
// Release the view Mat immediately as ViewportManager handles the data.
view.release();
@@ -91,7 +88,8 @@ public static MatchResult match(String templateImg, BufferedImage baseImg, doubl
return new MatchResult(null, Double.MAX_VALUE, false, "Template image is empty");
}
- base = Java2DFrameUtils.toMat(baseImg);
+ // Internally swaps channels to from RGBA to BGRA or RGB to BGR
+ base = bufferedImageToMat(baseImg);
if (base.empty()) {
return new MatchResult(null, Double.MAX_VALUE, false, "Base image is empty");
@@ -177,8 +175,7 @@ public static Mat loadMatFromResource(String resourcePath) throws IOException {
throw new IllegalArgumentException("Resource not found: " + resourcePath);
}
- // Create a temp file to write the resource contents (OpenCV imread needs a file
- // path)
+ // Create a temp file to write the resource contents
Path tempFile = Files.createTempFile("opencv-temp-", ".png");
tempFile.toFile().deleteOnExit();
@@ -194,4 +191,100 @@ public static Mat loadMatFromResource(String resourcePath) throws IOException {
return mat;
}
+
+ /**
+ * Converts a Java {@link BufferedImage} into an OpenCV {@link Mat} object. To ensure
+ * compatibility with standard OpenCV processing pipeline expectations, this method forces a
+ * standardisation step. It draws the input image onto a fresh canvas explicitly formatted as
+ * {@code BufferedImage.TYPE_3BYTE_BGR}.
+ *
+ * @param image the source {@code BufferedImage} to convert
+ * @return a {@code Mat} containing the BGR pixel data of the image, or an empty {@code Mat} if
+ * the input image is null
+ */
+ public static Mat bufferedImageToMat(BufferedImage image) {
+ if (image == null) {
+ return new Mat();
+ }
+ // Convert ARGB/GRAY images to BGR for standardisation
+ BufferedImage bgrImage =
+ new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
+ Graphics2D g = bgrImage.createGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ // Extract data
+ byte[] cleanData = ((DataBufferByte) bgrImage.getRaster().getDataBuffer()).getData();
+ // Create mat of correct format and size
+ Mat mat = new Mat(bgrImage.getHeight(), bgrImage.getWidth(), CV_8UC3);
+ // Put data into mat
+ mat.data().put(cleanData);
+ return mat;
+ }
+
+ /**
+ * Converts an OpenCV {@link Mat} object into a Java {@link BufferedImage}.
+ *
+ * This method dynamically uses the number of {@code sourceMat.channels()} to create output
+ * mats:
+ *
+ *
+ *
+ *
+ * @param mat the source OpenCV matrix to convert; may be uncontinuous, but must not be null or
+ * empty
+ * @return a {@code BufferedImage} matching the dimensions and colour depth of the input, or
+ * {@code null} if the input matrix is null or empty
+ * @throws IllegalArgumentException if the matrix has an unsupported number of channels (e.g., 2
+ * channels)
+ */
+ public static BufferedImage matToBufferedImage(Mat mat) {
+ if (mat == null || mat.empty()) {
+ return null;
+ }
+
+ Mat sourceMat = mat.isContinuous() ? mat : mat.clone();
+
+ byte[] sourcePixels = new byte[sourceMat.cols() * sourceMat.rows() * sourceMat.channels()];
+ sourceMat.data().get(sourcePixels);
+
+ BufferedImage image;
+ byte[] targetPixels;
+
+ if (sourceMat.channels() == 3) {
+ image = new BufferedImage(sourceMat.cols(), sourceMat.rows(), BufferedImage.TYPE_3BYTE_BGR);
+ targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+ System.arraycopy(sourcePixels, 0, targetPixels, 0, sourcePixels.length);
+
+ } else if (sourceMat.channels() == 4) {
+
+ image = new BufferedImage(sourceMat.cols(), sourceMat.rows(), BufferedImage.TYPE_4BYTE_ABGR);
+ targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+
+ for (int i = 0; i < sourcePixels.length; i += 4) {
+ targetPixels[i] = sourcePixels[i + 3];
+ targetPixels[i + 1] = sourcePixels[i];
+ targetPixels[i + 2] = sourcePixels[i + 1];
+ targetPixels[i + 3] = sourcePixels[i + 2];
+ }
+
+ } else if (sourceMat.channels() == 1) {
+ image = new BufferedImage(sourceMat.cols(), sourceMat.rows(), BufferedImage.TYPE_BYTE_GRAY);
+ targetPixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
+ System.arraycopy(sourcePixels, 0, targetPixels, 0, sourcePixels.length);
+
+ } else {
+ throw new IllegalArgumentException("Unsupported channel count: " + sourceMat.channels());
+ }
+
+ if (!sourceMat.isContinuous() && sourceMat != mat) {
+ sourceMat.release();
+ }
+
+ return image;
+ }
}
diff --git a/src/main/java/com/chromascape/utils/core/screen/window/ScreenManager.java b/src/main/java/com/chromascape/utils/core/screen/window/ScreenManager.java
index ada48b9..f92d768 100644
--- a/src/main/java/com/chromascape/utils/core/screen/window/ScreenManager.java
+++ b/src/main/java/com/chromascape/utils/core/screen/window/ScreenManager.java
@@ -21,8 +21,6 @@ public class ScreenManager {
private static RemoteInput remoteInput;
- private static Pointer screenBuffer = null;
-
/**
* Captures a {@link Rectangle} region on the client screen, intended to be used when
* screenshotting zones for template matching and or colour extraction.
@@ -54,12 +52,13 @@ public static synchronized BufferedImage captureWindow() {
return null;
}
- if (screenBuffer == null) {
- screenBuffer = remoteInput.getImageBuffer();
+ Pointer currentScreenBuffer = remoteInput.getImageBuffer();
+ if (currentScreenBuffer == null) {
+ return null;
}
int bufferSize = width * height * 4;
- byte[] data = screenBuffer.getByteArray(0, bufferSize);
+ byte[] data = currentScreenBuffer.getByteArray(0, bufferSize);
return createBufferedImage(data, width, height);
}
@@ -68,7 +67,7 @@ public static synchronized BufferedImage captureWindow() {
* Internal helper to create a buffered image from a C++ style byte array of pixels in BGRA
* format.
*
- * @param pixels The byte array of pixel data in BGRA format
+ * @param pixels The byte array of pixel data in [B, G, R, A] format
* @param width The width of the client in pixels
* @param height The height of the client in pixels
* @return A {@link BufferedImage} representing the image
@@ -77,15 +76,15 @@ private static BufferedImage createBufferedImage(byte[] pixels, int width, int h
DataBufferByte buffer = new DataBufferByte(pixels, pixels.length);
WritableRaster raster =
Raster.createInterleavedRaster(
- buffer, width, height, width * 4, 4, new int[] {2, 1, 0, 3}, null);
+ buffer, width, height, width * 4, 4, new int[] {2, 1, 0}, null);
ColorModel cm =
new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB),
- new int[] {8, 8, 8, 8},
- true,
+ new int[] {8, 8, 8},
+ false,
false,
- Transparency.TRANSLUCENT,
+ Transparency.OPAQUE,
DataBuffer.TYPE_BYTE);
return new BufferedImage(cm, raster, false, null);
diff --git a/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java b/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java
index 14e7701..5b36be7 100644
--- a/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java
+++ b/src/main/java/com/chromascape/utils/domain/ocr/Ocr.java
@@ -12,6 +12,7 @@
import com.chromascape.utils.core.screen.colour.ColourObj;
import com.chromascape.utils.core.screen.topology.ColourContours;
+import com.chromascape.utils.core.screen.topology.TemplateMatching;
import com.chromascape.utils.core.screen.window.ScreenManager;
import com.chromascape.utils.domain.zones.MaskZones;
import java.awt.Rectangle;
@@ -30,7 +31,6 @@
import java.util.Objects;
import javax.imageio.ImageIO;
import org.bytedeco.javacpp.DoublePointer;
-import org.bytedeco.javacv.Java2DFrameUtils;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Point;
import org.bytedeco.opencv.opencv_core.Rect;
@@ -130,7 +130,7 @@ private static void processFontFile(String path, String fileName, Map