☰
航拍路面标志检测实战:566张9类别数据集YOLOv8训练与踩坑
2026/9/28 16:44:36 网站建设 项目流程

简介:本资源为航拍路面直行与转弯标志检测数据集,面向从事交通标志识别、自动驾驶感知及计算机视觉算法训练的研究者与开发者,尤其适合需要多方向指示标志样本的目标检测项目。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及对应的xml、txt标注文件,可直接接入主流检测框架进行训练与验证。压缩包共1700个文件,其中566张jpg原图、566个VOC格式xml标注、568个txt文件(含YOLO标注及类别说明),整体约236.49MB,采用7z打包便于下载解压。标注类别共9类,覆盖left、left_back、left_right、right、straight、straight_back、straight_left、straight_left_right、straight_right,能较完整地刻画直行、转弯及组合方向的路面指示标志。目前已有144人学习下载。需要说明的是,图片经过增强处理,建议先查看博文中的样本预览确认风格与场景符合需求后再下载使用。

1. 航拍路面直行和转弯标志检测:566张9类别数据集到底能跑出什么结果

拿到「航拍路面直行和转弯标志检测数据集VOC+YOLO格式566张9类别」这个标题,第一反应不是兴奋,而是先算一笔账:566张图、9个类别,平均每类不到63张,这个量级放在通用目标检测里连热身都算不上,但放在航拍路面标志这个垂直场景里,反而可能是目前能公开拿到的最务实的一批数据。航拍视角下的路面标志和车载视角完全是两回事——透视畸变、尺度剧烈变化、地面标线被车辆遮挡、光照反射导致白色箭头过曝,这些在常规数据集里很少同时出现。直行箭头和转弯箭头在航拍图里往往只占几十个像素,YOLO默认的640输入尺寸下,小目标召回率会掉得很难看。这个数据集真正解决的问题是:让你在不需要自己爬取和标注的情况下,快速验证一个航拍路面标志检测的pipeline能不能跑通,以及跑通之后mAP大概在什么水位。适合谁用?做无人机巡检、智慧交通、高精地图更新的团队,想先拿一个小规模数据集试水,确认技术路线可行再决定要不要投入标注资源。不适合谁?指望拿它直接上生产、或者做多模态大模型预训练的人,566张的体量撑不起那个野心。

2. VOC和YOLO双格式拆解:为什么566张图要存两份标注

2.1 VOC的XML结构和YOLO的TXT结构到底差在哪

VOC格式每张图对应一个XML文件,里面用<object>标签逐个记录目标,包含类别名、边界框的xmin/ymin/xmax/ymax。YOLO格式则是每张图一个TXT,每行一个目标,格式是类别索引 中心x 中心y 宽 高,所有坐标都归一化到0到1之间。这两种格式的差异不只是文件后缀,而是坐标语义的根本不同。VOC存的是绝对像素坐标,YOLO存的是相对比例。这意味着你把VOC转YOLO的时候,必须用原图的宽高做除数,一旦某张图的尺寸记录错了,转换出来的框会整体偏移甚至跑到图外。

import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, img_w, img_h, class_list): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): cls_name = obj.find('name').text if cls_name not in class_list: continue cls_id = class_list.index(cls_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) # 归一化并转为中心点+宽高 cx = (xmin + xmax) / 2.0 / img_w cy = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 边界裁剪,防止归一化后越界 cx = max(0.0, min(1.0, cx)) cy = max(0.0, min(1.0, cy)) w = max(0.0, min(1.0, w)) h = max(0.0, min(1.0, h)) lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") return lines

这段代码的关键在最后四行的裁剪逻辑。航拍图里经常出现目标被图像边缘截断的情况,VOC标注时xmax可能等于图像宽度,归一化后正好是1.0,但YOLO训练时如果某个坐标超过1.0,部分框架会直接报错或者静默丢弃该目标。我一般会在转换阶段就把所有坐标夹到0到1之间,宁可损失一点精度也不让训练中断。参数说明:class_list必须和后续训练时的names顺序完全一致,否则类别索引全乱;img_w和img_h要从对应的图片文件读取,不能硬编码,因为航拍数据集里不同来源的图尺寸可能不统一。

