公司动态
柑橘目标检测数据集制作:从labelme标注到YOLO训练实战
简介在农业视觉领域目标检测模型的落地效果往往取决于训练数据的质量与场景匹配度。面对公开数据集与果园实景脱节的问题构建专用的小规模数据集成为工程实践中的重要路径。通过标注工具对图像进行类别定义与边界框标注再将标注结果转换为模型可读的格式这一流程贯穿数据准备的核心环节。借助YOLO等主流检测框架可以有效训练出适应特定农业场景的检测模型服务于果园产量预估、落果统计以及采摘机器人视觉感知等任务。本文以柑橘目标检测为例介绍从图像采集、标注规范、格式转换到模型训练的完整流程分享小数据集制作中的关键经验与常见陷阱帮助开发者快速搭建属于自己的农业检测数据集。 做农业视觉的人应该都体会过“公开数据集看着挺多一进果园就傻眼”的滋味。我最早想跑柑橘目标检测翻来找去就是找不到能直接上手的“柑橘果目标检测数据集”搜到的要么是超市水果特写要么是国外果园的大远景等真的放到国内柑橘园里做产量预估、采摘机器人定位和落果统计时别说鲁棒性连基本场景都对不上。后来干脆自己动手用labelme一张张标注整理出一套580张目标检测图片的数据集里面同时覆盖了树上柑橘On-tree和树下柑橘果Under-tree两类目标。这篇文章不打算只丢一个网盘链接我会把为什么要分这两类、标注规范怎么定、格式怎么转、训练时踩了哪些坑完整交代一遍。适合正好想做农业目标检测、或者打算自己造小数据集的朋友参考。1. 树上树下两个类别藏着完全不同的农活逻辑1.1 从视觉模型视角看这两类目标根本不是一回事先看“树上柑橘”。画面大多是树冠区域果实挂在枝头被叶子、枝干遮挡是常态。同一个果子从侧面看可能是完整球形从正面看可能只露出一小块橙黄色。背景是密集的绿色叶片颜色跟未成熟果实的青绿色很接近模型学的其实是“颜色加纹理”的组合特征而不是单纯靠色块分割。再加上光照晴天时叶片缝隙里透下来的强光会形成高光斑果面出现反光阴天时整体偏灰果实的边缘对比度下降。这放在目标检测里属于不折不扣的困难样本。再看“树下落果”。目标通常躺在地上一部分被杂草、落叶盖住果面可能沾泥土颜色从橙黄变成棕褐色都很常见。画面背景是地面、草、土块光照普遍偏暗树冠阴影下尤其明显。但反过来树下果的尺度通常比树上远景果要稳定因为拍摄角度基本都是从上往下俯拍果实大小不会像树冠那样忽大忽小。这两个类放在一起训练如果labelme标注时没有分清楚模型很容易把“树上的圆形橙点”和“地上的圆形橙点”搅在一起。分开标注的意义不仅仅是输出两个类别名称而是让模型分别学习两套不同的上下文特征枝叶、树冠、高光对应树上果草地、泥土、阴影对应地下果。这样模型在推理时才能根据周围环境判断“这个橙色的东西到底是在树上还是在地上”。1.2 树上和树下的检测结果分别解决果园里的什么问题做产量预估时核心逻辑是统计树上果实数量。这里不追求识别每一个果实的品种而是要框得准能让后续计数模块根据框的中心点做聚合避免同一果实多帧重复计数。果实框中心点偏差太大计数结果会直接失真。树下果的检测更多用于农事管理。落果数量如果很大往往意味着病虫害风险、成熟期掉果、或者采摘作业时有遗漏。在果园里落果腐烂会滋生霉菌如果不及时清理会影响下一季果树。能自动统计树下落果管理人员就可以定点清理不用满园巡检。还有一个很实际的应用是采摘机器人。机械臂规划路径时如果摄像头只检测树上果可能把地面上已经掉落的果实误当成可采摘目标导致夹具扑空。把树上树下分开检测机器人就能提前排除树下区域的错误行径规划出来的路径会更干净。1.3 580张图放在工程角度是什么水平没有数据集使用经验的朋友听到“580张”可能会觉得太少。这里多说一句目标检测在垂直场景上的数据集数量重要但不是唯一重点。580张中等分辨率图像假设每张平均有6到10个目标标注实例就有几千个对微调一个小型模型来说其实够用。关键是场景覆盖要均匀、标注要干净这比凑到5000张但到处漏标要实用得多。农业AI项目尤其是预研性质的小团队几百张专用数据集往往比几万张但场景不符的公开集更容易快速跑通验证。当然580张也有明显上限。它适合做baseline验证适合做算法选型适合做教学示例但如果要做全品种、全气候、全果园的通用柑橘检测系统这点数据远远不够。我在后面会专门讲边界这里先给结论它是起点不是终点。2. 从果园到labelme采集和标注的全过程2.1 拍照片也需要设计不是拿着手机边走边拍拍果园数据比多数人想的要讲究。我第一次采集全赶上晴天中午高光把果面纹理全盖住了后期模型在阴天场景怎么调都差一口气。建议按“晴天、阴天、清晨或傍晚、雨后”几个条件分配拍摄时间让光照尽量多样。以我这批数据为例树上场景大概占六成树下场景占四成这个比例能保证两类都有足够的训练样本同时不会让某一类在损失函数里被彻底淹没。拍摄设备用手机完全够用但尽量不要开自动HDR和AI美颜。这些后处理会改变果实的真实颜色导致训练和实际部署之间的特征偏差。画幅统一横屏分辨率统一到1920乘1080或者1280乘720保存成jpg就行。最关键的一点是不要中途压缩同一批图片全部保持原始尺寸否则后面转换脚本处理坐标时会多出很多麻烦。拍树下落果时不要为了“好看”把盖住果实的草叶全部拨开。真实场景中落果大多有遮挡标注时只标能看清的部分模型到现场才不会崩。如果训练数据全是摆拍一样干净的目标到了真实果园满眼都是草叶遮挡检测率立马跳水。文件命名建议带上时间、地块、场景属性比如20230512_orchard01_on_0123.jpg。这个习惯看似只是改个名字但后期做数据划分、排查错误样本时能省大量时间。2.2 labelme标注规范框多大、标什么、怎么存Labelme安装很简单pip install labelme labelme打开后左侧工具栏有画矩形和多边形的选项。如果目标检测用矩形框建议直接按快捷键R画矩形而不是用多边形画完后转外接矩形。因为人工画多边形的外接矩形往往比直接画的矩形大一圈。不过在labelme的JSON里保存出来的shape_type如果是polygon转换脚本要处理求最大最小坐标如果是rectanglepoints里就是两个对角点处理起来更直接。一个典型的labelme JSON长这样{ version: 5.2.1, flags: {}, shapes: [ { label: on-tree, points: [[156, 89], [203, 142]], group_id: null, shape_type: rectangle, flags: {} } ], imagePath: 20230512_orchard01_on_0123.jpg, imageData: null, imageHeight: 1080, imageWidth: 1920 }这里label必须严格统一成小写on-tree和under-tree不要出现On_tree这样的变体否则格式转换时容易漏类。points存的是框的两个对角点YOLO后续需要的中心坐标和宽高都可以由这两个点算出来。标注规范里有几条要提前定死。一是“可标”的标准果实可见部分超过三分之一才标被遮挡超过三分之二不标远处小于15乘15像素的目标也不标。原因很简单看不见的果实没法生成可学习的语义标了反而制造噪声。二是漏标问题一张图里只要出现的目标能标就尽量全标。漏标一个果实对模型来说就是在告诉它“这个位置的果子不算数”训练出来容易出现奇怪的漏检。三是框的松紧标准我习惯让框贴着果实可见部分的外接矩形左右上下各留百分之一的余量这个标准后面细说。2.3 一人标注一人复核质量才靠得住小项目很多时候是一个人标完直接训练我强烈建议至少安排两个人一个画框一个核对。复核不是看图画得漂不漂亮而是逐张对比该标的是否漏标、框是否明显偏大偏小、类别是否贴错。落地果和浅色土壤接近时类别误标很常见一个人盯着屏幕时间长了很容易把“地上的枯叶”标成“果”。双人复核模式下我一般会记录每个图片路径、复核人、可疑目标。复核阶段发现的漏标目标会实时补框。最终检查文件数量是否与图片一一对应标注一个图旁边会多一个同名.json580张图就该有580个json缺一个后期训练就会报错。我在一个项目里真遇到过连续编号断档导致训练中断的情况排查了很久最后发现是标注过程中有一张图被误删了。3. 从labelme到模型训练格式转换、划分、训练策略3.1 labelme JSON转YOLO TXT的转换逻辑Labelme的JSON是通用标注格式训练框架一般不能直接读。YOLO训练要求每张图片对应一个同名.txt每行是class_id cx cy w h其中坐标是相对图片宽度和高度的归一化值。转换的核心就是把shapes里的矩形或多边形坐标变成外接矩形再归一化。下面这段脚本可以直接改成自己的路径用import json import os class_mapping {on-tree: 0, under-tree: 1} def convert_labelme_json(json_path, output_dir_label): with open(json_path, r, encodingutf-8) as f: data json.load(f) img_w data[imageWidth] img_h data[imageHeight] image_name os.path.basename(data[imagePath]) label_name os.path.splitext(image_name)[0] .txt out_label_path os.path.join(output_dir_label, label_name) lines [] for shape in data[shapes]: label shape[label] if label not in class_mapping: continue pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] xmin max(0.0, min(xs)) ymin max(0.0, min(ys)) xmax min(img_w, max(xs)) ymax min(img_h, max(ys)) if xmax - xmin 0 or ymax - ymin 0: continue cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{class_mapping[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) os.makedirs(output_dir_label, exist_okTrue) with open(out_label_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: json_dir labels_json out_dir labels_yolo for fn in os.listdir(json_dir): if fn.endswith(.json): convert_labelme_json(os.path.join(json_dir, fn), out_dir)这几个点建议特别注意坐标一定要除以图片宽高如果图片后来被裁剪过不能继续用原始坐标否则框全是偏移的。class_mapping里的类名必须和标注时的label完全一致大小写差异都会被跳过。JSON里的imageData字段可能很大但转换脚本用不到可以不用处理。如果文件实在太大再单独写脚本把这个字段置空不影响标注信息。3.2 数据划分别偷懒按场景属性切分常规做法是把580张图片随机按8比1比1分成训练、验证、测试集。但对于果园数据我建议按“场景来源”划分比如按拍摄日期或者果园地块分。原因在于同一棵树连续拍摄的多张照片高度相似如果这些相似图片同时落在训练集和验证集验证集的指标会虚高实际部署到新果园时效果立刻下降而且非常隐蔽不看数据分布根本发现不了。实际做法可以这样按日期或目录把某一时间段拍的图整体划入验证集验证集至少保证包含两类目标不能只含有on-tree否则训练过程中对under-tree的召回变化完全看不到。如果没有另外做测试集至少要把验证集留好迭代时不要反复拿同一组数据调参调过头否则验证集本身也会过拟合。3.3 快速跑通基线的训练配置用YOLOv8举例。先把图片和转换好的标签按下面的目录放好citrus_dataset/ images/train/ labels/train/ images/val/ labels/val/然后写一个数据配置文件path: citrus_dataset train: images/train val: images/val names: 0: on-tree 1: under-tree训练命令pip install ultralytics yolo detect train datacitrus.yaml modelyolov8n.pt epochs100 imgsz640 batch16基于小数据集我会从yolov8n.pt起步模型小、跑得快先让整个模型训练几十轮看loss能不能正常下降再决定要不要换s或m版本。如果想把预训练权重的作用发挥到位可以分两阶段前50轮冻结backbone只训练检测头后50轮解冻全部微调。小数据集上这么做通常比一开始全部参数一起训练更稳不容易出现loss震荡。3.4 训练过程中学会看关键指标训练时不要只盯着一张曲线图。建议每5轮记录一次验证集上的mAP0.5和mAP0.5:0.95同时看每个类别的AP。如果on-tree的AP明显高于under-tree基本可以判定类别不平衡问题已经影响到了模型如果验证集曲线波动剧烈先检查batch是不是设小了或者数据增强是不是太强导致小数据集反复被“变形”。对于这种场景mAP0.5能到0.8以上算正常mAP0.5:0.95如果低于0.5也不用太焦虑小目标多、遮挡多的数据集这个指标本来就不容易做高。真正要紧的是看每类的召回率农业机器人摘果时漏检比误检更致命。4. 踩坑实录小数据训练最常翻车的几个地方4.1 标注框的“松紧”不一致模型位置学不准这个问题最隐蔽因为表象是“训练收敛挺正常但可视化检测结果不对”。打开预测图一看预测框和真实果实边缘对不齐有时候框到了相邻两片叶子上。原因基本是标注标准不统一。一个标注者习惯紧贴果实边界画框另一个喜欢多留几个像素余量模型在回归任务里学到的目标中心点、宽高分布就会混乱。解决方法是把框的标准写进标注规范以果实可见部分的外接矩形为基准左右上下各留约百分之一框宽的余量。这个标准定下来之后模型学习到的框与果实边界会有一个稳定的小偏移后续做抓取定位也方便。4.2 重叠果实太多NMS后框的数量对不上树上的柑橘经常三五成群挤在一起很多框之间的IoU会超过0.5。训练时这是合理样本推理阶段却会出问题模型对重叠目标会输出多个互相重叠的候选框NMS一压可能只保留一个果实数量就被低估了。这种情况建议提前在标注规则里定死两个果实只要视觉上还能分辨出各自边界即便挨得很近也分别画框如果一个果实百分之八十以上被另一个挡住只给可见面积大的果实画框。训练时把NMS的IoU阈值从默认的0.45调到0.3左右能减少重叠框被合并的情况。如果项目迭代时间充裕也可以试试soft-NMS它对拥挤场景更友好。4.3 小目标太多mAP被拖垮得很惨580张数据里如果远景占了一部分一个果实在1920分辨率下可能只占40像素左右在YOLO特征金字塔的深层这样的目标几乎看不见。如果还在用640输入尺寸训练结果就是近处果实检测得很好稍远一点的果实全漏。实用思路有三个。第一切片训练把大图切成多个patch例如把1920乘1080切成4个960乘540变相增大目标在网络输入中的比例但patch重叠区域要注意不要重复统计。第二推理阶段用SAHI工具它会自动对大图做切片推理再合并结果适合模型训好后直接提升小目标召回率。第三如果硬件允许训练时把imgsz从640提升到1280模型对小目标细节的感知会明显变好代价是显存占用和推理耗时都会上涨。4.4 类别不平衡One-tree框多Under-tree框少只要统计一下转换后的txt里每类的框数量大概率会发现on-tree的实例数比under-tree多很多。这是场景本身决定的一棵树上有几十个果实而地上的落果通常比较稀疏。类别不平衡会让模型偏向检出on-treeunder-tree的召回率偏低。简单处理是给少数类别加权YOLO的训练配置里可以调整分类损失的权重更直接的方式是训练时对under-tree的图片做过采样比如复制少数类样本让每个epoch里两类图片数量更接近。如果原始标注已经足够也可以试试mixup增强把under-tree目标贴到其他背景上生成合成样本这样既增加了数量又不损失背景多样性。5. 这套数据集的边界和下一步扩展5.1 它能做什么不能做什么目前来看这套580张的数据集用于中等分辨率、白天自然光场景下的树上树下柑橘检测足够跑出一个可用的baseline。用它来验证算法流程、做对比实验、搭建采摘机器人的初版感知模块都是可以的。但它不是万能的。如果换一个差异很大的品种比如砂糖橘、柠檬、柚子果实颜色和大小完全不同直接用这个数据集大概率表现不佳。夜间、雨天、强逆光等极端天气场景也没有专门覆盖。所以不要把它当成全品种全气候的通用柑橘数据集而是当成一个可靠起点。5.2 想让模型更皮实应该怎么补数据提升泛化第一步不是继续拍更多普通照片而是做困难样本挖掘。把当前模型在果园视频流上跑一遍把置信度低、漏检、错检的帧抽出来人工筛选后补标注。这类数据对模型的提升往往比随机增加几百张常规图片更明显。第二步是扩展传感器维度。比如加多光谱或红外图像可以在夜间和叶片背景复杂时提供更丰富的特征用无人机构建俯拍视角检测树冠顶部果实这与地面视角互补对产量估算的完整度很有帮助。5.3 一条可以复制的迭代路线如果继续推进这个数据集我建议按下面的路线走版本1.0当前580张建立baseline记录各类别的mAP、precision、recall。版本1.1用baseline对视频帧做困难样本挖掘挑选困难帧补到800到1000张重新训练。版本1.2按果园区域划分做跨域验证统计模型从A园到B园的性能下降幅度找出哪些场景最影响泛化。版本2.0加入多光谱或红外图像训练多模态融合检测模型面向夜间作业和复杂天气场景。每一步都记录指标方便后续复盘。记录格式可以简单一点一个Excel表就够了但字段要稳定日期、模型结构、训练集数量、各类别AP、mAP、备注。数据量小的时候不觉得等迭代到版本2.0回头查版本1.0的指标你就知道这个记录习惯有多重要。我在实际做这批数据的过程中最大的体会是“干净”比“大”重要。580张图听起来不多但双人复核加格式统一之后模型收敛速度比我预期快很多。后续如果谁想扩展这套数据建议先从困难样本挖掘做起而不是急着堆数量。另外动手训练之前一定先跑一遍脚本检查json和图片是否一一对应这个动作能替你省掉一晚上的排错时间。本文还有配套的精品资源点击获取