665张图片玩转目标检测:YOLOv8道路坑洼检测全流程实战
2026/8/26 8:39:45 网站建设 项目流程

简介:目标检测是计算机视觉的核心任务之一,其原理在于通过神经网络定位图像中目标的位置与类别。在工程实践中,数据规模往往成为制约模型落地的关键因素。对于道路坑洼检测这类场景,小型数据集反而具备迭代快、问题可控、适合学术验证等独特价值。YOLOv8作为当前主流的检测框架,凭借简洁的训练接口与优秀的性能平衡,成为快速验证算法思路的理想选择。本文基于一个665张标注图像的道路坑洼数据集,完整演示从数据标注格式转换、迁移学习训练策略到过拟合抑制、ONNX部署的闭环流程,为入门者与研究者提供一套可复现的小样本目标检测方法论。 去年做课程项目的时候,我拿到过一个名为“道路坑洼目标检测-665-label”的小型数据集。665张带边界框标注的道路图像,单类别pothole,没有额外的花哨内容。当时第一反应是这数据量也太寒酸了,但一轮迭代下来,反而觉得这种百级规模的小型数据集,正是目标检测入门、课程实验和学术研究里最真实的形态。很多人一上来就奔着几万张图的大数据集去,结果卡在资源、标注和训练时间上,反而不如把这么一套小型数据集吃透,把所有环节跑通,把模型行为分析明白。

这篇文章就以这个665-label道路坑洼数据集为线索,完整走一遍从数据准备、标注格式转换、YOLOv8训练,到结果评估和推理部署的全流程。适合刚接触目标检测的学生、准备做相关方向毕业设计的人,以及想用小数据集快速验证算法想法的人。里面会把参数为什么这么设、坑为什么这么踩讲清楚,你完全可以照着复现。

1. 这个665张图的数据集,到底能用来做什么

1.1 道路坑洼检测为什么值得做

道路坑洼检测在工程上是个很实际的需求。道路养护部门需要定期巡查路面破损情况,传统方式靠人工巡检,效率低且主观性强;自动驾驶系统需要提前识别前方路面异常,避免车辆受损或引发事故;再有就是共享出行、物流车辆的路况评估,坑洼路面对底盘和轮胎的损耗是实打实的成本。

从算法角度看,这是一个典型的单类别目标检测任务:在图像中定位坑洼的位置并画出边界框。它有几个特点:目标形态不规则,边界模糊,背景(沥青路面、阴影、裂缝)干扰严重,不同拍摄距离下目标尺度差异明显。这些特性决定了它不是那种随便跑跑就能拿到漂亮结果的简单任务,但又不至于像多类别检测那样需要复杂的类别关系建模,非常适合作为目标检测学习与研究的切入点。

1.2 小型数据集在学术研究和学习中的定位

665张图像在工业级项目里确实不够看,但在学习与学术研究场景下,这个规模有它独特的价值。

一是迭代速度快。500多张训练图,用YOLOv8n这种小模型,单张消费级显卡训练一轮只要几十秒,跑100个epoch也就一顿饭的工夫。这意味着你可以快速验证数据增强策略、超参数调整、模型结构改动带来的影响,而这种高频迭代恰恰是学习阶段最需要的。

二是问题可控。数据量小,模型拟合起来容易,出现的过拟合、漏检、误检等问题都相对干净,便于分析根因。不像大数据集,出了性能问题你很难定位是数据问题、模型问题还是训练策略问题。

三是学术研究的基准意义。很多论文在做小样本学习、数据增强有效性验证、域适应等方向的实验时,都需要这种规模适中的公开数据集作为评测基准。665-label的数据规模正好处于“够用但不过剩”的区间,既能看出算法差异,又不会因为数据量太大掩盖方法本身的问题。

1.3 拿到数据集后先做这几步检查

很多人拿到数据集第一件事就直接开训,这是最容易翻车的操作。我习惯先花十几分钟做一轮数据体检。

第一步,统计图像信息。用脚本遍历所有图像,看尺寸分布、通道数、是否有损坏图片。道路图像往往来自不同设备,尺寸可能不统一,YOLO训练前需要确认这一点。

第二步,检查标注框的分布。画出所有边界框的宽度、高度分布图,看看是否存在大量极小的框,或者异常超大框。这一步能提前暴露尺度问题。

