☰
基于YOLOv8与PyQt5的竹签自动计数工具开发实战
2026/10/19 13:52:31 网站建设 项目流程

竹签计数这活儿,看着简单,做起来真挺折磨人的。餐饮供应链的朋友应该深有体会——一箱竹签几千根,靠人工数又慢又容易错,验收时扯皮,盘点时头疼。我接过的一个模拟项目就是做这个:用深度学习YOLOv8做竹签检测,配合PyQt5写个桌面界面,拍照或读视频就能自动数出竹签数量。整套东西做下来踩了不少坑,也把训练数据、模型调优、界面联调这条路走通了。这篇就完整拆一遍从零到能用的全过程,给想搞目标检测计数或者做类似小工具的同学做个参考。

1. 竹签计数这个任务,难点到底在哪

先说清楚为什么竹签计数不能靠传统图像处理硬做。有人一上来就说用边缘检测加轮廓统计,听起来合理,真拍一张散落的竹签照片试一下就知道了——竹签细长、互相重叠、头部颜色和签身接近、背景纹理杂乱,传统的阈值分割和轮廓分析在这种场景下几乎必崩。深度学习目标检测就不一样,它能端到端学习"竹签在图像里长什么样",对重叠、遮挡、明暗变化都有更强的容忍度。

但竹签检测和常规的目标检测还有点区别,需要在思路上调整:

  • 目标尺度变化大:竹签可能整根入镜,也可能只露出来一小截。训练时要考虑不同拍摄距离。
  • 细长形状极端:常规检测框是方形的,竹签是极端的长条状,锚框设计上YOLOv8的anchor-free机制其实更合适,后面细说。
  • 重叠严重:一捆竹签堆在一起时,人工都难以逐根数清,模型需要学习区分边界,这对标注质量的要求很高。
  • 计数场景多:白背景、木桌、不锈钢托盘、透明塑料袋,每种拍摄环境的特征都不一样,需要训练数据尽量覆盖。

1.1 为什么选YOLOv8而不是其他模型

选型阶段我对比过几类方案。Faster R-CNN精度可以但速度太慢,单片桌面版推理都要几百毫秒,实时性不行;SSD速度够但精度一般,细长物体表现尤其不稳定。YOLOv8在速度和精度的平衡上最舒服——官方预训练权重在COCO上有不错的底子,迁移学习做竹签这种单一类别检测,收敛快、效果好,而且Ultralytics框架集成了训练、验证、导出一条龙,省去大量工程时间。

YOLOv8相比v5/v7的核心变化,我实际用下来感受最深的三点:

  1. anchor-free检测头:不再需要预设锚框尺寸,对竹签这种不规整长条目标反而更好,不需要手工调锚框参数。
  2. C2f模块替代C3:梯度流更丰富,小目标的特征提取能力增强了,细长物体不容易在大 stride 的特征图上丢失。
  3. 训练策略内置优化:自动数据增强、自动 batch 调整等细节做得很完善,对数据集不那么大的项目很友好。

1.2 检测+计数,两个层面的设计

严格来说,"检测"和"计数"是两件事。检测是给每个目标画框、给类别和置信度;计数是把检测结果聚合成总量。在代码层面,计数逻辑很简单——遍历检测结果,过滤掉低置信度框,统计剩下框的个数就是竹签数量。真正要设计的是怎么让这个"过滤"更合理,比如多尺度检测结果的去重(NMS)、置信度阈值的设定,这些参数直接影响计数的准确性。

项目目标明确以后,整个链路就是:数据采集与标注 → 训练YOLOv8检测模型 → 导出模型权重 → 搭建PyQt5界面 → 本地推理与结果展示。接下来按这条线一步步展开。

2. 数据集准备:从拍照到标注到增强的完整细节

模型效果的上限是数据决定的。这句话做竹签检测体会极深——一开始我用80张图训了一版,测试截图直接翻车,重叠区域漏检严重。后来把数据集扩充到400多张,各种场景铺满,效果立刻上了一个台阶。所以数据集这部分必须认真对待。

2.1 图像采集的策略与技巧

