ARTICLE DETAIL

资讯详情

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

TensorFlow车牌识别项目源码全解析:从数据流水线到模型部署

TensorFlow车牌识别项目源码全解析:从数据流水线到模型部署 简介面向深度学习开发者和人工智能初学者的TensorFlow车牌识别完整项目源代码提供从图像数据到模型训练与预测的整套实现。项目针对车牌省份简称、城市代号和编号分别建立了独立的训练与识别模块便于分段学习和灵活替换也适合作为计算机视觉课程设计或毕业设计的参考工程。资源包共含2000个文件以BMP格式车牌样本图像和TensorFlow模型存档含index、meta、checkpoint文件为主另有Python源码脚本、配置说明文档等整体压缩包大小约356.03MB。目前已有4282人浏览学习对于希望快速掌握TensorFlow图像识别全流程、理解模型训练与推理环节的开发者来说具有直接的借鉴和复用价值。通过该项目学习者可了解数据目录编排、模型保存与恢复、多类别分类等实践要点并能够在此基础上扩展自己的数据集或改进网络结构是深度学习和机器视觉入门阶段一份扎实的参考资源。1. 一份“完整”的 TensorFlow 车牌识别源代码到底该长什么样很多人把“车牌识别项目”当成一个单点模型输入图片输出车牌号。真去跑一份声称“完整”的源代码就会发现模型在整条链路里可能只占 20%剩下 80% 是字符集约定、数据流水线、标签编码、后处理逻辑和部署脚本。这篇文章要解决的问题就是拿到一份 TensorFlow 车牌识别完整项目源代码后你应该按什么顺序读、每个文件在干什么、哪些参数动了会影响识别率、跑不通时先查哪里。识别的硬骨头不是“检测出车牌”而是中文省份简称的 31 分类、字母与数字在相似字形上的区分以及训练数据里各省车牌分布天然不平衡。把这条链路拆开看清楚比直接改模型结构更有用。适合自己动手训练过一两个视觉模型、现在想把项目做成型的工程师往下读。2. 源码工程结构数据、配置、模型、工具四层怎么拆一份能长期维护的车牌识别源码目录结构本身就是文档。常见的拆法是按数据流方向分层原始数据、离线处理脚本、训练入口、模型定义、推理工具。不要把图像、脚本、模型定义全堆在根目录否则跑通一次容易想换数据集或者调超参的时候会非常痛苦。2.1 先按数据流拆目录让“全部源代码”可被逐层阅读我一般会把工程拆成下面这种结构它不算新但很抗改license_plate_tf/ ├── configs/ │ ├── data.yaml │ └── train.yaml ├── data_process/ │ ├── build_tfrecord.py │ ├── char_map.py │ └── augments.py ├── models/ │ ├── detector.py │ ├── recognizer.py │ └── losses.py ├── tools/ │ ├── train.py │ ├── evaluate.py │ └── export_tflite.py ├── inference/ │ ├── decode.py │ └── service.py └── requirements.txt配置文件单独放出来训练分辨率、字符集路径、批大小就不用在代码里到处找。数据预处理脚本独立成文件因为车牌数据的标注格式经常要换——可能是 XML、JSON也可能是一张 txt 里四行坐标。工具层只负责编排训练和评估不写业务逻辑。推理目录里的decode.py是模型输出转成车牌字符串的唯一入口这个文件值得好好读后面章节会单独展开。启动训练之前先确认环境TensorFlow 本身用 pip 正常安装即可CPU 版先把数据流水线跑通再切 GPU 版。工程代码里尽量不要硬编码绝对路径用pathlib.Path基于工程根目录去拼换机器的时候能少踩一半坑。2.2 车牌字符集与标签编码31 个汉字加字母数字的映射表车牌识别的标签系统和通用 OCR 不一样。普通蓝牌是定长 7 位第 1 位是省份汉字第 2 位是发牌机关字母第 3 到第 7 位是数字和字母的组合。省份汉字在标准民用牌照里常用 31 个字母这边要避开 I 和 O避免和数字 1、0 混淆。# data_process/char_map.py PROVINCES 京津晋冀蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新 LETTERS ABCDEFGHJKLMNPQRSTUVWXYZ # 去掉 I、O DIGITS 0123456789 ALL_CHARS PROVINCES LETTERS DIGITS CHAR_TO_IDX {c: i for i, c in enumerate(ALL_CHARS)} IDX_TO_CHAR {i: c for i, c in enumerate(ALL_CHARS)} NUM_CLASSES len(ALL_CHARS) # 31 24 10 65标签编码的做法是“每个字符位置一个 one-hot 向量”。识别模型输出形状是(7, 65)即 7 个位置、每个位置 65 个类。所以训练标签的 shape 是(7, 65)而不是一个稀疏的整数序列。ALL_CHARS的顺序一旦确定就不要随便改否则重训模型后旧权重全部作废。这个映射表要同时用在训练代码和推理代码里建议用单独的char_map.py维护不要复制。字符集顺序为什么值得单独拎出来说因为很多人拿到源码后第一件事就是加字符比如想支持新能源绿牌的 8 位字符于是往ALL_CHARS里塞一个“使”或“领”。可行但必须同步重建 TFRecord、同步修改模型输出维度。改字符集不改标签的错位 bug在识别项目里是最难排查的静默错误之一。2.3 用 tf.data 把图像和标签对接进训练流水线处理车牌图片的标准路径是读图、转灰度或保留 RGB、做透视矫正、缩放到统一尺寸、归一化。透视矫正这步非常关键因为车牌在画面里往往是斜的直接用矩形框裁剪喂给识别网络字符边缘会被背景干扰。# data_process/build_tfrecord.py import tensorflow as tf def _parse_function(example_proto): feature_desc { image: tf.io.FixedLenFeature([], tf.string), width: tf.io.FixedLenFeature([], tf.int64), height: tf.io.FixedLenFeature([], tf.int64), label: tf.io.FixedLenFeature([7], tf.int64), # 7 个字符的索引 } parsed tf.io.parse_single_example(example_proto, feature_desc) image tf.image.decode_jpeg(parsed[image], channels3) image tf.image.resize(image, [TRAIN_H, TRAIN_W]) image tf.cast(image, tf.float32) / 127.5 - 1.0 # 归一化到 [-1, 1] label tf.one_hot(parsed[label], depthNUM_CLASSES) return image, label dataset tf.data.TFRecordDataset(tfrecord_files) dataset dataset.map(_parse_function, num_parallel_callstf.data.AUTOTUNE) dataset dataset.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)逻辑说明parse_single_example把 TFRecord 里存的一串字节还原成图像张量、车牌宽高和标签索引。label在 TFRecord 里保存的是 7 个整数每个字符在CHAR_TO_IDX中的下标解析后再用tf.one_hot展开成(7, 65)。归一化统一用[-1, 1]不要一部分代码减均值另一部分只除 255这个不一致会导致训练曲线一直抖动。字符集、TFRecord、模型输出维度这三者的关系是TFRecord 里的 label 必须是CHAR_TO_IDX的词典下标模型输出通道数必须是len(ALL_CHARS)最后推理还原字符串必须用IDX_TO_CHAR。任何一环对不上程序不一定报错但识别结果会错得莫名其妙。工程上强烈建议在train.py里加一条断言启动时检查model.output.shape[-1] NUM_CLASSES能挡住一大批低级事故。字符分组数量典型作用是否参与训练省份汉字31车牌第 1 位分类任务主力承担部分是字母去 I/O24第 2 位及后续位注意字形混淆是数字10高频出现样本最充裕是背景/空白0-1定长解码时不用单独留类否省份汉字类别在公开数据集里频率不均比如“京”“苏”“粤”数据量大“藏”“琼”可能只有几百张。这个问题会直接影响训练后面章节单独说处理办法。3. 检测与识别解耦模型结构怎么选代码怎么写车牌识别的标准技术路线是两级先检测车牌位置再裁剪并识别字符。常见做法是检测子网络用轻量目标检测头识别子网络用定长 CNN。检测负责回答“车牌在哪”识别负责回答“车牌是什么”。这两个任务在标注成本和解码逻辑上完全不一样混在一个模型里会让“完整源代码”变成一团互相牵扯的代码。3.1 检测子网络回归车牌四角点而不是矩形框自然场景里的车牌是倾斜的矩形检测框带回大量背景字符识别时会受到干扰。更可靠的做法是让检测头直接回归车牌的四个角点一共 8 个坐标值。不用旋转框的角度参数理由很简单四角点不受角度定义域限制也不会有 90 度和 -90 度等价的回归歧义。损失函数用 Smooth L1Huber loss会比纯 L2 稳很多坐标值在 0 到 1 之间归一化到特征图尺寸避免大数值梯度。这个设计的另一个好处是拿到四角点后可以直接用 OpenCV 的getPerspectiveTransform把车牌矫正成正视角喂给识别子网络之前完成对齐。如果你想让识别模型也能独立跑通可以先用标注文件把四个角点的最小外接矩形裁出来跳过检测直接训练识别。代码库里这两个子网络的权重互相独立训练顺序也独立排错时可以先冻结检测、专心调识别。3.2 识别网络别一上来就上 OCR 大模型先写好定长 7 字符 CNN识别网络的目标是把矫正后的车牌图映射成一个(7, 65)的输出。对普通蓝牌来说定长 7 完全够用。别第一步就引入 CTC 或者 Transformer 解码器项目里 90% 的场景不需要变长能力反而把代码复杂度成倍放大。# models/recognizer.py import tensorflow as tf from tensorflow.keras import layers class PlateRecognizer(tf.keras.Model): def __init__(self, num_classes65, char_len7): super().__init__() self.char_len char_len self.conv_stack tf.keras.Sequential([ layers.Conv2D(32, 3, strides2, paddingsame, activationrelu), layers.Conv2D(64, 3, strides2, paddingsame, activationrelu), layers.Conv2D(128, 3, strides2, paddingsame, activationrelu), layers.Conv2D(256, 3, strides2, paddingsame, activationrelu), layers.GlobalAvgPool2D(), ]) self.fc layers.Dense(512, activationrelu) self.logits layers.Dense(num_classes * char_len) def call(self, images, trainingFalse): x self.conv_stack(images, trainingtraining) x self.fc(x) x self.logits(x) return tf.reshape(x, [-1, self.char_len, self.num_classes])逻辑说明四个卷积层把输入逐步下采样GlobalAvgPool2D把特征图压成向量最后全连接层输出65 * 7个值再 reshape 成(batch, 7, 65)。这样写的好处是每一层都看得懂替换主干也方便——想试 ResNet、MobileNet 时只需换conv_stack而不动后面的分类头。参数细节输入分辨率建议(64, 192)左右宽高比接近车牌本身的 3:1。卷积核数量从 32 翻到 256看起来参数不多但对字符分类已经够用。真正让识别率上不去的原因往往不是模型太小而是数据里的字符没对齐——透视矫正做得差字符位置偏移半个像素卷积网络再深也救不回来。选模型这件事在工程上还有一个现实维度TensorFlow 与 PyTorch 的流行趋势这两年让不少团队犹豫。结论很直接车牌识别属于中小型模型任务两种框架实现成本相当。选 TensorFlow 的充分理由是整套项目源代码要用 TFRecord、TF Serving、TensorFlow Lite 打通部署链路如果团队以前就用 PyTorch 写检测那也别硬换稳定落地比赶框架潮流重要。3.2.1 所以源码里模型文件应该保持“单输入单输出”有些完整项目会把检测输出和识别输出拼在一起搞成多任务共享骨干。对车牌这种业务来说收益有限还增加训练负担。源码里两个模型文件、两份权重、两个训练入口是最清晰的结构。共享骨干意味着所有车牌数据都要同时带框和字符标注数据不齐时训练直接崩。4. 训练阶段的参数、损失函数与数据增强模型结构定了训练参数和数据处理才是决定识别率上限的部分。这一章给出可以直接抄的训练配置、数据增强策略以及几个高概率踩到的训练问题。4.1 训练参数表从 batch size 到学习率热身先把一组在 3 万到 5 万张车牌数据上表现稳定的参数列出来你可以从这组值开始调。参数推荐值说明输入尺寸64 x 192识别模型输入别用 224 x 224 浪费计算量批大小64显存不够就减半不要改分辨率初始学习率1e-3配合热身策略用学习率衰减Cosine比 step 衰减更省心热身轮数3 epoch前 3 轮线性升到 1e-3训练轮数30-50数据集小就增加轮数配早停优化器AdamW权重衰减 1e-4采样方式省份平衡采样解决汉字样本不均衡训练入口的代码可以写成 Keras 风格# tools/train.py model.compile( optimizertf.keras.optimizers.AdamW(weight_decay1e-4), losstf.keras.losses.CategoricalCrossentropy(from_logitsTrue), metrics[accuracy], ) callbacks [ tf.keras.callbacks.CosineDecay( initial_learning_rate1e-3, decay_stepstotal_steps ), tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), ] model.fit(train_dataset, validation_dataval_dataset, epochs50, callbackscallbacks)逻辑说明from_logitsTrue表示模型输出的是没经过 softmax 的原始 logitsKeras 会自己在损失函数里做 softmax数值上比先 softmax 再算交叉熵更稳。CosineDecay从这个名字就能看出来是余弦退火。损失函数用CategoricalCrossentropy即可因为标签是 one-hot 形式。注意这里不要用sparse_categorical_crossentropy除非你把标签存成整数。用错损失函数不报错但 shape 对不上时的表现是 loss 直接变成负数或乱跳。训练时监控两项整体 accuracy 和前 2 位字符的 accuracy后者更能反映省份简称的识别情况。4.2 数据增强透视变换、HSV 扰动和遮挡模拟车牌数据增强有个反直觉的原则不要做水平翻转。车牌字符串和形状都带绝对方向性翻转等于把标签改成错的。常见增强组合是随机透视变换、亮度饱和度扰动、轻微模糊和局部遮挡。# data_process/augments.py import tensorflow as tf def augment(image, label): # 透视扰动模拟拍摄角度变化 image tf.image.random_perspective( image, distortion0.2, fill_value0 ) # 亮度对比度扰动 image tf.image.random_brightness(image, max_delta0.2) image tf.image.random_contrast(image, lower0.8, upper1.2) # 随机遮挡字符加强模型鲁棒性 mask tf.ones_like(image) h, w image.shape[:2] x, y tf.random.uniform([], 0, w), tf.random.uniform([], 0, h) bw, bh tf.random.uniform([], 0.1, 0.3) * w, tf.random.uniform([], 0.1, 0.2) * h mask tf.image.pad_to_bounding_box( mask, 0, 0, tf.shape(image)[0], tf.shape(image)[1] ) return image * mask, labelrandom_perspective的distortion参数不要超过 0.3太大会把字符扭曲到失真的程度反而降低训练效果。遮挡模拟的作用是让模型不要只依赖某几个强特征字符。整张图随机加一个矩形黑块位置和大小都随机模拟摄像头前有异物遮挡的现场情况。增强操作全部在tf.data的 map 阶段做好处是利用 TensorFlow 原语直接上 GPU 或并行 CPU 流水线不额外增加磁盘 I/O 压力。数据量大之后增强本身会成为瓶颈这时候调整num_parallel_calls或改用tf.data.experimental.service做分布式数据分发。4.3 训练不收敛或 loss 震荡时的排查顺序第一个排查点是字符集映射是否错位。打印一个 batch 的标签和对应 char_map 映射表肉眼看字符顺序是否为预期车牌号。第二个排查点是学习率loss 在第几个 epoch 开始震荡如果前 3 个 epoch 内就震荡说明初始学习率偏大。第三个排查点极其隐蔽训练集和验证集的图像增强不同步验证集也做了随机遮蔽会导致 val loss 永远下不来。省份汉字类别不均衡的问题靠普通交叉熵会牺牲“藏”“琼”这些低频字符。两种常用解法一是按省份做采样每个 batch 里确保各省数量接近二是给损失函数按类别频率加权重。采样方式实现简单、效果直接我更推荐它。数据准备阶段按省份字段分组每组单独一个 TFRecord然后interleave时均匀取文件就能做到基本均衡。如果训练 EPOCH 内 loss 一直降但在验证集上停滞先怀疑数据泄漏原始图片有没有在裁剪车牌的时候混入边框边框里的汉字和底纹会被模型记住公共数据集的坑大多在这。这个检查光看代码看不出来必须抽验证集图片实际看一眼。5. 推理端到端代码从模型输出到车牌字符串模型训练完真正交付的不是model.h5而是“输入一张图、输出一个车牌字符串”的完整函数。解码这一步看似简单实际上写着写着就容易变成一坨裸奔的 numpy 循环。这里给出一个拿来就用、可以嵌进 API 服务的推理模块。# inference/decode.py import numpy as np def decode_logits(logits, idx_to_char): # logits: [7, 65] 或 [batch, 7, 65] pred_idx np.argmax(logits, axis-1) # [7] plate .join(idx_to_char[i] for i in pred_idx) confidence float(np.max(tf.nn.softmax(logits, axis-1), axis-1).numpy().mean()) return plate, confidence # 使用示例 # logits recognizer(tf.expand_dims(cropped_img, 0))[0] # plate, conf decode_logits(logits.numpy(), IDX_TO_CHAR)逻辑说明对每个位置直接取 argmax 得到字符下标然后按IDX_TO_CHAR还原成字符串即可。confidence 取 7 个位置置信度的均值判断识别结果可不可信。这个指标在业务上够用但要注意单字符错误被平均置信度稀释的问题比如 7 个字符里错 1 个mean 之后数字还是很高。更严格的评估指标是 EEP完全正确率即一个 batch 里整串完全正确的比例发布模型时的性能指标应该以 EEP 为准。推理阶段还要处理一个容易忽略的细节检测框坐标和矫正图的尺寸基准。如果检测输出坐标是在原图上而识别模型输入是64x192那么做透视变换时需要把四角点坐标同步缩放。这里建议直接记住“用原图角点做变换、把目标尺寸定为识别输入尺寸”避免先裁剪再缩放的两次插值损失。TensorFlow 项目落到边缘设备时通常要把识别模型转成 TensorFlow Lite 格式。转换时的关键参数是校准数据集int8 量化需要准备几百张典型车牌图做统计校准集要和训练集分布一致否则量化之后字符识别率可能掉 2 到 5 个百分点。float16 量化损失小很多适合 GPU 设备int8 适合纯 CPU 或 TensorFlow Lite Micro 场景。转换时用tf.lite.TFLiteConverter.from_keras_model一行代码就能做但真正决定量化质量的是校准数据而不是转换本身。最后一个工程建议把解码、置信度阈值和业务输出做一个薄薄的 service 层对外只暴露recognize(image) - (plate, confidence)接口。这样无论是接道闸控制还是接管理系统的数据库都只在 service 层改代码模型文件、解码逻辑、字符映射表都不需要动。本文还有配套的精品资源点击获取
返回列表