
简介基于PythonCNN实现的道路坑洼检测项目源于大三学年的期末大作业经导师指导并通过评审得分98分。资源面向计算机相关专业正在完成课程设计或期末大作业的学生也适合需要项目实战练习的Python与计算机视觉学习者提供一套完整且可直接复用的实现方案。包内共14个文件、总计10.49MB包括11个Python脚本、2个H5权重文件与1个Markdown说明文档脚本覆盖AlexNet、LeNet-5等网络模型构建、数据预测、性能测试等环节H5为训练后的模型权重MD文档用于讲解项目结构与运行方式。目前已有905人学习/下载读者可从中理解CNN在道路坑洼检测中的完整流程对照源码掌握数据加载、模型搭建、训练验证与结果预测等关键步骤也可将测试脚本与权重文件直接用于实验或作为大作业参考。1. 道路坑洼检测这门大作业为什么用 Python CNN 才稳看到“基于 PythonCNN 实现的道路坑洼检测”这个题目我第一反应是直接上目标检测框架结果被标注数据折腾到差点换题。后来把方案降维先用 CNN 把“有没有坑”做成二分类再用滑窗输出坑洼坐标没想到这套组合成了整个计算机视觉大作业里最稳的一环。这篇笔记就是按照这条路线写的落地过程从方案选型、数据集组织、模型搭建到训练、评估、滑窗定位最后是一份完整的避坑清单。适合正在做道路坑洼检测大作业、想拿高分而不是只想跑通代码的同学也适合想用 Python CNN 快速验证一个视觉点子的人。2. 方案选型先想清楚“检测”两个字做到什么程度课程大作业里的“检测”和工业项目里的目标检测并不一定是一回事。老师写“检测”时可能只希望判断图像里有没有坑洼也可能要你在图上画出位置。如果不先确认这一点后面所有工作都容易推翻。2.1 大作业里的“检测”不一定要走目标检测在计算机视觉课程里很多经典作业都建议你先从分类任务建立 baseline再逐步做到检测。因为纯目标检测方案需要准备边界框标注而坑洼恰恰是极不适合手工标框的目标它形状不规则边缘是碎裂的小的坑洼可能只有几十个像素标框时很容易漏标或错标。如果班级里所有人都用同一份公开数据标注质量几乎决定你的分数上限。我一般会把“道路坑洼检测”拆成两步先用 CNN 做二分类判断一个图像块有没有坑洼。再用滑动窗口遍历测试图把所有被判为坑洼的窗口合并成检测框。这种做法不需要额外标框只需要准备pothole和normal两类文件夹就能在作业里同时回答“有没有坑”和“坑在哪”两个问题。更关键的是二分类的 CNN 结构简单训练稳定性高代码量也比检测模型小一个量级出现“训练完了但看不出学到什么”的概率低很多。如果你的老师明确要求输出 bounding box那就顺手把滑窗结果画出来。如果老师只是说“检测到坑洼并可视化”分类加滑窗已经是满配。2.2 为什么放弃传统边缘检测选择 CNN做过传统图像处理的同学最初可能想用 Canny 边缘检测加形态学操作来找坑洼。这个思路在干净、光照均匀的路面上有效但一到真实道路就露馅坑洼边缘不是统一的直线或圆弧浅坑几乎没有明显边缘裂缝区域边缘密度又高阴影、井盖、道路标线都会产生大量假边缘。CNN 能赢在这里卷积核在局部邻域内反复提取纹理、边缘、颜色突变再通过池化逐步扩大感受野。它看到的不是“某个像素是不是边缘”而是“这个局部区域的灰度模式像不像坑洼”。换句话说传统方法是手工定规则CNN 是从数据里学规则。我在选型时列过一个对比供你参考方案是否需标注光照变化鲁棒性实现成本大作业讲解难度Canny 形态学无需标注差阴影和标线误报高低但调参玄学难说清为什么有效颜色阈值无需标注差路面颜色波动大低容易被认为太简单自建 CNN 分类只需分类文件夹较好配合增强更好中原理清晰好展开预训练 CNN 迁移学习同上很好中上可讲数据少时迁移学习如果你只在交作业的边缘试探自建 CNN 足够如果想把报告写得漂亮迁移学习是加分项。但在数据量只有几百张图时自建轻量 CNN 反而更好控制预训练模型容易在少量数据上过拟合。2.3 项目目录与源码结构先搭骨架再写模型源代码作业最怕的是“只有几个.py文件散在桌面”。老师判分时除了看你模型效果还会看你工程组织。我建议一开始就把目录树建好pothole_detection_project/ │ ├── data/ │ ├── train/ │ │ ├── pothole/ │ │ └── normal/ │ └── val/ │ ├── pothole/ │ └── normal/ │ ├── models/ │ └── cnn_model.py │ ├── scripts/ │ ├── train.py │ ├── evaluate.py │ └── slide_window.py │ ├── results/ │ ├── curves.png │ └── result.jpg │ └── README.mddata下按类别分文件夹是 Kerasflow_from_directory直接能读取的结构不需要写 CSV 标签文件。models里放模型定义scripts里放训练、评估、滑窗脚本results放输出图。README 里写清楚 Python 版本、依赖库和“先运行哪个文件”这些细节在分数评估里很值钱。我见过太多代码模型写对了但数据路径写死换个目录就跑不起来。后文所有代码都会默认你遵循这个目录结构。3. 用 Keras 搭建坑洼检测 CNN模型结构、训练配置和三个必调参数代码层面我选 Keras因为它像搭积木适合大作业演示也方便用 TensorFlow 附带的工具做可视化和训练监控。下面是一套通用性很强的轻量 CNNCPU 上也能跑。3.1 轻量 CNN 结构小卷积核堆叠 BatchNorm Dropout模型采用三个卷积块每个块由卷积、批归一化、最大池化组成最后接全连接层做二分类。为了让后面的 Grad-CAM 热力图能用我把最后一个卷积层命名为last_conv。import tensorflow as tf from tensorflow.keras import layers, models def build_pothole_cnn(input_shape(224, 224, 3)): inputs layers.Input(shapeinput_shape) # 第一组卷积32 个 3x3 卷积核提取边缘和浅层纹理 x layers.Conv2D(32, (3, 3), activationrelu, paddingsame, nameconv1)(inputs) x layers.BatchNormalization()(x) x layers.MaxPooling2D((2, 2))(x) # 第二组卷积64 个卷积核组合局部纹理 x layers.Conv2D(64, (3, 3), activationrelu, paddingsame, nameconv2)(x) x layers.BatchNormalization()(x) x layers.MaxPooling2D((2, 2))(x) # 第三组卷积128 个卷积核抽象出“坑洼”的高级特征 x layers.Conv2D(128, (3, 3), activationrelu, paddingsame, namelast_conv)(x) x layers.BatchNormalization()(x) x layers.MaxPooling2D((2, 2))(x) x layers.Flatten()(x) x layers.Dropout(0.5)(x) # 防止全连接层过拟合 x layers.Dense(64, activationrelu)(x) outputs layers.Dense(1, activationsigmoid)(x) model models.Model(inputs, outputs) return model几个参数的经验值输入尺寸224x224这是 ImageNet 预训练模型最常见的输入尺寸将来一旦想换 VGG16、ResNet 做迁移学习不需要改图大小。卷积核固定3x3连续两个 3x3 比一个 5x5 参数少、非线性更强已经是 CNN 里的共识。paddingsame保证特征图尺寸在卷积后不变只有池化会减半避免后期尺寸算错。Dropout(0.5)这个值不是随便填的。二分类任务全连接层很容易过拟合0.5 是常见默认值训练集很小的时候可以提高到 0.6。如果电脑性能一般可以把input_shape改成(128, 128, 3)并将滑窗尺寸同步调成 128训练速度会快很多准确率通常只下降两三个点。3.2 数据加载目录结构 ImageDataGenerator 增强大作业的数据集通常很“朴素”可能只有两三百张图。直接丢进model.fitCNN 很快就会开始背训练样本。最常见的补救措施是训练时在线做数据增强。from tensorflow.keras.preprocessing.image import ImageDataGenerator # 训练集做增强同一批图每轮都会以轻微变形的样子出现 train_datagen ImageDataGenerator( rescale1.0 / 255, # 像素值从 0-255 缩放到 0-1 rotation_range15, # 随机旋转角度范围 width_shift_range0.1, # 水平平移比例 height_shift_range0.1, # 垂直平移比例 brightness_range[0.8, 1.2],# 亮度扰动坑洼在阴天和晴天差异很大 shear_range0.1, # 剪切变换模拟透视变形 zoom_range0.15, # 随机缩放 horizontal_flipTrue, # 水平翻转 fill_modenearest # 空像素用最近邻填充 ) # 验证集只做缩放不做增强否则验证分数失真 val_datagen ImageDataGenerator(rescale1.0 / 255) train_generator train_datagen.flow_from_directory( data/train, target_size(224, 224), batch_size16, class_modebinary, shuffleTrue, seed42 ) val_generator val_datagen.flow_from_directory( data/val, target_size(224, 224), batch_size16, class_modebinary, shuffleFalse, seed42 )增强参数里brightness_range是道路坑洼任务里最容易见效的一个。同一段路在顺光和逆光下看起来像两个场景模型如果没在训练时见过亮度变化测试时很容易把阴影误判为坑洼。shuffleFalse对验证集很重要。后面要算混淆矩阵时y_true的顺序必须和验证集文件夹顺序一一对应。如果验证集也 shuffle预测结果和真实标签就对不上了。3.3 训练配置compile、EarlyStopping 和 ModelCheckpoint训练配置决定你是“半天调不出模型”还是“二十分钟拿到有效结果”。下面这组参数可以直接抄。model build_pothole_cnn() model.compile( optimizeradam, # 默认学习率 1e-3 lossbinary_crossentropy, # 二分类用二元交叉熵 metrics[accuracy] ) from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks [ EarlyStopping( monitorval_loss, patience5, # 连续 5 轮验证损失不下降就停止 restore_best_weightsTrue # 结束恢复最优权重相当于后悔药 ), ModelCheckpoint( best_pothole_cnn.h5, monitorval_accuracy, save_best_onlyTrue ) ] history model.fit( train_generator, steps_per_epochtrain_generator.samples // train_generator.batch_size, validation_dataval_generator, validation_stepsval_generator.samples // val_generator.batch_size, epochs30, callbackscallbacks, verbose1 )steps_per_epoch设为样本数除以 batch size可以显示更稳定的训练进度。如果你不想丢最后一小批数据也可以不写steps_per_epochKeras 会自己跑完整一轮。三个必调参数的经验batch_size16入门 GPU 和纯 CPU 都能跑。太小比如 4梯度更新频繁损失震荡太大比如 64容易收敛到尖锐极小值泛化反而变差。learning_rateadam 默认 1e-3 够用。如果损失曲线来回震荡把学习率降到 1e-4 并重训。epochs不要硬扛 30重点看 EarlyStopping。训练集小的时候十轮左右就可能过拟合靠 patience 自动收手。4. 训练和评估把一张测试图变成可交差的检测结果模型训练只是中间环节大作业要拿高分得让老师看到“训练过程正常、评估指标可信、测试图上有检测框”。这一章按这个顺序写。4.1 训练过程监控loss 曲线和过拟合怎么判断很多同学只贴最终准确率这会让老师怀疑你会不会看训练过程。画一条训练曲线是最直观的“过程证据”。import matplotlib.pyplot as plt def plot_history(history, save_pathresults/curves.png): plt.figure(figsize(10, 4)) # 左图loss 曲线 plt.subplot(1, 2, 1) plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() # 右图准确率曲线 plt.subplot(1, 2, 2) plt.plot(history.history[accuracy], labeltrain_acc) plt.plot(history.history[val_accuracy], labelval_acc) plt.xlabel(epoch) plt.ylabel(accuracy) plt.legend() plt.tight_layout() plt.savefig(save_path, dpi150)怎么判断状态训练 loss 下降验证 loss 也跟着下降正常。训练 loss 继续降验证 loss 反而上升过拟合优先加强数据增强或提高 Dropout。两条 loss 都高且几乎不降学习率太大或太小先调学习率不要换模型。把这脚本放在scripts/evaluate.py里运行完保存results/curves.png报告里直接引用。老师看到你监控过拟合比看到空洞的“我们用了 CNN”有说服力得多。4.2 评估指标用混淆矩阵、精确率和召回率说话道路坑洼数据集通常类别不平衡正常路面样本远多于坑洼样本。只看 accuracy 会被骗比如 90% 是正常路面时模型全预测正常也能有 90% 准确率。必须看混淆矩阵和 F1。import numpy as np from sklearn.metrics import confusion_matrix, classification_report # 在验证集上预测 val_generator.reset() # 确保从第一个样本开始 y_prob model.predict(val_generator, stepsval_generator.samples // val_generator.batch_size 1) y_prob y_prob[:val_generator.samples] # 防止最后多余样本混入 y_pred (y_prob 0.5).astype(int).reshape(-1) y_true val_generator.classes[:len(y_pred)] # 只在 shuffleFalse 时有效 print(classification_report(y_true, y_pred, target_names[normal, pothole])) print(confusion_matrix(y_true, y_pred))这段代码会输出四行指标precision、recall、F1、support。大作业里重点讲坑洼类的 recall坑洼样本被漏掉的比例越低说明检测能力越强。如果 recall 低阈值可以往下调代价是误报增加。混淆矩阵建议在报告里用表格呈现不要只贴 std 输出。四格含义要写清楚真正例是“有坑且预测有坑”假反例是“有坑但漏了”假正例是“把正常路面当成坑”。这三句话的讲解能让你在答辩时少被追问很多。4.3 滑窗检测从“图里有坑”到“坑在哪里”二分类结果只能回答“这张图有没有坑”。要把任务落到“检测”我用滑窗遍历测试图把所有高概率窗口合并成框。这一步不需要额外标注是纯推理逻辑。import cv2 import numpy as np def batch_slide_detect(image, model, window_size224, stride112, threshold0.5): h, w image.shape[:2] patches [] positions [] # 按固定步长切窗口 for y in range(0, h - window_size 1, stride): for x in range(0, w - window_size 1, stride): patch cv2.resize( image[y:y window_size, x:x window_size], (224, 224) ) patches.append(patch.astype(float32) / 255.0) positions.append((x, y)) # 批量预测避免 for 循环里反复 model.predict probs model.predict(np.stack(patches), batch_size16, verbose0) boxes [] for pos, prob in zip(positions, probs): if prob[0] threshold: x, y pos boxes.append((x, y, x window_size, y window_size, float(prob[0]))) return boxes这里有两个关键参数stride112窗口是 224步长取一半相邻窗口有 50% 重叠不容易漏坑。步长越小越密但计算量变大。threshold0.5这个值不是永远最优下一章我会讲怎么在验证集上选阈值。批量预测比单窗口预测快很多。如果一张 1080p 图切成 224 窗口、112 步长大约会有 100 到 150 个 patch一次性np.stack喂给模型在 CPU 上也就一两秒如果写for patch in patches: model.predict(patch[None, ...])因为每次都要构建计算图很容易跑十几秒。滑窗会产生大量重叠框同一个坑会被多个窗口命中。需要做非极大值抑制 NMS保留概率最高的框去掉重叠度高的其他框。def iou(a, b): # a, b 都是 (x1, y1, x2, y2, score) ax1, ay1, ax2, ay2 a[:4] bx1, by1, bx2, by2 b[:4] inter_w max(0, min(ax2, bx2) - max(ax1, bx1)) inter_h max(0, min(ay2, by2) - max(ay1, by1)) inter_area inter_w * inter_h union_area (ax2 - ax1) * (ay2 - ay1) \ (bx2 - bx1) * (by2 - by1) - inter_area return inter_area / union_area if union_area 0 else 0 def nms(boxes, iou_threshold0.4): # 按分数从高到低排序逐个保留 boxes sorted(boxes, keylambda box: box[4], reverseTrue) kept [] while boxes: best boxes.pop(0) kept.append(best) boxes [box for box in boxes if iou(box, best) iou_threshold] return kept使用 NMS 时iou_threshold越大保留的重叠框越多取 0.4 左右同一个坑基本只会留下一个框。最终检测结果可以用cv2.rectangle画在图上保存到results/result.jpg作为你大作业里的“效果图”。5. 避坑指南道路坑洼检测大作业最常见的 5 个翻车点下面这些坑不解决轻则指标难看重则让你的 CNN 被老师当场问到“你在做什么”。每一条都是我用时间和分数换来的血泪经验。5.1 数据坑网图当训练集、直接 resize 丢小坑坑 1训练集全用网图验证集用手机实拍准确率直接跳水。现象训练准确率 95%验证准确率只有 60% 出头。两者来源不同风格差异太大。原因网图大多是完好的构图曝光均匀路面纹理干净手机实拍会有透视形变、阴影、车辆遮挡。模型记住的是“网图风格”不是坑洼本身。解决在训练集里混入 20% 左右的实拍图如果实在拿不到就用 ImageNet 预训练模型 VGG16 或 ResNet 做迁移学习冻结前面的卷积层只微调最后几层。这样即使训练风格单一底层的边缘和纹理特征也不会被带偏。坑 2直接把整张 1080p 测试图 resize 成 224x224 再输入 CNN。现象大坑能检测到小坑几乎全漏。原因大图缩小的过程中小坑从几十个像素变成两三个像素特征直接丢失。CNN 不是“看不清小坑”而是“小坑在缩小后已经不存在了”。解决测试阶段不要 resize 整图用滑窗裁切原图区域再把每个窗口单独 resize 成 224x224。滑窗天然保留了小坑的原始像素密度这也是我在第四章坚持用滑窗定位的原因。5.2 模型与推理坑负样本太干净、滑窗预测慢、随机种子没固定坑 3负样本太“干净”正常路面全是平整沥青。现象模型上线测试后把井盖、阴影、水渍、桥梁伸缩缝全部判成坑洼。原因负样本里缺少“伪坑洼”样本。CNN 没有见过井盖和阴影不知道它们和坑洼有什么区别。解决把负样本扩成四类难例路面阴影、井盖、水渍、道路标线裂纹。训练后如果发现某类误报集中把模型预测错的图加入训练集再做一轮微调这种方法在目标检测里叫 hard negative mining大作业里用上非常加分。坑 4滑窗检测一张图要二十秒演示现场直接冷场。现象单张测试图预测很慢窗口多的时候尤其明显。原因在同一张图上用 for 循环调用model.predict每个窗口都触发一次完整的 TensorFlow 计算图初始化速度极慢。解决先收集所有 patch统一np.stack后一次性model.predict这是最直接的性能提升。如果还慢把模型的输入尺寸改成 128x128滑窗时也按 128 裁切速度能再快两倍以上。坑 5没固定随机种子同一个代码跑两次结果不同。现象早停轮次不同验证准确率有小幅浮动拿到的“最优模型”也不一样。原因数据增强的随机变换、模型权重初始化和训练集 shuffle 都带随机性。没有种子实验不可复现。解决在数据增强、flow_from_directory和模型构建前都固定seed42同时设置 TensorFlow 全局随机种子。这样即使重跑只要数据不变结果基本一致。6. 给大作业加分的收尾技巧Grad-CAM 热力图和置信度阈值如果你觉得这套方案已经能跑通下面这两步还能再拉开一点差距让老师看到你懂“模型为什么有效”。6.1 用 Grad-CAM 证明 CNN 在看“坑”而不是“影子”分类准确率高不代表模型看的就是坑洼区域它可能在看阴影、树干或者图片水印。用 Grad-CAM 可以生成一张热力图直观展示 CNN 的决策依据。这里需要用到之前命名过的last_conv层。import tensorflow as tf def grad_cam_heatmap(model, image, layer_namelast_conv): grad_model tf.keras.models.Model( inputs[model.input], outputs[model.get_layer(layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_out, pred grad_model(np.expand_dims(image, axis0)) loss pred[:, 0] grads tape.gradient(loss, conv_out) weights tf.reduce_mean(grads, axis(0, 1, 2)) heatmap tf.reduce_mean(conv_out[0] * weights, axis-1) heatmap np.maximum(heatmap.numpy(), 0) heatmap heatmap / (np.max(heatmap) 1e-8) return heatmap这段代码的原理是用坑洼类别的输出对最后一个卷积层求梯度梯度大的通道就是对坑洼类别贡献更大的通道再把通道加权求和得到热力图。将热力图放大到原图尺寸后和原图叠加如果高亮区域集中在坑洞、裂缝处说明模型学到了真正有用的视觉特征。注意输入image必须是已经缩放、并保持和模型输入一致尺寸的数组不要再加 batch 维。6.2 在验证集上选择一个比 0.5 更合适的阈值滑窗和分类的默认阈值都是 0.5但在坑洼检测里漏掉一个坑比误报一个坑更致命。用验证集画出精确率和召回率曲线可以找到更适合作业目标的阈值。from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_true, y_prob) f1 2 * precision * recall / (precision recall 1e-8) best_idx np.argmax(f1) best_threshold thresholds[best_idx] print(fbest threshold: {best_threshold:.3f}, F1: {f1[best_idx]:.3f})如果你更在乎“不漏坑”可以把阈值再往下调一点让 recall 更高如果老师批评误报太多就上调节阈值。把这个最佳阈值替换掉滑窗函数里的threshold0.5测试结果会明显稳定。我现在拿到任何视觉任务第一件事都是先固定随机种子、跑通一个小模型再谈调参。参数调整看起来像玄学其实多数坑都出在数据分布和验证方式上。希望帮到你。本文还有配套的精品资源点击获取