做柿子成熟度检测这个项目,一开始我的想法其实很简单:既然目标检测模型已经这么成熟,为什么不能直接套用一个现成的框架,把柿子的成熟度给识别出来?但真的动手之后才发现,从数据标注到模型训练,从图像推理到摄像头实时检测,每一步都有不少坑。今天就把我在这套“基于YOLOv10的柿子成熟度检测系统”里踩过的坑、总结出来的经验,一次性写清楚。这套系统用PyTorch框架搭起来,Python源码实现,支持图片、视频文件以及摄像头实时检测三种方式,适合想用目标检测解决农业场景问题的同学参考,也适合给那些刚开始接触YOLO系列却不知道如何落地到实际项目的人一个完整的思路。
如果你对目标检测的印象还停留在“跑个demo、识别一下猫狗”的程度,那看完这篇文章应该会有新的收获。因为成熟的柿子检测是个很典型的细粒度识别场景——同样是红色,生柿子和熟柿子的颜色差异非常微妙,光照稍微变一变,模型的判断可能就漂了。这也是为什么不能直接拿预训练模型去怼,必须自己准备数据、微调模型、调推理逻辑。下面我按照从整体设计到具体实现的顺序,一步步拆给你看。
1. 项目整体设计与需求解析
1.1 为什么选择YOLOv10来做柿子成熟度检测
市面上目标检测方案那么多,YOLOv8、SSD、Faster R-CNN、DETR都能用,最后我选了YOLOv10,原因有几个。最核心的一条是YOLOv10去掉了传统目标检测里的NMS(非极大值抑制)后处理步骤,整个模型的推理链路变成了真正的端到端。什么意思?传统的YOLO系列在模型预测完一堆候选框之后,还要做一次NMS来去掉重复的框,这个步骤虽然不大,但在批量推理或者实时检测时,会占掉不少时间,而且非常依赖置信度阈值的设置。YOLOv10通过一对一的标签分配策略,直接让每个目标只对应一个预测框,省掉了NMS,推理速度更快,代码逻辑也更简洁。
另一个重要原因是YOLOv10的结构在效率和精度之间平衡得比较好。它延续了YOLOv8的CSPDarknet骨干结构,但引入了更多轻量化设计,比如部分自注意力模块和更高效的特征融合方式。我最初尝试时对比过YOLOv8n和YOLOv10n在相同数据集上的表现,YOLOv10在柿子这种小目标并不密集、但颜色差异敏感的场景下,mAP能高出1到2个点,而推理速度还能快个10%左右。对于农业项目来说,往往需要在果园现场用普通电脑甚至边缘设备跑实时检测,这种效率优势很实在。
当然,选型也要考虑生态成熟度。YOLOv10的官方实现基于ultralytics框架,训练和推理代码跟YOLOv8几乎一样的书写方式,网上资料多,遇到问题也容易查。加上PyTorch作为底层框架,调试起来非常顺手,想要看中间层的特征图、改改损失函数,都不需要额外折腾。所以综合速度、精度、易用性和社区资源,YOLOv10是这套系统里最稳的选择。
1.2 系统功能拆解:图像、视频、实时摄像头三大模式
这个系统不是单纯做一个离线检测脚本,而是把三种输入方式整合到了一起。图像检测是最基础的,你给它一张柿子的图片,它返回带边界框和成熟度标签的结果。视频检测则是把视频文件按帧读取,逐帧送入模型,然后输出带标注的连续画面。实时摄像头检测跟视频文件检测的原理基本一致,只不过输入来源换成了摄像头的视频流,这对延时要求更高,所以我专门做了推理线程和显示线程的分离处理。
三种模式放在一起,主要是为了覆盖完整的应用场景。图像模式适合做抽样检查和样本分析,比如在分拣环节对传送带上的柿子拍照,快速判断成熟度分布。视频模式适合回放历史监控录像,分析某一段时间内果实成熟的变化过程。而实时摄像头模式则是最有落地价值的,比如果园里的监控摄像头对着果树,系统自动实时识别哪些柿子已经达到采摘标准,采摘机器人或者工人只需要看着屏幕上的标注框就能精准作业。
我在实现时把这三种模式统一封装成一个检测核心类,输入可以是图片路径、视频路径或者摄像头ID,输出都是统一的检测结果列表,包含每个目标的类别、置信度和边界框坐标。这样的设计让三种模式共用同一套模型和后期处理逻辑,维护起来非常轻松,想加新的输入源(比如网络RTSP流)也很简单。这也是我希望给各位一个提醒:不要把代码写成“图片检测一个函数、视频检测另一个函数、摄像头检测第三个函数”,功能可以分开,但核心逻辑一定要收敛。
2. 环境准备与数据集构建
2.1 环境安装与依赖配置
这套系统的开发环境其实很常规,不需要特别冷门的东西。我的主力配置是Python 3.9、PyTorch 2.0以上、CUDA 11.8,然后安装ultralytics库,它本身已经包含了YOLOv10的模型定义和训练推理工具。OpenCV是必须的,视频和摄像头读取全靠它。虽然ultralytics内部也依赖OpenCV,但建议单独装一个opencv-python,方便自己直接操作图像和视频流。
# 建议使用conda创建独立环境,避免污染系统Python conda create -n yolo10 python=3.9 conda activate yolo10 # 安装PyTorch,一定要根据自己机器的CUDA版本选择命令 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics和opencv等依赖 pip install ultralytics opencv-python有几点需要特别注意。如果你的电脑没有独立显卡,也能跑,只是训练速度会慢很多,建议直接使用CPU版本PyTorch,并选择YOLOv10n这个最小的模型。另外一定要核对ultralytics和PyTorch的版本兼容性,我之前遇到过因为ultralytics版本太旧导致YOLOv10的权重加载报错的情况,解决办法是升级到最新版。环境问题往往是新手最容易卡壳的地方,我自己的习惯是装完环境后立刻跑一个官方预训练模型的检测demo,确认一切正常再开始正式干活。
2.2 柿子成熟度数据集怎么做:采集、标注、增强
数据集是整个项目里最容易被低估的部分。很多人觉得目标检测就是把图片扔进模型训练就好了,实际上数据质量直接决定最终系统的上限。我做的第一个版本就是因为数据集太干净,导致一到真实果园环境里就频繁误检。
首先要明确类别定义。柿子成熟度一般可以分为未熟、半熟和成熟三个类别,在采摘场景里可能还要加一个“过熟”用于剔除坏果。不要贪多,类别越细,标注一致性越难保证。我最终用的是青果、转色果、成熟果三类,转色果就是半黄半红的状态,这个类别的特征非常模糊,也是模型最容易出错的地方。
数据采集时要注意覆盖不同的光照方向、拍摄距离、当天的时间段,以及不同的枝条遮挡情况。我用了大约两千张图片,其中一部分在果园实拍,另一部分是从不同视频里截帧得到的。这里有个经验:不要只用高清近景图,还要加入一些中等距离、有小面积遮挡的照片,因为真实监控摄像头装的不会离每一个柿子都那么近。
标注工具我用的是labelImg,输出YOLO格式的txt文件,每行是“类别 中心点x 中心点y 宽度 高度”。标注是件枯燥的事,但有一点必须强调——边界框不要卡着柿子边缘,一定要稍微留一点余量。因为成熟度判断需要依赖果皮颜色,如果框太紧,后期图像预处理可能会截掉一部分颜色信息,导致特征不完整。
数据增强我用的是ultralytics自带的增强策略,包括水平翻转、随机旋转、亮度对比度调整、HSV变化、缩放和mosaic。对于柿子这种颜色敏感的目标,亮度和颜色增强要格外小心,过度增强会让模型误以为颜色不重要。我的做法是把亮度扰动的范围控制在正负20%以内,色彩扰动控制在正负10%以内,这样模型既能适应不同光照,又不会丢失颜色这个关键特征。最后按照7:2:1划分训练集、验证集和测试集,注意同一棵树的不同图片尽量放进同一个集合,避免测试时模型“见过”同一棵树的颜色分布。
3. 模型训练与优化
3.1 YOLOv10的模型结构特点与训练参数
这里不打算把网络结构每个模块都背一遍,只讲跟柿子检测关系最大的几个特点。YOLOv10的主干网络用了C2f结构,相比以前的CSP结构,梯度流更丰富,对小目标的特征提取更友好。柿子在画面里的尺寸往往不大,尤其监控视角下可能只有几十个像素宽,所以这种结构优势很关键。颈部用了PANet做特征融合,把高层语义信息和低层细节信息混合,让模型同时具备“知道这里有个柿子”和“知道这个柿子的颜色具体长什么样”的能力。
训练命令我用了ultralytics的标准写法:
yolo detect train data=ven.yaml model=yolov10n.pt epochs=150 imgsz=640 batch=16 device=0 optimizer=AdamW lr0=0.01ven.yaml里面写的是数据集路径、类别数和类别名。imgsz我选640,虽然1280可能会带来更高精度,但训练时间和显存占用都会成倍增加,对柿子这种不算特别小的目标来说,640已经够用了。batch大小要根据显存来,我用的16GB显卡能跑到32,但建议保守一点用16,防止中途OOM。
训练轮数我设了150,但实际训练到80轮左右就已经收敛。我开了early stopping,连续20轮验证集mAP没有提升就自动停止,避免过拟合。optimizer选AdamW,学习率0.01,配合余弦退火调度。如果你用SGD,建议初始学习率调到0.02左右,但AdamW在这种中小数据集上更省心。
3.2 训练过程中的技巧与调参心得
第一次训练很容易犯一个错误:直接拿最终版本的数据集去训练,结果训练到一半发现某些类别总学不好,又回去加数据,来回折腾。我建议第一次训练前先只拿三分之一的数据快速跑50轮,目的不是追求精度,而是把整个流程走通,顺便看看三类样本的损失收敛情况。确认能正常收敛之后,再用完整数据和更长的训练轮数来跑最终模型。
类别不均衡是柿子检测里一个很现实的问题。大丰收季节里成熟果占多数,青果往往只有一小部分。如果直接训练,模型会对成熟果非常敏感,却经常漏掉青果。处理办法有两个:一是数据层面,在标注阶段留意各类别数量,尽量让青果也保证在总数的20%以上;二是训练层面,用ultralytics里的class weights参数,给样本少类别更高的权重。
另外一个很容易被忽略的点就是增强策略跟真实场景的匹配。我在训练时用了很大的mosaic增强,模型在验证集上表现不错,但放到真实摄像头画面上就崩了。这是因为mosaic把多张图拼接在一起,会改变目标的相对尺度,跟监控视角那种整体缩小的画面不一致。后来我把mosaic的开启概率调低,同时增加了一部分纯背景图的负样本训练,模型在真实视频里的表现才稳定下来。这种问题在标准benchmark上看不出来,必须靠实际场景测试反馈去调。
3.3 模型评估与推理速度实测
训练结束后不要只看loss值,要关注验证集上的精确率、召回率以及每个类别的AP。我用ultralytics自带的验证命令:
yolo detect val data=ven.yaml model=runs/detect/train/weights/best.pt它会输出mAP50、mAP50-95以及PR曲线。对于柿子成熟度检测,我更关心mAP50,因为只要框的位置大致正确的就可以了,对边界框像素级精度的要求没有那么苛刻。我最后训练出来的模型mAP50大约在0.95左右,三类中青果的AP最高,成熟果稍低一两个点,转色果最低,原因是转色果跟成熟果的颜色边界在标注时就存在主观差异。
推理速度方面,我用YOLOv10n模型配合半精度推理,在RTX 3060上跑640分辨率,单张图片的推理时间大约8毫秒,视频检测能做到超过100 FPS,摄像头实时检测绰绰有余。如果换成CPU,同样的模型单张图大约需要150毫秒,勉强可以做到每秒五六帧,用作实时检测会比较吃力。所以如果你要在没有GPU的环境下跑,建议再把输入分辨率降到416,而且尽量用nano模型,牺牲一点精度换取流畅度。
4. 检测系统的完整实现
4.1 图像与视频检测的核心代码流程
这里我不打算放完整的项目源码,毕竟代码量不小,但核心流程完全可以讲清楚。图像检测的逻辑就是:读取图片,送入模型,解析预测结果,然后画框和标签。用ultralytics的Python API写起来非常直观:
from ultralytics import YOLO model = YOLO("best.pt") results = model.predict("test.jpg", conf=0.5, imgsz=640, verbose=False) for result in results: boxes = result.boxes.xyxy.cpu().numpy() scores = result.boxes.conf.cpu().numpy() classes = result.boxes.cls.cpu().numpy().astype(int) for box, score, cls in zip(boxes, scores, classes): label = f"{model.names[cls]} {score:.2f}" # 用OpenCV画框和文字视频检测的本质就是循环读帧。我用的OpenCV的VideoCapture,每读一帧就做一次推理,然后把标注帧写入输出视频。需要注意两个性能点:第一,模型推理前要把帧缩放成640x640,但画框时要放回原坐标尺度,这一步要写对,否则框会偏移;第二,OpenCV默认读的是BGR格式,而模型训练时用的是RGB,推理前需要转换一下通道顺序,虽然ultralytics内部会做处理,但为了调试方便,我自己显式做了转换。
import cv2 cap = cv2.VideoCapture("test_video.mp4") while True: ret, frame = cap.read() if not ret: break rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = model.predict(rgb_frame, conf=0.5, imgsz=640, verbose=False) # 在frame上画框实际处理视频时,我还加入了一个抽帧间隔参数。如果是离线场景,不需要每一帧都做检测,每三帧检测一次然后把结果沿用到底,就能很大程度减少计算量,对分析成熟度变化来说完全够用。
4.2 摄像头实时检测的实现要点
摄像头实时检测是跟视频检测最大的区别在于实时性。我的处理方式是把摄像头读取和模型推理分离到两个线程,读帧线程不断抓取最新帧,推理线程负责把最新帧送入模型,显示线程只负责展示最新结果。这样做的好处是,如果某次推理慢了一点,摄像头缓冲区不会堆积老帧导致画面越来越卡,你看到的始终是接近实时的结果。
开启摄像头用cv2.VideoCapture(0)就行,如果是外接USB摄像头,可能ID不是0,需要根据实际情况调整。另外摄像头默认分辨率往往比较高,比如1920x1080,但推理端我们只需要640分辨率,所以我会先把帧resize到640再做推理,这样既能保证帧率,又不会丢失太多目标。实测下来,用YOLOv10n配合GPU,整个流程的端到端延迟大约30到50毫秒,体感上基本没有延迟。
实时模式里还有一个容易忽略的问题:曝光和白平衡。果园户外光线的变化非常剧烈,摄像头自动曝光会让柿子颜色忽明忽暗,影响检测稳定性。我后来在代码里固定了摄像头的曝光参数和白平衡参数,并且做了一层简单的颜色校准,就是拿一张标准白纸在系统启动时校正一下白平衡偏差。这套做法虽然朴素,但在实际使用中让误检率明显下降。
4.3 界面设计与部署打包
为了给现场人员使用,我做了一个简单的图形界面,用的Tkinter,左侧显示检测画面,右侧显示当前帧的统计结果,比如各类成熟度的数量。界面本身不复杂,重点是把模型推理和界面刷新协调好,不然界面会卡死。我用的是after循环去定时更新画面,而不是用while True阻塞主线程。
真正让我头疼的是部署打包。我给现场电脑打包了一个Windows可执行程序,用PyInstaller。这个过程中最麻烦的是要把ultralytics和PyTorch相关的动态库都一起打包进去,exe体积瞬间超过3GB,启动还慢。后来我改用另一个方案:不打包成单文件,而是打包成一个目录,把必要的模型权重文件放在资源目录里,启动时用相对路径加载。这样打包体积降到900多MB,启动速度也恢复正常。
如果你也是给现场环境做工具,我的建议是不要追求“双击一下就跑”的完美体验,因为任何打包方案都会遇到杀毒软件误报或者缺少VC运行库的问题。更稳妥的做法是直接在目标机器上装好Python环境,写一个启动脚本,然后给现场人员一个极简的操作说明。省事,也减少后续出问题的概率。
5. 常见问题与排查技巧
5.1 训练不收敛或过拟合的几个排查方向
很常见的问题是loss降不下去。如果验证集loss一直在高位震荡,首先怀疑学习率。可以试试把学习率调低一个数量级,比如从0.01降到0.001,然后重新训练。其次是数据问题,检查数据集里有没有大量的空标签图片,这类图占比太高会让模型全体都学不出有效特征。再一个就是类别混淆,尤其是转色果和成熟果之间的边界不清晰,会明显拖低整体loss。
过拟合则表现为训练集loss持续下降但验证集mAP不再提升。柿子数据集如果太小,这种情况非常常见。解决办法除了增加数据之外,最有效的是调整增强强度,尤其是对颜色和翻转的增强。我试过把随机擦除增强也打开,模拟柿子和叶片彼此遮挡的情况,虽然刚开始验证集mAP会掉一点,但到了真实场景反而更稳。
还有一个容易忽略的训练细节是预热阶段。ultralytics默认有前3轮warmup,如果你的数据集很小,比如只有几百张图,建议把warmup轮数提高到5,避免模型一开始就学偏。这个参数很多教程不会特意提,但在小数据集上作用很明显。
5.2 摄像头检测掉帧、卡顿的优化方案
摄像头检测卡顿,首先要确定瓶颈在哪里。如果CPU占用很高而GPU占用很低,说明预处理或后处理代码拖慢了速度。我遇到过比较坑的情况是OpenCV读取帧时默认开启了硬件加速选项,在某些机器上反而导致读取速度下降。可以直接用cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)来限制缓冲区大小,这样读取到的永远是最新帧,还能减少内存拷贝。
如果GPU占用已经很高但还是卡顿,就考虑降低推理分辨率,从640降到480,精度损失不大,但速度能快出一截。另外把推理用的模型权重转成半精度,也能收获立竿见影的效果。做法很简单,训练完成后这样转换:
import torch model = torch.load("best.pt") # 这里的加载方式以ultralytics实际接口为准 model.half()半精度推理在支持FP16的显卡上记得先设置torch.backends.cudnn.benchmark = True,让CUDA选择一个最快卷积算法。另外不要在每次推理时都重新加载模型,这个错误很基础,但确实有人犯。模型初始化一次,之后反复预测就可以。
5.3 不同环境下的光照干扰与防误检处理
光照是柿子成熟度检测里最大的干扰源。阳光直射时,青果表面会反光,看起来像成熟果;树荫下的熟果又会被阴影笼罩,颜色显得很暗。为了应对这个问题,我在后处理阶段做了两个优化。第一个是置信度阈值调整,默认0.25太低了,在户外容易出误检,我把阈值调到了0.5,宁可漏检也不误检,因为漏检还可以通过下一帧再补回来,误检会干扰现场判断。
第二个是连续帧确认机制。实时检测时,如果某个柿子在连续5帧里都被同一个类别识别,才认定它的成熟度状态改变了。这个机制能过滤掉一半以上的闪烁误检。图像检测模式下没有连续帧,我就额外增加了一个“边缘抑制”逻辑——高光区域和深色阴影区域先做一次亮度掩膜,再把这些区域的检测置信度乘以一个小于1的系数,减少它们的输出优先级。
最后还有一个心得是关于类别数设计的。如果现场确实只需要“能不能摘”这个二元判断,那就不要硬分三四个成熟度等级,因为中间态的类别边界模糊,模型自己都学不明白。我后期专门做过一次对比,把转色果归到成熟果里,只保留生熟两类,检测的稳定性提升了一个大档次。所以大家在设计自己的数据集时,一定要反复问自己:现场真正需要的决策是什么?类别粒度是跟着决策走,不要跟着论文走。
说实话,这套系统做下来,我最深的感觉是模型本身不是最难的,难的是把“成熟度”这个视觉概念定义清楚。同一个人在不同光线条件下标注出来的类别都可能不一致,更别说要教会模型去理解这种主观标准了。我的建议是在标注阶段做一张参考色卡,标注前先看一遍,每次归档前再随机抽一批重新核对,哪怕只花半小时,对最终模型精度的提升都远超过加跑几十轮训练。另外一个小技巧,就是把模型预测失败的那些图片单独建一个文件夹,持续回去翻看,你会发现错误模式其实非常集中,改数据比改模型更管用。这套系统后续想扩展的话,可以加一层简单的成熟度面积占比统计,让系统不止告诉你有几个熟柿子,还能估算出整棵树的成熟比例,用来指导采摘顺序。这个方向做起来也不复杂,就是在现有检测结果上按类别累加轮廓面积而已。