test_match_pic0_screenshot.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 诊断 pic0 误匹配:找出为何 pic0.png 会匹配到 草稿箱 区域而非正确缩略图。
  5. 用法: 在项目根目录执行
  6. python python/scripts/test_match_pic0_screenshot.py
  7. 输出: 不同 scale_min/threshold 下的匹配位置与相关系数,并生成标注图。
  8. """
  9. from __future__ import print_function
  10. import os
  11. import sys
  12. import json
  13. SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
  14. PROJECT_ROOT = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..'))
  15. if PROJECT_ROOT not in sys.path:
  16. sys.path.insert(0, PROJECT_ROOT)
  17. PROCESS_DIR = os.path.join(PROJECT_ROOT, 'static', 'process', 'GenerateNote')
  18. TMP_DIR = os.path.join(PROCESS_DIR, 'tmp')
  19. SCREENSHOT_PATH = os.path.join(TMP_DIR, 'Screenshot-pic0.png')
  20. TEMPLATE_PATH = os.path.join(TMP_DIR, 'pic0.png')
  21. MARKED_WRONG = os.path.join(TMP_DIR, 'Screenshot-pic0-marked-wrong.png') # 当前错误匹配
  22. MARKED_BEST = os.path.join(TMP_DIR, 'Screenshot-pic0-marked-best.png') # 推荐参数匹配
  23. try:
  24. import cv2
  25. import numpy as np
  26. except ImportError:
  27. print('请安装: pip install opencv-python numpy')
  28. sys.exit(1)
  29. import importlib.util
  30. _image_match_path = os.path.join(SCRIPT_DIR, 'image-match.py')
  31. _spec = importlib.util.spec_from_file_location('image_match', _image_match_path)
  32. _image_match = importlib.util.module_from_spec(_spec)
  33. _spec.loader.exec_module(_image_match)
  34. load_image = _image_match.load_image
  35. multi_scale_template_match = _image_match.multi_scale_template_match
  36. def multi_scale_with_correlation(screenshot, template, threshold=0.50, scale_min=0.05, scale_max=2.0, steps=60):
  37. """多尺度模板匹配,返回 (x, y, w, h, center_x, center_y, best_correlation) 或 None。"""
  38. gray_screen = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
  39. gray_tpl = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
  40. sh, sw = screenshot.shape[:2]
  41. t_h, t_w = template.shape[:2]
  42. best = None
  43. best_val = threshold
  44. step = max(0.02, (scale_max - scale_min) / float(steps))
  45. for scale in np.arange(scale_min, scale_max + step * 0.5, step):
  46. w = max(8, int(round(t_w * scale)))
  47. h = max(8, int(round(t_h * scale)))
  48. if h > sh or w > sw:
  49. continue
  50. resized = cv2.resize(gray_tpl, (w, h), interpolation=cv2.INTER_AREA)
  51. result = cv2.matchTemplate(gray_screen, resized, cv2.TM_CCOEFF_NORMED)
  52. min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  53. if max_val > best_val:
  54. best_val = max_val
  55. x, y = int(max_loc[0]), int(max_loc[1])
  56. center_x = x + w // 2
  57. center_y = y + h // 2
  58. best = (x, y, w, h, center_x, center_y, best_val)
  59. return best
  60. def crop_template_square(template, percent=0.8, base='w'):
  61. """与 image-match.py 一致的方形中心裁剪。"""
  62. t_h, t_w = template.shape[:2]
  63. side_raw = (t_w if base == 'w' else t_h) * percent
  64. side = min(max(1, int(round(side_raw))), t_w, t_h)
  65. x0 = (t_w - side) // 2
  66. y0 = (t_h - side) // 2
  67. return template[y0:y0 + side, x0:x0 + side].copy()
  68. def draw_match(screenshot, x, y, w, h, center_x, center_y, out_path, label='match'):
  69. img = screenshot.copy()
  70. cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
  71. cv2.circle(img, (center_x, center_y), 8, (0, 0, 255), 2)
  72. cv2.putText(img, label, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 1)
  73. out_dir = os.path.dirname(out_path)
  74. if out_dir:
  75. os.makedirs(out_dir, exist_ok=True)
  76. cv2.imwrite(out_path, img)
  77. print('已保存:', out_path)
  78. def main():
  79. screenshot_path = SCREENSHOT_PATH
  80. template_path = TEMPLATE_PATH
  81. if len(sys.argv) >= 3:
  82. screenshot_path = sys.argv[1]
  83. template_path = sys.argv[2]
  84. if not os.path.isfile(screenshot_path):
  85. print('截图不存在:', screenshot_path)
  86. sys.exit(1)
  87. if not os.path.isfile(template_path):
  88. print('模板不存在:', template_path)
  89. sys.exit(1)
  90. screenshot = load_image(screenshot_path)
  91. template = load_image(template_path)
  92. if screenshot is None or template is None:
  93. print('无法加载图片')
  94. sys.exit(1)
  95. # 与 process 一致:使用 [0.8, "w"] 裁剪模板
  96. template = crop_template_square(template, percent=0.8, base='w')
  97. sh, sw = screenshot.shape[:2]
  98. t_h, t_w = template.shape[:2]
  99. print('截图尺寸: {}x{}, 裁剪后模板: {}x{}'.format(sw, sh, t_w, t_h))
  100. print('')
  101. # 草稿箱通常在右上角;正确缩略图在内容区(偏左/中,偏上)
  102. def region_label(cx, cy):
  103. if cx >= sw * 0.65 and cy <= sh * 0.25:
  104. return '疑似草稿箱'
  105. return '内容区'
  106. scale_min_list = [0.05, 0.10, 0.15, 0.20, 0.25]
  107. threshold_list = [0.40, 0.44, 0.48, 0.52]
  108. results = []
  109. for scale_min in scale_min_list:
  110. for th in threshold_list:
  111. r = multi_scale_with_correlation(
  112. screenshot, template,
  113. threshold=th, scale_min=scale_min, scale_max=2.0, steps=60
  114. )
  115. if r is not None:
  116. x, y, w, h, cx, cy, corr = r
  117. label = region_label(cx, cy)
  118. results.append({
  119. 'scale_min': scale_min,
  120. 'threshold': th,
  121. 'x': x, 'y': y, 'w': w, 'h': h,
  122. 'center_x': cx, 'center_y': cy,
  123. 'corr': corr,
  124. 'label': label
  125. })
  126. if not results:
  127. print('未找到任何匹配,请降低阈值或扩大 scale 范围。')
  128. sys.exit(1)
  129. # 按相关系数从高到低排序
  130. results.sort(key=lambda r: -r['corr'])
  131. print('===== 所有匹配(按相关系数降序)=====')
  132. print('scale_min threshold center_x center_y corr 区域')
  133. print('-' * 60)
  134. for r in results[:20]:
  135. print(' {:.2f} {:.2f} {:4d} {:4d} {:.3f} {}'.format(
  136. r['scale_min'], r['threshold'], r['center_x'], r['center_y'], r['corr'], r['label']))
  137. # 当前逻辑:scale_min=0.05,阈值 0.40~0.52 中第一个命中的
  138. current = None
  139. for th in (0.52, 0.48, 0.44, 0.40):
  140. r = multi_scale_with_correlation(screenshot, template, threshold=th, scale_min=0.05, scale_max=2.0, steps=60)
  141. if r is not None:
  142. x, y, w, h, cx, cy, corr = r
  143. current = (x, y, w, h, cx, cy, corr)
  144. break
  145. if current:
  146. x, y, w, h, cx, cy, corr = current
  147. draw_match(screenshot, x, y, w, h, cx, cy, MARKED_WRONG, 'current(scale_min=0.05) corr=%.3f' % corr)
  148. print('')
  149. print('【当前逻辑】scale_min=0.05 时匹配到: center=({}, {}), corr={:.3f}, 区域={}'.format(
  150. cx, cy, corr, region_label(cx, cy)))
  151. # 推荐:取「内容区」且相关系数最高的匹配
  152. best_content = None
  153. for r in results:
  154. if r['label'] == '内容区':
  155. best_content = r
  156. break
  157. if best_content:
  158. r = best_content
  159. draw_match(screenshot, r['x'], r['y'], r['w'], r['h'], r['center_x'], r['center_y'],
  160. MARKED_BEST, 'best(scale_min=%.2f) corr=%.3f' % (r['scale_min'], r['corr']))
  161. print('')
  162. print('【推荐】内容区最佳: scale_min={:.2f}, threshold={:.2f}, center=({}, {}), corr={:.3f}'.format(
  163. r['scale_min'], r['threshold'], r['center_x'], r['center_y'], r['corr']))
  164. print('')
  165. print('建议: 在 image-match.py 中,对相册缩略图(pic) 将 scale_min_use 改为 min(scale_min, 0.15) 或 min(scale_min, 0.20),')
  166. print(' 避免 scale_min=0.05 时小尺度模板误匹配到右上角 草稿箱 区域。')
  167. else:
  168. print('')
  169. print('未找到内容区匹配,请检查截图与模板是否一致。')
  170. # 诊断结论
  171. print('')
  172. print('===== 诊断结论 =====')
  173. if current and region_label(current[4], current[5]) == '疑似草稿箱':
  174. print('原因: scale_min=0.05 时,模板被缩得很小,与右上角 草稿箱 区域产生较高相关系数,被选为最佳匹配。')
  175. print('解决: 提高相册缩略图的最小缩放 scale_min(如 0.15 或 0.20),只在该尺度以上搜索,避免误匹配。')
  176. else:
  177. print('当前参数下匹配位置正常,若仍出现误匹配可适当提高 threshold 或 scale_min。')
  178. if __name__ == '__main__':
  179. main()