YOLOv8实战VisDrone:无人机小目标检测完整流程与踩坑记录

发布时间:2026/10/18 18:54:41
YOLOv8实战VisDrone:无人机小目标检测完整流程与踩坑记录 1. 项目概述VisDrone 这个数据集做过无人机视角目标检测的应该都不陌生。它是目前无人机航拍场景下最常用的公开数据集之一涵盖了城市、乡村、公路、操场等多种俯视场景里面的目标基本都是小目标密集程度还特别高。这次做的项目就是在 YOLOv8 上把 VisDrone 的 baseline 完整跑通一遍从 VisDrone 原始标注怎么转成 YOLO 格式、环境怎么配、模型怎么选、训练参数怎么调到最后验证集指标怎么看、检测效果怎么可视化整套流程全部捋一遍。适合刚接触目标检测、想在无人机航拍数据上动手跑模型的新手也适合已经跑过 COCO 或 VOC 数据集、想切换到 VisDrone 这类小目标密集场景的开发者参考。先说一下为什么选 YOLOv8 而不是别的模型。YOLOv8 是当前 YOLO 系列里集成度最高、使用最方便的一个版本ultralytics库把数据加载、增强、训练、验证、导出全流程都封装好了一个命令行就能跑起来。VisDrone 的标注格式比较特殊是四角坐标和 COCO 的x1,y1,w,h、YOLO 的cx,cy,w,h都不一样所以数据集格式转换是绕不开的第一步。而 YOLOv8 对自定义数据集的接入方式非常友好只要准备好images和labels目录再写一个.yaml配置文件就能直接开训。这个 baseline 的意义在于先建立一个可复现的参考基准后续不管你是要改模型结构、做数据增强、做切图推理还是换更复杂的检测头都要拿这个 baseline 来对比效果。所以这次我会把每一步的细节都记录下来包括踩过的坑。2. VisDrone 数据集深度拆解2.1 VisDrone 的格式到底长什么样VisDrone2019-DET 数据集解压出来之后目录结构大致是这样的VisDrone2019-DET/ ├── train/ ├── val/ ├── test-dev/ └── annotations/每个文件夹下面又有images和annotations两个子目录。图片都是jpg格式分辨率不固定常见的有2000x1500、1360x765之类的基本都是高清大图。标注文件是txt格式每一行对应一个目标但它的字段不是 YOLO 那种class_id cx cy w h而是:bbox_left, bbox_top, bbox_width, bbox_height, score, class_id, truncation, occlusion八个字段用逗号分隔。这里面有几个关键差异要特别注意bbox_left, bbox_top, bbox_width, bbox_height是边界框的左上角坐标加上宽高单位是像素不是归一化值。score在训练集中恒为1在test-dev中可能是别的值表示置信度。truncation表示目标是否被截断0表示完整1表示部分截断。occlusion表示遮挡状态0无遮挡1部分遮挡2严重遮挡。还有一个特殊情况某些类别为0的标注行表示ignored区域这类区域在训练时应当跳过否则会引入大量噪声。类别一共有 10 类索引从 0 开始索引类别名常见场景0ignored忽略区域不参与训练1pedestrian行人2people人群多人聚集3bicycle自行车4car小汽车5van面包车/厢式货车6truck卡车7tricycle三轮车8awning-tricycle带棚三轮车9bus公交车10motor摩托车这个类别设计本身就很有意思把pedestrian和people分开把tricycle和awning-tricycle分开这些都是无人机俯视场景下常见的细粒度目标区分。但在做 baseline 的时候我建议先把0号类别ignored过滤掉后面几个细分类别可以保留因为它们正好是区分模型能力的关键点。2.2 为什么 VisDrone 比 COCO 更难搞做过 COCO 检测的人上手 VisDrone第一感觉就是怎么这么小。VisDrone 里大量目标的像素尺寸在20x20甚至10x10以下属于典型的小目标。这类目标在 YOLOv8 的 P3 特征层下采样 8 倍上可能只占几个像素点特征信息严重不足。另外VisDrone 的密集程度非常高一辆公交车上可能同时出现十几个行人道路上各种车辆互相遮挡标注框大量重叠。这个时候 NMS 的处理非常关键YOLOv8 默认的 NMS 参数在 COCO 上表现很好但到了 VisDrone 这种密集场景iou阈值可能需要适当调整否则很容易把相邻目标合并掉。还有一个容易被忽略的点VisDrone 是俯视视角目标的角度、形体和自然场景图片差异非常大。模型的预训练权重如果是 COCO 上来的迁移到 VisDrone 上需要一个不小的适应过程这也是为什么跑 baseline 时不能一上来就期望 mAP 跟 COCO 一样高。2.3 数据集统计与类别分布我在转换完成后对训练集做了简单的统计总共 6471 张训练图片371 个验证图片。类别分布很不均衡pedestrian和car数量最多各占 30% 左右。people、van、truck中等数量。awning-tricycle、bus数量很少有些类别在验证集中甚至凑不够一个 batch 的正样本。这个不均衡直接影响训练效果。YOLOv8 默认的损失函数对每个类别是等权重的在小类别上很容易欠拟合。后面训练的时候需要关注这一点可以考虑调整类别权重或者用数据增强策略去平衡。3. 环境准备与工具选型3.1 硬件与软件环境这次跑的机器配置GPU 是单张 24GB 显存的显卡。但这并不意味着没有大显存就跑不了yolov8n在imgsz640、batch16的情况下大概只需要 6-8GB 显存普通消费级显卡完全能跑。软件环境我用的版本组合Python 3.10 PyTorch 2.0.1cu118 ultralytics 8.0.xx CUDA 11.8ultralytics库可以直接用 pip 安装它会自动把 YOLOv8 所需的核心依赖一起装好。不建议手动去源码编译除非你要改底层代码否则用官方包就够了。3.2 目录结构规划在开始转换之前我建议先规划好整体的目录结构避免后面手忙脚乱visdrone_yolov8/ ├── datasets/ │ ├── VisDrone2019-DET/ │ │ ├── train/ │ │ ├── val/ │ │ └── test-dev/ ├── visdrone.yaml ├── convert_visdrone.py ├── runs/ ├── weights/这样把数据集、代码、训练产物切分开训练的时候日志、权重输出到runs/下不会把根目录搞乱。4. VisDrone 标注转 YOLO 格式的完整过程4.1 转换脚本的编写思路VisDrone 标注转 YOLO 格式核心公式是center_x bbox_left bbox_width / 2center_y bbox_top bbox_height / 2norm_cx center_x / image_widthnorm_cy center_y / image_heightnorm_w bbox_width / image_widthnorm_h bbox_height / image_heightYOLO 格式要求的是归一化后的cx, cy, w, h都是 0 到 1 之间的浮点数。这里有一个很关键的细节VisDrone 的bbox_left, bbox_top是左上角坐标而 YOLO 的cx, cy是中心点坐标转换时不能直接套用 COCO 转 YOLO 的代码一定要把左上角转换成中心点。4.2 完整转换代码下面是我写的转换脚本支持过滤ignored类别和截断目标import os from pathlib import Path def visdrone_to_yolo(txt_path, img_width, img_height): 将 VisDrone 的标注文件转换为 YOLO 格式 yolo_lines [] with open(txt_path, r) as f: lines f.readlines() for line in lines: line line.strip() if not line: continue parts line.split(,) if len(parts) 6: continue bbox_left float(parts[0]) bbox_top float(parts[1]) bbox_width float(parts[2]) bbox_height float(parts[3]) score float(parts[4]) class_id int(parts[5]) # 过滤 ignored 类别class_id 0 if class_id 0: continue # 过滤无效标注 if bbox_width 0 or bbox_height 0: continue # 过滤 truncated 或严重遮挡的目标 # 如果你觉得 occlusion 信息有价值也可以保留 # 这里我们只保留 score 0 的 if score 0: continue # VisDrone 的 class_id 1-10 转 YOLO 的 0-9 # 因为忽略了 class_id 0所以这里要减 1 yolo_class_id class_id - 1 center_x bbox_left bbox_width / 2.0 center_y bbox_top bbox_height / 2.0 norm_cx center_x / img_width norm_cy center_y / img_height norm_w bbox_width / img_width norm_h bbox_height / img_height # 防止边界超出 0-1 范围 norm_cx min(max(norm_cx, 0), 1) norm_cy min(max(norm_cy, 0), 1) norm_w min(max(norm_w, 0), 1) norm_h min(max(norm_h, 0), 1) yolo_lines.append(f{yolo_class_id} {norm_cx:.6f} {norm_cy:.6f} {norm_w:.6f} {norm_h:.6f}) return yolo_lines def convert_folder(image_dir, annotation_dir, output_label_dir): 遍历一个文件夹下的所有图片转换标注 os.makedirs(output_label_dir, exist_okTrue) image_files list(Path(image_dir).glob(*.jpg)) list(Path(image_dir).glob(*.png)) for img_path in image_files: # 读取图片尺寸 import cv2 img cv2.imread(str(img_path)) if img is None: print(fWarning: Failed to read image {img_path}) continue img_height, img_width img.shape[:2] # 对应的标注文件 ann_path Path(annotation_dir) / (img_path.stem .txt) if not ann_path.exists(): print(fWarning: Annotation not found for {img_path}) continue yolo_lines visdrone_to_yolo(ann_path, img_width, img_height) # 写入 YOLO 格式 label 文件 label_path Path(output_label_dir) / (img_path.stem .txt) with open(label_path, w) as f: for line in yolo_lines: f.write(line \n) if len(yolo_lines) 0: # 没有目标也保留空文件YOLO 会跳过 print(fInfo: No valid objects in {img_path}) if __name__ __main__: base_dir datasets/VisDrone2019-DET # 转换训练集 convert_folder( os.path.join(base_dir, train/images), os.path.join(base_dir, train/annotations), os.path.join(base_dir, train/labels) ) # 转换验证集 convert_folder( os.path.join(base_dir, val/images), os.path.join(base_dir, val/annotations), os.path.join(base_dir, val/labels) )4.3 转换后的目录结构转换完成后train/和val/目录下分别会有images和labels两个子目录train/ ├── images/ │ ├── 0000001_00001_d_0000001.jpg │ ├── ... └── labels/ ├── 0000001_00001_d_0000001.txt ├── ...这里要注意VisDrone 的标注文件命名和图片文件命名是一一对应的都是类似0000001_00001_d_0000001这种长文件名。转换脚本是按照图片文件名去匹配标注文件所以不会出错。4.4 转换时最容易踩的坑第一个坑是class_id偏移。VisDrone 的类别索引 0 是ignored而 YOLO 的类别索引从 0 开始。因为过滤了ignored所以转换后 YOLO 的类别 0 对应 VisDrone 的类别 1pedestrian依此类推最后 YOLO 的类别 9 对应 VisDrone 的类别 10motor。visdrone.yaml文件里的类别列表必须和这个顺序一致否则训练出来的模型类别全乱了。第二个坑是坐标系误差。VisDrone 标注是整数像素值转换后要保留 6 位小数足够精确。但有些标注框的bbox_left bbox_width可能超出图片宽度这时候归一化后的值会大于 1必须做裁剪否则 YOLO 训练时会出问题。第三个坑是空标注。有些图片上可能没有任何有效目标转换后会产生空的 txt 文件。YOLO 训练时遇到空标注会直接跳过该图片不影响训练但如果你用的是随机数据增强空标注图片可能出现概率不高影响不大。5. 配置 visdrone.yaml 与数据校验5.1 编写数据配置文件YOLOv8 训练自定义数据集需要一个.yaml文件# visdrone.yaml path: datasets/VisDrone2019-DET # 数据集根目录 train: train/images # 训练图片相对路径 val: val/images # 验证图片相对路径 nc: 10 # 类别数量 names: 0: pedestrian 1: people 2: bicycle 3: car 4: van 5: truck 6: tricycle 7: awning-tricycle 8: bus 9: motor注意path最好用绝对路径或者相对于你运行训练命令的目录的相对路径。train和val指向的是图片目录YOLOv8 会自动去同名目录下找labels。也就是说如果你指定train: train/images那么它自动去train/labels找标注。所以图片和标注目录必须挨着且名字匹配。5.2 可视化标注检查转换完直接开训是有点风险的万一坐标转换算错了训练出来的模型就是垃圾。我建议先做一个可视化检查把转换后的标注画回图片上看一眼。import cv2 import random from pathlib import Path # 随机选几张图检查 img_dir Path(datasets/VisDrone2019-DET/train/images) label_dir Path(datasets/VisDrone2019-DET/train/labels) class_names [pedestrian, people, bicycle, car, van, truck, tricycle, awning-tricycle, bus, motor] for img_path in random.sample(list(img_dir.glob(*.jpg)), 5): img cv2.imread(str(img_path)) label_path label_dir / (img_path.stem .txt) if not label_path.exists(): continue with open(label_path, r) as f: lines f.readlines() h, w img.shape[:2] for line in lines: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) cx float(parts[1]) * w cy float(parts[2]) * h bw float(parts[3]) * w bh float(parts[4]) * h x1 int(cx - bw / 2) y1 int(cy - bh / 2) x2 int(cx bw / 2) y2 int(cy bh / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, class_names[cls_id], (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(fcheck_{img_path.stem}.jpg, img)这一步非常重要。我见过太多人跳过可视化直接开训结果发现标注框整体偏移或者类别错乱白白浪费几小时的训练时间。6. 模型选型与训练参数配置6.1 YOLOv8 模型选型YOLOv8 官方提供了几个预训练模型yolov8n、yolov8s、yolov8m、yolov8l、yolov8x。它们的参数量和速度递增精度也递增。我这次选了yolov8s作为 baseline。理由很简单yolov8n速度最快但 VisDrone 的小目标多模型容量太小容易欠拟合。yolov8m精度更高但训练时间和显存消耗都会上升对于验证 baseline 来说有点重。用yolov8s可以在一个晚上跑完 300 个 epoch 的完整训练同时精度够用。当然如果你的显卡显存比较小用yolov8n也是可以的只是后续对比提升空间时可能显得 baseline 太低。6.2 训练命令与关键参数YOLOv8 的训练命令非常简洁yolo train datavisdrone.yaml modelyolov8s.pt epochs300 imgsz640 batch16 device0 workers8 patience20逐项解释一下这些参数modelyolov8s.pt表示加载 COCO 预训练权重作为起点这样可以利用 COCO 上学到的通用特征比随机初始化收敛快得多。如果不想用预训练权重可以传yolov8s.yaml但我不推荐因为 VisDrone 的数据量6000 张不足以从头训练一个大模型。imgsz640是训练输入尺寸。VisDrone 原始图片是 2000x1500直接缩放成 640 会导致小目标更小。但 YOLOv8 在训练时做了自适应缩放和 Mosaic 增强中等尺寸下对小目标的稳定性更好。后续想提升精度可以试试imgsz1280但显存和训练时间都会翻倍。batch16取决于显存。24GB 显存下yolov8simgsz640batch16没问题。如果显存不够优先减 batch其次是 imgsz。patience20是早停的耐心值。如果验证集 mAP 连续 20 个 epoch 没有提升训练会提前终止防止过拟合和浪费时间。workers8是数据加载的进程数。数值越高GPU 等待时间越少但不要超过 CPU 核心数。6.3 训练超参数背后的逻辑有人可能会问为什么epochs300这么多VisDrone 数据集只有 6471 张图理论上 100 个 epoch 也能收敛但小目标检测需要更多次迭代来稳定学习边界框回归。我实测中前 50 个 epoch 的 mAP 上升很快但从 100 到 200 个 epoch 之间提升非常缓慢如果没有耐心等用patience提前停掉也行。还有一个容易忽略的参数是mosaic。YOLOv8 默认在训练前 10 个 epoch 关闭 Mosaic防止早期语义混乱。但如果你的数据集中小目标很多Mosaic 对提升小目标检测帮助很大因为它把 4 张图拼在一起让模型在更小的尺度上学习目标特征。我建议保持默认的mosaic1.0不要关。关于数据增强VisDrone 场景的增强策略和 COCO 不太一样。俯视视角下目标不会倒过来垂直翻转增强意义不大。fliplr0.5可以保留因为无人机影像左右对称是合理的。hsv_h、hsv_s、hsv_v这些颜色增强可以稍微调大一点因为无人机在不同光照条件下拍摄的颜色差异很大。6.4 实际训练过程观察训练启动后终端会打印每个 epoch 的 loss 和 mAP 信息。我习惯重点观察以下指标box_loss边界框回归损失下降平稳说明框定位在逐步优化。cls_loss分类损失如果这个值一直降不下去可能是类别不均衡导致的。dfl_loss分布焦点损失这是 YOLOv8 特有的跟边界框的分布建模有关。第一个 epoch 结束后box_loss大概在 2-3 左右cls_loss在 1-2 左右。到第 50 个 epochbox_loss会降到 1 以下cls_loss会降到 0.3-0.5 左右。如果这些 loss 在后期出现明显反弹大概率是学习率过大或者过拟合了可以适当调低lr0。7. 验证集评估与预测结果分析7.1 评估指标怎么看训练结束后yolo会自动在验证集上做评估输出 mAP50、mAP50-95 等指标。对于 VisDrone baselinemAP50能到 0.35-0.45 左右就算正常mAP50-950.2 左右也不意外。不要拿 COCO 的指标来比VisDrone 的小目标难度本身就比 COCO 高一个量级。如果想单独在验证集上重新评估可以运行yolo val modelruns/detect/train/weights/best.pt datavisdrone.yaml它会输出每个类别的 AP方便你分析模型在哪个类别上表现最差。按照我的经验awning-tricycle和bus这两个类别的 AP 通常最低因为它们样本量太小。如果你发现pedestrian的 AP 也不高那大概率是小目标的问题可以考虑后续做切图或提高输入分辨率。7.2 预测可视化光看数字不够直观最好把预测结果画到图片上yolo predict modelruns/detect/train/weights/best.pt sourcedatasets/VisDrone2019-DET/val/images saveTrue conf0.25conf0.25表示只保留置信度大于 0.25 的框。实际应用中 VisDrone 密集场景下这个值可以适当调低到 0.15 或 0.2因为小目标本身的置信度就偏低设太高会漏检严重。查看预测图片时重点关注这几类问题远处的行人是不是漏检了密集人群中是不是出现重复框大卡车是不是被拆成了多个框类别是否混淆比如truck被识别成van这些问题会直接引导你后续的优化方向。7.3 常见问题与排查技巧训练和评估过程中可能会遇到的一些典型问题问题现象可能原因排查方法loss 不下降学习率过高或过低、数据集路径错误检查 loss 曲线降低或升高 lr0mAP 为 0类别编号错乱、标签为空可视化检查标注显存不足batch 太大或 imgsz 太大降低 batch 或 imgsz训练时间过长workers 太少、GPU 利用率低增加 workers、检查 nvidia-smi验证集上所有类别 AP 都很低过拟合或预训练权重不匹配增加数据增强、减少 epochs有一个我踩过比较多坑的地方是visdrone.yaml里的names列表顺序必须和转换后的类别索引完全一致。很多人转完标注直接写 yaml结果类别顺序和索引对不上训练结束一评估mAP 低得离谱但看 loss 又是正常下降的。还有一个坑是关于cache参数。YOLOv8 默认会缓存图片到内存或磁盘如果数据集路径有变动缓存可能失效。如果出现OSError或者读取图片失败删掉labels.cache文件重新训练即可。7.4 精度提升的几个方向baseline 跑通之后如果还有余力可以尝试以下几种优化方向提高输入分辨率到imgsz1280。VisDrone 的小目标在高分辨率下特征更清晰mAP50 通常能提升 5-10 个点。使用yolov8m或yolov8l更大的模型对密集场景的表征能力更强。改用SAHISlicing Aided Hyper Inference做切图推理把大图切成小块分别检测再合并对小目标特别友好。使用tune自动搜索超参数但会消耗大量时间。在训练集上补充更多的数据增强策略比如随机缩放、随机平移、翻转等。这些方向我以后会单独展开写baseline 的意义就在于它给了你一个稳定的参照物所有的改进都可以拿它来对比。8. 实操汇总与个人心得最后记录几条训练 VisDrone baseline 时最有价值的笔记。第一数据集转换这一步千万不要图省事。坐标转换公式很简单但class_id偏移、边界裁剪、空标注处理每一项都值得你多花 20 分钟检查。我当时先写了一个带可视化检查的小脚本随机抽了几张图对照原图和转换后的标注一眼就发现了问题省下了一整晚的无效训练时间。第二YOLOv8 的默认参数在 COCO 上表现很好但不是 VisDrone 的最佳配置。对于小目标密集的无人机数据我建议把imgsz从默认的 640 提到 960 或 1280把conf阈值调低这会直接影响检测的查全率。baseline 阶段用默认参数是为了快速跑通流程真正用好这个数据集还需要针对场景做适配。第三跑深度学习项目日志和版本管理真的很重要。YOLOv8 会自动把每次训练的输出放到runs/detect/train/目录下里面包含了weights、args.yaml、results.csv等文件。我习惯在训练结束后把最关键的几张预测图挑出来拷到一个专门目录里方便后面地对比实验。VisDrone 这个数据集跑出一个还不错的 baseline 只是第一步真正挑战在于如何在保持速度的前提下把小目标检准。后续可以尝试的方向很多切图检测、多尺度训练、使用带 Transformer 结构的检测头、加入注意力机制等等。祝大家在 VisDrone 上的实验都能顺利跑通mAP 节节高。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询