☰
青椒辣椒检测数据集:VOC+YOLO双格式构建与训练实践
2026/10/19 13:55:22 网站建设 项目流程

做农业视觉这行的朋友应该都有个同感:训练模型之前,先被数据按在地上摩擦。青椒辣椒类型检测数据集这个项目,光是整理标注就花了两周,真正训练验证其实只用了一天半。这套VOC+YOLO双格式、1086张、4个类别的数据集,单独拿出来写一篇,是因为它踩过的坑,比很多上千张的数据集都典型——温室里的反光、叶子遮挡、青椒和未成熟尖椒长得几乎一样,这些问题不亲手处理一遍,很难理解智慧农业的数据到底难在哪。

这篇文章会把整个数据集的构建过程拆开讲:4个类别为什么这么定、VOC和YOLO两种格式怎么安全转换、标注边界怎么把握、小体量数据集怎么训练才不翻车。适合准备做农业目标检测、搞温室巡检、采摘机器人,或者只是想把手头零散图片整理成可用数据集的开发者。接下来直接进入正题。

1. 先想清楚:这套检测数据集到底服务什么场景

1.1 智慧农业里的青椒辣椒检测需求

青椒和辣椒是设施农业里非常典型的检测对象。温室巡检机器人要统计每棵植株上不同果实的数量和成熟状态,采摘机械臂要定位果梗附近的可抓取位置,后端分拣线要按品种和成熟度分流。这些任务落到视觉上,第一步都是同一件事:把画面里的辣椒果实找出来,并且告诉下游系统它属于哪一类。

注意这里说的是“类型检测”,不是单纯做“有没有辣椒”的二分类。实际业务里,青椒、红椒、尖椒、朝天椒的商品价值完全不同,采摘时机也不同。青椒还在生长期摘了就亏,红椒已经转色再按青椒处理就会打乱分级。所以模型输出不能只是“辣椒”,必须是带类别标签的检测框。这也是为什么数据集要按类别标注,而不是简单画个框了事。

1.2 4个类别这样划分,模型才不会越学越糊涂

标题里写了4个类别,但没有说明具体是哪4类。按农业视觉项目里最常见的标注方案,这4类一般是:

类别典型外观业务含义
青椒(圆椒)圆果型甜椒,绿色果面,果肉厚未成熟或未完全转色的甜椒,通常按青椒销售
红椒甜椒转色后的红色果实,圆果型成熟甜椒,商品价值高于青椒
尖椒(线椒/牛角椒)长条型,果面偏绿,尾部渐尖常见炒食辣椒品种,长度和宽比明显区别于圆椒
朝天椒(小米椒)小果型,簇生或单生,成熟后多为红色干制、调味料原料,尺寸远小于甜椒

这里有一个容易犯的错误:有人会把类别直接定义为“青色的椒”和“红色的椒”,用颜色当类别。问题在于,同一棵植株上的甜椒会在几天内从绿色转红色,中间还有半绿半红的过渡期。如果只按颜色分,同一个品种、同一个成熟阶段的不同个体会被拆得乱七八糟。更合理的做法是“品种+成熟度”混标:青椒、红椒都指甜椒,只是成熟阶段不同;尖椒和朝天椒是独立品种。这样做,模型学到的不仅是对颜色的感知,还有果实的形状、大小、纹理等结构信息,后续换场景时鲁棒性更好。

类别定义还直接影响标注成本。我有一个建议:类别名称一定要写在项目文档里,配上3到5张参考图,标注员和算法工程师用同一份标准。否则每个人对标“尖椒”的理解都可能不同,返工是必然的。

1.3 1086张图片够不够用?小数据集的真实水位

先说结论:1086张不算大,但也不是不能用的水平。关键在于这1086张里包含了多少个目标实例。假设平均每张图有3到5个果实框,总实例数就在3000到5000之间。对于4类目标检测任务,单类别的实例数量如果能到800个以上,配合预训练权重,已经能训练出一个可用的baseline模型。

真正危险的不是总图片数量少,而是类别不均衡。如果青椒占了600张,朝天椒只有100张,那朝天椒的AP很可能一塌糊涂。我在项目里通常会要求每个类别至少要有150张以上的图片,并且在采集阶段就做一轮数量统计,发现哪类少了就补拍哪类,而不是最后统一用增强硬撑。

