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

391 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 碗尺寸识别模块 (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 # 标定 GUICustomTkinter
│ - 加载大/中/小碗图片
│ - 自动检测直径并统计
│ - 计算阈值并写入 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 项目的子模块,服务于智慧秤的食物净重计算场景。*