image-center-location.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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. try {
  154. const { stdout, stderr } = await execAsync(command, {
  155. timeout: 30000,
  156. maxBuffer: 10 * 1024 * 1024,
  157. cwd: join(__dirname, '..', '..'),
  158. encoding: 'utf8',
  159. env: { ...env, PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1' }
  160. });
  161. // 清理临时文件
  162. try {
  163. await import('fs/promises').then(fs => fs.unlink(tempScriptPath));
  164. } catch (e) {
  165. // 忽略删除失败
  166. }
  167. // 解析输出
  168. const cleanStdout = stdout.replace(/\[33m.*?\[0m/g, '').replace(/DeprecationWarning.*?\n/g, '');
  169. try {
  170. const result = JSON.parse(cleanStdout.trim());
  171. return result;
  172. } catch (parseError) {
  173. return { success: false, error: `解析图像匹配结果失败: ${parseError.message}` };
  174. }
  175. } catch (error) {
  176. // 如果有 stderr 输出,包含在错误信息中
  177. let errorMsg = error.message || '图像匹配失败';
  178. if (error.stderr) {
  179. errorMsg += `\nPython stderr: ${error.stderr}`;
  180. }
  181. if (error.stdout) {
  182. errorMsg += `\nPython stdout: ${error.stdout.substring(0, 500)}`;
  183. }
  184. if (error.message && error.message.includes('timeout')) {
  185. return { success: false, error: '图像匹配超时,请检查网络连接或稍后重试' };
  186. }
  187. return { success: false, error: errorMsg };
  188. }
  189. }