☰
基于YOLOv10的柿子成熟度检测:从数据构建到部署全流程解析
2026/10/17 7:18:34 网站建设 项目流程

做了两年果实检测,我最大的感悟是:能分清苹果和橘子的模型不难,难的是分清“明天摘”和“后天摘”的柿子。基于YOLOv10的柿子成熟度检测系统,本质上不是让模型认识柿子,而是让它学会从颜色、纹理和果形中判断所处的成熟阶段,并且同时支持图像、视频和摄像头实时检测三种输入方式。整个项目基于PyTorch框架搭建、用Python源码实现,从数据集构建到训练推理都有不少值得展开的细节。这套做法适合正在做农业视觉项目的开发者,同样适合想从通用目标检测往细粒度识别方向进阶的同学参考。

1. 为什么柿子的成熟度检测比想象中难得多

1.1 成熟度不是“是什么”的分类,而是“到了哪个阶段”的判断

通用目标检测解决的是“这是什么物体”,成熟度检测要解决的是“这个物体处于什么状态”。这两件事的难度完全不是一个量级。柿子从青涩到完熟,每一阶段的颜色、光泽、果实硬度、表面蜡质都在变化,而真正让视觉模型头疼的是:成熟度的变化是一条连续光谱,不存在严格意义上的“类间边界”。

举个例子,同一棵树上同一侧的柿子,相邻两个果实的成熟度可能只差一两天,但它们的橘子色在RGB数值上可能只差几个灰度级。这个差异比“阴天拍的青柿”和“晴天拍的转色柿”之间的差异还要小。模型一旦只学到光照纹理,就会把晴天青柿判成半熟,把阴影下半熟判成青果。所以做这类项目时,第一个要接受的事实是:不能把成熟度检测当成一个简单的多分类问题,而要当成一个对细微差别的回归理解问题。

实际落地更复杂。柿子成熟不均匀,很多品种是从顶部开始着色,蒂部还留着绿色;有的一侧向阳已经橙红,另一侧还发青。标注的时候如果不制定统一标准,十个标注入能给出八种不同结果,模型也会被标注噪声带偏。后面会专门展开数据集的分级标准,这里先提醒一句:训练代码反而是整个项目里最不容易出问题的部分,真正决定上限的是数据口径。

1.2 三种典型场景决定了不同的技术选型

同一个系统要支持图像、视频和摄像头实时检测,看起来很酷,但每一种输入对算力、延迟和精度的要求并不一样。我接触过的一个果园分拣试验项目,最初只想做静态图像识别,后来发现工人实际操作时希望对着托盘或传送带直接看结果,于是加了摄像头实时识别。这种需求演进很常见,但如果你一开始就照着一个固定分辨率、固定背景去设计模型,后面会很被动。

动态采集的场景中有三个明显不同的亚类:

场景摄像头安装方式核心诉求推荐模型规格
分拣线俯视固定机位、均匀光照高精度、单帧或多帧稳定YOLOv10m 或 l
手持设备巡检手持拍摄、自然光照中精度、低功耗、实时YOLOv10s
采摘机器人视觉运动平台、快速响应低延迟、边缘算力YOLOv10n 或 s + TensorRT

一开始我的建议是不要直接上最大模型。摄像头实时场景下Python推理本身就有一层开销,如果输入分辨率再上到960,帧率会非常难看。把基础版本做成可选择模型规格的形式,在代码里用参数切换n/s/m,实测下来比“一招鲜”的方案更实用。

1.3 YOLOv10在成熟度任务上的优势不是“更准”,而是“更适合部署”

YOLOv10发布之后,很多人关注的是它在COCO上的精度提升,但对柿子成熟度这种细粒度农业任务,我反而更看重它两个工程向特性。

第一,YOLOv10去掉了传统NMS后处理,采用一致的标签分配策略。推理链路少了后处理这一步,帧率能提高一点,而且导出的部署模型少一个环节,省掉了不少算子兼容的坑。第二,它的骨干部分引入了更强的注意力建模,处理“大面积同色背景下找颜色差异”这种任务时,比同体量的早期模型能多抓住一些纹理线索。虽然差距没有论文写的那样夸张,但在真实果园场景里确实能感受到。

当然,任何模型都不是万能的。YOLOv10依然需要你提供足够好的数据和合理的训练策略才能发挥价值。把PyTorch环境搭好、权重文件下载好,只是万里长征第一步。