第三步,确认标注类别和格式。单类别任务相对简单,但也要确认类别id从0开始,标注文件里没有空标签或多标签的情况。

第四步,划分训练集和验证集。这里要用随机划分,但必须保证同一条道路的连续帧不要既出现在训练集又出现在验证集,否则会高估模型性能。如果数据集是按视频抽帧得到的,最好按时间段或地点分组划分。

2. 从零准备数据:标注工具选型与格式转换细节

如果这个数据集是你自己采集图像做标注,或者你需要在现有数据上补充标注,工具选型就很重要了。我在这个项目里用的是Label Studio,它在处理目标检测标注任务时流程清晰,导出格式多样,社区活跃度也高。

2.1 用Label Studio完成坑洼框标注

Label Studio的安装很简单,一条命令的事:

pip install label-studio label-studio start

启动后在浏览器打开默认地址,创建项目,标注模板选择Object Detection with Bounding Boxes。把图像通过Data Manager上传后,就可以开始标注了。

标注坑洼有个实践经验:坑洼的边界往往不平整,不要试图让矩形框完美贴合边缘,抓住两个关键点即可——框住坑洼主体区域,包含明显的破损边界。如果坑洼被车辆遮挡了一部分,按可见部分框;如果一块区域是连片的坑洼群,拆成多个框而不是一个大框,这样模型的定位目标更明确。

标注完成后导出COCO格式。Label Studio会生成一个JSON文件,包含images和annotations两个主要字段,结构符合COCO标准,方便后续转换。

2.2 从COCO到YOLO格式的转换脚本

YOLO系列训练需要的是TXT格式标签,每行一个目标,格式为:class_id x_center y_center width height,坐标均归一化到[0,1]。而Label Studio导出的是COCO的JSON格式,边界框记录为[x, y, width, height]像素坐标。这里需要写一个转换脚本。

import json import os # 读取COCO格式标注 with open('annotations.json', 'r', encoding='utf-8') as f: coco = json.load(f) # 建立image_id到文件名的映射 image_id_to_name = {} for img in coco['images']: image_id_to_name[img['id']] = img['file_name'] # 建立类别id到类别名的映射 cat_id_to_name = {} for cat in coco['categories']: cat_id_to_name[cat['id']] = cat['name'] os.makedirs('labels', exist_ok=True) # 将每个图像的标注写出为YOLO格式 for ann in coco['annotations']: img_id = ann['image_id'] file_name = image_id_to_name[img_id] label_file = os.path.join('labels', file_name.replace('.jpg', '.txt').replace('.png', '.txt')) bbox = ann['bbox'] # [x, y, width, height] x_center = (bbox[0] + bbox[2] / 2) / coco['images'][img_id]['width'] y_center = (bbox[1] + bbox[3] / 2) / coco['images'][img_id]['height'] w_norm = bbox[2] / coco['images'][img_id]['width'] h_norm = bbox[3] / coco['images'][img_id]['height'] # 防止归一化后坐标越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w_norm = min(max(w_norm, 0.0), 1.0) h_norm = min(max(h_norm, 0.0), 1.0) # 打开文件追加写入,因为同一张图有多个标注 with open(label_file, 'a', encoding='utf-8') as f: class_id = list(cat_id_to_name.keys())[0] # 单类别场景 f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n")

转换完后必须做一步验证:把TXT标签重新叠画到原图上,肉眼确认目标位置和框大小是否正确。这一步能发现坐标错位、宽高颠倒、归一化分母用错等低级但致命的错误。

2.3 数据划分与目录组织

数据划分比例我建议8:2或7:3,像665张这种规模,用大约530张训练、135张验证是合理的选择。训练集还可以再切出极小一部分做早停参考,但YOLOv8的patience机制会基于val集判断,所以训练集和验证集分开就够了。

目录结构按YOLO惯例组织:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

图像和标签保持同名同目录层级,这是YOLO系列模型读取数据的默认约定。data.yaml的内容:

train: /absolute/path/to/dataset/images/train val: /absolute/path/to/dataset/images/val nc: 1 names: ['pothole']

注意train和val路径建议写绝对路径,避免相对路径在不同工作目录下产生的路径解析问题。

