☰
基于Python的车牌检测与识别系统实战:YOLO+CRNN全流程
2026/10/1 13:38:59 网站建设 项目流程

简介:这是一套面向计算机视觉初学者与课程设计者的车牌检测识别实战项目,基于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.10,PyTorch 选 1.13 或 2.x 的稳定版。下面这套依赖是我在 Ubuntu 和 Windows 上都跑通过的组合,直接抄:

# 创建独立环境,避免和系统 Python 打架 conda create -n plate python=3.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_id=0): os.makedirs(out_dir, exist_ok=True) 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 \ data=plate.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=plate_det \ name=exp1

逻辑说明:model=yolov8n.pt是拿官方预训练权重做迁移学习,车牌这种小数据集从零训基本没戏。imgsz=640是输入分辨率,车牌在原图里占比小的话可以提到 960,但显存和速度要权衡。patience=20是早停,20 轮验证指标不涨就停,省时间。lr0=0.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, pad=4): img = cv2.imread(img_path) results = model(img, conf=0.4, iou=0.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

逻辑说明:conf=0.4是置信度阈值,低于这个的框不要,宁可漏检也别误检,误检会把背景裁进去让识别模型懵。iou=0.5是 NMS 的重叠阈值,车牌一般不会重叠,这个值不用太纠结。pad=4是外扩像素,按你车牌在图中实际大小调,车牌大就多扩点。

参数上,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 能处理变长,不用你手动对齐每个字符的位置。

输入尺寸我一般定imgW=128, imgH=32,太小字符糊,太大推理慢。字符集要提前定好,把所有可能出现的省份简称、字母、数字列成一个列表,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, bidirectional=True, batch_first=True) 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 会自动处理。

参数上,imgH=32对应两次高度池化后剩 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, dim=2) # CTC 要求输入是 T, B, C log_probs = log_probs.permute(1, 0, 2) loss = F.ctc_loss(log_probs, labels, input_lengths=torch.full((imgs.size(0),), log_probs.size(0), dtype=torch.long), target_lengths=label_lens, blank=0) optimizer.zero_grad() loss.backward() optimizer.step() return loss.item()

逻辑说明:log_softmax必须在ctc_loss之前做,CTC 内部要的是对数概率。permute把维度换成T, B, C,这是 PyTorch CTC 的硬要求,忘了这步会报维度错误。blank=0要和字符集里 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(dim=1).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, key=cv2.contourArea) rect = cv2.minAreaRect(cnt) box = cv2.boxPoints(rect) box = np.int0(box) # 把四个角点排序:左上、右上、右下、左下 s = box.sum(axis=1) diff = np.diff(box, axis=1) ordered = np.zeros((4, 2), dtype=np.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]], dtype=np.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 度以内的车牌,识别率能提十几个点。代价是每张图多几毫秒,但比重新训一个抗倾斜的识别模型划算得多。

我自己踩过的最大坑是:一开始觉得校正没必要,硬训识别模型去扛倾斜,结果数据增强开到最大还是不稳。后来加上这步透视变换,问题迎刃而解。做视觉项目,能在预处理阶段用几何方法解决的,别都堆给模型。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询