ARTICLE DETAIL

资讯详情

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

基于Python的车牌检测与识别系统实战:YOLO+CRNN全流程

基于Python的车牌检测与识别系统实战:YOLO+CRNN全流程 简介这是一套面向计算机视觉初学者与课程设计者的车牌检测识别实战项目基于Python实现覆盖图像预处理、车牌定位、字符分割与字符识别四大核心环节帮助读者理解从传统图像处理到深度学习模型的完整技术链路。压缩包共48个文件约22.11MB包含7个py脚本、17个png与8个jpg图像素材、2个dat训练数据、1个ui界面文件及1个mp4演示视频另附requirement.txt、run.bat与README.md等配置说明便于快速搭建运行环境。项目内含SVM训练脚本、字符分割模块与主界面程序配合测试图片与训练数据可直观复现车牌定位与字符识别流程并参考数据增强、多线程与GPU加速等优化思路。目前已有150人学习下载适合作为计算机视觉入门练手或毕业设计参考。1. 车牌检测识别系统从一张过曝卡口图说起去年帮朋友处理一个停车场出入口的抓拍项目现场调试图传回来我盯着屏幕愣了半天——车牌区域白花花一片肉眼都费劲更别说让模型去认。这就是很多人做基于Python的车牌检测和识别系统时遇到的第一个下马威实验室里跑得漂漂亮亮的模型到了真实卡口、地库、逆光场景检测框直接飘走识别结果变成一串乱码。车牌检测和识别看着是个经典问题但真正落地时光照、角度、运动模糊、车牌脏污、新能源绿牌和蓝牌混跑每一个都能让流水线翻车。这套系统要解决的核心链路其实就两段先从整张图里把车牌位置框出来检测再把框里的字符逐个读出来识别。适合谁适合已经会一点 Python、想找一个完整视觉项目练手的人也适合手上有个出入口、地磅、园区门禁场景、想自己搭一套识别流水线的工程师。它不需要你从零训一个 backbone但需要你搞清楚检测和识别各自吃什么输入、输出什么格式、中间怎么衔接。下面我按自己实际搭过的一套方案把选型、代码、参数和踩过的坑讲清楚你照着能跑通一个最小可用版本。2. 检测与识别的技术选型为什么我不建议一上来就端到端2.1 两阶段流水线 vs 端到端先想清楚你要什么车牌识别这个任务业界主流做法一直是「检测 识别」两阶段而不是一个网络直接输出字符串。原因很实际检测阶段要解决的是「车牌在哪、多大、什么角度」识别阶段要解决的是「这七个字符是什么」两个子任务的输入尺度、数据增强方式、损失函数都不一样。硬塞进一个端到端模型训练时你很难判断到底是定位没学好还是字符分类没学好调参全靠玄学。两阶段的好处是每一段都能单独验证。检测框画得准不准你直接可视化就能看出来识别错在哪你把裁剪图存下来一张张看。我一般会先把检测做到 mAP 能接受再单独调识别最后拼起来。代价是推理多了一次前向但对车牌这种小目标、字符固定的场景这点开销完全值得。具体到检测常见选择是 YOLO 系列或者轻量的 SSD。YOLOv5/v8 的 Python 生态最成熟ultralytics一个包就能训练和推理对新手最友好。识别这边车牌字符集很小省份简称 字母 数字蓝牌一共 65 类左右不需要动辄上亿参数的模型。我常用的是 CRNN CTC或者更简单的 CNN 全连接分类固定 7 位车牌。CRNN 能处理不定长适合新能源 8 位牌和双层黄牌但训练时 CTC 的收敛需要点耐心。提示如果你的场景车牌位数固定、角度很正CNN 直接分类 7 个位置反而更稳别为了「先进」硬上 CTC。2.2 环境搭建Python 版本和依赖怎么选不打架环境这块血泪经验是别用最新版 Python 去配老版深度学习框架。我一般锁 Python 3.8 到 3.10PyTorch 选 1.13 或 2.x 的稳定版。下面这套依赖是我在 Ubuntu 和 Windows 上都跑通过的组合直接抄# 创建独立环境避免和系统 Python 打架 conda create -n plate python3.9 -y conda activate plate # 安装 PyTorch按你的 CUDA 版本去官网选对应命令这里以 CUDA 11.8 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 检测用 ultralytics识别和图像处理用这些 pip install ultralytics opencv-python numpy pillow pip install imutils scikit-learn逻辑说明conda create建独立环境是后悔药后面装崩了直接删环境重来不影响系统。PyTorch 的 index-url 一定要按自己显卡驱动对应的 CUDA 版本选装错了会报CUDA error: no kernel image is available。ultralytics会自动带一份 YOLO 的推理和训练接口省得你自己写 anchor 匹配。参数上opencv-python用默认版即可别装opencv-contrib-python和它共存会冲突。如果你在 Windows 上遇到python was not found这类提示多半是没把 conda 环境加进 PATH激活环境后再执行就没问题。2.3 数据准备车牌数据集长什么样标注怎么标检测训练需要的是「图 车牌框坐标」识别训练需要的是「车牌小图 字符标签」。公开数据集里 CCPD 和 CRPD 比较常用但真实项目我建议至少自己补几百张现场图因为公开集的场景和你的卡口差异可能很大。标注格式上YOLO 要的是归一化的class x_center y_center width height每张图一个.txt。识别这边我习惯把每张车牌裁剪图存成皖A12345.jpg这种文件名标签直接从文件名解析省得再维护一个 label 文件。下面是把标注转成 YOLO 格式的小脚本import os import cv2 # 假设原始标注是 x1,y1,x2,y2 格式写在同名的 txt 里 def convert_to_yolo(img_dir, label_dir, out_dir, class_id0): os.makedirs(out_dir, exist_okTrue) for name in os.listdir(label_dir): img_path os.path.join(img_dir, name.replace(.txt, .jpg)) img cv2.imread(img_path) if img is None: continue h, w img.shape[:2] with open(os.path.join(label_dir, name)) as f: lines f.read().strip().splitlines() out_lines [] for line in lines: x1, y1, x2, y2 map(float, line.split(,)) # 归一化并转成中心点加宽高 cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw (x2 - x1) / w bh (y2 - y1) / h out_lines.append(f{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(out_dir, name), w) as f: f.write(\n.join(out_lines)) convert_to_yolo(images, labels_raw, labels_yolo)逻辑说明读原图只为拿宽高做归一化坐标全部压到 0 到 1 之间这是 YOLO 的硬要求。class_id车牌场景一般就一个类填 0。参数上cx/cy/bw/bh保留 6 位小数足够别用科学计数法。跑完记得抽查几张把生成的 txt 画回图上看看框对不对这一步偷懒后面训练 loss 不降你会找半天原因。3. 检测模型训练用 YOLO 把车牌框出来3.1 训练命令与关键参数怎么设检测这步我直接用ultralytics的 YOLO 接口数据组织成它要求的目录结构images/train、images/val、labels/train、labels/val再写一个data.yaml指向这些路径和类别名。训练命令就一行yolo detect train \ dataplate.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectplate_det \ nameexp1逻辑说明modelyolov8n.pt是拿官方预训练权重做迁移学习车牌这种小数据集从零训基本没戏。imgsz640是输入分辨率车牌在原图里占比小的话可以提到 960但显存和速度要权衡。patience20是早停20 轮验证指标不涨就停省时间。lr00.01是初始学习率小数据集可以降到 0.001 更稳。参数上重点盯三个batch按显存调爆显存就减半epochs别死磕 100看验证集 mAP 曲线平了就停imgsz如果车牌特别小宁可裁图分块检测也别无脑拉高分辨率速度会崩。3.2 推理与裁剪把检测框里的车牌抠出来训练完拿到best.pt推理时要把框里的区域裁出来送给识别模型。这里有个细节框别贴太紧往外扩一点边给识别留点上下文尤其是字符挨着边框的时候。from ultralytics import YOLO import cv2 model YOLO(plate_det/exp1/weights/best.pt) def detect_and_crop(img_path, pad4): img cv2.imread(img_path) results model(img, conf0.4, iou0.5)[0] crops [] for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) # 外扩 pad 像素防止字符被切 x1 max(0, x1 - pad) y1 max(0, y1 - pad) x2 min(img.shape[1], x2 pad) y2 min(img.shape[0], y2 pad) crops.append(img[y1:y2, x1:x2]) return crops逻辑说明conf0.4是置信度阈值低于这个的框不要宁可漏检也别误检误检会把背景裁进去让识别模型懵。iou0.5是 NMS 的重叠阈值车牌一般不会重叠这个值不用太纠结。pad4是外扩像素按你车牌在图中实际大小调车牌大就多扩点。参数上conf是检测和识别衔接的关键。设太高漏检设太低误检我一般从 0.4 起步看实际漏检情况再微调。裁出来的图最好统一 resize 到识别模型要求的尺寸比如94x24或128x32别指望识别模型自己处理任意尺寸。3.3 检测效果怎么验证别只看 loss训练日志里的 box_loss、cls_loss 降了不代表框得准。我一般会跑一个可视化脚本把验证集的预测框和真值框画在一起对比重点看逆光、倾斜、小目标这几类。如果发现某个角度系统性偏移多半是训练集里这类样本太少补数据比调参管用。还有一个容易忽略的点检测框的宽高比。车牌标准比例大概 3:1 到 4:1如果你发现预测框经常是正方形说明模型没学到形状先验可以检查标注是不是把整个车头都框进去了。验证这步花十分钟能省后面几小时的瞎调。4. 识别模型CRNN 把字符读出来4.1 CRNN 结构拆解与输入输出约定识别这步我用 CRNN结构分三块CNN 提特征、RNN 看序列、CTC 做对齐。输入是统一尺寸的车牌灰度或彩色图输出是字符序列。为什么用 CTC 而不是逐位置分类因为车牌可能有 7 位也可能 8 位CTC 能处理变长不用你手动对齐每个字符的位置。输入尺寸我一般定imgW128, imgH32太小字符糊太大推理慢。字符集要提前定好把所有可能出现的省份简称、字母、数字列成一个列表CTC 的输出维度就是字符集大小加一个 blank。下面是一个精简的 CRNN 定义import torch import torch.nn as nn class CRNN(nn.Module): def __init__(self, num_classes): super().__init__() # CNN 部分把 32x128 的图逐步下采样成序列特征 self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(64, 128, 3, 1, 1), nn.ReLU(), nn.MaxPool2d(2, 2), nn.Conv2d(128, 256, 3, 1, 1), nn.BatchNorm2d(256), nn.ReLU(), nn.Conv2d(256, 256, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2, 1)), nn.Conv2d(256, 512, 3, 1, 1), nn.BatchNorm2d(512), nn.ReLU(), nn.Conv2d(512, 512, 3, 1, 1), nn.ReLU(), nn.MaxPool2d((2, 1)), ) # RNN 部分双向 LSTM 看上下文 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, H, W b, c, h, w feat.shape feat feat.view(b, c * h, w) # 把高和通道合并 feat feat.permute(0, 2, 1) # 变成 B, W, C out, _ self.rnn(feat) return self.fc(out) # B, W, num_classes逻辑说明CNN 里最后两个池化用(2,1)是为了只在高度方向下采样保留宽度方向的序列长度这样 CTC 才有足够的时间步。view那步把通道和高度合并是 CRNN 的经典操作把二维特征图压成一维序列。num_classes要包含 blank训练时 CTC loss 会自动处理。参数上imgH32对应两次高度池化后剩 8再两次(2,1)后剩 2序列长度由宽度决定。字符集大小按你的实际场景定蓝牌绿牌黄牌混跑的话要把所有字符并进去。4.2 训练循环与 CTC loss 的坑CTC 训练最容易翻车的地方是标签长度和输入序列长度的关系。CTC 要求输入时间步数大于等于标签长度加重复字符数否则 loss 直接报错或者训不动。我一般保证W在 32 以上7 位车牌绰绰有余。import torch.nn.functional as F def train_step(model, imgs, labels, label_lens, optimizer): model.train() logits model(imgs) # B, T, C log_probs F.log_softmax(logits, dim2) # CTC 要求输入是 T, B, C log_probs log_probs.permute(1, 0, 2) loss F.ctc_loss(log_probs, labels, input_lengthstorch.full((imgs.size(0),), log_probs.size(0), dtypetorch.long), target_lengthslabel_lens, blank0) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()逻辑说明log_softmax必须在ctc_loss之前做CTC 内部要的是对数概率。permute把维度换成T, B, C这是 PyTorch CTC 的硬要求忘了这步会报维度错误。blank0要和字符集里 blank 的位置一致我一般把 blank 放 0字符从 1 开始编号。参数上input_lengths全填T就行因为我们的输入是定长的。target_lengths是每个样本的真实字符数变长车牌就靠它。学习率识别这边我一般用 0.001 起步CTC 对学习率比较敏感太大直接不收敛。4.3 解码从 CTC 输出到可读车牌训练完推理时CTC 输出要解码成字符串。最简单的是贪心解码每个时间步取最大概率去掉重复和 blank。车牌场景贪心基本够用beam search 提升有限还慢。def greedy_decode(logits, idx2char): # logits: T, C preds logits.argmax(dim1).tolist() result [] prev None for p in preds: if p ! 0 and p ! prev: # 0 是 blank重复字符要合并 result.append(idx2char[p]) prev p return .join(result)逻辑说明p ! prev这步是 CTC 解码的核心连续相同的字符只保留一个中间必须有 blank 隔开才表示两个相同字符。idx2char是编号到字符的映射要和训练时的字符集完全一致顺序错了识别结果全乱。参数上贪心解码没有可调参数简单可靠。如果你发现某些字符经常混淆比如0和O、1和I可以在后处理里加规则按车牌格式做校验和纠正。5. 避坑与排查那些让我加班到凌晨的问题5.1 检测框飘忽不定识别结果乱码现象同一张图检测框时大时小裁出来的车牌要么缺字要么带一堆背景识别结果随机跳。原因检测模型过拟合到训练集的特定角度或者conf阈值设太低把一些边缘响应也当成了车牌。解决先把conf提到 0.5 以上看是否稳定如果还飘检查训练集里是否有大量相似角度样本补一些不同角度和光照的图。另外确认推理时的imgsz和训练时一致不一致会导致尺度错位。5.2 CTC loss 变成 nan 或者一直不降现象训练几个 step 后 loss 变 nan或者卡在某个值不动。原因学习率太大或者输入序列长度小于标签长度CTC 直接算不出有效路径。解决先把学习率降到 0.0001 试如果还 nan打印log_probs看有没有 inf。检查T是否大于标签长度车牌 7 位的话T至少 15 以上才稳。还有一个隐蔽原因标签里出现了字符集之外的编号CTC 找不到对应类别。5.3 绿牌新能源识别率明显低于蓝牌现象蓝牌识别挺准绿牌一上来就错。原因训练集里绿牌样本太少字符集虽然包含了字母数字但绿牌是 8 位CTC 对长序列的建模没训够。解决单独统计绿牌样本数量补到和蓝牌一个量级。如果补不了可以在识别前先判断车牌颜色绿牌走一个单独微调的模型别指望一个模型通吃。5.4 推理速度慢单张图要几百毫秒现象检测加识别跑一张图要 300ms 以上视频流根本扛不住。原因检测输入分辨率太高或者识别模型在 CPU 上跑。解决检测imgsz从 960 降到 640 甚至 416速度能翻倍。识别模型参数量砍一半或者用 ONNX Runtime 加速。如果还慢把检测和识别拆到两个进程并行别串行等。5.5 车牌倾斜时识别全错现象正角度车牌识别没问题一倾斜就崩。原因识别模型训练时用的都是水平裁剪图没见过倾斜样本。解决在识别训练数据里加随机旋转增强角度范围 -10 到 10 度。如果倾斜很严重检测后先做透视变换把车牌摆正再送识别。这一步多花几毫秒但识别率提升明显。6. 进阶技巧用透视校正把倾斜车牌拉正检测框出来之后如果车牌是斜的直接裁出来送识别字符会变形CRNN 也救不回来。我一般会在检测和识别之间加一步透视校正把车牌四个角点找出来映射成一个标准矩形。这步不需要额外训练模型用 OpenCV 的轮廓和最小外接矩形就能做。import cv2 import numpy as np def align_plate(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 二值化把字符和背景分开 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 找轮廓取最大的那个当作车牌区域 contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return plate_img cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) box cv2.boxPoints(rect) box np.int0(box) # 把四个角点排序左上、右上、右下、左下 s box.sum(axis1) diff np.diff(box, axis1) ordered np.zeros((4, 2), dtypenp.int0) ordered[0] box[np.argmin(s)] ordered[2] box[np.argmax(s)] ordered[1] box[np.argmin(diff)] ordered[3] box[np.argmax(diff)] # 映射到标准矩形宽高按车牌比例 dst np.array([[0, 0], [128, 0], [128, 32], [0, 32]], dtypenp.float32) M cv2.getPerspectiveTransform(ordered.astype(np.float32), dst) return cv2.warpPerspective(plate_img, M, (128, 32))逻辑说明minAreaRect拿到最小外接旋转矩形boxPoints取四个角点。排序那步是关键sum最小的是左上最大的是右下diff最小的是右上最大的是左下这样角点顺序不会乱。getPerspectiveTransform算变换矩阵warpPerspective把倾斜车牌拉成 128x32 的标准图。参数上dst的宽高要和识别模型的输入尺寸一致我定 128x32。二值化用 OTSU 自动找阈值省得手动调。如果车牌背景复杂导致轮廓找不准可以先做一次颜色过滤只保留蓝色或绿色区域再找轮廓。这套校正加在检测和识别之间实测对倾斜 15 度以内的车牌识别率能提十几个点。代价是每张图多几毫秒但比重新训一个抗倾斜的识别模型划算得多。我自己踩过的最大坑是一开始觉得校正没必要硬训识别模型去扛倾斜结果数据增强开到最大还是不稳。后来加上这步透视变换问题迎刃而解。做视觉项目能在预处理阶段用几何方法解决的别都堆给模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表