| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- # -*- coding: utf-8 -*-
- """
- 共享工具函数:
- - imread_unicode: 兼容 Windows 中文路径的图片读取
- - segment_object_grabcut: 用 GrabCut 从背景(转盘)中分割出物体,返回二值掩码
- - pick_two_points: 交互式点选2个点(用于标定时点选已知长度的两端)
- - select_roi_unicode: 封装 cv2.selectROI,用于框选物体大致范围供 GrabCut 使用
- """
- import cv2
- import numpy as np
- import json
- import os
- def imread_unicode(path):
- """兼容 Windows 下含中文/特殊字符路径的图片读取(cv2.imread 对非ASCII路径常读取失败)。"""
- data = np.fromfile(path, dtype=np.uint8)
- img = cv2.imdecode(data, cv2.IMREAD_COLOR)
- return img
- def imread_unicode_unchanged(path):
- """
- 同上,但保留 alpha 通道(用于读取抠图后的透明背景PNG)。
- 返回的图像若原图有透明通道,shape为 (H,W,4),否则为 (H,W,3)。
- """
- data = np.fromfile(path, dtype=np.uint8)
- img = cv2.imdecode(data, cv2.IMREAD_UNCHANGED)
- return img
- def mask_from_alpha(png_img, alpha_thresh=127):
- """
- 从抠图PNG的alpha通道直接生成二值掩码,不需要GrabCut/手动框选。
- 要求 png_img 是4通道(BGRA)图像。
- """
- if png_img is None:
- raise RuntimeError("图片读取失败(为None)。")
- if png_img.ndim != 3 or png_img.shape[2] != 4:
- raise RuntimeError(
- "该图片没有透明通道(不是4通道BGRA的PNG),无法从alpha直接取掩码。"
- "请确认传入的是抠图后的透明背景PNG,而不是普通jpg/无透明通道的png。"
- )
- alpha = png_img[:, :, 3]
- mask = np.where(alpha > alpha_thresh, 255, 0).astype(np.uint8)
- # 轻微开闭运算去除抠图边缘的毛刺噪点,不改变整体轮廓
- kernel = np.ones((3, 3), np.uint8)
- mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
- mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
- return mask
- def composite_on_white(bgra_img):
- """
- 把带透明通道的抠图结果合成到白色背景上,得到一张普通3通道BGR图,
- 用于在没有提供原图时也能生成清晰的可视化标注图。
- """
- b, g, r, a = cv2.split(bgra_img.astype(np.float32))
- alpha = a / 255.0
- white = np.full_like(b, 255.0)
- out_b = b * alpha + white * (1 - alpha)
- out_g = g * alpha + white * (1 - alpha)
- out_r = r * alpha + white * (1 - alpha)
- return cv2.merge([out_b, out_g, out_r]).astype(np.uint8)
- def imwrite_unicode(path, img):
- """兼容 Windows 中文路径的图片写入。"""
- ext = os.path.splitext(path)[1]
- ok, buf = cv2.imencode(ext, img)
- if ok:
- buf.tofile(path)
- return ok
- def save_json(path, data):
- with open(path, "w", encoding="utf-8") as f:
- json.dump(data, f, ensure_ascii=False, indent=2)
- def load_json(path):
- with open(path, "r", encoding="utf-8") as f:
- return json.load(f)
- def select_roi_unicode(window_name, img, max_display_width=1200):
- """
- 框选物体大致范围(拖出一个矩形框住鞋子,尽量贴合但不用太精确)。
- 大图会先等比缩小显示,避免窗口超出屏幕,选完自动换算回原图坐标。
- 操作:鼠标拖拽画框 -> 按空格/回车确认 -> 按c取消重选。
- """
- h, w = img.shape[:2]
- scale = min(1.0, max_display_width / w)
- disp = cv2.resize(img, (int(w * scale), int(h * scale))) if scale < 1.0 else img.copy()
- print(f"[{window_name}] 请用鼠标拖拽框选出物体大致范围(不用太精确,比物体略大即可),"
- f"框完按空格/回车确认。")
- r = cv2.selectROI(window_name, disp, showCrosshair=True, fromCenter=False)
- cv2.destroyWindow(window_name)
- x, y, rw, rh = r
- if rw == 0 or rh == 0:
- raise RuntimeError("未选择有效区域,请重新运行并框选物体。")
- # 换算回原图坐标
- return (int(x / scale), int(y / scale), int(rw / scale), int(rh / scale))
- def segment_object_grabcut(img, rect, iter_count=5, margin=0.06):
- """
- 用 GrabCut 从背景(转盘/桌面)中把物体分割出来。
- rect: (x, y, w, h) 物体大致所在的框选区域(来自 select_roi_unicode)
- margin: 在框选区域基础上再收缩一点作为"确定前景"的种子区域,提高分割稳定性
- 返回: 二值掩码(255=物体, 0=背景),与原图同尺寸
- """
- mask = np.zeros(img.shape[:2], np.uint8)
- bgd_model = np.zeros((1, 65), np.float64)
- fgd_model = np.zeros((1, 65), np.float64)
- cv2.grabCut(img, mask, rect, bgd_model, fgd_model, iter_count, cv2.GC_INIT_WITH_RECT)
- mask2 = np.where((mask == cv2.GC_FGD) | (mask == cv2.GC_PR_FGD), 255, 0).astype(np.uint8)
- # 形态学开闭运算去除噪点、填补小空洞
- kernel = np.ones((5, 5), np.uint8)
- mask2 = cv2.morphologyEx(mask2, cv2.MORPH_OPEN, kernel)
- mask2 = cv2.morphologyEx(mask2, cv2.MORPH_CLOSE, kernel)
- return mask2
- def largest_contour(mask):
- """从二值掩码中取面积最大的轮廓(即物体主体,排除噪点)。"""
- contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- if not contours:
- raise RuntimeError("未能从分割结果中找到有效轮廓,请重新框选或检查背景是否干净。")
- return max(contours, key=cv2.contourArea)
- class TwoPointPicker:
- """
- 交互式点选2个点,用于标定时点出已知长度参照物的两端
- (比如标定尺的0刻度和300mm刻度,或标定块的顶部和底部)。
- """
- def __init__(self, window_name, img, labels=("点1", "点2"), max_display_width=1400):
- self.window_name = window_name
- self.orig_img = img
- h, w = img.shape[:2]
- self.scale = min(1.0, max_display_width / w)
- self.img = cv2.resize(img, (int(w * self.scale), int(h * self.scale))) \
- if self.scale < 1.0 else img.copy()
- self.display = self.img.copy()
- self.points = []
- self.labels = labels
- def _on_mouse(self, event, x, y, flags, param):
- if event == cv2.EVENT_LBUTTONDOWN and len(self.points) < 2:
- self.points.append((x, y))
- self._redraw()
- def _redraw(self):
- self.display = self.img.copy()
- for i, (x, y) in enumerate(self.points):
- cv2.circle(self.display, (x, y), 6, (0, 0, 255), -1)
- cv2.putText(self.display, self.labels[i], (x + 8, y - 8),
- cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
- if len(self.points) == 2:
- cv2.line(self.display, self.points[0], self.points[1], (0, 255, 0), 2)
- cv2.imshow(self.window_name, self.display)
- def run(self):
- cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL)
- cv2.setMouseCallback(self.window_name, self._on_mouse)
- print(f"请依次点击: {self.labels[0]} -> {self.labels[1]}")
- print("点完2个点后按任意键确认,按 r 重来。")
- self._redraw()
- while True:
- key = cv2.waitKey(20) & 0xFF
- if key == ord('r'):
- self.points = []
- self._redraw()
- elif len(self.points) == 2 and key != 255:
- break
- cv2.destroyAllWindows()
- # 换算回原图坐标
- return [(x / self.scale, y / self.scale) for x, y in self.points]
|