
西红柿分拣这件事凡是去产地待过几天的人都知道它是个纯靠眼睛和经验的体力活。一条分拣线上七八个工人盯着传送带把青果、转色果、红果、过熟果分开一天下来眼睛发花情绪一波动标准就飘。我前后在几个果蔬合作社做过小规模的自动化改造核心诉求其实很朴素把这颗西红柿到底熟了没这个判断从人的主观经验里搬到机器上让判定标准统一、可复现。这套基于YOLOv8深度学习的西红柿成熟度检测系统干的就是这件事——它用目标检测的思路一次性把画面里所有西红柿的位置圈出来同时给出每一颗的成熟度类别再套一个PyQt5做的桌面界面让不写代码的人也能点按钮跑起来。Python源码、数据集、训练代码、GUI全都在一个工程里适合想入门目标检测落地的新手也适合手里有类似农产品分级需求、想改改就能用的朋友。下面我把这套东西从需求拆解到训练到界面到踩坑一层层摊开讲。1. 需求拆解与技术选型为什么这么设计1.1 从人工分拣的三个死穴说起先别急着上模型得搞清楚这套系统要替代的到底是什么。人工分拣西红柿有三个绕不开的问题标准漂移、速度瓶颈、数据不可追溯。标准漂移是指同一个工人在上午和下午对半熟的判断可能就不一样疲劳之后宽容度会变化速度瓶颈在于一条线的人手产出有物理上限旺季的时候堆果严重数据不可追溯则是说今天分出来这批果的成熟度分布到底如何、有没有异常批次全靠回忆没法沉淀成数据。这三点决定了系统的功能边界。它不能只是检测到西红柿就完事必须能稳定输出成熟度类别而且这个类别定义要足够清晰让标注员、训练、现场使用三方对同一个词的理解完全一致。另外系统最好能顺手把每颗果的类别数量统计出来这样合作社能直接拿去做批次记录。我在设计阶段就把可统计当成一个硬指标后面界面里的计数面板就是为它服务的。1.2 为什么是成熟度检测而不是简单的目标检测很多人第一反应是目标检测就是框出物体那西红柿检测不也是框出来吗这里有个容易踩的坑。纯目标检测只回答有没有、在哪而成熟度检测要求同一个类别的物体按状态再细分。这意味着类别数不是1而是按成熟度分成若干档。类别一多模型要学的不只是这是西红柿的纹理和形状还要学这颗的色相、明度、饱和度处在哪个区间难度是叠加的。更麻烦的是同色系之间的边界特别模糊。比如转色期和半熟期肉眼都要盯着看好几秒模型如果没有足够多这种中间态样本训练出来的分类边界会非常抖同一个果在相邻两帧里一会儿判成转色一会儿判成成熟。我后面在数据增强那一步花了很大力气去补这个中间态原因就在这。1.3 技术选型的取舍逻辑选YOLOv8不是因为它最新而是因为在单阶段检测这个赛道里它的工程化程度最高。Ultralytics把训练、验证、导出、推理做成了一套统一的命令行和Python API你换个数据集只要改一个yaml不用重写训练脚本这对做落地的人太重要了。相比之下两阶段的方法精度在某些场景确实好一点但推理速度上不去桌面端做实时视频检测会卡。界面选PyQt5也是同样的逻辑。OpenCV自带的窗口没法做复杂交互你想要的按钮、下拉框、表格、进度条它都做不了而PyQt5生态成熟控件齐全和Python的集成成本低配上QThread做多线程界面不会因为推理阻塞而假死。硬件方面我用过GTX 1660 Ti这种6GB显存的卡跑YOLOv8batch size设在16、图像尺寸640时显存占用大概2到3GB完全够用没有独显的话用CPU推理也能跑只是帧率会掉到个位数适合离线批量处理不适合实时。提示不要一上来就追求大模型。yolov8n和yolov8s在成熟度这种类内差异不大的任务上配合好数据集效果往往比盲目上yolov8x更稳因为大模型更容易在小数据集上过拟合。2. 数据集决定系统上限的关键环节2.1 成熟度分级标准怎么定这是整个项目里最需要和业务方对齐的一步。我一般把西红柿成熟度分成四档但这个分法必须写下来配上标准色卡和实物照片贴在标注员电脑旁边。类别编号类别名外观特征描述现场判定依据0unripe未熟整体青绿无红色透出果面均匀绿硬度高1half_ripe半熟/转色果肩出现橙红或淡红晕红绿混杂面积小于一半2ripe成熟整体红润色泽饱满红色占主体果肩果顶均匀3overripe过熟深红发暗果面起皱或软塌颜色暗沉有失水或破损迹象四档的好处是粒度适中标注不容易错而且和实际销售分级基本能对上。有的项目我见过分六档甚至七档标注一致性直接崩掉最后mAP上不去不是因为模型不行是因为标注本身矛盾。2.2 采集与标注的实操要点采集阶段建议覆盖三类变量光照、遮挡、成熟度分布。光照上大棚的散射光、正午的强光、傍晚的暖光都拍一些不然模型会把偏黄的色温错判成成熟遮挡上要有果挨果、叶片遮挡、框边截断的样本成熟度分布上别全拍红果那模型没见过青果上线必翻车。标注工具用LabelImg或者Ultralytics自带的标注流程都行输出YOLO格式的txt。每个txt一行代表一个框格式是类别编号 中心x 中心y 宽 高其中后四个值都是相对于图片宽高的归一化值范围0到1。举个例子一张1280x720的图里有个西红柿框左上角(320,180)右下角(480,300)那么中心点是(400,240)宽160高120。归一化后x中心400/12800.3125y中心240/7200.3333w160/12800.125h120/7200.1667。写成一行就是2 0.3125 0.3333 0.125 0.1667。注意框一定要贴紧果实外轮廓别为了好看多留一圈背景。多留背景会让模型学到错误的颜色上下文尤其是相邻的果和叶容易把类别判串。2.3 数据增强与数据集划分数据增强不是越多越好要针对性地补短板。我用过的组合是HSV色相扰动这是核心因为成熟度本质是颜色任务色相轻微扰动能让模型对光照色温更鲁棒、随机缩放和裁剪、马赛克拼接Mosaic把四张图拼成一张让模型在同一张图里见到不同尺度和背景、轻微的随机旋转。但水平翻转和垂直翻转要慎用因为西红柿在藤上的朝向有先验乱翻会引入不合理的形态。划分上我一般按训练集70%、验证集15%、测试集15%。关键是划分要按场景或批次来分而不是纯随机。如果你把同一批照片随机打散训练集和验证集里可能包含几乎一样的图验证指标虚高上线打脸。按拍摄场次划分更稳。数据集目录结构建议这样组织tomato_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── tomato.yaml对应的tomato.yaml内容path: ./tomato_dataset train: images/train val: images/val test: images/test nc: 4 names: [unripe, half_ripe, ripe, overripe]这个小文件是整个训练的入口路径写错是最常见的新手问题报错往往是找不到图片或者No labels found先检查这里。3. YOLOv8训练实战从环境到权重3.1 环境搭建与版本选择环境这块我先说结论Python 3.8到3.10都行别用太新的。核心依赖就一个ultralytics包它会自动带上PyTorch、OpenCV这些。安装命令pip install ultralytics pip install pyqt5 opencv-python如果你要用GPU训练先确认CUDA和PyTorch版本对得上。装完之后跑一句yolo checks它会打印出你的环境信息包括PyTorch版本、CUDA是否可用、当前设备。看到CUDA:0 (你的显卡型号)就说明GPU能用。实操心得虚拟环境一定要用。我踩过最坑的一次是全局环境里装了旧版PyTorch结果ultralytics跑起来报了一堆符号错误折腾半天才发现是版本冲突。用conda或者venv建个干净环境省下的时间远超建环境那两分钟。3.2 配置文件与训练参数训练命令看着简单参数每个都有讲究yolo detect train modelyolov8s.pt datatomato.yaml epochs200 imgsz640 batch16 lr00.01 patience50 device0 workers4逐个拆modelyolov8s.pt是预训练权重从它开始做迁移学习比从零训快得多也稳得多因为COCO预训练已经学到了通用的边缘和纹理特征。epochs200配patience50是早停机制如果50轮验证指标没提升就自动停防止过拟合和浪费算力。imgsz640是输入尺寸越大精度越高但显存和速度代价越大640是通用甜点。batch16在6GB显存上比较合适显存不够就降到8。lr00.01是初始学习率迁移学习用这个值没问题如果从零训要调小。关于学习率和batch的关系这里有个可参考的经验学习率大致和batch size的平方根成正比。如果你把batch从16降到8学习率可以适当乘0.7左右或者干脆开自动学习率lr00.01保持不变问题也不大YOLOv8默认带学习率预热和余弦退火容忍度比较高。3.3 训练过程监控与损失曲线解读训练一开始屏幕会打印每一轮的各种损失和指标。看什么首看box_loss定位损失、cls_loss分类损失、dfl_loss分布焦点损失。三个loss整体下降就是正常。第二个看验证指标mAP50和mAP50-95它们涨上去说明模型真的在学到东西。跑完之后训练目录下会有results.csv和一堆曲线图。用YOLOv8自带的绘图能力可以直接生成损失曲线也可以在Python里读csv用matplotlib画。看曲线有几个判读技巧如果训练loss一直降而验证loss开始升是过拟合的典型信号加数据增强或者减少epoch如果两个loss都不动可能是学习率太小或者数据有问题如果loss剧烈震荡多半是学习率太大或者batch太小导致梯度噪声大。提示不同loss的绝对数值没有可比性只能看趋势和相对变化。别因为cls_loss是0.3而box_loss是1.2就觉得分类有问题它们量纲本就不同。3.4 评估指标与调优方向核心指标是mAP50和mAP50-95。mAP50是把IoU阈值设成0.5时算的平均精度门槛低反映能大致框对的能力mAP50-95是把IoU从0.5到0.95每0.05取一个值算平均更严格反映定位精度。成熟度检测里因为果实大小差别不大mAP50-95通常比mAP50低不少这是正常的。除了mAP还得看混淆矩阵。成熟度任务特别容易在相邻类别间混淆比如half_ripe和ripe互错。如果混淆矩阵显示这两类互相打架解决办法有三个方向一是补充这两类的边界样本二是检查标注标准是不是太模糊三是考虑把这两类暂时合并再看效果。我一般会先做混淆矩阵分析再决定调优方向而不是盲目加epoch。4. PyQt5界面把模型变成能用的工具4.1 界面布局与功能设计界面设计的目标是让不懂代码的人能用。我的布局通常是左边一块大显示区放图片或视频右边一列控制区。控制区从上到下依次是载入图片按钮、载入视频按钮、开始检测按钮、置信度滑块、IoU阈值滑块、结果计数面板、导出结果按钮。置信度滑块控制的是conf参数也就是只有预测置信度高于这个值的框才会画出来调低会看到更多框但误检增加调高则漏检增加默认0.25。IoU阈值对应NMS非极大值抑制里的阈值作用是去掉重叠的重复框值越小心越狠默认0.45。这两个参数做成可调是因为不同场景下最优值不同现场的人可以自己微调比锁死在人定的值上灵活。4.2 QThread多线程推理别让界面假死这是PyQt5项目最容易翻车的地方。如果你直接在按钮的槽函数里跑推理界面的主线程被占住整个窗口会变成无响应的白色用户以为程序崩了。正确做法是把推理逻辑放到一个继承自QThread的worker类里主线程只负责发信号和收结果。from PyQt5.QtCore import QThread, pyqtSignal import cv2 from ultralytics import YOLO class DetectWorker(QThread): frame_ready pyqtSignal(object) stats_ready pyqtSignal(dict) def __init__(self, source, conf0.25, iou0.45): super().__init__() self.source source self.conf conf self.iou iou self.running True def run(self): model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results model.predict(frame, confself.conf, iouself.iou, verboseFalse) annotated results[0].plot() self.frame_ready.emit(annotated) self.stats_ready.emit(self.count_classes(results[0])) cap.release() def count_classes(self, result): names result.names counter {v: 0 for v in names.values()} for cls in result.boxes.cls.tolist(): counter[names[int(cls)]] 1 return counter def stop(self): self.running False主线程里创建worker、连接信号到更新显示的函数、调用start()启动。窗口关闭时记得调用stop()再wait()否则线程残留会导致程序关不掉。注意model.predict要设verboseFalse否则每一帧都会在控制台刷一堆日志视频一跑起来控制台直接爆炸拖慢性能。4.3 结果可视化与统计导出YOLOv8的results[0].plot()会自动帮你画框、打标签、标置信度直接返回一个numpy数组转成QImage就能显示在QLabel里。颜色是自动分配的每个类别一个颜色看起来挺清楚。如果需要自定义比如成熟用红色、未熟用绿色那就自己遍历boxes画用cv2.rectangle和cv2.putText。统计面板用QLabel或者QTableWidget展示各类别的数量实时刷新。导出功能我用pandas把检测结果整理成csv或者用openpyxl写进Excel字段包括帧号、类别、置信度、框坐标。这个数据对合作社做批次记录非常有用也是这套系统比纯检测demo更有价值的地方。4.4 打包与部署开发完想给别人用用PyInstaller打包。命令大致是pyinstaller -F -w --add-data runs;runs main.py-w是去掉控制台窗口--add-data把模型权重一起打进去。这里有个大坑PyInstaller打包后程序读取的文件路径会变__file__相关的相对路径会失效必须用sys._MEIPASS处理资源路径。我踩过好几次打包出来能开但读不到模型就是因为路径没处理。实操心得打包体积会很大因为PyTorch本身就占几百MB。如果一定要减体积可以考虑把模型导出成ONNX用onnxruntime推理能省不少但要注意导出时的动态维度设置和推理时的预处理对齐否则精度会掉。5. 踩坑实录与常见问题速查5.1 训练侧的高频问题训练阶段最常见的是指标不动和类别混淆。指标不动先查三件事数据集路径对不对看训练日志里有没有成功读到图片数量、标注格式对不对类别编号从0开始别用1、有没有足够的正样本。类别混淆前面说过靠混淆矩阵定位靠补充边界样本解决。还有一个坑是数据不平衡。如果某一类比如overripe样本特别少模型会倾向于把它判成别的类。补救办法是过采样加上针对性增强或者在配置里给类别加权。YOLOv8本身对轻微不平衡有容忍度但差得太多必须处理。5.2 界面与部署侧的高频问题界面侧最典型的是显示不出来和卡顿。显示不出来常见于两点一是QImage的内存没有正确拷贝CV2读出来的是BGR、QImage要RGB格式不对就是花屏或黑屏记得用cv2.cvtColor转换并按RGB排列二是某些环境下OpenGL相关组件导致窗口渲染异常这种情况可以尝试改渲染后端或调整显卡驱动设置实在不行退回到软件渲染。卡顿则多半是单线程推理或者每帧都重新加载模型。模型一定要在worker初始化时加载一次别在主循环里反复加载。另外视频抽帧处理也是个优化手段如果源是30帧的视频其实没必要每帧都推隔帧检测对流畅度提升很明显。5.3 常见问题速查表现象可能原因排查与解决训练报No labels found标签路径或格式错误检查图片和txt是否同名同目录类别编号从0开始训练loss不下降学习率不当或数据无标注打印数据可视化确认标注框调整lr0验证集指标虚高上线翻车划分时同批次图片泄漏按拍摄场景划分数据集视频检测界面假死推理在主线程执行改用QThread信号传回主线程更新UI显示画面花屏/颜色错BGR与RGB格式没转换cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)打包后读不到模型相对路径在打包后失效用sys._MEIPASS处理资源路径相邻成熟度类别互错边界样本不足或标准模糊补充中间态样本重新对齐标注标准显存不足OOMbatch或imgsz过大降batch到8或imgsz到512最后分享一个我自己反复验证过的经验别一开始就追求把所有类别都分得干干净净先把成熟和未成熟这种差异最大的二分做好跑通全流程拿到一个能用的基线再往上加类别、加数据、调参数。我见过太多人一上来就定五六档卡在数据标注那一步就放弃了。工程的正确姿势是先跑起来再优化而不是一步到位。另外这套系统的骨架其实是可以迁移的。你把西红柿换成苹果、柑橘、芒果换掉数据集和类别名训练命令和界面逻辑几乎不用动界面里的成熟度改成等级就行。我后来把这套代码改成了柑橘的糖度外观分级前前后后只花了一个下午迁移和重训。所以与其把它当成一个西红柿专用工具不如把它当成一个农产品视觉分级的通用模板来理解和复用这样这套代码的价值会大得多。