turntable_calib_utils.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. """
  3. 共享工具函数:
  4. - imread_unicode: 兼容 Windows 中文路径的图片读取
  5. - segment_object_grabcut: 用 GrabCut 从背景(转盘)中分割出物体,返回二值掩码
  6. - pick_two_points: 交互式点选2个点(用于标定时点选已知长度的两端)
  7. - select_roi_unicode: 封装 cv2.selectROI,用于框选物体大致范围供 GrabCut 使用
  8. """
  9. import cv2
  10. import numpy as np
  11. import json
  12. import os
  13. def imread_unicode(path):
  14. """兼容 Windows 下含中文/特殊字符路径的图片读取(cv2.imread 对非ASCII路径常读取失败)。"""
  15. data = np.fromfile(path, dtype=np.uint8)
  16. img = cv2.imdecode(data, cv2.IMREAD_COLOR)
  17. return img
  18. def imread_unicode_unchanged(path):
  19. """
  20. 同上,但保留 alpha 通道(用于读取抠图后的透明背景PNG)。
  21. 返回的图像若原图有透明通道,shape为 (H,W,4),否则为 (H,W,3)。
  22. """
  23. data = np.fromfile(path, dtype=np.uint8)
  24. img = cv2.imdecode(data, cv2.IMREAD_UNCHANGED)
  25. return img
  26. def mask_from_alpha(png_img, alpha_thresh=127):
  27. """
  28. 从抠图PNG的alpha通道直接生成二值掩码,不需要GrabCut/手动框选。
  29. 要求 png_img 是4通道(BGRA)图像。
  30. """
  31. if png_img is None:
  32. raise RuntimeError("图片读取失败(为None)。")
  33. if png_img.ndim != 3 or png_img.shape[2] != 4:
  34. raise RuntimeError(
  35. "该图片没有透明通道(不是4通道BGRA的PNG),无法从alpha直接取掩码。"
  36. "请确认传入的是抠图后的透明背景PNG,而不是普通jpg/无透明通道的png。"
  37. )
  38. alpha = png_img[:, :, 3]
  39. mask = np.where(alpha > alpha_thresh, 255, 0).astype(np.uint8)
  40. # 轻微开闭运算去除抠图边缘的毛刺噪点,不改变整体轮廓
  41. kernel = np.ones((3, 3), np.uint8)
  42. mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  43. mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  44. return mask
  45. def composite_on_white(bgra_img):
  46. """
  47. 把带透明通道的抠图结果合成到白色背景上,得到一张普通3通道BGR图,
  48. 用于在没有提供原图时也能生成清晰的可视化标注图。
  49. """
  50. b, g, r, a = cv2.split(bgra_img.astype(np.float32))
  51. alpha = a / 255.0
  52. white = np.full_like(b, 255.0)
  53. out_b = b * alpha + white * (1 - alpha)
  54. out_g = g * alpha + white * (1 - alpha)
  55. out_r = r * alpha + white * (1 - alpha)
  56. return cv2.merge([out_b, out_g, out_r]).astype(np.uint8)
  57. def imwrite_unicode(path, img):
  58. """兼容 Windows 中文路径的图片写入。"""
  59. ext = os.path.splitext(path)[1]
  60. ok, buf = cv2.imencode(ext, img)
  61. if ok:
  62. buf.tofile(path)
  63. return ok
  64. def save_json(path, data):
  65. with open(path, "w", encoding="utf-8") as f:
  66. json.dump(data, f, ensure_ascii=False, indent=2)
  67. def load_json(path):
  68. with open(path, "r", encoding="utf-8") as f:
  69. return json.load(f)
  70. def select_roi_unicode(window_name, img, max_display_width=1200):
  71. """
  72. 框选物体大致范围(拖出一个矩形框住鞋子,尽量贴合但不用太精确)。
  73. 大图会先等比缩小显示,避免窗口超出屏幕,选完自动换算回原图坐标。
  74. 操作:鼠标拖拽画框 -> 按空格/回车确认 -> 按c取消重选。
  75. """
  76. h, w = img.shape[:2]
  77. scale = min(1.0, max_display_width / w)
  78. disp = cv2.resize(img, (int(w * scale), int(h * scale))) if scale < 1.0 else img.copy()
  79. print(f"[{window_name}] 请用鼠标拖拽框选出物体大致范围(不用太精确,比物体略大即可),"
  80. f"框完按空格/回车确认。")
  81. r = cv2.selectROI(window_name, disp, showCrosshair=True, fromCenter=False)
  82. cv2.destroyWindow(window_name)
  83. x, y, rw, rh = r
  84. if rw == 0 or rh == 0:
  85. raise RuntimeError("未选择有效区域,请重新运行并框选物体。")
  86. # 换算回原图坐标
  87. return (int(x / scale), int(y / scale), int(rw / scale), int(rh / scale))
  88. def segment_object_grabcut(img, rect, iter_count=5, margin=0.06):
  89. """
  90. 用 GrabCut 从背景(转盘/桌面)中把物体分割出来。
  91. rect: (x, y, w, h) 物体大致所在的框选区域(来自 select_roi_unicode)
  92. margin: 在框选区域基础上再收缩一点作为"确定前景"的种子区域,提高分割稳定性
  93. 返回: 二值掩码(255=物体, 0=背景),与原图同尺寸
  94. """
  95. mask = np.zeros(img.shape[:2], np.uint8)
  96. bgd_model = np.zeros((1, 65), np.float64)
  97. fgd_model = np.zeros((1, 65), np.float64)
  98. cv2.grabCut(img, mask, rect, bgd_model, fgd_model, iter_count, cv2.GC_INIT_WITH_RECT)
  99. mask2 = np.where((mask == cv2.GC_FGD) | (mask == cv2.GC_PR_FGD), 255, 0).astype(np.uint8)
  100. # 形态学开闭运算去除噪点、填补小空洞
  101. kernel = np.ones((5, 5), np.uint8)
  102. mask2 = cv2.morphologyEx(mask2, cv2.MORPH_OPEN, kernel)
  103. mask2 = cv2.morphologyEx(mask2, cv2.MORPH_CLOSE, kernel)
  104. return mask2
  105. def largest_contour(mask):
  106. """从二值掩码中取面积最大的轮廓(即物体主体,排除噪点)。"""
  107. contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  108. if not contours:
  109. raise RuntimeError("未能从分割结果中找到有效轮廓,请重新框选或检查背景是否干净。")
  110. return max(contours, key=cv2.contourArea)
  111. class TwoPointPicker:
  112. """
  113. 交互式点选2个点,用于标定时点出已知长度参照物的两端
  114. (比如标定尺的0刻度和300mm刻度,或标定块的顶部和底部)。
  115. """
  116. def __init__(self, window_name, img, labels=("点1", "点2"), max_display_width=1400):
  117. self.window_name = window_name
  118. self.orig_img = img
  119. h, w = img.shape[:2]
  120. self.scale = min(1.0, max_display_width / w)
  121. self.img = cv2.resize(img, (int(w * self.scale), int(h * self.scale))) \
  122. if self.scale < 1.0 else img.copy()
  123. self.display = self.img.copy()
  124. self.points = []
  125. self.labels = labels
  126. def _on_mouse(self, event, x, y, flags, param):
  127. if event == cv2.EVENT_LBUTTONDOWN and len(self.points) < 2:
  128. self.points.append((x, y))
  129. self._redraw()
  130. def _redraw(self):
  131. self.display = self.img.copy()
  132. for i, (x, y) in enumerate(self.points):
  133. cv2.circle(self.display, (x, y), 6, (0, 0, 255), -1)
  134. cv2.putText(self.display, self.labels[i], (x + 8, y - 8),
  135. cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
  136. if len(self.points) == 2:
  137. cv2.line(self.display, self.points[0], self.points[1], (0, 255, 0), 2)
  138. cv2.imshow(self.window_name, self.display)
  139. def run(self):
  140. cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
  141. cv2.setMouseCallback(self.window_name, self._on_mouse)
  142. print(f"请依次点击: {self.labels[0]} -> {self.labels[1]}")
  143. print("点完2个点后按任意键确认,按 r 重来。")
  144. self._redraw()
  145. while True:
  146. key = cv2.waitKey(20) & 0xFF
  147. if key == ord('r'):
  148. self.points = []
  149. self._redraw()
  150. elif len(self.points) == 2 and key != 255:
  151. break
  152. cv2.destroyAllWindows()
  153. # 换算回原图坐标
  154. return [(x / self.scale, y / self.scale) for x, y in self.points]