Skip to content

Commit 03672e4

Browse files
authored
Merge pull request #39 from nut-tree/feature/38/split_image_processing_and_matching
Closes #38. Split image processing and matching
2 parents 224dd3d + 988a245 commit 03672e4

13 files changed

+262
-125
lines changed

.github/ISSUE_TEMPLATE/enhancement.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
name: Enhancement
3+
about: Suggest a possible enhancement to the project
4+
---
5+
6+
**Short overview**
7+
8+
**Use case**
9+
10+
**Detailed description**
11+
12+
**Additional content**
13+
> Please provide any (mandatory) additional data for your enhancement
12.5 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* A DataSink should provide methods to store data
3+
*
4+
* @interface DataSink
5+
*/
6+
export interface DataSink {
7+
/**
8+
* store will write data to disk
9+
* @param data Data to write
10+
* @param path Absolute output file path
11+
*/
12+
store(data: any, path: string): Promise<any>;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* A DataSource should provide methods to load data
3+
*
4+
* @interface DataSource
5+
*/
6+
export interface DataSource {
7+
/**
8+
* load will load image data from disk
9+
* @param path Absolute path to output file
10+
*/
11+
load(path: string): Promise<any>;
12+
}
Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { Image } from "../../image.class";
21
import { MatchRequest } from "../../match-request.class";
32
import { MatchResult } from "../../match-result.class";
4-
import { Region } from "../../region.class";
53

64
/**
75
* A Finder should provide an abstraction layer to perform
@@ -10,15 +8,6 @@ import { Region } from "../../region.class";
108
* @interface FinderInterface
119
*/
1210
export interface FinderInterface {
13-
/**
14-
* loadImage should allow to load an image from filesystem
15-
*
16-
* @param {string} path The filesystem path to the image
17-
* @returns {*} An image
18-
* @memberof VisionProviderInterface
19-
*/
20-
loadImage(path: string): any;
21-
2211
/**
2312
* findMatch should provide an abstraction to search for an image needle
2413
* in another image haystack
@@ -38,35 +27,4 @@ export interface FinderInterface {
3827
* @memberof FinderInterface
3928
*/
4029
findMatches(matchRequest: MatchRequest): Promise<MatchResult[]>;
41-
42-
/**
43-
* fromImageWithAlphaChannel should provide a way to create a library specific
44-
* image with alpha channel from an abstract Image object holding raw data and image dimension
45-
*
46-
* @param {Image} img The input Image
47-
* @param {Region} [roi] An optional Region to specify a ROI
48-
* @returns {Promise<any>} An image
49-
* @memberof VisionProviderInterface
50-
*/
51-
fromImageWithAlphaChannel(img: Image, roi?: Region): Promise<any>;
52-
53-
/**
54-
* fromImageWithoutAlphaChannel should provide a way to create a library specific
55-
* image without alpha channel from an abstract Image object holding raw data and image dimension
56-
*
57-
* @param {Image} img The input Image
58-
* @param {Region} [roi] An optional Region to specify a ROI
59-
* @returns {Promise<any>} An image
60-
* @memberof VisionProviderInterface
61-
*/
62-
fromImageWithoutAlphaChannel(img: Image, roi?: Region): Promise<any>;
63-
64-
/**
65-
* rgbToGrayScale should provide a way to convert an image from RGB to grayscale
66-
*
67-
* @param {*} img Input image, RGB
68-
* @returns {Promise<any>} Output image, grayscale
69-
* @memberof VisionProviderInterface
70-
*/
71-
rgbToGrayScale(img: any): Promise<any>;
7230
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { resolve } from "path";
2+
import { ImageProcessor } from "./image-processor.class";
3+
import { ImageReader } from "./image-reader.class";
4+
5+
describe("ImageProcessor", () => {
6+
it("should allow to create a cv.Mat from an Image with alpha channel, alpha channel is dropped", async () => {
7+
// GIVEN
8+
const imageReader = new ImageReader();
9+
const imagePath = resolve(__dirname, "./__mocks__/alpha_channel.png");
10+
const image = await imageReader.load(imagePath);
11+
12+
// WHEN
13+
const mat = await ImageProcessor.fromImageWithAlphaChannel(image);
14+
15+
// THEN
16+
expect(image.hasAlphaChannel).toBeTruthy();
17+
expect(mat.channels).toEqual(3);
18+
expect(mat.rows).toEqual(image.height);
19+
expect(mat.cols).toEqual(image.width);
20+
expect(mat.empty).toBeFalsy();
21+
});
22+
23+
it("should allow to create a cv.Mat from an Image without alpha channel", async () => {
24+
// GIVEN
25+
const imageReader = new ImageReader();
26+
const imagePath = resolve(__dirname, "./__mocks__/mouse.png");
27+
const image = await imageReader.load(imagePath);
28+
29+
// WHEN
30+
const mat = await ImageProcessor.fromImageWithoutAlphaChannel(image);
31+
32+
// THEN
33+
expect(image.hasAlphaChannel).toBeFalsy();
34+
expect(mat.channels).toEqual(3);
35+
expect(mat.rows).toEqual(image.height);
36+
expect(mat.cols).toEqual(image.width);
37+
expect(mat.empty).toBeFalsy();
38+
});
39+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as cv from "opencv4nodejs";
2+
import { Image } from "../../image.class";
3+
import { Region } from "../../region.class";
4+
5+
export class ImageProcessor {
6+
/**
7+
* fromImageWithAlphaChannel should provide a way to create a library specific
8+
* image with alpha channel from an abstract Image object holding raw data and image dimension
9+
*
10+
* @param {Image} img The input Image
11+
* @param {Region} [roi] An optional Region to specify a ROI
12+
* @returns {Promise<any>} An image
13+
* @memberof VisionProviderInterface
14+
*/
15+
public static async fromImageWithAlphaChannel(
16+
img: Image,
17+
roi?: Region,
18+
): Promise<cv.Mat> {
19+
const mat = await new cv.Mat(img.data, img.height, img.width, cv.CV_8UC4).cvtColorAsync(cv.COLOR_BGRA2BGR);
20+
if (roi) {
21+
return mat.getRegion(new cv.Rect(roi.left, roi.top, roi.width, roi.height));
22+
} else {
23+
return mat;
24+
}
25+
}
26+
27+
/**
28+
* fromImageWithoutAlphaChannel should provide a way to create a library specific
29+
* image without alpha channel from an abstract Image object holding raw data and image dimension
30+
*
31+
* @param {Image} img The input Image
32+
* @param {Region} [roi] An optional Region to specify a ROI
33+
* @returns {Promise<any>} An image
34+
* @memberof VisionProviderInterface
35+
*/
36+
public static async fromImageWithoutAlphaChannel(
37+
img: Image,
38+
roi?: Region,
39+
): Promise<cv.Mat> {
40+
const mat = new cv.Mat(img.data, img.height, img.width, cv.CV_8UC3);
41+
if (roi) {
42+
return mat.getRegion(new cv.Rect(roi.left, roi.top, roi.width, roi.height));
43+
} else {
44+
return mat;
45+
}
46+
}
47+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as path from "path";
2+
import { ImageReader } from "./image-reader.class";
3+
4+
describe("Image loader", () => {
5+
it("should resolve to a non-empty Mat on successful load", async () => {
6+
// GIVEN
7+
const SUT = new ImageReader();
8+
const imagePath = path.resolve(__dirname, "./__mocks__/mouse.png");
9+
10+
// WHEN
11+
const result = await SUT.load(imagePath);
12+
13+
// THEN
14+
expect(result.height).toBeGreaterThan(0);
15+
expect(result.width).toBeGreaterThan(0);
16+
});
17+
18+
it("loadImage should reject on unsuccessful load", async () => {
19+
// GIVEN
20+
const SUT = new ImageReader();
21+
const imagePath = "./__mocks__/foo.png";
22+
23+
// WHEN
24+
const call = SUT.load;
25+
26+
// THEN
27+
await expect(call(imagePath)).rejects.toEqual(`Failed to load image from '${imagePath}'`);
28+
});
29+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as cv from "opencv4nodejs";
2+
import { Image } from "../../image.class";
3+
import { DataSource } from "./data-source.interface";
4+
5+
export class ImageReader implements DataSource {
6+
public async load(path: string): Promise<Image> {
7+
return new Promise<Image>(async (resolve, reject) => {
8+
try {
9+
const image = await cv.imreadAsync(path, cv.IMREAD_UNCHANGED);
10+
resolve(new Image(image.cols, image.rows, image.getData(), image.channels));
11+
} catch (e) {
12+
reject(`Failed to load image from '${path}'`);
13+
}
14+
});
15+
}
16+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { existsSync, unlinkSync } from "fs";
2+
import { resolve } from "path";
3+
import { ImageReader } from "./image-reader.class";
4+
import { ImageWriter } from "./image-writer.class";
5+
6+
const INPUT_PATH = resolve(__dirname, "./__mocks__/mouse.png");
7+
const OUTPUT_PATH_PNG = resolve(__dirname, "./__mocks__/output.png");
8+
const OUTPUT_PATH_JPG = resolve(__dirname, "./__mocks__/output.jpg");
9+
10+
beforeEach(() => {
11+
for (const file of [OUTPUT_PATH_JPG, OUTPUT_PATH_PNG]) {
12+
if (existsSync(file)) {
13+
unlinkSync(file);
14+
}
15+
}
16+
});
17+
18+
describe.each([[OUTPUT_PATH_PNG], [OUTPUT_PATH_JPG]])(
19+
"Image writer", (outputPath) => {
20+
test("should allow to store image data to disk", async () => {
21+
// GIVEN
22+
const imageReader = new ImageReader();
23+
const image = await imageReader.load(INPUT_PATH);
24+
const imageWriter = new ImageWriter();
25+
26+
// WHEN
27+
await imageWriter.store(image, outputPath);
28+
29+
// THEN
30+
expect(existsSync(outputPath)).toBeTruthy();
31+
});
32+
});

0 commit comments

Comments
 (0)