2.2 9个类别的类别不平衡怎么处理

566张图分9类,必然存在长尾。直行箭头可能有两百多个实例,而某个特殊转弯标志可能只有十几个。YOLO默认的损失函数对类别不平衡没有特殊处理,训练时少数类会被多数类淹没。常见做法是在数据加载阶段用重采样,或者调整损失权重。我一般会先统计每个类别的实例数,然后对实例数少于50的类别做过采样,同时在data.yaml里把fl_gamma调大一点,让难样本的梯度贡献更突出。

import os from collections import Counter def count_instances(label_dir): counter = Counter() for txt_file in os.listdir(label_dir): if not txt_file.endswith('.txt'): continue with open(os.path.join(label_dir, txt_file), 'r') as f: for line in f: cls_id = int(line.strip().split()[0]) counter[cls_id] += 1 return counter # 输出每个类别的实例数,决定是否需要过采样 counts = count_instances('./labels/train') for cls_id, cnt in sorted(counts.items()): print(f"class {cls_id}: {cnt} instances")

跑完这个统计,你就能看到哪些类别是“贫困户”。如果某个类别实例数低于30,建议在训练时对该类别的图片做2到3倍复制,但要注意复制后的图片不能和验证集重叠,否则验证指标会虚高。另一个坑是:过采样之后要重新划分训练集和验证集,保证同一张原图不会同时出现在两边。

2.3 从7z压缩包到可训练目录的标准操作流程

拿到.7z压缩包之后,不要直接解压到当前目录,先建一个干净的项目结构。我习惯的目录布局是datasets/road_sign/下面分images/train、images/val、labels/train、labels/val,外加一个data.yaml。解压命令用7z x而不是7za,因为7za对某些压缩算法的支持不完整。

# 创建项目目录 mkdir -p datasets/road_sign/{images,labels}/{train,val} # 解压7z包到临时目录 7z x road_sign_dataset.7z -o./temp_extract # 假设解压后VOC格式在temp_extract/VOC/,图片和XML混在一起 # 先按8:2划分训练验证集 python split_dataset.py --src ./temp_extract/VOC --dst ./datasets/road_sign --ratio 0.8

split_dataset.py需要自己写,核心逻辑是:收集所有图片文件,随机打乱后按比例分配,同时把对应的XML转成YOLO TXT放到labels目录。注意随机种子要固定,否则每次跑出来的划分不一样,实验没法复现。我一般设random.seed(42),这个数字没什么特殊含义,纯粹是习惯。

3. 用YOLOv8跑通第一个baseline:从环境配置到训练完成

3.1 环境配置的版本锁定和常见依赖冲突

YOLOv8对PyTorch版本有要求,不是越新越好。我踩过的坑是:PyTorch 2.1配CUDA 12.1,训练到第10个epoch左右loss突然变NaN,换回PyTorch 2.0.1加CUDA 11.8就稳了。所以环境配置这一步,版本锁定比什么都重要。

# 创建conda环境 conda create -n yolov8_road python=3.9 -y conda activate yolov8_road # 安装PyTorch,指定CUDA版本 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 安装ultralytics pip install ultralytics==8.0.200 # 验证安装 python -c "from ultralytics import YOLO; print('ok')"

参数说明:ultralytics==8.0.200是我在多个项目里验证过比较稳的版本,新版本有时候会改默认参数导致复现困难。如果你用的是V100或者更老的卡,CUDA 11.8是安全选择;如果是40系卡,可以上CUDA 12.1配PyTorch 2.1,但要做好loss异常的心理准备。

3.2 data.yaml的九个类别名和路径写法

