基于Python与CNN的道路坑洼识别实战:从数据集到模型部署
2026/9/24 19:11:07 网站建设 项目流程

简介:基于Python与PyTorch的深度学习道路坑洼识别项目,配有完整数据集和源码,面向计算机视觉初学者、深度学习者及道路智能检测研究人员,可用于课程设计、毕设或工业巡检中的路面病害自动识别。资源包共252个文件,包含246张jpg路面图片、3个Python脚本和3个txt说明,整体仅10.19MB;脚本分别完成数据集标签生成、模型训练和PyQt5界面展示,txt提供环境配置与运行指引。目前已有133人学习下载。项目需依次运行三个脚本:先读取各类别图片生成路径与标签,再训练验证集并保存模型,最后通过界面进行可视化识别。数据预处理通过在短边补灰边转为正方形并随机旋转实现增强,训练日志会记录每个epoch的验证集损失与准确率,便于调参。从环境配置到模型部署均有清晰指引,适合快速跑通CNN分类流程。

1. 基于python-cnn深度学习的道路坑洼识别:它到底能帮你解决什么问题

道路养护部门每天要处理大量巡检视频,人工盯屏看坑洼,看半小时眼睛就花了,漏检率随疲劳指数上升。基于python-cnn深度学习的道路坑洼识别,核心就是让卷积神经网络自动从路面图像里判断“有没有坑、坑在哪”。项目标题里带了数据集,意味着你不需要从零攒图,解压后就能直接训练一个可用的识别模型。这个方向适合三类人:做市政信息化项目需要快速出原型的开发者、研究生想拿真实数据集做实验、以及想转行视觉落地的工程师。它的价值不在模型多深,而在于把数据、训练、评估这条路走通,让你从“会调库”变成“能交付”。

2. 数据集解压后的第一件事:核对目录结构与标注格式,而不是急着跑代码

很多人在拿到“含数据集.zip”之后的第一反应是解压、找个训练脚本、把路径一填就开跑。这通常会在半小时后翻车:报错、维度对不上、标签读不进来。我一般会先花十分钟把数据集的目录结构摸清楚,再决定用什么方式加载。

2.1 两种最常见的组织方式:分类文件夹与检测标注

标题写的是“识别”,这个词在不同数据集里含义不同。常见做法有两种组织方式,你先看清楚再动手。

第一种是分类结构,适合“判断这张图里有没有坑洼”:

dataset/ ├── train/ │ ├── pothole/ (含坑洼) │ └── normal/ (正常路面) ├── val/ │ ├── pothole/ │ └── normal/ └── test/ ├── pothole/ └── normal/

第二种是检测结构,适合“框出坑洼的位置”,通常会带标签文件。VOC格式是xml,YOLO格式是txt,COCO是json。你需要先打开几个样本看看标注坐标落在哪个取值范围:

标注格式坐标形式读取工具常见坑
ImageFolder无标注,靠目录名torchvision.datasets.ImageFolder目录层级必须与类名严格对应
VOC xmlxmin/ymin/xmax/ymaxxml.etree.ElementTree 或 xmltodict坐标可能带小数,需转int
YOLO txtcx, cy, w, h(相对值)手写解析或 ultralytics 内置加载坐标已归一化到0~1,千万别当像素用

判断方法很简单:解压后先执行find . -type f | head -50,把实际文件结构列出来,再打开一两个标签文件看格式。不要依赖 README 里的描述,文件结构才是真相。

2.2 用PyTorch把数据变成DataLoader:最小可用代码

无论数据集原始格式是什么,训练前都要转成统一的加载管线。以下代码处理分类结构,这是最省事也最不容易出错的路径:

import os from torchvision import datasets, transforms from torch.utils.data import DataLoader data_dir = "./dataset" batch_size = 32 train_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder( os.path.join(data_dir, "train"), transform=train_transforms) val_dataset = datasets.ImageFolder( os.path.join(data_dir, "val"), transform=val_transforms) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True) print("类别映射:", train_dataset.class_to_idx) print("训练样本数:", len(train_dataset)) print("验证样本数:", len(val_dataset))

