ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

900张路标数据集:YOLO目标检测训练与避坑实践

900张路标数据集:YOLO目标检测训练与避坑实践 简介已标注的YOLO目标检测路标数据集包含877张道路场景图像以及对应的YOLO与VOC格式标注文件适合计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业和毕业设计中的目标检测模型训练与效果验证。压缩包共2635个文件其中877张PNG图像、877个XML标注文件与881个TXT文本整体约218.43MBXML对应VOC格式的目标框标注TXT为YOLO格式可直接读取的标签信息标注体系完整规范能够直接接入主流YOLO训练流程省去自行采集与标注的时间。已有217人浏览学习数据集内还附带参数化编程源码代码思路清晰、注释详细关键参数便于修改适合快速理解和二次开发作者为资深算法工程师长期从事计算机视觉与目标检测仿真读者在实验过程中遇到问题可以交流。对需要真实道路标志数据完成课设或毕设的学生来说这份经过整理的数据集和配套代码具有较高的实用价值。1. 这份 900 张路标数据集把目标检测项目里最耗时的环节替你省了做目标检测的人应该都有体会跑通 YOLO 训练不难难的是凑一批带标注的数据。要么从网上扒图要么扛着相机去路边拍拍完还要用 LabelImg 一张一张框遇到限速标志、停止标志这类小目标一张图框二十分钟很正常。这份路标数据集一共 900 张图像图片内容以道路场景为主限速、停止、让行、施工警告这类常见标志都有覆盖而且标注文件同时给了 YOLO 的 txt 和 VOC 的 xml 两套格式等于把收集、清洗、打标这三个环节全替你做完了。适合正在做课程设计、期末大作业、毕业设计的学生也适合想快速跑通 YOLO 训练全流程、但不想在数据准备上耗时间的从业者。下面按从读数据到验证结果的顺序把这份资源怎么用、坑在哪讲透。2. 数据长什么样PNG、TXT 与 XML 三种文件的对应关系拿到压缩包不要急着解压后直接扔进 train.py先拆开看结构。这份数据集的核心是三类文件图像文件是 PNGYOLO 标注是 txtVOC 标注是 xml。很多刚接触目标检测的人第一反应是“我已经有 xml 了为什么还要 txt”其实两种格式服务于不同的训练链路后面的章节会展开讲。这里先从文件组织的角度把对应关系理清楚。2.1 三类文件的组织方式与双格式标注的用途一份标准的路标检测数据集解压后目录结构通常是这样的目录/文件格式作用images 或 JPEGImages.png原始道路图像900 张labels 或 Annotations.txtYOLO 格式标签每张图一个同名文件Annotations 或 VOC 目录.xmlVOC 格式标签每张图一个同名文件classes.txt部分压缩包有.txt类别名列表顺序与标签 id 一一对应文件名一一对应是这套资源能直接用的前提也就是road51.png对应road51.txt和road51.xml。如果解压后发现某个文件多了个空格、后缀大小写不一致训练时就会报“No labels found”这个坑在避坑章节里单独说。YOLO 格式的 txt 是给 YOLO 系列训练脚本直接读的每一行代表一个目标框内容是“类别 id 归一化坐标”没有多余信息读取效率高是训练时的主力格式。VOC 的 xml 则是更完整的标注描述包含图片尺寸、文件名、目标类别、框的像素坐标适合用来做数据可视化、格式转换、或者喂给 Faster R-CNN 这类需要 XML 的检测框架。两个格式同时存在意味着你可以在这份数据上自由切换训练框架不用自己写转换脚本。2.2 YOLO 标签一行五列数字是怎么还原成像素框的YOLO 格式的 txt 文件里每一行是五个数字空格分隔3 0.514844 0.412500 0.053906 0.095833从左到右依次是类别 id、目标中心点的 x 坐标、中心点的 y 坐标、目标的宽度、目标的高度。注意后四个值全部是归一化的范围在 0 到 1 之间是相对于图片宽高的比例不是像素值。def yolo_label_to_pixel(txt_path, img_w, img_h): 把 YOLO 归一化标签还原成像素坐标返回 (类别, xmin, ymin, xmax, ymax) boxes [] with open(txt_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f[跳过] {txt_path} 中有一行不是5列: {line.strip()}) continue cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) # 归一化坐标乘以图片宽高还原成像素值 x_min int((cx - bw / 2) * img_w) y_min int((cy - bh / 2) * img_h) x_max int((cx bw / 2) * img_w) y_max int((cy bh / 2) * img_h) boxes.append((cls_id, x_min, y_min, x_max, y_max)) return boxes这段代码是整套数据操作的地基。img_w和img_h不能写死要用 cv2 或 PIL 从对应图片里读因为不同照片分辨率可能不一样。计算时先算中心点坐标再加减宽高的一半得到左上角和右下角这个顺序不要搞反搞反了画出来的框就是错位的。理解了这五列数字的含义后面写校验脚本、画框验证、调整检测结果才有基础。2.3 VOC 的 XMLsize、object 与 bndbox 的配合VOC 格式的 xml 文件结构比 txt 复杂一些但信息量也更大。一个典型的结构是这样的annotation folderJPEGImages/folder filenameroad51.png/filename size width1280/width height720/height depth3/depth /size object namespeed_limit/name bndbox xmin542/xmin ymin310/ymin xmax610/xmax ymax378/ymax /bndbox /object /annotation很多新手会把注意力全放在object的bndbox上忽略了size里的宽高。实际上size是整个 xml 里最容易出问题的地方——如果一张 1280×720 的图xml 里却写成 1920×1080那么转换成 YOLO 格式时所有框都会偏移。filename字段有时候带路径前缀有时候只有文件名解析时建议用os.path.basename统一处理。import xml.etree.ElementTree as ET def voc_xml_to_yolo(xml_path): 把 VOC xml 转成 YOLO 的一行字符串用于双格式互转 tree ET.parse(xml_path) root tree.getroot() img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) yolo_lines [] for obj in root.findall(object): name obj.find(name).text # 类别名需要映射成 id 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) # 转成 YOLO 归一化坐标 cx ((xmin xmax) / 2) / img_w cy ((ymin ymax) / 2) / img_h bw (xmax - xmin) / img_w bh (ymax - ymin) / img_h yolo_lines.append(f{name} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) return \n.join(yolo_lines)类别名要映射成 id映射表必须来自这份数据集自带的类别定义通常是一个 classes.txt 或者 readme 里的说明不能自己猜。转换后建议抽样几张图把画了框的结果和原图叠在一起看这一步能发现绝大多数格式问题。3. 训练前先校验一个脚本解决类别错位、坐标越界和空标注直接拿数据开训是新手最常见的做法但我的建议是训练前先花十分钟做一次全量校验。900 张图加 900 个标签文件靠人眼检查不现实写一个脚本把所有异常一次性扫出来是最划算的投入。校验要覆盖三个层面图片和标签是否配对、标签内容是否合法、类别 id 是否与训练配置一致。3.1 图片与标签的配对普查最容易出问题的就是配对。图片有 900 张标签少了两三个YOLO 训练脚本不会报错顶多 warning 一下但你的模型就少学了两三个样本。反过来如果一个标签文件对应不到图片训练时读到的是孤儿标签也会影响数据加载。import os import glob IMG_DIR images # 改成你的图片目录 LBL_DIR labels # 改成你的标签目录 def check_pairing(img_dir, lbl_dir): imgs sorted(glob.glob(os.path.join(img_dir, *.png)) glob.glob(os.path.join(img_dir, *.jpg))) missing_label [] orphan_label [] for img_path in imgs: base os.path.splitext(os.path.basename(img_path))[0] if not os.path.exists(os.path.join(lbl_dir, base .txt)): missing_label.append(img_path) for lbl_path in glob.glob(os.path.join(lbl_dir, *.txt)): base os.path.splitext(os.path.basename(lbl_path))[0] if not os.path.exists(os.path.join(img_dir, base .png)) and \ not os.path.exists(os.path.join(img_dir, base .jpg)): orphan_label.append(lbl_path) print(f图片总数: {len(imgs)}) print(f缺少标签的图片: {len(missing_label)} 张) print(f没有对应图片的标签: {len(orphan_label)} 个) for p in missing_label[:10]: print( [缺标签], p) for p in orphan_label[:10]: print( [孤儿标签], p) if __name__ __main__: check_pairing(IMG_DIR, LBL_DIR)配对检查的核心是找“基名”也就是去掉扩展名后的名字。图片扩展名是 .png 还是 .jpg 不影响配对但标签文件的扩展名必须是 .txt。如果图片是 .png、标签是 .jpg.txt 这种叠加后缀基名对不上就会误报建议先把文件名统一成一个规范。3.2 标签内容合法性检查越界、空文件、负数配对通过后要检查标签文件的内容。常见问题有三种空文件、某一行不是 5 列、坐标越界。坐标越界又分两种一种是中心点或宽高不在 0 到 1 之间另一种是中心点合法但框的边缘超出了图片范围。后者更隐蔽很多校验脚本只检查中心点导致边缘框漏检。import os import glob LBL_DIR labels def check_label_content(lbl_dir): bad_lines [] empty_files [] for lbl_path in sorted(glob.glob(os.path.join(lbl_dir, *.txt))): with open(lbl_path, r, encodingutf-8) as f: lines f.read().strip().splitlines() if not lines: empty_files.append(lbl_path) continue for idx, line in enumerate(lines): parts line.strip().split() if len(parts) ! 5: bad_lines.append((lbl_path, idx 1, f列数不对: {line})) continue try: cls_id int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) except ValueError: bad_lines.append((lbl_path, idx 1, f非数值: {line})) continue # 坐标必须在 (0,1)且框整体不能超出图像边界 if not (0 cx 1 and 0 cy 1 and 0 bw 1 and 0 bh 1): bad_lines.append((lbl_path, idx 1, f坐标越界: {line})) continue if cx - bw / 2 0 or cx bw / 2 1 or cy - bh / 2 0 or cy bh / 2 1: bad_lines.append((lbl_path, idx 1, f框越界: {line})) print(f空标注文件: {len(empty_files)} 个) print(f异常标签行: {len(bad_lines)} 处) for p, line_no, reason in bad_lines[:10]: print(f {p} 第{line_no}行: {reason}) if __name__ __main__: check_label_content(LBL_DIR)这个脚本关注两个细节一是用try/except捕获非数值内容很多打标工具会莫名其妙在文件末尾塞一个空行split 后列数不对数值解析也会报错二是边界检查用cx - bw / 2 0而不是只查cx本身这样能发现那些中心点在图像内、但框体已经伸出图像外的标注这类标注在训练时会把负坐标裁掉yolo 会生成无效 anchor。3.3 data.yaml类别顺序错一位训练结果全乱标签内容合法不代表能直接训练还有一个隐含条件txt 里的类别 id 必须和训练脚本读到的 names 顺序严格一致。YOLOv5 和 YOLOv8 训练时都依赖一个 yaml 配置文件里面指定了训练集、验证集路径、类别数量和类别名列表。# data.yaml 示例 train: /datasets/road_sign/images/train val: /datasets/road_sign/images/val nc: 8 names: 0: speed_limit 1: no_entry 2: stop 3: no_parking 4: yield 5: construction 6: pedestrian 7: turn_left这里最容易翻车的场景是数据集的 classes.txt 里第 0 行是stop你却在 data.yaml 里把stop写在了第 2 行。标注 id 是打标时按 classes.txt 顺序生成的训练时按 yaml 顺序解析两边错位的结果就是模型把限速标志当成停止标志来学损失函数照样下降预测时却张冠李戴。判断这份数据集到底有几个类、类别顺序是什么压缩包内如果有 classes.txt 就按它来没有的话抽查几个 txt 文件统计类别 id 的最大值再对照 xml 里的name字段反推顺序。nc写错了也会直接报错因为数据集里出现了超出nc范围的 id 时训练脚本会崩溃。3.4 训练集与验证集划分分层抽样防止验证集缺类900 张图不能全用来训练要留一部分做验证。很多人图省事直接random.shuffle然后按比例切分但这有隐患如果某个类别只出现在少数几张图里随机切分后可能全部落进训练集验证集里完全没有这个类别mAP 曲线就会显示这个类别的 AP 为 0。import os import random import shutil from collections import defaultdict random.seed(42) # 固定随机种子保证可复现 SRC_IMG images SRC_LBL labels OUT_ROOT road_sign_split RATIO 0.8 def collect_by_class(img_dir, lbl_dir): 按类别组合收集图片返回 {类别组合: [图片文件名]} mapping defaultdict(list) for img_name in os.listdir(img_dir): if not img_name.lower().endswith((.png, .jpg)): continue base os.path.splitext(img_name)[0] lbl_path os.path.join(lbl_dir, base .txt) if not os.path.exists(lbl_path): continue with open(lbl_path, r, encodingutf-8) as f: cls_ids [line.split()[0] for line in f if line.strip()] if not cls_ids: continue key ,.join(sorted(set(cls_ids))) # 多类同框时归入同一组合 mapping[key].append(img_name) return mapping # 按类内比例切分保证每个类别组合在训练集和验证集里都有 mapping collect_by_class(SRC_IMG, SRC_LBL) train_list, val_list [], [] for key, img_names in mapping.items(): random.shuffle(img_names) n_train int(len(img_names) * RATIO) train_list img_names[:n_train] val_list img_names[n_train:] # 复制到新目录保持 images/labels 的结构 for phase, img_list in [(train, train_list), (val, val_list)]: img_out os.path.join(OUT_ROOT, images, phase) lbl_out os.path.join(OUT_ROOT, labels, phase) os.makedirs(img_out, exist_okTrue) os.makedirs(lbl_out, exist_okTrue) for img_name in img_list: base os.path.splitext(img_name)[0] shutil.copy(os.path.join(SRC_IMG, img_name), os.path.join(img_out, img_name)) shutil.copy(os.path.join(SRC_LBL, base .txt), os.path.join(lbl_out, base .txt)) print(f训练集: {len(train_list)} 张, 验证集: {len(val_list)} 张)这段脚本按“类别组合”分组再切分而不是单张随机切分。比如某张图同时包含限速和停止两个标志它就会被归到0,2这个组里训练集和验证集各自保留这个组合的一定比例。参数方面RATIO是训练集占比小数据集建议 0.8 而不是 0.7因为 900 张图去掉 20% 验证后就剩 720 张训练图再少就不好训了random.seed(42)固定随机种子保证你两次跑划分脚本得到相同的结果方便复现和排查。4. 让 YOLO 跑起来环境配置、训练命令与损失曲线判断数据和划分都准备好了这一步就是把训练跑起来。YOLO 的训练链路已经非常成熟网上各种环境配置教程多如牛毛但实际踩坑点集中在三个地方环境版本搭配、训练参数设置、训练过程监控。这一章把这三件事一次讲透。4.1 环境配置用 conda 快速搭一个不打架的 Python 环境YOLO 训练本质上就是装一套 PyTorch 环境然后跑 train 脚本。常见做法是用 conda 建一个独立环境避免和系统 Python 混在一起。Python 版本建议选 3.9 或 3.10太新的版本某些依赖还没跟上。# 创建 python 3.10 环境 conda create -n yolo python3.10 -y conda activate yolo # 安装 pytorch这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 验证 GPU 是否可用 python -c import torch; print(torch.cuda.is_available())如果最后一行输出True说明 GPU 环境就绪输出False就需要检查显卡驱动和 CUDA 版本是否匹配。YOLOv5 需要额外安装依赖pip install -r requirements.txtYOLOv8 则直接pip install ultralytics一个包搞定。如果下载速度慢把 pip 换成国内镜像源即可。环境配置是最容易劝退新手的一关但也是出错最单一的一关严格按照报错信息去搜基本都能解决。4.2 一条训练命令从 0 到 1 的拆解YOLOv5 和 YOLOv8 的训练入口不同但核心参数几乎一样# YOLOv5 训练 python train.py \ --data data.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --device 0 \ --patience 20 # YOLOv8 训练 yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ device0 \ patience20参数作用建议值data指向 data.yaml训练核心配置用绝对路径最稳妥weights / model预训练权重决定模型大小新手选 s 或 n跑得快epochs训练轮数小数据集 100~200batch-size每批送入的图片数显存 8G 用 1616G 用 32imgsz训练输入尺寸640 起步路标小目标可试 960patience验证指标 n 轮不提升就早停20~50imgsz是训练时把图片缩放成的分辨率不是原始分辨率。路标这种小目标640 输入下远处的标志可能只有十几个像素如果训练结果不理想把imgsz提到 960 或 1280 通常能带来明显提升代价是显存占用和训练时间翻倍。batch-size同理显存不够就减半不要硬撑。4.3 训练过程看什么box_loss、cls_loss 与 mAP训练启动后不要干等YOLOv5 会在runs/train/exp下生成results.csvYOLOv8 会输出到runs/detect/train里面记录了每一轮的损失和指标。重点看四个值。box_loss是框回归损失这个值应该逐步下降如果震荡剧烈说明学习率偏大。cls_loss是分类损失它下降意味着模型在学区别不同路标。mAP50是主要质量标准指的是 IoU 阈值 0.5 时所有类别的平均精度路标数据集训练到位的话这个值应该能到 0.8 以上。mAP50-95更严格要求预测框和真实框重叠度更高小目标数据集这个值通常偏低不要因为它不涨就焦虑。训练过程中如果发现验证集上的box_loss降到底后开始反弹同时训练集的 loss 还在降说明过拟合了。此时patience参数会生效连续 20 轮验证指标没有提升就自动停止best.pt 会保存历史最优权重。训练结束后用 best.pt而不是用最后一轮的 last.pt这是很多人容易忽略的细节。5. 避坑清单路标数据集训练里最常见的五个坑这一章是在实际处理同类数据集时踩出来的经验。下面五个坑每一个都对应真实报错或异常表现按“现象、原因、解决”三步记录遇到类似问题可以直接对照排查。5.1 训练时报错expected 5 values, got 6现象训练刚启动数据加载阶段直接报错提示某一行标签应该有 5 个值却读到了 6 个。原因YOLO 的 txt 标签要求每行严格是类别id cx cy w h五个值。这份数据集的标注文件如果是在 Windows 上生成txt 末尾可能带上\r用split(,)而不是split()时就会把换行符也算进去或者某一行里同时存在空格和逗号比如0, 0.5, 0.5, 0.1, 0.1按空格切分后不是 5 列。解决统一用line.strip().split()解析它会自动处理连续空格和换行符。如果split()后确实多了列打开那个 txt 文件检查是不是有逗号残留把逗号替换成空格再重新跑第 3 章的校验脚本。5.2 类别标签全是乱的names 顺序没对齐现象训练一切正常损失正常下降mAP 也到了 0.7 以上但把预测结果画到图上模型把限速标志框成 stop把让行标志框成禁止驶入而且错得很有规律。原因txt 里的类别 id 是打标时按某个顺序生成的data.yaml 里的 names 却是按另一个顺序写的。YOLO 训练脚本不会校验这个对应关系它只认 id 的数字模型学到的语义标签因此全部错位。解决找到压缩包内的 classes.txt或 readme 里的类别说明把里面的类别名按行号一一对应填进 data.yaml 的 names。写完后不要急着训练先随机抽三张图用可视化脚本画出标注框和类别名看 GT 框显示的类别是否和图片内容一致。5.3 中文路径导致图片读不出来现象路径里包含中文目录名或者中文文件名训练时报No labels found in train set但自己打开目录看文件明明都在。原因OpenCV 的cv2.imread对中文路径支持不好会静默返回 NoneYOLO 训练脚本读取图片失败后不会报错只会跳过这些样本累积下来就是大量样本没进训练。解决把整个数据集项目路径改成纯英文字符图片文件名也确认是road51.png这种格式。如果你的解压工具把文件名解析成乱码建议重新解压一次解压时选择 UTF-8 编码。5.4 小目标太多mAP50-95 上不去现象mAP50 已经到 0.8说明模型找到了大部分目标但 mAP50-95 只有 0.2~0.3远低于预期。原因路标是典型的小目标尤其远处的小标志在 640×640 的输入下只有十几个像素框的 IoU 稍微偏一点mAP50-95 就掉得厉害。解决训练时把imgsz提到 960 或 1280小目标会多出几倍的像素特征。也可以对图片做切片增强把一张大图切成四块分别训练进一步提升小目标召回率。推理阶段调低置信度阈值到 0.1~0.15先把候选框全部召回再用 NMS 去重。5.5 训练集和验证集有重复图片现象训练时验证集 loss 比训练集还低mAP 虚高换一批新图片测试效果立马打回原形。原因划分训练集和验证集时用了随机切分但没有检查图片间的关系。道路场景拍摄经常会连拍road51 和 road52 可能是同一场景的连续两帧如果一张进了训练集、一张进了验证集模型等于在开卷考试。解决用第 3 章的划分脚本按类别组合分组再加一道去重对比图片文件的 MD5 哈希完全相同的图片只保留一份。同一个场景的连续帧如果高度相似建议按文件名前缀分组后整组划入同一侧。6. 训完别急着收工画框对比验证再调置信度阈值训练完拿到 best.pt不要直接拿去交作业先做可视化验证。很多人只看日志里的 mAP但 mAP 是统计指标掩盖了单张图上的细节问题。我自己拿到新权重后的第一件事是把预测框和真实标注框画在同一张图上看对比。import cv2 import torch # 以 YOLOv5 为例加载训练好的权重 model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt) model.conf 0.25 # 置信度阈值 model.iou 0.45 # NMS 的 IoU 阈值 img cv2.imread(road51.png) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results model(img_rgb) # 把真实标注按 YOLO 格式画上去这里用前面写的还原函数 gt_boxes yolo_label_to_pixel(road51.txt, img.shape[1], img.shape[0]) for cls_id, x_min, y_min, x_max, y_max in gt_boxes: cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) # 绿色 GT # 预测框画红色 for *xyxy, conf, cls_id in results.xyxy[0]: x_min, y_min, x_max, y_max map(int, xyxy) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), (0, 0, 255), 2) # 红色预测 cv2.imwrite(compare_road51.png, img)GT 框用绿色预测框用红色叠在一起后一眼就能看出两类问题预测框大量缺失说明置信度阈值偏高预测框与 GT 错位明显说明模型的框回归没学好后者往往来自训练数据里坐标越界的坏样本。如果整体匹配良好再根据漏检和误检的比例决定要不要调conf阈值。漏检多就往下降比如 0.15误检多就往上升比如 0.4。调完阈值要用一批未参与训练的新图片再验证一次确保不是过拟合到验证集。从那以后我每次拿到数据集都强制走一遍校验脚本训完再画一次对比图确认无误才把结果发给对方。这样的习惯帮我挡掉了不少返工也希望帮到你。本文还有配套的精品资源点击获取
返回列表