3. YOLOv8训练实操:配置、命令与参数选择

3.1 为什么要选YOLOv8,而不是更早的版本

目标检测模型迭代到今天,YOLOv8在易用性、训练稳定性和部署生态上达到了一个很舒服的平衡点。相比更早的YOLOv5,v8在C2f模块、Anchor-Free解耦头、TaskAlignedAssigner等设计上做了多处改进,训练更稳定,收敛更平滑,对小目标的检测能力也有提升。相比更新的v9、v10、v11,v8的文档、社区交流和工具链都更成熟,遇到问题更好查到解决方案。

Ultralytics团队把训练流程封装得极其简洁,几行代码就能跑起来,这大大降低了学习门槛。对于这个665-label项目来说,YOLOv8n这个nano版本是首选,模型参数量小,在这个规模的数据集上不容易过拟合,训练速度快,而且检测精度完全够用。

3.2 安装环境与最终目录检查

创建虚拟环境,然后安装ultralytics:

conda create -n pothole python=3.10 conda activate pothole pip install ultralytics

训练前最后检查一遍目录结构,确认images和labels两个文件夹下train、val子目录的文件是一一对应的。我写了个小脚本快速校验:

import os for split in ['train', 'val']: img_dir = f'dataset/images/{split}' lbl_dir = f'dataset/labels/{split}' img_files = set(os.listdir(img_dir)) lbl_files = set(os.listdir(lbl_dir)) img_stems = {f.split('.')[0] for f in img_files if f.endswith(('.jpg', '.png'))} lbl_stems = {f.split('.')[0] for f in lbl_files if f.endswith('.txt')} print(f'{split}: 缺少标签的图像 = {img_stems - lbl_stems}') print(f'{split}: 缺少图像的标签 = {lbl_stems - img_stems}') # 顺便检查是否存在空标签 empty_files = [] for lf in lbl_files: if os.path.getsize(os.path.join(lbl_dir, lf)) == 0: empty_files.append(lf) print(f'{split}: 空标签文件数 = {len(empty_files)}')

这一步能把文件缺失、空标签这类问题提前拦截掉。空标签文件在训练中会导致无目标loss的异常波动,最好直接把对应图像也从训练集移除,或者补标。

3.3 训练命令与关键参数解读

一切就绪后,训练命令如下:

yolo detect train \ model=yolov8n.pt \ data=dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ patience=30 \ project=runs/pothole \ name=exp1

逐个参数说明:

  • model=yolov8n.pt:加载YOLOv8n的COCO预训练权重。迁移学习的基础,非常重要。虽然COCO类别里没有坑洼,但模型从COCO学到的底层特征(边缘、纹理、形状)对坑洼检测依然有效。
  • epochs=100:小数据集上100轮足够。如果发现loss还在下降且val指标没有饱和,可以适当增加。
  • imgsz=640:标准训练分辨率。坑洼在道路图像中通常不是特别小的目标,640是比较合理的折中。
  • batch=16:根据显存调整。训练时显存占用大约在4~6GB,如果你的卡只有6GB显存,建议降到8。
  • patience=30:训练过程中如果连续30轮val指标没有提升,自动早停。这是防止过拟合的有效机制。

训练过程中要盯着两个关键指标:train loss是否持续下降,val set上的mAP@0.5是否仍在提升。如果train loss下降但val mAP在第40轮左右开始停滞甚至回落,那就是过拟合信号,应该提前终止训练,取最佳权重。

3.4 迁移学习的正确打开方式:冻结训练再解冻

这个小数据集上有个技巧特别有效:分两阶段训练。

第一阶段,冻结backbone骨干网络,只训练检测头。用命令里的freeze参数:

yolo detect train \ model=yolov8n.pt \ data=dataset/data.yaml \ epochs=30 \ imgsz=640 \ batch=16 \ freeze=10

freeze=10表示冻结模型前10层(YOLOv8n的backbone部分),只更新检测头参数。这样做能保留COCO预训练学到的底层视觉特征,避免小数据量下直接全量微调导致特征被破坏。

第一阶段结束后,加载得到的best.pt,解冻全部参数进行第二阶段的微调:

yolo detect train \ model=runs/pothole/exp1/weights/best.pt \ data=dataset/data.yaml \ epochs=70 \ imgsz=640 \ batch=16