data.yaml是YOLO训练的数据入口,写错了直接报错退出。路径可以用绝对路径也可以用相对路径,但相对路径是相对于你执行训练命令的目录,不是相对于data.yaml文件本身,这个设计很容易让人翻车。

# data.yaml path: /home/user/datasets/road_sign # 数据集根目录 train: images/train # 相对于path val: images/val test: # 可选 names: 0: straight_arrow 1: left_turn_arrow 2: right_turn_arrow 3: straight_left_arrow 4: straight_right_arrow 5: u_turn_arrow 6: merge_arrow 7: crosswalk_warning 8: stop_line

注意names的索引必须从0开始连续,不能跳号。如果你从VOC转换时类别名有空格或者特殊字符,建议统一改成下划线,避免YAML解析出问题。另外path字段在ultralytics 8.0.200里是支持的,老版本可能要用train: ../images/train这种写法,升级版本后记得检查。

3.3 训练命令的六个关键参数怎么设

YOLOv8的训练命令看起来简单,但每个参数背后都有取舍。566张图的小数据集,如果按默认参数跑,大概率过拟合。

yolo detect train \ data=./data.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=1024 \ batch=8 \ lr0=0.001 \ patience=30 \ augment=True \ mosaic=0.5 \ project=./runs/road_sign \ name=exp01

逐个说:model=yolov8n.pt选nano版本,因为数据量小,大模型参数量多反而容易过拟合;imgsz=1024比默认的640大,航拍小目标在640下几乎不可见,1024是显存和精度的折中;batch=8配合1024输入,8G显存刚好够用;lr0=0.001比默认的0.01小一个量级,小数据集需要更保守的学习率;patience=30表示30个epoch验证指标不提升就早停,防止无效训练;mosaic=0.5把mosaic增强概率降到0.5,默认的1.0对小数据集太激进,容易把目标切得太碎。

训练过程中重点看三个指标:box_loss是否稳定下降、mAP50是否在50个epoch后还在涨、val/box_loss和train/box_loss的差距是否越来越大。如果val/box_loss连续10个epoch上升而train/box_loss还在降,就是过拟合的明确信号,该早停就早停。

4. 航拍小目标检测的翻车现场:五个血泪踩坑记录

4.1 坑一:mAP50看着不错但实际漏检严重

现象:训练完看指标,mAP50有0.75,觉得还行,但拿几张验证集图片跑推理,发现转弯箭头基本没框出来。原因:mAP50是IoU阈值0.5下的平均精度,对于小目标来说,框稍微偏一点IoU就掉到0.5以下,但模型可能学到了“大致位置”,指标上体现不出来。更关键的是,9个类别里直行箭头实例多,拉高了整体mAP,少数类的低召回被平均掉了。解决:不要只看整体mAP,用yolo detect val命令输出每个类别的AP,单独看转弯类别的指标。如果某个类别AP低于0.3,要么增加该类样本,要么在推理时降低该类别的置信度阈值。

# 输出每个类别的详细指标 yolo detect val model=./runs/road_sign/exp01/weights/best.pt data=./data.yaml split=val verbose=True

4.2 坑二:图像尺寸不统一导致训练中途报错

现象:训练到第3个epoch突然报RuntimeError: stack expects each tensor to be equal size。原因:航拍数据集里混入了不同分辨率的图片,YOLO的dataloader在默认配置下要求同一batch内图片尺寸一致,虽然训练时会做resize,但如果某张图的宽高比极端(比如全景拼接图),resize后仍然和其他图对不齐。解决:在训练前统一把所有图片resize到固定尺寸,或者用rect=True参数让dataloader按长边对齐。我一般直接写个脚本把所有图短边缩到1024,长边按比例缩放,这样既保留信息又避免报错。

