尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

自建农业目标检测数据集:胡萝卜成熟度检测VOC标注全流程实操

自建农业目标检测数据集:胡萝卜成熟度检测VOC标注全流程实操 简介目标检测模型的训练效果高度依赖数据质量而在农业场景中通用公开数据集难以覆盖地下根茎类作物细长、半埋土的形态特征自建数据集成为解决这一痛点的关键路径。为构建高质量的农业检测数据集需要从数据采集、图像清洗、标注规范到格式转换建立完整闭环。本文以VOC格式为标注基础系统讲解如何利用LabelImg完成目标框标注解析XML核心字段含义与质量校验方法并演示VOC转YOLO格式的归一化坐标换算逻辑。通过合理的标注口径与数据划分可有效提升模型在田间复杂环境下的泛化能力该实践适用于胡萝卜等农作物检测任务为农业智能视觉系统的数据工程提供可复用的参考方案。 去年夏天我在做一个胡萝卜成熟度检测的小项目一开始想着直接拿开源数据集来练手结果翻了一圈自动驾驶、行人检测、工业缺陷检测的数据集倒是不少唯独胡萝卜这种细长、半埋土里的目标公开数据集几乎为零。后来只能自己动手从田间采集到标注再到训练完整走了一遍胡萝卜检测数据集VOC格式标签的制作流程。这篇文章就把这段实操经验完整写下来包括采集方案、标注规范、VOC格式细节、格式转换脚本和踩过的坑希望能帮到同样在做农业目标检测的同学。1. 胡萝卜检测数据集整体设计与格式选型1.1 为什么必须自建数据集而非用公开数据很多刚接触目标检测的人容易有个错觉只要模型选得好数据集随便找一个就能用。但农业场景尤其是地下根茎类作物和通用目标检测的差别非常明显。胡萝卜在图像里通常是细长条状只有顶部露出土面颜色和土壤、枯叶、石块很容易混在一起光照一变同样的萝卜可能看起来完全是另一个样子。通用数据集里的物体类别和这个形态差异太大迁移效果非常差所以必须自建。另外任务目标不同标注口径也不一样。如果你的项目是胡萝卜成熟度检测那么标注的是露出土面的根茎部分通过根茎的粗细、长度、颜色来判断是否可收获如果你的项目是田间杂草与胡萝卜识别标注对象又会变成整株叶片。所以自建数据集不仅是为了解决有没有的问题更是为了解决标注口径是否匹配任务的问题。我强烈建议动手采集之前先把检测目标定义清楚最好用文字写下来后续所有标注人员都按这个标准执行。1.2 为什么选择VOC格式作为基础标注格式现在目标检测的标注格式五花八门VOC、COCO、YOLO txt、LabelMe json……我最终选择VOC格式主要是因为它的兼容性和可读性。VOC格式以XML文件为载体每个标注文件对应一张图片结构清晰用文本编辑器就能打开检查出了问题定位非常方便。更重要的是当前主流检测框架几乎都能直接或间接支持VOC格式后续要转成YOLO格式、COCO格式都有成熟脚本。还有一个很实际的考虑LabelImg等主流标注工具默认就是VOC格式。用这个格式可以最大程度减少工具链的折腾成本让标注工作专注于画框本身而不是跟格式较劲。下面是我最终使用的数据集目录结构CarrotDataset/ ├── JPEGImages/ # 原始图像 ├── Annotations/ # VOC格式XML标注文件 ├── ImageSets/ │ └── Main/ # train.txt / val.txt / test.txt ├── labels/ # 转成YOLO格式后的txt文件可选 └── classes.txt # 类别列表这个结构其实就是PASCAL VOC的标准布局后续接入任何训练框架都方便。不过要注意一点VOC原始格式中ImageSets/Main下的txt文件通常只写图片文件名不含扩展名每行一个用来划分数据集这个细节很多人一开始会忽略。2. 数据采集与预处理实操从田间到可用图像2.1 田间拍摄方案的三个关键维度数据采集决定了数据集质量的上限标注只是尽可能保住这个上限所以采集阶段千万不能敷衍。我在拍摄时重点控制了三个维度。第一是场景多样性。不要只在一小块地里拍建议至少覆盖3个以上不同地块包含不同土壤类型沙土、壤土、不同生长阶段幼苗期、膨大期、成熟期、不同密度稀疏种植、密植。这样模型才能学到胡萝卜在不同环境下的样子。第二是光照与天气。我分别在大晴天上午、傍晚逆光、阴天、雨后土湿等条件下采集。事实证明雨后带水珠的胡萝卜和暴晒后发蔫的胡萝卜在图像特征上差异非常大而这些差异恰恰是模型在真实田间部署时必须面对的。如果只挑晴天中午拍模型很容易过拟合到强光干土的单一风格上。第三是拍摄角度与距离。检测场景分为近景和远景两类近景是机器人收获时的视角距离大约30到60厘米胡萝卜占画面较大远景是无人机或田间监测柱的视角胡萝卜在画面中较小甚至只有几十个像素。建议两类都采集并按照目标尺寸分布合理分配数量避免模型对小目标完全失灵。我最终采集了3800张有效图像其中近景约占六成远景占四成。分辨率统一处理为1920x1080这个分辨率在后续训练时能兼顾显存占用和标注效率。如果源图是4000x3000的大图建议缩放到1920边再标注不然标注软件会明显卡顿。2.2 图像清洗原则宁缺毋滥采集回来的原始图像不能直接进入标注流程必须做一轮清洗。我在清洗时定了一个三不标原则严重模糊的不标、目标占比过小且无法确定位置的不标、重复度过高的不标。模糊图像为什么不能要因为目标检测模型对标注框内的纹理特征极其敏感模糊图像会让模型学到错误的边缘信息尤其是胡萝卜这种颜色对比度不高的目标模糊图基本等于噪声。我遇到过一批用运动相机拍摄的图像车辆行进中拍出来全是拖影人眼勉强能看出轮廓但标注后训练发现mAP反而下降后来排查才发现就是这批低质量图拖了后腿。重复度过高的问题更隐蔽。连续拍摄的连拍序列里相邻几帧可能几乎一样如果这些近似重复的图像同时进了训练集和验证集会造成严重的数据泄露评估结果虚高。我建议用感知哈希算法对图像做去重简单来说就是计算每张图的hash值把相似度高于阈值的图只保留一张避免同一场景的不同帧被反复学习。2.3 批量重命名与统一格式清洗完后我写了一个简单的Python脚本做批量重命名和格式统一。命名规则我采用的是carrot_序号.jpg序号用6位数字补零方便排序和检索。import os import shutil src_dir raw_images dst_dir CarrotDataset/JPEGImages os.makedirs(dst_dir, exist_okTrue) count 1 for filename in sorted(os.listdir(src_dir)): if not filename.lower().endswith((.jpg, .jpeg, .png)): continue ext filename.lower().rsplit(., 1)[-1] new_name fcarrot_{count:06d}.{ext} shutil.copy(os.path.join(src_dir, filename), os.path.join(dst_dir, new_name)) count 1 print(ftotal {count-1} images)这里有个小提醒不要直接用image_1.jpg这种不补零的命名因为排序时image_10会排在image_2前面后面做数据集划分和可视化检查时顺序会很乱。一切用脚本批量处理不要手动改文件名这是数据工程最基本的纪律。3. VOC格式标签全流程制作从LabelImg到XML文件3.1 用LabelImg完成VOC标注实操标注工具我选的是LabelImg它是目前最简单实用的VOC标注工具界面直观支持快捷键操作非常适合快速出活。安装方式很简单pip install labelImg labelImg启动后先点击左侧的Open Dir选择JPEGImages目录再点击Change Save Dir选择Annotations目录然后点击PascalVOC按钮确保保存格式是VOC默认情况下就是这个格式。标注流程是打开一张图后按W键开始画框在胡萝卜目标上拖出矩形框松开鼠标后会弹出类别选择窗口选择carrot类别即可然后按CtrlS保存XMLD键切换到下一张图。如果要修改某个框直接用鼠标点击框的边缘拖动即可按Delete删除选中的框。这里有几个实操经验值得提开启自动保存模式Auto Save mode避免标注大量图片后忘记保存导致前功尽弃。如果一张图里有很多小胡萝卜建议先用鼠标滚轮放大图像到合适比例再画框不要在原尺寸上硬画否则框的位置容易偏移。画框时不要为了省事把两根紧挨着的胡萝卜画成一个框哪怕它们看起来像是重叠在一起。一个框只能包含一个目标这是目标检测标注的铁律。标注完一张图后一定要在右侧列表里确认XML文件已经生成并且大小不为0。我见过有人标完几百张最后发现保存路径设错了全部XML跑到了系统默认目录白白浪费了几个小时。3.2 XML标注文件字段逐项拆解标注完成后打开一个XML文件你会看到如下结构。这个结构是VOC格式的核心我逐项解释一下方便你排查问题annotation folderJPEGImages/folder filenamecarrot_000001.jpg/filename source databaseCarrotDataset/database /source size width1920/width height1080/height depth3/depth /size segmented0/segmented object namecarrot/name poseUnspecified/pose truncated0/truncated difficult0/difficult bndbox xmin120/xmin ymin80/ymin xmax640/xmax ymax520/ymax /bndbox /object /annotationfolder和filename分别记录图像所在目录和文件名。source的database字段是自定义数据库名这里写的是CarrotDataset。size里的width、height、depth必须和实际图像完全一致注意这里depth在RGB图像中恒为3但如果你处理的是灰度图depth应该为1。很多训练框架在读取VOC时会对size字段做校验如果width和实际图像宽度不一致训练会直接报错或者静默失败。object节点是标注核心代表一个目标实例。name是类别名这里就是carrot。truncated和difficult分别表示目标是否被截断、是否难以辨认。在自制数据集中我建议统一设置为0因为新手难以把握这两个标记的标准混用反而会造成数据不一致。bndbox里的xmin、ymin、xmax、ymax是检测框的左上角和右下角坐标单位是像素。有一点需要注意某些检测框架在读取VOC框坐标时要求xmin、xmax等必须是整数。如果标注工具在某些情况下生成了浮点数建议在预处理脚本里统一取整避免后续训练报类型错误。3.3 标注质量控制与双重复检标注是纯手工活出错是必然的关键是要有质检流程。我当时的做法是分两级检查。第一级是工具自动检查。写一个脚本遍历所有XML检查filename对应的JPEG文件是否存在、size字段是否与图片尺寸一致、坐标是否越界xmax是否超过width、ymin是否为负数、是否存在空XML。这一步能过滤掉大多数低级错误。第二级是人工抽查。随机抽20%的XML在原图上绘制标注框并可视化输出人工核对每个框是否贴合目标边缘、类别是否正确、是否有漏标和错标。把可视化结果生成为带框的图片后按顺序快速翻看效率很高。我建议可视化脚本一定要写因为只看XML数字是看不出问题的。下面这段代码是用OpenCV绘制标注框并输出到指定目录import cv2 import os import xml.etree.ElementTree as ET annotations_dir CarrotDataset/Annotations images_dir CarrotDataset/JPEGImages output_dir CarrotDataset/visual_check os.makedirs(output_dir, exist_okTrue) for xml_file in sorted(os.listdir(annotations_dir)): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(annotations_dir, xml_file)) root tree.getroot() filename root.find(filename).text img_path os.path.join(images_dir, filename) img cv2.imread(img_path) if img is None: print(fwarning: {filename} not found) continue for obj in root.iter(object): name obj.find(name).text box obj.find(bndbox) xmin int(box.find(xmin).text) ymin int(box.find(ymin).text) xmax int(box.find(xmax).text) ymax int(box.find(ymax).text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 0, 255), 2) cv2.putText(img, name, (xmin, max(ymin - 10, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) out_path os.path.join(output_dir, filename) cv2.imwrite(out_path, img)这套可视化检查在实际使用中帮我发现了十几个问题比如有两张图框选颠倒了左下角vs左上角、有一张图把土里的石块标成了胡萝卜、还有几处重复框选。这些错误光靠肉眼盯XML根本发现不了。4. 数据集划分、校验与VOC转YOLO实战4.1 正确生成ImageSets索引文件VOC格式中ImageSets/Main目录下的train.txt、val.txt、test.txt是训练框架划分数据集的依据。格式很简单每行一个图片文件名不加扩展名。例如carrot_000001 carrot_000002 carrot_000003 ...划分的时候我采用8:1:1的比例也就是训练集3040张、验证集380张、测试集380张。这里有个容易忽略的问题如果同一株胡萝卜被拍摄了多张不同角度的图像这些图像应该被划分到同一个数据子集中防止同一目标同时出现在训练和验证中。我建议在采集时就给图像打上场景编号比如field1_cam2_0001.jpg这样按场景分组划分就不会串了。下面是划分脚本的核心代码我按场景分组后打乱再按比例切分import os import random from collections import defaultdict jpeg_dir CarrotDataset/JPEGImages main_dir CarrotDataset/ImageSets/Main os.makedirs(main_dir, exist_okTrue) images [f.rsplit(., 1)[0] for f in os.listdir(jpeg_dir) if f.endswith(.jpg)] scene_groups defaultdict(list) for img in images: scene img.rsplit(_, 1)[0] # 如 carrot_000001 - carrot scene_groups[scene].append(img) all_scenes list(scene_groups.keys()) random.seed(42) random.shuffle(all_scenes) train_scenes all_scenes[: int(len(all_scenes) * 0.8)] val_scenes all_scenes[int(len(all_scenes) * 0.8): int(len(all_scenes) * 0.9)] test_scenes all_scenes[int(len(all_scenes) * 0.9):] def write_list(filename, scenes): with open(filename, w) as f: for scene in scenes: for img in sorted(scene_groups[scene]): f.write(img \n) write_list(os.path.join(main_dir, train.txt), train_scenes) write_list(os.path.join(main_dir, val.txt), val_scenes) write_list(os.path.join(main_dir, test.txt), test_scenes)随机种子设定为固定值这样每次运行划分结果一致方便复现。如果你的项目对随机性没有特殊要求也可以不加随机种子但个人建议还是加上否则调试时很难说清结果差异是数据变化还是模型变化引起的。4.2 VOC转YOLO归一化坐标换算详解现在主流的目标检测框架尤其是Ultralytics YOLO系列训练时并不直接读取VOC格式而是使用YOLO txt格式。所以VOC转YOLO就是绕不开的一步。YOLO格式每行代表一个目标格式为class_id center_x center_y width height其中class_id从0开始计center_x、center_y是框中心点坐标除以图像宽高得到的归一化值width、height同理。转换的公式很简单center_x (xmin xmax) / 2.0 / width center_y (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height对应的转换脚本如下import os import xml.etree.ElementTree as ET classes [carrot] annotations_dir CarrotDataset/Annotations labels_dir CarrotDataset/labels os.makedirs(labels_dir, exist_okTrue) for xml_file in sorted(os.listdir(annotations_dir)): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(annotations_dir, xml_file)) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) txt_name xml_file.rsplit(., 1)[0] .txt with open(os.path.join(labels_dir, txt_name), w) as f: for obj in root.iter(object): cls_name obj.find(name).text if cls_name not in classes: continue cls_id classes.index(cls_name) 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) center_x (xmin xmax) / 2.0 / img_w center_y (ymin ymax) / 2.0 / img_h box_w (xmax - xmin) / img_w box_h (ymax - ymin) / img_h f.write(f{cls_id} {center_x:.6f} {center_y:.6f} {box_w:.6f} {box_h:.6f}\n) print(VOC to YOLO conversion done)转换后建议抽查几个txt文件确认归一化坐标都在0到1之间。如果出现了大于1或小于0的值说明原始XML坐标越界了需要回到标注阶段修复。另一个常见坑是有些工具生成的XML里bndbox是整数但width和height是字符串转换时会报类型错误所以代码里一定要用float或int显式转换。4.3 用YOLOv8快速验证数据集质量数据集做完后我习惯先用YOLOv8快速跑一轮训练验证数据集可用的同时也检查标注质量是否达标。这一步能暴露很多隐藏问题。我当时的训练命令很简单yolo detect train datacarrot.yaml modelyolov8n.pt epochs100 imgsz640 batch16其中carrot.yaml指向数据集配置path: ./CarrotDataset train: ImageSets/Main/train.txt val: ImageSets/Main/val.txt names: 0: carrot这里有个容易搞错的地方Ultralytics YOLO训练时train和val字段可以直接指向txt文件但它默认要求txt里写的是图片的绝对路径或者相对于path的相对路径。如果你的txt里只写了不带扩展名的文件名可能需要改成写相对路径比如JPEGImages/carrot_000001.jpg。所以上面生成的train.txt要稍微改造一下把JPEGImages/前缀和.jpg后缀补上。训练完成后重点看两个东西一是训练集loss和验证集loss曲线是否同步下降如果验证集loss明显高于训练集说明标注噪声大或过拟合严重二是验证集的PR曲线看carrot类别的AP值是否能达到合理水平。如果AP很低优先怀疑标注问题而不是模型问题。5. 标注过程高频问题与质量提升实录5.1 高频问题排查速查表整个流程走下来我整理了下面这份问题速查表覆盖了我和身边同事最常见的坑你可以直接对照排查现象可能原因解决方案训练时提示XML尺寸不匹配XML中size.width与实际图像宽度不一致写脚本遍历所有XML根据实际图像尺寸修正size字段转换YOLO格式后坐标出现负值或大于1标注框坐标越界检查原始XML重点排查边界附近的框手动修正图像和标注数量不一致标注过程中跳过或漏标了图像用集合差集脚本找出缺少XML的图片补充标注loss下降但mAP很低标注框边缘不贴合目标加强人工复检可视化绘制检查验证集效果比训练集差很多数据划分时按单张图随机分割导致数据泄露改为按场景或时间序列分组划分模型把石块误检成胡萝卜标注时混入了部分被土壤覆盖的误标样本明确标注口径只标根茎清晰可见的目标统一标准5.2 标注口径统一是质量的最大杠杆我观察到一个普遍现象同样的数据集不同人标注出的检测框风格差异非常大。有人习惯把框画得紧凑正好包住目标有人习惯留一点余量把目标连同阴影一起框进去。这在单类别检测时看起来似乎影响不大但实际训练中会让模型对目标边界的理解变得模糊最终导致检测框不稳定。所以我在标注之前会先定三条规则写在一个标记文件里所有参与标注的人必须遵守检测框必须紧贴目标可见边缘不包含背景胡萝卜被遮挡超过50%时标注为difficult1或直接跳过不标多个目标紧挨时每个目标独立用一个框禁止合并。这个规则听起来简单但实际操作中特别容易违反。尤其是田间场景中胡萝卜常常一半埋在土里只露出10厘米左右标注员会下意识把整个土堆轮廓带进去导致框变得很大其实里面大部分是背景。后来我们在标注规范里加了一条只框露出的根茎主体不包含土堆阴影。这之后数据质量明显提升。5.3 一个提高标注效率的小技巧最后分享一个提升标注效率的技巧先用一个预训练模型做预标注再人工修正。当时我用YOLOv8在已有数据上训练了一个初版模型然后用它对未标注图像做推理生成粗标注框再在LabelImg中打开这些粗标注框进行人工调整。这一个步骤至少节省了30%的标注时间尤其是对于胡萝卜这种长条状目标模型给出的框往往已经很接近人工只需微调边缘即可。具体操作上我会把YOLO预测结果先转成VOC格式XML放到Annotations目录然后在LabelImg中逐个检查。检查时按A和D切换图像对明显错误的框直接删除或重画。这个方法省时省力而且因为人工修正时注意力集中在检查而非从零画框标注质量反而更高。我自己做完这套胡萝卜检测数据集最深的体会是数据集的质量上限在采集下限在标注而格式只是连接两者的桥梁。VOC格式看似古老但它结构简单、工具支持广泛、易于转换作为中间格式是极其稳妥的。如果你也正在做农业场景的目标检测数据集建议先把标注口径、目录结构、命名规范这些基础动作做扎实再谈模型调优。数据集做扎实了后面的训练和部署都会顺畅很多。本文还有配套的精品资源点击获取
返回列表