| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- # -*- coding: utf-8 -*-
- """
- 把 calibrate.py 和 measure_shoe_v2.py 的逻辑拷贝并封装为两个类,
- 外部直接 new + 传参使用,不再依赖命令行和 calib.json 文件传递。
- - Calibrator : 标定。不保存 calib.json,calibrate() 直接返回标定结果 dict
- (键名与原来 calib.json 完全一致,可直接喂给 ShoeMeasurer)。
- - ShoeMeasurer : 测量。debug=True 时保存 result_*.jpg 标注图,
- measure() 返回长/宽/高结果 dict。
- 使用方法:
- from objmark_api import Calibrator, ShoeMeasurer
- # 1) 标定(结果直接拿到 dict,不落盘)
- calib = Calibrator(
- topdown_png="标定尺顶拍抠图.png",
- topdown_known_length_mm=300,
- side_png="标定块水平拍抠图.png",
- side_known_height_mm=100,
- side_known_length_mm=150, # 可选,单独标定长度方向kl
- front_png="标定块正面拍抠图.png", # 可选,单独标定宽度方向kw
- front_known_width_mm=60,
- ).calibrate()
- # 2) 测量(calib 可以直接传上面拿到的 dict,也可以传 calib.json 路径)
- result = ShoeMeasurer(
- topdown_png="鞋顶拍抠图.png",
- side_png="鞋水平拍抠图.png",
- front_png="鞋正面拍抠图.png", # 可选
- calib=calib,
- width_slope=0.085833, # 可选,顶拍宽度透视修正
- width_scale0=1.146419,
- length_factor=1.030640, # 可选,长度修正因子
- debug=True, # 保存 result_*.jpg 标注图
- ).measure()
- print(result["length_mm"], result["width_mm"], result["height_mm"])
- """
- import datetime
- import os
- import cv2
- import numpy as np
- from .turntable_calib_utils import (
- imread_unicode, imread_unicode_unchanged, imwrite_unicode, load_json,
- mask_from_alpha, composite_on_white, largest_contour
- )
- # ===========================================================================
- # Calibrator —— 拷贝自 calibrate.py,封装为类,不保存 calib.json
- # ===========================================================================
- def _calibrate_topdown_from_mask(png_path, known_length_mm):
- """顶拍标定:标定物轮廓最长边的像素长度 -> k(mm/像素)"""
- img = imread_unicode_unchanged(png_path)
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {png_path}")
- mask = mask_from_alpha(img)
- contour = largest_contour(mask)
- (cx, cy), (w_px, h_px), angle = cv2.minAreaRect(contour)
- length_px = max(w_px, h_px)
- if length_px < 1e-6:
- raise RuntimeError("顶拍标定物轮廓异常(长度接近0),请检查抠图是否正确。")
- k = known_length_mm / length_px
- return k, length_px
- def _calibrate_side_from_mask(png_path, known_height_mm, known_length_mm=None):
- """
- 水平拍标定:从同一个标定物同时提取
- - kh: 竖直方向比例系数(用轮廓最高点到最低点的像素高度)
- - kl: 水平方向比例系数(用轮廓最左到最右的像素宽度),需提供 known_length_mm
- 分开标定的原因:如果侧拍相机不是严格正对,或存在畸变,
- 水平和竖直方向的比例尺会不一致。
- """
- img = imread_unicode_unchanged(png_path)
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {png_path}")
- mask = mask_from_alpha(img)
- contour = largest_contour(mask)
- ys = contour[:, 0, 1]
- xs = contour[:, 0, 0]
- height_px = float(ys.max() - ys.min())
- length_px = float(xs.max() - xs.min())
- if height_px < 1e-6:
- raise RuntimeError("水平拍标定物轮廓异常(高度接近0),请检查抠图是否正确。")
- kh = known_height_mm / height_px
- kl = None
- if known_length_mm:
- if length_px < 1e-6:
- raise RuntimeError("水平拍标定物轮廓异常(水平跨度接近0),请检查抠图是否正确。")
- kl = known_length_mm / length_px
- return kh, height_px, kl, length_px
- def _calibrate_front_from_mask(png_path, known_width_mm):
- """
- 正面拍标定:从标定物正面照片提取水平方向比例系数 kw(mm/像素)。
- 原理与侧拍标定完全一致,只是拍摄角度换成正对物体的一端,测宽度而不是长度。
- """
- img = imread_unicode_unchanged(png_path)
- if img is None:
- raise FileNotFoundError(f"无法读取图片: {png_path}")
- mask = mask_from_alpha(img)
- contour = largest_contour(mask)
- xs = contour[:, 0, 0]
- width_px = float(xs.max() - xs.min())
- if width_px < 1e-6:
- raise RuntimeError("正面拍标定物轮廓异常(宽度接近0),请检查抠图是否正确。")
- kw = known_width_mm / width_px
- return kw, width_px
- class Calibrator:
- """
- 标定(类封装版)。new 时传入三张(或两张)标定物抠图PNG及各自的真实尺寸,
- 调用 calibrate() 直接返回标定结果 dict,不保存 calib.json。
- 参数(对应原 calibrate.py 的命令行参数):
- topdown_png 顶拍标定物抠图PNG(带alpha)
- topdown_known_length_mm 顶拍标定物真实长度(mm),取其最长边作为参照
- side_png 水平拍标定物抠图PNG(带alpha)
- side_known_height_mm 水平拍标定物真实高度(mm),竖直方向那条边
- side_known_length_mm 可选,水平拍标定物水平方向那条边的真实长度(mm),
- 用于单独标定长度方向系数kl
- front_png 可选,正面拍标定物抠图PNG(带alpha)
- front_known_width_mm 可选,正面拍标定物真实宽度(mm)
- verbose 是否打印标定过程/结果(默认True)
- """
- def __init__(self, topdown_png, topdown_known_length_mm,
- side_png, side_known_height_mm,
- side_known_length_mm=None,
- front_png=None, front_known_width_mm=None,
- verbose=True):
- self.topdown_png = topdown_png
- self.topdown_known_length_mm = topdown_known_length_mm
- self.side_png = side_png
- self.side_known_height_mm = side_known_height_mm
- self.side_known_length_mm = side_known_length_mm
- self.front_png = front_png
- self.front_known_width_mm = front_known_width_mm
- self.verbose = verbose
- def calibrate(self):
- """
- 执行标定,直接返回标定结果 dict(不落盘)。
- 返回键名与旧版 calib.json 完全一致:
- k_mm_per_px / kh_mm_per_px / [kl_mm_per_px] / [kw_mm_per_px]
- 以及对应的 known_* / pixel_* 记录字段
- """
- k, length_px = _calibrate_topdown_from_mask(
- self.topdown_png, self.topdown_known_length_mm
- )
- kh, height_px, kl, side_length_px = _calibrate_side_from_mask(
- self.side_png, self.side_known_height_mm, self.side_known_length_mm
- )
- result = {
- "k_mm_per_px": k,
- "topdown_known_length_mm": self.topdown_known_length_mm,
- "topdown_pixel_length": length_px,
- "kh_mm_per_px": kh,
- "side_known_height_mm": self.side_known_height_mm,
- "side_pixel_height": height_px,
- }
- if self.verbose:
- print("\n========== 标定结果 ==========")
- print(f"[顶拍/宽度] 标定物像素长度: {length_px:.2f} px , "
- f"真实长度: {self.topdown_known_length_mm:.2f} mm")
- print(f"[顶拍/宽度] 比例系数 k : {k:.6f} mm/像素")
- print(f"[侧拍/高度] 标定物像素高度: {height_px:.2f} px , "
- f"真实高度: {self.side_known_height_mm:.2f} mm")
- print(f"[侧拍/高度] 比例系数 kh: {kh:.6f} mm/像素")
- if kl:
- result["kl_mm_per_px"] = kl
- result["side_known_length_mm"] = self.side_known_length_mm
- result["side_pixel_length"] = side_length_px
- if self.verbose:
- print(f"[侧拍/长度] 标定物像素宽度: {side_length_px:.2f} px , "
- f"真实长度: {self.side_known_length_mm:.2f} mm")
- print(f"[侧拍/长度] 比例系数 kl: {kl:.6f} mm/像素")
- ratio = kl / kh
- print(f"\n[诊断] kl/kh = {ratio:.4f}")
- if abs(ratio - 1) > 0.05:
- print(f" ⚠ 水平与竖直方向比例系数相差 {abs(ratio - 1) * 100:.1f}%,"
- f"说明侧拍相机确实存在倾斜/畸变,"
- f"分开标定是必要的(这正是之前长度偏差的来源)。")
- else:
- print(f" 两个方向比例系数接近,侧拍视角基本正常。")
- elif self.verbose:
- print("[侧拍/长度] 未提供 side_known_length_mm,长度将沿用kh换算(可能有系统性偏差)")
- if self.front_png and self.front_known_width_mm:
- kw, front_width_px = _calibrate_front_from_mask(
- self.front_png, self.front_known_width_mm
- )
- result["kw_mm_per_px"] = kw
- result["front_known_width_mm"] = self.front_known_width_mm
- result["front_pixel_width"] = front_width_px
- if self.verbose:
- print(f"[正面拍/宽度] 标定物像素宽度: {front_width_px:.2f} px , "
- f"真实宽度: {self.front_known_width_mm:.2f} mm")
- print(f"[正面拍/宽度] 比例系数 kw: {kw:.6f} mm/像素")
- print("===============================") if self.verbose else None
- return result
- # ===========================================================================
- # ShoeMeasurer —— 拷贝自 measure_shoe_v2.py,封装为类,debug 参数可选保存结果图
- # ===========================================================================
- def _get_vis_background(png_img, original_path):
- """优先用原图做可视化背景(更真实),没提供原图则把抠图合成到白底上。"""
- if original_path:
- bg = imread_unicode(original_path)
- if bg is None:
- raise FileNotFoundError(f"无法读取原图: {original_path}")
- if bg.shape[:2] != png_img.shape[:2]:
- print(f"警告: 原图尺寸{bg.shape[:2]}与抠图PNG尺寸{png_img.shape[:2]}不一致,"
- f"标注框可能对不齐,建议确认两者是否为同一张照片导出。")
- return bg
- return composite_on_white(png_img)
- def _measure_width_front(png_img, vis_bg, kw_mm_per_px):
- """
- 正面拍图:测宽度(水平像素跨度×kw)。
- 相比顶拍测宽度,正面拍能看到被鞋面遮挡的鞋底外沿部分,对这类鞋型精度明显更高。
- """
- mask = mask_from_alpha(png_img)
- contour = largest_contour(mask)
- xs = contour[:, 0, 0]
- left_x, right_x = int(xs.min()), int(xs.max())
- width_px = right_x - left_x
- width_mm = width_px * kw_mm_per_px
- vis = vis_bg.copy()
- ys = contour[:, 0, 1]
- mid_y = int(np.mean(ys))
- cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
- cv2.line(vis, (left_x, mid_y), (right_x, mid_y), (255, 0, 0), 2)
- cv2.circle(vis, (left_x, mid_y), 6, (255, 0, 0), -1)
- cv2.circle(vis, (right_x, mid_y), 6, (255, 0, 0), -1)
- cv2.putText(vis, f"W(front)={width_mm:.1f}mm",
- (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 0, 0), 2)
- return width_mm, vis
- def _measure_width_topdown(png_img, vis_bg, k_mm_per_px):
- """顶拍图:只测宽度(minAreaRect短边),不再用顶拍算长度(避免鞋子高度导致的透视外扩误差)"""
- mask = mask_from_alpha(png_img)
- contour = largest_contour(mask)
- (cx, cy), (w_px, h_px), angle = cv2.minAreaRect(contour)
- width_px = min(w_px, h_px)
- width_mm = width_px * k_mm_per_px
- vis = vis_bg.copy()
- box = np.intp(cv2.boxPoints(((cx, cy), (w_px, h_px), angle)))
- cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
- cv2.drawContours(vis, [box], 0, (0, 0, 255), 2)
- cv2.putText(vis, f"W={width_mm:.1f}mm",
- (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
- return width_mm, vis
- def _measure_length_height_side(png_img, vis_bg, kh_mm_per_px, kl_mm_per_px=None):
- """
- 侧拍图:测长度(水平像素跨度×kl)和高度(竖直像素跨度×kh)。
- kl 与 kh 分开使用,因为侧拍相机若有倾斜/畸变,水平与竖直方向比例尺不同。
- 若未提供 kl,则退回沿用 kh,但长度可能有系统性偏差。
- """
- if kl_mm_per_px is None:
- kl_mm_per_px = kh_mm_per_px
- mask = mask_from_alpha(png_img)
- contour = largest_contour(mask)
- xs = contour[:, 0, 0]
- ys = contour[:, 0, 1]
- left_x, right_x = int(xs.min()), int(xs.max())
- top_y, bottom_y = int(ys.min()), int(ys.max())
- length_px = right_x - left_x
- height_px = bottom_y - top_y
- length_mm = length_px * kl_mm_per_px
- height_mm = height_px * kh_mm_per_px
- vis = vis_bg.copy()
- mid_y = int(np.mean(ys))
- mid_x = int(np.mean(xs))
- cv2.drawContours(vis, [contour], -1, (0, 255, 0), 2)
- # 长度标注线(水平)
- cv2.line(vis, (left_x, mid_y), (right_x, mid_y), (255, 0, 0), 2)
- cv2.circle(vis, (left_x, mid_y), 6, (255, 0, 0), -1)
- cv2.circle(vis, (right_x, mid_y), 6, (255, 0, 0), -1)
- # 高度标注线(竖直)
- cv2.line(vis, (mid_x, top_y), (mid_x, bottom_y), (0, 0, 255), 2)
- cv2.circle(vis, (mid_x, top_y), 6, (0, 0, 255), -1)
- cv2.circle(vis, (mid_x, bottom_y), 6, (0, 0, 255), -1)
- cv2.putText(vis, f"L={length_mm:.1f}mm H={height_mm:.1f}mm",
- (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2)
- return length_mm, height_mm, vis
- def _annotate_final_summary(vis, length_mm, width_mm, height_mm,
- timestamp_str, width_corrected):
- """
- 在图片左上角统一标出这次测量的完整长/宽/高结果(mm)和时间戳,
- 每张结果图都会加这个统一信息条,方便单独看某一张图时也能知道完整结果。
- """
- vis = vis.copy()
- lines = [
- f"L={length_mm:.1f}mm W={width_mm:.1f}mm"
- f"{' (adjusted)' if width_corrected else ''} H={height_mm:.1f}mm",
- f"{timestamp_str}",
- ]
- # 用不透明底色条完全盖住图片本身已有的测量标注文字,避免重影
- box_h = 140 * len(lines) + 20
- cv2.rectangle(vis, (0, 0), (580, box_h), (255, 255, 255), -1)
- y = 140
- for line in lines:
- cv2.putText(vis, line, (15, y), cv2.FONT_HERSHEY_SIMPLEX, 4.8,
- (0, 0, 200), 2, cv2.LINE_AA)
- y += 140
- return vis
- class ShoeMeasurer:
- """
- 测量(类封装版)。new 时传入各视角抠图PNG、标定结果及可选修正参数,
- 调用 measure() 返回长/宽/高结果 dict。
- debug=True 时把 result_*.jpg 标注图保存到 output_dir 目录。
- 参数(对应原 measure_shoe_v2.py 的命令行参数):
- topdown_png 顶拍抠图PNG(带alpha)
- side_png 水平拍抠图PNG(带alpha)
- front_png 可选,正面拍抠图PNG(带alpha),提供后宽度改用正面拍测量
- topdown_original 可选,顶拍原图(可视化背景)
- side_original 可选,水平拍原图(可视化背景)
- calib 标定结果:Calibrator.calibrate() 返回的 dict,
- 或 calib.json 文件路径(str),默认 "calib.json"
- width_slope 可选,宽度修正的高度斜率(mm宽度/mm高度)
- width_scale0 可选,宽度在零高度时的固有放大倍数
- (以上两个配合做顶拍宽度透视修正)
- length_factor 可选,长度修正因子(测出值除以此数)
- debug 可选,是否保存 result_*.jpg 标注图(默认False)
- output_dir debug保存目录(默认当前目录)
- verbose 是否打印测量过程/结果(默认True)
- """
- def __init__(self, topdown_png, side_png, front_png=None,
- topdown_original=None, side_original=None,
- calib="calib.json",
- width_slope=None, width_scale0=None,
- length_factor=None,
- debug=False, output_dir=".",
- verbose=True):
- self.topdown_png = topdown_png
- self.side_png = side_png
- self.front_png = front_png
- self.topdown_original = topdown_original
- self.side_original = side_original
- self.calib = calib
- self.width_slope = width_slope
- self.width_scale0 = width_scale0
- self.length_factor = length_factor
- self.debug = debug
- self.output_dir = output_dir
- self.verbose = verbose
- def measure(self):
- """
- 执行测量,返回结果 dict:
- length_mm / width_mm / height_mm 最终长/宽/高(mm)
- length_mm_raw / width_mm_raw 修正前的原始测量值
- width_from_front_mm 正面拍宽度(未用正面拍时为None)
- width_corrected 宽度是否经过修正
- result_images debug=True 时保存的标注图路径列表
- """
- calib = self.calib if isinstance(self.calib, dict) else load_json(self.calib)
- k = calib["k_mm_per_px"]
- kh = calib["kh_mm_per_px"]
- kl = calib.get("kl_mm_per_px")
- if kl is None and self.verbose:
- print("提示: 标定结果里没有 kl_mm_per_px(长度方向系数),长度将沿用kh换算。"
- "建议在 Calibrator 里提供 side_known_length_mm 以消除长度系统性偏差。")
- kw = calib.get("kw_mm_per_px")
- top_png = imread_unicode_unchanged(self.topdown_png)
- side_png = imread_unicode_unchanged(self.side_png)
- if top_png is None:
- raise FileNotFoundError(f"无法读取图片: {self.topdown_png}")
- if side_png is None:
- raise FileNotFoundError(f"无法读取图片: {self.side_png}")
- top_bg = _get_vis_background(top_png, self.topdown_original)
- side_bg = _get_vis_background(side_png, self.side_original)
- if self.verbose:
- print("正在处理顶拍抠图(测宽度)...")
- width_mm_raw, vis_top = _measure_width_topdown(top_png, top_bg, k)
- if self.verbose:
- print("正在处理侧拍抠图(测长度和高度)...")
- length_mm, height_mm, vis_side = _measure_length_height_side(side_png, side_bg, kh, kl)
- vis_front = None
- width_from_front = None
- if self.front_png:
- if not kw:
- if self.verbose:
- print("警告: 提供了 front_png 但标定结果里没有 kw_mm_per_px,"
- "请先在 Calibrator 里提供 front_png/front_known_width_mm。"
- "本次仍使用顶拍宽度。")
- else:
- front_png = imread_unicode_unchanged(self.front_png)
- if front_png is None:
- raise FileNotFoundError(f"无法读取图片: {self.front_png}")
- front_bg = _get_vis_background(front_png, None)
- if self.verbose:
- print("正在处理正面拍抠图(测宽度,能看到顶拍被遮挡的鞋底外沿)...")
- width_from_front, vis_front = _measure_width_front(front_png, front_bg, kw)
- print(f" 顶拍测宽度(可能被遮挡偏小): {width_mm_raw:.2f}mm")
- print(f" 正面拍测宽度(更准): {width_from_front:.2f}mm")
- width_mm = width_from_front if width_from_front is not None else width_mm_raw
- width_corrected = False
- length_mm_raw = length_mm
- if self.length_factor:
- length_mm = length_mm_raw / self.length_factor
- if self.verbose:
- print(f"\n[长度修正] 因子={self.length_factor:.6f}: "
- f"{length_mm_raw:.2f}mm -> {length_mm:.2f}mm")
- if width_from_front is None and self.width_slope is not None and self.width_scale0 is not None:
- # 修正模型(由标准块实验拟合得到,两部分),仅用于顶拍宽度:
- # 1) 减去随高度线性递增的透视放大量: width_slope * height
- # 2) 除以零高度时的固有放大倍数: width_scale0
- # 注意: 这个修正解决的是"透视外扩",不解决"鞋面遮挡鞋底"问题,
- # 后者只能靠 front_png 正面拍视角解决。
- width_mm = (width_mm_raw - self.width_slope * height_mm) / self.width_scale0
- width_corrected = True
- if self.verbose:
- print(f"\n[顶拍宽度透视修正] 斜率={self.width_slope:.6f}, "
- f"零高度放大={self.width_scale0:.4f}, 物体高度={height_mm:.1f}mm")
- print(f"[顶拍宽度透视修正] 修正前: {width_mm_raw:.2f}mm -> 修正后: {width_mm:.2f}mm")
- elif width_from_front is not None:
- width_corrected = True # 正面拍宽度本身就是"已修正"(更准)的结果
- if self.verbose:
- print("\n========== 测量结果 ==========")
- print(f"长 (length): {length_mm:.2f} mm")
- print(f"宽 (width) : {width_mm:.2f} mm"
- + (" (已修正)" if width_corrected else " (未修正)"))
- print(f"高 (height): {height_mm:.2f} mm")
- print("===============================")
- timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
- timestamp_tag = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
- outputs = []
- if self.debug:
- vis_top_final = _annotate_final_summary(
- vis_top, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
- vis_side_final = _annotate_final_summary(
- vis_side, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
- topdown_out = os.path.join(self.output_dir, f"result_topdown_{timestamp_tag}.jpg")
- side_out = os.path.join(self.output_dir, f"result_side_{timestamp_tag}.jpg")
- imwrite_unicode(topdown_out, vis_top_final)
- imwrite_unicode(side_out, vis_side_final)
- outputs = [topdown_out, side_out]
- if vis_front is not None:
- vis_front_final = _annotate_final_summary(
- vis_front, length_mm, width_mm, height_mm, timestamp_str, width_corrected)
- front_out = os.path.join(self.output_dir, f"result_front_{timestamp_tag}.jpg")
- imwrite_unicode(front_out, vis_front_final)
- outputs.append(front_out)
- if self.verbose:
- print(f"标注结果图已保存: {', '.join(outputs)}")
- return {
- "length_mm": length_mm,
- "width_mm": width_mm,
- "height_mm": height_mm,
- "length_mm_raw": length_mm_raw,
- "width_mm_raw": width_mm_raw,
- "width_from_front_mm": width_from_front,
- "width_corrected": width_corrected,
- "result_images": outputs,
- }
|