from PIL import Image import os def resize_images(img_dir, target_short=1024): for fname in os.listdir(img_dir): if not fname.lower().endswith(('.jpg', '.png', '.jpeg')): continue img_path = os.path.join(img_dir, fname) img = Image.open(img_path) w, h = img.size if w < h: new_w = target_short new_h = int(h * target_short / w) else: new_h = target_short new_w = int(w * target_short / h) img = img.resize((new_w, new_h), Image.BILINEAR) img.save(img_path)

注意resize之后标注文件也要同步更新,因为YOLO的归一化坐标是相对于原图尺寸的,图片尺寸变了但标注没变,框会全部错位。所以这个操作要在VOC转YOLO之前做,或者转完之后重新归一化。

4.3 坑三:验证集mAP波动大,每次跑结果不一样

现象:同样的数据和参数,跑三次训练,mAP50分别是0.72、0.68、0.75,波动超过5个点。原因:小数据集对随机种子极度敏感,数据划分、权重初始化、数据增强的随机顺序都会影响最终结果。566张图里验证集只有113张,一张图的预测变化就能让mAP跳好几个点。解决:固定所有随机种子,包括Python的random、NumPy的np.random、PyTorch的torch.manual_seed,以及CUDA的torch.cuda.manual_seed_all。另外用K折交叉验证代替单次划分,虽然训练时间翻倍,但指标更可靠。

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)

torch.backends.cudnn.deterministic = True会降低训练速度,但换来确定性。如果你赶时间,可以只固定前三个种子,cudnn的确定性关掉,波动会小一些但不会完全消除。

4.4 坑四:7z解压后文件名乱码导致图片和标注对不上

现象:解压后图片文件名显示正常,但标注XML里的<filename>字段和实际文件名不一致,导致转换脚本找不到对应关系。原因:7z在Windows下打包时用了GBK编码,在Linux下解压后文件名变成乱码,虽然ls看着像正常中文,但实际字节序列和XML里记录的不一样。解决:解压时加-mcp=936参数指定代码页,或者解压后用convmv工具统一转成UTF-8。更稳妥的做法是:转换脚本不依赖XML里的<filename>字段,而是用XML文件名去匹配同名的图片文件。

# 解压时指定代码页 7z x road_sign_dataset.7z -o./temp_extract -mcp=936 # 或者解压后转换文件名编码 convmv -f GBK -t UTF-8 -r --notest ./temp_extract

4.5 坑五:推理时置信度阈值设0.25导致大量误检

现象:用训练好的模型跑测试图片,背景里的白色斑马线、路面反光都被框成了箭头。原因:YOLO默认的置信度阈值是0.25,对于航拍路面这种背景复杂、目标与背景颜色接近的场景,0.25太低了。模型对“像箭头但不是箭头”的区域给出了0.3左右的置信度,全被保留下来。解决:把推理阈值提到0.5以上,同时开NMS的IoU阈值调到0.3,把重叠的误检框压掉。如果还是误检多,说明训练数据里负样本不够,需要额外收集一些不含目标的航拍路面图作为背景负样本加入训练。

yolo detect predict \ model=./runs/road_sign/exp01/weights/best.pt \ source=./test_images \ conf=0.5 \ iou=0.3 \ save=True

5. 把566张图用到极致:小数据集涨点的四个进阶技巧

5.1 用预训练权重做领域适配而不是从头训

566张图从头训YOLOv8,收敛慢且最终精度低。正确做法是加载COCO预训练的yolov8n.pt,然后冻结backbone的前几层,只训head和后面的层。ultralytics默认就会加载预训练权重,但你可以通过freeze参数控制冻结层数。

yolo detect train \ data=./data.yaml \ model=yolov8n.pt \ freeze=10 \ epochs=150 \ imgsz=1024 \ batch=8 \ lr0=0.001

freeze=10表示冻结前10层,这些层学的是通用边缘和纹理特征,航拍路面标志和COCO里的物体在低层特征上有共通之处。冻结层数不是越多越好,我试过freeze=20,mAP反而掉了3个点,因为高层语义特征被锁死了,模型没法适应航拍视角。一般freeze=5到freeze=10之间比较稳。