小数据集还有一个容易被忽视的问题:泛化能力有限。1086张如果全是从同一个大棚、同一种光照条件下拍的,换个温室或者换个季节,模型精度会明显掉点。所以小数据集的策略不是“多拍”,而是“覆盖米”多。每个场景少拍几张,场景总数多几个,比在一个地方拍上千张要管用得多。

2. VOC与YOLO双格式理解与转换

2.1 两种格式分别长什么样

VOC格式和YOLO格式是目标检测领域最常用的两种标注格式,但它们的数据组织方式完全不同。VOC格式是“一图一个XML”,图片信息和每个目标框的坐标一起存在XML文件里;YOLO格式是“一图一个TXT”,每行一条目标记录,坐标全部归一化到0到1之间。

一个完整的VOC数据集目录通常长这样:

pepper_dataset/ ├── JPEGImages/ # 原始图片 ├── Annotations/ # 同名XML标注文件 └── ImageSets/ └── Main/ ├── train.txt # 训练集图片名清单 ├── val.txt # 验证集图片名清单 └── test.txt # 测试集图片名清单

XML文件内部是通过标签层级来描述一个目标的:

<annotation> <folder>pepper</folder> <filename>IMG_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>green_bell</name> <difficult>0</difficult> <bndbox> <xmin>960</xmin> <ymin>540</ymin> <xmax>1500</xmax> <ymax>900</ymax> </bndbox> </object> </annotation>

YOLO格式的目录结构则更扁平:

pepper_dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ └── labels/ ├── train/ ├── val/ └── test/

每个TXT文件内容如下,每行分别是“类别ID 归一化中心x 归一化中心y 归一化宽 归一化高”:

0 0.6406 0.6667 0.2813 0.3333 2 0.7200 0.3500 0.1500 0.2000

为什么同一份数据要做成两种格式?因为训练生态和配套工具的口味不一样。很多现成的数据增强脚本、模型评估工具、可视化工具对VOC格式的支持更成熟,而YOLO训练框架对TXT格式是开箱即用。双格式的好处是拿到手之后不用再自己写转换,直接用就行。

2.2 坐标换算:最容易翻车的环节

VOC到YOLO的坐标转换是整个流程里翻车率最高的地方,公式本身很简单:

xc = ((xmin + xmax) / 2) / W yc = ((ymin + ymax) / 2) / H w = (xmax - xmin) / W h = (ymax - ymin) / H

这里最关键的坑是:分母W和H必须是原图的宽高,不是缩放后的,也不是归一化后的值。我自己就见过同事写转换脚本时,把已经归一化的坐标再除以一遍图片尺寸,导致所有框直接缩成几十分之一大小。

拿前面那个XML例子里的框来算一遍:原图1920×1080,bbox的xmin=960,ymin=540,xmax=1500,ymax=900。那么:

框宽 = 1500 - 960 = 540 框高 = 900 - 540 = 360 中心x = (960 + 1500) / 2 = 1230 中心y = (540 + 900) / 2 = 720 xc = 1230 / 1920 = 0.6406 yc = 720 / 1080 = 0.6667 w = 540 / 1920 = 0.2813 h = 360 / 1080 = 0.3333

YOLO的TXT里保存的就是这4个归一化小数。注意类别ID从0开始,green_bell如果映射到0,那它就是0,不是1。这个细节在混用不同版本数据时也容易错。

反向转换(YOLO到VOC)同样容易踩坑:把归一化的中心坐标和宽高换算回像素时,xmin = (xc - w/2) * W,ymax = (yc + h/2) * H。如果图片尺寸信息丢了,这步就无从算起。所以双格式数据集在归档时,一定要保留图片尺寸元信息,最好在文件名结构里就带上拍摄分辨率。

2.3 格式自查与校验脚本

做完格式转换之后,别急着开始训练,先跑一遍校验。我每次都会写一个简短的检查脚本,重点查3类问题:坐标越界、类别ID非法、图片和标注文件不对齐。

