draw-rect.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. """
  2. 在屏幕截图上画模板区域:支持 LightGlue 风格字典、通用匹配结果字典,或 LoFTR 命名风格的匹配对象。
  3. LightGlue 风格:有有效四角坐标则画透视四边形,否则用大图上匹配点包一个轴对齐框。
  4. """
  5. from __future__ import annotations
  6. import argparse
  7. import sys
  8. from dataclasses import dataclass
  9. from pathlib import Path
  10. from typing import Any
  11. import cv2
  12. import numpy as np
  13. @dataclass(frozen=True)
  14. class LoFTRTemplateAgainstScreenshotMatch:
  15. """匹配结果对象(原 ``loftr_template_match`` 类型;绘制轮廓用)。"""
  16. screenshot_bgr_full_size: np.ndarray
  17. homography_template_inference_to_screenshot_inference: np.ndarray | None
  18. template_width_pixels_at_inference: int
  19. template_height_pixels_at_inference: int
  20. divisor_inference_screenshot_x_to_fullsize_x: float
  21. divisor_inference_screenshot_y_to_fullsize_y: float
  22. high_confidence_match_points_on_screenshot_inference: np.ndarray
  23. template_original_width_pixels: int
  24. template_original_height_pixels: int
  25. refined_template_bbox_xywh_full_size: tuple[float, float, float, float] | None
  26. ransac_inlier_points_screenshot_inference: np.ndarray
  27. def _homography_quad_plausible_on_full_image(
  28. corners_full: np.ndarray,
  29. full_w: int,
  30. full_h: int,
  31. tw0: int,
  32. th0: int,
  33. ) -> bool:
  34. _ = tw0, th0
  35. c = np.asarray(corners_full, dtype=np.float64).reshape(4, 2)
  36. if c.shape != (4, 2) or not np.all(np.isfinite(c)):
  37. return False
  38. margin = 4.0
  39. if np.min(c[:, 0]) < -margin or np.min(c[:, 1]) < -margin:
  40. return False
  41. if np.max(c[:, 0]) > full_w + margin or np.max(c[:, 1]) > full_h + margin:
  42. return False
  43. xs, ys = c[:, 0], c[:, 1]
  44. area = 0.5 * abs(
  45. float(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1)))
  46. )
  47. img_area = float(max(1, full_w * full_h))
  48. if area < max(16.0, 1e-4 * img_area):
  49. return False
  50. if area > img_area * 3.0:
  51. return False
  52. return True
  53. def draw_and_save_screenshot_with_template_match_outline(
  54. match_result: LoFTRTemplateAgainstScreenshotMatch,
  55. output_image_path: Path,
  56. outline_color_bgr: tuple[int, int, int] = (0, 255, 0),
  57. outline_thickness_pixels: int = 6,
  58. ) -> None:
  59. """
  60. 在完整分辨率截图上画出模板匹配区域并写入文件。
  61. """
  62. output_image_path = Path(output_image_path)
  63. output_image_path.parent.mkdir(parents=True, exist_ok=True)
  64. annotated_screenshot_bgr = match_result.screenshot_bgr_full_size.copy()
  65. template_w = match_result.template_width_pixels_at_inference
  66. template_h = match_result.template_height_pixels_at_inference
  67. div_x = match_result.divisor_inference_screenshot_x_to_fullsize_x
  68. div_y = match_result.divisor_inference_screenshot_y_to_fullsize_y
  69. full_h, full_w = annotated_screenshot_bgr.shape[:2]
  70. tw0 = int(match_result.template_original_width_pixels)
  71. th0 = int(match_result.template_original_height_pixels)
  72. H = match_result.homography_template_inference_to_screenshot_inference
  73. corners_full: np.ndarray | None = None
  74. if (
  75. H is not None
  76. and np.asarray(H).size == 9
  77. and np.all(np.isfinite(np.asarray(H, dtype=np.float64)))
  78. and int(template_w) > 0
  79. and int(template_h) > 0
  80. ):
  81. tw = int(template_w)
  82. th = int(template_h)
  83. tpl_c = np.array(
  84. [
  85. [0.0, 0.0],
  86. [float(tw - 1), 0.0],
  87. [float(tw - 1), float(th - 1)],
  88. [0.0, float(th - 1)],
  89. ],
  90. dtype=np.float32,
  91. ).reshape(1, 4, 2)
  92. ci = cv2.perspectiveTransform(tpl_c, np.asarray(H, dtype=np.float64))[0]
  93. corners_full = np.stack([ci[:, 0] / div_x, ci[:, 1] / div_y], axis=1)
  94. if not _homography_quad_plausible_on_full_image(
  95. corners_full, full_w, full_h, tw0, th0
  96. ):
  97. corners_full = None
  98. if corners_full is not None:
  99. pts = corners_full.astype(np.int32).reshape(-1, 1, 2)
  100. cv2.polylines(
  101. annotated_screenshot_bgr,
  102. [pts],
  103. isClosed=True,
  104. color=outline_color_bgr,
  105. thickness=int(outline_thickness_pixels),
  106. lineType=cv2.LINE_AA,
  107. )
  108. elif match_result.refined_template_bbox_xywh_full_size is not None:
  109. rx, ry, rw, rh = match_result.refined_template_bbox_xywh_full_size
  110. x0, y0 = int(round(rx)), int(round(ry))
  111. x1, y1 = int(round(rx + rw)), int(round(ry + rh))
  112. cv2.rectangle(
  113. annotated_screenshot_bgr,
  114. (x0, y0),
  115. (x1, y1),
  116. outline_color_bgr,
  117. int(outline_thickness_pixels),
  118. lineType=cv2.LINE_AA,
  119. )
  120. else:
  121. inf = np.asarray(
  122. match_result.ransac_inlier_points_screenshot_inference,
  123. dtype=np.float64,
  124. ).reshape(-1, 2)
  125. if inf.shape[0] < 3:
  126. inf = np.asarray(
  127. match_result.high_confidence_match_points_on_screenshot_inference,
  128. dtype=np.float64,
  129. ).reshape(-1, 2)
  130. xy = np.stack([inf[:, 0] / div_x, inf[:, 1] / div_y], axis=1)
  131. xmin = int(np.floor(np.min(xy[:, 0])))
  132. ymin = int(np.floor(np.min(xy[:, 1])))
  133. xmax = int(np.ceil(np.max(xy[:, 0])))
  134. ymax = int(np.ceil(np.max(xy[:, 1])))
  135. cv2.rectangle(
  136. annotated_screenshot_bgr,
  137. (xmin, ymin),
  138. (xmax, ymax),
  139. outline_color_bgr,
  140. int(outline_thickness_pixels),
  141. lineType=cv2.LINE_AA,
  142. )
  143. if not cv2.imwrite(str(output_image_path), annotated_screenshot_bgr):
  144. raise OSError(f"无法写入:{output_image_path}")
  145. def draw_and_save_screenshot_with_match_dict_outline(
  146. match_dict: dict[str, Any],
  147. output_image_path: str | Path,
  148. outline_color_bgr: tuple[int, int, int] = (0, 255, 0),
  149. outline_thickness_pixels: int = 6,
  150. ) -> None:
  151. """
  152. 根据匹配管线输出的字典(含 ``screenshot_image_path``、四角或关键点等)在截图上画框或四边形。
  153. """
  154. output_image_path = Path(output_image_path)
  155. output_image_path.parent.mkdir(parents=True, exist_ok=True)
  156. scr_path = Path(str(match_dict["screenshot_image_path"]))
  157. bgr = cv2.imread(str(scr_path), cv2.IMREAD_COLOR)
  158. if bgr is None:
  159. raise FileNotFoundError(f"无法读取截图:{scr_path}")
  160. full_h, full_w = bgr.shape[:2]
  161. corners = match_dict.get("template_corners_on_screenshot_xy")
  162. if corners is not None:
  163. c = np.asarray(corners, dtype=np.float64).reshape(4, 2)
  164. if c.shape == (4, 2) and np.all(np.isfinite(c)):
  165. pts = c.astype(np.int32).reshape(-1, 1, 2)
  166. cv2.polylines(
  167. bgr,
  168. [pts],
  169. isClosed=True,
  170. color=outline_color_bgr,
  171. thickness=int(outline_thickness_pixels),
  172. lineType=cv2.LINE_AA,
  173. )
  174. if not cv2.imwrite(str(output_image_path), bgr):
  175. raise OSError(f"无法写入:{output_image_path}")
  176. return
  177. rb = match_dict.get("refined_template_bbox_xywh_full_size")
  178. if rb is not None and len(rb) >= 4:
  179. rx, ry, rw, rh = float(rb[0]), float(rb[1]), float(rb[2]), float(rb[3])
  180. x0, y0 = int(round(rx)), int(round(ry))
  181. x1, y1 = int(round(rx + rw)), int(round(ry + rh))
  182. cv2.rectangle(
  183. bgr,
  184. (x0, y0),
  185. (x1, y1),
  186. outline_color_bgr,
  187. int(outline_thickness_pixels),
  188. lineType=cv2.LINE_AA,
  189. )
  190. if not cv2.imwrite(str(output_image_path), bgr):
  191. raise OSError(f"无法写入:{output_image_path}")
  192. return
  193. mk = match_dict.get("matched_keypoints_original_xy")
  194. if mk is None:
  195. raise ValueError("字典中缺少可用的 template_corners、bbox 或 matched_keypoints")
  196. xy = np.asarray(mk, dtype=np.float64).reshape(-1, 2)
  197. if xy.shape[0] < 1:
  198. raise ValueError("matched_keypoints_original_xy 为空")
  199. xmin = max(0, int(np.floor(np.min(xy[:, 0]))))
  200. ymin = max(0, int(np.floor(np.min(xy[:, 1]))))
  201. xmax = min(full_w - 1, int(np.ceil(np.max(xy[:, 0]))))
  202. ymax = min(full_h - 1, int(np.ceil(np.max(xy[:, 1]))))
  203. cv2.rectangle(
  204. bgr,
  205. (xmin, ymin),
  206. (xmax, ymax),
  207. outline_color_bgr,
  208. int(outline_thickness_pixels),
  209. lineType=cv2.LINE_AA,
  210. )
  211. if not cv2.imwrite(str(output_image_path), bgr):
  212. raise OSError(f"无法写入:{output_image_path}")
  213. def main() -> None:
  214. parser = argparse.ArgumentParser(
  215. description="根据匹配 JSON 字典在截图上画模板区域(测试用)。"
  216. )
  217. parser.add_argument(
  218. "json_path",
  219. type=Path,
  220. nargs="?",
  221. help="含 screenshot_image_path、template_corners_on_screenshot_xy 等的 JSON",
  222. )
  223. parser.add_argument(
  224. "-o",
  225. "--out",
  226. type=Path,
  227. default=Path("output") / "match_outline.png",
  228. help="输出图片路径",
  229. )
  230. args = parser.parse_args()
  231. if args.json_path is None:
  232. parser.print_help()
  233. raise SystemExit(2)
  234. import json
  235. data = json.loads(Path(args.json_path).read_text(encoding="utf-8"))
  236. draw_and_save_screenshot_with_match_dict_outline(data, args.out)
  237. print(f"已写入:{args.out.resolve()}", flush=True)
  238. if __name__ == "__main__":
  239. if sys.platform == "win32":
  240. for stream in (sys.stdout, sys.stderr):
  241. try:
  242. stream.reconfigure(encoding="utf-8")
  243. except Exception:
  244. pass
  245. main()