不锈钢筷子头挨着头、反光闪瞎眼,OpenCV 霍夫圆直接投降。这个小项目走了一遍从传统 CV 到深度学习的完整路径。
那些反光的筷子头,把 OpenCV 逼疯了
事情是这样的:要数一把不锈钢筷子有多少根。听起来很简单对吧?拿个相机拍一下,数圆圈就行了。
用 OpenCV 的霍夫圆变换(HoughCircles)试了一下——惨不忍睹。不锈钢顶部的金属反光和阴影被边缘检测器当成独立实体,满屏幕乱飞的假阳性圆圈。筷子头挨得又近,阴影轮廓互相粘连,算法根本分不清这是 16 根筷子还是 2 个异形物体。
更要命的是调参。换个光照条件,param1、param2 又得重来。换个桌面背景,再来一遍。这不是写代码,这是在做手工。
这就是传统 CV 的死穴:它只认几何形状,不认"东西"。霍夫圆变换的逻辑是"找到符合圆方程的像素点集合",它不理解"这是一根筷子的截面"这个概念。光照一变、角度一变、排列一变,像素梯度就变了,算法就废了。
为什么 YOLO 能搞定
YOLO 的思路完全不同。它不是在像素层面找几何形状,而是从数百万张图片里学会了"什么是筷子头"。它理解的是语义特征,不是像素梯度。
具体到这个场景:
| 问题 | OpenCV (霍夫圆) | YOLO |
|---|---|---|
| 不锈钢高光 | 把光斑误判为实体 | 能通过上下文推理出这是光斑 |
| 筷子头紧密排列 | 把多个物体融合成一个 | 擅长分辨紧密排列的多个实例 |
| 换光照/换背景 | 每次都要重调参 | 一次训练,到处通用 |
简单说:OpenCV 是"按规则找",YOLO 是"凭经验认"。规则怕变化,经验怕没见过——但筷子头这种常见物体,YOLO 的训练集里见得多了。
从标注到训练:三步走
第一步:安装标注工具
先检查 Python 版本:
python --version
💡 LabelMe 依赖的
pyqt5在 Python 3.8 以下版本经常装不上,折腾半小时不如直接换工具。
国内环境建议先配 pip 镜像:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# LabelMe(推荐,支持圆形标注)
pip install labelme
# 或者 LabelImg(Python ≤ 3.8 时用)
pip install labelImg
LabelMe vs LabelImg 怎么选?
| LabelMe | LabelImg | |
|---|---|---|
| 标注类型 | 矩形、圆形、多边形、线段 | 仅矩形框 |
| 输出格式 | JSON(含完整坐标信息) | XML(Pascal VOC)或 YOLO TXT |
| 圆形目标 | ✅ 原生支持,圆心+圆周点 | ❌ 只能画矩形近似 |
| Python 要求 | ≥ 3.9 | ≥ 3.6 |
| 安装难度 | 中(PyQt5 依赖偶尔翻车) | 低 |
| 适合场景 | 需要精确形状标注(圆、多边形) | 快速矩形框标注 |
本项目的筷子头是圆形的,用 LabelMe 的圆形标注比矩形框更贴合,训练时边界更精准。如果你只是标注方方正正的物体(盒子、瓶子),LabelImg 完全够用。
第二步:标注
labelme dataset_labelme/
打开 GUI,用圆形标注(不是矩形框)在每根筷子头上画圈,标签统一叫 circle1。圆形标注比矩形框更贴合筷子头的形状,训练时边界更精准。
LabelMe 保存的是 JSON 格式,里面记录了圆心坐标和圆周上的一个点。这一步是最枯燥的,但标注质量直接决定模型上限——垃圾标注出垃圾模型,没有例外。
第三步:JSON → YOLO 格式转换
LabelMe 的 JSON 不能直接给 YOLO 用,需要转换成 YOLO 的 TXT 格式:
class_id x_center y_center width height (归一化到 0~1)
核心难点在于 LabelMe 的圆形标注存的是"圆心 + 圆周点",而 YOLO 要的是矩形边界框。转换逻辑:
# 圆心就是边界框中心,半径决定宽高
cx, cy = points[0] # 圆心
px, py = points[1] # 圆周点
r = math.sqrt((px - cx)**2 + (py - cy)**2)
# YOLO 格式:中心坐标 + 宽高(归一化)
x = cx / img_w
y = cy / img_h
w = (2 * r) / img_w
h = (2 * r) / img_h
💡 踩坑提醒:坐标系别搞混。LabelMe 用的是左上角原点的像素坐标,YOLO 要的是归一化的中心坐标。漏了归一化这一步,模型直接训废。
第四步:训练
from ultralytics import YOLO
model = YOLO("yolov8n.pt") # 预训练的轻量级模型
model.train(
data="./dataset/dataset.yaml",
epochs=200,
imgsz=640,
device="cpu", # 有 GPU 用 "0",Mac 用 "mps"
)
YAML 配置文件很简单:
path: ./dataset
train: images/train
val: images/val
names:
0: circle1
用 CPU 跑 200 个 epoch,最终 mAP50 达到 0.81,recall 达到 1.0(所有筷子头都检出来了)。对于只有 3 张训练图的极端小数据集,这个结果相当不错——说明 YOLO 的预训练权重里已经包含了足够的"圆形物体"先验知识。
推理:从矩形框到圆形标注
YOLO 检测输出的是矩形边界框,但筷子头是圆的。为了让可视化更精准,做了一个后处理修正:
- 在 YOLO 检测到的矩形框内,截取灰度图的局部 ROI
- 用自适应阈值二值化,把筷子头截面的轮廓抠出来
- 计算这个局部区域的真实质心(用图像矩 moments)
- 以质心为圆心画圆,替代 YOLO 的矩形框
roi = gray[y1:y2, x1:x2]
roi_thresh = cv2.adaptiveThreshold(
roi, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 2
)
moments = cv2.moments(roi_thresh)
center_x = int(moments["m10"] / moments["m00"]) + x1
center_y = int(moments["m01"] / moments["m00"]) + y1
这个思路是YOLO 负责"找",OpenCV 负责"精修"。深度学习擅长语义定位(大概在哪),传统 CV 擅长亚像素级精度(精确到哪)。两者结合,比单独用任何一个都好。
踩坑与排查
1. 数据集只有 3 张图,能训吗?
能,但要过拟合。这个项目的目的不是做通用模型,而是验证 YOLO 的训练流程。把同一张图同时放进训练集和验证集,确保模型能"记住"这些图。实际项目中,至少需要 100+ 张标注图。
2. 椭圆标注(圆的边界框)转 YOLO 的坑
LabelMe 的圆形标注存的是 (圆心, 圆周点),不是 (左上角, 右下角)。如果你当成矩形框处理,生成的边界框会完全错误。转换脚本必须判断 shape_type == "circle" 并走专用逻辑。
3. 小目标检测不理想
如果筷子头在图中占比很小(比如远景拍整把筷子),YOLO 的 640×640 输入分辨率可能不够。可以试试 imgsz=1280,或者用 YOLOv8 的小目标检测头。
4. CPU 训练太慢
200 epochs 用 CPU 跑了约 2 分钟(3 张图的数据集)。如果数据量上千,CPU 可能要跑几小时。建议至少有一张入门级 GPU。
效果对比:一眼看出差距
OpenCV 霍夫圆

问题很明显:
- 绿圈大小不一,有些比筷子头大出一圈,把旁边的缝隙也圈进去了
- 中间密集区域严重重叠,根本分不清哪根是哪根
- 高光区域被误判为独立的圆圈,假阳性满屏飞
YOLO + OpenCV 精修

同样的图,同样的筷子:
- 16 根筷子全部识别,每根一个圈,没有遗漏
- 圆圈大小均匀,精准贴合筷子头截面
- 编号 1-16,计数一目了然
- 紧密排列的筷子也能准确分开
核心差异:OpenCV 只认“圆的形状”,YOLO 认的是“筷子头这个东西”。前者被光照和排列骗了,后者用语义理解扛住了。
核心收获
- 传统 CV 和深度学习不是替代关系,是互补关系。YOLO 负责语义定位,OpenCV 负责亚像素精修,组合拳比单打独斗强。
- 标注质量 > 模型选择。3 张精心标注的图,比 100 张随便框的图效果好。
- YOLO 的预训练权重是宝藏。即使数据集极小,预训练权重里的先验知识也能让模型快速收敛。
- 圆形标注比矩形框更适合圆形目标。虽然转换脚本复杂一点,但训练时边界更精准,mAP 更高。
项目代码和数据集已开源:yolo-detection