import os import xml.etree.ElementTree as ET image_dir = "pepper_dataset/JPEGImages" anno_dir = "pepper_dataset/Annotations" for xml_file in os.listdir(anno_dir): tree = ET.parse(os.path.join(anno_dir, xml_file)) root = tree.getroot() w = int(root.find("size/width").text) h = int(root.find("size/height").text) for obj in root.iter("object"): name = obj.find("name").text box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) if xmin < 0 or ymin < 0 or xmax > w or ymax > h: print(f"坐标越界: {xml_file} {name}") if xmax <= xmin or ymax <= ymin: print(f"宽高为负: {xml_file} {name}")

YOLO格式的校验逻辑是检查每行是否有5个数值,且数值都在0到1之间,以及图片文件名和TXT文件名是否一一对应。宁可多花10分钟做校验,也不要带着错误标注去训练,不然排查问题的时候你根本分不清是模型问题还是数据问题。

3. 数据集构建全流程实操

3.1 采集与初筛:多样性和清晰度哪个更重要

采集阶段的目标不是拍满1086张就收工,而是让每张图都有存在的理由。我按下面这个思路来规划拍摄清单:至少覆盖3个不同的大棚或地块,每个地块分早中晚不同光照时段,拍摄距离从半米以内的近景到两三米的远景都要有,角度包括平视、俯视和侧视。这样一份覆盖矩阵,比单点重复拍摄更能提升模型的鲁棒性。

拍摄设备用普通工业相机、手机或者巡检机器人自带相机都行,但分辨率尽量不低于1280×720,后期标注和训练都不吃亏。JPG格式即可,不要再用截图工具二次压缩。

初筛是很多人会跳过的步骤,直接后果就是模型在模糊帧上表现不稳。我常用的做法是写一个批量脚本,计算每张图的拉普拉斯方差,低于阈值的手动确认。这个阈值跟具体图像噪声有关,1080p环境下我一般先用100做初筛线,再人工看一眼。经验是,模糊和过曝的图片坚决不要,哪怕它含目标,因为在训练中它们贡献的大多是噪声。

3.2 标注规范与边界条件:什么该标,什么不该标

标注是整个工程里最耗时也最影响模型上限的环节。画框本身没什么难度,难的是边界判断。青椒被叶子挡住一半要不要标?两个尖椒重叠在一起怎么处理?远处的模糊果实能不能标?这些规则如果在开工前不定好,返工成本会非常高。

我在这套4类别数据集中定下的标注边界规则如下:

情况处理方式
果实可见面积占整体30%以下不标,标注了会污染边界回归
果实轮廓明确但局部被叶子遮挡标注,用可见区域的外接矩形,不主动猜测被挡住的完整轮廓
两个果实挨在一起或轻微重叠分别标两个框,边界重叠一点点可以接受
果实完全出画面,只剩小部分不标,除非截断部分小于整个果实面积的1/3
远景太小、无法确定品种的果实不标,宁可漏标也不要瞎标
成簇的朝天椒数量极多且密集如果任务是计数,逐果标;如果任务是区域识别,可以按簇标,但类别定义里要写清楚

这里有一个很重要的心得:检测框的边界不需要画到像素级完美,偏差几个像素通常不影响mAP太多。真正对精度有毁灭性影响的是两件事,一是类别标错,二是漏标。青椒和尖椒在小果期都长得细长且颜色接近,是标注错误的重灾区。我一般会在标注界面上把“青椒/尖椒”这类易混类别放在一起对比查看,人工二次确认。

3.3 数据增强与数据集划分:别让增强毁掉农业语义

数据增强在1086张小数据集上几乎是必须的。YOLO训练框架自带的增强配置里,我常用的基础组合是:mosaic增强(前10个epoch开启,之后关闭)、水平翻转、HSV颜色扰动、随机缩放和仿射变换。这些增强能让模型对光照变化、视角变化更鲁棒。

但农业场景有两个增强需要慎重。第一是垂直翻转,辣椒通常是垂直悬挂在植株上的,把图片上下翻过去之后,果实像从地面长上来,虽然模型可能不在意,但这种物理不自然的样本加多了会让低层特征产生偏移。除非你的采集数据里确实有大量倒挂视角,否则建议关闭。第二是色彩饱和度扰动幅度不要拉太大。温室里叶子是绿色的,青椒也是绿色的,如果把绿色调到发黄甚至发紫,模型会学到错误颜色关联。我一般把HSV的hue扰动控制在0.015以内,saturation和value可以放宽到0.5到0.7。

