Skip to content

Commit bdf3452

Browse files
authored
Merge pull request #81 from nut-tree/feature/68/scaled_search
Closes #68
2 parents 36629e7 + 10afb84 commit bdf3452

19 files changed

+460
-178
lines changed
8.85 KB
Loading
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { lowerBound, upperBound } from "./bound-value.function";
2+
3+
describe("lowerBound function", () => {
4+
it.each([
5+
[5, 10, 1, 1],
6+
[5, 5, 10, 10],
7+
[5, 1, 10, 5],
8+
[0, 0, 0, 0]
9+
])("Input: %f, Boundary: %f, minValue: %f, Expected: %f",
10+
(input: number, boundary: number, minValue: number, expected: number) => {
11+
// WHEN
12+
const result = lowerBound(input, boundary, minValue);
13+
14+
// THEN
15+
expect(result).toBe(expected);
16+
});
17+
});
18+
19+
describe("upperBound function", () => {
20+
it.each([
21+
[5, 10, 1, 5],
22+
[5, 5, 10, 10],
23+
[5, 1, 10, 10],
24+
[5, 5, 5, 5]
25+
])("Input: %f, Boundary: %f, maxValue: %f, Expected: %f",
26+
(input: number, boundary: number, maxValue: number, expected: number) => {
27+
// WHEN
28+
const result = upperBound(input, boundary, maxValue);
29+
30+
// THEN
31+
expect(result).toBe(expected);
32+
});
33+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function lowerBound(value: number, boundary: number, minValue: number): number {
2+
return (value <= boundary) ? minValue : value;
3+
}
4+
5+
export function upperBound(value: number, boundary: number, maxValue: number): number {
6+
return (value >= boundary) ? maxValue : value;
7+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { mockPartial } from "sneer";
2+
import { Image } from "../../image.class";
3+
import { MatchRequest } from "../../match-request.class";
4+
import { Region } from "../../region.class";
5+
import { determineScaledSearchRegion } from "./determine-searchregion.function";
6+
7+
describe("determineSearchRegion", () => {
8+
it("should return a search region adopted to pixel density", () => {
9+
// GIVEN
10+
const imageMock = mockPartial<Image>({
11+
pixelDensity: {
12+
scaleX: 1.5,
13+
scaleY: 2.0
14+
}
15+
});
16+
const needlePath = "/path/to/needle";
17+
const inputSearchRegion = new Region(0, 0, 100, 100);
18+
const expectedSearchRegion = new Region(0, 0, 150, 200);
19+
20+
const matchRequest = new MatchRequest(
21+
imageMock,
22+
needlePath,
23+
inputSearchRegion,
24+
0.99
25+
);
26+
27+
// WHEN
28+
const result = determineScaledSearchRegion(matchRequest);
29+
30+
// THEN
31+
expect(result).toEqual(expectedSearchRegion);
32+
});
33+
34+
it.each([[0, 1], [1, 0]])("should not adjust searchregion for factor 0: scaleX: %i scaleY: %i",
35+
(scaleX: number, scaleY: number) => {
36+
// GIVEN
37+
const imageMock = mockPartial<Image>({
38+
pixelDensity: {
39+
scaleX,
40+
scaleY
41+
}
42+
});
43+
const needlePath = "/path/to/needle";
44+
const inputSearchRegion = new Region(0, 0, 100, 100);
45+
const expectedSearchRegion = new Region(0, 0, 100, 100);
46+
47+
const matchRequest = new MatchRequest(
48+
imageMock,
49+
needlePath,
50+
inputSearchRegion,
51+
0.99
52+
);
53+
54+
// WHEN
55+
const result = determineScaledSearchRegion(matchRequest);
56+
57+
// THEN
58+
expect(result).toEqual(expectedSearchRegion);
59+
});
60+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { MatchRequest } from "../../match-request.class";
2+
import { Region } from "../../region.class";
3+
4+
export function determineScaledSearchRegion(matchRequest: MatchRequest): Region {
5+
const searchRegion = matchRequest.searchRegion;
6+
const scaleX = matchRequest.haystack.pixelDensity.scaleX || 1.0;
7+
const scaleY = matchRequest.haystack.pixelDensity.scaleY || 1.0;
8+
searchRegion.width *= scaleX;
9+
searchRegion.height *= scaleY;
10+
return searchRegion;
11+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as cv from "opencv4nodejs-prebuilt";
2+
import { mockPartial } from "sneer";
3+
import { findEdges } from "./find-edges.function";
4+
5+
describe("findEdges", () => {
6+
it("should convert an image to grayscale and run Canny edge detection", async () => {
7+
// GIVEN
8+
const grayImageMock = mockPartial<cv.Mat>({
9+
cannyAsync: jest.fn()
10+
});
11+
const inputImageMock = mockPartial<cv.Mat>({
12+
cvtColorAsync: jest.fn(() => Promise.resolve(grayImageMock))
13+
});
14+
15+
// WHEN
16+
await findEdges(inputImageMock);
17+
18+
// THEN
19+
expect(inputImageMock.cvtColorAsync).toBeCalledWith(cv.COLOR_BGR2GRAY);
20+
expect(grayImageMock.cannyAsync).toBeCalledWith(50, 200);
21+
});
22+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as cv from "opencv4nodejs-prebuilt";
2+
3+
export async function findEdges(image: cv.Mat): Promise<cv.Mat> {
4+
const gray = await image.cvtColorAsync(cv.COLOR_BGR2GRAY);
5+
return gray.cannyAsync(50, 200);
6+
}

lib/provider/opencv/image-processor.class.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import * as cv from "opencv4nodejs-prebuilt";
22
import { Image } from "../../image.class";
33
import { Region } from "../../region.class";
44

5-
const determineROI = (img: Image, roi: Region): cv.Rect => {
5+
function determineROI(img: Image, roi: Region): cv.Rect {
66
return new cv.Rect(
77
Math.min(Math.max(roi.left, 0), img.width),
88
Math.min(Math.max(roi.top, 0), img.height),
99
Math.min(roi.width, img.width - roi.left),
1010
Math.min(roi.height, img.height - roi.top));
11-
};
11+
}
1212

1313
export class ImageProcessor {
1414
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as cv from "opencv4nodejs-prebuilt";
2+
import { mockPartial } from "sneer";
3+
import { matchImages } from "./match-image.function";
4+
5+
describe("matchImages", () => {
6+
it("should return minLoc position and needle size", async () => {
7+
// GIVEN
8+
const minLocX = 100;
9+
const minLocY = 1000;
10+
const matchMock = mockPartial<cv.Mat>({
11+
minMaxLocAsync: jest.fn(() => Promise.resolve({
12+
maxLoc: new cv.Point2(
13+
200,
14+
2000
15+
),
16+
maxVal: 100,
17+
minLoc: new cv.Point2(
18+
minLocX,
19+
minLocY
20+
),
21+
minVal: 0,
22+
}))
23+
});
24+
const haystackMock = mockPartial<cv.Mat>({
25+
matchTemplateAsync: jest.fn(() => Promise.resolve(matchMock))
26+
});
27+
const needleMock = mockPartial<cv.Mat>({
28+
cols: 123,
29+
rows: 456
30+
});
31+
32+
// WHEN
33+
const result = await matchImages(haystackMock, needleMock);
34+
35+
// THEN
36+
expect(result.location.left).toEqual(minLocX);
37+
expect(result.location.top).toEqual(minLocY);
38+
expect(result.location.width).toEqual(needleMock.cols);
39+
expect(result.location.height).toEqual(needleMock.rows);
40+
});
41+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as cv from "opencv4nodejs-prebuilt";
2+
import { MatchResult } from "../../match-result.class";
3+
import { Region } from "../../region.class";
4+
5+
export async function matchImages(haystack: cv.Mat, needle: cv.Mat): Promise<MatchResult> {
6+
const match = await haystack.matchTemplateAsync(
7+
needle,
8+
cv.TM_SQDIFF_NORMED,
9+
);
10+
const minMax = await match.minMaxLocAsync();
11+
return new MatchResult(
12+
1.0 - minMax.minVal,
13+
new Region(
14+
minMax.minLoc.x,
15+
minMax.minLoc.y,
16+
needle.cols,
17+
needle.rows,
18+
),
19+
);
20+
}

0 commit comments

Comments
 (0)