- 新增碗尺寸识别主应用界面,支持图片拖拽、选择和摄像头实时检测 - 界面显示碗的尺寸类别、直径、置信度及算法方法 - 支持计算食物重量,依据秤示数和碗重差值计算 - 实现核心检测模块,采用霍夫圆变换和轮廓法两种检测方法融合 - 检测结果包含尺寸分类和置信度估计,支持绘制调试信息 - 提供交互式标定工具,支持批量加载样本图片并自动检测直径 - 标定工具计算阈值和碗重量,生成并保存配置文件config.json - 界面友好,提供当前状态提示和结果预览功能
391 lines
12 KiB
Markdown
391 lines
12 KiB
Markdown
# 碗尺寸识别模块 (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 项目的子模块,服务于智慧秤的食物净重计算场景。*
|