数据集划分同样有讲究。最忌讳的是随机划分时不考虑图片来源。同一个大棚同一时间段连拍的10张图,长得极其相似,如果其中8张进了训练集、2张进了验证集,那验证集分数会虚高得离谱。正确的做法是按拍摄批次划分:同一个地块、同一台设备、同一段时间拍的图,全部放进同一个集合。我用的是7:2:1的比例,训练、验证、测试,划分时固定随机种子,保证可复现。

4. 训练验证与常见问题排查

4.1 用这个数据集跑通YOLO训练

数据准备好了,训练本身倒是相对轻量。我用的是YOLOv8系列,网络结构在当前目标检测任务里属于稳定可靠的默认选择。数据集配置YAML文件如下:

path: /data/pepper train: images/train val: images/val test: images/test nc: 4 names: ['green_bell', 'red_bell', 'pointed_pepper', 'chili']

训练命令不复杂:

yolo train data=pepper.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=8

为什么起步用nano而不是larger模型?1086张的数据量喂给大模型很容易过拟合,nano参数少、泛化能力相对好,适合先跑通流程拿到baseline。如果验证集mAP不够,再往上换small或medium,同时配合更强增强。

训练时重点盯两个指标:mAP50和mAP50-95,前者是主要参考,后者看模型对框的定位精度是否细致。数据质量达标的前提下,4类小数据集mAP50跑到0.85以上不算难,0.8以下基本要先怀疑标注问题,而不是调超参数。

4.2 高频问题与排查技巧速查

把实际训练中高频遇到的问题整理成一张表,方便按图索骥:

现象可能原因处理办法
青椒在绿叶背景里漏检严重目标与背景颜色高度接近,特征区分度低补充侧面光和逆光样本,适当增强对比度扰动,增加不同绿色背景的图片
红椒与成熟朝天椒互相误检两者都是红色,模型只认颜色没学会形状增加红椒的大果近景样本和朝天椒的小果远景样本,让尺寸和形状信息更充分
远景小果目标漏检小目标像素占比太少,特征不足训练时用imgsz=960,推理时保持对应分辨率,或者对高分辨率图做滑动窗口切图
训练loss下降但val mAP来回跳数据划分泄漏或过拟合检查是否同一拍摄批次的图同时进了train和val,按批次重新划分,增加增强强度
某一个类别AP显著低于其他类该类样本数量偏少或边界定义模糊补拍该类图片,检查该类标注是否前后标准不一致,必要时对该类难例单独重标

这些坑我基本都踩过一遍。最典型的一次是青椒漏检,当时调的增强参数把绿色色相拉得太大,模型反而学习了“植物叶子的绿色”和“青椒的绿色”的变化规律,越训越不稳。最后把hsv_h从0.1降到0.015,同类样本的AP立刻回升。很多时候模型表现差不是架构问题,是数据本身给模型传递了错误信号。

4.3 一些小体量数据集特有的经验心得

小数据集有一个特别好用的策略:难例挖掘。在验证集上跑一遍已经训练好的模型,把所有置信度低、漏检、误检的图片单独收集起来,人工逐张看原因。如果发现是某种光照条件没覆盖到,就补拍这个条件的图;如果发现是标注错了,就马上改标注。这样迭代两三轮,比单纯把数据集从1086张扩到2000张效率更高,因为每张新图都打在模型的痛点上。

另外,负样本不该省。所谓负样本,就是完全没有辣椒果实的植株背景图,对应的TXT文件为空。加个50到100张背景图,能显著降低模型把叶子卷曲、水滴反光误识别成辣椒的概率。这在温室场景里特别管用,因为大棚膜和滴灌带的反光极容易产生假目标。

推理阶段也可以用一点小技巧:在估产和巡检这类离线场景中,开启TTA(测试时增强)能多捞回一些边缘漏检的果实,代价是推理时间变长。对于实时采摘机械臂,TTA不建议用,但离线统计果树数量时值得一试。

这套双格式数据集后续要扩展也很方便,如果哪天从检测框升级到实例分割,VOC里已有的矩形框可以作为分割标注的预标注,标注员只需要在框内修轮廓,能省掉大半从头画多边形的时间。数据格式的兼容性和规范性,在项目后期会变成一种隐性的生产力。

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

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

立即咨询