两个阶段加起来正好100个epoch,但比从头直接训100轮稳定得多。实测下来,两阶段训练比一次性训练在val mAP@0.5上能高出3~5个百分点,而且收敛过程更平滑。

如果不想手动分两次跑,也可以用ultralytics的Python API在代码层面动态调整,但命令行的方式对新手更友好。

4. 小型数据集最容易踩的坑,我一个个排给你看

4.1 过拟合的判定与处理

过拟合在小数据集上是绝对的主角。我这次训练,train loss一路降到0.03附近,val loss却在前40轮下降后开始反弹,mAP@0.5在第55轮达到峰值约0.87,随后缓慢下滑,这是教科书式的过拟合形态。

处理手段优先级如下:

一是早停。YOLOv8的patience参数会自动保留val指标最好的权重,所以即使训练过拟合,best.pt仍然是峰值状态。这个机制一定要用起来。

二是数据增强。YOLOv8默认开了Mosaic、MixUp、HSV扰动、随机翻转等增强策略,但默认强度不一定是最优的。可以通过augment参数调整,或者手动设置hsv_h、hsv_s、hsv_v、degrees、translate、scale、fliplr等增强系数。对道路场景,我建议增加一些亮度对比度扰动(hsv_s: 0.5左右),因为不同天气和光照条件下坑洼的视觉表现差异很大。

三是降低模型复杂度。如果yolov8n还是过拟合,可以进一步减少训练分辨率,比如imgsz从640降到416,相当于减小输入维度,变相降低模型容量,缩短训练时间。或者加dropout,YOLOv8配置文件里可以修改dropout参数。

四是正则化。增加weight_decay系数,默认0.0005,可以试着调到0.001,稍微加大惩罚力度。

4.2 标注漏标与误标:模型会默默学会你的错误

小型数据集对标注质量极其敏感。我遇到过一个很典型的问题:模型在验证集的某些图上,对一个明显坑洼的区域给出了很低的置信度,甚至完全没检测出来。翻查原始图像才发现,这个坑洼在训练集同场景的相似图像里被漏标了。

模型学习的是标注里呈现的规律,漏标等于告诉模型“这个样子的路面不是坑洼”,这会直接压低模型在类似区域上的响应。误标同理,如果把阴影框成坑洼,模型就会学到“阴影=坑洼”。

所以训练完第一版后,一定要花时间做一轮标注质量复盘:把训练集里所有标注框和检测结果叠画出来,逐个检查有没有漏标和错标。650多张图看起来多,但用脚本批量生成标注预览图,过一遍也就半小时。这个时间花得非常值。

4.3 坑洼的尺度问题:小目标为什么总漏检

道路图像里坑洼的尺度变化很大。近距离巡检图像里坑洼可能占图像的三分之一,远距离车载视角下坑洼可能只有二三十个像素。YOLOv8在640分辨率下,对极端小目标(小于16×16像素)的检测能力是有限的,因为经过5次下采样后,这些小目标在特征图上只剩下不到2×2的像素,信息几乎丢失。

面对这种情况,有两个常用处理思路。

一是提高训练分辨率,用imgsz=960或1280,但这会增加显存占用和训练时间。如果数据集中小目标占比不高,收益可能不明显。

二是切图策略,把大图按滑动窗口切分成若干小块,分别检测,再把结果映射回原图坐标。对道路坑洼检测这种场景(目标局部分布)比较有效。但665张图本身就少,切图需要配合数据增强,不然又回到数据量不足的问题上。

我的建议是:先跑一版640分辨率的,统计val集上漏检目标的尺寸分布,如果漏检集中在超小目标上,再考虑针对性优化。不要一上来就为小目标问题做复杂设计。

4.4 背景误检:裂缝、沥青纹理和阴影怎么防

道路路面最烦人的就是背景干扰。沥青的随机纹理、路面裂缝、树影和桥墩阴影,都可能被模型识别成坑洼。我第一版模型在val集上出现了不少这类误检,PR曲线在低召回区间的表现很差。

处理背景误检的几个有效手段:

一是增加负样本。收集一些没有坑洼的道路图像加入训练集,但不标注任何目标。YOLO支持无标签图片作为背景样本参与训练,这会显著降低误检率。我加了几十张干净路面和阴影路面图像后,误检数量明显下降。

