ARTICLE DETAIL

资讯详情

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

Python深度学习车牌识别系统实战:从两阶段检测到CRNN字符识别全流程

Python深度学习车牌识别系统实战:从两阶段检测到CRNN字符识别全流程 简介这份Python项目源码面向计算机视觉与深度学习方向的课程设计、毕业设计学生及入门开发者提供一套完整的车牌识别系统实现方案可用于交通管理、智能停车场等场景的算法学习与二次开发。项目围绕车牌定位、字符分割与字符识别三大核心环节展开结合OpenCV的高斯模糊、Sobel算子等图像处理技术增强车牌特征并借助TensorFlow或PyTorch构建卷积神经网络完成字符识别同时提供PyQt交互界面与模型训练优化接口便于按需调整。压缩包为zip格式整体约27.74MB文件类型以Python源码、模型文件及界面资源为主结构清晰便于按模块查阅。目前已有134人学习下载适合希望掌握车牌识别全流程、积累深度学习项目实战经验的读者参考借鉴。1. 车牌识别系统到底难在哪从一张过曝的卡口图说起很多刚接触 python 深度学习 车牌识别系统 的朋友第一反应是「这不就是个 OCR 吗」。我一开始也这么想直到拿到一张中午逆光的卡口图车牌区域过曝成一片白边缘和车身糊在一起传统阈值分割直接失效。车牌识别系统真正的难点从来不是「认字」而是「在乱七八糟的光照、角度、遮挡下先把车牌框准再把字符切干净」。这个 python 项目基于深度学习的思路本质是把检测和识别拆成两段神经网络让模型自己去学那些你写不出来的规则。这套方案适合谁适合已经会 python 基础语法、装过 pytorch 或 tensorflow、想找一个能跑通全流程的深度学习实战项目案例的人。它不要求你从零推导反向传播但要求你能看懂张量维度、会调 dataloader。整条链路是数据标注 → 车牌检测定位框→ 字符识别序列预测→ 后处理矫正。下面我按自己复现时的顺序把每一步的参数、坑和验证方法讲清楚你照着能跑出一个可用的 demo也能看清工业级落地还差什么。2. 先想清楚检测和识别怎么分工两阶段还是端到端2.1 为什么我建议新手从两阶段做起车牌识别系统 python 代码 在网上能搜到一堆但很多是「检测识别」揉在一个网络里的端到端方案。端到端听起来优雅实际训练时两个任务的 loss 会互相拉扯收敛慢调参玄学。我一般会拆成两阶段第一阶段用目标检测网络把车牌框出来第二阶段把框内的图送进识别网络读字符。这样做的好处是每一段都能单独验证——检测框不准你一眼能从可视化结果看出来识别错字你能单独换识别模型不用重训检测。两阶段的代价是推理速度慢一点多了一次裁剪和缩放。但对学习和中小规模落地来说可调试性远比那几毫秒重要。常见做法是检测用 YOLO 系列或轻量 SSD识别用 CRNNCTC 或轻量 Transformer。选型理由很直接YOLO 生态成熟、预训练权重多、部署工具链全CRNN 对不定长字符序列友好CTC 省去了字符分割这一步正好绕开车牌字符粘连的老大难问题。2.2 数据标注的格式与目录约定动手前先把数据理顺。检测阶段需要每张图里车牌的边界框识别阶段需要每个车牌图的字符文本。我习惯用这样的目录结构后面写 dataloader 时直接按这个读dataset/ ├── detection/ │ ├── images/ # 原始卡口图 │ └── labels/ # 每张图对应一个 txt每行 class x_center y_center w h归一化 ├── recognition/ │ ├── crops/ # 从检测框裁出的车牌小图 │ └── labels.txt # 每行文件名 车牌文本 └── charset.txt # 字符集一行一个字符检测标签用归一化的中心点加宽高是 YOLO 格式的通用约定换别的检测框架时记得转换。识别标签里中文车牌要特别注意省份简称和字母数字混排charset 里必须包含所有可能出现的字符漏一个字符模型永远学不会它。我踩过的坑是 charset 里把「O」和「0」当成一个结果模型在两者之间反复横跳后来强制分开才稳定。2.3 用 python 检查数据分布的最小脚本标注完别急着训练先跑个统计脚本看看框的尺寸分布和字符长度分布这能提前暴露很多问题import os from collections import Counter label_dir dataset/detection/labels widths, heights, char_lens [], [], [] for name in os.listdir(label_dir): with open(os.path.join(label_dir, name)) as f: for line in f: parts line.strip().split() if len(parts) ! 5: print(f格式异常: {name} - {line.strip()}) continue _, _, _, w, h map(float, parts) widths.append(w) heights.append(h) with open(dataset/recognition/labels.txt) as f: for line in f: text line.strip().split()[-1] char_lens.append(len(text)) print(框宽分布:, Counter([round(w, 2) for w in widths]).most_common(5)) print(框高分布:, Counter([round(h, 2) for h in heights]).most_common(5)) print(字符长度分布:, Counter(char_lens).most_common())这段脚本做三件事校验每行标签是不是 5 个字段统计框的宽高分布统计车牌字符长度分布。参数上round(w, 2)是为了把连续值离散化方便计数你可以按自己数据调整精度。如果发现某些框宽高接近 0 或者超过 1说明标注坐标没归一化或者标错了必须回去修。字符长度分布如果出现 4 或 9 这种异常值多半是标签文本写错了趁早发现省得训练完才后悔。3. 检测网络怎么训从 anchor 设置到数据增强3.1 检测模型的输入尺寸与 anchor 匹配车牌在整张卡口图里占比很小这是检测阶段最大的特点。如果你直接把 1920×1080 的原图塞进网络车牌可能只有几十个像素特征还没提取就没了。我一般会把输入缩放到 640×640同时确认缩放后车牌短边不低于 20 像素。低于这个值检测召回率会明显掉。anchor 的设置要贴合你数据里车牌的宽高比。中国蓝牌大致是 440×140宽高比约 3.14:1新能源绿牌略宽。用统计脚本算出的框宽高分布取几个聚类中心当 anchor比默认的 COCO anchor 强很多。常见做法是用 k-means 对标注框做聚类聚出 6 到 9 个 anchor。这一步不做模型会花大量时间在明显不匹配的 anchor 上浪费算力。3.2 训练命令与关键超参假设你用 ultralytics 的 YOLO 实现训练命令大概长这样yolo detect train \ dataplate.yaml \ modelyolov8n.pt \ imgsz640 \ epochs100 \ batch16 \ lr00.01 \ lrf0.01 \ mosaic1.0 \ degrees10.0 \ translate0.1 \ scale0.5 \ nameplate_det逐项说imgsz640是输入边长显存不够就降到 512但别低于 416否则小目标吃亏。batch16按显存调8G 显存跑 yolov8n 大概能到 16。lr00.01是初始学习率lrf0.01是最终学习率相对初始值的比例余弦退火到 1e-4 左右。mosaic1.0是马赛克增强对小目标检测帮助很大但训练最后 10 个 epoch 建议关掉让模型适应真实分布。degrees10.0做小角度旋转增强车牌倾斜是常态这个必须开。scale0.5做缩放增强模拟远近不同的车。3.3 训练过程看什么指标训练时别只盯着 loss。检测任务重点看 mAP0.5 和 mAP0.5:0.95 两个指标。mAP0.5 高但 mAP0.5:0.95 低说明框的位置不够准可能是回归分支学得不好或者 anchor 匹配有问题。如果训练集 mAP 一直涨但验证集不涨就是过拟合加数据增强或者减模型容量。还有一个容易被忽略的指标是每类的小目标召回。车牌检测只有一个类但你可以按框的面积分桶看召回率。面积小于 32×32 的桶如果召回明显低说明小目标没学好要么提高输入分辨率要么在数据增强里多保留小目标样本。我一般会在验证脚本里加一段按面积分桶统计的代码比只看总体 mAP 有用得多。4. 识别网络怎么训CRNN 的输入对齐与 CTC 解码4.1 车牌裁剪图的预处理检测框裁出来的车牌图尺寸五花八门识别网络需要统一输入。CRNN 的常见输入是 32×100 或 32×160高度固定 32宽度按比例缩放后 padding。这里有个细节缩放时保持宽高比短边补齐别直接拉伸拉伸会让字符变形识别率掉得厉害。import cv2 import numpy as np def preprocess_plate(img, target_h32, target_w100): h, w img.shape[:2] ratio target_h / h new_w int(w * ratio) resized cv2.resize(img, (new_w, target_h)) if new_w target_w: pad np.zeros((target_h, target_w - new_w, 3), dtypenp.uint8) resized np.hstack([resized, pad]) else: resized cv2.resize(resized, (target_w, target_h)) return resized逻辑是先按高度缩放到 32宽度等比变化如果宽度不够目标宽度就右侧补黑边如果超了就再压一次。参数target_w100对应能容纳大约 7 到 8 个字符中国车牌 7 位留点余量。补边用黑色是因为车牌背景通常不是纯黑补边区域和真实字符区域区分明显CTC 更容易对齐。4.2 CRNN 网络结构与 CTC lossCRNN 的结构是 CNN 提特征 RNN 学序列 CTC 解码。CNN 部分用几个卷积池化把高度压到 1宽度保留时间步。RNN 用双向 LSTM输出每个时间步的字符概率分布。CTC 负责把不定长的序列预测和不定长的标签对齐不需要字符级标注。import torch import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, padding1), nn.ReLU(), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 512, 3, padding1), nn.ReLU(), nn.MaxPool2d((2, 1)), ) self.rnn nn.LSTM(512, 256, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(512, num_classes) def forward(self, x): feat self.cnn(x) # B, 512, 1, W feat feat.squeeze(2).permute(0, 2, 1) # B, W, 512 out, _ self.rnn(feat) return self.fc(out) # B, T, num_classes关键点池化层高度方向用 (2,1) 是为了把高度压到 1 而宽度保留宽度方向的时间步就是 CTC 的序列长度。num_classes要包含字符集大小加 1那个 1 是 CTC 的 blank 标签。训练时用nn.CTCLoss注意输入要先 log_softmaxtarget 要拼接成一维并给出长度。4.3 训练参数与解码验证识别训练的学习率比检测小我一般用 1e-3 起步Adam 优化器batch 64。训练 50 到 100 个 epoch看验证集的字符错误率 CER。CER 降到 1% 以下基本可用但要注意验证集不能和训练集有同一辆车的不同帧否则指标虚高。解码用贪心解码就够入门每个时间步取概率最大的字符去掉重复和 blank。如果发现模型输出大量重复字符多半是 CTC 的 blank 没学好检查 target 长度和输入长度是否满足T 2*L1的约束不满足会直接报错或者学不动。这个约束是 CTC 的硬性要求输入时间步必须大于等于两倍标签长度加一否则无法对齐。5. 避坑与排查那些让我重训三次的问题5.1 检测框抖动导致识别输入不稳定现象视频流里同一辆车连续几帧识别结果在正确和错误之间跳。原因检测框每帧位置有轻微抖动裁剪出的车牌图内容偏移识别网络对偏移敏感。解决在检测后加一个简单的跟踪或平滑对连续帧的框做指数移动平均或者用 IoU 匹配关联前后帧。我一般用box 0.7 * box_new 0.3 * box_old这种简单平滑效果立竿见影。5.2 字符集顺序不一致导致标签错乱现象训练 loss 正常下降但推理结果全是乱码。原因训练时 charset 的顺序和推理时不一致模型输出的索引对应的字符变了。解决把 charset 存成文件训练和推理都从这个文件读绝不硬编码。这个坑很隐蔽因为 loss 只关心索引对不对不关心索引映射到什么字符。5.3 过曝和欠曝样本被增强放大现象模型在正常光照下很好一到强光或夜间就崩。原因数据增强里的亮度对比度调整是双向的把本来就过曝的图调得更过曝模型学到了极端噪声。解决对已经过曝的样本增强时只做变暗不做变亮或者干脆对过曝样本单独处理。我一般会在 dataloader 里判断图像平均亮度超过阈值就限制增强幅度。5.4 中文省份简称识别率低现象字母数字识别很准但省份简称经常错。原因省份简称样本少且字形复杂CTC 在样本不均衡时偏向高频字符。解决对省份简称做过采样或者在 loss 里给低频字符加权。另一个办法是把省份识别单独拆出来做一个分类头因为省份位置固定分类比序列预测更稳。5.5 推理时 batch 维度和训练不一致现象训练好的模型推理时报维度错误。原因训练时 batch 维是 1推理时忘了加 batch 维或者反过来。解决推理前统一img img.unsqueeze(0)并且确认模型 eval 模式。这个坑低级但高频尤其是从训练脚本复制代码到推理脚本时容易漏。6. 把 demo 推到可用量化、批处理与一个验证技巧走到这里你有一个能跑的模型了但离「可用」还差一步。我一般会做三件事导出 ONNX 做推理加速、写一个批处理脚本跑整个测试集、用一个混淆矩阵定位最差的字符对。导出 ONNX 的命令很简单重点是确认输入输出名字和动态维度import torch model CRNN(num_classeslen(charset) 1) model.load_state_dict(torch.load(crnn_best.pth, map_locationcpu)) model.eval() dummy torch.randn(1, 3, 32, 100) torch.onnx.export( model, dummy, crnn.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )dynamic_axes把 batch 维设成动态这样推理时 batch 可以变。导出后用 onnxruntime 跑一遍对比 pytorch 和 onnx 的输出差异差异在 1e-4 以内算正常。如果差异大检查有没有 opset 不支持的算子。批处理验证脚本的核心是统计每个字符的识别准确率生成混淆矩阵。我习惯把结果按字符排序找出错误率最高的 10 个字符回头看这些字符的训练样本是不是有问题。很多时候不是模型不行是那几类样本标注错了或者太少。这个习惯帮我省了大量盲目调参的时间。最后一个技巧用真实场景的短视频做端到端验证而不是只看测试集指标。测试集是静态图视频里有运动模糊、帧间抖动、光照渐变这些才是真实工况。我一般会录一段 30 秒的路口视频跑完整流程人工数 100 辆车牌算实际准确率。这个数字比任何 mAP 都诚实。希望帮到你。本文还有配套的精品资源点击获取
返回列表