image-center-location.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /**
  2. * 图像中心点定位功能(Node.js 实现)
  3. * 在截图中查找模板图片的位置,返回中心点坐标
  4. *
  5. * 直接调用 Python 的 OpenCV,通过内联 Python 代码实现
  6. */
  7. import { exec } from 'child_process';
  8. import { promisify } from 'util';
  9. import { join, isAbsolute } from 'path';
  10. import { fileURLToPath } from 'url';
  11. import { dirname } from 'path';
  12. import { writeFile } from 'fs/promises';
  13. const execAsync = promisify(exec);
  14. const __filename = fileURLToPath(import.meta.url);
  15. const __dirname = dirname(__filename);
  16. /**
  17. * 匹配图像
  18. * @param {string} screenshotPath - 截图路径
  19. * @param {string} templatePath - 模板图片路径
  20. * @param {number} width - 设备宽度(可选)
  21. * @param {number} height - 设备高度(可选)
  22. * @returns {Promise<{success: boolean, x?: number, y?: number, width?: number, height?: number, error?: string}>}
  23. */
  24. export async function matchImage(screenshotPath, templatePath, width, height) {
  25. const pythonExePath = join(__dirname, '..', '..', 'py', 'venv', 'Scripts', 'python.exe');
  26. // 构建内联 Python 脚本
  27. const pythonCode = `
  28. import sys
  29. import os
  30. import cv2
  31. import numpy as np
  32. import json
  33. from pathlib import Path
  34. def read_image_safe(image_path):
  35. """安全读取包含 Unicode 字符的图片"""
  36. abs_path = os.path.abspath(str(image_path))
  37. try:
  38. with open(abs_path, 'rb') as f:
  39. image_data = f.read()
  40. img_array = np.frombuffer(image_data, np.uint8)
  41. img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
  42. if img is None:
  43. raise ValueError(f"cv2.imdecode 无法解码图片: {abs_path}")
  44. return img
  45. except FileNotFoundError:
  46. raise FileNotFoundError(f"图片文件不存在: {abs_path}")
  47. except Exception as e:
  48. raise Exception(f"读取图片失败: {abs_path}, 错误: {str(e)}")
  49. def match_image(image1_path, image2_path, image1_size=None):
  50. """
  51. 在 image1 中查找 image2 的位置
  52. Args:
  53. image1_path: 截图路径(大图)
  54. image2_path: 模板图片路径(小图)
  55. image1_size: image1 的目标尺寸 (width, height),可选
  56. Returns:
  57. 如果找到,返回 (x, y, width, height, similarity),否则返回 (None, similarity)
  58. """
  59. img1_path = Path(image1_path)
  60. img2_path = Path(image2_path)
  61. if not img1_path.exists():
  62. raise FileNotFoundError(f"图片1不存在: {image1_path}")
  63. if not img2_path.exists():
  64. raise FileNotFoundError(f"图片2不存在: {image2_path}")
  65. # 安全读取图片(支持 Unicode 路径)
  66. img1 = read_image_safe(img1_path)
  67. img2 = read_image_safe(img2_path)
  68. # 如果指定了 image1 的目标尺寸,先调整大小
  69. if image1_size is not None:
  70. target_width, target_height = image1_size
  71. img1 = cv2.resize(img1, (target_width, target_height))
  72. # 转换为灰度图以提高匹配速度
  73. img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
  74. img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
  75. # 获取模板尺寸
  76. template_height, template_width = img2_gray.shape
  77. # 如果模板比截图大,无法匹配
  78. if template_height > img1_gray.shape[0] or template_width > img1_gray.shape[1]:
  79. return None
  80. # 执行模板匹配
  81. # cv2.TM_CCOEFF_NORMED 方法返回 0-1 的值,1 表示完全匹配
  82. result = cv2.matchTemplate(img1_gray, img2_gray, cv2.TM_CCOEFF_NORMED)
  83. # 找到最佳匹配位置
  84. min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  85. # 设置阈值(0.5 表示 50% 相似度,降低阈值以提高匹配成功率)
  86. threshold = 0.5
  87. if max_val >= threshold:
  88. # 找到匹配位置
  89. top_left = max_loc
  90. x, y = top_left
  91. # 返回坐标和尺寸 (x, y, width, height, similarity)
  92. return (x, y, template_width, template_height, max_val)
  93. else:
  94. # 未找到匹配,返回 (None, None, None, None, similarity) 以便获取匹配度
  95. return (None, None, None, None, max_val)
  96. # 主逻辑
  97. if __name__ == '__main__':
  98. image1_path = sys.argv[1]
  99. image2_path = sys.argv[2]
  100. image1_width = int(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3] else None
  101. image1_height = int(sys.argv[4]) if len(sys.argv) > 4 and sys.argv[4] else None
  102. image1_size = None
  103. if image1_width and image1_height:
  104. image1_size = (image1_width, image1_height)
  105. try:
  106. result = match_image(image1_path, image2_path, image1_size)
  107. if result and len(result) >= 5:
  108. x, y, w, h, max_similarity = result[0], result[1], result[2], result[3], result[4]
  109. if x is not None and y is not None:
  110. # 匹配成功
  111. output = {
  112. 'success': True,
  113. 'x': int(x),
  114. 'y': int(y),
  115. 'width': int(w),
  116. 'height': int(h),
  117. 'similarity': float(max_similarity)
  118. }
  119. else:
  120. # 匹配失败,但返回了匹配度信息
  121. similarity_percent = round(max_similarity * 100, 2)
  122. output = {
  123. 'success': False,
  124. 'error': f'未找到匹配的图像(相似度: {similarity_percent}%,阈值: 50%)',
  125. 'similarity': float(max_similarity)
  126. }
  127. else:
  128. # 意外情况
  129. output = {
  130. 'success': False,
  131. 'error': '图像匹配失败:无法获取匹配结果'
  132. }
  133. print(json.dumps(output, ensure_ascii=False))
  134. except Exception as e:
  135. output = {
  136. 'success': False,
  137. 'error': f'图像匹配失败: {str(e)}'
  138. }
  139. print(json.dumps(output, ensure_ascii=False))
  140. sys.exit(1)
  141. `;
  142. // 将 Python 代码写入临时文件
  143. const tempScriptPath = join(__dirname, '..', '..', 'temp_img_reg.py');
  144. await writeFile(tempScriptPath, pythonCode, 'utf8');
  145. // 构建命令
  146. const normalizedScreenshotPath = screenshotPath.replace(/\\/g, '/');
  147. const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
  148. const command = `"${pythonExePath}" "${tempScriptPath}" "${normalizedScreenshotPath}" "${normalizedTemplatePath}" ${width || ''} ${height || ''}`;
  149. const env = {
  150. ...process.env,
  151. DISABLE_MODEL_SOURCE_CHECK: 'True'
  152. };
  153. console.log('[matchImage] 执行Python脚本,命令:', command);
  154. try {
  155. const { stdout, stderr } = await execAsync(command, {
  156. timeout: 30000,
  157. maxBuffer: 10 * 1024 * 1024,
  158. cwd: join(__dirname, '..', '..'),
  159. encoding: 'utf8',
  160. env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
  161. });
  162. // 打印 Python 脚本的 stdout 和 stderr 输出(用于调试)
  163. console.log('[matchImage] Python stdout 长度:', stdout ? stdout.length : 0);
  164. if (stdout) {
  165. console.log('[matchImage] Python stdout:', stdout.substring(0, 500)); // 只打印前500字符
  166. }
  167. console.log('[matchImage] Python stderr 长度:', stderr ? stderr.length : 0);
  168. if (stderr && stderr.trim()) {
  169. try {
  170. const decodedStderr = Buffer.from(stderr, 'utf8').toString('utf8');
  171. console.log('[matchImage] Python stderr:', decodedStderr.trim());
  172. } catch (e) {
  173. console.log('[matchImage] Python stderr:', stderr.trim());
  174. }
  175. }
  176. // 清理临时文件
  177. try {
  178. await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
  179. } catch (e) {
  180. // 忽略删除失败
  181. }
  182. // 解析输出
  183. const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
  184. try {
  185. const result = JSON.parse(cleanStdout.trim());
  186. return result;
  187. } catch (parseError) {
  188. console.error('图像匹配结果解析失败:', parseError);
  189. console.error('原始输出:', cleanStdout);
  190. return { success: false, error: `解析图像匹配结果失败: ${parseError.message}` };
  191. }
  192. } catch (error) {
  193. console.error('[matchImage] 图像匹配失败:', error);
  194. console.error('[matchImage] 错误类型:', error.constructor.name);
  195. console.error('[matchImage] 错误消息:', error.message);
  196. // 如果有 stderr 输出,包含在错误信息中
  197. let errorMsg = error.message || '图像匹配失败';
  198. if (error.stderr) {
  199. console.error('[matchImage] Python stderr:', error.stderr);
  200. errorMsg += `\nPython stderr: ${error.stderr}`;
  201. }
  202. if (error.stdout) {
  203. console.error('[matchImage] Python stdout:', error.stdout);
  204. errorMsg += `\nPython stdout: ${error.stdout.substring(0, 500)}`;
  205. }
  206. if (error.message && error.message.includes('timeout')) {
  207. return { success: false, error: '图像匹配超时,请检查网络连接或稍后重试' };
  208. }
  209. return { success: false, error: errorMsg };
  210. }
  211. }