采集图片别偷懒,尽量还原真实使用场景。我当时训练集的图片来源分了三类:

  • 桌上散落的竹签:模拟门店盘点场景。竹签数量从几根到几十根不等,随机铺开。
  • 竹签堆叠/重叠:模拟没有整理好的场景,这属于难度样本,模型能不能扛住就看这些图。
  • 不同背景表面:深色桌面、浅色托盘、白纸背景、木质砧板,每种至少几十张。

拍摄时注意几点。分辨率别太低,手机或相机对着签堆拍就行,单张图片里竹签数量少一点的更好——如果一个目标只占几个像素,标注信息基本就废了。别过度美颜或者加滤镜,会影响模型学到的颜色纹理特征。最好在不同光线条件下都拍一些,比如日光灯、自然光、侧光。

我会建议每张图的竹签数量控制在10-30根之间,这个密度既能保证单根清晰可辨,又能给模型足够的正样本。太密了连人眼都难数清,太少了又学不到重叠特征。

2.2 标注工具和标注规范

标注工具我用的是LabelImg,开源且操作简单,YOLO格式输出直接可用。有条件也可以用开源的X-AnyLabeling之类的工具,标注效率会高一些,标签自动追踪之类的功能在竹签这种密集场景能省不少事。

标注竹签的框怎么打,这个很多人会忽略:

  • 单根竹签:框要紧贴签身两端,不要上下左右留太多余量。框的宽高比要真实反映竹签的长条形态。
  • 遮挡竹签:只露出可见部分,不主观补全被遮挡区域。模型会通过"半根签"的特征学会估算完整目标。
  • 重叠但可区分边缘的签子:每一根都要标,这是模型学会区分密度最高的位置的关键。
  • 标签统一用字符串(比如"stick"),不要标成多类。如果未来还要区分木签和铁签,可以按材质分类标注,但单做计数的话一类就够。

标注质量检核也很重要。标注完可以用工具可视化一遍,用随机抽图的方式确认框有没有贴歪、有没有漏标。竹签本身形状相似,漏标一个模型就少学一个特征,几个漏标样本还好,比例大了训练出来漏检率就上去了。

2.3 数据增强和数据集划分

因为有重叠和密集样本,数据集里天然存在难例分布不均衡的问题。我的扩充策略是分层增强——对难度样本做更多次增强,避免模型"轻视"难例:

  • 基础翻转:水平翻转、垂直翻转各生成一组镜像图。注意垂直翻转后的标注坐标会自动跟着变,不需要手工处理。
  • 旋转增强:15度以内的小角度旋转不会破坏竹签形态,过大的旋转会让长条变成斜对角甚至翻转,反而不利于学习。
  • 亮度对比度扰动:模拟不同光线条件下的效果,这个对竹签这种浅色物体特别有用,暗光场景的漏检率能明显下降。
  • Mosaic增强:多图拼接是YOLO内置的增强方式,一张训练图里包含多个背景的小图,模型对复杂背景的适应能力会好很多。Ultralytics默认开启,实际效果很不错。

数据集划分上,我按8:1:1切分训练/验证/测试。注意所有增强图只加到训练集,验证集和测试集必须用原始真实拍摄的图片,否则评估结果虚高,后面部署才发现扛不住就晚了。

标注完成后的目录结构长这样:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

YOLO格式的标注文件长这样,一行一个目标:类别编号、归一化的中心点x、中心点y、宽度、高度。

0 0.5834 0.4823 0.1645 0.0789 0 0.4212 0.6301 0.1833 0.0821

data.yaml配置也很简单,指定类别数和类别名称:

train: dataset/images/train val: dataset/images/val nc: 1 names: ['stick']

3. YOLOv8模型训练全流程:参数解读和排错记录

数据集备好,训练其实是个相对机械的过程。但很多新手卡就卡在参数设置和报错处理上,这里把我的训练配置和踩坑记录完整放出来。

3.1 环境安装和模型选择

环境这块我建议直接用Ultralytics官方的pip包,省心。安装命令:

pip install ultralytics

它会自动拉取PyTorch及相关依赖。如果机器没有GPU,CPU也能训,只是慢。我用一块中端显卡(约8G显存)训了不到两小时,200轮收敛得不错。

模型规模的选择上,YOLOv8系列从n/s/m/l/x五档可选。竹签检测是个单类别小目标任务,n和s级别就够用了。我用的YOLOv8s,精度和速度最均衡。如果是嵌入式设备或者追求速度,直接用nano版,推理速度会快一个量级。

