Files

84 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
});
});