逻辑说明:ImageFolder 会自动扫描目录结构,以一级子目录名作为类别名,并生成 class_to_idx 映射。这里不需要手写标签 CSV,目录名就是标签。训练集加了 RandomHorizontalFlip 和 ColorJitter,提升模型对翻转和光照变化的鲁棒性,验证集不做随机增强,保证评估指标稳定可复现。

参数说明:Resize 到 224x224 是 ImageNet 预训练模型的默认输入尺寸,ResNet、VGG、MobileNet 都吃这个尺寸。Normalize 的三个均值、三标准差,对应 ImageNet 统计值,迁移学习时不要改成自己算的。num_workers 在 Windows 上建议设 0 或 2,设大了容易报 DataLoader worker 错误;Linux 可以放开到 4 或 8。如果显存不够 32 的 batch,先降到 16,不要动图片尺寸——尺寸一改预训练权重就不匹配。

2.3 样本均衡:坑洼图太少时别急着加模型容量

数据集里最常见的现状是:正常路面远多于坑洼路面,比例可能达到 5:1 甚至 10:1。如果不做处理,模型会学成一个“永远预测正常”的省事分类器,验证准确率看起来还挺高,但坑洼的召回率惨不忍睹。

我一般会先统计一下类别分布,再决定用哪种手段:

from collections import Counter for phase in ["train", "val", "test"]: path = os.path.join(data_dir, phase) counts = Counter() for cls_name in os.listdir(path): cls_path = os.path.join(path, cls_name) if os.path.isdir(cls_path): counts[cls_name] = len(os.listdir(cls_path)) print(phase, dict(counts))

如果发现比例严重失衡,解决优先级如下:先做数据增强(旋转、平移、亮度扰动、随机裁剪),增强提不上来再用类别权重,最后才考虑换模型。原因是坑洼的形态是“路面纹理断裂+阴影”,而不是精细的纹理细节,常用的翻转和颜色扰动就能显著扩充有效样本。

3. 模型选型与搭建:为什么图像任务绕不开CNN

标题锁定的是CNN,不是Transformer,也不是传统的图像处理算子。这不是拍脑袋,而是从数据量和任务特性出发的合理选择。

3.1 对比全连接与CNN:为什么图像任务不敢用前馈神经网络

一张 224x224 的 RGB 图像,拉平后是 150528 维。如果第一层用 1024 个神经元的全连接层,参数就是 1.5 亿个,训练集里的几千张图根本喂不饱。更重要的是,全连接网络丢掉了空间结构:像素向右平移一个位置,在向量里是完全不同的特征,模型却要重新学一遍。

CNN 的两个核心设计正好解决这两个问题。局部感受野让每个卷积核只看一个小邻域,用滑动窗口的方式扫过全图;权值共享让同一个卷积核在图像任何位置用同一套参数,平移不变性就成了内置属性。这也是“图像处理为啥用cnn不用前馈神经网络”的答案:参数量少两三个数量级,泛化能力反而更强。至于 RNN,它擅长的是序列建模,路面上一个坑没有任何前后文依赖,硬套 LSTM 属于自找麻烦。

3.2 用ResNet迁移学习搭出识别模型:现成权重是最大的后悔药

从零训练一个 CNN 在几千张图上很难收敛。常见做法是加载在 ImageNet 上预训练好的 ResNet18,把最后一层全连接换成自己的分类头,这就是迁移学习。ResNet18 只有约 1100 万参数,在 CPU 上也能跑推理,部署不吃力。

import torch.nn as nn import torchvision.models as models def build_model(num_classes=2, pretrained=True): model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后一层全连接,输出维度等于类别数 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(p=0.3), nn.Linear(in_features, num_classes) ) return model model = build_model(num_classes=2) print(model)

逻辑说明:pretrained=True 会下载 ImageNet 预训练权重,之后我们只替换最后的全连接层。前置卷积层已经学会了边缘、纹理、形状等通用特征,这些特征对路面坑洼同样有效。Dropout 加在全连接前,防止小数据集上的过拟合。

参数说明:num_classes 根据数据集实际类别数改。如果数据集里的坑洼还细分为“裂缝”“坑槽”“修补”,就在 build_model 里相应调整。至于为什么不选 ResNet50,样本量只有几千时 ResNet18 更容易收敛,推理速度也更快;ResNet50 是数据量到几万张以后再考虑的事。

3.3 分类问题还是检测问题:先把任务边界定死