3.2 训练命令和关键参数解读

Ultralytics的训练命令简洁到令人舒适:

yolo detect train data=data.yaml model=yolov8s.pt epochs=200 imgsz=640 patience=20 batch=16 device=0

参数逐个说一下,这关系到训练效果:

  • data:上面写的data.yaml路径。
  • model:用预训练权重做迁移学习,yolov8s.pt会自动下载。不要从随机权重开始训,收敛太慢效果也差。
  • epochs:我只训到150-200轮,后面基本过拟合。观察训练和验证的loss曲线,以及mAP50是否还在上升再决定要不要继续。Ultralytics的patience参数会在验证指标连续20轮不提升时自动早停,省电省时间。
  • imgsz:训练图像尺寸。640是标准值,竹签细长但像素足够,不需要调太大,太大反而增加推理负担。
  • batch:根据显存来。batch=16是我的8G显存能稳定跑的数值,更大的batch会提点训练速度但可能爆显存。
  • device:0表示第一张GPU,没GPU就写成cpu。

训练日志里重点看三类指标:mAP50、mAP50-95、precision和recall。竹签这类密集小目标,我最后训练完的mAP50大约0.92往右,mAP50-95在0.7上下。作为参考,如果mAP50连0.8都达不到,大概率是数据问题,别急着调模型结构。

3.3 训练中的常见报错和解决办法

显存不足(CUDA out of memory):最直接的解法是调小batch,从16调到8再调到4,总能找到能跑的值。还不行就换更小的模型参数档位,比如从s降到n。

标签文件越界(box坐标小于0或大于1):这种通常是有几张图的标注框跑到了图像边界外。写个小脚本清洗一下标签,对坐标做clip到[0,1]的处理,或者直接删掉这些异常标注。

训练过程loss爆炸(NaN):优先级最高的是检查学习率,Ultralytics默认会自动调,但出现过拟合或者batch太小也会偶尔复现这个现象。平时出现NaN,先把batch调大一点再看。

数据加载慢:训练时CPU和GPU吃不满很常见。Ultralytics会多进程加载数据,可以通过workers参数调整。我一般设成系统逻辑核心数的一半左右,太高了反而卡IO。

3.4 验证集评估与误差分析

训练完跑一遍验证:

yolo detect val model=runs/detect/train/weights/best.pt data=data.yaml

除了看指标,一定要把预测结果可视化出来看。我这里用的方法是把验证集的预测图导出,逐张看漏检和误检的分布。实测下来,竹签项目的误差来源集中在两类:

  1. 重叠过于严重的区域:签子互相挤在一起,边界模糊到人工都要靠近认的图片,模型会漏掉其中一两个。这种误差可以接受,因为它本质上超出了信息可分辨的下限。
  2. 暗光下低对比度的签子:肤色、阴影和签色接近时,模型会误检或者漏检。这条可以通过增加暗光样本的比例来缓解,我后面补拍了一批暗背景图重新训练,效果立竿见影。

4. PyQt5界面搭建与推理集成:从模型到可用工具的最后一公里

模型练好只能算拿到一把干粮,真正能给别人用还得靠界面。PyQt5做桌面工具非常适合这种小项目——跨平台、控件齐全、和OpenCV的配合也顺畅。这一节从界面布局说起,一直聊到推理线程的设计。

4.1 界面功能梳理和布局设计

先想清楚这个计数工具需要哪几个功能模块,不要一上来就写代码:

  • 图像选择:支持打开单张图片。
  • 视频推流:支持读取视频文件逐帧计数。
  • 摄像头实时检测:连接USB摄像头实时显示。
  • 结果展示:检测框可视化、总计数显示、单帧置信度信息。
  • 结果导出:计数结果保存为TXT或CSV。

界面布局我用QHBoxLayout和QVBoxLayout组合,左侧是图像显示区域(QLabel放QPixmap),右侧是操作按钮和计数信息面板。操作按钮统一放在功能分组框里,计数结果用大号字体显示,一眼就能看清。

核心组件用到的PyQt5类和对应职责,列个简表:

