
简介这份资源面向计算机视觉课程学习者与期末大作业需求者提供一套基于Python与CNN实现道路坑洼检测的完整项目方案适合作为高分课程设计、毕业设计参考也便于零基础读者理解与二次开发。压缩包共14个文件约10.49MB以11个Python脚本为核心涵盖AlexNet、LeNet-5等多种网络实现及预测、测试模块另含2个h5模型权重文件与1份README说明文档下载后可直接运行并查看代码注释。目前已有404人学习下载说明该方案在同类作业中具有一定参考价值。项目结构清晰包含模型训练、预测推理与测试评估等环节读者可据此掌握CNN在道路病害识别中的完整流程理解数据预处理、网络搭建与模型保存等关键步骤并在此基础上替换数据集或调整网络结构完成个性化改进。1. 道路坑洼检测从一张颠簸的路面照片说起减速带、井盖、补丁、阴影这些路面上的日常元素在计算机视觉里都是坑洼检测的干扰项。我做过一个基于 Python CNN 的道路坑洼检测项目最初以为把图片塞进卷积网络就能出结果实际跑起来才发现模型把井盖边缘全标成了坑洼误报率高得离谱。这个方向属于计算机视觉与目标检测的交叉地带核心诉求很明确给定一张路面图像判断是否存在坑洼并尽可能定位它的位置。适合谁做计算机视觉大作业的学生、想入门深度学习 CNN 的 Python 开发者、以及需要做道路巡检原型验证的工程师。它不需要昂贵的采集设备手机拍的路面图就能起步但要想真正可用数据标注、模型选型、后处理每一步都有讲究。下面把我踩过的路和验证过的方案拆开讲。2. 为什么道路坑洼检测绕不开 CNN从像素到特征的选型逻辑2.1 前馈网络处理图像的致命短板图像处理为啥用 CNN 不用前馈神经网络这个问题在 cs231n 计算机视觉课程里讲得很透。一张 224×224 的 RGB 路面图展平后是 150528 维向量。如果接一个 512 维的全连接隐藏层参数量就是 150528×512 ≈ 7700 万。参数量爆炸只是表面问题更深层的是全连接层丢失了空间结构——它把相邻像素和相隔很远的像素同等对待而坑洼的判别恰恰依赖局部纹理边缘、凹陷阴影和空间关系坑洼通常是不规则闭合区域。CNN 用三个机制解决这个问题局部感受野让每个神经元只看一小块区域权重共享让同一个卷积核扫过整张图池化带来平移不变性。落到坑洼检测上浅层卷积核学到的是边缘和角点中层学到的是裂纹、凹陷的局部模式深层才能组合出“坑洼”这个语义概念。这也是为什么基于 CNN 的方案在道路病害检测里成了默认选择。2.2 分类、检测、分割三条技术路线的取舍做道路坑洼检测先要明确任务形式。常见做法有三类路线输出标注成本适用场景图像分类整图有无坑洼低只标图片级标签快速筛查、二分类预警目标检测坑洼边界框中需画框定位坑洼位置、计数语义分割像素级掩码高需逐像素标注精确面积估算、精细巡检大作业场景我一般推荐从分类或轻量检测入手。分类任务用 CNN 就能跑通检测任务可以上 YOLO 系列或 Faster R-CNN。如果标题只要求“检测”多数情况下做二分类加可视化热力图就能交差但要想结果有说服力建议至少做到边界框级别。2.3 用 Python 和 PyTorch 搭一个最小 CNN 分类器先跑通最小闭环。环境用 Python 3.8装 PyTorch、torchvision、OpenCV、matplotlib。vscode python 环境配置这里不展开选好解释器即可。import torch import torch.nn as nn import torch.nn.functional as F class PotholeCNN(nn.Module): def __init__(self, num_classes2): super(PotholeCNN, self).__init__() # 输入 3×224×224 self.conv1 nn.Conv2d(3, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(32) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(64) self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(128) self.pool nn.MaxPool2d(2, 2) self.adaptive_pool nn.AdaptiveAvgPool2d((7, 7)) self.fc1 nn.Linear(128 * 7 * 7, 256) self.dropout nn.Dropout(0.5) self.fc2 nn.Linear(256, num_classes) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) # 112×112 x self.pool(F.relu(self.bn2(self.conv2(x)))) # 56×56 x self.pool(F.relu(self.bn3(self.conv3(x)))) # 28×28 x self.adaptive_pool(x) # 7×7 x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x逻辑说明三层卷积逐级提取特征每层后接 BatchNorm 加速收敛并缓解过拟合MaxPool 降采样。AdaptiveAvgPool2d 把任意尺寸特征图压到 7×7避免全连接层输入维度写死。Dropout 0.5 在全连接层前丢弃一半神经元这是小数据集上防过拟合的常规操作。参数说明卷积核统一 3×3、padding1 保持空间尺寸池化窗口 2×2 使特征图每次减半。通道数 32→64→128 是常见递增策略数据量小可以减半。num_classes2 对应有坑洼/无坑洼如果做多级病害分类改成对应类别数。2.4 数据准备与训练循环的关键参数数据按 7:2:1 切分训练/验证/测试。坑洼样本往往少于正常路面用 WeightedRandomSampler 做类别平衡或者对少数类做数据增强随机翻转、亮度抖动、小角度旋转。from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import transforms, datasets train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) # 按类别样本数计算权重缓解不均衡 targets [s[1] for s in train_ds.samples] class_count [targets.count(i) for i in range(2)] weights [1.0 / class_count[t] for t in targets] sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4)训练用交叉熵损失、Adam 优化器初始学习率 1e-3每 10 个 epoch 衰减到 0.1 倍。批大小 32 在 8G 显存上够用显存小就降到 16。训练 3050 个 epoch观察验证集 loss 是否还在下降早停耐心值设 7。提示Normalize 的均值和标准差用 ImageNet 预训练统计量即使从零训练也建议保持一致方便后续换预训练权重。3. 从分类到定位把坑洼框出来的检测方案3.1 检测头怎么加在 CNN 主干后接回归分支分类只告诉你“有坑洼”检测要告诉你“在哪”。最直接的做法是在 CNN 主干后接两个并行分支一个分类头输出类别置信度一个回归头输出边界框偏移量中心点 x,y 和宽高 w,h。这就是单阶段检测器的基本结构。class DetectionHead(nn.Module): def __init__(self, in_channels128, num_anchors3, num_classes2): super(DetectionHead, self).__init__() self.num_anchors num_anchors self.num_classes num_classes # 分类分支每个 anchor 输出 num_classes 个分数 self.cls_head nn.Conv2d(in_channels, num_anchors * num_classes, 1) # 回归分支每个 anchor 输出 4 个偏移量 self.reg_head nn.Conv2d(in_channels, num_anchors * 4, 1) def forward(self, x): cls self.cls_head(x) # B × (A*C) × H × W reg self.reg_head(x) # B × (A*4) × H × W return cls, reg逻辑说明1×1 卷积在不改变空间尺寸的前提下调整通道数把主干特征图映射成每个空间位置的预测。分类分支输出每个 anchor 在每个位置的类别分数回归分支输出框的偏移量。训练时用 anchor 匹配正负样本正样本算分类损失加回归损失负样本只算分类损失。参数说明num_anchors 根据坑洼尺度分布设定路面坑洼通常偏小anchor 尺寸建议覆盖 32×32 到 128×128。num_classes2 表示背景和坑洼两类。回归损失用 Smooth L1分类损失用 Focal Loss 缓解正负样本不均衡。3.2 用 YOLOv5 快速跑通检测基线从零写检测器训练流程容易翻车工程上更推荐用成熟框架。YOLOv5 在道路病害检测里落地案例多代码结构清晰改数据配置就能跑。# 克隆仓库并安装依赖 git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 准备数据images 和 labels 目录labels 为 YOLO 格式 txt # 每行class_id center_x center_y width height归一化到 0-1 # 修改 data/pothole.yaml # train: ../data/train/images # val: ../data/val/images # nc: 1 # names: [pothole] # 开始训练 python train.py --img 640 --batch 16 --epochs 100 \ --data data/pothole.yaml --weights yolov5s.pt逻辑说明YOLOv5 把检测当回归问题一次前向就输出所有框。--weights yolov5s.pt 加载预训练权重做迁移学习小数据集上比从零训练收敛快得多。--img 640 是输入分辨率坑洼目标小可以提到 1280但显存和速度要权衡。参数说明batch 16 在 8G 显存上跑 640 分辨率基本够用爆显存就降到 8。epochs 100 配合预训练权重通常够收敛看 mAP 曲线决定是否加。如果坑洼样本少于 500 张建议冻结主干前几层只训练检测头。3.3 标注格式转换与数据增强的边界YOLO 格式要求归一化坐标如果标注是 VOC 的 XML需要转换import xml.etree.ElementTree as ET import os def voc_to_yolo(xml_path, img_w, img_h, class_map): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): cls_name obj.find(name).text if cls_name not in class_map: continue cls_id class_map[cls_name] bbox obj.find(bndbox) x1 float(bbox.find(xmin).text) y1 float(bbox.find(ymin).text) x2 float(bbox.find(xmax).text) y2 float(bbox.find(ymax).text) # 归一化并转为中心点宽高 cx (x1 x2) / 2.0 / img_w cy (y1 y2) / 2.0 / img_h w (x2 - x1) / img_w h (y2 - y1) / img_h lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines逻辑说明VOC 用左上角和右下角绝对坐标YOLO 用归一化中心点加宽高。转换时先算中心点再除以图像宽高保留 6 位小数避免精度损失。class_map 把类别名映射到从 0 开始的整数。参数说明img_w 和 img_h 必须和实际图像尺寸一致标注时如果图像被缩放这里要用原始尺寸。边界框超出图像范围要裁剪到 [0,1]否则训练时会被过滤掉。数据增强方面Mosaic 和 MixUp 对坑洼检测有效但要注意坑洼的纹理和阴影是判别关键过度颜色抖动可能把阴影增强成坑洼。我一般把 ColorJitter 的强度控制在 0.2 以内旋转角度不超过 15 度。4. 道路坑洼检测的避坑与排查5 个血泪教训4.1 现象模型把井盖和补丁全标成坑洼原因训练集里负样本正常路面太单一缺少井盖、修补痕迹、阴影这些难负样本。模型没学会区分“凹陷”和“圆形深色区域”。解决专门收集 200300 张含井盖、补丁、阴影的负样本加入训练集或者在损失函数里对难负样本加大权重。我一般会做一轮 hard negative mining用训练好的模型跑负样本把误报的图挑出来重新标注加入训练。4.2 现象验证集准确率 95%实际拍的路面图全错原因训练集和实际场景分布不一致。训练图可能是固定角度、固定光照的巡检车数据实际手机拍的图角度、光照、分辨率都不同。解决训练时就做多尺度、多光照增强验证集必须包含不同来源的图。如果实际部署场景明确尽量用同源数据训练。跨场景时考虑域自适应或者至少做一次实际场景的微调。4.3 现象小坑洼漏检严重大坑洼框不准原因anchor 尺寸和坑洼实际尺度不匹配或者输入分辨率太低导致小目标特征丢失。解决统计训练集里坑洼的宽高分布用 K-means 聚类重新生成 anchor。输入分辨率从 640 提到 1024 或 1280小目标召回会明显改善。如果显存不够用切片推理把大图切成重叠的小块分别检测再合并。4.4 现象训练 loss 震荡不收敛原因学习率太大、批大小太小、或者数据标注有错误比如框超出图像、类别标错。解决先把学习率降到 1e-4 试跑几个 epoch看 loss 是否稳定下降。批大小至少 8太小的话梯度噪声大。然后抽查 50 张标注图可视化确认框的位置和类别没问题。标注错误是 loss 震荡最常见的原因没有之一。4.5 现象推理速度太慢达不到实时原因模型太大、输入分辨率太高、或者后处理 NMS 耗时。解决换轻量主干MobileNet、ShuffleNet或者用 YOLOv5s 而不是 YOLOv5l。输入分辨率降到 416 或 320速度能翻倍。NMS 的 IoU 阈值从 0.45 提到 0.5 可以减少候选框数量。如果还不行考虑 TensorRT 或 ONNX Runtime 加速推理。5. 进阶技巧用 Grad-CAM 验证模型到底在看哪里模型准确率高不代表它学到了正确的东西。我遇到过分类准确率 96% 的模型Grad-CAM 热力图显示它关注的是图像角落的水印而不是坑洼本身。这种模型换一批图就废。Grad-CAM 的原理是取目标类别分数对最后一层卷积特征图的梯度做全局平均池化得到每个通道的权重加权求和后 ReLU 得到热力图。它告诉你模型做决策时关注了图像的哪些区域。import torch import cv2 import numpy as np from torchvision import models, transforms def grad_cam(model, img_tensor, target_layer, class_idxNone): model.eval() features [] grads [] def forward_hook(module, input, output): features.append(output) def backward_hook(module, grad_in, grad_out): grads.append(grad_out[0]) handle_f target_layer.register_forward_hook(forward_hook) handle_b target_layer.register_backward_hook(backward_hook) output model(img_tensor) if class_idx is None: class_idx output.argmax(dim1).item() model.zero_grad() output[0, class_idx].backward() feat features[0].detach() # B×C×H×W grad grads[0].detach() # B×C×H×W weights grad.mean(dim(2, 3), keepdimTrue) # 全局平均池化 cam (weights * feat).sum(dim1, keepdimTrue) cam torch.relu(cam) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() - cam.min() 1e-8) cam cv2.resize(cam, (224, 224)) handle_f.remove() handle_b.remove() return cam逻辑说明前向钩子抓特征图反向钩子抓梯度。梯度在空间维度上平均得到通道权重权重乘特征图再求和ReLU 去掉负响应最后归一化到 0-1 并缩放到原图尺寸。叠加到原图上就能看到模型关注区域。参数说明target_layer 一般选最后一个卷积层太浅的热力图太局部太深的分辨率不够。class_idx 指定要看哪个类别的决策依据不指定就取预测分数最高的类。如果热力图集中在坑洼区域说明模型学到了正确特征如果集中在背景、水印、边框说明模型走了捷径需要重新检查数据。我现在的习惯是任何检测模型训练完先跑 20 张验证图的 Grad-CAM确认关注区域合理再继续调参。这个步骤花不了几分钟但能省下大量“指标好看、实际翻车”的后悔药。希望帮到你。本文还有配套的精品资源点击获取