# 碗尺寸识别模块 (bowl_size) > 基于纯几何视觉方法的大/中/小碗自动识别方案,无需深度学习模型,轻量、快速、可解释。 --- ## 目录 1. [为什么不用深度学习?](#为什么不用深度学习) 2. [核心思路:几何先验](#核心思路几何先验) 3. [检测算法详解](#检测算法详解) - [预处理:CLAHE 对比度增强](#1-预处理clahe-对比度增强) - [方法一:霍夫圆变换](#2-方法一霍夫圆变换) - [方法二:轮廓法](#3-方法二轮廓法) - [双路融合策略](#4-双路融合策略) - [自适应降级重试](#5-自适应降级重试) 4. [标定系统](#标定系统) 5. [尺寸分类逻辑](#尺寸分类逻辑) 6. [文件结构](#文件结构) 7. [使用流程](#使用流程) 8. [调参指南](#调参指南) 9. [已知局限性与改进方向](#已知局限性与改进方向) --- ## 为什么不用深度学习? 直觉上,"识别大中小碗"应该用神经网络分类器。但本场景有一个极强的物理先验: > **摄像头安装高度固定 → 碗的像素直径与真实直径成线性比例关系。** 这意味着只需要量出碗在图像中的像素直径,就可以直接判断是大/中/小碗,而不需要学习任何"视觉特征"。 | 维度 | 深度学习方案 | 本方案(纯几何) | |------|-------------|----------------| | 数据需求 | 每类 100+ 张 | **一次标定即可** | | 部署新秤 | 重新采集 + 训练 | **重跑标定脚本(5分钟)** | | 可解释性 | 黑盒 | **直接看像素直径** | | 计算开销 | GPU / 较慢 | **CPU 毫秒级** | | 适用性 | 高通用性 | 依赖固定安装高度 | --- ## 核心思路:几何先验 ``` 摄像头(固定高度 H) | | H 固定 | ┌─────┴─────┐ │ 秤台 │ │ [ 碗 ] │ └───────────┘ 真实直径 D(cm) ←→ 像素直径 d(px) 关系:d = k × D (k 是与安装高度相关的比例系数) ``` 由于 `k` 对于固定安装的摄像头是常数,我们不需要求出 `k` 的具体值,只需要: 1. **标定阶段**:拍大/中/小碗图片,记录每种碗的 `d`(像素直径)均值 2. **推理阶段**:检测新图像中碗的 `d`,与标定阈值比较,直接输出大/中/小 --- ## 检测算法详解 核心代码在 `bowl_detector.py` 的 `detect()` 方法中。整体流程: ``` 原始图像 (BGR) │ ▼ 1. CLAHE 对比度增强(处理浅色碗+浅色背景) │ ▼ 2. 高斯模糊(去除噪点) │ ├──► 霍夫圆变换(HoughCircles)────┐ │ │ └──► 轮廓法(Canny + findContours)─┤ │ ▼ 双路结果融合 │ (两路都失败?→ 宽松参数重试) │ ▼ 尺寸分类 │ 大 / 中 / 小碗 ``` --- ### 1. 预处理:CLAHE 对比度增强 **问题**:白色/米色碗放在浅灰色背景上,边缘对比度极低,Canny 检测不到边。 **CLAHE(限制对比度自适应直方图均衡化)** 是一种局部对比度增强算法: ```python clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) ``` 原理: - 将图像切成 8×8 的小块(tile) - 对每个小块独立做直方图均衡化 - `clipLimit=3.0` 限制放大倍数,防止噪点被过度放大 - 相邻块之间做双线性插值,避免块状伪影 效果:即使背景和碗颜色接近,碗边缘的微弱灰度差也会被局部放大,让后续边缘检测能找到碗边。 --- ### 2. 方法一:霍夫圆变换 霍夫圆变换(Hough Circle Transform)是 OpenCV 专门用于检测圆形的经典算法。 **参数说明(重要!):** ```python cv2.HoughCircles( image, cv2.HOUGH_GRADIENT, dp=1.2, # 分辨率倒数比(1=与原图同分辨率,>1降采样加速) minDist=xxx, # 两个圆心之间的最小距离(防止重复检测同一个碗) param1=60, # Canny边缘检测的高阈值(内部调用) param2=25, # 圆心累加器阈值,越小越容易检测到(也越容易误检) minRadius=xxx, # 最小圆半径(像素) maxRadius=xxx, # 最大圆半径(像素) ) ``` **`param2` 是最关键的参数**: - 值越大 → 只检测"完美圆",漏检多 - 值越小 → 容易检测到"不完美圆",误检多 - 本方案默认 25,对于碗这种规则形状已经足够 **为什么选最大半径的圆?** ```python best = max(circles, key=lambda c: c[2]) # c[2] 是半径 ``` 碗边是最大的圆形轮廓,碗底花纹、碗内装饰线都是更小的圆,取最大的才是碗边。 --- ### 3. 方法二:轮廓法 作为霍夫圆的备用方案,用 Canny 边缘检测 + 轮廓分析来找圆。 **步骤:** ```python # 1. Canny 边缘检测 edges = cv2.Canny(blurred, threshold1=20, threshold2=60) # 2. 膨胀边缘(让断裂的边缘连接起来) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) edges = cv2.dilate(edges, kernel, iterations=1) # 3. 找外部轮廓 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ``` **筛选条件:** 1. **面积过滤**:轮廓面积必须在图像面积的 3%~95% 之间(太小是噪点,太大是背景) 2. **圆度过滤**:`circularity = 4π × 面积 / 周长²`,值为 1.0 表示完美圆,本方案要求 ≥ 0.5 ``` circularity 直觉示例: 圆形 ≈ 1.0 正方形 ≈ 0.785 细长条 ≈ 0.1 ``` **评分机制:** ```python score = circularity × area ``` 越圆、越大的轮廓得分越高 → 最终选出最可能是碗边缘的轮廓。 --- ### 4. 双路融合策略 ```python def _merge_results(self, hough, contour): if hough and contour: # 两路都成功 → 取平均,互相校验,提高精度 d = (hough["diameter"] + contour["diameter"]) / 2 ... return {"method": "hough+contour", ...} elif hough: return hough # 只有霍夫成功 elif contour: return contour # 只有轮廓成功 return {"diameter": 0, ...} # 全部失败 ``` 两路方法的优缺点互补: | | 霍夫圆变换 | 轮廓法 | |--|-----------|-------| | 擅长 | 圆弧不完整也能检测 | 任意形状,更灵活 | | 弱点 | 噪声敏感,参数敏感 | 需要轮廓连续完整 | 融合取平均后,单方误差对最终结果的影响减半。 --- ### 5. 自适应降级重试 如果两路都失败(比如图像质量极差),自动用更宽松的参数再试一次: ```python if hough_result is None and contour_result is None: relaxed_cfg = dict(cfg) relaxed_cfg["hough_param2"] //= 2 # 更容易检测到圆 relaxed_cfg["canny_threshold1"] //= 2 # 更敏感的边缘检测 relaxed_cfg["min_circularity"] = 0.4 # 允许更不圆的轮廓 # 用宽松参数重试... ``` 这是一个"渐进式降级"策略,优先保证精度,实在不行再放宽约束。 --- ## 标定系统 标定是本方案的核心步骤,**只需做一次**,永久有效(除非更换摄像头安装高度)。 ### 标定原理 ``` 大碗样本均值: d_large ≈ 420 px 中碗样本均值: d_medium ≈ 320 px 小碗样本均值: d_small ≈ 220 px 小碗阈值上限 = (d_small + d_medium) / 2 = 270 px 中碗阈值上限 = (d_medium + d_large) / 2 = 370 px 推理时: d ≤ 270 → 小碗 d ≤ 370 → 中碗 d > 370 → 大碗 ``` 取两档均值的**中点**作为阈值,使得各类别的误判容限最大化。 ### 标定数据保存到 config.json ```json { "calibrated": true, "thresholds": { "small_max_diameter": 270.0, "medium_max_diameter": 370.0 }, "calibration_averages": { "small": 220.0, "medium": 320.0, "large": 420.0 }, "bowl_weights_grams": { "small": 180, "medium": 260, "large": 350 } } ``` --- ## 尺寸分类逻辑 分类后还会给出一个**置信度**,反映检测到的直径距离阈值边界有多远: ```python # 以小碗为例: # d 越远离阈值(small_max),置信度越高 conf = min(1.0, 0.7 + (small_max - d) / small_max × 0.3) ``` 置信度范围在 0.7~1.0 之间: - `1.0`:直径远离边界,非常确定 - `0.7`:直径刚好落在边界附近,较模糊 --- ## 文件结构 ``` bowl_size/ ├── bowl_detector.py # 核心检测类 BowlDetector │ - detect(image) → BowlDetectionResult │ - 双路检测 + CLAHE + 自适应降级 │ ├── calibrate.py # 标定 GUI(CustomTkinter) │ - 加载大/中/小碗图片 │ - 自动检测直径并统计 │ - 计算阈值并写入 config.json │ ├── app.py # 主应用 GUI │ - 图片模式:拖拽/选择图片 │ - 摄像头模式:实时检测 │ - 重量计算:秤示数 - 碗重 = 食物重量 │ ├── config.json # 标定配置(标定后自动生成) │ - 检测参数 │ - 尺寸阈值 │ - 碗的实际重量 │ └── README.md # 本文件 ``` --- ## 使用流程 ### 第一次使用(标定) ```bash python bowl_size/calibrate.py ``` 1. 选择"小碗"→ 点击"加载该碗型图片"→ 导入 10~20 张小碗图片 2. 重复步骤 1,完成中碗、大碗的图片导入 3. 左侧"检测统计"栏会显示每种碗的平均检测直径 4. 填入各碗的实际重量(克) 5. 点击"✅ 生成标定配置" → 自动写入 `config.json` > 💡 建议每种碗的图片在实际使用场景中拍摄(有食物、实际光线),这样标定结果更准确。 ### 日常使用(识别) ```bash python bowl_size/app.py ``` - **图片模式**:拖拽图片到预览区,或点击"打开图片" - **摄像头模式**:点击"开启摄像头",实时检测并显示结果 - **重量计算**:识别成功后,在"秤示数"输入框填入重量,点击"计算食物重量" --- ## 调参指南 如果检测效果不理想,可修改 `config.json` 中的 `detection` 部分: | 参数 | 作用 | 调大效果 | 调小效果 | |------|------|---------|---------| | `hough_param2` | 霍夫圆灵敏度 | 只检测完美圆(漏检多)| 容易误检 | | `hough_param1` | 内部 Canny 高阈值 | 只检测强边缘 | 边缘更敏感 | | `canny_threshold2` | 轮廓法 Canny 高阈值 | 只检测强边缘 | 边缘更敏感 | | `min_circularity` | 轮廓圆度要求 | 只接受更圆的轮廓 | 接受更不规则的形状 | | `blur_kernel_size` | 高斯模糊强度 | 去除更多噪点(可能模糊边缘)| 保留更多细节 | **推荐调参顺序:** 1. 先降低 `hough_param2`(如从 25 → 15) 2. 再降低 `canny_threshold2`(如从 60 → 40) 3. 如果还不行,降低 `min_circularity`(如从 0.5 → 0.4) --- ## 已知局限性与改进方向 ### 当前局限 | 场景 | 问题 | 影响 | |------|------|------| | 碗没放正/严重倾斜 | 投影从圆形变为椭圆,直径偏小 | 可能误判为更小的碗 | | 多个碗同时在画面中 | 取最大圆,可能选到错误的碗 | 建议每次识别只有一个碗 | | 碗边被遮挡 > 30% | 轮廓不连续,圆度降低,可能检测失败 | 确保碗边清晰可见 | | 摄像头高度改变 | 原标定失效 | 需要重新运行 calibrate.py | ### 可能的改进方向 1. **加入 YOLOv8 目标检测**:先 YOLO 定位碗的 bounding box,再在 ROI 内做几何检测,解决多碗场景 2. **椭圆拟合**:将 `minEnclosingCircle` 改为 `fitEllipse`,应对轻微倾斜 3. **结合秤重量信号**:大碗比小碗重,可做双重校验,拒绝明显矛盾的结果 4. **在线自适应**:记录历史检测直径,发现漂移时自动提醒重新标定 --- *本模块是 FoodClassifier 项目的子模块,服务于智慧秤的食物净重计算场景。*