二是检查标注框是否包含了过多的背景区域。如果坑洼的边界框比实际坑洼大很多,框内大部分是正常路面,模型学到的是“坑洼周围也有坑洼”的错误认知,容易把正常路面的纹理当成坑洼特征。

三是调整置信度阈值。推理阶段把conf-thres从默认的0.25提高到0.35或0.4,可以过滤掉更多低置信度误检,但代价是可能漏掉一些真值,需要在precision和recall之间做权衡。

5. 评估结果和实际推理:别只盯着mAP

5.1 从训练日志里看懂模型真实水平

训练结束后,Ultralytics会在runs/pothole/exp目录下输出一堆结果文件。重点看三个:

  • results.png:训练过程的loss曲线和指标曲线,一眼能看出收敛情况和过拟合拐点。
  • val_batch0_pred.jpg:验证集预测结果可视化,叠画了检测框、类别和置信度,快速判断检测效果。
  • confusion_matrix.png:混淆矩阵,单类别下主要看pothole类的召回率和背景误检情况。

mAP@0.5是最直观的指标,但不要只看它。mAP@0.5:0.95对边界框位置的准确性要求更高,在坑洼检测这种需要精确定位的任务里更重要。我在这个项目上拿到了mAP@0.5约0.87、mAP@0.5:0.95约0.55的成绩,作为小数据集的基线已经够用。

5.2 用混淆矩阵和置信度分布找错误模式

混淆矩阵能告诉你模型把什么当成了什么。单类别检测里,最常见的两类错误是:把背景预测成坑洼(误检),把坑洼漏掉(漏检)。

我习惯再做一步置信度分布分析。用训练好的模型去预测验证集,把所有预测框按置信度排序,然后人工检查高置信度的错误框和低置信度的正确框。这个操作看似繁琐,但能非常直观地告诉你模型学得自信的错误是什么形态。比如我这次发现,模型对带明显边缘凹陷的阴影区域置信度很高,这提示我应该补充大量带阴影负样本。

命令行跑推理很简单:

yolo predict model=runs/pothole/exp/weights/best.pt \ source=test_images/ \ conf=0.3 \ save=True

conf参数调整置信度阈值,save=True把可视化结果保存下来。

5.3 导出ONNX,在边缘设备上跑实时检测

训练完成不代表项目结束,部署才是很多实际场景的终点。YOLOv8支持一键导出ONNX格式:

yolo export model=runs/pothole/exp/weights/best.pt format=onnx imgsz=640

导出后可以用ONNX Runtime跑推理,摆脱PyTorch环境依赖:

import cv2 import numpy as np import onnxruntime as ort # 加载ONNX模型 session = ort.InferenceSession('best.onnx', providers=['CPUExecutionProvider']) # 预处理 img = cv2.imread('test.jpg') img_resized = cv2.resize(img, (640, 640)) blob = img_resized[:, :, ::-1].transpose(2, 0, 1) # BGR转RGB,HWC转CHW blob = np.ascontiguousarray(blob, dtype=np.float32) / 255.0 blob = blob[np.newaxis, :, :, :] # 推理 outputs = session.run(None, {session.get_inputs()[0].name: blob})[0] # outputs形状为 (1, 84, 8400),8400是anchor点数量,84=4+1+80 # 但因为类别数只有1,实际输出通道是 4+1+1=6

如果目标是部署到Jetson、RK3588这类边缘设备,ONNX还可以进一步转为TensorRT或RKNN格式,进一步提升推理速度。以这个665-label数据集训练出的模型,在CPU上用ONNX Runtime处理单帧640×640图像大约需要80~120ms,在GPU上则能轻松达到实时。

最后再分享一个我自己的习惯:训练完模型后,挑几张验证集图像,把真实标注框和模型预测框画在同一张图上对比,放大后看边界框与实际坑洼边缘的贴合程度。这个细节比mAP数值更能反映模型的实际可用性。我自己用下来的体会是,把一个小数据集彻底读懂,比盲目刷大数据集更能训练出对目标检测技术的直觉——模型的强项、弱点、数据的偏见,全都明明白白地写在那些几百张图里了。

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

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

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

立即咨询