说实话,做智慧农业项目最讨厌的一环,不是算法调参,也不是设备调试,而是数据。尤其是那种“看起来简单、做起来想骂人”的农产品识别任务。青椒和辣椒,颜色相近、形态各异,不同品种之间甚至比跨越物种还难分。半年多前我们给某智慧农业项目做采摘分拣和长势评估的视觉模块,最大的瓶颈就是缺一套干净、规范、能直接喂给模型的数据集。前前后后攒了小半年,最后整理出一套包含1086张图片、4个类别、同时提供VOC和YOLO两种标注格式的青椒辣椒类型检测数据集。这篇文章就把这套数据集的构建逻辑、标注细节、格式转换和实际训练经验完整拆给你,供正打算做类似数据的团队参考。
1. 为什么“认得出辣椒”在智慧农业里比想象中难
1.1 品种识别是所有下游任务的地基
先聊一个容易被低估的问题。智能分拣、产量预测、成熟度判断、病虫害定位,这些听起来高大上的农业AI功能,第一步动作几乎都是同一件事——先框出果实,再判断它是什么品种。青椒和辣椒虽然在人类眼里都是“辣椒”,但在算法看来完全是两回事。
举个例子,我们在做温室辣椒长势评估时,需要统计不同品种的挂果数、果实直径分布和颜色变化曲线。如果第一层的检测器就把螺丝椒认成了朝天椒,那后面的所有统计指标全部作废。这类错误不像车牌识别错了那么显眼,但会让整个决策系统逐渐偏离真实情况。
市面上的公开数据集以自然场景为主,主要覆盖猫狗、车辆、行人,真正面向田间地头的农产品检测数据非常稀缺。即便搜到一些蔬菜数据,也是两三类的简单划分——比如“青椒 vs 番茄”——根本无法支撑“不同品种青椒辣椒类型检测”这种细粒度任务。所以自建数据集几乎是唯一出路。
1.2 现有公开数据的三大“硬伤”
我先后比对了多个公开农作物数据集,总结出三个明显问题,相信做农业视觉的人都有共鸣:
第一,类别粒度太粗。很多数据集只分“辣椒”一个类,可实际生产中,泡椒、菜椒、朝天椒、螺丝椒在形态、颜色、生长姿态上差异巨大,合在一起训练,模型完全学不到区分细节。
第二,拍摄场景过于单一。实验室白墙背景、强光均匀照明下拍出来的透明背景图,拿进大棚、露地、遮阳网、早晚不同光照的现场后,检测精度断崖式下跌。农业视觉最大的敌人不是算法,是环境光照的变化。
第三,标注质量参差不齐。有的标注框已经把叶子和茎包进去了,有的缺标、漏标严重,训练出来的模型在测试集上看着达标,换一批照片就会发现边界框乱跳。
正是这三个问题,推动我们决定从零构建一套真正面向实际农业场景、颗粒度到“品种级”的检测数据集。
2. 数据集整体设计与类别体系划分
2.1 为什么锁定4个类别,而不是更多
从任务需求出发,我们的项目主要覆盖菜椒、尖椒、朝天椒、螺丝椒这4类市面上最常见、也是分拣场景里最容易被混淆的品种。有些朋友会问,为什么不多做几个类别?比如杭椒、二荆条、小米辣、美人椒?
这个问题的核心是“类间差异”和“类内差异”的平衡。如果一个数据集里塞了十几种辣椒,其中几种长得几乎一样,只是粗细长短略有区别,那模型的分错率会急剧上升,标注和训练成本也都是指数级上升的。
相反,我们选的4个类别,彼此在关键特征上有足够清晰的区分维度:
| 类别代号 | 品种类型 | 形态特征 | 颜色与表面特征 | 典型大小 |
|---|---|---|---|---|
| C1 | 厚皮菜椒类 | 短粗、灯笼形或圆胖形 | 深绿至墨绿,果面光滑,有光泽 | 80–120mm |
| C2 | 尖椒类 | 长锥形,尾部渐尖,略微弯曲 | 绿色到黄绿色,表面微皱 | 150–250mm |
| C3 | 朝天椒类 | 短小,圆锥形,多向上生长 | 浅绿至红色,果皮较薄 | 30–60mm |
| C4 | 螺丝椒类 | 细长柱形,有明显螺旋褶皱 | 绿色带深色纵条纹,表面不平整 | 180–300mm |
之所以用代号而不是直接写品种名,是因为各地对品种的叫法差异极大——同样是“尖椒”,不同产区的形态可以差出好几个等级,用代号反而能减少歧义。模型学到的是形态特征的内在区分逻辑,而不是某个商业品种名字。
2.2 1086张图片的分布拆解
整套数据集总共1086张有效图片,4个类别各自约250–300张。为了保证类别平衡,标注完成后我们做了抽样统计,并且对样本量较少的类别进行了针对性补拍和网上公开图片的筛选补充,最终形成了以下分布:
- C1(厚皮菜椒类):287张,占比约26.4%
- C2(尖椒类):276张,占比约25.4%
- C3(朝天椒类):251张,占比约23.1%
- C4(螺丝椒类):272张,占比约25.1%
单张图片中目标数量的分布也很有讲究。有的图片只有一颗辣椒,有的图片一串五六颗,多目标图片约占45%。加入多目标样本的核心目的是模拟真实场景中果实互相遮挡、不同成熟度共存的情况,以及让模型学会在密集场景下正确分离不同个体。
每张图片的分辨率统一调整为648×640到1920×1080之间,大部分原始照片在2400万像素级别。我们没有将所有图片强行缩放到同一尺寸,而是保持自然比例,只在标注时严格参照原始分辨率下的框坐标。这个细节对后续目标尺寸的多样性至关重要——如果图片全被压缩到统一小尺寸,小目标的信息早就丢了。
3. 从零构建标注数据的完整流程
3.1 数据来源:三个场景缺一不可
自建数据集的第一个难点不是标注,而是怎么把“足够的多样性”装进1086张图片里。这段时间的采集主要覆盖三个场景:
第一是田间近景。使用手持设备在大棚、露地直接拍摄辣椒植株,拍摄时注意带上花萼、果柄、甚至相连的枝叶。这个场景下的样本能提供最自然的长势信息和空间上下文。
第二是分拣输送带场景。在模拟分拣台上、不同光照条件下拍摄,图片中往往只有果实没有枝叶,更接近收官前端的抓拍视角。
第三是市场货架场景。光线杂乱、背景(塑料筐、纸箱、其他蔬菜)干扰多,这部分图片对提升模型在复杂场景下的鲁棒性非常有效。
三种场景的比例大约控制在6:2:2。田间近景是主力,因为从实际使用需求来看,植株挂果状态的检测频率最高;输送带和货架场景更像是“压力测试”,确保模型不会因为换个环境就失灵。
3.2 标注细则:哪些必须框,哪些坚决不框
标注规范是整套数据集中最花心思的部分。我在一开始就定了几条硬规则,参与标注的同事都按这套规则执行:
- 只框出果实部分。花萼、果柄、茎叶不属于果实,不纳入边界框。
- 果实被遮挡但有可识别轮廓的,照常标注,但要确保边界框内主要区域是目标果实。遮挡超过60%且无法判断品种的不标注。
- 同一果实出现在两张相邻图片中,各自独立标注,不做跨图关联。
- 模糊到人类标注者都无法确认品种的果实,直接跳过。
- 边界框严格贴合果实的最小外接矩形,尤其是细长型的尖椒和螺丝椒,框的短边误差控制很关键。
这里想强调的是,标注标准的不统一是模型精度最大的隐形杀手。同一个果实,A标注员框得紧,B标注员多框了5%的背景,模型练出来之后最直接的体现就是mAP曲线波动大。所以我们在标注完成后专门安排了一位不参与初标的人做全量复核,对不符合规范或者边界框偏差严重的标注进行重新调整。
标注工具我们用的是LabelImg,原因无他——老牌、稳定、Python环境直接可用,而且对Pascal VOC格式的支持非常方便。虽然它界面比较朴素,但大批量数据标注时反而不会因为功能太花哨分散注意力。人工标注1086张图片的全部目标,两个标注员一共用了大概68个小时,加上复核调整的时间,总计在80小时上下。
4. VOC和YOLO双格式的适配逻辑与实际转换
4.1 两种格式到底差在哪,各自适合谁
VOC(Pascal VOC)格式和YOLO格式是目标检测任务中最常见的两种标注格式,但它们之间的差异不只是文件后缀名不同。VOC以XML文件存储每个目标的类别、名称和边界框坐标,坐标是绝对值,直接以像素为单位,人类读起来非常直观。YOLO格式则以纯文本文件存储,每行一个目标,格式依次是:类别ID、中心点x、中心点y、目标宽度、目标高度,且全部以图片宽高的比例归一化,取值在0到1之间。
从使用场景看,如果你用的是老牌两阶段检测器(各类基于Faster R-CNN的变体)或者SSD,VOC格式会更顺手,因为很多框架的原生数据接口就是为VOC设计的。而如果你选用YOLOv5、YOLOv8、YOLOv11或者同生态的模型,YOLO格式是它们从数据加载层面就直接支持的,省去了一份写自定义Dataset类的功夫。
正因如此,同时提供两种格式,等于把“选模型”的自由度全部还给使用者。你不需要为了用某个框架而去学另一个数据格式的解析方法,拿到手直接用。
4.2 从VOC到YOLO的转换原理和代码实现
整个转换过程本身不复杂,核心逻辑就是把XML中的绝对坐标(xmin, ymin, xmax, ymax)转换成归一化的(x_center, y_center, width, height),再把类别名称映射成从0开始的整数ID。
下面是我在实际转换中使用的核心代码片段,基于Python实现。这一块可以原样拿去套用,只需要根据自己的类别列表和文件路径做调整。
import os import xml.etree.ElementTree as ET from PIL import Image # 类别列表需要与训练时保持一致,索引从0开始 class_list = ["C1", "C2", "C3", "C4"] def convert_voc_to_yolo(xml_path, output_dir, image_dir): """将单个VOC标注XML文件转换为YOLO格式txt文件""" if not os.path.exists(output_dir): os.makedirs(output_dir) tree = ET.parse(xml_path) root = tree.getroot() # 读取图片尺寸 size = root.find("size") img_width = int(size.find("width").text) img_height = int(size.find("height").text) # 获取与XML同名的图片文件 img_filename = os.path.splitext(os.path.basename(xml_path))[0] + ".jpg" img_path = os.path.join(image_dir, img_filename) yolo_lines = [] for obj in root.iter("object"): class_name = obj.find("name").text if class_name not in class_list: continue class_id = class_list.index(class_name) bbox = obj.find("bndbox") xmin = float(bbox.find("xmin").text) ymin = float(bbox.find("ymin").text) xmax = float(bbox.find("xmax").text) ymax = float(bbox.find("ymax").text) # 转换为YOLO格式 x_center = (xmin + xmax) / 2.0 / img_width y_center = (ymin + ymax) / 2.0 / img_height width = (xmax - xmin) / img_width height = (ymax - ymin) / img_height yolo_lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 写入txt文件 txt_filename = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(output_dir, txt_filename), "w") as f: f.write("\n".join(yolo_lines)) # 批量处理 def batch_convert(xml_dir, output_dir, image_dir): for xml_file in os.listdir(xml_dir): if xml_file.endswith(".xml"): convert_voc_to_yolo( os.path.join(xml_dir, xml_file), output_dir, image_dir ) # 用法示例 batch_convert("path/to/xml_folder", "path/to/yolo_labels", "path/to/images")这段代码有几点需要注意:第一,读取图片尺寸信息一定要用XML里size标签的值,不要自己用PIL再去读一次。因为有些图片在存储时会带EXIF旋转信息,PIL读出来的宽高和标注时的宽高可能不一致,转换出来的归一化坐标就全错了。第二,输出时保留6位小数足够了,YOLO对精度的需求到6位之后实际训练时差异完全可以忽略。第三,转换完成后找一个文件抽查一下,确认坐标值都在0~1区间内,这是最快速的错误粗筛。
4.3 数据划分和目录组织
我一直坚持:数据集的划分要一次性固定下来,不要每次训练前临时划分。原因是多次随机划分会在不同轮次训练中产生数据泄漏的隐患——比如同一植株的多个视角图片分别落在训练集和验证集,视觉特征极度相似,验证集的评估结果就会虚高。
最终划分比例是8:1:1,即868张用于训练、109张用于验证、109张用于测试。划分的时候按图片所在目录做了一次分组,确保同一个采集场景的图片尽量集中在同一个集合中,避免场景泄露。
目录结构采用最经典的组织方式:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels_voc/ │ ├── train/ │ ├── val/ │ └── test/ └── labels_yolo/ ├── train/ ├── val/ └── test/5. 用这份数据集训练YOLO系列模型的心得
5.1 实测基线和关键超参
数据集整理完毕之后,我们用它跑了几组实验,基准模型选的YOLOv8s。硬件是一张普通消费级显卡,训练全程约3.5小时,图片默认640×640输入。以下是最有参考价值的一组基线和调参数据:
| 配置项 | 数值 | 说明 |
|---|---|---|
| 输入尺寸 | 640×640 | 兼顾速度和精度,后续可试1280 |
| batch size | 16 | 显存够用的话调32也可以 |
| epoch | 120 | 实测100轮后mAP已趋于收敛 |
| 优化器 | AdamW | 相比SGD前期收敛更快 |
| 初始lr | 0.001 | 配合warmup 3 epoch |
| 数据增强 | mosaic=1.0, flipud=0.0 | 关闭上下翻转,因为竖挂的辣椒反转后会引入语义错误 |
| mAP@0.5 | 0.918 | 基线结果 |
| mAP@0.5:0.95 | 0.743 | 说明中等难度目标被部分漏检 |
5.2 最容易踩的三个坑,逐个说清楚
第一个坑是类别不均衡导致的误检。前期我们图片分布没控制好,朝天椒的样本明显偏少,结果模型在螺丝椒上反而学会了把细小辣椒误报成朝天椒。最后靠补充样本和给损失函数加上类别权重才解决。
第二个坑是数据增强中的翻转设置。田间挂果的辣椒绝大多数是竖直向下或斜向上生长,如果开了上下翻转增强,模型会被“教”出大量语义矛盾的特征。我们验证过:不开上下翻转,mAP提高了将近2个百分点。
第三个坑是细长目标的边界框退化。尖椒和螺丝椒都是高长宽比物体,YOLO系列的回归头在长宽比大于5:1的目标上有时会出现边界框不稳定的情况。实测中我推荐在训练时把anchor策略调整一下,或者直接用YOLOv8默认的anchor-free设计,但需要给模型更多epoch去收敛这类几何形状。
5.3 合成数据增强的补充实验
为了让模型对“不同品种青椒辣椒”的泛化能力更强,我们还做了一组简单的合成增强实验:将图片随机叠加田间背景纹理、模拟雾天和逆光条件,用裁剪和拼接的方式生成了一部分额外训练样本。这个操作对真实田间场景的鲁棒性提升非常明显,尤其在阴雨和背光环境下,检测置信度提升约5%–8%。不过要注意的是,合成样本的比例不宜超过总训练集的30%,否则模型会在真实照片上出现“过度想象”的倾向——我在测试时发现加入过多合成样本后,模型会把一些背景纹理误判成果实表面纹理。
6. 数据集的边界范围与后续扩展思路
6.1 它擅长什么,不擅长什么
没有一份数据集是万能的,这套青椒辣椒检测数据集同样有自己的边界。
它擅长的场景是:固定或半固定的光照条件下的植株挂果检测、分拣线俯拍视角的果实识别、以及常规市场环境下不同品种辣椒的同框区分。
它不擅长的场景包括:极端逆光且阴影面积过大的图像、果实与背景颜色极其接近(例如绿色辣椒趴在深绿色叶子里,重叠区域过大时的分割难度)、以及超出4类之外的稀有品种识别。另外,这套数据以可见光图像为主,红外、热成像或多光谱输入不在支持范围内。
6.2 两个可行的扩展方向
如果你拿到这份数据集后想扩展,我建议先从两个方向入手:
第一个方向是增加类别。在现有4类基础上逐步纳入线椒、二荆条、美人椒等形态差异明显的类型,每类建议至少补足200张图片,然后重新训练。不要一次性加太多类别,否则标注和训练的回归成本会同时爆炸。
第二个方向是向“实例分割”升级。很多智慧农业项目其实需要的不只是框,而是果实区域的精确轮廓——比如精准喷药或者机械臂采摘时,定位到像素级边界比一个矩形框有用得多。你可以在当前数据集上把边界框标注升级为多边形标注,直接在标注工具里加载现有框,再做轮廓微调就行,成本比从零标注低很多。
在实际使用中我们还有一个体会:数据集的价值不在于它有多大,而在于标注逻辑有多么统一,场景覆盖有多么贴近真实。同样是1086张图片,一份规范整齐的数据集比一份三倍大的粗糙数据集更能训练出稳定模型。希望这份青椒辣椒检测数据集能帮你少走一些数据上的弯路。