道路坑洞检测实战:YOLO数据集校验与训练全流程解析
2026/9/12 22:22:50 网站建设 项目流程

简介:这是一份面向计算机视觉开发者的道路坑洞检测目标检测数据集,基于YOLO系列算法整理,适用于YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流版本。道路坑洞检测是基础设施维护与自动驾驶场景中的常见任务,该数据集已按训练与验证需求划分好,并附带data.yaml配置文件,解压后即可直接开展模型训练与测试。压缩包共两千个文件,包含五百八十个TXT格式的YOLO标注文件与一千四百二十个XML格式的VOC标注文件,两种格式分别存放。其中TXT文件每行记录类别、归一化中心点坐标与宽高比例,XML文件则保存目标框绝对坐标与类别信息,方便在不同框架间切换使用。标签数据可帮助用户省去手动标注与格式转换的时间,整体大小约一百六十六点五二MB,已有148人学习,适合需要快速搭建道路病害识别、路面质量检测实验的算法工程师与科研人员。

1. 1990张图像带标签,这套坑洞检测数据集能直接开跑吗

拿到“yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip”,第一反应通常是解压、扔进训练脚本、看 mAP。但做道路场景检测的人会多留一个心眼:1990 张图不算多,却也不至于不够用,真正的问题在于这 1990 张图里包含多少种路面、多少种光照、多少种坑洞形态,以及标签是“能用”还是“刚好能训”。很多 YOLO 项目卡住的不是模型,而是数据集的校验、格式转换和划分方式。这篇文章不讨论“去下载更多数据”,而是把这一份 zip 从交付到上线的完整路径拆开:先验证压缩包和数据本身,再统一标注格式,接着按坑洞场景的特点做数据划分,然后用 YOLOv8 训练并盯住损失和评估指标,最后一轮用模型预测反向审计标注质量。适合正在做道路巡检、市政养护、施工验收或者车路协同感知的工程师,也适合刚拿到别人打包的数据集、准备训练第一个检测模型的同学。

2. 打开 zip 之前的检查,先验证数据集完整性再谈训练

2.1 不要急着解压,先跑一遍完整性校验

这种数据集大概率经过网盘、邮件或者移动硬盘转手,压缩包在传输过程中可能出现尾部截断或文件损坏。直接解压后训练到一半发现缺图、标签读不出来,返工成本远高于先花一分钟做校验。Linux 下最直接的做法是让 unzip 自己检查 CRC:

unzip -t yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip | tail -20

-t参数让 unzip 进入测试模式,逐个文件计算校验值并与压缩包内记录比对。tail -20只看最后的结果汇总,正常输出会有No errors detected in compressed data of ...。如果中间出现bad CRCmismatch相关字样,说明该文件在打包或者传输时已经损坏。

Windows 环境下没有 unzip 命令时,用 Python 的 zipfile 模块也能达到同样目的:

import zipfile zpath = "yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip" bad_files = [] with zipfile.ZipFile(zpath, "r") as zf: for info in zf.infolist(): try: with zf.open(info.filename) as f: f.read() except zipfile.BadZipFile: bad_files.append(info.filename) print(f"文件总数: {len(zf.infolist())}") print(f"损坏文件: {bad_files if bad_files else '无'}")

这段脚本先遍历压缩包内所有文件,逐个完整读入并校验 CRC。BadZipFile异常会捕获具体是哪个文件损坏。输出里同时给出文件总数,方便和标题声称的 1990 张图对照。如果文件总数和预期差异很大,说明数据可能被重新筛选过,训练前需要重新评估类别分布。

提示:zip 内文件名如果含中文,Python 在某些老版本上会遇到编码问题。遇到乱码时先尝试设置encoding="gbk"再重新打开。

2.2 目录结构和标注格式识别,先看清楚再写转换脚本

解压之后的下一步是确认标注格式。市面上的 YOLO 数据包常见的交付形态有三种,识别特征非常明显:

标注格式文件后缀关键特征对应目录常见形态
YOLO txt.txt每行 5 个数:class x_center y_center width height,全部归一化到 0-1images/ 与 labels/ 平级
VOC XML.xml<object><name>,<bndbox>节点JPEGImages/ 与 Annotations/
COCO JSON.json顶层含imagesannotationscategories三个数组单 JSON + 图目录

先用最轻量的命令看一眼:

unzip -l yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip | head -30