2. 数据集的命脉:成熟度分级怎么定、怎么采集、怎么标注

2.1 分级标准:没有统一标准之前,模型都是空中楼阁

做成熟度检测最怕的就是在类别划分上“差不多就行”。我建议直接采用四阶段划分法:unripe(青涩)、color-turning(转色)、half-ripe(半熟)、ripe(完熟)。

这四类怎么界定?只看颜色是不够的,至少要参考三个维度:

  • 底色变化:柿果底色从深绿到浅绿、再到黄绿、橙黄、橙红。
  • 着色面积:红色或橙色着色区域占整个果面的比例,建议用10%、50%、90%作为三档分界线。
  • 果皮光泽:完熟果通常表面偏亮、反光明显,青果表面偏哑光。

为什么建议四类而不是三类?三类(青果、半熟、完熟)虽然标注省事,但转色和半熟之间跨度太大,模型在边界附近会频繁产生低置信度预测。五个类又会让标注者陷入“这到底算半熟还是快熟”的纠结。四类是在主观标注一致性和模型区分能力之间比较平衡的点位。

这里有一个让标注一致性大幅提升的小技巧:给每张参考图配上一张“黄金标准图”,标注颜色、尺寸、着色面积三个栏目的参考值。比如:unripe是果面绿色占比超过90%;color-turning是着色面积10%-40%之间;half-ripe是40%-90%之间;ripe是90%以上。实际标注时允许标注入在相邻两类的边缘做二次确认,最终所有标注结果至少经过一次交叉核对。这个流程比单纯增加标注人数有效得多。

2.2 数据采集:不要只在“最漂亮”的条件下拍

很多AI项目在实验室数据上表现很好,一到果园就翻车,核心原因是采集视角和光照太单一。你为了图省事把摄像头固定在正上方拍俯视图,训练集里就全是俯视的柿子。等你去农户手里做实测,手持设备从侧面拍,模型性能直接掉一大截。这是我在实际项目里真实发生的事情,后面踩坑部分会详讲。

正确做法是刻意制造“数据多样性”:

  • 多机位:俯视、45度角侧视、水平侧视都要覆盖;采摘场景还要模拟手伸进树冠时枝叶遮挡的状态。
  • 多光照:晴天直射、阴天散射、树荫下、傍晚弱光。同一批果实在不同光照下的色温差异巨大,不把光照多样性塞进训练集,模型就会把“阴天纹理”错误关联到“成熟度”上。
  • 多背景:绿色叶片背景、棕色干枝背景、黑色分拣筐背景、地面落果背景。尤其是青柿和绿色叶片混在一起时是最难的负样本。
  • 多成熟阶段混合:不要一株树只拍一种成熟度的果实。实际采摘场景中树上往往是青一半、黄一半、熟一半,训练集必须模拟这种混合局面。

关于数据量,我的经验是:每类至少要有800到1200个实例框,整体训练图片不少于2500张。如果条件有限,第一版可以缩到每类500个实例,但前提是覆盖的视角和光照必须够多样。数量不足靠多样性补,多样性不足再多图片也只是在重复记忆同一种纹理。

2.3 标注与格式转换:用矩形框还是多边形?

果实检测标注有个常见误区——果实又大又圆,用矩形框随便一拉就行。实际上柿子之间经常互相遮挡,一个矩形框很可能框住两个果实的局部。矩形框虽然也能训出模型,但会让边框回归的目标不干净,尤其影响对边缘果实的定位。

我推荐使用多边形标注,或者至少使用紧密贴合的旋转矩形。标注工具有两个选择:经典LabelImg轻量,适合快速标注;如果图片数量大,用带半自动分割能力的标注工具效率更高,比如X-AnyLabeling这类支持模型辅助标注的工具。先训一个初版模型,用它做预标注,人工只负责修正错框和漏框,能把工作量压掉60%以上。

标注完成后需要转成YOLO格式。每张txt文件的每一行格式是:class_id x_center y_center width height,其中中心坐标和宽高都除以图片宽高归一化到0到1。建议写个脚本自动划分训练集、验证集、测试集,比例8:1:1,同时保证同一棵树的照片不能出现在两个集合里,避免信息泄露导致验证指标虚高。

