ARTICLE DETAIL

资讯详情

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

TensorFlow交通标志检测识别实战:CNN模型训练与工程扩展

TensorFlow交通标志检测识别实战:CNN模型训练与工程扩展 简介这是一份面向智慧交通场景的人工智能实战项目围绕交通标志检测与识别展开适合熟悉Python和深度学习基础、希望走进图像识别领域的开发者学习。压缩包内文件总数为两百四十七个大小约五十五兆字节包含TensorFlow模型权重文件、Python源码、样例图片和说明文本其中权重文件由meta、index、data分片组成py脚本覆盖训练与评估流程目录结构清晰。项目以TensorFlow为核心框架使用卷积神经网络对交通标志图像进行特征提取和分类同时利用Numpy完成图像归一化与数据增强借助easydict简化配置信息读取完整覆盖从数据准备到模型训练、评估与保存的流程。目前已有181人学习。通过该资源可以获取可直接运行的检测识别脚本、训练好的模型文件及项目组织思路为后续在自动驾驶或智能交通系统中落地相关技术提供扎实参考。1. 交通标志检测与识别TensorFlow 实战项目拆解人工智能在智慧交通里落地最快、最容易出效果的方向不是自动驾驶那种动辄激光雷达加高精地图的重投入而是先把路上的标志看懂。这套交通标志检测与识别项目用 Python 3.5 TensorFlow 构建 CNN 模型覆盖了从图像预处理、模型训练到测试评估的完整流程。它包含的数据集、训练脚本和模型定义代码非常适合正在做 AI 大作业、毕设或者刚接触 CV 的从业者拿来复现。我拆完整个压缩包后确认它不依赖 GPU 也能跑通小规模训练踩过的坑我都记在后面的章节里。2. 环境准备与项目结构把压缩包变成能跑的代码2.1 版本选型为什么是 Python 3.5 和 TensorFlow 1.x拿到 zip 解压之后先别急着跑第一件事是确认版本。项目要求 Python 3.5TensorFlow 大于等于 1.0.0。这个组合放到今天看确实有点旧但它是这个项目正常的运行环境。注意TensorFlow 2.x 移除了大量 1.x 的 API比如tf.Session()、tf.placeholder()、tf.Variable()的旧式用法。如果你直接用 TF 2.x 跑这个项目会在导入模型定义文件时就报错。我的做法是用 Anaconda 单独建一个虚拟环境conda create -n traffic_sign python3.5 conda activate traffic_sign pip install tensorflow1.14.0 pip install numpy easydict参数说明tensorflow1.14.0是 1.x 系列中最后几个稳定的版本之一对 Python 3.5 兼容良好NumPy 建议装 1.16 左右的版本因为新版 NumPy 在部分 API 上和旧版 TensorFlow 存在兼容性问题。安装顺序上先装 TensorFlow 再装 NumPy 会比较稳避免依赖解析时自动升级到不兼容版本。验证安装是否成功在命令行里执行python -c import tensorflow as tf; print(tf.__version__)能看到类似1.14.0的输出就说明 TensorFlow 部分没问题。如果报错提示缺失 DLL 或者找不到tensorflow模块大概率是虚拟环境没激活或者装错了 CPU/GPU 版本。2.2 解压后的目录到底有什么解压后我看到的目录结构大致是这样的不同打包者可能略有差异Traffic_sign_detect-master/ ├── data/ │ ├── train/ # 训练集图像 │ ├── test/ # 测试集图像 │ └── labels/ # 标注文件 ├── model/ │ ├── lenet.py # LeNet 网络结构定义 │ └── vgg.py # VGG 网络结构定义 ├── preprocessing/ │ ├── resize.py # 图像缩放 │ └── normalize.py # 归一化 ├── train.py # 训练入口 ├── test.py # 测试入口 └── config.py # 配置项含路径和超参数需要说明的是交通标志数据集常见的有德国 GTSRB 和国内的 TT100K 两类这套项目的data/目录里可能自带了部分样本也可能需要你自行放置。如果train/目录为空就需要自己去下载数据集放进去目录结构保持类别名/图像文件的形式即可。它的标签文件通常是 CSV 格式包含文件名和对应类别 ID预处理脚本会按这个映射关系去读图。config.py是每次改动最频繁的文件里面定义了图像尺寸、批大小、学习率、类别数等参数。我一般会先打开这个文件把里面的路径改成绝对路径否则相对路径在 IDE 和命令行之间切换时容易出错。2.3 用 easydict 读取配置文件项目里用 easydict 来操作配置数据这个库的作用是把字典的键访问从config[batch_size]变成config.batch_size。在跑训练之前我习惯先写一段脚本把配置读出来看一眼确认路径没配错from easydict import EasyDict as edict cfg edict() cfg.BATCH_SIZE 64 cfg.LEARNING_RATE 0.001 cfg.CLASS_NUM 43 cfg.DATA_PATH ./data/train print(cfg.BATCH_SIZE) print(cfg.DATA_PATH)逻辑说明edict()创建了一个类字典对象赋值之后可以直接通过.访问属性。实际项目中config.py里还会写cfg edict({...})这种一次性初始化方式当配置项较多时更简洁。包含多层嵌套时它也能逐层点号读取这就是选 easydict 的原因——省掉了cfg[model][backbone]这种繁琐写法。3. 数据预处理与模型构建核心代码逐段拆解3.1 图像读取和缩放不要直接拿原图训练交通标志图像尺寸不一有的大几百像素有的才几十像素。CNN 要求输入尺寸固定所以第一步就是统一缩放。这个项目里常见的做法是缩放到 32×32 或 64×64既保留关键特征又不至于计算量太大。import cv2 import numpy as np def load_and_resize(img_path, target_size(32, 32)): img cv2.imread(img_path) if img is None: raise ValueError(图像读取失败: {}.format(img_path)) # 注意 OpenCV 读进来是 BGR 顺序转成 RGB 再处理 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) return img参数说明cv2.INTER_AREA是缩小图像时最合适的插值算法可以避免出现明显锯齿target_size(32, 32)是输入分辨率如果后续分类效果不理想可以改成(64, 64)试试这取决于你硬件能承受的算力。cvtColor那行是隐藏坑——训练好的模型在 OpenCV 下推理如果颜色怪异多半是漏了这一步。3.2 归一化和数据增强让模型更快收敛原始像素值范围是 0 到 255直接喂进 CNN 会导致梯度更新不稳定。常见的做法是缩放到 0 到 1 之间或者做标准化。def normalize(img): # 方法一除以 255 缩放到 [0, 1] img img.astype(np.float32) / 255.0 # 方法二标准化到均值为 0、方差为 1 mean np.mean(img, axis(0, 1, 2)) std np.std(img, axis(0, 1, 2)) img (img - mean) / (std 1e-8) return img逻辑说明两种方式在项目注释里都有提到astype(np.float32)这一步很关键因为 uint8 类型做除法会直接取整导致所有像素值变成 0 或 1训练直接报废。1e-8是防止某些图像区域标准差为 0 时出现除零错误。数据增强部分建议用 TensorFlow 自带的图像处理 API在训练阶段动态随机变换相当于每轮都看到略微不同的样本def augment(img): # 随机水平翻转概率 50% if np.random.rand() 0.5: img cv2.flip(img, 1) # 随机旋转 -10 到 10 度 angle np.random.uniform(-10, 10) matrix cv2.getRotationMatrix2D((img.shape[1] / 2, img.shape[0] / 2), angle, 1) img cv2.warpAffine(img, matrix, (img.shape[1], img.shape[0])) return img这里的随机翻转对部分交通标志是有问题的——有些标志具有方向性比如靠右行驶的指示牌翻转后会变成语义错误的内容。所以数据增强不能盲目套用要结合具体的交通标志类别去设计。3.3 LeNet 与 VGG两种网络结构的选型逻辑压缩包里的model/目录一般包含两种网络定义其中lenet.py结构简单层数浅、参数少在 32×32 的低分辨率输入上表现良好适合快速验证流程能不能跑通vgg.py则是 VGG 风格层数更深需要更多数据和更长的训练时间但特征表达能力更强。# model/lenet.py 核心片段 import tensorflow as tf def lenet_network(x, num_classes43): # x 的形状: [batch, height, width, channels] net tf.layers.conv2d(x, filters32, kernel_size5, activationtf.nn.relu, nameconv1) net tf.layers.max_pooling2d(net, pool_size2, strides2, namepool1) net tf.layers.conv2d(net, filters64, kernel_size5, activationtf.nn.relu, nameconv2) net tf.layers.max_pooling2d(net, pool_size2, strides2, namepool2) # 全连接前先展平 flatten tf.layers.flatten(net) dense tf.layers.dense(flatten, units512, activationtf.nn.relu, namefc1) # 输出层不需要激活函数交给 softmax_cross_entropy 处理 logits tf.layers.dense(dense, unitsnum_classes, namefc2) return logits参数说明kernel_size5是 LeNet 的经典配置感受野适中适合捕获标志的边框和内部图形特征filters从 32 逐渐加深到 64属于 CNN 的常见设计模式——空间分辨率逐层减半通道数逐层翻倍。flatten这一步经常有人忘记卷积输出是四维的[batch, h, w, c]全连接层只接受二维输入忘记展平会直接报维度不匹配的错误。3.4 训练脚本主流程数据管道到模型保存训练脚本做的事可以用一段伪代码概括读取所有图像路径和标签、打乱顺序、分批喂给网络、计算损失、反向传播更新权重、定期保存 checkpoint。import tensorflow as tf def train(): # 1. 加载数据路径和标签 images, labels load_data(./data/train) # 2. 定义输入占位符 x tf.placeholder(tf.float32, [None, 32, 32, 3], nameinput_x) y tf.placeholder(tf.int32, [None], nameinput_y) # 3. 构建网络 logits lenet_network(x, num_classes43) # 4. 损失函数和优化器 loss tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits( logitslogits, labelsy)) optimizer tf.train.AdamOptimizer(learning_rate0.001).minimize(loss) # 5. 初始化会话并开始训练 saver tf.train.Saver(max_to_keep5) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(30): for batch in range(num_batches): batch_x, batch_y next_batch(images, labels, batch_size64) sess.run(optimizer, feed_dict{x: batch_x, y: batch_y}) # 每轮结束保存一次 checkpoint saver.save(sess, ./checkpoints/model.ckpt, global_stepepoch)逻辑说明tf.placeholder是 TensorFlow 1.x 的标准写法先定义数据入口后续运行时通过feed_dict往里塞真实数据。tf.nn.sparse_softmax_cross_entropy_with_logits要求标签是整数索引而不是 one-hot 编码如果你的标注文件已经是 one-hot就需要换成另一个 API。max_to_keep5意味着 checkpoint 目录里最多保留 5 份历史权重防止磁盘被占满。AdamOptimizer 是这个项目的默认选择因为它相比 SGD 不敏感于学习率的设置在中小规模数据集上更容易获得可用的收敛结果。如果发现 loss 震荡不降可以把学习率降低一个数量级再试。4. 训练调参与避坑指南常见问题的修复记录4.1 问题一TensorFlow 2.x 环境直接报错现象打开 train.py 运行报ModuleNotFoundError: No module named tensorflow.contrib或者提示Session未定义。原因TensorFlow 2.x 移除了 contrib 目录和旧式 Session API而这套项目大量使用了 1.x 时代的写法。解决创建 Python 3.5 TF 1.14 的独立虚拟环境如 2.1 节所示。不用新版本是因为这个项目的代码结构决定了改造成本高换旧环境是最经济的选择。4.2 问题二easydict 访问嵌套 JSON 报 KeyError现象配置里存在cfg.model.backbone代码按这个写法运行到部分场景时抛KeyError。原因某些路径是手动创建的 Python dict而不是 easydict 对象混用两种数据结构导致访问方式不一致。解决统一入口——所有配置在config.py里统一用edict创建其他模块只导入cfg不允许自行创建 dict。如果必须处理外部 JSON 数据先做一步转换import json from easydict import EasyDict as edict with open(config.json) as f: cfg edict(json.load(f))4.3 问题三训练时 loss 一直是 4.0 左右不动现象模型训练了几轮交叉熵损失停留在 4.0 附近准确率不升。原因logits 输出层和损失函数之间维度对不上或者网络输出没有被正确连接到计算图。当类别数是 43 时随机初始化的交叉熵理论上就是 ln(43) ≈ 3.764.0 说明模型完全没有学到东西。解决先跑一小批数据比如 8 张图 5 个 step看 loss 是否从接近 ln(43) 开始逐步下降。如果一直不动打印 logits 的维度是不是[batch, 43]以及 labels 的取值范围是否在[0, 42]之间。4.4 问题四OpenCV 读图颜色不对训练结果一塌糊涂现象同样的代码在部分机器上训练出的模型测试时红色标志识别成了蓝色标志。原因OpenCV 默认以 BGR 顺序读图如果没转成 RGB模型看见的颜色通道语义就发生了错位。这不算 bug但影响很大——导向标志通常以蓝底白图案为主通道顺序错了浅层卷积核学到的颜色特征全部失效。解决在预处理管线里固定写cv2.cvtColor(img, cv2.COLOR_BGR2RGB)然后统一送入模型的张量顺序。最好在数据管道入口设置一个断言检查传入图像的通道数和顺序避免后续代码散落各处。4.5 问题五训练到一半显存溢出现象GPU 环境下 batch_size 设得比较大训练到几百个 step 后程序崩掉提示ResourceExhaustedError。原因不仅占显存的是模型权重还有计算图中间层的所有激活输出。网络越深、batch 越大中间数据量越是翻倍增长。解决把batch_size从 64 改到 32 或 16同时把图像尺寸从 64×64 降到 32×32。再不行就启用 TF 的显存动态分配config tf.ConfigProto() config.gpu_options.allow_growth True sess tf.Session(configconfig)allow_growthTrue让 TensorFlow 按需申请显存而不是一次性占满全部显存。4.6 问题六中文路径导致读取失败现象数据集放在带有中文目录名比如D:\下载\交通标志\Train下读取图片时部分文件报错。原因OpenCV 的imread在 Windows 下对中文路径支持得不好返回空对象后面所有操作都跟着失败。解决把数据集放到纯英文路径下。如果没法移动文件用np.fromfile配合cv2.imdecode绕过去import numpy as np import cv2 def imread_chinese(path): data np.fromfile(path, dtypenp.uint8) img cv2.imdecode(data, cv2.IMREAD_COLOR) return img提示这个方式能解决读取问题但后续保存结果到中文路径时也会遇到类似的编码问题最省事的方法还是目录名全用英文。5. 模型评估与验证方法准确率之外还要看什么5.1 评估脚本如何计算指标test.py里默认会输出整体准确率但这在交通标志识别里不够。我习惯在此基础上按类别拆分准确率因为有些标志类别样本少整体准确率把它的低分掩盖了。import tensorflow as tf import numpy as np def evaluate(): images, labels load_data(./data/test) x tf.placeholder(tf.float32, [None, 32, 32, 3]) y tf.placeholder(tf.int32, [None]) logits lenet_network(x, num_classes43) saver tf.train.Saver() with tf.Session() as sess: # 加载最后一次保存的 checkpoint saver.restore(sess, tf.train.latest_checkpoint(./checkpoints)) predicted tf.argmax(logits, axis1) preds sess.run(predicted, feed_dict{x: images}) correct np.equal(preds, labels) overall_acc np.mean(correct) print(整体准确率: {:.4f}.format(overall_acc)) # 按类别统计准确率 for cls in range(43): mask (labels cls) if np.sum(mask) 0: cls_acc np.mean(correct[mask]) print(类别 {} 准确率: {:.4f} (样本数 {}).format( cls, cls_acc, np.sum(mask)))逻辑说明saver.restore是从 checkpoint 恢复模型权重而tf.global_variables_initializer()是初始化新权重两者不能同时用否则恢复的权重会被初始化覆盖。按类别统计的价值在于——如果某个类别的样本数很少但正确率是 0就要考虑是数据不够还是这个类别的特征和另一个类别太像需要针对性补数据。5.2 可视化错误样本找到模型混淆的具体对象准确率数字只能说明整体水平实际动手改进时需要看模型把哪两个类别搞混了。可以输出一个混淆矩阵找出错误集中区域。常见的混淆对是限速 30 和限速 50 这种外形极度相似的标志它们之间的区别可能就是数字笔画粗细在低分辨率下人眼都容易认错。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix cm confusion_matrix(labels, preds) # 找出被错误最多的 5 对类别组合 errors [] for i in range(43): for j in range(43): if i ! j and cm[i][j] 0: errors.append((cm[i][j], i, j)) errors.sort(reverseTrue) for count, true_cls, pred_cls in errors[:5]: print(真实类别 {} 被识别为 {}{} 个样本.format( true_cls, pred_cls, count))这一步跑完之后最先要做的是去数据目录里看看这些样本是不是本身就标注错了。我遇到过标注错了一半的类别模型怎么调准确率都上不去。数据质量永远排在模型结构前面。5.3 推理检查不在训练集里的照片才有说服力训练脚本跑出来的测试集准确率可能有 99%但真实路况下依然翻车。一个务实做法是从网上下载几张真实道路场景图自己用程序裁剪出标志区域喂给模型看识别结果。# 单张图片推理 img load_and_resize(road_sign_sample.jpg, target_size(32, 32)) img normalize(img) img_batch img[np.newaxis, :, :, :] # 增加 batch 维度 with tf.Session() as sess: saver.restore(sess, tf.train.latest_checkpoint(./checkpoints)) result sess.run(predicted, feed_dict{x: img_batch}) print(识别结果为类别:, result[0])如果真实场景图识别准确率明显低于测试集指标优先检查两个方向一是测试集的图像和真实图像的清晰度、光照条件差异大不大二是模型是否过拟合了训练集中的背景信息。交通标志检测任务里常有模型靠背景猜类别的现象——标志被抠图换背景之后准确率暴跌基本可以判定模型学的是背景而不是标志本身。此时需要在训练数据里加入更多不同背景的负样本或者对标志区域加一些随机遮挡。6. 从识别到检测扩展目标的实用思路6.1 在测试图上框出所有交通标志原始项目只做识别也就是输入已经是裁剪好的标志图。但真实场景下一张照片里包含大量无关区域需要先定位再识别。常见的检测思路是滑动窗口加分类器用不同大小的窗口在图像上移动每块区域都送入已经训练好的 CNN 分类器判断是否有交通标志。def sliding_window_detect(image, step16, window_sizes[(64, 64), (96, 96)]): h, w image.shape[:2] for win_w, win_h in window_sizes: for y in range(0, h - win_h, step): for x in range(0, w - win_w, step): crop image[y:ywin_h, x:xwin_w] # 送入识别模型 result classify(crop) if result is not None: boxes.append((x, y, win_w, win_h, result)) return boxes这种暴力方法的缺点是计算量大一张 1080p 图像要检测上千个窗口。加速手段是先用颜色特征粗筛——交通标志普遍具有高饱和度的红、蓝、黄色HSV 空间下按颜色阈值把不可能区域直接过滤掉剩余候选窗口数量能减少 90% 以上然后再跑 CNN 分类器。6.2 完整项目扩展路线如果想把这套项目做到一个更接近工程化的状态我的建议是沿着三个方向扩展。第一个方向是把单张图片识别改成视频流检测用cv2.VideoCapture逐帧读取配合帧间目标跟踪去避免重复计算。第二个方向是把 LeNet 替换成 YOLO 或 SSD 这种单阶段检测器从源码层面支持端到端的检测加识别。第三个方向是换用更现代的模型结构比如 MobileNet 系列做迁移学习在保持精度的同时把模型体积压缩到适合嵌入式设备部署的大小。这中间有一个值得注意的取舍检测算法从滑动窗口升级到 YOLO 之后精度和速度都会显著提升但代码复杂度也随之变大。如果你的目标只是把作业完整跑通LeNet 加滑动窗口已经足够如果你想拿这个项目去面试或者说展示工程能力建议至少把 YOLO 的推理部分接进来。6.3 权重文件管理的习惯最后说一下 checkpoint 的维护。我在多次训练中养成的习惯是每次训练都单独建一个带时间戳的目录把模型权重、训练日志和配置文件全部放进去。这样即使后续调参把效果弄得更差也能回滚到之前表现最好的版本。以前我在一个目录里反复覆盖保存结果某次训练崩溃后连之前的好权重都被写坏了那种后悔药没得吃的感受体验过一次就够了。从那以后我每次训练都强制走一遍「新目录、新日志、新权重」的流程虽然只是多敲几个字母但换来的是一整天的安心。希望这套拆解能帮你在交通标志识别项目上少走一圈弯路快速拿到一个能跑、能改、能说得清楚的结果。本文还有配套的精品资源点击获取
返回列表