| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- /**
- * 图像中心点定位功能(Node.js 实现)
- * 在截图中查找模板图片的位置,返回中心点坐标
- *
- * 直接调用 Python 的 OpenCV,通过内联 Python 代码实现
- */
- import { exec } from 'child_process';
- import { promisify } from 'util';
- import { join, isAbsolute } from 'path';
- import { fileURLToPath } from 'url';
- import { dirname } from 'path';
- import { writeFile } from 'fs/promises';
- const execAsync = promisify(exec);
- const __filename = fileURLToPath(import.meta.url);
- const __dirname = dirname(__filename);
- /**
- * 匹配图像
- * @param {string} screenshotPath - 截图路径
- * @param {string} templatePath - 模板图片路径
- * @param {number} width - 设备宽度(可选)
- * @param {number} height - 设备高度(可选)
- * @returns {Promise<{success: boolean, x?: number, y?: number, width?: number, height?: number, error?: string}>}
- */
- export async function matchImage(screenshotPath, templatePath, width, height) {
- const pythonExePath = join(__dirname, '..', '..', 'py', 'venv', 'Scripts', 'python.exe');
-
- // 构建内联 Python 脚本
- const pythonCode = `
- import sys
- import os
- import cv2
- import numpy as np
- import json
- from pathlib import Path
- def read_image_safe(image_path):
- """安全读取包含 Unicode 字符的图片"""
- abs_path = os.path.abspath(str(image_path))
- try:
- with open(abs_path, 'rb') as f:
- image_data = f.read()
- img_array = np.frombuffer(image_data, np.uint8)
- img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
- if img is None:
- raise ValueError(f"cv2.imdecode 无法解码图片: {abs_path}")
- return img
- except FileNotFoundError:
- raise FileNotFoundError(f"图片文件不存在: {abs_path}")
- except Exception as e:
- raise Exception(f"读取图片失败: {abs_path}, 错误: {str(e)}")
- def match_image(image1_path, image2_path, image1_size=None):
- """
- 在 image1 中查找 image2 的位置
-
- Args:
- image1_path: 截图路径(大图)
- image2_path: 模板图片路径(小图)
- image1_size: image1 的目标尺寸 (width, height),可选
-
- Returns:
- 如果找到,返回 (x, y, width, height, similarity),否则返回 (None, similarity)
- """
- img1_path = Path(image1_path)
- img2_path = Path(image2_path)
-
- if not img1_path.exists():
- raise FileNotFoundError(f"图片1不存在: {image1_path}")
- if not img2_path.exists():
- raise FileNotFoundError(f"图片2不存在: {image2_path}")
-
- # 安全读取图片(支持 Unicode 路径)
- img1 = read_image_safe(img1_path)
- img2 = read_image_safe(img2_path)
-
- # 如果指定了 image1 的目标尺寸,先调整大小
- if image1_size is not None:
- target_width, target_height = image1_size
- img1 = cv2.resize(img1, (target_width, target_height))
-
- # 转换为灰度图以提高匹配速度
- img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
- img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
-
- # 获取模板尺寸
- template_height, template_width = img2_gray.shape
-
- # 如果模板比截图大,无法匹配
- if template_height > img1_gray.shape[0] or template_width > img1_gray.shape[1]:
- return None
-
- # 执行模板匹配
- # cv2.TM_CCOEFF_NORMED 方法返回 0-1 的值,1 表示完全匹配
- result = cv2.matchTemplate(img1_gray, img2_gray, cv2.TM_CCOEFF_NORMED)
-
- # 找到最佳匹配位置
- min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
-
- # 设置阈值(0.5 表示 50% 相似度,降低阈值以提高匹配成功率)
- threshold = 0.5
-
- if max_val >= threshold:
- # 找到匹配位置
- top_left = max_loc
- x, y = top_left
-
- # 返回坐标和尺寸 (x, y, width, height, similarity)
- return (x, y, template_width, template_height, max_val)
- else:
- # 未找到匹配,返回 (None, None, None, None, similarity) 以便获取匹配度
- return (None, None, None, None, max_val)
- # 主逻辑
- if __name__ == '__main__':
- image1_path = sys.argv[1]
- image2_path = sys.argv[2]
- image1_width = int(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3] else None
- image1_height = int(sys.argv[4]) if len(sys.argv) > 4 and sys.argv[4] else None
-
- image1_size = None
- if image1_width and image1_height:
- image1_size = (image1_width, image1_height)
-
- try:
- result = match_image(image1_path, image2_path, image1_size)
-
- if result and len(result) >= 5:
- x, y, w, h, max_similarity = result[0], result[1], result[2], result[3], result[4]
-
- if x is not None and y is not None:
- # 匹配成功
- output = {
- 'success': True,
- 'x': int(x),
- 'y': int(y),
- 'width': int(w),
- 'height': int(h),
- 'similarity': float(max_similarity)
- }
- else:
- # 匹配失败,但返回了匹配度信息
- similarity_percent = round(max_similarity * 100, 2)
- output = {
- 'success': False,
- 'error': f'未找到匹配的图像(相似度: {similarity_percent}%,阈值: 50%)',
- 'similarity': float(max_similarity)
- }
- else:
- # 意外情况
- output = {
- 'success': False,
- 'error': '图像匹配失败:无法获取匹配结果'
- }
-
- print(json.dumps(output, ensure_ascii=False))
- except Exception as e:
- output = {
- 'success': False,
- 'error': f'图像匹配失败: {str(e)}'
- }
- print(json.dumps(output, ensure_ascii=False))
- sys.exit(1)
- `;
- // 将 Python 代码写入临时文件
- const tempScriptPath = join(__dirname, '..', '..', 'temp_img_reg.py');
- await writeFile(tempScriptPath, pythonCode, 'utf8');
- // 构建命令
- const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
- const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
- const command = `"${pythonExePath}" "${tempScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width || ''} ${height || ''}`;
- const env = {
- ...process.env,
- DISABLE_MODEL_SOURCE_CHECK: 'True'
- };
- try {
- const { stdout, stderr } = await execAsync(command, {
- timeout: 30000,
- maxBuffer: 10 * 1024 * 1024,
- cwd: join(__dirname, '..', '..'),
- encoding: 'utf8',
- env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
- });
- // 清理临时文件
- try {
- await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
- } catch (e) {
- // 忽略删除失败
- }
- // 解析输出
- const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
-
- try {
- const result = JSON.parse(cleanStdout.trim());
- return result;
- } catch (parseError) {
- return { success: false, error: `解析图像匹配结果失败: ${parseError.message}` };
- }
- } catch (error) {
- // 如果有 stderr 输出,包含在错误信息中
- let errorMsg = error.message || '图像匹配失败';
- if (error.stderr) {
- errorMsg += `\nPython stderr: ${error.stderr}`;
- }
- if (error.stdout) {
- errorMsg += `\nPython stdout: ${error.stdout.substring(0, 500)}`;
- }
-
- if (error.message && error.message.includes('timeout')) {
- return { success: false, error: '图像匹配超时,请检查网络连接或稍后重试' };
- }
- return { success: false, error: errorMsg };
- }
- }
|