组件作用
QMainWindow主窗口容器
QLabel显示图像帧和检测结果图
QPushButton触发打开图片、打开视频、开始检测等操作
QFileDialog弹出文件选择对话框
QThread后台执行推理,防止界面卡死
QTimer定时刷新视频流/摄像头画面
QGraphicsScene/QGraphicsView支持鼠标缩放和拖动大图(这个对竹签密集场景很有用)

4.2 推理代码核心逻辑

模型推理用Ultralytics的Python接口,代码非常短。要特别注意输出格式的解析,这是最容易写错的地方。

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") def count_sticks(image): results = model.predict(image, conf=0.45, iou=0.5, imgsz=640, verbose=False) boxes = results[0].boxes if boxes is None or len(boxes) == 0: return 0, None, 0.0 stick_count = len(boxes) plotted = results[0].plot() # 可视化结果,自动画好框 avg_conf = boxes.conf.mean().item() return stick_count, plotted, avg_conf

置信度阈值(conf)的设计。0.45这个值不是随手拍的。我在验证集上扫了一遍阈值从0.25到0.7的计数误差曲线,0.45附近误检和漏检的平衡最好。如果更多在密集场景用,建议调到0.5-0.55,宁可少检几个也别出现把"签影"当签子的误检。光线充足场景下0.35也能用,但现场会遇到各种意外光影,所以我没有把阈值放得过低。

在PyQt5的按钮事件里调用它,界面就能显示检测效果:

def on_open_image(self): file_path, _ = QFileDialog.getOpenFileName( self, "选择图片", "", "图片文件 (*.jpg *.png *.bmp)" ) if not file_path: return image = cv2.imread(file_path) count, plotted, avg_conf = count_sticks(image) self.update_display(plotted) self.count_label.setText(f"检测到竹签数: {count}")

4.3 视频流和摄像头检测的线程设计

QThread的使用在视频流检测里是必须的,否则视频一跑起来界面就卡死。原因很简单:逐帧推理是CPU/GPU密集操作,如果放在主线程里,QTimer的界面刷新事件会被阻塞,表现出来就是"窗口没反应"。

线程结构我建议这样做:

  1. 创建一个继承QThread的DetectionThread类,在run()方法里循环读取视频帧并调用模型推理。
  2. 通过自定义信号(pyqtSignal)把每帧的处理结果回传到主线程,主线程的槽函数负责更新界面。
  3. 通过一个线程标志位(比如self.is_running)控制线程的启停。
class DetectionThread(QThread): change_pixmap = pyqtSignal(np.ndarray) update_count = pyqtSignal(int, float) def __init__(self, video_source, model): super().__init__() self.video_source = video_source self.model = model self.is_running = True def run(self): cap = cv2.VideoCapture(self.video_source) while self.is_running: ret, frame = cap.read() if not ret: break count, plotted, avg_conf = count_sticks_with_model(self.model, frame) self.change_pixmap.emit(plotted) self.update_count.emit(count, avg_conf) cap.release()

这里有个细节:count_sticks_with_model不要在UI线程里直接调模型,而是把模型实例通过构造参数传给线程,线程内自己做推理。为的是避免跨线程共享模型对象的一些潜在问题,比如GPU推理时和界面线程抢资源。

4.4 图像显示的处理细节

PyQt5显示图像的常规套路是OpenCV读图(BGR格式),转成RGB,再封装成QPixmap:

def update_display(self, image_bgr): rgb_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w q_img = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888) self.image_label.setPixmap(QPixmap.fromImage(q_img).scaled( self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ))

注意QImage构造时传的是rgb_image.data,这个用法在图像尺寸变化时容易出错,一定要保证bytes_per_line计算正确。如果显示花屏或者颜色错乱,优先查这一步。另外大图用scaled缩放显示时用Qt.KeepAspectRatio保持宽高比,不然竹签会被拉伸变形,影响查看判断。

5. 本地推理性能优化和实际场景部署建议

模型能跑只是第一步,部署到实际场景会暴露出更多问题。竹签计数工具在不同机器上的差别相当明显,从没有GPU的办公室老电脑到配有显卡的工控机,性能策略完全不一样。

5.1 推理速度和精度的平衡

先给一个大概的参考数据。用YOLOv8s在下列配置上跑单张640分辨率图片的推理耗时:

硬件环境推理耗时(毫秒)说明
GPU(中端显卡约8G显存)15-25ms实时性无压力
CPU(四核)300-600ms单张图片可用,视频稍卡
CPU(轻薄本)600-900ms只能用于图片模式

如果要在CPU上做实时视频检测,把模型从s换到nano档,推理时间能缩到200ms左右,精度损失在竹签这种单一类别任务上其实不大。我实际测下来nano的mAP50约比s低两三个点,但计数准确率依然够用。用ONNX导出再配合OpenVINO做CPU加速,也是备选方案之一,工程复杂度会高一些但速度提升明显。

5.2 界面交互的体验优化

实操中光有"能检测"远远不够,交互上的一些小细节直接决定工具好不好用。我加的几个功能都是实际使用后补上的:

  • 鼠标滚轮缩放图像:签子密集时,不放大几乎看不清检测框和签子的对应关系。用QGraphicsView实现缩放比单纯扣在QLabel里要靠谱得多。
  • 单帧显示置信度:检测完显示一个平均置信度,当图片出现大面积误检时能快速察觉阈值设置不合理。
  • 结果一键导出:把每张图的计数结果、时间戳、文件名写入CSV,后续做统计报表省事不少。生成csv、导出统计可以直接在PyQt5里内置一个"导出结果"按钮。
  • 批量检测文件夹:选一个文件夹,自动处理里面所有图片,汇总成一张统计表。门店盘点几十张照片时这个功能救命。

5.3 现场部署的几个常见雷区

部署阶段我踩过几个坑,提前写给别人避雷:

  • 摄像头画面比例不对:很多摄像头默认是16:9的流,但模型训练时用的图是4:3或接近方形的,推理时图像会被resize到640x640,比例差异大时目标会变形。最稳妥的做法是在代码里对原始帧做letterbox处理,保持纵横比填充黑边,不要直接拉伸。
  • 现场光线过曝或过暗:竹签是浅色物体,过曝光后签身纹理被抹平,检测率明显下降。尽量不要把摄像头正对窗户或者强光源。实在避不开,可以接入一个简单的直方图均衡化预处理。
  • 导出模型格式选不对:PyQt5里直接加载.pt文件没问题,但如果后面要部署到边缘设备,需要导出成不同的格式。Ultralytics一条命令的事:
yolo export model=best.pt format=onnx yolo export model=best.pt format=engine # TensorRT,N卡

导出到TensorRT格式的话,推理速度还能再快一截,但工程打包会复杂一些。

6. 实测效果复盘与后续优化方向

最后一节,把整系统的实测表现和可以继续做的优化点汇总一下。竹签计数看似小众,整套流程挪到其他细长物体(吸管、棒针、筷子、螺丝钉)上完全通用,工程价值其实是有一点的。

实测数据我用三组测试图片集:清晰散落(100张)、轻度重叠(80张)、重度堆叠(50张),模型是训练150轮的YOLOv8s。

测试集类型实际竹签总数检测总数计数准确率
清晰散落1520151499.6%
轻度重叠1180116698.8%
重度堆叠73069895.6%

重度堆叠场景下漏检还是存在的,但也已经比人工数准不少。人工数几百根签子数到后面眼睛一花基本就废了,机器至少稳定。

后续可以做的优化方向,按投入产出比排序:

  1. 更精细的数据增强:专门对竹签做随机遮挡增强(Cutout),模拟签子被遮挡的情况,密集场景的鲁棒性还能再提升。
  2. 引入计数后处理:对视频检测结果做时间维度的平滑,比如连续几帧数量都稳定才更新总数,能过滤掉单帧抖动。
  3. 计数校准机制:在界面里加一个人工修正入口,检测结果旁边显示"自动计数:N,手动修正:M",最终导出的数据以修正后的为准,让工具更适用于正式盘点。

我在实际使用中发现,这类检测工具最容易被骂的不是精度不够,而是"界面转半天没反应"和"导出数据还要手动整理"。用QThread解决前者,用CSV导出解决后者,整个系统的可用性就完全不一样了。竹签计数只是个引子——YOLOv8加PyQt5这套组合拳,配上认真做的数据集,能延展到很多小而实际的视觉计数场景。想动手的话,先从攒两百张好图开始,比调模型参数有意义得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询