标题说的是“识别”,这个词有歧义。如果你的交付物只需要告诉养护人员“这段路有坑”,那二分类模型就够了。但如果需要派出维修车精准修补,就必须知道坑的精确位置和大小,那就要从分类切到目标检测。常见做法是转入 YOLO 系单阶段检测器,数据整理思路和前面完全一致,只是标签不再是目录名,而是标注框坐标。我的建议是:先用分类模型快速验证数据集质量,如果分类效果都做不出来,换检测模型只会更糟糕——检测的收敛难度比分类高一个档次。

4. 训练与调参:让损失曲线平滑下降的实操配置

模型搭好了,数据能加载了,接下来是最磨人的训练环节。这一章把所有关键参数摆出来,并解释为什么这么设。

4.1 迁移学习冻结策略:什么时候冻结、什么时候解冻

常见的做法是先冻结卷积基,只训练新加的全连接层。原因是预训练特征已经足够通用,新数据少的时候动卷积核容易把原有特征破坏掉。预热几个 epoch 后再解冻全部层,用小学习率微调。

def freeze_backbone(model, freeze=True): for name, param in model.named_parameters(): if "fc" not in name: # fc 层保持可训练 param.requires_grad = not freeze return model # 先冻结主干,只训练分类头 model = freeze_backbone(model, freeze=True)

逻辑说明:冻结的含义是让 PyTorch 在反向传播时不为这些参数计算梯度,省显存也省时间。这时只有全连接层的参数在更新,训练速度快,且不容易把预训练特征带偏。等损失不再下降,再解冻全部层用更小学习率微调。

4.2 训练关键参数与推荐初始值

以下是我在道路坑洼这类中小型数据集上验证过的初始配置:

参数推荐值说明
优化器AdamW相比 Adam 有更好的权重衰减处理
初始学习率1e-3(冻结阶段)/ 1e-4(解冻后)迁移学习忌讳大学习率
批大小16~32显存不足优先降 batch,不动尺寸
轮数30~50数据量少时 50 轮足够
学习率调度CosineAnnealingLR比阶梯下降省心,曲线更平滑
损失函数CrossEntropyLoss多分类默认选择
类别权重按样本数反比计算解决类别不平衡

AdamW 的默认 betas 是 (0.9, 0.999),一般不用动。weight_decay 设 1e-4 到 5e-4,太大会让模型欠拟合,太小起不到约束作用。学习率是整个训练里最玄学也最影响结果的一项,我见过太多人用一个 1e-2 从头训 ResNet,损失曲线直接起飞。记住一条经验:迁移学习场景下,学习率超过 1e-3 基本属于自毁。

4.3 完整训练脚本:带验证与断点续训

import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30) best_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) # 验证阶段 model.eval() correct, total = 0, 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f"Epoch {epoch+1:02d} | Loss: {running_loss/len(train_dataset):.4f} | Val Acc: {acc:.4f}") if acc > best_acc: best_acc = acc torch.save({ "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "best_acc": best_acc, }, "best_model.pth") print("模型已保存,验证准确率:", best_acc)

逻辑说明:每个 epoch 先训练再验证,验证阶段用torch.no_grad()关闭梯度计算,省显存且加速。保存的是字典而不是裸的 state_dict,这样恢复训练时可以继续加载优化器状态和学习率调度器的进度,是断点续训的后悔药。

参数说明:T_max要和总 epoch 数对应,CosineAnnealingLR 会在 30 个 epoch 内把学习率从初始值余弦下降到接近 0。打印 Loss 时除以训练集样本数,得到平均样本损失,方便跨 epoch 比较。保存时机只认验证集准确率,不认训练损失——训练损失降但验证不涨,说明在过拟合。

5. 道路坑洼识别注意与排查:四次典型踩坑记录

训练过程不会一帆风顺。这一章把我做道路识别时踩过的坑集中整理,每条都是“现象 → 原因 → 解决”的结构,希望你能跳过这些弯路。

5.1 验证集准确率95%,实拍视频全是误报

现象:测试集上模型表现很好,准确率超过95%。但把行车记录仪的视频抽帧输入模型,正常路面大量报坑洼,完全没法用。

