文件
PRA-only-demo/packages/rpa-core/test/capabilities/vision-match.test.ts
T

84 行
4.0 KiB
TypeScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
import { describe, expect, it } from "vitest";
import { PNG } from "pngjs";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { findImage } from "../../src/capabilities/vision";
function makeScreen(width: number, height: number, patch: { x: number; y: number; w: number; h: number }): PNG {
const png = new PNG({ width, height });
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const inPatch = x >= patch.x && x < patch.x + patch.w && y >= patch.y && y < patch.y + patch.h;
// 背景: 平滑斜向渐变(贴近真实界面); 补丁: 纯红色
png.data[i] = inPatch ? 220 : (x + y) % 90;
png.data[i + 1] = inPatch ? 30 : (x + y + 30) % 90;
png.data[i + 2] = inPatch ? 40 : (x + y + 60) % 90;
png.data[i + 3] = 255;
}
}
return png;
}
function crop(source: PNG, x: number, y: number, w: number, h: number): PNG {
const out = new PNG({ width: w, height: h });
PNG.bitblt(source, out, x, y, w, h, 0, 0);
return out;
}
describe("findImage 匹配质量", () => {
it("相似度0.9时精确命中模板位置且不误报", async () => {
const patch = { x: 123, y: 77, w: 48, h: 36 };
const screen = makeScreen(400, 300, patch);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vision-"));
const screenPath = path.join(dir, "screen.png");
const templatePath = path.join(dir, "template.png");
fs.writeFileSync(screenPath, PNG.sync.write(screen));
fs.writeFileSync(templatePath, PNG.sync.write(crop(screen, patch.x, patch.y, patch.w, patch.h)));
// 相似度 0.9 → 最大差异 0.1
const result = await findImage(templatePath, screenPath, 0.1);
expect(result).not.toBeNull();
// 采样步长落在整数边界时, 相邻1px偏移可能同样满分, 允许±1px
expect(Math.abs(result!.x - patch.x)).toBeLessThanOrEqual(1);
expect(Math.abs(result!.y - patch.y)).toBeLessThanOrEqual(1);
expect(result!.score).toBeGreaterThanOrEqual(0.9);
// 无该补丁的屏幕应返回 null 而非乱命中
const other = makeScreen(400, 300, { x: 0, y: 0, w: 1, h: 1 });
const otherPath = path.join(dir, "other.png");
fs.writeFileSync(otherPath, PNG.sync.write(other));
const miss = await findImage(templatePath, otherPath, 0.1);
expect(miss).toBeNull();
});
it("模板被缩放(1.25倍)时按尺度兜底命中", async () => {
const patch = { x: 200, y: 120, w: 60, h: 40 };
const screen = makeScreen(500, 350, patch);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vision-"));
const screenPath = path.join(dir, "screen.png");
fs.writeFileSync(screenPath, PNG.sync.write(screen));
// 模板取自 DIP 截图(比物理屏小1.25倍): 把补丁缩小1/1.25后存为模板
const { createCanvas, loadImage } = await import("@napi-rs/canvas");
const small = crop(screen, patch.x, patch.y, patch.w, patch.h);
const srcCanvas = createCanvas(small.width, small.height);
const srcCtx = srcCanvas.getContext("2d");
const imgData = srcCtx.createImageData(small.width, small.height);
imgData.data.set(small.data);
srcCtx.putImageData(imgData, 0, 0);
const dstCanvas = createCanvas(Math.round(small.width / 1.25), Math.round(small.height / 1.25));
const dstCtx = dstCanvas.getContext("2d");
dstCtx.drawImage(srcCanvas, 0, 0, dstCanvas.width, dstCanvas.height);
// canvas → pngjs PNG(PNG.sync.write 只认 pngjs 对象)
const scaledData = dstCtx.getImageData(0, 0, dstCanvas.width, dstCanvas.height);
const templatePng = new PNG({ width: dstCanvas.width, height: dstCanvas.height });
templatePng.data = Buffer.from(scaledData.data);
const templatePath = path.join(dir, "template-small.png");
fs.writeFileSync(templatePath, PNG.sync.write(templatePng));
const result = await findImage(templatePath, screenPath, 0.1, [1.25, 1 / 1.25]);
expect(result).not.toBeNull();
expect(Math.abs(result!.x - patch.x)).toBeLessThanOrEqual(3);
expect(Math.abs(result!.y - patch.y)).toBeLessThanOrEqual(3);
void loadImage;
});
});