testdriverai 1.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/index.js +863 -0
- package/lib/analytics.js +11 -0
- package/lib/commander.js +152 -0
- package/lib/commands.js +535 -0
- package/lib/focus-application.js +50 -0
- package/lib/focusWindow.ps1 +46 -0
- package/lib/generator.js +76 -0
- package/lib/history.js +38 -0
- package/lib/init.js +137 -0
- package/lib/keymap.js +125 -0
- package/lib/logger.js +114 -0
- package/lib/notify.js +18 -0
- package/lib/parser.js +100 -0
- package/lib/redraw.js +51 -0
- package/lib/sdk.js +113 -0
- package/lib/session.js +10 -0
- package/lib/speak.js +14 -0
- package/lib/system.js +112 -0
- package/lib/valid-version.js +19 -0
- package/package.json +49 -3
- package/postinstall.js +20 -0
- package/subimage/index.js +70 -0
- package/subimage/opencv.js +72 -0
package/lib/parser.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// parses markdown content to find code blocks, and then extracts yaml from those code blocks
|
|
2
|
+
const Parser = require('markdown-parser');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
|
|
5
|
+
let parser = new Parser();
|
|
6
|
+
|
|
7
|
+
// use markdown parser to find code blocks within AI response
|
|
8
|
+
const findCodeBlocks = (markdownContent) => {
|
|
9
|
+
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
parser.parse(markdownContent, (err, result) => {
|
|
12
|
+
|
|
13
|
+
if (err) {
|
|
14
|
+
return reject(err);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let codes = result.codes.filter((code) => {
|
|
18
|
+
return code.code.indexOf('yml') > -1 || code.code.indexOf('yaml') > -1;
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return resolve(codes);
|
|
22
|
+
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// parse the yml from the included codeblock and clean it up
|
|
30
|
+
const getYAMLFromCodeBlock = function(codeblock) {
|
|
31
|
+
|
|
32
|
+
let lines = codeblock.code.split('\n');
|
|
33
|
+
|
|
34
|
+
// if first line is yaml or yml, remove it
|
|
35
|
+
if (lines[0].indexOf('yaml') > -1 || lines[0].indexOf('yml') > -1) {
|
|
36
|
+
lines.shift();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// count the whitespace in each line, and remove the line if it's all whitespace
|
|
40
|
+
lines = lines.filter((line) => {
|
|
41
|
+
return line.trim().length > 0 && line.trim()[0] !== ',';
|
|
42
|
+
// sometimes it produces yaml with breaks, or just a single comma
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// do the actual parsing
|
|
50
|
+
// this library is very strict
|
|
51
|
+
// note that errors are sent to the AI will it may self-heal
|
|
52
|
+
const parseYAML = async function(inputYaml) {
|
|
53
|
+
|
|
54
|
+
let doc = await yaml.load(inputYaml);
|
|
55
|
+
return doc;
|
|
56
|
+
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
findCodeBlocks,
|
|
61
|
+
getYAMLFromCodeBlock,
|
|
62
|
+
getCommands: async function(codeBlock) {
|
|
63
|
+
|
|
64
|
+
const yml = getYAMLFromCodeBlock(codeBlock);
|
|
65
|
+
let yamlArray = await parseYAML(yml);
|
|
66
|
+
|
|
67
|
+
let steps = yamlArray?.steps;
|
|
68
|
+
|
|
69
|
+
if (steps) {
|
|
70
|
+
|
|
71
|
+
let commands = [];
|
|
72
|
+
|
|
73
|
+
// combine them all as if they were a single step
|
|
74
|
+
steps.forEach((s) => {
|
|
75
|
+
commands = commands.concat(s.commands);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// filter undefined values
|
|
79
|
+
commands = commands.filter((r) => {
|
|
80
|
+
return r;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!commands.length) {
|
|
84
|
+
throw new Error('No actions found in yaml. Individual commands must be under the `commands` key.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return commands;
|
|
88
|
+
|
|
89
|
+
} else {
|
|
90
|
+
let commands = yamlArray?.commands;
|
|
91
|
+
|
|
92
|
+
if (!commands?.length) {
|
|
93
|
+
throw new Error('No actions found in yaml. Individual commands must be under the `commands` key.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return commands;
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/lib/redraw.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const {captureScreenPNG} = require('./system');
|
|
2
|
+
|
|
3
|
+
const Jimp = require('jimp');
|
|
4
|
+
|
|
5
|
+
async function compareImages(image1Url, image2Url) {
|
|
6
|
+
const image1 = await Jimp.read(image1Url);
|
|
7
|
+
const image2 = await Jimp.read(image2Url);
|
|
8
|
+
|
|
9
|
+
// Perceived distance
|
|
10
|
+
const distance = Jimp.distance(image1, image2);
|
|
11
|
+
// Pixel difference
|
|
12
|
+
const diff = Jimp.diff(image1, image2);
|
|
13
|
+
|
|
14
|
+
if (diff.percent < 0.15) {
|
|
15
|
+
return false;
|
|
16
|
+
} else {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let startImage = null;
|
|
22
|
+
|
|
23
|
+
async function start() {
|
|
24
|
+
startImage = await captureScreenPNG();
|
|
25
|
+
return startImage;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function wait(timeoutMs) {
|
|
29
|
+
return new Promise(async (resolve, reject) => {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
|
|
32
|
+
async function checkCondition() {
|
|
33
|
+
|
|
34
|
+
let nowImage = await captureScreenPNG();
|
|
35
|
+
let result = await compareImages(startImage, nowImage);
|
|
36
|
+
|
|
37
|
+
if (result) {
|
|
38
|
+
resolve('Condition met');
|
|
39
|
+
} else if (Date.now() - startTime >= timeoutMs) {
|
|
40
|
+
resolve('Timeout reached');
|
|
41
|
+
} else {
|
|
42
|
+
setTimeout(checkCondition, 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
checkCondition();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {start, wait};
|
package/lib/sdk.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// custom "sdk" for calling our API
|
|
2
|
+
const root = "http://api.testdriver.ai";
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const session = require('./session')
|
|
6
|
+
const package = require('../package.json');
|
|
7
|
+
const version = package.version;
|
|
8
|
+
|
|
9
|
+
let token = null;
|
|
10
|
+
|
|
11
|
+
let outputError = (e) => {
|
|
12
|
+
|
|
13
|
+
console.log(chalk.red(e.code || e.response.data.code))
|
|
14
|
+
|
|
15
|
+
if (e.response) {
|
|
16
|
+
|
|
17
|
+
console.log(chalk.red(e.response?.status), chalk.red(e.response?.statusText))
|
|
18
|
+
if(e.response.data) { console.log(e.response.data) }
|
|
19
|
+
if(e.response.data.message) { console.log(e.response.data.message) }
|
|
20
|
+
if (e.response.data.problems) {
|
|
21
|
+
console.log('-----')
|
|
22
|
+
console.log(e.response.data.problems.join('\n'))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let auth = async () => {
|
|
28
|
+
|
|
29
|
+
// data.apiKey = process.env.DASHCAM_API_KEY; @todo add-auth
|
|
30
|
+
|
|
31
|
+
if (!data.apiKey) {
|
|
32
|
+
console.log(chalk.red('API key not found. Set DASHCAM_API_KEY in your environment.'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let config = {
|
|
37
|
+
method: 'post',
|
|
38
|
+
maxBodyLength: Infinity,
|
|
39
|
+
url: [root, 'auth/exchange-api-key'].join('/'),
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
},
|
|
43
|
+
data
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// this is a dashcam api url
|
|
47
|
+
try {
|
|
48
|
+
let res = await axios.request(config);
|
|
49
|
+
token = res.data.token;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
outputError(e);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let req = async (path, data) => {
|
|
58
|
+
|
|
59
|
+
// for each value of data, if it is empty remove it
|
|
60
|
+
for (let key in data) {
|
|
61
|
+
if (!data[key]) {
|
|
62
|
+
delete data[key];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
data.session = session.get()?.id;
|
|
67
|
+
data = JSON.stringify(data);
|
|
68
|
+
|
|
69
|
+
let dataCopy = JSON.parse(data);
|
|
70
|
+
delete dataCopy.image
|
|
71
|
+
|
|
72
|
+
let url = [root, 'api', 'v' + version, 'testdriver', path].join('/');
|
|
73
|
+
|
|
74
|
+
let config = {
|
|
75
|
+
method: 'post',
|
|
76
|
+
maxBodyLength: Infinity,
|
|
77
|
+
url,
|
|
78
|
+
headers: {
|
|
79
|
+
'Content-Type': 'application/json',
|
|
80
|
+
// 'Authorization': 'Bearer ' + token @todo add-auth
|
|
81
|
+
},
|
|
82
|
+
data
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let response;
|
|
86
|
+
let redirect = null
|
|
87
|
+
try {
|
|
88
|
+
response = await axios.request(config);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
|
|
91
|
+
if (e.response?.status === 301) {
|
|
92
|
+
config.url = root + e.response.data;
|
|
93
|
+
redirect = config;
|
|
94
|
+
} else {
|
|
95
|
+
outputError(e);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (redirect) {
|
|
101
|
+
response = await axios.request(redirect).catch(e => {
|
|
102
|
+
outputError(e);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!response) {
|
|
107
|
+
return;
|
|
108
|
+
} else {
|
|
109
|
+
return response.data;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {req, auth};
|
package/lib/session.js
ADDED
package/lib/speak.js
ADDED
package/lib/system.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// utilities for getting information about the system
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const screenshot = require('screenshot-desktop')
|
|
6
|
+
const si = require('systeminformation');
|
|
7
|
+
const activeWindow = require('active-win');
|
|
8
|
+
const sharp = require('sharp')
|
|
9
|
+
const robot = require('robotjs');
|
|
10
|
+
|
|
11
|
+
let displayMultiple = 0;
|
|
12
|
+
let primaryDisplay = null;
|
|
13
|
+
|
|
14
|
+
// get the primary display
|
|
15
|
+
// this is the only display we ever target, because fuck it
|
|
16
|
+
// the vm only has one and most people only have one
|
|
17
|
+
const getPrimaryDisplay = async () => {
|
|
18
|
+
|
|
19
|
+
// calculate scaling resolution
|
|
20
|
+
let graphics = await si.graphics();
|
|
21
|
+
let primaryDisplay = graphics.displays.find((display) => display.main == true);
|
|
22
|
+
|
|
23
|
+
return primaryDisplay;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getSystemInformationOsInfo = async() => {
|
|
27
|
+
return await si.osInfo();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// this hepls us understand how to scale things for retina screens
|
|
31
|
+
const calculateDisplayMultiple = async () => {
|
|
32
|
+
let primaryDisplay = await getPrimaryDisplay();
|
|
33
|
+
displayMultiple = primaryDisplay.currentResX / primaryDisplay.resolutionX;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getDisplayMultiple = async () => {
|
|
37
|
+
|
|
38
|
+
if (!displayMultiple) {
|
|
39
|
+
await calculateDisplayMultiple();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return displayMultiple;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const tmpFilename = () => {
|
|
46
|
+
return path.join(os.tmpdir(), `${new Date().getTime() + Math.random()}.png`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// our handy screenshot function
|
|
50
|
+
const captureScreenBase64 = async () => {
|
|
51
|
+
|
|
52
|
+
let primaryDisplay = await getPrimaryDisplay();
|
|
53
|
+
|
|
54
|
+
let step1 = tmpFilename();
|
|
55
|
+
let step2 = tmpFilename();
|
|
56
|
+
|
|
57
|
+
await screenshot({ filename: step1, format: 'png' });
|
|
58
|
+
|
|
59
|
+
// resize to 1:1 px ratio
|
|
60
|
+
await sharp(step1)
|
|
61
|
+
.resize(primaryDisplay.currentResX, primaryDisplay.currentResY)
|
|
62
|
+
.toFile(step2);
|
|
63
|
+
|
|
64
|
+
let image = fs.readFileSync(step2, "base64");
|
|
65
|
+
|
|
66
|
+
return image;
|
|
67
|
+
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const captureScreenPNG = async () => {
|
|
71
|
+
|
|
72
|
+
let step1 = tmpFilename();
|
|
73
|
+
await screenshot({ filename: step1, format: 'png' });
|
|
74
|
+
|
|
75
|
+
return step1;
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const platform = () => {
|
|
80
|
+
let platform = process.platform;
|
|
81
|
+
if (platform === 'darwin') {
|
|
82
|
+
platform = 'mac';
|
|
83
|
+
} else if (platform === 'win32') {
|
|
84
|
+
platform = 'windows';
|
|
85
|
+
} else if (platform === 'linux') {
|
|
86
|
+
platform = 'linux';
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error('Unsupported platform');
|
|
89
|
+
}
|
|
90
|
+
return platform;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// this is the focused window
|
|
94
|
+
const activeWin = async () => {
|
|
95
|
+
return await activeWindow();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const getMousePosition = async () => {
|
|
99
|
+
return await robot.getMousePos();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
captureScreenBase64,
|
|
104
|
+
captureScreenPNG,
|
|
105
|
+
getDisplayMultiple,
|
|
106
|
+
calculateDisplayMultiple,
|
|
107
|
+
getMousePosition,
|
|
108
|
+
primaryDisplay,
|
|
109
|
+
activeWin,
|
|
110
|
+
platform,
|
|
111
|
+
getSystemInformationOsInfo
|
|
112
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const semver = require('semver')
|
|
2
|
+
const package = require('../package.json');
|
|
3
|
+
|
|
4
|
+
// Function to check if the new version's minor version is >= current version's minor version
|
|
5
|
+
module.exports = (inputVersion) => {
|
|
6
|
+
|
|
7
|
+
const currentParsed = semver.parse(package.version);
|
|
8
|
+
const inputParsed = semver.parse(inputVersion.replace('v', ''));
|
|
9
|
+
|
|
10
|
+
if (!currentParsed || !inputParsed) {
|
|
11
|
+
throw new Error('Invalid version format');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Compare major and minor versions
|
|
15
|
+
if (inputParsed.major === currentParsed.major && inputParsed.minor >= currentParsed.minor) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
|
|
4
5
|
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"testdriver": "./index.js"
|
|
8
|
+
},
|
|
5
9
|
"scripts": {
|
|
6
|
-
"
|
|
10
|
+
"start": "node index.js",
|
|
11
|
+
"dev": "DEV=true node index.js",
|
|
12
|
+
"debug": "DEV=true VERBOSE=true node index.js",
|
|
13
|
+
"bundle": "node build.mjs",
|
|
14
|
+
"postinstall": "node postinstall.js"
|
|
7
15
|
},
|
|
8
16
|
"author": "",
|
|
9
17
|
"license": "ISC",
|
|
10
|
-
"
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@electerm/strip-ansi": "^1.0.0",
|
|
20
|
+
"active-win": "^8.2.1",
|
|
21
|
+
"axios": "^1.6.8",
|
|
22
|
+
"chalk": "^4.1.2",
|
|
23
|
+
"cli-progress": "^3.12.0",
|
|
24
|
+
"datadog-winston": "^1.6.0",
|
|
25
|
+
"decompress": "^4.2.1",
|
|
26
|
+
"dotenv": "^16.4.5",
|
|
27
|
+
"jimp": "^0.22.12",
|
|
28
|
+
"js-yaml": "^4.1.0",
|
|
29
|
+
"markdown-parser": "0.0.8",
|
|
30
|
+
"marked": "^12.0.1",
|
|
31
|
+
"marked-terminal": "^7.0.0",
|
|
32
|
+
"node-notifier": "^10.0.1",
|
|
33
|
+
"prompts": "^2.4.2",
|
|
34
|
+
"remark-parse": "^11.0.0",
|
|
35
|
+
"rimraf": "^5.0.5",
|
|
36
|
+
"robotjs": "^0.6.0",
|
|
37
|
+
"say": "^0.16.0",
|
|
38
|
+
"screenshot-desktop": "^1.15.0",
|
|
39
|
+
"semver": "^7.6.2",
|
|
40
|
+
"sharp": "^0.33.4",
|
|
41
|
+
"systeminformation": "^5.22.7",
|
|
42
|
+
"tmp": "^0.2.3",
|
|
43
|
+
"uuid": "^10.0.0",
|
|
44
|
+
"winston": "^3.13.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"esbuild": "0.20.2",
|
|
48
|
+
"esbuild-plugin-fileloc": "^0.0.6",
|
|
49
|
+
"node-addon-api": "^8.0.0",
|
|
50
|
+
"node-gyp": "^10.1.0"
|
|
51
|
+
},
|
|
52
|
+
"optionalDependencies": {
|
|
53
|
+
"@esbuild/linux-x64": "^0.21.5",
|
|
54
|
+
"@img/sharp-libvips-win32-x64": "^1.0.2",
|
|
55
|
+
"@img/sharp-win32-x64": "^0.33.4"
|
|
56
|
+
}
|
|
11
57
|
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
let platform = require('os').platform();
|
|
2
|
+
let exec = require('child_process').exec;
|
|
3
|
+
|
|
4
|
+
if (platform !== 'darwin') {
|
|
5
|
+
console.log('TestDriver Setup: Skipping codesign becasue not on Mac');
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
console.log('TestDriver Setup: Codesigning terminal-notifier.app');
|
|
10
|
+
|
|
11
|
+
let signScript = `codesign --sign - --force --deep node_modules/node-notifier/vendor/mac.noindex/terminal-notifier.app`;
|
|
12
|
+
|
|
13
|
+
exec(signScript, (error, stdout, stderr) => {
|
|
14
|
+
if (error) {
|
|
15
|
+
console.error(`exec error: ${error}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
console.log(`stdout: ${stdout}`);
|
|
19
|
+
console.error(`stderr: ${stderr}`);
|
|
20
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const Jimp = require("jimp");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
cv = require('./opencv.js');
|
|
4
|
+
|
|
5
|
+
async function findTemplateImage(haystack, needle, threshold) {
|
|
6
|
+
try {
|
|
7
|
+
const positions = [];
|
|
8
|
+
|
|
9
|
+
const imageSource = await Jimp.read(path.join(haystack));
|
|
10
|
+
const imageTemplate = await Jimp.read(path.join(needle));
|
|
11
|
+
|
|
12
|
+
const templ = cv.matFromImageData(imageTemplate.bitmap);
|
|
13
|
+
let src = cv.matFromImageData(imageSource.bitmap);
|
|
14
|
+
let processedImage = new cv.Mat();
|
|
15
|
+
let mask = new cv.Mat();
|
|
16
|
+
|
|
17
|
+
cv.matchTemplate(src, templ, processedImage, cv.TM_CCOEFF_NORMED, mask);
|
|
18
|
+
|
|
19
|
+
cv.threshold(
|
|
20
|
+
processedImage,
|
|
21
|
+
processedImage,
|
|
22
|
+
threshold,
|
|
23
|
+
1,
|
|
24
|
+
cv.THRESH_BINARY
|
|
25
|
+
);
|
|
26
|
+
processedImage.convertTo(processedImage, cv.CV_8UC1);
|
|
27
|
+
let contours = new cv.MatVector();
|
|
28
|
+
let hierarchy = new cv.Mat();
|
|
29
|
+
|
|
30
|
+
cv.findContours(
|
|
31
|
+
processedImage,
|
|
32
|
+
contours,
|
|
33
|
+
hierarchy,
|
|
34
|
+
cv.RETR_EXTERNAL,
|
|
35
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < contours.size(); ++i) {
|
|
39
|
+
let [x, y] = contours.get(i).data32S; // Contains the points
|
|
40
|
+
positions.push({
|
|
41
|
+
x,
|
|
42
|
+
y,
|
|
43
|
+
height: templ.rows,
|
|
44
|
+
width: templ.cols,
|
|
45
|
+
centerX: x + templ.cols / 2,
|
|
46
|
+
centerY: y + templ.rows / 2,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
src.delete();
|
|
51
|
+
mask.delete();
|
|
52
|
+
templ.delete();
|
|
53
|
+
|
|
54
|
+
return positions;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('OpenCV threw an error');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onRuntimeInitialized() {
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Finally, load the open.js as before. The function `onRuntimeInitialized` contains our program.
|
|
64
|
+
Module = {
|
|
65
|
+
onRuntimeInitialized,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
findTemplateImage
|
|
70
|
+
}
|