另外特别提醒:类别不平衡是成熟度项目里最容易出现的问题。因为完熟果最好采、最好拍,训练集中可能占了一大半;青果则往往因为“看起来没什么用”而被忽略,最终导致青果漏检率高得离谱。解决办法是在采集阶段刻意追补稀少类别,标注阶段对青果样本做过采样复制,训练阶段再配合损失函数权重调整。这三步缺一不可。

3. 环境配置与训练参数:YOLOv10落地前必须越过的坎

3.1 版本搭配:别让Python、PyTorch、CUDA互相打架

很多新手项目在第一步环境配置就卡住了,原因通常不是哪个软件装不上,而是版本互相不兼容。对于YOLOv10 + PyTorch这套组合,我实测比较稳定的搭配是:

  • Python 3.10
  • PyTorch 2.x(用官方对应CUDA版本安装)
  • CUDA 11.8 或 12.1
  • ultralytics >= 8.1.0
  • OpenCV >= 4.6

安装命令很简单:

# 创建独立环境,避免污染系统Python conda create -n persimmon python=3.10 -y conda activate persimmon pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install ultralytics pip install opencv-python

安装完可以跑一行命令验证:yolo detect predict source=https://ultralytics.com/images/bus.jpg model=yolov10n.pt。如果能看到输出图片,说明环境基本通了。有一类问题比较隐蔽:电脑里有多个Python环境,pip install装到了另一个环境,然后又用yolo命令发现找不到包。建议全程用conda环境激活后再执行安装和训练,不要混用全局pip。

3.2 数据集配置:YAML文件里的每一个字段都要仔细核对

YOLOv10在ultralytics框架下训练需要准备一个YAML数据集配置文件,核心结构如下:

path: ./datasets/persimmon # 数据集根目录 train: images/train # 训练图片目录 val: images/val # 验证图片目录 test: images/test # 测试图片目录 nc: 4 # 类别数量 names: 0: unripe 1: color-turning 2: half-ripe 3: ripe

三个容易踩坑的细节:第一,path建议用绝对路径或用相对当前工作目录的正确路径,搞错了会报“dataset not found”;第二,nc必须和类别数量严格一致,我见过有人改了names却忘了改nc,训练出来的模型类别张冠李戴;第三,训练图片目录里只放图片,不要把txt标注文件也丢进去,ultralytics会自动去相邻labels目录找同名txt。

文件目录结构推荐这样组织:

persimmon/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── persimmon.yaml

每一张images/train/xxx.jpg对应一个labels/train/xxx.txt,文件名要完全一致,否则训练时这张图被视为无标注背景图。

3.3 训练参数:先跑通再调优,别第一步就追求极限

训练命令其实不复杂:

yolo detect train \ model=yolov10s.pt \ data=persimmon.yaml \ epochs=200 \ imgsz=640 \ batch=16 \ device=0 \ patience=30

但每个参数背后都有取舍逻辑。imgsz=640是精度和速度的平衡点,逐级提升到960对微小颜色差别的判定有微弱帮助,但显存占用和推理耗时都明显增加,摄像头实时场景下不建议一开始就上960。batch大小取决于显存,8GB显存跑s模型用16,如果爆显存就降到8并同步降低workers。patience=30表示验证集指标连续30个epoch无提升就早停,合理设置能省不少时间。

训练完成后不要只看最终权重文件,一定把runs/detect/train/目录里的三个文件看一遍:

  • results.png:损失曲线和mAP曲线,看有没有过拟合迹象。
  • confusion_matrix.png:看成熟度相邻类别之间互相错分的情况。
  • val_batch_pred.jpg:直接看验证集上的预测效果,能直观发现青果漏检还是半熟误报。

训练结束后从runs/detect/train/weights/目录选择best.pt(验证集上指标最好的权重),不是最后一轮的last.pt。这一个小细节能直接决定后续推理效果。

4. 一份推理代码同时搞定图像、视频和摄像头

4.1 统一入口:用一个参数切换三种输入源

三种检测方式的推理逻辑本质上是一样的,区别只在读图环节:图像是一次性读取,视频是逐帧读取,摄像头是实时逐帧读取。所以合理的设计是写一个统一入口,用--source参数决定输入来源,避免维护三套重复代码。

import argparse import cv2 from ultralytics import YOLO def parse_args(): parser = argparse.ArgumentParser(description="柿子成熟度检测") parser.add_argument("--source", type=str, default="0", help="输入来源:图片路径 / 视频路径 / 摄像头索引(0)") parser.add_argument("--weights", type=str, default="best.pt", help="模型权重路径") parser.add_argument("--conf", type=float, default=0.45, help="置信度阈值") parser.add_argument("--imgsz", type=int, default=640, help="推理分辨率") parser.add_argument("--save", action="store_true", help="是否保存标注结果") return parser.parse_args()