5.2 用切片推理提升小目标召回

航拍图里箭头可能只占30×30像素,直接resize到1024输入,箭头变成60×60,信息量还是不够。切片推理的思路是:把原图切成若干重叠的小块,每块单独推理,再把结果拼回去。这样每个小目标在切片里占的像素比例更大,召回率明显提升。

from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type='yolov8', model_path='./runs/road_sign/exp01/weights/best.pt', confidence_threshold=0.4, device='cuda:0' ) result = get_sliced_prediction( 'test_road.jpg', detection_model, slice_height=512, slice_width=512, overlap_height_ratio=0.2, overlap_width_ratio=0.2 )

slice_height和slice_width设512,因为原图短边是1024,切4块刚好。overlap设0.2是为了避免目标被切在边界上导致漏检。SAHI这个库不是ultralytics自带的,需要pip install sahi单独装。切片推理的代价是速度慢3到4倍,但小目标召回能提升10到15个点,值不值得看你的场景对漏检的容忍度。

5.3 用类别权重和Focal Loss缓解长尾问题

9个类别里,直行箭头可能有两百个实例,而某个转弯标志只有十几个。YOLOv8默认的BCE损失对每个类别的梯度贡献是均等的,少数类会被多数类淹没。可以在训练时给每个类别加权重,权重和实例数成反比。

# 在data.yaml同级目录建一个class_weights.py # 训练时通过回调修改损失函数的权重 from ultralytics import YOLO import torch model = YOLO('yolov8n.pt') # 假设统计出的实例数如下 instance_counts = [220, 180, 150, 90, 85, 40, 35, 25, 15] total = sum(instance_counts) weights = [total / (len(instance_counts) * c) for c in instance_counts] weights = torch.tensor(weights).cuda() # 在训练回调里替换损失权重 def on_train_start(trainer): trainer.model.loss_weights = weights model.add_callback('on_train_start', on_train_start) model.train(data='./data.yaml', epochs=150, imgsz=1024, batch=8)

这个做法需要改ultralytics的源码或者用回调注入,不是官方标准接口,版本升级后可能失效。更稳妥的做法是用过采样:把少数类的图片复制几份,让每个类别的实例数大致均衡。虽然简单粗暴,但兼容性好,不会因为库版本变化而翻车。

5.4 用混淆矩阵定位类别混淆的具体对

训练完之后,runs/road_sign/exp01/目录下会生成confusion_matrix.png。这个图比mAP更有诊断价值,因为它告诉你模型把哪个类别错认成了哪个类别。航拍路面标志里,直行箭头和直行左转箭头在低分辨率下很容易混,左转箭头和掉头箭头在特定角度下也容易混。看到混淆矩阵里某两个类别的交叉值很高,就针对性地补充这两类的区分性样本,或者调整输入分辨率让箭头形状更清晰。

# 用验证集生成混淆矩阵 yolo detect val \ model=./runs/road_sign/exp01/weights/best.pt \ data=./data.yaml \ split=val \ plots=True

plots=True会输出混淆矩阵、PR曲线、F1曲线等一整套诊断图。我一般先看混淆矩阵,再看PR曲线。如果某个类别的PR曲线在召回0.6之后精度断崖下跌,说明模型对该类别的置信度校准不好,需要调整训练时的标签分配策略。

5.5 一个我常用的训练习惯

每次跑完实验,不管结果好坏,我都会把data.yaml、训练命令、最终mAP、混淆矩阵截图存到一个experiments/目录下,用日期加序号命名。566张图的数据集,你可能要跑十几轮才能找到最优参数组合,没有记录的话,两周后根本想不起来哪组参数对应哪个结果。这个习惯看起来笨,但省去了大量重复试错的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询