ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VisDrone农机检测实战:YOLO格式转换与YOLOv8训练全流程

VisDrone农机检测实战:YOLO格式转换与YOLOv8训练全流程 简介这份资源是面向无人机俯视视角的农场场景目标检测数据集适合从事农业智能化、无人机巡检及目标检测算法实践的开发者与研究者使用可解决农场中车辆、农机与行人识别任务缺乏高质量标注数据的问题。数据集包含1000余张图像压缩包共2000个文件其中497张jpg为无人机航拍原图1502个txt为对应的YOLO格式标注文件另有1个yaml配置文件整体约522.36MB。目录已按train、val、test划分完毕data.yaml中定义了car、people、tractor、van四类目标yolov5、yolov7、yolov8等主流框架可直接加载训练省去格式转换与划分工作。目前已有451人学习下载配套博文提供了数据集说明与检测结果参考便于读者快速验证模型效果并复现实验流程适合作为农业场景目标检测的入门与进阶训练素材。1. 农场里的农机检测VisDrone 数据集为什么值得你花一个周末跑通去年帮一个做智慧农业的朋友看模型他拿 COCO 预训练的 YOLO 直接去数拖拉机结果把收割机认成卡车、把旋耕机认成摩托mAP 掉到 0.2 出头。问题不在模型在数据——通用数据集里农业机械的样本少得可怜类别定义也对不上。VisDrone 原本是无人机视角的视觉目标检测数据集里面包含大量俯拍场景下的车辆、行人、农机等目标其中农业机械这一类在农田、果园、牧场场景下有相当数量的标注。这个标题指向的就是把 VisDrone 里跟农业机械相关的子集抽出来整理成 YOLO 格式直接用于训练自己的农机检测模型。适合谁做农业无人机巡检、农机自动驾驶、果园管理机器人的一线开发者以及想找一个真实场景数据集练手 YOLO 全流程的工程师。你不需要从零标注几千张图但需要理解这个数据集的格式、类别映射和转换逻辑否则拿到手也是一堆看不懂的 txt。2. VisDrone 原始标注拆解与 YOLO 格式对齐2.1 VisDrone 的标注文件长什么样VisDrone 数据集常见发布形式是每张图片对应一个 txt 标注文件文件名与图片同名。标注内容按行组织每行 8 个字段用逗号分隔。字段顺序依次是bbox_left、bbox_top、bbox_width、bbox_height、score、object_category、truncation、occlusion。其中 score 表示标注置信度1 表示有效object_category 是类别编号truncation 和 occlusion 分别表示截断程度和遮挡程度取值 0 或 1。类别编号在 VisDrone 官方定义中通常有 10 类左右包括 ignored regions、pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor、others。农业机械并没有单独一个类而是分散在 truck、van、others 等类别里。这意味着你不能直接拿 VisDrone 的类别定义去训练农机检测必须做一步类别筛选和重映射。我一般会先写一个统计脚本看看每个类别在标注里出现多少次再决定哪些类别归入农机。比如 truck 里可能包含拖拉机、收割机van 里可能包含农用运输车others 里可能包含旋耕机、播种机等。这一步不做后面训练出来的模型类别就是乱的。2.2 从 VisDrone 格式转 YOLO 格式的完整脚本YOLO 格式要求每张图片对应一个 txt每行 5 个字段class_id、x_center、y_center、width、height。所有坐标都要归一化到 0 到 1 之间。转换的核心逻辑是读取 VisDrone 标注行过滤掉不需要的类别把 bbox 的左上角坐标和宽高转成中心点坐标和宽高再除以图片宽高做归一化。下面是我常用的转换脚本依赖 Pillow 和 tqdm直接放在数据集根目录跑就行。import os from PIL import Image from tqdm import tqdm # VisDrone 类别编号到名称的映射按你实际拿到的数据集版本调整 VISDRONE_CATEGORIES { 0: ignored, 1: pedestrian, 2: people, 3: bicycle, 4: car, 5: van, 6: truck, 7: tricycle, 8: awning-tricycle, 9: bus, 10: motor, 11: others } # 你希望保留并映射到 YOLO 的类别这里只保留农机相关 # 键是 VisDrone 类别编号值是 YOLO 类别编号 CATEGORY_MAP { 5: 0, # van - agricultural_machine 6: 0, # truck - agricultural_machine 11: 0, # others - agricultural_machine } # YOLO 类别名称顺序与 CATEGORY_MAP 的值对应 YOLO_NAMES [agricultural_machine] def convert_annotation(anno_path, img_path, output_path): # 读取图片尺寸用于归一化 with Image.open(img_path) as im: img_w, img_h im.size lines_out [] with open(anno_path, r) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue # VisDrone 字段顺序left, top, width, height, score, category, truncation, occlusion left, top, w, h map(float, parts[:4]) score int(parts[4]) category int(parts[5]) # 跳过无效标注和不需要的类别 if score 0 or category not in CATEGORY_MAP: continue # 跳过宽高为 0 的异常框 if w 0 or h 0: continue # 转成中心点坐标并归一化 x_center (left w / 2.0) / img_w y_center (top h / 2.0) / img_h norm_w w / img_w norm_h h / img_h # 裁剪到 0-1 范围防止越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) norm_w min(max(norm_w, 0.0), 1.0) norm_h min(max(norm_h, 0.0), 1.0) class_id CATEGORY_MAP[category] lines_out.append(f{class_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}) # 即使没有目标也要生成空文件YOLO 训练时要求图片和标签一一对应 with open(output_path, w) as f: f.write(\n.join(lines_out)) def main(): # 假设目录结构dataset/images/ 和 dataset/annotations/ img_dir dataset/images anno_dir dataset/annotations label_dir dataset/labels os.makedirs(label_dir, exist_okTrue) img_files [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))] for img_file in tqdm(img_files, descConverting): base os.path.splitext(img_file)[0] img_path os.path.join(img_dir, img_file) anno_path os.path.join(anno_dir, base .txt) out_path os.path.join(label_dir, base .txt) if not os.path.exists(anno_path): # 没有标注文件的图片生成空标签 open(out_path, w).close() continue convert_annotation(anno_path, img_path, out_path) # 生成 classes.txt方便后续训练和可视化 with open(os.path.join(label_dir, classes.txt), w) as f: f.write(\n.join(YOLO_NAMES)) if __name__ __main__: main()逻辑说明脚本先读取图片真实宽高因为 VisDrone 的 bbox 是绝对像素坐标必须除以图片尺寸才能归一化。CATEGORY_MAP 是核心你把哪些 VisDrone 类别归入农机就改这个字典。score 为 0 的行直接跳过那是无效标注。宽高为 0 的框也要跳过否则归一化后会出现 NaN。最后生成空标签文件是必须的YOLO 训练时如果某张图没有对应标签文件会被当成负样本处理但空文件表示这张图确实没有目标两者语义不同。参数说明VISDRONE_CATEGORIES 只用于打印和调试不影响转换结果。CATEGORY_MAP 的键是 VisDrone 原始类别编号值是你自定义的 YOLO 类别编号从 0 开始连续编号。YOLO_NAMES 的顺序必须与 CATEGORY_MAP 的值一一对应。归一化后的坐标保留 6 位小数足够YOLO 官方实现也是这个精度。2.3 转换后必须做的三项校验转换脚本跑完不代表数据就能用。我每次都会做三项校验少一项都可能在后头翻车。第一项检查标签文件数量是否与图片数量一致。如果 labels 目录下的 txt 数量少于 images 目录下的图片数量说明有图片没生成标签训练时会报错。用一条 shell 命令就能查echo images: $(ls dataset/images | wc -l) echo labels: $(ls dataset/labels/*.txt | wc -l)第二项随机抽 10 张图做可视化把 YOLO 格式的框画回原图看位置和类别对不对。我一般用 Python 的 PIL 画框不依赖 matplotlib速度快。from PIL import Image, ImageDraw import os, random img_dir dataset/images label_dir dataset/labels names open(os.path.join(label_dir, classes.txt)).read().strip().split(\n) for img_file in random.sample(os.listdir(img_dir), 10): base os.path.splitext(img_file)[0] img Image.open(os.path.join(img_dir, img_file)).convert(RGB) draw ImageDraw.Draw(img) w, h img.size label_path os.path.join(label_dir, base .txt) if os.path.exists(label_path): for line in open(label_path): parts line.strip().split() if len(parts) ! 5: continue cid, xc, yc, nw, nh int(parts[0]), *map(float, parts[1:]) x1 (xc - nw / 2) * w y1 (yc - nh / 2) * h x2 (xc nw / 2) * w y2 (yc nh / 2) * h draw.rectangle([x1, y1, x2, y2], outlinered, width2) draw.text((x1, y1), names[cid], fillred) img.save(fcheck_{base}.jpg)第三项统计每个类别的框数量确认没有类别为空。如果 agricultural_machine 的框数为 0说明 CATEGORY_MAP 配错了或者这个子集里根本没有农机标注。这时候要么换子集要么重新审视类别映射。3. 用 YOLOv8 在 VisDrone 农机子集上跑通训练3.1 环境配置与数据目录组织YOLOv8 的环境配置现在比两年前省事多了但 Anaconda 环境下还是有几个版本坑。我一般用 Python 3.9 到 3.10PyTorch 选 2.0 以上CUDA 版本跟显卡驱动匹配就行。安装命令就一行pip install ultralyticsUltralytics 包会自动拉取匹配的 PyTorch 版本但如果你之前装过旧版 torch建议先卸载再装否则容易出现 CUDA 版本冲突。装完后跑yolo checks看一眼环境确认 CUDA 可用。数据目录按 YOLO 官方推荐的结构组织dataset/ images/ train/ val/ labels/ train/ val/ data.yamltrain 和 val 的划分比例我一般用 8:2如果图片总数少于 500 张用 7:3验证集大一点更能反映真实泛化能力。划分脚本很简单用 sklearn 的 train_test_split 或者自己写随机抽样都行。注意图片和标签要同步移动不能只移图片。data.yaml 的内容如下path: /absolute/path/to/dataset train: images/train val: images/val nc: 1 names: [agricultural_machine]path 必须写绝对路径YOLOv8 对相对路径的处理有时候会出玄学问题。nc 是类别数names 的顺序必须与转换脚本里的 YOLO_NAMES 一致。3.2 训练命令与关键参数怎么设训练命令本身不复杂但参数设不对结果差很多。我常用的起手命令yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience20 \ device0 \ projectruns/visdrone_agri \ nameexp1model 选 yolov8s.pt 还是 yolov8n.pt取决于你的部署端算力。如果最终要跑在 RK3588 或者 Atlas 这类边缘设备上建议从 yolov8n.pt 开始精度不够再往上换。imgsz 设 640 是通用起点VisDrone 图片分辨率普遍偏高如果农机目标在图中占比很小可以尝试 1280但显存占用会翻倍。batch 根据显存调16 是 8GB 显存的保守值。lr0 初始学习率 0.01 是 YOLOv8 的默认值如果 loss 震荡厉害降到 0.005。patience 设 20 表示 20 个 epoch 没有提升就早停省时间。训练过程中重点看三个指标box_loss、cls_loss、mAP50。box_loss 下降说明框位置在收敛cls_loss 下降说明类别判断在改善。如果 box_loss 一直不降检查标签格式是不是有越界坐标。如果 cls_loss 降不下去可能是类别映射有问题或者某些类别样本太少。3.3 训练完怎么验证模型真的学到了农机训练结束后runs 目录下会生成权重文件和验证结果。不要只看 mAP 数字一定要做可视化推理。用训练好的 best.pt 跑一批验证集图片yolo detect predict \ modelruns/visdrone_agri/exp1/weights/best.pt \ sourcedataset/images/val \ conf0.25 \ saveTrue \ projectruns/visdrone_agri/predictconf 设 0.25 是起点如果发现漏检多降到 0.15 试试如果误检多升到 0.4。VisDrone 的俯拍视角下农机目标有时候会被树冠或建筑遮挡模型容易漏检。这时候可以看预测结果里哪些图漏了单独拿出来分析是标注问题还是模型能力问题。我还会做一件事把验证集里所有包含农机的图片单独抽出来算一个子集的 mAP。因为整体 mAP 可能被大量简单样本拉高掩盖了农机类别的真实表现。如果农机子集 mAP 明显低于整体说明模型对农机特征学习不够需要增加农机样本或做数据增强。4. 农机检测训练中的避坑与排查记录4.1 标签越界导致训练直接报错现象训练启动后几秒钟就报错提示坐标超出 0 到 1 范围。原因VisDrone 原始标注里有些框的左上角坐标加宽高超过了图片边界转换时没有做裁剪。解决在转换脚本里加一步裁剪把 x_center、y_center、width、height 都限制在 0 到 1 之间。更稳妥的做法是先算 x1、y1、x2、y2裁剪到图片范围内再反算中心点和宽高。我上面给的脚本已经做了简单裁剪但如果原始框完全在图片外裁剪后宽高会变成 0需要跳过。4.2 类别映射错位导致模型学出四不像现象训练 loss 正常下降但推理时把所有农机都标成同一个类或者把背景标成农机。原因CATEGORY_MAP 的键值对写反了或者 YOLO_NAMES 的顺序与 CATEGORY_MAP 的值不对应。解决转换完先跑一遍统计脚本打印每个 YOLO 类别对应的框数量确认没有类别为空、没有类别数量异常偏多。另外classes.txt 里的名称顺序必须与 data.yaml 里的 names 完全一致大小写和空格都要对齐。4.3 验证集 mAP 虚高但实际部署误检严重现象验证集 mAP50 到 0.85但拿真实农田视频跑误检率很高把石头、草垛都认成农机。原因VisDrone 的验证集和训练集来自同一分布图片风格、光照、角度都相似模型过拟合了。解决从真实场景里抽 50 到 100 张图手工标注后作为独立测试集不参与训练。如果测试集 mAP 掉到 0.5 以下说明泛化能力不够需要加数据增强比如随机裁剪、色彩抖动、马赛克增强。YOLOv8 默认开启了马赛克增强但你可以调 mosaic 参数控制强度。4.4 小目标漏检严重现象大农机检测没问题但远处的小农机基本漏检。原因VisDrone 俯拍视角下远处农机可能只有几十个像素YOLO 的下采样倍率导致小目标特征丢失。解决把 imgsz 从 640 提到 1280或者用 YOLOv8 的 P2 检测头。P2 检测头会增加计算量但小目标召回率提升明显。另一个办法是切片推理把大图切成小块分别检测再合并适合离线批量处理场景。4.5 训练到一半 loss 突然变成 NaN现象前几十个 epoch 正常突然 loss 变成 NaN训练中断。原因学习率过高导致梯度爆炸或者某批数据里有异常标签。解决先把 lr0 降到 0.001 重跑如果还出 NaN检查标签文件里有没有空行、有没有非数字字符。VisDrone 转换后的标签文件如果某行只有 4 个字段YOLO 解析时会出错。用脚本扫一遍所有标签文件确保每行要么为空要么正好 5 个字段。5. 把农机检测模型推到边缘设备前的最后几步训练出一个能跑的模型只是开始真正落地到农场场景还有几件事必须做。第一件是模型导出。YOLOv8 支持导出 ONNX、TensorRT、OpenVINO 等格式如果你要部署到 RK3588一般走 ONNX 再转 RKNN。导出命令yolo export modelruns/visdrone_agri/exp1/weights/best.pt formatonnx opset12 simplifyTrueopset 选 12 是兼容性比较好的版本simplify 会做一层图优化减小模型体积。导出后先用 onnxruntime 跑一遍推理确认输出与 PyTorch 一致再往边缘设备上转。第二件是置信度门限的调整。YOLO 默认 conf 是 0.25但在农田场景下光照变化大、背景杂乱我一般会把部署时的 conf 提到 0.4 到 0.5牺牲一点召回率换准确率。如果业务允许漏检但不能误检就设高如果宁可误检也不能漏就设低。这个门限没有标准答案拿一批真实场景图跑一遍画 precision-recall 曲线选 F1 最高的点。第三件是输入尺寸的权衡。边缘设备算力有限1280 的输入可能跑不到实时。我一般会做一次 benchmark测 640、960、1280 三档的推理耗时和 mAP选一个满足帧率要求且精度可接受的尺寸。RK3588 上跑 YOLOv8n 加 640 输入一般能做到 30 FPS 以上够用了。最后说一个我自己的习惯每次训练完把 data.yaml、转换脚本、训练命令、验证结果截图全部归档到一个文件夹命名带上日期和数据集版本。农机检测这个方向数据集会不断更新模型也会迭代没有归档三个月后你根本记不清当时是怎么跑的。这个习惯帮我省过很多次后悔药。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进