objmark_api.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. # -*- coding: utf-8 -*-
  2. """
  3. 把 calibrate.py 和 measure_shoe_v2.py 的逻辑拷贝并封装为两个类,
  4. 外部直接 new + 传参使用,不再依赖命令行和 calib.json 文件传递。
  5. - Calibrator : 标定。不保存 calib.json,calibrate() 直接返回标定结果 dict
  6. (键名与原来 calib.json 完全一致,可直接喂给 ShoeMeasurer)。
  7. - ShoeMeasurer : 测量。debug=True 时保存 result_*.jpg 标注图,
  8. measure() 返回长/宽/高结果 dict。
  9. 使用方法:
  10. from objmark_api import Calibrator, ShoeMeasurer
  11. # 1) 标定(结果直接拿到 dict,不落盘)
  12. calib = Calibrator(
  13. topdown_png="标定尺顶拍抠图.png",
  14. topdown_known_length_mm=300,
  15. side_png="标定块水平拍抠图.png",
  16. side_known_height_mm=100,
  17. side_known_length_mm=150, # 可选,单独标定长度方向kl
  18. front_png="标定块正面拍抠图.png", # 可选,单独标定宽度方向kw
  19. front_known_width_mm=60,
  20. ).calibrate()
  21. # 2) 测量(calib 可以直接传上面拿到的 dict,也可以传 calib.json 路径)
  22. result = ShoeMeasurer(
  23. topdown_png="鞋顶拍抠图.png",
  24. side_png="鞋水平拍抠图.png",
  25. front_png="鞋正面拍抠图.png", # 可选
  26. calib=calib,
  27. width_slope=0.085833, # 可选,顶拍宽度透视修正
  28. width_scale0=1.146419,
  29. length_factor=1.030640, # 可选,长度修正因子
  30. debug=True, # 保存 result_*.jpg 标注图
  31. ).measure()
  32. print(result["length_mm"], result["width_mm"], result["height_mm"])
  33. """
  34. import datetime
  35. import os
  36. import cv2
  37. import numpy as np
  38. from .turntable_calib_utils import (
  39. imread_unicode, imread_unicode_unchanged, imwrite_unicode, load_json,
  40. mask_from_alpha, composite_on_white, largest_contour
  41. )
  42. # ===========================================================================
  43. # Calibrator —— 拷贝自 calibrate.py,封装为类,不保存 calib.json
  44. # ===========================================================================
  45. def _calibrate_topdown_from_mask(png_path, known_length_mm):
  46. """顶拍标定:标定物轮廓最长边的像素长度 -> k(mm/像素)"""
  47. img = imread_unicode_unchanged(png_path)
  48. if img is None:
  49. raise FileNotFoundError(f"无法读取图片: {png_path}")
  50. mask = mask_from_alpha(img)
  51. contour = largest_contour(mask)
  52. (cx, cy), (w_px, h_px), angle = cv2.minAreaRect(contour)
  53. length_px = max(w_px, h_px)
  54. if length_px < 1e-6:
  55. raise RuntimeError("顶拍标定物轮廓异常(长度接近0),请检查抠图是否正确。")
  56. k = known_length_mm / length_px
  57. return k, length_px
  58. def _calibrate_side_from_mask(png_path, known_height_mm, known_length_mm=None):
  59. """
  60. 水平拍标定:从同一个标定物同时提取
  61. - kh: 竖直方向比例系数(用轮廓最高点到最低点的像素高度)
  62. - kl: 水平方向比例系数(用轮廓最左到最右的像素宽度),需提供 known_length_mm
  63. 分开标定的原因:如果侧拍相机不是严格正对,或存在畸变,
  64. 水平和竖直方向的比例尺会不一致。
  65. """
  66. img = imread_unicode_unchanged(png_path)
  67. if img is None:
  68. raise FileNotFoundError(f"无法读取图片: {png_path}")
  69. mask = mask_from_alpha(img)
  70. contour = largest_contour(mask)
  71. ys = contour[:, 0, 1]
  72. xs = contour[:, 0, 0]
  73. height_px = float(ys.max() - ys.min())
  74. length_px = float(xs.max() - xs.min())
  75. if height_px < 1e-6:
  76. raise RuntimeError("水平拍标定物轮廓异常(高度接近0),请检查抠图是否正确。")
  77. kh = known_height_mm / height_px
  78. kl = None
  79. if known_length_mm:
  80. if length_px < 1e-6:
  81. raise RuntimeError("水平拍标定物轮廓异常(水平跨度接近0),请检查抠图是否正确。")
  82. kl = known_length_mm / length_px
  83. return kh, height_px, kl, length_px
  84. def _calibrate_front_from_mask(png_path, known_width_mm):
  85. """
  86. 正面拍标定:从标定物正面照片提取水平方向比例系数 kw(mm/像素)。
  87. 原理与侧拍标定完全一致,只是拍摄角度换成正对物体的一端,测宽度而不是长度。
  88. """
  89. img = imread_unicode_unchanged(png_path)
  90. if img is None:
  91. raise FileNotFoundError(f"无法读取图片: {png_path}")
  92. mask = mask_from_alpha(img)
  93. contour = largest_contour(mask)
  94. xs = contour[:, 0, 0]
  95. width_px = float(xs.max() - xs.min())
  96. if width_px < 1e-6:
  97. raise RuntimeError("正面拍标定物轮廓异常(宽度接近0),请检查抠图是否正确。")
  98. kw = known_width_mm / width_px
  99. return kw, width_px
  100. class Calibrator:
  101. """
  102. 标定(类封装版)。new 时传入三张(或两张)标定物抠图PNG及各自的真实尺寸,
  103. 调用 calibrate() 直接返回标定结果 dict,不保存 calib.json。
  104. 参数(对应原 calibrate.py 的命令行参数):
  105. topdown_png 顶拍标定物抠图PNG(带alpha)
  106. topdown_known_length_mm 顶拍标定物真实长度(mm),取其最长边作为参照
  107. side_png 水平拍标定物抠图PNG(带alpha)
  108. side_known_height_mm 水平拍标定物真实高度(mm),竖直方向那条边
  109. side_known_length_mm 可选,水平拍标定物水平方向那条边的真实长度(mm),
  110. 用于单独标定长度方向系数kl
  111. front_png 可选,正面拍标定物抠图PNG(带alpha)
  112. front_known_width_mm 可选,正面拍标定物真实宽度(mm)
  113. verbose 是否打印标定过程/结果(默认True)
  114. """
  115. def __init__(self, topdown_png, topdown_known_length_mm,
  116. side_png, side_known_height_mm,
  117. side_known_length_mm=None,
  118. front_png=None, front_known_width_mm=None,
  119. verbose=True):
  120. self.topdown_png = topdown_png
  121. self.topdown_known_length_mm = topdown_known_length_mm
  122. self.side_png = side_png
  123. self.side_known_height_mm = side_known_height_mm
  124. self.side_known_length_mm = side_known_length_mm
  125. self.front_png = front_png
  126. self.front_known_width_mm = front_known_width_mm
  127. self.verbose = verbose
  128. def calibrate(self):
  129. """
  130. 执行标定,直接返回标定结果 dict(不落盘)。
  131. 返回键名与旧版 calib.json 完全一致:
  132. k_mm_per_px / kh_mm_per_px / [kl_mm_per_px] / [kw_mm_per_px]
  133. 以及对应的 known_* / pixel_* 记录字段
  134. """
  135. k, length_px = _calibrate_topdown_from_mask(
  136. self.topdown_png, self.topdown_known_length_mm
  137. )
  138. kh, height_px, kl, side_length_px = _calibrate_side_from_mask(
  139. self.side_png, self.side_known_height_mm, self.side_known_length_mm
  140. )
  141. result = {
  142. "k_mm_per_px": k,
  143. "topdown_known_length_mm": self.topdown_known_length_mm,
  144. "topdown_pixel_length": length_px,
  145. "kh_mm_per_px": kh,
  146. "side_known_height_mm": self.side_known_height_mm,
  147. "side_pixel_height": height_px,
  148. }
  149. if self.verbose:
  150. print("\n========== 标定结果 ==========")
  151. print(f"[顶拍/宽度] 标定物像素长度: {length_px:.2f} px , "
  152. f"真实长度: {self.topdown_known_length_mm:.2f} mm")
  153. print(f"[顶拍/宽度] 比例系数 k : {k:.6f} mm/像素")
  154. print(f"[侧拍/高度] 标定物像素高度: {height_px:.2f} px , "
  155. f"真实高度: {self.side_known_height_mm:.2f} mm")
  156. print(f"[侧拍/高度] 比例系数 kh: {kh:.6f} mm/像素")
  157. if kl:
  158. result["kl_mm_per_px"] = kl
  159. result["side_known_length_mm"] = self.side_known_length_mm
  160. result["side_pixel_length"] = side_length_px
  161. if self.verbose:
  162. print(f"[侧拍/长度] 标定物像素宽度: {side_length_px:.2f} px , "
  163. f"真实长度: {self.side_known_length_mm:.2f} mm")
  164. print(f"[侧拍/长度] 比例系数 kl: {kl:.6f} mm/像素")
  165. ratio = kl / kh
  166. print(f"\n[诊断] kl/kh = {ratio:.4f}")
  167. if abs(ratio - 1) > 0.05:
  168. print(f" ⚠ 水平与竖直方向比例系数相差 {abs(ratio - 1) * 100:.1f}%,"
  169. f"说明侧拍相机确实存在倾斜/畸变,"
  170. f"分开标定是必要的(这正是之前长度偏差的来源)。")
  171. else:
  172. print(f" 两个方向比例系数接近,侧拍视角基本正常。")
  173. elif self.verbose:
  174. print("[侧拍/长度] 未提供 side_known_length_mm,长度将沿用kh换算(可能有系统性偏差)")
  175. if self.front_png and self.front_known_width_mm:
  176. kw, front_width_px = _calibrate_front_from_mask(
  177. self.front_png, self.front_known_width_mm
  178. )
  179. result["kw_mm_per_px"] = kw
  180. result["front_known_width_mm"] = self.front_known_width_mm
  181. result["front_pixel_width"] = front_width_px
  182. if self.verbose:
  183. print(f"[正面拍/宽度] 标定物像素宽度: {front_width_px:.2f} px , "
  184. f"真实宽度: {self.front_known_width_mm:.2f} mm")
  185. print(f"[正面拍/宽度] 比例系数 kw: {kw:.6f} mm/像素")
  186. print("===============================") if self.verbose else None
  187. return result
  188. # ===========================================================================
  189. # ShoeMeasurer —— 拷贝自 measure_shoe_v2.py,封装为类,debug 参数可选保存结果图
  190. # ===========================================================================
  191. def _get_vis_background(png_img, original_path):
  192. """优先用原图做可视化背景(更真实),没提供原图则把抠图合成到白底上。"""
  193. if original_path:
  194. bg = imread_unicode(original_path)
  195. if bg is None:
  196. raise FileNotFoundError(f"无法读取原图: {original_path}")
  197. if bg.shape[:2] != png_img.shape[:2]:
  198. print(f"警告: 原图尺寸{bg.shape[:2]}与抠图PNG尺寸{png_img.shape[:2]}不一致,"
  199. f"标注框可能对不齐,建议确认两者是否为同一张照片导出。")
  200. return bg
  201. return composite_on_white(png_img)
  202. def _measure_width_front(png_img, vis_bg, kw_mm_per_px):
  203. """
  204. 正面拍图:测宽度(水平像素跨度×kw)。
  205. 相比顶拍测宽度,正面拍能看到被鞋面遮挡的鞋底外沿部分,对这类鞋型精度明显更高。
  206. """
  207. mask = mask_from_alpha(png_img)
  208. contour = largest_contour(mask)
  209. xs = contour[:, 0, 0]
  210. left_x, right_x = int(xs.min()), int(xs.max())
  211. width_px = right_x - left_x
  212. width_mm = width_px * kw_mm_per_px
  213. vis = vis_bg.copy()
  214. ys = contour[:, 0, 1]
  215. mid_y = int(np.mean(ys))
  216. cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
  217. cv2.line(vis, (left_x, mid_y), (right_x, mid_y), (255, 0, 0), 2)
  218. cv2.circle(vis, (left_x, mid_y), 6, (255, 0, 0), -1)
  219. cv2.circle(vis, (right_x, mid_y), 6, (255, 0, 0), -1)
  220. cv2.putText(vis, f"W(front)={width_mm:.1f}mm",
  221. (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 0, 0), 2)
  222. return width_mm, vis
  223. def _measure_width_topdown(png_img, vis_bg, k_mm_per_px):
  224. """顶拍图:只测宽度(minAreaRect短边),不再用顶拍算长度(避免鞋子高度导致的透视外扩误差)"""
  225. mask = mask_from_alpha(png_img)
  226. contour = largest_contour(mask)
  227. (cx, cy), (w_px, h_px), angle = cv2.minAreaRect(contour)
  228. width_px = min(w_px, h_px)
  229. width_mm = width_px * k_mm_per_px
  230. vis = vis_bg.copy()
  231. box = np.intp(cv2.boxPoints(((cx, cy), (w_px, h_px), angle)))
  232. cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
  233. cv2.drawContours(vis, [box], 0, (0, 0, 255), 2)
  234. cv2.putText(vis, f"W={width_mm:.1f}mm",
  235. (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
  236. return width_mm, vis
  237. def _measure_length_height_side(png_img, vis_bg, kh_mm_per_px, kl_mm_per_px=None):
  238. """
  239. 侧拍图:测长度(水平像素跨度×kl)和高度(竖直像素跨度×kh)。
  240. kl 与 kh 分开使用,因为侧拍相机若有倾斜/畸变,水平与竖直方向比例尺不同。
  241. 若未提供 kl,则退回沿用 kh,但长度可能有系统性偏差。
  242. """
  243. if kl_mm_per_px is None:
  244. kl_mm_per_px = kh_mm_per_px
  245. mask = mask_from_alpha(png_img)
  246. contour = largest_contour(mask)
  247. xs = contour[:, 0, 0]
  248. ys = contour[:, 0, 1]
  249. left_x, right_x = int(xs.min()), int(xs.max())
  250. top_y, bottom_y = int(ys.min()), int(ys.max())
  251. length_px = right_x - left_x
  252. height_px = bottom_y - top_y
  253. length_mm = length_px * kl_mm_per_px
  254. height_mm = height_px * kh_mm_per_px
  255. vis = vis_bg.copy()
  256. mid_y = int(np.mean(ys))
  257. mid_x = int(np.mean(xs))
  258. cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
  259. # 长度标注线(水平)
  260. cv2.line(vis, (left_x, mid_y), (right_x, mid_y), (255, 0, 0), 2)
  261. cv2.circle(vis, (left_x, mid_y), 6, (255, 0, 0), -1)
  262. cv2.circle(vis, (right_x, mid_y), 6, (255, 0, 0), -1)
  263. # 高度标注线(竖直)
  264. cv2.line(vis, (mid_x, top_y), (mid_x, bottom_y), (0, 0, 255), 2)
  265. cv2.circle(vis, (mid_x, top_y), 6, (0, 0, 255), -1)
  266. cv2.circle(vis, (mid_x, bottom_y), 6, (0, 0, 255), -1)
  267. cv2.putText(vis, f"L={length_mm:.1f}mm H={height_mm:.1f}mm",
  268. (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
  269. return length_mm, height_mm, vis
  270. def _annotate_final_summary(vis, length_mm, width_mm, height_mm,
  271. timestamp_str, width_corrected):
  272. """
  273. 在图片左上角统一标出这次测量的完整长/宽/高结果(mm)和时间戳,
  274. 每张结果图都会加这个统一信息条,方便单独看某一张图时也能知道完整结果。
  275. """
  276. vis = vis.copy()
  277. lines = [
  278. f"L={length_mm:.1f}mm W={width_mm:.1f}mm"
  279. f"{' (adjusted)' if width_corrected else ''} H={height_mm:.1f}mm",
  280. f"{timestamp_str}",
  281. ]
  282. # 用不透明底色条完全盖住图片本身已有的测量标注文字,避免重影
  283. box_h = 140 * len(lines) + 20
  284. cv2.rectangle(vis, (0, 0), (580, box_h), (255, 255, 255), -1)
  285. y = 140
  286. for line in lines:
  287. cv2.putText(vis, line, (15, y), cv2.FONT_HERSHEY_SIMPLEX, 4.8,
  288. (0, 0, 200), 2, cv2.LINE_AA)
  289. y += 140
  290. return vis
  291. class ShoeMeasurer:
  292. """
  293. 测量(类封装版)。new 时传入各视角抠图PNG、标定结果及可选修正参数,
  294. 调用 measure() 返回长/宽/高结果 dict。
  295. debug=True 时把 result_*.jpg 标注图保存到 output_dir 目录。
  296. 参数(对应原 measure_shoe_v2.py 的命令行参数):
  297. topdown_png 顶拍抠图PNG(带alpha)
  298. side_png 水平拍抠图PNG(带alpha)
  299. front_png 可选,正面拍抠图PNG(带alpha),提供后宽度改用正面拍测量
  300. topdown_original 可选,顶拍原图(可视化背景)
  301. side_original 可选,水平拍原图(可视化背景)
  302. calib 标定结果:Calibrator.calibrate() 返回的 dict,
  303. 或 calib.json 文件路径(str),默认 "calib.json"
  304. width_slope 可选,宽度修正的高度斜率(mm宽度/mm高度)
  305. width_scale0 可选,宽度在零高度时的固有放大倍数
  306. (以上两个配合做顶拍宽度透视修正)
  307. length_factor 可选,长度修正因子(测出值除以此数)
  308. debug 可选,是否保存 result_*.jpg 标注图(默认False)
  309. output_dir debug保存目录(默认当前目录)
  310. verbose 是否打印测量过程/结果(默认True)
  311. """
  312. def __init__(self, topdown_png, side_png, front_png=None,
  313. topdown_original=None, side_original=None,
  314. calib="calib.json",
  315. width_slope=None, width_scale0=None,
  316. length_factor=None,
  317. debug=False, output_dir=".",
  318. verbose=True):
  319. self.topdown_png = topdown_png
  320. self.side_png = side_png
  321. self.front_png = front_png
  322. self.topdown_original = topdown_original
  323. self.side_original = side_original
  324. self.calib = calib
  325. self.width_slope = width_slope
  326. self.width_scale0 = width_scale0
  327. self.length_factor = length_factor
  328. self.debug = debug
  329. self.output_dir = output_dir
  330. self.verbose = verbose
  331. def measure(self):
  332. """
  333. 执行测量,返回结果 dict:
  334. length_mm / width_mm / height_mm 最终长/宽/高(mm)
  335. length_mm_raw / width_mm_raw 修正前的原始测量值
  336. width_from_front_mm 正面拍宽度(未用正面拍时为None)
  337. width_corrected 宽度是否经过修正
  338. result_images debug=True 时保存的标注图路径列表
  339. """
  340. calib = self.calib if isinstance(self.calib, dict) else load_json(self.calib)
  341. k = calib["k_mm_per_px"]
  342. kh = calib["kh_mm_per_px"]
  343. kl = calib.get("kl_mm_per_px")
  344. if kl is None and self.verbose:
  345. print("提示: 标定结果里没有 kl_mm_per_px(长度方向系数),长度将沿用kh换算。"
  346. "建议在 Calibrator 里提供 side_known_length_mm 以消除长度系统性偏差。")
  347. kw = calib.get("kw_mm_per_px")
  348. top_png = imread_unicode_unchanged(self.topdown_png)
  349. side_png = imread_unicode_unchanged(self.side_png)
  350. if top_png is None:
  351. raise FileNotFoundError(f"无法读取图片: {self.topdown_png}")
  352. if side_png is None:
  353. raise FileNotFoundError(f"无法读取图片: {self.side_png}")
  354. top_bg = _get_vis_background(top_png, self.topdown_original)
  355. side_bg = _get_vis_background(side_png, self.side_original)
  356. if self.verbose:
  357. print("正在处理顶拍抠图(测宽度)...")
  358. width_mm_raw, vis_top = _measure_width_topdown(top_png, top_bg, k)
  359. if self.verbose:
  360. print("正在处理侧拍抠图(测长度和高度)...")
  361. length_mm, height_mm, vis_side = _measure_length_height_side(side_png, side_bg, kh, kl)
  362. vis_front = None
  363. width_from_front = None
  364. if self.front_png:
  365. if not kw:
  366. if self.verbose:
  367. print("警告: 提供了 front_png 但标定结果里没有 kw_mm_per_px,"
  368. "请先在 Calibrator 里提供 front_png/front_known_width_mm。"
  369. "本次仍使用顶拍宽度。")
  370. else:
  371. front_png = imread_unicode_unchanged(self.front_png)
  372. if front_png is None:
  373. raise FileNotFoundError(f"无法读取图片: {self.front_png}")
  374. front_bg = _get_vis_background(front_png, None)
  375. if self.verbose:
  376. print("正在处理正面拍抠图(测宽度,能看到顶拍被遮挡的鞋底外沿)...")
  377. width_from_front, vis_front = _measure_width_front(front_png, front_bg, kw)
  378. print(f" 顶拍测宽度(可能被遮挡偏小): {width_mm_raw:.2f}mm")
  379. print(f" 正面拍测宽度(更准): {width_from_front:.2f}mm")
  380. width_mm = width_from_front if width_from_front is not None else width_mm_raw
  381. width_corrected = False
  382. length_mm_raw = length_mm
  383. if self.length_factor:
  384. length_mm = length_mm_raw / self.length_factor
  385. if self.verbose:
  386. print(f"\n[长度修正] 因子={self.length_factor:.6f}: "
  387. f"{length_mm_raw:.2f}mm -> {length_mm:.2f}mm")
  388. if width_from_front is None and self.width_slope is not None and self.width_scale0 is not None:
  389. # 修正模型(由标准块实验拟合得到,两部分),仅用于顶拍宽度:
  390. # 1) 减去随高度线性递增的透视放大量: width_slope * height
  391. # 2) 除以零高度时的固有放大倍数: width_scale0
  392. # 注意: 这个修正解决的是"透视外扩",不解决"鞋面遮挡鞋底"问题,
  393. # 后者只能靠 front_png 正面拍视角解决。
  394. width_mm = (width_mm_raw - self.width_slope * height_mm) / self.width_scale0
  395. width_corrected = True
  396. if self.verbose:
  397. print(f"\n[顶拍宽度透视修正] 斜率={self.width_slope:.6f}, "
  398. f"零高度放大={self.width_scale0:.4f}, 物体高度={height_mm:.1f}mm")
  399. print(f"[顶拍宽度透视修正] 修正前: {width_mm_raw:.2f}mm -> 修正后: {width_mm:.2f}mm")
  400. elif width_from_front is not None:
  401. width_corrected = True # 正面拍宽度本身就是"已修正"(更准)的结果
  402. if self.verbose:
  403. print("\n========== 测量结果 ==========")
  404. print(f"长 (length): {length_mm:.2f} mm")
  405. print(f"宽 (width) : {width_mm:.2f} mm"
  406. + (" (已修正)" if width_corrected else " (未修正)"))
  407. print(f"高 (height): {height_mm:.2f} mm")
  408. print("===============================")
  409. timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  410. timestamp_tag = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  411. outputs = []
  412. if self.debug:
  413. vis_top_final = _annotate_final_summary(
  414. vis_top, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
  415. vis_side_final = _annotate_final_summary(
  416. vis_side, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
  417. topdown_out = os.path.join(self.output_dir, f"result_topdown_{timestamp_tag}.jpg")
  418. side_out = os.path.join(self.output_dir, f"result_side_{timestamp_tag}.jpg")
  419. imwrite_unicode(topdown_out, vis_top_final)
  420. imwrite_unicode(side_out, vis_side_final)
  421. outputs = [topdown_out, side_out]
  422. if vis_front is not None:
  423. vis_front_final = _annotate_final_summary(
  424. vis_front, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
  425. front_out = os.path.join(self.output_dir, f"result_front_{timestamp_tag}.jpg")
  426. imwrite_unicode(front_out, vis_front_final)
  427. outputs.append(front_out)
  428. if self.verbose:
  429. print(f"标注结果图已保存: {', '.join(outputs)}")
  430. return {
  431. "length_mm": length_mm,
  432. "width_mm": width_mm,
  433. "height_mm": height_mm,
  434. "length_mm_raw": length_mm_raw,
  435. "width_mm_raw": width_mm_raw,
  436. "width_from_front_mm": width_from_front,
  437. "width_corrected": width_corrected,
  438. "result_images": outputs,
  439. }