竹签计数这活儿看着简单做起来真挺折磨人的。餐饮供应链的朋友应该深有体会——一箱竹签几千根靠人工数又慢又容易错验收时扯皮盘点时头疼。我接过的一个模拟项目就是做这个用深度学习YOLOv8做竹签检测配合PyQt5写个桌面界面拍照或读视频就能自动数出竹签数量。整套东西做下来踩了不少坑也把训练数据、模型调优、界面联调这条路走通了。这篇就完整拆一遍从零到能用的全过程给想搞目标检测计数或者做类似小工具的同学做个参考。1. 竹签计数这个任务难点到底在哪先说清楚为什么竹签计数不能靠传统图像处理硬做。有人一上来就说用边缘检测加轮廓统计听起来合理真拍一张散落的竹签照片试一下就知道了——竹签细长、互相重叠、头部颜色和签身接近、背景纹理杂乱传统的阈值分割和轮廓分析在这种场景下几乎必崩。深度学习目标检测就不一样它能端到端学习竹签在图像里长什么样对重叠、遮挡、明暗变化都有更强的容忍度。但竹签检测和常规的目标检测还有点区别需要在思路上调整目标尺度变化大竹签可能整根入镜也可能只露出来一小截。训练时要考虑不同拍摄距离。细长形状极端常规检测框是方形的竹签是极端的长条状锚框设计上YOLOv8的anchor-free机制其实更合适后面细说。重叠严重一捆竹签堆在一起时人工都难以逐根数清模型需要学习区分边界这对标注质量的要求很高。计数场景多白背景、木桌、不锈钢托盘、透明塑料袋每种拍摄环境的特征都不一样需要训练数据尽量覆盖。1.1 为什么选YOLOv8而不是其他模型选型阶段我对比过几类方案。Faster R-CNN精度可以但速度太慢单片桌面版推理都要几百毫秒实时性不行SSD速度够但精度一般细长物体表现尤其不稳定。YOLOv8在速度和精度的平衡上最舒服——官方预训练权重在COCO上有不错的底子迁移学习做竹签这种单一类别检测收敛快、效果好而且Ultralytics框架集成了训练、验证、导出一条龙省去大量工程时间。YOLOv8相比v5/v7的核心变化我实际用下来感受最深的三点anchor-free检测头不再需要预设锚框尺寸对竹签这种不规整长条目标反而更好不需要手工调锚框参数。C2f模块替代C3梯度流更丰富小目标的特征提取能力增强了细长物体不容易在大 stride 的特征图上丢失。训练策略内置优化自动数据增强、自动 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.yamlYOLO格式的标注文件长这样一行一个目标类别编号、归一化的中心点x、中心点y、宽度、高度。0 0.5834 0.4823 0.1645 0.0789 0 0.4212 0.6301 0.1833 0.0821data.yaml配置也很简单指定类别数和类别名称train: dataset/images/train val: dataset/images/val nc: 1 names: [stick]3. YOLOv8模型训练全流程参数解读和排错记录数据集备好训练其实是个相对机械的过程。但很多新手卡就卡在参数设置和报错处理上这里把我的训练配置和踩坑记录完整放出来。3.1 环境安装和模型选择环境这块我建议直接用Ultralytics官方的pip包省心。安装命令pip install ultralytics它会自动拉取PyTorch及相关依赖。如果机器没有GPUCPU也能训只是慢。我用一块中端显卡约8G显存训了不到两小时200轮收敛得不错。模型规模的选择上YOLOv8系列从n/s/m/l/x五档可选。竹签检测是个单类别小目标任务n和s级别就够用了。我用的YOLOv8s精度和速度最均衡。如果是嵌入式设备或者追求速度直接用nano版推理速度会快一个量级。3.2 训练命令和关键参数解读Ultralytics的训练命令简洁到令人舒适yolo detect train datadata.yaml modelyolov8s.pt epochs200 imgsz640 patience20 batch16 device0参数逐个说一下这关系到训练效果data上面写的data.yaml路径。model用预训练权重做迁移学习yolov8s.pt会自动下载。不要从随机权重开始训收敛太慢效果也差。epochs我只训到150-200轮后面基本过拟合。观察训练和验证的loss曲线以及mAP50是否还在上升再决定要不要继续。Ultralytics的patience参数会在验证指标连续20轮不提升时自动早停省电省时间。imgsz训练图像尺寸。640是标准值竹签细长但像素足够不需要调太大太大反而增加推理负担。batch根据显存来。batch16是我的8G显存能稳定跑的数值更大的batch会提点训练速度但可能爆显存。device0表示第一张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 modelruns/detect/train/weights/best.pt datadata.yaml除了看指标一定要把预测结果可视化出来看。我这里用的方法是把验证集的预测图导出逐张看漏检和误检的分布。实测下来竹签项目的误差来源集中在两类重叠过于严重的区域签子互相挤在一起边界模糊到人工都要靠近认的图片模型会漏掉其中一两个。这种误差可以接受因为它本质上超出了信息可分辨的下限。暗光下低对比度的签子肤色、阴影和签色接近时模型会误检或者漏检。这条可以通过增加暗光样本的比例来缓解我后面补拍了一批暗背景图重新训练效果立竿见影。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, conf0.45, iou0.5, imgsz640, verboseFalse) 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的界面刷新事件会被阻塞表现出来就是窗口没反应。线程结构我建议这样做创建一个继承QThread的DetectionThread类在run()方法里循环读取视频帧并调用模型推理。通过自定义信号pyqtSignal把每帧的处理结果回传到主线程主线程的槽函数负责更新界面。通过一个线程标志位比如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再封装成QPixmapdef 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 modelbest.pt formatonnx yolo export modelbest.pt formatengine # TensorRTN卡导出到TensorRT格式的话推理速度还能再快一截但工程打包会复杂一些。6. 实测效果复盘与后续优化方向最后一节把整系统的实测表现和可以继续做的优化点汇总一下。竹签计数看似小众整套流程挪到其他细长物体吸管、棒针、筷子、螺丝钉上完全通用工程价值其实是有一点的。实测数据我用三组测试图片集清晰散落100张、轻度重叠80张、重度堆叠50张模型是训练150轮的YOLOv8s。测试集类型实际竹签总数检测总数计数准确率清晰散落1520151499.6%轻度重叠1180116698.8%重度堆叠73069895.6%重度堆叠场景下漏检还是存在的但也已经比人工数准不少。人工数几百根签子数到后面眼睛一花基本就废了机器至少稳定。后续可以做的优化方向按投入产出比排序更精细的数据增强专门对竹签做随机遮挡增强Cutout模拟签子被遮挡的情况密集场景的鲁棒性还能再提升。引入计数后处理对视频检测结果做时间维度的平滑比如连续几帧数量都稳定才更新总数能过滤掉单帧抖动。计数校准机制在界面里加一个人工修正入口检测结果旁边显示自动计数N手动修正M最终导出的数据以修正后的为准让工具更适用于正式盘点。我在实际使用中发现这类检测工具最容易被骂的不是精度不够而是界面转半天没反应和导出数据还要手动整理。用QThread解决前者用CSV导出解决后者整个系统的可用性就完全不一样了。竹签计数只是个引子——YOLOv8加PyQt5这套组合拳配上认真做的数据集能延展到很多小而实际的视觉计数场景。想动手的话先从攒两百张好图开始比调模型参数有意义得多。