feat: CV 组件支持可选搜索区域(第一阶段)
- locateTextOnScreen 支持区域参数: 框选截图(imagePath)+屏幕偏移(x,y) 或纯坐标范围过滤
- locateFor 读取 searchRegion/region 参数; parseRegionArg 兼容 JSON 与图片路径
- 框选截图时 OCR 只识别该图, 命中坐标加偏移得到屏幕绝对坐标, 避免同名文本干扰
- pickRegionToField: region 类型字段存完整 {x,y,width,height,imagePath}
- 明日继续: catalog 给 CV 四组件加 searchRegion 字段 + region 分支屏幕框选按钮
This commit is contained in:
parent
c93543be1c
commit
5aa113ea0e
@ -44,28 +44,72 @@ async function grabScreen(imagePath: string): Promise<void> {
|
||||
|
||||
interface OcrLine { text: string; box?: number[][]; mean?: number }
|
||||
|
||||
export async function locateTextOnScreen(target: string, nodeId: string): Promise<{ target: string; text: string; cx: number; cy: number }> {
|
||||
const shotPath = path.join(os.tmpdir(), "rpa-cv-screen-" + Date.now() + ".png");
|
||||
await grabScreen(shotPath);
|
||||
/** 搜索区域: 框选生成时含 imagePath(区域截图)+x/y(屏幕偏移); 也可只给坐标范围 */
|
||||
interface CvRegion { imagePath?: string; x?: number; y?: number; width?: number; height?: number }
|
||||
|
||||
function parseRegionArg(value: unknown): CvRegion | null {
|
||||
if (!value) return null;
|
||||
if (typeof value === "object") return value as CvRegion;
|
||||
const s = String(value).trim();
|
||||
if (!s) return null;
|
||||
if (s.startsWith("{")) {
|
||||
try { return JSON.parse(s) as CvRegion; } catch { return null; }
|
||||
}
|
||||
if (/\.(png|jpe?g|bmp)$/i.test(s) || fs.existsSync(s)) return { imagePath: s };
|
||||
return null;
|
||||
}
|
||||
|
||||
function boxCenter(box: number[][]): { cx: number; cy: number } {
|
||||
const xs = box.map(p => p[0]);
|
||||
const ys = box.map(p => p[1]);
|
||||
return { cx: (Math.min(...xs) + Math.max(...xs)) / 2, cy: (Math.min(...ys) + Math.max(...ys)) / 2 };
|
||||
}
|
||||
|
||||
export async function locateTextOnScreen(
|
||||
target: string,
|
||||
nodeId: string,
|
||||
region?: CvRegion | null
|
||||
): Promise<{ target: string; text: string; cx: number; cy: number }> {
|
||||
let shotPath: string | null = null;
|
||||
let imagePath: string;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
if (region?.imagePath && fs.existsSync(region.imagePath)) {
|
||||
// 框选截图: OCR 只识别该图, 命中坐标加上框选时的屏幕偏移得到绝对坐标
|
||||
imagePath = region.imagePath;
|
||||
offsetX = Number(region.x ?? 0) || 0;
|
||||
offsetY = Number(region.y ?? 0) || 0;
|
||||
} else {
|
||||
shotPath = path.join(os.tmpdir(), "rpa-cv-screen-" + Date.now() + ".png");
|
||||
await grabScreen(shotPath);
|
||||
imagePath = shotPath;
|
||||
}
|
||||
const ocr = await getOcr();
|
||||
const result = await ocr.detect(shotPath);
|
||||
const result = await ocr.detect(imagePath);
|
||||
const texts = result.texts ?? [];
|
||||
const keyword = target.trim();
|
||||
const hit = texts.find(t => t.text && t.text.includes(keyword));
|
||||
if (!hit || !hit.box || hit.box.length < 4) {
|
||||
// 找不到目标时保留截屏, 便于排查(截屏范围/遮挡/清晰度)
|
||||
throw new RuntimeError("cv_not_found", "屏幕上未找到目标文本:" + keyword, {
|
||||
nodeId,
|
||||
componentId: nodeId,
|
||||
recommendation: "确认目标文本在屏幕上可见且清晰;本次截屏已保留: " + shotPath
|
||||
const candidates = texts.filter(t => t.text && t.text.includes(keyword) && t.box && t.box.length >= 4);
|
||||
let hit: OcrLine | undefined = candidates[0];
|
||||
// 未用框选图但给了坐标范围时, 过滤中心落在区域内的命中
|
||||
if (hit && !region?.imagePath && region?.width && region?.height) {
|
||||
const rx = Number(region.x ?? 0) || 0;
|
||||
const ry = Number(region.y ?? 0) || 0;
|
||||
hit = candidates.find(t => {
|
||||
const c = boxCenter(t.box!);
|
||||
return c.cx >= rx && c.cx <= rx + region.width! && c.cy >= ry && c.cy <= ry + region.height!;
|
||||
});
|
||||
}
|
||||
try { fs.unlinkSync(shotPath); } catch {}
|
||||
const xs = hit.box.map(p => p[0]);
|
||||
const ys = hit.box.map(p => p[1]);
|
||||
const cx = (Math.min(...xs) + Math.max(...xs)) / 2;
|
||||
const cy = (Math.min(...ys) + Math.max(...ys)) / 2;
|
||||
return { target: keyword, text: hit.text, cx, cy };
|
||||
if (!hit || !hit.box || hit.box.length < 4) {
|
||||
// 找不到目标时保留截屏, 便于排查(截屏范围/遮挡/清晰度)
|
||||
throw new RuntimeError("cv_not_found", "屏幕上未找到目标文本:" + keyword + (region?.imagePath ? "(搜索范围: 框选区域)" : ""), {
|
||||
nodeId,
|
||||
componentId: nodeId,
|
||||
recommendation: "确认目标文本在" + (region?.imagePath ? "框选区域内" : "屏幕上") + "可见且清晰;本次识别图片已保留: " + (region?.imagePath ?? shotPath)
|
||||
});
|
||||
}
|
||||
if (shotPath) { try { fs.unlinkSync(shotPath); } catch {} }
|
||||
const c = boxCenter(hit.box);
|
||||
return { target: keyword, text: hit.text, cx: c.cx + offsetX, cy: c.cy + offsetY };
|
||||
}
|
||||
function screenUser32(): string {
|
||||
return [
|
||||
@ -116,7 +160,9 @@ function coordinateScript(x: number, y: number, action: "hover" | "click" | "dou
|
||||
|
||||
async function locateFor(args: Record<string, unknown>, node: { id: string; componentId: string }) {
|
||||
const target = cvTarget(args, node.id);
|
||||
const located = await locateTextOnScreen(target, node.id);
|
||||
// 可选搜索区域: 框选生成 {x,y,width,height,imagePath}, 限定 OCR 查找范围(字段明天接入 UI)
|
||||
const region = parseRegionArg(args.searchRegion ?? args.region);
|
||||
const located = await locateTextOnScreen(target, node.id, region);
|
||||
const offsetX = Number(args.offsetX ?? args.offset_x ?? 0) || 0;
|
||||
const offsetY = Number(args.offsetY ?? args.offset_y ?? 0) || 0;
|
||||
return { located, x: Math.round(located.cx + offsetX), y: Math.round(located.cy + offsetY) };
|
||||
|
||||
@ -609,8 +609,14 @@ function FieldControl({
|
||||
setPicking(true);
|
||||
try {
|
||||
const region = await window.rpa.desktop.pickRegion();
|
||||
onChange(field.key, region.imagePath);
|
||||
onToast("已框选并截图:" + region.width + "×" + region.height);
|
||||
if (field.widget === "region") {
|
||||
// region 字段存完整信息: 坐标+尺寸+截图路径, 运行时可据此定位区域内容在屏幕上的绝对位置
|
||||
onChange(field.key, JSON.stringify({ x: region.x, y: region.y, width: region.width, height: region.height, imagePath: region.imagePath }));
|
||||
onToast("已框选区域:" + region.width + "×" + region.height + " @(" + region.x + "," + region.y + ")");
|
||||
} else {
|
||||
onChange(field.key, region.imagePath);
|
||||
onToast("已框选并截图:" + region.width + "×" + region.height);
|
||||
}
|
||||
} catch (error) {
|
||||
onToast(error instanceof Error ? error.message : String(error), "error");
|
||||
} finally {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user