
说实话做智慧农业项目最讨厌的一环不是算法调参也不是设备调试而是数据。尤其是那种“看起来简单、做起来想骂人”的农产品识别任务。青椒和辣椒颜色相近、形态各异不同品种之间甚至比跨越物种还难分。半年多前我们给某智慧农业项目做采摘分拣和长势评估的视觉模块最大的瓶颈就是缺一套干净、规范、能直接喂给模型的数据集。前前后后攒了小半年最后整理出一套包含1086张图片、4个类别、同时提供VOC和YOLO两种标注格式的青椒辣椒类型检测数据集。这篇文章就把这套数据集的构建逻辑、标注细节、格式转换和实际训练经验完整拆给你供正打算做类似数据的团队参考。1. 为什么“认得出辣椒”在智慧农业里比想象中难1.1 品种识别是所有下游任务的地基先聊一个容易被低估的问题。智能分拣、产量预测、成熟度判断、病虫害定位这些听起来高大上的农业AI功能第一步动作几乎都是同一件事——先框出果实再判断它是什么品种。青椒和辣椒虽然在人类眼里都是“辣椒”但在算法看来完全是两回事。举个例子我们在做温室辣椒长势评估时需要统计不同品种的挂果数、果实直径分布和颜色变化曲线。如果第一层的检测器就把螺丝椒认成了朝天椒那后面的所有统计指标全部作废。这类错误不像车牌识别错了那么显眼但会让整个决策系统逐渐偏离真实情况。市面上的公开数据集以自然场景为主主要覆盖猫狗、车辆、行人真正面向田间地头的农产品检测数据非常稀缺。即便搜到一些蔬菜数据也是两三类的简单划分——比如“青椒 vs 番茄”——根本无法支撑“不同品种青椒辣椒类型检测”这种细粒度任务。所以自建数据集几乎是唯一出路。1.2 现有公开数据的三大“硬伤”我先后比对了多个公开农作物数据集总结出三个明显问题相信做农业视觉的人都有共鸣第一类别粒度太粗。很多数据集只分“辣椒”一个类可实际生产中泡椒、菜椒、朝天椒、螺丝椒在形态、颜色、生长姿态上差异巨大合在一起训练模型完全学不到区分细节。第二拍摄场景过于单一。实验室白墙背景、强光均匀照明下拍出来的透明背景图拿进大棚、露地、遮阳网、早晚不同光照的现场后检测精度断崖式下跌。农业视觉最大的敌人不是算法是环境光照的变化。第三标注质量参差不齐。有的标注框已经把叶子和茎包进去了有的缺标、漏标严重训练出来的模型在测试集上看着达标换一批照片就会发现边界框乱跳。正是这三个问题推动我们决定从零构建一套真正面向实际农业场景、颗粒度到“品种级”的检测数据集。2. 数据集整体设计与类别体系划分2.1 为什么锁定4个类别而不是更多从任务需求出发我们的项目主要覆盖菜椒、尖椒、朝天椒、螺丝椒这4类市面上最常见、也是分拣场景里最容易被混淆的品种。有些朋友会问为什么不多做几个类别比如杭椒、二荆条、小米辣、美人椒这个问题的核心是“类间差异”和“类内差异”的平衡。如果一个数据集里塞了十几种辣椒其中几种长得几乎一样只是粗细长短略有区别那模型的分错率会急剧上升标注和训练成本也都是指数级上升的。相反我们选的4个类别彼此在关键特征上有足够清晰的区分维度类别代号品种类型形态特征颜色与表面特征典型大小C1厚皮菜椒类短粗、灯笼形或圆胖形深绿至墨绿果面光滑有光泽80–120mmC2尖椒类长锥形尾部渐尖略微弯曲绿色到黄绿色表面微皱150–250mmC3朝天椒类短小圆锥形多向上生长浅绿至红色果皮较薄30–60mmC4螺丝椒类细长柱形有明显螺旋褶皱绿色带深色纵条纹表面不平整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 两种格式到底差在哪各自适合谁VOCPascal VOC格式和YOLO格式是目标检测任务中最常见的两种标注格式但它们之间的差异不只是文件后缀名不同。VOC以XML文件存储每个目标的类别、名称和边界框坐标坐标是绝对值直接以像素为单位人类读起来非常直观。YOLO格式则以纯文本文件存储每行一个目标格式依次是类别ID、中心点x、中心点y、目标宽度、目标高度且全部以图片宽高的比例归一化取值在0到1之间。从使用场景看如果你用的是老牌两阶段检测器各类基于Faster R-CNN的变体或者SSDVOC格式会更顺手因为很多框架的原生数据接口就是为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兼顾速度和精度后续可试1280batch size16显存够用的话调32也可以epoch120实测100轮后mAP已趋于收敛优化器AdamW相比SGD前期收敛更快初始lr0.001配合warmup 3 epoch数据增强mosaic1.0, flipud0.0关闭上下翻转因为竖挂的辣椒反转后会引入语义错误mAP0.50.918基线结果mAP0.5:0.950.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张图片一份规范整齐的数据集比一份三倍大的粗糙数据集更能训练出稳定模型。希望这份青椒辣椒检测数据集能帮你少走一些数据上的弯路。