原因:数据分布偏差。公开数据集里的坑洼大多是天气好、光线足、近距离拍的“标准坑”,和真实道路场景里的阴影、油渍、裂缝、水渍混在一起,模型学到的是“深色区域=坑洼”这种偷懒特征。

解决:从目标使用场景里抽帧补充训练集,把视频中的正常路面也放进去,打上 normal 标签。如果实拍样本不够,至少要用验证集的场景来源做一次盲测,别拿同分布的数据评估部署效果。

5.2 loss稳步下降,精确率却纹丝不动

现象:训练损失从 0.6 降到 0.2,看起来学得不错,但验证集精确率和召回率都没有明显增长。

原因:类别不均衡被忽略。我一开始没统计样本分布,直接跑了 CrossEntropyLoss,模型把所有样本都预测为多数类,损失也能很低。准确率高是因为多数类占比大,但坑洼这个少数类的召回率基本是 0。

解快:先运行前面写的分布统计脚本,确认比例。然后在损失函数里加类别权重,torch 里可以直接传 weight 参数:

class_weights = torch.tensor([1.0, 3.0]).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)

权重按正常:坑洼的数量比反比设置,让模型对少数类的误判付出更高代价。

5.3 白天识别正常,夜间灯一照就崩

现象:白天测试效果不错,转到夜间场景,漏检率非常高。

原因:亮度均值偏移。训练集里大多是白天自然光图像,夜间的路灯、车灯在路面上形成高光和深影,和白天图像在像素分布上有明显差异。CNN 对光照敏感,夜间的坑洼阴影被灯一照可能完全看不见。

解决:在训练增强里加入亮度扰动已经不够,最有效的是采集夜间数据加入训练。如果没有现成夜间数据,就按灰度转换后的对比度做随机增强,提升模型对明暗变化的容忍度。这个问题的本质是域漂移,仅靠调参救不回来。

5.4 训练中途显存溢出,程序直接崩掉

现象:训练到第二个 epoch,报CUDA out of memory,程序退出。

原因:输入的图片尺寸太大、batch_size 设置过高,或者训练过程中把验证集也装进了显存。

解决:优先把 batch_size 从 32 降到 16,再不行就降到 8。如果还溢出,检查验证阶段是否忘了torch.no_grad()。另外确认 num_workers 开得够多,数据加载才不会成为瓶颈。批量大小是 16 还是 8 对最终效果影响不大,但程序崩掉一次浪费时间,不如一开始就留足余量。

6. 验证与落地:从F1分数到能交给养护部门用的模型

训练的终点不是 loss 收敛,而是交付一个经得起实际场景检验的模型。

6.1 用混淆矩阵替代单一准确率

准确率在类别不均衡时是骗子。我习惯在测试集上输出混淆矩阵,重点看坑洼类的召回率——即“实际有坑,模型检出多少”。如果召回率低于 0.85,说明漏检风险偏高,不适合直接上巡检车。

from sklearn.metrics import confusion_matrix, classification_report import numpy as np all_preds, all_labels = [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names=val_dataset.classes))

6.2 导出ONNX做部署验证

训练验证完毕,把模型导出为 ONNX 格式。ONNX 是跨平台的中间表示,可以转换到 TensorRT、ONNXRuntime 或 OpenVINO,在嵌入式设备上跑推理比原生 PyTorch 快不少。

dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export(model, dummy_input, "pothole_model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}})

ONNX 导出的坑在于预处理必须保持一致:推理时同样要 Resize 到 224、ToTensor、归一化,任何一步不一致都会让精度走下坡路。建议把预处理封装成一个函数,训练和推理共用。

6.3 面向场景的进阶方向

如果要在真实巡检中落地,单一图像分类还不够。我习惯的做法是加入连续帧一致性判断——同一段路在连续 5 帧里都被判定为坑洼才上报,单帧误报直接丢掉;同时把相机安装高度和焦距记录下来,将像素位置映射到实际路面坐标,这样上报的不只是“有坑”,还有“坑的大致面积”。道路坑洼识别的核心瓶颈从来不是模型结构,而是数据分布覆盖不住真实场景。做这一行最重要的是对自己的模型保持怀疑,每换一个使用场景就重新采集盲测数据验证,用数据说话。这个原则帮我避免了很多次盲目交付,希望帮到你。

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

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

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

立即咨询