--source这里做了一个小技巧:优先判断是否为摄像头索引。因为数字字符串“0”既可能被当成文件名也可能被当成摄像头索引,先尝试用isdigit()判断,避免把摄像头当成图片路径去读。

4.2 图像和视频推理:核心代码只有几行

图像检测和视频检测可以用同一个predict函数处理:

def predict_image(model, img_path, conf, imgsz): results = model.predict( source=img_path, conf=conf, imgsz=imgsz, verbose=False ) return results[0] def predict_video(model, video_path, conf, imgsz, save=False): cap = cv2.VideoCapture(video_path) assert cap.isOpened(), "无法打开视频文件" fps = cap.get(cv2.CAP_PROP_FPS) w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) writer = None if save: fourcc = cv2.VideoWriter_fourcc(*"mp4v") writer = cv2.VideoWriter("output.mp4", fourcc, fps, (w, h)) while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model.predict(frame, conf=conf, imgsz=imgsz, verbose=False) annotated = results[0].plot() if writer: writer.write(annotated) cv2.imshow("persimmon detection", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() if writer: writer.release() cv2.destroyAllWindows()

值得注意的细节是results[0].plot()能直接在原图上画好检测框和类别标签,省去自己写绘制逻辑。如果想自定义标签文字和颜色,可以用results[0].boxes拿原始数据:boxes.cls是类别索引、boxes.conf是置信度、boxes.xyxy是归一化前的坐标框。拿到原始数据后,你就能把它插进自己的业务系统,比如对接机械臂分拣逻辑或自动统计成熟果数量。

视频推理时把stream=True参数加进model.predict(),可以显著减少每帧的初始化开销,提升整体处理速度。这个参数在许多场景测试中都能感受到实际差距。

4.3 摄像头实时检测:延迟和帧率才是检验标准

摄像头的代码逻辑和视频几乎完全一样,区别主要在于帧率优化和稳定性处理:

def predict_camera(model, camera_index, conf, imgsz): cap = cv2.VideoCapture(int(camera_index)) if not cap.isOpened(): raise RuntimeError(f"无法打开摄像头索引 {camera_index}") # 摄像头默认分辨率有时很低,适当拉高 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while True: ret, frame = cap.read() if not ret: print("获取帧失败,尝试重新连接...") time.sleep(1) cap.release() cap = cv2.VideoCapture(int(camera_index)) continue results = model.predict(frame, conf=conf, imgsz=imgsz, verbose=False, stream=True) for r in results: annotated = r.plot() # 这里可以扩展:统计每个成熟度的果实数量,绘制曲线等 cv2.imshow("persimmon realtime", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

摄像头实时识别的性能瓶颈主要在模型推理这一步,而不是读帧。实测在一张消费级显卡上,YOLOv10s + imgsz=640的推理时间大约在十几毫秒到二十几毫秒之间,加上画框绘图,整体能保持几十帧。如果帧率还是不够,优先降imgsz到480,而不是换回更大的模型。如果连CPU上都要跑,那就只能选YOLOv10n并把输入降到416,牺牲一部分精度换取可用性。

另一个摄像头特有的问题:Windows下笔记本摄像头索引可能是0也可能被其他软件占用,建议启动时打印已打开摄像头的实际分辨率和索引,便于排错。摄像头在弱光环境下的自动白平衡会让画面颜色不停漂移,如果部署在固定光照的分拣线,最好在摄像头设置里关掉自动白平衡,否则成熟度判定会跟着画面色温一起抖动。

5. 从训练到部署的踩坑记录:真正花时间的不是训通

5.1 视角单一导致的实际应用翻车事故

前面提过的那个分拣试验项目,第一版训练集全部用固定俯视摄像头拍摄,验证集mAP50达到0.91,看起来效果不错。结果拿去给人工手持设备试用时,mAP直接掉到0.72,而且青果大量漏检。排查时发现俯视拍摄背景是深色传送带,柿子轮廓清晰;手持侧视拍摄背景是绿色叶片和天空光,青果混进叶片里几乎“隐身”。

这个问题的本质是模型把“背景与果实的对比关系”也当成了一种特征。解决办法不是加更多俯视图,而是重新去果园补拍不同视角和背景的照片,让模型学会忽略背景、关注果面本身。调整后同场景mAP回到0.87。此后再做任何农业检测项目,我都会在数据采集方案里强制要求“至少三种机位+三种背景”,而不是等实测翻车再补救。

5.2 光照突变带来的青果误报

另一个高频问题:阴天转晴天过程中,同一颗青柿在不同时刻的RGB值差异巨大。有一次实测系统在上午十点阳光直射时把大量转色果判成了完熟果,因为表面反光让颜色看起来更深更红。这让我意识到成熟度检测的输入特征受光照影响程度远超预期。

这次修复分两步走。第一步在训练阶段做数据增强,加大hsv色域扰动的幅度,让模型见过更多“同果不同色”的情况。ultralytics训练参数中的hsv_h、hsv_s、hsv_v就是干这个的,成熟度任务可以适当比COCO默认值再调大一些。第二步在推理阶段给摄像头加了简单的曝光补偿逻辑:采集一帧后先做白平衡校正,再送入模型。这两步配合之后,光照突变造成的误报少了很多。如果你在手机上做实时检测,建议同样在原生相机里固定曝光参数。

5.3 类别不平衡的表现与对策

训练初期我只看了总体mAP,觉得还不错,直到翻看混淆矩阵才发现问题:完熟果召回率0.93,青果召回率只有0.51。原因很简单,训练集中完熟果占了接近60%,青果样本不到15%。模型学到的最优策略就是“多报完熟果”,反正大数类会把梯度覆盖掉。

对策分三层。数据层面,重新补拍青果和转色果,把青果实例比例提升到25%以上;同时用成熟的半自动标注工具给青果做copy-paste增强,把青果贴到绿色叶片背景上,造出更多难例。训练层面,在数据集配置里设置类别权重,给青果更高的损失权重。评估层面,把验证指标改成“每类别的mAP”,而不是只盯总体mAP,每类低于阈值就直接判定训练未达标。这套三层方案实施后,青果召回率被拉到0.82,整体系统才真正具备可用性。

5.4 导出部署时的算子与精度问题

训练完成后如果要在实际设备上部署,一般会走两种路径:直接用PyTorch推理,或者导出ONNX再转TensorRT。PyTorch推理省事,但内存占用高、启动加载慢,适合摄像头路数少的情况。导出ONNX的命令很简单:

yolo export model=best.pt format=onnx imgsz=640

但导出前必须确认ultralytics版本与训练时一致,否则可能出现算子不兼容、导出的模型跑不出结果。YOLOv10虽然免了NMS,但如果版本不同,个别层的融合方式还是有差异,导出后最好用测试集数据和onnxruntime对比一遍输出结果,再决定是否进入TensorRT环节。Jetson设备上使用TensorRT的收益最大,能把延迟压到很低的水平,代价是集成时间较长。小项目可以先上ONNX,实测比PyTorch推理在CPU上也有明显提升。

5.5 关于迭代节奏和验收标准

这类成熟度检测系统最忌讳“一步到位”的开发节奏。我个人的习惯是先用两三百张图片做一次快速验证,看看四类成熟度在现有模型规格下能不能分开,能分开再扩数据;不能分开就先检查数据口径和标注一致性,而不是急着换更大的模型。每次数据集变更后重新训练,都要固定住验证集,保证不同版本之间的指标可比。

验收标准也不要只用mAP说话。一个果实检测系统是否可用,要看三个指标:成熟度的分类召回率、相邻类别的错分率、单帧处理耗时。错分率这个指标论文里很少提,但农场主最在意——把完熟果认成半熟只是不好看,把半熟果认成完熟卖出去,消费者投诉退货才是真问题。所以我在最终汇报里永远把混淆矩阵作为第一页。

最后再分享一个实用的小技巧:摄像头实时系统上线前,先用一段包含了晴天、阴天、树荫移动、人工拿果展示等复杂情况的视频做回放测试,把视频当成摄像头输入反复跑,能发现很多单帧测试发现不了的问题。我用这个方法找出过白平衡抖动、果框闪烁、置信度在临界值附近反复横跳等一堆问题。柿子成熟度检测刚做出来时看着我挺满意,真正过完视频回放测才发现距离稳定商用还有不少路要走。这些坑每踩一次,系统就扎实一分。

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

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

立即咨询