- 新增碗尺寸识别主应用界面,支持图片拖拽、选择和摄像头实时检测 - 界面显示碗的尺寸类别、直径、置信度及算法方法 - 支持计算食物重量,依据秤示数和碗重差值计算 - 实现核心检测模块,采用霍夫圆变换和轮廓法两种检测方法融合 - 检测结果包含尺寸分类和置信度估计,支持绘制调试信息 - 提供交互式标定工具,支持批量加载样本图片并自动检测直径 - 标定工具计算阈值和碗重量,生成并保存配置文件config.json - 界面友好,提供当前状态提示和结果预览功能
碗尺寸识别模块 (bowl_size)
基于纯几何视觉方法的大/中/小碗自动识别方案,无需深度学习模型,轻量、快速、可解释。
目录
为什么不用深度学习?
直觉上,"识别大中小碗"应该用神经网络分类器。但本场景有一个极强的物理先验:
摄像头安装高度固定 → 碗的像素直径与真实直径成线性比例关系。
这意味着只需要量出碗在图像中的像素直径,就可以直接判断是大/中/小碗,而不需要学习任何"视觉特征"。
| 维度 | 深度学习方案 | 本方案(纯几何) |
|---|---|---|
| 数据需求 | 每类 100+ 张 | 一次标定即可 |
| 部署新秤 | 重新采集 + 训练 | 重跑标定脚本(5分钟) |
| 可解释性 | 黑盒 | 直接看像素直径 |
| 计算开销 | GPU / 较慢 | CPU 毫秒级 |
| 适用性 | 高通用性 | 依赖固定安装高度 |
核心思路:几何先验
摄像头(固定高度 H)
|
| H 固定
|
┌─────┴─────┐
│ 秤台 │
│ [ 碗 ] │
└───────────┘
真实直径 D(cm) ←→ 像素直径 d(px)
关系:d = k × D (k 是与安装高度相关的比例系数)
由于 k 对于固定安装的摄像头是常数,我们不需要求出 k 的具体值,只需要:
- 标定阶段:拍大/中/小碗图片,记录每种碗的
d(像素直径)均值 - 推理阶段:检测新图像中碗的
d,与标定阈值比较,直接输出大/中/小
检测算法详解
核心代码在 bowl_detector.py 的 detect() 方法中。整体流程:
原始图像 (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)
筛选条件:
- 面积过滤:轮廓面积必须在图像面积的 3%~95% 之间(太小是噪点,太大是背景)
- 圆度过滤:
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 # 标定 GUI(CustomTkinter)
│ - 加载大/中/小碗图片
│ - 自动检测直径并统计
│ - 计算阈值并写入 config.json
│
├── app.py # 主应用 GUI
│ - 图片模式:拖拽/选择图片
│ - 摄像头模式:实时检测
│ - 重量计算:秤示数 - 碗重 = 食物重量
│
├── config.json # 标定配置(标定后自动生成)
│ - 检测参数
│ - 尺寸阈值
│ - 碗的实际重量
│
└── README.md # 本文件
使用流程
第一次使用(标定)
python bowl_size/calibrate.py
- 选择"小碗"→ 点击"加载该碗型图片"→ 导入 10~20 张小碗图片
- 重复步骤 1,完成中碗、大碗的图片导入
- 左侧"检测统计"栏会显示每种碗的平均检测直径
- 填入各碗的实际重量(克)
- 点击"✅ 生成标定配置" → 自动写入
config.json
💡 建议每种碗的图片在实际使用场景中拍摄(有食物、实际光线),这样标定结果更准确。
日常使用(识别)
python bowl_size/app.py
- 图片模式:拖拽图片到预览区,或点击"打开图片"
- 摄像头模式:点击"开启摄像头",实时检测并显示结果
- 重量计算:识别成功后,在"秤示数"输入框填入重量,点击"计算食物重量"
调参指南
如果检测效果不理想,可修改 config.json 中的 detection 部分:
| 参数 | 作用 | 调大效果 | 调小效果 |
|---|---|---|---|
hough_param2 |
霍夫圆灵敏度 | 只检测完美圆(漏检多) | 容易误检 |
hough_param1 |
内部 Canny 高阈值 | 只检测强边缘 | 边缘更敏感 |
canny_threshold2 |
轮廓法 Canny 高阈值 | 只检测强边缘 | 边缘更敏感 |
min_circularity |
轮廓圆度要求 | 只接受更圆的轮廓 | 接受更不规则的形状 |
blur_kernel_size |
高斯模糊强度 | 去除更多噪点(可能模糊边缘) | 保留更多细节 |
推荐调参顺序:
- 先降低
hough_param2(如从 25 → 15) - 再降低
canny_threshold2(如从 60 → 40) - 如果还不行,降低
min_circularity(如从 0.5 → 0.4)
已知局限性与改进方向
当前局限
| 场景 | 问题 | 影响 |
|---|---|---|
| 碗没放正/严重倾斜 | 投影从圆形变为椭圆,直径偏小 | 可能误判为更小的碗 |
| 多个碗同时在画面中 | 取最大圆,可能选到错误的碗 | 建议每次识别只有一个碗 |
| 碗边被遮挡 > 30% | 轮廓不连续,圆度降低,可能检测失败 | 确保碗边清晰可见 |
| 摄像头高度改变 | 原标定失效 | 需要重新运行 calibrate.py |
可能的改进方向
- 加入 YOLOv8 目标检测:先 YOLO 定位碗的 bounding box,再在 ROI 内做几何检测,解决多碗场景
- 椭圆拟合:将
minEnclosingCircle改为fitEllipse,应对轻微倾斜 - 结合秤重量信号:大碗比小碗重,可做双重校验,拒绝明显矛盾的结果
- 在线自适应:记录历史检测直径,发现漂移时自动提醒重新标定
本模块是 FoodClassifier 项目的子模块,服务于智慧秤的食物净重计算场景。