ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python+CNN的道路坑洼检测:从数据集到推理的完整实战指南

基于Python+CNN的道路坑洼检测:从数据集到推理的完整实战指南 简介这份资源面向计算机视觉课程设计、期末大作业及入门深度学习实践的本科生与自学者围绕道路坑洼检测这一典型场景提供基于Python与CNN的完整实现方案帮助读者理解卷积神经网络在图像分类与缺陷识别中的落地流程。压缩包共14个文件以11个py脚本为核心涵盖AlexNet、LeNet-5及其改进版本、预测与测试模块另含2个h5权重文件与1份md说明文档整体约10.49MB下载后可直接运行并对照注释阅读。目前已有404人学习下载说明该方案在同类作业中具备一定参考价值。读者可从中获得完整的模型定义、训练与推理代码、预训练权重、测试脚本及项目说明既能作为高分课程设计模板也便于在此基础上替换数据集或调整网络结构进行二次开发适合希望快速掌握CNN项目全流程的学习者。1. 道路坑洼检测大作业从数据集到 CNN 推理的完整落地路径道路坑洼检测这个题目在计算机视觉大作业里出现的频率极高但真正能跑通、能讲清楚、能拿得出手的并不多。多数人卡在三个地方数据集从哪来、CNN 模型怎么选、训练完怎么验证效果。这篇笔记就围绕「基于 Python CNN 实现道路坑洼检测」这条主线把数据准备、模型搭建、训练调参、推理验证、避坑排查串成一条可复现的路径。适合正在做计算机视觉大作业的学生也适合想快速上手 CNN 图像分类/检测任务的 Python 开发者。读完你应该能自己搭出一套能跑、能改、能解释的坑洼检测流程而不是只抄一份源代码交差。2. 数据集准备与 CNN 输入管线的搭建2.1 坑洼数据从哪来三类常见来源与取舍做道路坑洼检测第一步不是写模型而是搞清楚数据从哪来。常见做法有三类公开数据集、自己采集、以及从视频里抽帧。公开数据集里比较常被提到的是 Road Damage Dataset 系列里面包含坑洼、裂缝等路面病害标注但下载和格式转换有一定门槛。自己采集的话用手机固定在车头或电动车把手上沿路拍视频回来按帧切图是最直接的方式。视频抽帧可以用 OpenCV 几行代码搞定但要注意光照和车速太快会糊太慢数据冗余。从大作业角度我一般建议先用公开数据集跑通流程再补一小批自己采集的图做验证。这样既有规模又有真实场景。数据量上二分类坑洼/非坑洼至少准备 800 到 1500 张其中正负样本比例尽量接近 1:1 到 1:1.5否则模型会偏向多数类。图像尺寸统一缩到 224×224 或 256×256前者适配大多数轻量 CNN后者留一点细节余量。提示如果公开数据集标注格式是 VOC 或 COCO不要手动一张张改写个转换脚本批量处理后面会讲。2.2 用 Python 把原始图整理成 CNN 能吃的目录结构CNN 训练最怕目录乱。我习惯用ImageFolder那种按类别分文件夹的结构训练集和验证集分开。下面这段脚本做三件事读取原始图片、统一尺寸、按 8:2 划分训练/验证集并写入对应目录。import os import shutil import random import cv2 # 原始图片目录正样本和负样本分别放在 pothole 和 normal 子目录 RAW_DIR raw_data # 输出目录 OUT_DIR dataset IMG_SIZE (224, 224) SPLIT_RATIO 0.8 for category in [pothole, normal]: src_dir os.path.join(RAW_DIR, category) files [f for f in os.listdir(src_dir) if f.lower().endswith((.jpg, .png, .jpeg))] random.shuffle(files) split_idx int(len(files) * SPLIT_RATIO) train_files files[:split_idx] val_files files[split_idx:] for phase, file_list in [(train, train_files), (val, val_files)]: dst_dir os.path.join(OUT_DIR, phase, category) os.makedirs(dst_dir, exist_okTrue) for fname in file_list: img cv2.imread(os.path.join(src_dir, fname)) if img is None: continue img cv2.resize(img, IMG_SIZE) cv2.imwrite(os.path.join(dst_dir, fname), img) print(数据集整理完成)逻辑说明先按类别读取再打乱顺序按比例切分最后统一 resize 并写入dataset/train和dataset/val。参数上IMG_SIZE根据你选的 CNN 输入改SPLIT_RATIO一般 0.8 够用数据少可以调到 0.7。注意cv2.imread遇到损坏图会返回 None这里直接跳过避免训练时炸掉。2.3 数据增强别让模型只记住晴天柏油路坑洼检测的难点在于光照、角度、路面材质变化大。如果训练集全是晴天直拍模型到阴天或侧光就翻车。用torchvision.transforms做增强是最省事的做法常见组合随机水平翻转、随机旋转 ±15 度、颜色抖动、随机裁剪。下面是一个可抄的 transform 配置。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])参数说明ColorJitter的 brightness 和 contrast 别开太大0.3 左右比较稳太大反而让坑洼纹理失真。RandomResizedCrop的 scale 下限 0.8避免把坑洼裁掉。Normalize 用的是 ImageNet 均值方差如果你从零训练可以改成自己数据集的统计值但用预训练权重时保持默认。3. CNN 模型选型与训练脚本的完整实现3.1 为什么图像任务用 CNN 而不是前馈网络一个必须讲清的点大作业答辩常被问为什么不用全连接网络核心原因就一个参数共享和局部感受野。一张 224×224×3 的图如果直接拉平接全连接第一层权重就是 224×224×3×隐藏单元数参数量爆炸而且丢失空间结构。CNN 的卷积核在整张图上滑动同一个核检测同一种纹理坑洼的边缘、暗斑、不规则形状正好适合这种局部特征提取。池化层再降维保留主要响应。所以图像任务用 CNN 不是跟风是结构和数据特性匹配。选型上大作业不建议从零搭太深的网络。常见做法是拿 ResNet18 或 MobileNetV2 做迁移学习把最后全连接层改成二分类。ResNet18 参数量约 1100 万MobileNetV2 更轻适合 CPU 推理。如果老师要求自己搭可以用 3 层卷积 2 层全连接的简单 CNN但准确率通常不如迁移学习。3.2 用 ResNet18 迁移学习搭一个坑洼二分类器下面这段代码用torchvision.models.resnet18加载预训练权重替换最后一层并冻结前面的卷积层。冻结是为了小数据集上防止过拟合等训练几轮后再解冻微调。import torch import torch.nn as nn from torchvision import models def build_model(num_classes2, freeze_backboneTrue): model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) if freeze_backbone: for param in model.parameters(): param.requires_grad False # 替换最后的全连接层 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model model build_model(num_classes2, freeze_backboneTrue) print(model.fc)逻辑说明weightsmodels.ResNet18_Weights.DEFAULT加载 ImageNet 预训练权重freeze_backboneTrue时只训练最后的fc层。参数上num_classes2对应坑洼/非坑洼。如果要做多分类坑洼、裂缝、正常改这个数字即可。解冻微调时把freeze_backbone设为 False但学习率要调小一般 1e-4 到 1e-5。3.3 训练循环损失、优化器、学习率三个必调参数训练脚本我一般写成函数方便反复跑。损失用交叉熵优化器用 Adam学习率分两段冻结时 1e-3解冻后 1e-4。下面是一个完整可跑的 train 函数。import torch from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train_model(model, train_dir, val_dir, epochs10, batch_size32, lr1e-3): device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) train_dataset ImageFolder(train_dir, transformtrain_transform) val_dataset ImageFolder(val_dir, transformval_transform) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers2) criterion torch.nn.CrossEntropyLoss() optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lrlr) for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_acc correct / total # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc val_correct / val_total print(fEpoch {epoch1}/{epochs} | Loss {running_loss/len(train_loader):.4f} | Train Acc {train_acc:.4f} | Val Acc {val_acc:.4f}) return model参数说明batch_size32是常见起点显存不够降到 16 或 8。lr1e-3对应冻结阶段解冻后改成 1e-4。epochs10先跑一轮看曲线如果验证准确率还在涨就加到 20 或 30。num_workers在 Windows 上有时会报错改成 0 即可。注意filter(lambda p: p.requires_grad, ...)只更新需要梯度的参数冻结层不会被更新。3.4 训练完保存与加载别让权重文件成为黑匣子训练完一定要保存权重并且保存时带上类别映射。常见做法是存state_dict再单独存一个classes.txt。加载时先建模型结构再load_state_dict。# 保存 torch.save(model.state_dict(), pothole_resnet18.pth) with open(classes.txt, w) as f: f.write(\n.join(train_dataset.classes)) # 加载 model build_model(num_classes2, freeze_backboneFalse) model.load_state_dict(torch.load(pothole_resnet18.pth, map_locationcpu)) model.eval()逻辑说明state_dict只存参数不存结构所以加载前必须用同样的代码建模型。map_locationcpu让 GPU 训练的权重也能在 CPU 上加载。classes.txt记录类别顺序推理时按这个顺序解释输出。4. 推理验证与效果评估模型到底能不能用4.1 单张图推理从读图到输出类别的完整链路训练完最直接验证方式就是拿一张没见过的图跑一遍。下面这段代码封装了预处理、推理、取最大概率类别。import torch from PIL import Image def predict_image(model, img_path, classes, transform, devicecpu): model.eval() img Image.open(img_path).convert(RGB) tensor transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, 1) return classes[pred.item()], conf.item() classes [normal, pothole] label, confidence predict_image(model, test.jpg, classes, val_transform) print(f预测{label}置信度{confidence:.4f})参数说明unsqueeze(0)增加 batch 维度因为模型期望 4 维输入。softmax把 logits 转成概率torch.max取最大值和对应索引。置信度低于 0.6 时建议人工复核尤其是二分类边界样本。4.2 批量评估混淆矩阵和三个必须看的指标单张图看不出整体效果批量跑验证集算混淆矩阵、准确率、召回率、F1。坑洼检测里召回率比准确率更重要漏检一个坑可能爆胎误检一个只是多看一眼。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate_model(model, val_loader, devicecpu): model.eval() all_preds [] all_labels [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[normal, pothole])) evaluate_model(model, val_loader)逻辑说明classification_report直接输出 precision、recall、f1-score。重点看 pothole 类的 recall如果低于 0.8说明漏检多需要补正样本或调阈值。混淆矩阵里假阴性实际坑洼预测正常的数量就是漏检数。4.3 可视化预测结果把翻车样本挑出来看评估指标之外我习惯把预测错的图单独存出来肉眼看看模型到底错在哪。常见错误阴影被当成坑洼、湿路面反光被当成坑洼、远处小坑没检出来。import os from PIL import Image def save_misclassified(model, val_loader, classes, devicecpu, out_direrrors): os.makedirs(out_dir, exist_okTrue) model.eval() idx 0 with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) _, preds torch.max(outputs, 1) for i in range(imgs.size(0)): if preds[i] ! labels[i]: img imgs[i].cpu().numpy().transpose(1, 2, 0) img (img * 0.229 0.485) * 255 img img.astype(np.uint8) Image.fromarray(img).save(os.path.join(out_dir, ferr_{idx}.jpg)) idx 1 print(f保存了 {idx} 张错分图)参数说明反归一化时用训练时的 mean 和 std这里对应 ImageNet 的 0.485/0.229。存出来的图可以直接看找出系统性错误再针对性补数据。5. 避坑与排查道路坑洼检测大作业里最容易翻车的 5 个点5.1 现象训练准确率 99%验证准确率 60%原因过拟合模型记住了训练集的纹理和背景没学到坑洼本质特征。常见于数据量少、没做增强、没冻结主干。解决先冻结主干只训 fc 层加数据增强加 dropout 或 weight decay。如果还不行减模型复杂度换 MobileNetV2。5.2 现象验证集准确率一直 50% 左右loss 不降原因标签映射错了或者正负样本目录放反。ImageFolder按文件夹名排序classes顺序和你以为的可能不一致。解决打印train_dataset.classes确认顺序检查classes.txt和推理时用的列表是否一致。另外检查图片是否真的读进去了len(train_dataset)看一眼。5.3 现象推理时置信度全是 0.5 附近原因模型没训练好或者加载权重时结构不匹配但没报错。load_state_dict默认 strictTrue但如果你改了层名可能静默失败。解决加载时加strictTrue并捕获异常确认fc层形状一致。另外检查推理时的 transform 是否和验证集一致Normalize 参数不同会导致输出偏移。5.4 现象GPU 显存不够batch_size 降到 1 还是炸原因图片尺寸太大或者模型没释放中间变量。224×224 的 ResNet18 在 4GB 显存上 batch_size 32 一般没问题如果炸了可能是图片没 resize。解决确认 dataset 里的图已经统一到 224×224训练时再加torch.cuda.empty_cache()。实在不行用 MobileNetV2参数量小很多。5.5 现象自己拍的图预测全错公开数据集上却很好原因域偏移。公开数据集可能是特定国家、特定路面、特定光照你自己拍的图分布不同。解决补自己场景的图进训练集至少 100 到 200 张做微调。另外检查自己拍的图是否偏色、过曝预处理时加直方图均衡化试试。6. 进阶技巧用 Grad-CAM 让 CNN 告诉你它在看哪里大作业如果只报准确率答辩容易被问「模型到底学到了什么」。Grad-CAM 是一个轻量可解释性工具能生成热力图显示模型判断坑洼时关注图像哪个区域。实现上取最后一个卷积层的输出和梯度加权求和再上采样。import torch import torch.nn.functional as F import cv2 import numpy as np 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_full_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() fmap features[0].detach() grad grads[0].detach() weights grad.mean(dim(2, 3), keepdimTrue) cam (weights * fmap).sum(dim1, keepdimTrue) cam F.relu(cam) cam F.interpolate(cam, size(224, 224), modebilinear, align_cornersFalse) cam cam.squeeze().cpu().numpy() cam (cam - cam.min()) / (cam.max() 1e-8) handle_f.remove() handle_b.remove() return cam # 使用示例 target_layer model.layer4[-1] img_tensor val_transform(Image.open(test.jpg).convert(RGB)).unsqueeze(0) cam grad_cam(model, img_tensor, target_layer) heatmap cv2.applyColorMap(np.uint8(255 * cam), cv2.COLORMAP_JET) cv2.imwrite(gradcam.jpg, heatmap)逻辑说明register_forward_hook拿特征图register_full_backward_hook拿梯度。权重是梯度在空间维度上的平均加权特征图后 ReLU 去掉负响应再插值回原图尺寸。参数上target_layer一般选最后一个卷积块ResNet18 是model.layer4[-1]。热力图红色区域就是模型认为最像坑洼的地方。我自己的习惯是每次训练完先跑一遍 Grad-CAM看热力图是否落在坑洼上。如果热力图集中在背景或边缘说明模型学偏了得回去查数据。这个技巧在大作业答辩里很加分也能帮你快速定位问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表