
简介面向道路养护与自动驾驶感知场景的坑洞目标检测数据集共收录665张真实道路图像并为每张图片提供VOC格式xml与YOLO格式txt两种标注文件类别统一为坑洞累计1740个矩形框。压缩包内共2000个文件以1332个txt、665个xml和少量jpg图片为主xml对应VOC标注txt对应YOLO归一化坐标可直接接入常见检测框架。整体约23.87MB便于快速下载、解压与多次迭代实验。已有412人学习下载。坑洞目标常存在尺寸不一、光照变化大、背景纹理复杂等难点该数据集由人工画框完成类别单一而框数较充足既可用于道路坑洞识别训练也适合开展数据增强、模型调参与精度对比对目标检测初学者、算法工程师及道路巡检项目前期验证均有实用价值也是学习标注格式转换与检测流程的非常合适素材。1. 花 665 张标注图搞定坑洞检测这份数据集到底能帮你省多少事做道路病害检测的人应该都有同感模型选型不是瓶颈数据才是。车载摄像头对着路面跑一圈能录下几千段视频可要变成能训练的标注数据集得逐帧挑选、画框、分类一天下来能标 200 张都算手快。所以我看到「道路坑洞目标检测数据集VOCYOLO格式665张1类别」这个包时第一反应是如果里面标注质量过关它能直接帮你绕过最痛苦的造数据阶段。665 张图对单类别检测来说不大但配合迁移学习足够验证算法流程、跑通 demo、甚至做设备端初步测试。这份数据集适合三种人刚入坑目标检测、想用现成数据熟悉 VOC/YOLO 两种格式差异的入门者正在做路面病害检测预研、需要快速验证模型选型的开发者以及想在小数据集上把训练、评估、迭代流程完整走一遍的团队。2. 打开压缩包VOC 与 YOLO 两种格式的真实结构2.1 解压后先别急着训练目录结构决定你的数据流拿到手先解压不要急着跑训练脚本。我吃过一次亏当时某开发者给我一个数据集压缩包我直接扔给训练脚本结果报错找不到标签文件。原因是他目录结构和常见约定不一致脚本里写死了路径。所以第一步永远是把目录树完整列出来看清根目录层级。常见做法是压缩包解压后出现两个平行目录一个是典型的 VOC 风格结构另一个是 YOLO 风格结构也可能两个格式共存于同一套目录里。VOC 格式的核心是三件套JPEGImages放原图、Annotations放对应的 XML 标注文件、ImageSets/Main放划分好的train.txt、val.txt列表文件。这里要注意ImageSets/Main里的 txt 只有图片文件名不带扩展名训练脚本靠它去JPEGImages和Annotations里找对应文件。而 YOLO 格式核心是images和labels两个目录同名图片和同名 txt 一一对应txt 每行是一个目标框。确认完目录结构后建议用tree命令或 Python 脚本统计一下图片数、标注数核对是否与标题声称的 665 张一致。我一般会写个三行脚本检查数量是否对得上避免后面训练时出现空标签报错。# 统计 JPEGImages 下图片数量 find JPEGImages -name *.jpg | wc -l # 统计 Annotations 下 XML 数量 find Annotations -name *.xml | wc -l # 统计 labels 下 txt 数量YOLO 格式每张图对应一个 txt find labels -name *.txt | wc -l逻辑说明三个数量应完全一致都是 665。如果 txt 数少于图片数说明部分图片没有标注对象单类别数据集中常见训练时这些无标签图片会被忽略但可能影响模型对负样本的感知如果多出来说明有脏文件。2.2 VOC 标注文件的真实面孔XML 里藏着哪些关键字段VOC 格式的本质是 XML 文件。虽然现在不少新框架直接吃 YOLO 格式但 VOC 依然是数据交换的通用语言很多标注工具导出的原生格式就是 VOC。打开一个 XML你会看到annotation根节点下主要有三块source描述图片来源size记录图片宽、高、通道数object列表记录每个目标框。训练时真正被读取的字段是size和object里的name、bndbox。annotation folderJPEGImages/folder filenameroad_001.jpg/filename size width1920/width height1080/height depth3/depth /size object namepothole/name bndbox xmin812/xmin ymin654/ymin xmax1093/xmax ymax831/ymax /bndbox /object /annotation逻辑说明注意size的值必须是原始图片的真实分辨率不能从缩略图或截图里取得否则后续所有坐标换算都会失真。bndbox里的四个值是像素坐标单位是整数像素xmin/ymin是框左上角xmax/ymax是右下角。参数说明XML 解析时要留意两类脏数据。第一类是坐标反了——某些标注工具或人工手误会产生xmax xmin的情况转换前要做排序归一。第二类是name字段带了多余空格或换行符比如pothole 分类时直接匹配不上我在脚本里一般会对所有name做strip()处理。另一点细节单类别数据集里类名可能并不统一有的标pothole有的标hole有的标pit。先用set()收集全部类名数量如果大于 1说明标注体系有噪音需要做类名映射归一——这是很多人容易跳过的一步。2.3 YOLO 标注的归一化坐标与 class_id 从 0 开始的约定YOLO 格式的标签是纯文本每行对应一个目标框五个字段用空格分隔class_id x_center y_center width height。注意四个坐标值全部是归一化的浮点数范围在 0 到 1 之间计算方式是目标框中心点坐标除以图片宽/高、框宽高除以图片宽/高。这个设计让标签与图片绝对分辨率解耦训练时无论输入尺寸怎么缩放坐标都能直接使用。对于这份数据集的单类别设定class_id固定为 0。0 0.49609375 0.68750000 0.14635417 0.16388889 0 0.72135417 0.35000000 0.11822917 0.17222222 0 0.25364583 0.83425926 0.09114583 0.12037037逻辑说明第一行表示类别 0框中心点位于图片宽度 49.6%、高度 68.75% 的位置框宽为图片宽的 14.6%、框高为图片高的 16.4%。反推像素坐标时乘回原图宽高即可。这里有个常见误解有人把 VOC 的xmin/ymin除以宽高当成 YOLO 坐标这是错的。VOC 给的是左上角和右下角YOLO 要的是中心点和宽高中间必须做一次换算x_center (xmin xmax) / 2width xmax - xmin再分别除以图片宽高。验证标签坐标是否合理的另一个方法是可视化画框。训练前把所有标注框画到图片上生成预览图肉眼扫一遍能发现大量单纯看数字发现不了的问题。常见做法是把标注文件路径和图片路径传入一个画框脚本用 OpenCV 的rectangle()绘制输出到预览目录后快速翻看。这一步治好了我无数个玄学报错——很多训练异常的根本原因其实是标签坐标在转换时发生了偏移。3. 从 665 张图到可训练状态校验、转换与划分3.1 训练前最该做的三件事校验图片、统计标签、定位脏数据不要直接拿压缩包开训。我的固定流程是三条脚本第一检查所有图片能否正常解码第二检查每张图的标签内容是否合法第三统计类别分布和框大小分布。第一件事尤其重要因为数据集经过压缩、传输、解压后极少数图片可能损坏或格式异常训练到一半才报错会浪费大量等待时间。import os from pathlib import Path from PIL import Image image_dir Path(JPEGImages) label_dir Path(labels) for img_path in sorted(image_dir.glob(*.jpg)): # 用 PIL 验证文件是否为完整图片 try: with Image.open(img_path) as img: img.verify() except Exception: print(f图片无法解码: {img_path.name}) continue # 重新打开获取真实尺寸 with Image.open(img_path) as img: width, height img.size label_file label_dir / (img_path.stem .txt) if not label_file.exists(): print(f缺少标签文件: {img_path.stem}) continue for line_no, line in enumerate(label_file.read_text().strip().splitlines(), 1): parts line.strip().split() if len(parts) ! 5: print(f{img_path.name} 第{line_no}行字段数不对: {line}) continue cls_id, xc, yc, w, h parts xc, yc, w, h map(float, (xc, yc, w, h)) if not (0 xc 1 and 0 yc 1 and 0 w 1 and 0 h 1): print(f{img_path.name} 第{line_no}行坐标超范围: {line}) # 还原像素坐标检查框是否超出图片边界 left (xc - w / 2) * width top (yc - h / 2) * height right (xc w / 2) * width bottom (yc h / 2) * height if left -5 or top -5 or right width 5 or bottom height 5: print(f{img_path.name} 第{line_no}行框超出图片边界)逻辑说明img.verify()只校验文件头和解码结构不加载像素数据速度很快随后重新open()是为了获取真实宽高。检查到问题后不要直接删除图片先收集列表再决定怎么处理。边界判断给 5 像素的容差因为有些标注工具会把框画得略微贴近边缘并非真正的标注错误。3.2 VOC 转 YOLO坐标换算的四个防御动作拿到手的数据集虽然宣称双格式都有但实际使用时依然建议自己跑一遍转换脚本。一方面是为了确认两套格式的框完全对应另一方面是训练框架之间有时只吃其中一种格式转换是必经步骤。VOC 转 YOLO 的核心公式不复杂真正要处理的是异常边界情况。下面这个脚本我每次转换都要用重点在防御逻辑。import xml.etree.ElementTree as ET from pathlib import Path def voc_to_yolo(xml_path, output_dir, class_list): tree ET.parse(xml_path) root tree.getroot() img_w float(root.find(size/width).text) img_h float(root.find(size/height).text) yolo_lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_list: continue class_id class_list.index(name) bndbox obj.find(bndbox) xmin float(bndbox.find(xmin).text) ymin float(bndbox.find(ymin).text) xmax float(bndbox.find(xmax).text) ymax float(bndbox.find(ymax).text) # 防御一坐标反了时交换 if xmin xmax: xmin, xmax xmax, xmin if ymin ymax: ymin, ymax ymax, ymin # 防御二裁剪到图片边界内 xmin max(0, min(xmin, img_w - 1)) xmax max(0, min(xmax, img_w - 1)) ymin max(0, min(ymin, img_h - 1)) ymax max(0, min(ymax, img_h - 1)) # 防御三过滤零宽高框 if xmax xmin or ymax ymin: continue x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 防御四归一化结果必须在 [0,1] if not (0 x_center 1 and 0 y_center 1): continue yolo_lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if yolo_lines: out_path Path(output_dir) / (Path(xml_path).stem .txt) out_path.write_text(\n.join(yolo_lines) \n) class_list [pothole] voc_dir Path(Annotations) yolo_dir Path(labels) yolo_dir.mkdir(exist_okTrue) for xml_file in voc_dir.glob(*.xml): voc_to_yolo(xml_file, yolo_dir, class_list)逻辑说明四个防御动作对应四种真实脏数据——坐标反序、框超出图片边界、零宽度/高度框、归一化后越界。前三种在人工标注的数据集中出现频率不低尤其多标注者协作时。输出时保留 6 位小数这个精度足够还原像素坐标又不会让文件体积膨胀。参数说明class_list的顺序决定了class_id顺序一旦确定就不要变动否则后续训练结果对不上。单类别时只有一个pothole但如果你后续要扩充类别比如增加裂缝、修补块重新转换前要把新增类名加到列表末尾不要插到中间否则已训练好的模型输出头对应关系会错位。3.3 训练集与验证集划分随机划分在坑洞数据上是错的665 张图怎么划分很多教程让你随机 8:2但坑洞数据一个典型特征是同一条路段的连拍帧之间极其相似——同一段路的坑洞从不同距离、不同角度拍了几十张。如果随机划分同一坑洞的相似图会同时出现在训练集和验证集里模型等于提前见过答案验证指标会严重虚高实测完全不是那么回事。划分时要按来源而不是按单张随机分。比如压缩包内的图片如果是按不同采集批次、不同路段存放的应该把整批文件划到同一侧。更稳妥的做法是按时间戳或文件名前缀分组后以组为单位做分层划分。from pathlib import Path import random image_dir Path(JPEGImages) all_images sorted(image_dir.glob(*.jpg)) random.Random(42).shuffle(all_images) train_ratio 0.8 split_idx int(len(all_images) * train_ratio) train_files all_images[:split_idx] val_files all_images[split_idx:] print(ftrain: {len(train_files)}, val: {len(val_files)}) # 按文件名的路段前缀分组而不是随机打乱 # 假设文件名格式为 road{id}_frame{seq}.jpg groups {} for img in all_images: prefix img.name.split(_)[0] groups.setdefault(prefix, []).append(img)逻辑说明如果文件名有明显的前缀表示拍摄路段或批次优先按组划分保证验证集里出现的路段在训练集中没有同源帧。如果实在无法判断来源才退回到随机划分并在心里对验证指标打个折扣。参数说明train_ratio在 665 张的规模下建议 0.8 到 0.85。验证集太小比如 100 张以下mAP 波动的置信区间会很大跑两次训练结果差 5 个点是常事。如果显存允许可以把验证集比例压到 0.15把更多数据投入训练。另有一种做法是 K-Fold 交叉验证665 张做 5 折每折 533 张训练、132 张验证能更准确评估模型稳定性代价是训练 5 次。4. 让模型在 665 张图上真的收敛迁移学习与关键参数4.1 为什么不能从零训练小数据集的算力账665 张单类别图片从零训练一个检测器过程会非常痛苦。目标检测模型通常包含 backbone特征提取、neck特征融合、head预测输出三部分其中 backbone 参数量占比最高。以常见的轻量级模型为例backbone 动辄上百万参数要从头学出通用的边缘、纹理、形状特征需要几十万张多样化图片。665 张图远远不够结果就是模型在训练集上疯狂记忆验证集上一塌糊涂。迁移学习是唯一的现实路径。用在大规模数据集上预训练好的权重作为初始值冻结 backbone 的大部分层只训练高层特征和检测头。这样做的原理是预训练模型已经学会了通用的视觉特征——边缘、颜色、纹理、局部形状这些特征对坑洞检测同样有效需要重新学习的只是坑洞这个类别在特征空间里长什么样这种高层语义。操作上常见的 YOLO 系列框架都支持--weights参数指定预训练权重比如在 COCO 上训好的权重。网络结构里类别的输出维度不同框架会自动把最后一层替换成新的类别数。695 张图配合预训练权重通常训练 100 到 200 个 epoch 就能收敛单卡显存 8G 足够。4.2 一次能跑通的最小训练命令拿到数据集并完成校验转换后训练脚本的核心是data.yaml和一条训练命令。data.yaml告诉训练框架数据在哪、类别有几类、名字是什么。常见做法是给数据集单独建一个目录把图片和标签按 train/val 分开放避免在命令里写一堆复杂路径。# data.yaml train: ./images/train val: ./images/val nc: 1 names: [pothole]# 用预训练权重做迁移学习输入 640 分辨率batch 16 python train.py --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --cache逻辑说明--weights yolov5s.pt指定从预训练权重起步这是训练能收敛的关键前提--cache表示把图片提前加载进内存665 张图的规模完全放得下能省掉每个 epoch 反复读取磁盘的时间。如果显存紧张把 batch 降到 8或者换更小的模型结构比如yolov5n.pt——坑洞不是极小目标轻量模型完全够用。参数说明--img 640是训练输入分辨率。如果原图是 1080p 或更高缩到 640 会丢失细节但训练速度和显存占用更友好。第一次训练先用 640 跑通全流程确认数据没问题后再尝试 1280 提升精度。--epochs的取值要看早停如果耐心观察 loss 曲线通常在 100 epoch 后趋于平缓200 epoch 是一个安全的上限。数据集太小训练过久会过拟合这不是多训就能解决的问题。4.3 imgsz、batch、anchor三个必须手动调的参数这三个参数在公共数据集上不敏感但在坑洞这种单类别小目标数据集上默认值往往不是最优。imgsz直接决定模型看到的目标有多大。坑洞本质上是中到小尺度目标如果按默认 640 输入远处坑洞可能只有十几个像素宽度特征提取阶段就丢失了信息。在显存允许的前提下我建议 640 跑通后优先试 1280。代价是训练时间几乎翻倍但小目标场景的增益通常是实打实的。另一个技巧是训练时用 640、验证时用 1280 的混合策略模型在相对低分辨率下学习评估时用高分辨率拿更高精度。batch影响梯度估计的稳定性。小数据集上 batch 太小比如 4每个 step 的梯度方向受单张图影响过大loss 曲线会剧烈抖动batch 太大比如 64需要降低学习率补偿而小数据集上学习率本来就难调。665 张图在 8G 显存下 batch 16 是合适起点每个 epoch 约 33 个 step梯度更新次数的量级足够模型充分学习。anchor锚框是最容易被忽略的。预训练权重的 anchor 是在 COCO 那 80 类目标的形状分布上算出来的而坑洞的形状和常见物体不同——它更偏扁椭圆或近圆形宽高比接近 1且绝对尺寸集中在图像宽度的 5% 到 30% 区间。用默认 anchor 会让预测框的初始形状偏离真实分布模型需要额外学习修正量。常见做法是在训练数据上用聚类算法重新计算 anchor再传给训练脚本。参数默认值坑洞场景建议理由imgsz640640 起步显存够用上 1280坑洞多属中小目标分辨率太低特征丢失严重batch8/1616 或 32665 张图规模下保证梯度稳定又不至于压爆显存anchor预训练默认按本数据集重新聚类COCO 目标的宽高比分布与坑洞差异大学习率0.010.005 或更低小数据集对学习率更敏感取低偏稳定5. 坑洞检测数据集避坑指南五个反复出现的训练坑5.1 训练中途报错无法读取图片解码失败和扩展名陷阱现象训练在跑到第 40 个 epoch 左右时突然中断报错信息指向某张图片无法解码或者提示图片通道数不对。第一次遇到很容易以为显卡不稳定、显存溢出实际上问题就出在某张损坏的图片上。当时我在某实验室帮 A 同学排查发现是标注工具导出的图片扩展名是 .jpg但内部编码实际是 PNG部分读取库能兼容另一部分直接报错。原因图片文件经过多次复制、压缩包传输后损坏或者标注工具导出时把不同编码格式统一改写成了 .jpg 扩展名。运行中才报错是因为数据加载是按顺序读取的坏图片恰好排在后面的 epoch 才被读到。解决训练前先跑一遍 3.1 节的图片完整性校验脚本把所有无法解码的图片查出来。对扩展名与编码不符的图片统一用 PIL 重编码为标准 JPEG 格式from PIL import Image from pathlib import Path img_dir Path(JPEGImages) for img_path in img_dir.glob(*.jpg): try: with Image.open(img_path) as img: img.load() # 重编码为标准 RGB JPEG if img.mode ! RGB: img img.convert(RGB) img.save(img_path, JPEG, quality95) except Exception: print(f损坏: {img_path.name})5.2 只剩 1 个类别训练脚本却报类别数不匹配现象data.yaml 里nc: 1训练在读取标签时却报错说发现标签中出现了class_id 1或更大的值或者训练能跑但输出结果里出现两个分类第二个分类一片空白。原因标注工具导出的类别编号习惯不同——有些工具从 0 开始编号有些从 1 开始转换脚本里如果用class_list.index(name)本应生成 0但如果 XML 里有标注工具额外写出的name字段比如空字符串或带前缀的类名就可能映射成错误编号。解决训练前遍历所有标签文件统计出现的最大类别编号用一句命令确认它在合法范围内。如果确实有越界数据写个小脚本把所有标签里的非法 class_id 过滤掉或修正为 0# 查看所有标签里的类别编号分布 cat labels/*.txt | awk {print $1} | sort -n | uniq -c5.3 验证集 mAP 挺高现场路测却频繁漏检现象验证集上 mAP 达到 0.75 以上看起来不错跑车载视频实拍中远距离的坑洞大量漏检或者只检到跟前的大坑小坑全部错过。这类现象在道路病害检测里太常见了。原因两个因素叠加。第一是划分不合理——验证集和训练集包含同路段的连拍画面模型相当于见过近似答案指标虚高第二是坑洞数据集天然存在尺度偏差——标注框大多集中在图像下半部分的中近处远处小坑标注既少又小模型没有足够样本学到远距离小目标的特征。解决按 3.3 节的思路重新划分数据集按路段/采集批次分组保证验证集与训练集没有同源帧。对尺度偏差单独建一个远距离测试集把图像距离较远的坑洞样本抽出来单独评估。如果这部分 mAP 明显低于总体指标说明模型对小目标确实没学好回到 4.3 节把输入分辨率调上去并考虑加大数据增强中的尺度扰动。5.4 沥青裂缝被当成坑洞标注噪声的连锁反应现象模型预测时把细长的沥青裂缝、路面水渍阴影、甚至桥面伸缩缝都框成坑洞。误检区域集中出现真伪比例接近 1:1让人怀疑模型到底学会了什么。原因数据集本身存在标注噪声——有些标注者把裂缝与坑洞混标在一个框里或者把大裂缝局部标成坑洞。模型学习到的是深色、细长的路面损伤区域这个模式而不是凹陷形态的坑洞这个更准确的模式。此外坑洞和裂缝在灰度特征上确实高度重叠单靠视觉特征本来就难以区分。解决第一步是清洗标签。写一个可视化脚本把每张图的标注框画出来并生成预览人工快速浏览把明显是裂缝的样本剔除或者把它们归为一个新的crack类别。如果不想增加类别至少要将这些样本从训练集中移除。第二步是在数据层面增强对非坑洞路面区域的采样——多收集坑洞附近完全正常的沥青路面图作为负样本让模型见过更多不像坑洞的干扰项。import cv2 from pathlib import Path image_dir Path(JPEGImages) label_dir Path(labels) out_dir Path(preview) out_dir.mkdir(exist_okTrue) for img_path in list(image_dir.glob(*.jpg))[:50]: # 先预览前50张 img cv2.imread(str(img_path)) h, w img.shape[:2] label_file label_dir / (img_path.stem .txt) if not label_file.exists(): continue for line in label_file.read_text().strip().splitlines(): cls_id, xc, yc, bw, bh map(float, line.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), 3) cv2.putText(img, fpothole_{cls_id}, (x1, max(0, y1 - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) cv2.imwrite(str(out_dir / img_path.name), img)5.5 训练 loss 不降反升学习率与增强参数的锅现象训练刚开始 loss 正常下降到第 20 到 30 个 epoch 时突然反弹之后在偏高位置震荡验证集 mAP 也随之下滑。看起来像是模型学坏了但代码和数据集都没改动。原因原因写learning rate 过大训练到中后期梯度更新步长超过了损失曲面的窄谷导致参数在最优解附近反复振荡另一重因素是数据增强过猛——像 Mosaic 增强会把多张图拼成一张而 665 张小数据集中大量是沥青路面的深色图像拼接出来的样本颜色分布偏差大模型被大量黑块干扰学不到有效特征。解决遇到 loss 反弹第一反应是把初始学习率调低一个量级比如从 0.01 降到 0.005 或 0.001第二是关闭或减弱 Mosaic 增强。我的经验是坑洞这种纯色背景、目标集中在中下部的数据Mosaic 的拼图效果不如在通用目标检测里那么正面。另一个技巧是余弦衰减配合预热前几个 epoch 用低学习率让模型稳定下来再逐步升高再衰减能有效缓解中期反弹。# 关闭 Mosaic 增强降低学习率重训 python train.py --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 200 \ --lr0 0.005 \ --mosaic 0.06. 把 665 张图的价值榨干增强策略与难例挖掘6.1 适合坑洞的增强组合克制比猛烈更重要图像增强是弥补小数据集样本不足的第一手段但坑洞数据的增强要讲究边界。坑洞是路面上的物理凹陷检测时相机通常正对或斜向路面目标不会出现大幅旋转旋转增强如果开到 90 度会制造出大量现实中不存在的倒立坑洞模型反而学到错误的空间先验。我的增强配置思路轻度 HSV 扰动模拟不同光照条件、中低强度的尺度缩放模拟远近变化、Copy-Paste 增强把坑洞从一张图复制到另一张图的随机位置——用标注的透明度较直接提高有效样本数。平移可以用坑洞在路面上的位置分布本身比较随机旋转最多开 10 度以内。Mosaic 建议先从 0 开始跑完一轮基准再逐步加回来而不是默认开到 1.0。6.2 难例挖掘把路测误检变成下一轮训练数据数据集只有 665 张但路测视频可以无限拍。模型在真实场景中的误检和漏检恰恰是比现有数据集更宝贵的训练材料。做法很简单车载视频抽帧用训练好的模型做推理把置信度落在 0.3 到 0.6 的不确定框抽出来。这些框可能有一半是错误的但把它们单独保存人工快速重新标注一版区分真坑洞、裂缝、阴影再合并进原数据集做增量训练。一轮下来模型对难样本的鲁棒性通常有明显提升。这部分工作不需要一次做完每轮增量加 100 到 200 张难例就够保持迭代节奏比一口气集齐所有数据更有效率。别把标注工作只看作成本它在难例场景下是持续提升模型上限的唯一路径。6.3 验证习惯先用一张图过拟合再谈全量训练我自己的固定习惯是任何新数据集到手上先用一张图做冒烟测试。把训练集临时改成单张图batch 设为 1跑 50 个 epoch如果 loss 不能降到接近 0说明数据处理链路或模型配置有问题——要么标签解析不对要么数据加载出错。这一步跑通了再切回全量数据正常训练能省下大量排错时间。另一个习惯是把验证集的预测结果画框导出成视频肉眼看过再下结论而不是只看 mAP 数字。665 张图的数据集虽然小但只要数据清洗到位、迁移学习用对、增强克制且难例持续补充它完全能撑起一个可用的坑洞检测原型。希望这篇笔记能让你少踩几个标注数据和训练调参的坑把精力留在真正影响落地效果的地方。本文还有配套的精品资源点击获取