简介:这份资源面向计算机视觉学习者与道路安全检测方向的开发者,提供一套基于YOLOv8实现路面坑洼识别的完整项目方案,涵盖从数据准备、模型训练到推理评估的全流程,适合具备一定Python与深度学习基础、希望动手实践目标检测的读者参考。压缩包共10个文件,约170.66MB,包含4个Python脚本(训练、测试、验证与预测)、2个pt模型权重、1份依赖清单、1段演示视频、1份说明文档及1张效果图,结构清晰便于快速上手。目前已有423人学习下载。项目说明详细记录了数据集信息、训练参数与结果分析,配合训练好的权重可直接用于坑洼检测推理,帮助读者理解YOLOv8在真实道路场景中的特征提取、非极大值抑制与mAP评估等关键环节,也可为交通标志识别、路面破损评估等同类任务提供可复用的工程参考。
1. 路面坑洼检测为什么值得用 YOLOv8 重做一遍
市政巡检的朋友跟我吐槽过一件事:他们之前用传统图像处理做路面坑洼检测,阈值调了三个月,晴天勉强能用,一到阴天或者路面有积水,误报率直接飙到七成以上。后来换成 YOLOv8 重新训练,同样的数据量,mAP50 从 0.61 拉到 0.87,推理速度在 GTX1660Ti 上单张只要 12ms 左右。这个差距不是调参调出来的,是检测范式变了。
路面坑洼检测本质上是一个小目标、类内差异极大的检测任务。坑洼的形状从细长裂缝到不规则凹陷都有,颜色从深黑到灰白不等,还经常和井盖、修补痕迹、阴影混淆。YOLOv8 的 Anchor-Free 解耦头加上 Task-Aligned Assigner 正样本分配策略,对这种形态多变的目标天然比 YOLOv5 的 Anchor-Based 方案更友好。这套方案适合谁?做市政巡检系统集成的工程师、想拿检测项目练手的学生、需要快速验证路面病害检测可行性的团队。下面从数据准备到模型训练再到推理部署,把整条链路拆开讲。
2. 从 Labelme 标注到 YOLOv8 数据集:格式转换与目录结构
2.1 为什么路面坑洼数据更适合 YOLO 格式而不是 COCO
Labelme 标注出来的是 JSON 文件,每个多边形点集对应一个坑洼区域。COCO 格式虽然通用,但它的 JSON 结构嵌套深,解析慢,而且 YOLOv8 原生训练接口直接吃 YOLO txt 格式,省去中间转换环节。YOLO 格式每行是class_id x_center y_center width height,全部归一化到 0~1 之间。对于坑洼检测这种单类别或者少类别任务,YOLO 格式的文件体积小、读取快,一个 5000 张图的数据集,标注文件总共不到 2MB。
常见做法是先用 Labelme 标注,然后写脚本批量转成 YOLO 格式。我一般会保留原始 JSON 备份,转换后的 txt 单独放一个目录,方便后面排查标注问题。
2.2 转换脚本与目录组织
import json import os from pathlib import Path def labelme_to_yolo(json_dir, output_dir, class_names): """ 将 Labelme JSON 转为 YOLO txt 格式 json_dir: Labelme JSON 文件目录 output_dir: 输出 txt 目录 class_names: 类别名列表,如 ['pothole'] """ os.makedirs(output_dir, exist_ok=True) for json_file in Path(json_dir).glob('*.json'): with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_names: continue cls_id = class_names.index(label) points = shape['points'] # 计算多边形外接矩形 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # 归一化中心点和宽高 x_center = (x_min + x_max) / 2.0 / img_w y_center = (y_min + y_max) / 2.0 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h # 过滤掉宽高为0的无效标注 if width <= 0 or height <= 0: continue lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") # 写入同名 txt txt_path = Path(output_dir) / (json_file.stem + '.txt') with open(txt_path, 'w') as f: f.write('\n'.join(lines)) # 调用示例 labelme_to_yolo( json_dir='./raw_annotations', output_dir='./labels', class_names=['pothole'] )这段脚本的核心逻辑是:读取 Labelme JSON 中的多边形点集,取外接矩形,再归一化成 YOLO 需要的中心点加宽高格式。class_names参数决定了类别索引,如果你有多个类别比如['pothole', 'crack'],索引 0 对应 pothole,索引 1 对应 crack。width <= 0的过滤是为了防止标注时误点产生的零面积框,这种框在训练时会引发 NaN 损失。
2.3 数据集划分与 data.yaml 配置
转换完成后,按 8:1:1 划分训练集、验证集、测试集。目录结构建议这样组织:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yamldata.yaml的内容:
path: ./dataset train: images/train val: images/val test: images/test nc: 1 names: ['pothole']nc是类别数,names是类别名列表。注意path用相对路径时,YOLOv8 会相对于data.yaml所在目录解析。如果图片和标签不在同一级目录,YOLOv8 默认会把images替换成labels去找标签文件,所以目录名必须严格对应。
提示:划分数据集时一定要用随机种子固定,否则每次跑出来的验证集不一样,指标没法对比。我一般用
random.seed(42)然后 shuffle 后再切分。
3. YOLOv8 训练参数怎么设:从 CPU 环境到 GPU 微调
3.1 Ubuntu 20.04 下 CPU 版本环境搭建
有些场景没有 GPU,比如在 RK3588 板端做前期验证,或者开发机就是普通笔记本。CPU 版本跑 YOLOv8 训练虽然慢,但做小规模数据集验证完全够用。安装步骤:
# 创建虚拟环境 python3 -m venv yolov8_env source yolov8_env/bin/activate # 安装 PyTorch CPU 版本(Ubuntu 20.04 默认 Python 3.8) pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装 ultralytics pip install ultralytics # 验证安装 yolo checksyolo checks会输出环境信息,包括 PyTorch 版本、CUDA 是否可用。CPU 版本会显示CUDA: None,这是正常的。如果后面要切到 GPU,直接重装对应 CUDA 版本的 PyTorch 即可,ultralytics 不用动。
3.2 训练命令与关键参数解读
yolo detect train \ data=./dataset/data.yaml \ model=yolov8n.pt \ epochs=200 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ patience=50 \ device=0 \ workers=8 \ project=./runs/pothole \ name=exp1逐项说明:model=yolov8n.pt用的是 nano 版本,参数量 3.2M,适合边缘部署;如果精度不够可以换yolov8s.pt或yolov8m.pt。imgsz=640是输入分辨率,坑洼目标普遍偏小,如果显存够建议上到 800 或 960,小目标召回会明显提升。lr0=0.01是初始学习率,lrf=0.01是最终学习率因子,YOLOv8 默认用余弦退火,从 0.01 降到 0.0001。patience=50表示 50 轮没有提升就早停,这个值别设太小,坑洼数据容易在 80 轮左右才出现明显下降。
batch=16在 8GB 显存上跑 640 分辨率刚好,如果 OOM 就降到 8 或者开amp=True混合精度。workers=8是数据加载线程数,CPU 核心少就降到 4,不然反而拖慢。
3.3 低显存和 CPU 训练的降级策略
GTX1660Ti 只有 6GB 显存,跑yolov8m加 640 分辨率会爆。我一般用两个策略:一是冻结主干前 10 层,freeze=10,只训练检测头,显存占用能降 40% 左右;二是用yolov8n加imgsz=512,精度损失大概 2 个点,但速度翻倍。CPU 训练的话,把batch降到 4,workers降到 2,epochs加到 300,用时间换精度。
# 低显存配置示例 yolo detect train \ data=./dataset/data.yaml \ model=yolov8n.pt \ epochs=300 \ imgsz=512 \ batch=4 \ freeze=10 \ device=0 \ amp=Truefreeze=10冻结的是 backbone 的前 10 层,这些层学的是通用特征,坑洼检测和 ImageNet 的底层特征差异不大,冻结后收敛更稳。amp=True开启自动混合精度,显存占用再降 20%,速度提升 15% 左右,精度几乎无损。
4. 训练过程排查与调优:损失曲线和验证指标怎么看
4.1 损失函数曲线的正确读法
YOLOv8 训练完会在runs/pothole/exp1/下生成results.csv,里面记录了每轮的 box_loss、cls_loss、dfl_loss 和 mAP 指标。用 pandas 画出来:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/pothole/exp1/results.csv') df.columns = df.columns.str.strip() fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].plot(df['epoch'], df['train/box_loss'], label='train') axes[0].plot(df['epoch'], df['val/box_loss'], label='val') axes[0].set_title('Box Loss') axes[0].legend() axes[1].plot(df['epoch'], df['train/cls_loss'], label='train') axes[1].plot(df['epoch'], df['val/cls_loss'], label='val') axes[1].set_title('Cls Loss') axes[1].legend() axes[2].plot(df['epoch'], df['metrics/mAP50(B)'], label='mAP50') axes[2].plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') axes[2].set_title('mAP') axes[2].legend() plt.tight_layout() plt.savefig('training_curves.png', dpi=150)正常情况:train loss 和 val loss 同步下降,最后趋于平稳。如果 train loss 继续降但 val loss 开始升,说明过拟合,需要加数据增强或者减模型容量。如果两个 loss 都震荡不降,检查学习率是不是太大,或者标注文件有没有问题。
4.2 验证指标异常时的排查顺序
mAP50 低于 0.5 时,按这个顺序查:第一,看标注框是不是把整个路面都框进去了,坑洼检测最常见的标注错误就是框太大,把正常路面也包进去,模型学不到判别特征。第二,看验证集和训练集的分布是不是差太多,比如训练集全是白天,验证集全是夜间,这种分布偏移会让指标崩掉。第三,看imgsz是不是太小,坑洼在 640 分辨率下可能只有 20x20 像素,上到 960 再试。
# 用验证集跑一次评估,看每类的 AP yolo detect val \ model=runs/pothole/exp1/weights/best.pt \ data=./dataset/data.yaml \ imgsz=960 \ batch=8输出里会分列mAP50和mAP50-95,如果mAP50高但mAP50-95低,说明框的位置不够准,可以调box损失权重或者加 DFL 的 reg_max 参数。如果两个都低,那就是特征没学好,换大模型或者加数据。
4.3 数据增强参数的针对性调整
YOLOv8 默认开了 mosaic、mixup、hsv 增强。坑洼检测里,mosaic 增强有时候会把坑洼拼到奇怪的位置,导致模型学到错误的上下文关系。我一般把mosaic=0.5降低概率,mixup=0.1几乎关掉,hsv_h=0.015、hsv_s=0.7、hsv_v=0.4保持默认,因为路面颜色变化本来就大,HSV 增强是合理的。flipud=0.0和fliplr=0.5,上下翻转对路面没意义,左右翻转可以保留。
yolo detect train \ data=./dataset/data.yaml \ model=yolov8s.pt \ epochs=200 \ imgsz=800 \ mosaic=0.5 \ mixup=0.1 \ fliplr=0.5 \ flipud=0.0 \ hsv_h=0.015 \ hsv_s=0.7 \ hsv_v=0.4这些参数没有绝对的最优值,但坑洼检测这个任务里,降低 mosaic 和 mixup 的收益比较稳定,我试过三组数据,mAP50 平均能涨 1.5 到 2 个点。
5. 避坑指南:路面坑洼检测训练中最容易翻车的 5 个点
5.1 标注框把阴影当成坑洼
现象:模型在验证集上把树荫、电线杆阴影都框成坑洼,误报率极高。原因:Labelme 标注时没有区分阴影和真实坑洼,阴影区域在灰度上和浅坑洼接近。解决:标注规范里明确写清楚,只有路面结构层破损、有明显凹陷或碎裂的才算坑洼,阴影、水渍、修补痕迹单独标成ignore类别或者直接不标。训练时把ignore类别的损失权重设为 0。
5.2 验证集 mAP 虚高但实际推理效果差
现象:训练日志里 mAP50 到 0.9 了,但拿手机拍几张路面照片推理,一个坑洼都检不出来。原因:训练集和验证集来自同一批数据,分布太单一,模型过拟合了采集设备的光照和角度。解决:验证集必须留一部分来自不同路段、不同时间、不同设备的数据。如果实在没有,至少做一次跨数据集验证,用公开的路面病害数据集跑一遍。
5.3 CPU 训练时 DataLoader 卡死
现象:在 Ubuntu 20.04 CPU 环境下跑训练,进度条卡在第 0 轮不动,CPU 占用 100% 但显存没变化。原因:workers设太大,CPU 核心不够,数据加载进程互相抢资源。解决:把workers降到 2 或者 0,batch降到 4。如果还卡,检查data.yaml里的路径是不是绝对路径,相对路径在 CPU 环境下有时候解析会出问题。
5.4 模型导出 ONNX 后推理结果和 PyTorch 不一致
现象:PyTorch 下推理正常,导出 ONNX 后用 onnxruntime 跑,框的位置偏移了几十个像素。原因:YOLOv8 导出 ONNX 时默认做了动态轴设置,如果推理时输入的尺寸和导出时不一致,后处理里的 anchor 解码会出错。解决:导出时固定imgsz,推理时严格用同样的尺寸。
yolo export model=best.pt format=onnx imgsz=640 opset=12 simplify=Truesimplify=True会做图优化,去掉冗余算子,opset=12兼容性最好。推理时用cv2.resize把图缩到 640x640,不要用 letterbox 之外的填充方式。
5.5 小目标坑洼在 640 分辨率下漏检严重
现象:宽度小于 30 像素的坑洼几乎全漏。原因:YOLOv8 的 P3 特征图 stride 是 8,640 输入下 P3 是 80x80,一个 20 像素的坑洼在 P3 上只有 2.5 个格子,特征太弱。解决:把imgsz提到 960 或 1280,P3 变成 120x120 或 160x160,小目标特征明显增强。如果显存不够,用yolov8n加 960,比yolov8m加 640 的小目标召回高 8 个点左右。
6. 推理部署与效果验证:从 PyTorch 到 RK3588 的最后一公里
训练完拿到best.pt只是第一步,真正落地要看推理端。我一般分两步验证:先在 PC 上用 PyTorch 跑一遍测试集,确认指标和训练日志一致;再导出 ONNX 或者 RKNN,在目标硬件上跑实际视频流。
PC 端推理脚本:
from ultralytics import YOLO import cv2 model = YOLO('runs/pothole/exp1/weights/best.pt') cap = cv2.VideoCapture('test_road.mp4') while cap.isOpened(): ret, frame = cap.read() if not ret: break results = model(frame, imgsz=960, conf=0.4, iou=0.5) annotated = results[0].plot() cv2.imshow('Pothole Detection', annotated) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()conf=0.4是置信度阈值,坑洼检测建议设 0.35 到 0.45 之间,太低误报多,太高漏报多。iou=0.5是 NMS 的 IoU 阈值,坑洼之间重叠少,0.5 够用。
如果要在 RK3588 上部署,流程是best.pt -> ONNX -> RKNN。RKNN 工具链对 YOLOv8 的支持已经比较成熟,但有几个参数必须注意:量化时用dataset.txt做混合量化,里面放 200 张左右的代表性路面图片;mean_values和std_values要和训练时一致,YOLOv8 默认是0-255输入,不做归一化;target_platform填rk3588。量化后 mAP 掉 1 到 2 个点是正常的,如果掉超过 5 个点,检查量化数据集是不是和训练集分布差太远。
验证方法上,我习惯用一段 1080p 的路面巡检视频,分别跑 PyTorch 和 RKNN,逐帧对比检测框。如果 RKNN 的框位置偏移超过 5 个像素,大概率是量化时的尺度因子没对齐。这时候把quantized_dtype改成w8a8再试,或者对检测头部分不做量化。
最后说一个我踩过的坑:导出 ONNX 时忘了加simplify=True,结果 RKNN 转换时报了一堆不支持的算子,折腾了一下午。后来养成习惯,导出命令里simplify=True和opset=12是标配,再也没翻过车。希望帮到你。
本文还有配套的精品资源,点击获取