基于YOLOv5的坑洼道路检测:从竞赛到工程实践全解析

📅 2026/8/27 10:21:13
基于YOLOv5的坑洼道路检测:从竞赛到工程实践全解析
1. 项目概述从竞赛题目到工程实践去年带队做MathorCup的A题“坑洼道路检测和识别”感触很深。这题目乍一看是个典型的计算机视觉问题但内核却是一次对数学建模思维和工程化能力的综合大考。它要求参赛者从零开始构建一个能自动识别路面坑洼的完整系统从数据预处理、特征工程到模型选择、训练调优最后还要输出可解释的结果。这不仅仅是调几个API那么简单背后涉及图像处理、模式识别、机器学习乃至一点点统计学的交叉应用。很多新手队伍一上来就直奔YOLO、Faster R-CNN这些时髦的模型结果往往在数据清洗和特征构造阶段就栽了跟头模型效果惨不忍睹。这篇文章我就结合当时的解题思路和后续的工程化复盘把这道题的完整建模过程、核心代码实现以及那些容易踩的“坑”系统地梳理一遍。无论你是正在备战类似数模竞赛的学生还是对计算机视觉实际应用感兴趣的开发者相信这份从实战中沉淀下来的“全解全析”都能给你带来直接的参考价值。2. 赛题核心与解题思路拆解2.1 问题定义与目标解析题目“坑洼道路检测和识别”的核心任务非常明确给定一系列道路图像要求算法能够定位图像中的坑洼区域即输出其位置如边界框并尽可能对坑洼的严重程度或类型进行分类识别。这本质上是一个目标检测Object Detection任务并可能附带一个细粒度分类Fine-grained Classification的子任务。在数学建模的语境下我们需要将这一工程问题抽象为可量化的数学模型。目标函数通常是最大化检测的准确率Precision和召回率Recall其综合指标如F1-Score同时最小化定位的误差如边界框的IoU损失。约束条件则包括计算资源如有限的训练时间、GPU内存、数据的不平衡性坑洼区域远小于正常路面以及图像的多样性不同光照、天气、拍摄角度。解题思路的顶层设计遵循“数据→特征→模型→评估→优化”的经典流程。但关键在于每一步都需要针对“坑洼”这一特定目标进行定制化思考而不是套用通用模板。2.2 技术路线选型与权衡面对目标检测任务主流技术路线有三条传统的基于手工特征的方法如HOGSVM、两阶段的基于区域提议的深度学习模型如Faster R-CNN、以及单阶段的端到端深度学习模型如YOLO系列、SSD。对于MathorCup这类竞赛时间紧、数据量通常不会特别巨大但要求有较好的效果和一定的创新性。传统方法虽然速度快、可解释性强但在复杂场景下的泛化能力较弱很难达到高分。两阶段模型精度高但速度慢训练和调参复杂度也高。因此以YOLOv5或YOLOv8为代表的单阶段检测器成为了绝大多数优秀方案的选择。它们实现了速度与精度的较好平衡且社区活跃预训练模型和调优技巧丰富。我们的选择是YOLOv5。理由如下首先其PyTorch实现非常成熟部署简单其次它提供了s小、m中、l大、x超大不同规模的模型可以方便地在模型性能和计算成本之间做权衡最后其内置的数据增强、超参数进化等功能能极大提升我们在有限数据下的训练效率和最终效果。注意模型选型不是一成不变的。如果竞赛提供的计算资源极其有限如只有CPU可能需要考虑更轻量的模型如YOLOv5s甚至MobileNet-SSD。如果数据量极大且对精度要求极高则可以尝试YOLOv8或最新的YOLO变体。核心是匹配任务需求与资源约束。3. 数据预处理与特征工程实战3.1 数据集分析与预处理管道竞赛通常会提供一个初始数据集包含图像和对应的标注文件通常是PASCAL VOC格式的XML或COCO格式的JSON。第一步绝不是直接开训而是彻底分析数据。数据概览统计图像数量、尺寸分布、坑洼标注框的数量、大小、宽高比。你会发现坑洼标注框通常面积很小占图像比例低且宽高比各异有圆形、长条形不规则坑。这直接指向了两个关键问题小目标检测和数据不平衡。图像预处理标准化/归一化将像素值从[0, 255]缩放到[0, 1]或进行标准化减均值除方差有助于模型稳定收敛。深度学习框架的ToTensor()转换通常会自动完成到[0,1]的缩放。尺寸统一YOLO系列要求输入尺寸固定如640x640。需要使用等比例缩放并填充Letterbox的方式避免直接拉伸导致图像失真。YOLOv5的datasets.py中内置了优秀的Letterbox实现。基础增强在训练初期可以应用一些简单且安全的增强如随机水平翻转。对于道路图像水平翻转是合理的不会改变坑洼的物理意义。标注格式转换竞赛标注格式可能不直接兼容YOLO。YOLO需要的是归一化的中心坐标和宽高[class_id, x_center, y_center, width, height]所有值在0到1之间。你需要编写脚本进行格式转换。import xml.etree.ElementTree as ET import os def convert_voc_to_yolo(xml_path, img_width, img_height, class_dict): 将VOC格式XML标注转换为YOLO格式的txt文件内容 tree ET.parse(xml_path) root tree.getroot() yolo_lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_dict: continue cls_id class_dict[cls_name] bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 计算归一化中心坐标和宽高 x_center (xmin xmax) / 2.0 / img_width y_center (ymin ymax) / 2.0 / img_height width (xmax - xmin) / img_width height (ymax - ymin) / img_height # 确保坐标在0-1范围内 x_center max(0, min(1, x_center)) y_center max(0, min(1, y_center)) width max(0, min(1, width)) height max(0, min(1, height)) yolo_lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) return yolo_lines3.2 针对坑洼检测的特征工程与数据增强这是提升模型性能的关键环节针对坑洼的特点颜色空间转换坑洼与正常路面在RGB空间差异可能不明显但在HSV或Lab颜色空间的某些通道如饱和度S、明度V上可能对比度更高。可以将图像转换到其他颜色空间作为额外通道输入或用于生成注意力图。纹理特征强化坑洼往往伴随纹理断裂。可以尝试应用局部二值模式LBP、灰度共生矩阵GLCM等算法提取纹理特征图与原始图像拼接作为模型的输入。这相当于给模型提供了手工构造的“先验知识”。针对性的数据增强Mosaic增强YOLOv5自带的Mosaic增强将四张图像拼成一张能极大地丰富背景并让模型学习在不同位置、不同尺度下检测小目标对于坑洼这类小目标检测效果提升显著。MixUp增强将两张图像线性混合其标签也对应混合。能提高模型对对抗性样本的鲁棒性。随机遮挡Random Erasing/CutOut随机擦除图像的一部分迫使模型不过度依赖局部特征增强对坑洼部分遮挡情况的泛化能力。光照与噪声扰动模拟不同天气雨雾、阴影、不同时间夜间低光照的拍摄条件。添加高斯噪声、调整亮度、对比度、饱和度。实操心得数据增强要“适度”且“合理”。过度增强如大角度的旋转、扭曲可能会生成不符合物理现实的“坑洼”图像例如坑洼的阴影方向与光源方向矛盾反而会误导模型。建议先使用YOLOv5默认的增强组合再根据验证集效果有选择地添加或调整增强参数。一个重要的技巧是可视化增强后的图像和标注确保增强没有破坏标签的正确性。4. 模型构建、训练与调优全流程4.1 YOLOv5模型配置与修改我们使用YOLOv5的官方代码库。核心配置文件是models/yolov5s.yaml以s模型为例。对于坑洼检测可能需要修改两个地方类别数将nc: 80修改为你的类别数例如只有“pothole”一类则改为nc: 1。锚框Anchor重聚类YOLO使用预定义的锚框来预测目标。默认锚框是基于COCO数据集聚类的对于宽高比分布特殊的坑洼框可能不是最优。我们可以用自己的训练集标注框重新进行K-means聚类生成更合适的锚框尺寸。# 使用YOLOv5自带的聚类脚本需要根据你的数据集路径修改 python utils/autoanchor.py --data ./data/pothole.yaml --img-size 640运行后脚本会评估当前锚框与数据集的匹配度并建议新的锚框值。将这些值更新到模型的yaml配置文件中。4.2 训练策略与超参数设置训练命令的核心如下python train.py --img 640 --batch 16 --epochs 100 --data ./data/pothole.yaml --cfg ./models/yolov5s.yaml --weights yolov5s.pt --name pothole_exp1关键超参数解析--img 640: 输入图像尺寸。更大的尺寸有助于检测小目标但会显著增加显存消耗和训练时间。640是速度和精度的常见折衷。--batch 16: 批次大小。在GPU显存允许的情况下尽可能设大有利于训练稳定。如果出现OOM内存不足可以减小batch或使用--multi-scale训练动态调整尺寸。--epochs 100: 训练轮数。需要根据损失曲线和验证集指标早停Early Stopping避免过拟合。--weights yolov5s.pt: 加载预训练权重。强烈建议使用预训练权重这能利用在ImageNet等大数据集上学到的通用特征加速收敛并提升最终性能。--data: 指向你的数据配置文件pothole.yaml。这个文件定义了训练/验证集的路径、类别数和类别名。数据配置文件pothole.yaml示例# 坑洼数据集配置文件 path: ../datasets/pothole # 数据集根目录 train: images/train # 训练集图像路径相对于path val: images/val # 验证集图像路径相对于path test: images/test # 测试集路径可选 # 类别数及名称 nc: 1 # 类别数量 names: [pothole] # 类别名称列表 # 可选下载数据集的URL # download: https://your-dataset-url.com4.3 训练过程监控与调优技巧训练开始后重点关注以下输出和文件控制台日志观察每个epoch的训练损失box_loss, obj_loss, cls_loss和验证集指标mAP0.5, mAP0.5:0.95。损失应稳步下降并趋于平缓mAP应逐步上升。TensorBoard或WB可视化YOLOv5默认集成Weights Biases或TensorBoard。这是调优的利器可以可视化损失曲线、指标曲线、验证集预测样例、混淆矩阵等。过拟合判断如果训练损失持续下降但验证集损失先降后升或验证集mAP不再增长可能出现了过拟合。对策增加数据增强强度、添加DropOut层、减少模型复杂度、提前停止训练。欠拟合判断如果训练损失和验证损失都很高且下降缓慢。对策检查数据质量、增加训练轮数、减小学习率、使用更复杂的模型如从YOLOv5s切换到YOLOv5m。学习率调度YOLOv5默认使用余弦退火Cosine Annealing学习率调度器配合热身Warmup阶段。通常效果很好。如果你想手动调整可以修改--lr0初始学习率和--lrf最终学习率因子。对于微调预训练模型初始学习率可以设小一点如1e-3。模型集成与测试时增强TTA在最终提交或评估时可以融合多个epoch的模型权重如最后几个epoch的权重平均并在推理时使用测试时增强水平翻转、多尺度推理这通常能稳定提升1-2个百分点的mAP但会成倍增加推理时间。5. 模型评估、结果分析与可视化5.1 评估指标深度解读模型训练完成后需要在独立的测试集上进行全面评估。目标检测的核心指标包括精确率Precision模型预测为正的样本中真正为正的比例。Precision TP / (TP FP)。高精确率意味着模型“找得准”虚警少。召回率Recall所有真实的正样本中被模型正确找出的比例。Recall TP / (TP FN)。高召回率意味着模型“找得全”漏检少。平均精度Average Precision, AP在不同召回率阈值下的精确率的平均值。它是综合衡量模型性能的核心指标。通常计算IoU阈值为0.5时的AP记为AP0.5或AP50以及IoU阈值从0.5到0.95步长0.05的平均AP记为AP[0.5:0.95]或mAP。F1-Score精确率和召回率的调和平均数。F1 2 * (Precision * Recall) / (Precision Recall)。当你想在精确率和召回率之间取得平衡时关注F1。对于坑洼检测召回率往往比精确率更重要。因为漏检一个坑洼低召回可能导致安全隐患而误检一个低精确可能只是增加了一次不必要的巡检。因此在调优时可以适当调整模型输出置信度的阈值或使用F2-Score更看重召回率作为优化目标。5.2 结果可视化与错误分析使用YOLOv5提供的val.py脚本或detect.py脚本在测试集上运行会生成丰富的可视化结果PR曲线Precision-Recall Curve直观展示不同置信度阈值下精确率和召回率的权衡关系。曲线下的面积就是AP。混淆矩阵查看模型在各类别上的分类错误情况。对于单类别检测主要看背景被误判为坑洼FP和坑洼被漏检FN的比例。检测样例图将模型预测的边界框画在原始图像上。这是最直接的评估方式。分析FP误检这些被误判为坑洼的区域有什么共同特征是阴影、水渍、树叶还是路面修补痕迹针对这些特征可以考虑在数据增强中加入更多类似负样本或在预处理阶段尝试抑制这些干扰。分析FN漏检哪些坑洼被漏掉了是尺寸太小、对比度太低、还是形状特别不规则针对小目标可以尝试减小模型下采样倍率如修改neck和head的结构或使用专门的小目标检测层如YOLOv5中的SPPF层之前添加一个更浅的特征图输出。推理速度FPS在指定的硬件上如Tesla T4 GPU测量模型处理单张图像所需的时间。这对于评估方案的实用性至关重要。import torch from PIL import Image from pathlib import Path # 加载训练好的最佳模型 model torch.hub.load(ultralytics/yolov5, custom, path./runs/train/pothole_exp1/weights/best.pt, force_reloadTrue) model.conf 0.25 # 置信度阈值 model.iou 0.45 # NMS的IoU阈值 # 单张图像推理 img_path ./test_images/road_001.jpg results model(img_path) # 结果显示与保存 results.show() # 屏幕显示 results.save(save_dir./inference_results/) # 保存结果图像 # 打印详细的检测信息 print(results.pandas().xyxy[0]) # 以Pandas DataFrame格式打印检测框信息xmin, ymin, xmax, ymax, confidence, class6. 完整代码结构解析与关键模块实现一个完整的竞赛解决方案不仅仅是训练脚本而是一个工程化的项目。以下是推荐的项目结构及关键文件说明pothole_detection_competition/ │ ├── data/ # 数据相关 │ ├── raw/ # 原始竞赛数据 │ ├── processed/ # 处理后的数据划分好的训练/验证/测试集 │ ├── annotations/ # 原始标注文件VOC XML等 │ ├── labels/ # 转换后的YOLO格式标注.txt文件 │ └── dataset.yaml # YOLO数据配置文件 │ ├── src/ # 源代码 │ ├── preprocess/ # 数据预处理脚本 │ │ ├── convert_annotations.py # 标注格式转换 │ │ ├── split_dataset.py # 数据集划分训练/验证/测试 │ │ └── analyze_data.py # 数据集统计分析 │ │ │ ├── training/ # 训练相关 │ │ ├── train.py # 可直接用YOLOv5的或在其基础上封装 │ │ ├── hyperparameter_tuning.py # 超参数搜索脚本可选 │ │ └── visualize_training.py # 训练过程可视化 │ │ │ ├── inference/ # 推理与评估 │ │ ├── evaluate.py # 在测试集上评估模型生成指标 │ │ ├── detect_video.py # 对视频流进行检测扩展应用 │ │ └── export_model.py # 模型导出如到ONNX、TensorRT │ │ │ └── utils/ # 通用工具函数 │ ├── metrics.py # 自定义评估指标计算 │ ├── visualization.py # 绘制PR曲线、混淆矩阵等 │ └── logger.py # 日志记录 │ ├── models/ # 模型定义与配置 │ ├── yolov5s_custom.yaml # 自定义的YOLOv5模型配置文件修改了nc和anchors │ └── (其他自定义模型结构) │ ├── runs/ # 实验记录由YOLOv5自动生成或自定义 │ ├── train/ # 训练实验 │ │ └── exp1/ # 第一次实验 │ │ ├── weights/ # 保存的模型权重best.pt, last.pt │ │ ├── results.png # 指标曲线图 │ │ └── events.out... # TensorBoard日志 │ └── detect/ # 推理实验 │ ├── configs/ # 配置文件目录集中管理超参数 │ └── train_config.yaml │ ├── requirements.txt # Python依赖包列表 ├── README.md # 项目说明文档 └── main.py # 项目主入口可选用于串联流程关键模块实现示例数据集划分脚本数据划分是确保模型评估公正性的基础。务必确保训练集、验证集和测试集的数据分布如坑洼大小、光照条件基本一致。import os import random import shutil from pathlib import Path def split_dataset(image_dir, label_dir, output_base, train_ratio0.7, val_ratio0.2, test_ratio0.1): 划分YOLO格式的数据集 Args: image_dir: 原始图像文件夹路径 label_dir: 对应标签文件夹路径.txt文件 output_base: 输出根目录 train_ratio, val_ratio, test_ratio: 划分比例 assert abs(train_ratio val_ratio test_ratio - 1.0) 1e-6, 比例之和必须为1 image_paths list(Path(image_dir).glob(*.jpg)) # 假设是jpg格式 random.shuffle(image_paths) # 随机打乱 total len(image_paths) train_end int(total * train_ratio) val_end train_end int(total * val_ratio) train_list image_paths[:train_end] val_list image_paths[train_end:val_end] test_list image_paths[val_end:] splits {train: train_list, val: val_list, test: test_list} for split_name, split_files in splits.items(): split_image_dir Path(output_base) / images / split_name split_label_dir Path(output_base) / labels / split_name split_image_dir.mkdir(parentsTrue, exist_okTrue) split_label_dir.mkdir(parentsTrue, exist_okTrue) print(fProcessing {split_name} set ({len(split_files)} files)...) for img_path in split_files: # 复制图像 dst_img_path split_image_dir / img_path.name shutil.copy(img_path, dst_img_path) # 复制对应的标签文件 label_path Path(label_dir) / (img_path.stem .txt) if label_path.exists(): dst_label_path split_label_dir / label_path.name shutil.copy(label_path, dst_label_path) else: print(fWarning: Label file not found for {img_path.name}) # 生成数据集YAML文件 with open(Path(output_base) / pothole.yaml, w) as f: f.write(fpath: {os.path.abspath(output_base)}\n) f.write(ftrain: images/train\n) f.write(fval: images/val\n) f.write(ftest: images/test\n\n) f.write(fnc: 1\n) f.write(fnames: [pothole]\n) print(Dataset split and YAML file generation completed.)7. 常见问题排查与竞赛技巧实录7.1 训练过程中的典型问题与解决方案问题现象可能原因排查步骤与解决方案Loss为NaN或突然变得巨大1. 学习率设置过高。2. 数据中存在损坏的图像或标注如坐标超出范围。3. 梯度爆炸。1. 大幅降低学习率如从1e-3降到1e-5重新训练。2. 运行数据检查脚本验证每张图像和标签是否能正常读取和解析。使用cv2.imread()检查图像解析标签看坐标是否在[0,1]区间。3. 使用梯度裁剪torch.nn.utils.clip_grad_norm_。mAP始终为0或极低1. 数据标注格式错误如类别ID不对。2. 数据路径配置错误模型实际上在空数据集上训练。3. 锚框anchors与数据集目标尺寸严重不匹配。4. 预训练权重加载失败或未加载。1. 随机抽样几张训练图像用脚本将标注框画上去肉眼检查是否正确。2. 检查dataset.yaml中的train和val路径是否正确确保这些文件夹下有图像和对应的标签文件。3. 运行utils/autoanchor.py重新聚类锚框并更新模型配置。4. 确认--weights参数指向正确的.pt文件并检查训练日志开头是否显示“Loading pretrained weights...”。训练集Loss下降验证集Loss上升过拟合1. 模型复杂度过高训练数据量不足。2. 数据增强不够。3. 训练轮数过多。1. 换用更小的模型如YOLOv5s-YOLOv5n或增加Dropout层。2. 增强数据增强的强度和多样性如Mosaic, MixUp, 随机遮挡。3. 使用早停Early Stopping或在验证集Loss连续几个epoch不下降时手动停止。训练速度非常慢1. 批次大小batch size太小。2. 图像输入尺寸过大。3. 未使用GPU训练。4. 数据加载是瓶颈如从慢速硬盘读取。1. 在GPU显存允许范围内增大batch size。2. 尝试减小--img尺寸如从640降到416。3. 检查torch.cuda.is_available()是否为True。4. 将数据预先加载到内存如果数据集不大或使用更快的存储如SSD并设置DataLoader的num_workers为CPU核心数如--workers 8。7.2 提升竞赛成绩的实战技巧伪标签Pseudo Labeling如果竞赛允许使用外部数据或提供大量未标注的测试集图像可以利用训练好的模型对这部分数据生成高置信度的预测将这些预测作为“伪标签”加入训练集进行第二轮训练。这能有效扩大训练数据规模尤其适用于数据稀缺的场景。模型集成Ensemble训练多个不同初始化、不同数据增强策略、甚至不同模型架构如YOLOv5m和YOLOv5l的模型。在推理时对它们的预测结果进行加权平均或非极大值抑制NMS融合。集成几乎总是能带来稳定的性能提升但会牺牲推理速度。测试时增强TTA推理在最终预测时对输入图像进行多种变换如水平翻转、多尺度缩放将每个变换后的图像输入模型得到预测再将所有预测结果逆变换回原图坐标并融合。YOLOv5中可以通过--augment参数开启。这能提高模型的鲁棒性尤其对于边缘案例。关注评估指标细节仔细阅读竞赛的评分规则。是只看mAP0.5还是也看mAP[0.5:0.95]是否对漏检有更严厉的扣分根据评分规则调整你的优化重点。例如如果规则更看重召回率你可以适当降低模型预测的置信度阈值--conf-thres。报告与可视化一份清晰的建模报告、严谨的实验记录使用了什么模型、数据增强、超参数、最终指标以及直观的结果可视化如检测效果对比图、PR曲线能在答辩或报告环节为你加分不少。使用TensorBoard或WB记录完整的实验过程是非常好的习惯。7.3 从竞赛到部署的思考竞赛获奖不是终点。一个完整的坑洼检测系统还需要考虑工程落地模型轻量化将训练好的PyTorch模型通过ONNX转换为TensorRT引擎或使用OpenVINO、NCNN等框架在边缘设备如Jetson Nano、手机上实现实时推理。前后端集成开发一个简单的Web应用使用Flask或FastAPI允许用户上传道路图片后端调用模型进行检测并将结果带标注框的图片和坑洼统计信息返回前端展示。视频流处理将模型应用于行车记录仪或监控摄像头的视频流实现实时坑洼检测与预警。这需要处理视频解码、帧抽取、批量推理、结果融合等一系列流水线操作。回过头看MathorCup这类竞赛的价值远不止于一个奖项。它提供了一个从问题定义、数据获取、算法选型、模型训练调优到结果分析的全流程实践机会。把每个环节扎扎实实地走通、吃透过程中积累的代码能力、调参经验和解决问题的思维才是真正宝贵的财富。希望这份基于实战的解析能帮你少走弯路更高效地构建出属于自己的优秀解决方案。