Files
FoodClassifier/bowl_size/README.md
T
zhangpu da90a803e4 feat(core): 实现碗尺寸识别与标定系统
- 新增碗尺寸识别主应用界面,支持图片拖拽、选择和摄像头实时检测
- 界面显示碗的尺寸类别、直径、置信度及算法方法
- 支持计算食物重量,依据秤示数和碗重差值计算
- 实现核心检测模块,采用霍夫圆变换和轮廓法两种检测方法融合
- 检测结果包含尺寸分类和置信度估计,支持绘制调试信息
- 提供交互式标定工具,支持批量加载样本图片并自动检测直径
- 标定工具计算阈值和碗重量,生成并保存配置文件config.json
- 界面友好,提供当前状态提示和结果预览功能
2026-08-26 18:19:45 +08:00

12 KiB
Raw Blame History

碗尺寸识别模块 (bowl_size)

基于纯几何视觉方法的大/中/小碗自动识别方案,无需深度学习模型,轻量、快速、可解释。


目录

  1. 为什么不用深度学习?
  2. 核心思路:几何先验
  3. 检测算法详解
  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.pydetect() 方法中。整体流程:

原始图像 (BGR)
    │
    ▼
1. CLAHE 对比度增强(处理浅色碗+浅色背景)
    │
    ▼
2. 高斯模糊(去除噪点)
    │
    ├──► 霍夫圆变换(HoughCircles)────┐
    │                                  │
    └──► 轮廓法(Canny + findContours)─┤
                                       │
                              ▼ 双路结果融合
                                       │
                    (两路都失败?→ 宽松参数重试)
                                       │
                              ▼ 尺寸分类
                                       │
                           大 / 中 / 小碗

1. 预处理:CLAHE 对比度增强

问题:白色/米色碗放在浅灰色背景上,边缘对比度极低,Canny 检测不到边。

CLAHE(限制对比度自适应直方图均衡化) 是一种局部对比度增强算法:

clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)

原理:

  • 将图像切成 8×8 的小块(tile
  • 对每个小块独立做直方图均衡化
  • clipLimit=3.0 限制放大倍数,防止噪点被过度放大
  • 相邻块之间做双线性插值,避免块状伪影

效果:即使背景和碗颜色接近,碗边缘的微弱灰度差也会被局部放大,让后续边缘检测能找到碗边。


2. 方法一:霍夫圆变换

霍夫圆变换(Hough Circle Transform)是 OpenCV 专门用于检测圆形的经典算法。

参数说明(重要!):

cv2.HoughCircles(
    image,
    cv2.HOUGH_GRADIENT,
    dp=1.2,         # 分辨率倒数比(1=与原图同分辨率,>1降采样加速)
    minDist=xxx,    # 两个圆心之间的最小距离(防止重复检测同一个碗)
    param1=60,      # Canny边缘检测的高阈值(内部调用)
    param2=25,      # 圆心累加器阈值,越小越容易检测到(也越容易误检)
    minRadius=xxx,  # 最小圆半径(像素)
    maxRadius=xxx,  # 最大圆半径(像素)
)

param2 是最关键的参数

  • 值越大 → 只检测"完美圆",漏检多
  • 值越小 → 容易检测到"不完美圆",误检多
  • 本方案默认 25,对于碗这种规则形状已经足够

为什么选最大半径的圆?

best = max(circles, key=lambda c: c[2])  # c[2] 是半径

碗边是最大的圆形轮廓,碗底花纹、碗内装饰线都是更小的圆,取最大的才是碗边。


3. 方法二:轮廓法

作为霍夫圆的备用方案,用 Canny 边缘检测 + 轮廓分析来找圆。

步骤:

# 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

评分机制:

score = circularity × area

越圆、越大的轮廓得分越高 → 最终选出最可能是碗边缘的轮廓。


4. 双路融合策略

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. 自适应降级重试

如果两路都失败(比如图像质量极差),自动用更宽松的参数再试一次:

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

{
  "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
  }
}

尺寸分类逻辑

分类后还会给出一个置信度,反映检测到的直径距离阈值边界有多远:

# 以小碗为例:
# 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       # 标定 GUICustomTkinter
│                        - 加载大/中/小碗图片
│                        - 自动检测直径并统计
│                        - 计算阈值并写入 config.json
│
├── app.py             # 主应用 GUI
│                        - 图片模式:拖拽/选择图片
│                        - 摄像头模式:实时检测
│                        - 重量计算:秤示数 - 碗重 = 食物重量
│
├── config.json        # 标定配置(标定后自动生成)
│                        - 检测参数
│                        - 尺寸阈值
│                        - 碗的实际重量
│
└── README.md          # 本文件

使用流程

第一次使用(标定)

python bowl_size/calibrate.py
  1. 选择"小碗"→ 点击"加载该碗型图片"→ 导入 10~20 张小碗图片
  2. 重复步骤 1,完成中碗、大碗的图片导入
  3. 左侧"检测统计"栏会显示每种碗的平均检测直径
  4. 填入各碗的实际重量(克)
  5. 点击" 生成标定配置" → 自动写入 config.json

💡 建议每种碗的图片在实际使用场景中拍摄(有食物、实际光线),这样标定结果更准确。

日常使用(识别)

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 项目的子模块,服务于智慧秤的食物净重计算场景。