unzip -l只列出压缩包内容清单,不解压。从头部路径能看到 images 和 labels 的目录层级。再抽查一张标签文件:

unzip -p yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip "*/labels/*.txt" | head -3

-p直接把文件内容输出到标准输出,不落盘。一行里如果是 5 个数且都在 0 和 1 之间,基本可以确定是 YOLO 格式。如果看到<annotation>开头,则是 VOC 系。

这里有个坑洞场景特有的问题:标注方可能把同一类破损路面拆成多个名字,比如“坑洞”“坑槽”“沉陷”“网状裂缝”,甚至同一张图里两种叫法混用。YOLO 训练时类别名和类别 id 的映射一旦错位,损失函数会混乱。先统计压缩包内所有 txt 文件的首列取值分布,排查类别 id 是否连续且与数据说明一致:

unzip -p yolo算法-道路坑洞检测数据集-1990张图像带标签-坑洼.zip "*.txt" | awk '{print $1}' | sort | uniq -c

awk 取出首列,sort 和 uniq -c 输出每个类别 id 出现的次数。如果出现 0 和 1 之外的值,按 YOLOv8 的默认类别约定就需要做重映射。也顺便看一眼最大类别 id,超过 2 说明这个数据集里可能混了其他路面病害类型,后续训练计划要相应调整。

2.3 标注质量快检,坑洞的框最容易标在阴影里

确认格式之后,不能直接进入转换。标签质量和图像内容不匹配的问题,在道路坑洞数据里尤其突出:坑洞边界模糊、周围常有阴影和积水反光,标注员有时候会把矩形框的一部分切进阴影区域,或者把同一个坑洞拆成两个半框。只看 txt 的数字检查不出这种问题,需要把标注画回到图像上。

写一个快速可视化脚本,逐张检查前几十张图:

import cv2 import os label_dir = "labels" image_dir = "images" sample_files = os.listdir(label_dir)[:50] for label_name in sample_files: img_path = os.path.join(image_dir, label_name.replace(".txt", ".jpg")) img = cv2.imread(img_path) h, w = img.shape[:2] with open(os.path.join(label_dir, label_name), "r") as f: for line in f: cls, xc, yc, bw, bh = map(float, line.strip().split()) x1 = int((xc - bw / 2) * w) y1 = int((yc - bh / 2) * h) x2 = int((xc + bw / 2) * w) y2 = int((yc + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, str(int(cls)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imshow("check", cv2.resize(img, (1024, 576))) cv2.waitKey(0)

这个脚本把归一化坐标乘以图像宽高还原成像素坐标,再画红色框。注意waitKey(0)会逐张暂停,按任意键换下一张。检查重点有两个:框是否完全贴着坑洞边缘、有没有出现框内大部分是路面的情况。数据标注的偏差会直接影响后续训练的收敛方向,YOLO 的损失函数会把边界框回归到标注框的位置,如果标注本身就偏移 10 个像素,模型学出来的框也会跟着偏 10 个像素。

提示:如果标注文件里出现wh大于 1 的值,说明坐标没有归一化或者单位不是“相对于图宽高的比例”。这类标签要回退给标注方确认,不能靠转换脚本硬改,因为单纯把大于 1 的值除以宽高并不总是能还原标注意图。

3. 标注转 YOLO 格式与数据划分,这一步决定后面能不能省心

3.1 VOC 或 COCO 转 YOLO txt,坐标换算是第一道坎

如果压缩包里是 YOLO txt,这一节可以跳过。但很多“道路坑洞检测数据集”实际交付的是 VOC 或 COCO 标注,标题写“带标签”并没承诺格式。转换的核心公式是把 VOC 的xmin/ymin/xmax/ymax像素坐标换成 YOLO 需要的归一化中心点加宽高:

def voc_to_yolo(xmin, ymin, xmax, ymax, img_w, img_h): x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h return x_center, y_center, box_w, box_h

分子上的中心点计算之所以用(xmin + xmax) / 2而不是(xmax - xmin) / 2,是因为前者的坐标中心考虑了起始偏移,后者只算了框宽。这一步算错会导致所有框整体偏移半个身位。转换时如果原标注是四点多边形而不是矩形框,坑洞场景常见的做法是取最小外接轴对齐矩形,因为 YOLO 系列的检测头输出就是轴对齐框,多边形训练需要走 mask 分支或者用旋转框检测框架,超出了普通检测任务范围。

类别映射也要一并处理:

category_map = {"pothole": 0, "corrugation": 1, "rut": 2}

把原数据的类别名转换成目标数字 id。类别字典要单独保存为 yaml 文件,训练和验证共用一份,避免训练脚本和验证脚本各写一套导致 id 错位。

3.2 划分数据集不能随机抽,按采集来源分组

随机划分在坑洞检测场景是典型的错误做法。道路数据往往来自同一路段连续拍摄的视频帧,同一个坑洞可能出现在连续十几张图里,只是视角和距离略有变化。随机划分会把同一坑洞的相邻帧同时分进 train 和 val,验证集的 mAP 虚高,真实场景里的检测能力被高估。

常用做法是按文件名的采集批次前缀分组。如果文件名形如jpgrp104_05460.jpg,前缀jpgrp104代表一个采集批次,按前缀粒度划分:

from sklearn.model_selection import GroupShuffleSplit from pathlib import Path import yaml labels = sorted(Path("labels").glob("*.txt")) groups = [p.name.split("_")[0] for p in labels] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(labels, groups=labels, groups=groups))

GroupShuffleSplit的核心思想是整个批次进一边,不拆散。没有 sklearn 环境时,手动按前缀去重再抽样划分的效果也一样。划分完要落到文件清单或者直接移动文件:

mkdir -p dataset/images/train dataset/images/val dataset/labels/train dataset/labels/val

划分时注意后缀统一,有些数据集里 jpg 和 JPG 混用、txt 和 txt 混用,先在脚本里全部转成小写后缀再处理。

3.3 统计类别分布和目标尺寸,1990 张图里小目标可能占一半

坑洞数据有一个显著特点:小目标占比高。车载相机视角下,远处刚出现的坑洞可能只有整张图的 1% 面积,靠近后才会变大。YOLO 训练这类数据之前,统计目标尺寸分布能提前预判要不要调整imgsz或者切图策略。

from PIL import Image import os img_dir = "images" label_dir = "labels" area_ratios = [] cls_counts = {} for label_file in os.listdir(label_dir): img = Image.open(os.path.join(img_dir, label_file.replace(".txt", ".jpg"))) img_w, img_h = img.size total_pixels = img_w * img_h with open(os.path.join(label_dir, label_file), "r") as f: for line in f: cls, _, _, bw, bh = map(float, line.strip().split()) cls_counts[int(cls)] = cls_counts.get(int(cls), 0) + 1 area_ratios.append(bw * bh) small_obj = sum(1 for r in area_ratios if r < 0.01) print(f"类别分布: {cls_counts}") print(f"面积占比 < 1% 的目标数: {small_obj}/{len(area_ratios)}")

bw * bh是归一化面积,乘上total_pixels就能得到像素面积。面积占比小于 1% 的目标在 640x640 输入下大约只占 40x40 像素,属于小目标范畴,需要关注后续训练时的分辨率选择。

4. 用 YOLOv8 跑通训练,损失和评估指标要盯这么几个

4.1 最小训练命令和参数设定

数据集目录准备成 YOLOv8 期望的结构后,先写 data.yaml:

path: /path/to/dataset train: images/train val: images/val names: 0: pothole 1: corrugation 2: rut

path是数据集根目录的绝对路径,train 和 val 指向 images 子目录。跑训练前先把检查点模型下载好,用最小的 nano 模型验证整体流程:

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=100 \ batch=16 \ device=0 \ project=runs/pothole \ name=exp1
参数建议值说明
modelyolov8n.pt先小模型跑通,确认数据和流程没问题再换 s 或 m
imgsz640取决于小目标占比,小目标多时可上调到 1280,但显存占用会翻数倍
epochs1001990 张图建议至少 100,观察损失是否还在下降再决定是否续训
batch按显存调单卡 16 是起步值,显存不够就降到 8 或 4
patience30早停参数,连续 30 轮 val loss 不降就自动停止

为什么建议先 nano?因为数据集只有 1990 张,很多漏检和误检问题在 nano 模型上就会暴露,排查清楚再换大模型,效率远超直接用 m 训练然后面对一堆不确定是不是数据问题的结果。

4.2 坑洞场景下损失函数趋势怎么判断

YOLOv8 的损失由三部分组成:box_loss回归框位置、cls_loss分类置信度、dfl_loss分布焦点损失。训练日志长这样:

Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size 30/100 4.12G 1.102 1.245 1.003 46 640

坑洞数据的规律:cls_loss会很快降到 1.2 附近并进入平台期,因为坑洞和背景的纹理差异容易被卷积网络学出来;但box_loss会持续缓慢下降,框的位置精度对数据集标注质量和阳光阴影非常敏感。如果box_loss在 60 轮之后还在缓慢走低,不必急着早停,通常说明模型还在精修边界。如果cls_loss始终降不到 1.0 以下,大概率是类别混淆——坑洞和排水井盖、阴影的形状太像,需要从标注层面再检查是否有误标。

训练过程中把mosaic=1.0默认开着的增强用在坑洞数据上,好处是让小目标在拼接图中获得更多采样机会,代价是坑洞本身边界模糊,拼接切边可能把坑洞切成两半。训练后期把 mosaic 关掉做最后若干轮微调,常见做法是:

yolo detect train \ data=data.yaml \ model=runs/pothole/exp1/weights/last.pt \ imgsz=640 \ epochs=20 \ batch=16 \ device=0 \ mosaic=0.0

mosaic=0.0相当于关闭马赛克增强,让模型在完整的、未拼接的图像上做最后的边界回归调整,对坑洞这类边界不锐利的目标有明显帮助。

4.3 评估指标拆开看,mAP50 会骗人

训练完成后用验证集评估:

yolo detect val \ data=data.yaml \ model=runs/pothole/exp1/weights/best.pt

输出里同时给出 mAP50 和 mAP50-95。坑洞检测只看 mAP50 会高估模型实际能力:mAP50 只要求在 IoU 0.5 以上判断为正确,框偏移十几个像素仍然算命中,而道路巡检里框能不能贴住坑洞边缘是后续测量和修补的基础。mAP50-95 从 0.5 到 0.95 做了多档 IoU 平均,数值通常比 mAP50 低 20 到 30 个百分点,差距过大说明框的定位精度不够,优先检查标注质量而不是调阈值。

误检方面,当前场景最常见的两类是深色水渍被框成坑洞、以及接缝阴影误报。排查方法是在验证集上把conf_thres调低到 0.05,输出全部候选框,再按类别统计得分分布:

yolo detect val \ data=data.yaml \ model=runs/pothole/exp1/weights/best.pt \ conf_thres=0.05

低置信度检出的目标如果在真实道路上根本不存在,说明模型学到的是颜色特征而非结构特征,需要补充带阴影和积水反光的负样本强化数据集。

5. 用模型预测反向审计标注,把漏标挖出来再补一轮

1990 张图训练出第一版模型之后,不要急着部署。最值得做的一件事,是拿这个模型去预测训练集本身,找出“模型认为有坑洞、但标注文件里没有框”的区域。这类区域大概率是漏标的坑洞——人工标注时视线疲劳漏掉的小目标,在模型眼里反而很明显。

from ultralytics import YOLO import os import numpy as np model = YOLO("runs/pothole/exp1/weights/best.pt") label_dir = "labels" image_dir = "images" for img_name in sorted(os.listdir(image_dir))[:200]: img_path = os.path.join(image_dir, img_name) label_path = os.path.join(label_dir, img_name.replace(".jpg", ".txt")) gt_boxes = [] if os.path.exists(label_path): with open(label_path) as f: for line in f: parts = line.strip().split() if len(parts) >= 5: _, xc, yc, bw, bh = map(float, parts[:5]) gt_boxes.append([xc, yc, bw, bh]) results = model.predict(img_path, conf=0.25, imgsz=640, verbose=False) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() xc = (x1 + x2) / 2 / results[0].orig_shape[1] yc = (y1 + y2) / 2 / results[0].orig_shape[0] in_gt = False for gx, gy, gw, gh in gt_boxes: if abs(xc - gx) < max(gw, (x2 - x1) / results[0].orig_shape[1]): in_gt = True break if not in_gt: print(f"疑似漏标: {img_name} 置信度 {box.conf[0]:.2f}")

脚本思路是用训练好的模型对图像做推理,然后把预测框中心点与所有真值框做比对,中心点没有落在任何真值框范围内的预测视为疑似漏标。conf=0.25是经验值,漏标候选太多就调高,几乎没结果就调低。筛出的文件名清单可以回交给标注人员复核,这部分工作量远小于重新标注全量数据。

第二轮训练时,把之前高置信度疑似漏标的图像拿回来修正标注,再把 mosaic 关掉做最后 20 轮微调。这个流程跑完,模型对小坑槽的框通常会更贴边,验证集上的 mAP50-95 会有新一轮提升。

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

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

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

立即咨询