简介:这份资源是一个基于Python与OpenCV的车牌识别完整项目包,面向具备一定编程基础、希望入门计算机视觉或图像处理的开发者与学习者。项目围绕车牌自动识别这一典型场景,串联起灰度化、二值化、高斯滤波去噪、Canny边缘检测、形态学膨胀腐蚀、连通组件字符分割以及模板匹配与深度学习字符识别等关键环节,并包含识别结果的纠错与格式校验思路,适合作为图像处理与自动驾驶感知方向的实践案例。压缩包共40个文件,约18.93MB,以jpg测试图片、xml配置、py源码、dat模型数据、png截图及7z子压缩包为主,另含少量js与说明文件,目录中区分了训练与测试素材,便于直接运行与调试。目前已有316人学习下载,读者可借此理解从图像预处理到字符识别的完整流程,掌握OpenCV常用算子与车牌定位、分割、识别的工程实现方法。
1. 车牌识别.zip 到手之后:一个压缩包到底能不能直接跑出车牌号
很多人第一次拿到「车牌识别.zip」这种命名的压缩包,第一反应是解压、找main.py、双击运行,然后被一堆ModuleNotFoundError和缺失的模型权重劝退。这个标题背后其实是一类非常典型的工程需求:把一张包含车辆的图片或一段道路视频,经过检测和字符识别两级模型,最终输出「京A12345」这样的结构化文本。它适合两类人:一类是想快速验证车牌识别能不能用在自己项目里的开发者,另一类是已经跑通过通用 OCR,但发现车牌这种「小目标 + 强透视 + 字符间距固定」的场景直接套通用模型效果很差,想找一套专门方案的人。
需要先建立一个认知:车牌识别不是一个模型,而是一条流水线。压缩包里通常包含三部分——车牌检测(定位车牌在画面中的位置)、车牌矫正(把倾斜车牌拉正)、字符识别(把矫正后的车牌图转成文字)。这三步任何一步出问题,最终结果都是错的。所以拿到压缩包后,不要急着跑,先搞清楚它用的是哪套检测方案(YOLO 系列还是 SSD)、哪套识别方案(CRNN+CTC 还是 PaddleOCR 微调),这决定了你后面调参和排错的方向。下面按「先看懂结构、再跑通最小闭环、最后处理真实场景」的顺序展开。
2. 拆开压缩包先看什么:目录结构与依赖的快速判断
2.1 三类典型目录布局与对应技术栈
拿到一个车牌识别压缩包,先别装依赖,用tree或文件管理器看两层目录,基本能判断出它的技术路线。常见的有三种布局。
第一种是「检测+识别分离」布局,根目录下同时存在detect/和recognize/两个文件夹,各自带weights/和inference.py。这种通常是 YOLO 做检测、CRNN 做识别的组合,灵活但需要你自己写串联逻辑。第二种是「端到端」布局,只有一个model/和一个predict.py,里面可能是一个多任务网络,检测和识别共享 backbone。这种跑起来简单,但想单独替换某一级很难。第三种是「PaddleOCR 二次封装」布局,目录里会出现ppocr/、rec_model/、det_model/这类命名,本质是调 PaddleOCR 的车牌微调模型,依赖较重但中文支持好。
判断方法很简单:看requirements.txt里有没有ultralytics、paddlepaddle、torch、opencv-python这几个关键包。有ultralytics基本是 YOLOv8/v11 系;有paddlepaddle就是百度系;只有torch和torchvision则可能是自己写的网络。
2.2 依赖安装与权重文件的放置位置
确认技术栈后,建一个干净的虚拟环境再装依赖,这一步能避开 80% 的版本冲突。命令如下:
# 建议 Python 3.8~3.10,太新的版本部分旧权重加载会报错 conda create -n lpr python=3.9 -y conda activate lpr # 先装 torch,注意和 CUDA 版本对应,没有 GPU 就用 cpu 版 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 再装项目依赖,requirements 里如果有固定版本号不要随意升级 pip install -r requirements.txt # 单独确认 opencv 能正常 import,车牌预处理大量依赖它 python -c "import cv2; print(cv2.__version__)"逻辑说明:先装 torch 再装 requirements,是因为很多项目的 requirements 里写的是torch>=1.7这种宽泛约束,pip 可能给你装一个和 CUDA 不匹配的版本,导致后面推理时CUDA error。参数上,--index-url指向官方 wheel 源,国内网络慢可以换镜像,但要注意镜像的 torch 版本是否齐全。
权重文件是第二个高频翻车点。压缩包里如果有weights/目录但里面是空的,或者只有README提示「请自行下载」,那说明作者没把权重打包进去。这时候不要随便找个 YOLO 通用权重塞进去,车牌检测的类别数和通用 COCO 不一样,直接加载会报size mismatch。正确做法是看代码里model.load_state_dict或YOLO('xxx.pt')那一行写的路径和文件名,按这个名字去找对应权重。如果实在找不到,就得用作者提供的训练脚本自己训,或者换用公开的车牌检测数据集重新训练,这是另一个工作量了。
提示:解压后先全局搜索
.pt、.pth、.onnx、.pdparams这几种后缀,确认权重到底在不在包里,比盲目跑代码高效得多。
3. 跑通最小闭环:单张图片从输入到输出车牌号
3.1 检测阶段:把车牌框出来并做透视矫正
检测阶段的目标是输出车牌的四个角点坐标,而不是简单的矩形框。因为车牌在真实场景中几乎总是带透视的,用水平矩形框裁剪出来的图会包含大量背景,直接送给识别模型会严重掉点。常见做法是检测模型输出旋转框或分割掩码,再取最小外接四边形。
import cv2 import numpy as np from ultralytics import YOLO # 加载车牌检测权重,这里假设是 YOLOv8 的旋转框模型 det_model = YOLO("weights/plate_det.pt") def detect_and_rectify(img_path): img = cv2.imread(img_path) results = det_model(img, conf=0.25, iou=0.45)[0] plates = [] for rbox in results.obb.xyxyxyxy: # 旋转框四个角点 pts = rbox.cpu().numpy().astype(np.float32) # 按左上、右上、右下、左下排序,保证透视变换顺序正确 pts = order_points(pts) w = int(max(np.linalg.norm(pts[0]-pts[1]), np.linalg.norm(pts[2]-pts[3]))) h = int(max(np.linalg.norm(pts[0]-pts[3]), np.linalg.norm(pts[1]-pts[2]))) dst = np.array([[0,0],[w,0],[w,h],[0,h]], dtype=np.float32) M = cv2.getPerspectiveTransform(pts, dst) warped = cv2.warpPerspective(img, M, (w, h)) plates.append(warped) return plates def order_points(pts): # 按 x+y 和 x-y 区分四个角,避免角点顺序错乱导致图像翻转 rect = np.zeros((4,2), dtype=np.float32) s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] d = np.diff(pts, axis=1) rect[1] = pts[np.argmin(d)] rect[3] = pts[np.argmax(d)] return rect逻辑说明:conf=0.25是检测置信度阈值,车牌场景建议比通用检测低一些,因为远距离车牌目标小、得分偏低,设太高会漏检。iou=0.45控制重叠框合并,车牌一般不会密集堆叠,这个值够用。order_points是血泪经验——不做角点排序,透视变换后车牌可能是上下颠倒或左右镜像的,识别模型直接懵掉。
参数调整上,如果发现车牌框偏大包含太多背景,把conf提到 0.4 左右;如果漏检严重,降到 0.15 并检查输入分辨率,YOLO 默认 640,远距离车牌可能需要imgsz=1280。
3.2 识别阶段:CRNN 解码与字符集对齐
识别阶段把矫正后的车牌图转成文字。CRNN+CTC 是车牌识别里最稳的方案,因为它不需要字符级标注,且对车牌这种定长字符序列很友好。关键点是字符集必须和训练时一致,否则解码出来的索引对不上,输出全是乱码。
import torch from crnn import CRNN # 假设项目里有这个定义 # 字符集顺序必须和训练时完全一致,差一个字符全盘错 CHARS = "京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" rec_model = CRNN(num_classes=len(CHARS)+1) # +1 是 CTC blank rec_model.load_state_dict(torch.load("weights/rec.pth", map_location="cpu")) rec_model.eval() def recognize(plate_img): # 统一高度 32,宽度按比例缩放,这是 CRNN 的标准输入 h, w = plate_img.shape[:2] new_w = int(w * 32 / h) img = cv2.resize(plate_img, (new_w, 32)) img = img.astype(np.float32) / 255.0 img = (img - 0.5) / 0.5 # 归一化,和训练预处理保持一致 tensor = torch.from_numpy(img).permute(2,0,1).unsqueeze(0) with torch.no_grad(): pred = rec_model(tensor) # CTC greedy decode pred_idx = pred.argmax(dim=2)[0] text = "" prev = -1 for idx in pred_idx: idx = idx.item() if idx != prev and idx < len(CHARS): text += CHARS[idx] prev = idx return text逻辑说明:num_classes必须是字符集长度加一,那个一是 CTC 的 blank 占位符,少了它训练和推理都会错位。归一化参数(x-0.5)/0.5是常见配置,但不同项目可能用 ImageNet 的均值和方差,必须翻训练代码确认,这是识别结果「看起来像但又不对」的头号原因。CTC 解码用 greedy 就够,车牌字符少,beam search 提升有限还拖速度。
把检测和识别串起来,一张图的完整流程就是:读图 → 检测旋转框 → 透视矫正 → 缩放到 32 高 → CRNN 推理 → CTC 解码 → 输出车牌号。跑通这一步,你才算真正验证了这个压缩包可用。
4. 真实场景下的参数调优与批量处理
4.1 视频流处理:跳帧与跟踪减少重复计算
单张图跑通后,下一步通常是处理视频。如果对每一帧都做检测+识别,1080p 视频在普通 GPU 上可能只有 5~10 FPS,而且同一辆车连续几十帧输出相同车牌,纯属浪费。常见做法是跳帧检测加简单跟踪。
import cv2 from collections import defaultdict cap = cv2.VideoCapture("road.mp4") fps = cap.get(cv2.CAP_PROP_FPS) frame_id = 0 track_cache = {} # track_id -> 最近一次车牌文本 while True: ret, frame = cap.read() if not ret: break frame_id += 1 # 每 5 帧做一次完整识别,中间帧复用上次结果 if frame_id % 5 != 0: continue plates = detect_and_rectify_frame(frame) for i, p in enumerate(plates): text = recognize(p) track_cache[i] = text # 实际项目中这里应接入 ByteTrack 或 DeepSORT 做 ID 关联 # 简化版用检测框位置做最近邻匹配逻辑说明:跳帧间隔5是经验值,车速快、车牌在画面中停留时间短就设 3,车速慢可以设 10。跟踪部分这里只给了框架,真实项目建议直接接 ByteTrack,它和 YOLO 系检测器配合成熟,能给出稳定的 track_id,这样同一辆车的车牌只需要识别一次,后续帧直接复用,整体吞吐能提升 3~5 倍。
4.2 低照度与运动模糊的预处理补偿
夜间和高速运动是车牌识别的两大杀手。夜间图像整体偏暗,车牌区域对比度低,检测模型容易漏;运动模糊则让字符边缘糊成一团,识别模型直接失效。预处理上可以做两件事:自适应直方图均衡化(CLAHE)提升局部对比度,以及限制对比度的锐化。
def preprocess_lowlight(img): # 转 LAB 空间只对 L 通道做 CLAHE,避免颜色失真 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)) l = clahe.apply(l) img = cv2.cvtColor(cv2.merge([l,a,b]), cv2.COLOR_LAB2BGR) # 非锐化掩蔽做轻度锐化,强度不能大否则噪声放大 blur = cv2.GaussianBlur(img, (0,0), 3) img = cv2.addWeighted(img, 1.5, blur, -0.5, 0) return img逻辑说明:clipLimit=2.0控制对比度增强上限,设太高夜间噪点会被放大成假字符。tileGridSize=(8,8)是分块大小,图像分辨率高就调大。锐化用addWeighted做非锐化掩蔽,权重 1.5 和 -0.5 是常用组合,超过 2.0 会出现明显光晕,反而干扰识别。这套预处理只对低质量图启用,白天正常图不要做,否则过增强也会掉点。
注意:预处理顺序是先去噪再增强,如果先 CLAHE 再降噪,噪声会被一起增强,后面很难去掉。
5. 避坑与排查:车牌识别压缩包最常见的五个翻车点
5.1 现象:识别结果字符顺序错乱或镜像
原因:透视变换时四个角点顺序没有统一,导致矫正后的车牌左右翻转或上下颠倒。识别模型对字符顺序敏感,翻转后 CTC 解码出来的就是反的。
解决:在order_points里严格按「左上、右上、右下、左下」排序,并且对矫正后的图做一次宽高比检查,正常蓝牌宽高比约 3:1,如果矫正后接近 1:3 说明旋转了 90 度,需要额外转回来。
5.2 现象:检测框正常但识别输出空字符串
原因:CTC 解码时所有时间步都预测为 blank,通常是输入图像归一化方式和训练不一致,或者输入尺寸比例严重失真。车牌被拉伸成正方形后,字符间距信息丢失,模型无法对齐。
解决:确认训练时的输入高度和归一化参数,翻训练脚本里的transforms或dataset定义。宽度按比例缩放,不要强制 resize 到固定宽高。如果训练用的是 32x100,推理也必须是 32 高、宽度按比例。
5.3 现象:CUDA out of memory 或推理极慢
原因:批量处理时一次送入太多图,或者模型没有切到 eval 模式导致 dropout 和 batchnorm 行为异常,显存占用翻倍。
解决:推理前必须model.eval()并torch.no_grad()。批量大小从 1 开始试,逐步加到显存上限的 80%。如果是视频流,用跳帧策略从源头减少计算量,比调 batch 更有效。
5.4 现象:新能源绿牌识别率明显低于蓝牌
原因:训练集里蓝牌占绝大多数,绿牌样本少,模型对绿牌字符的 feature 学得不充分。另外绿牌是 8 位字符,蓝牌 7 位,CTC 解码时长度预测容易出错。
解决:如果项目支持微调,收集绿牌样本做增量训练,重点补充 8 位字符的样本。推理阶段可以加一个后处理:根据车牌颜色判断预期长度,对 CTC 输出做长度约束,超出或不足的重新解码。
5.5 现象:换了新场景(地库、逆光)后整体准确率暴跌
原因:模型在训练集分布内表现好,但地库的低对比度和逆光的高光溢出属于分布外数据,检测和识别两级都会退化。
解决:不要指望一个模型打天下。工程上常见做法是准备两套预处理参数,白天和夜间各一套,根据图像整体亮度自动切换。亮度低于阈值走 CLAHE 增强分支,高于阈值走正常分支。这个切换逻辑比重新训练模型成本低得多,效果立竿见影。
6. 把车牌识别做成可复用的服务:一个后处理技巧和我的习惯
跑通单图、调好视频、处理完常见坑之后,真正让这套东西产生价值的是把它封装成可复用的服务。这里分享一个后处理技巧和一个我自己的工程习惯。
后处理技巧是「车牌格式校验 + 省份简称纠错」。CTC 解码出来的文本偶尔会把「京」识别成「凉」、「沪」识别成「护」,这类错误靠模型本身很难完全消除,但可以用规则兜底。中国车牌第一位必须是省份简称,维护一个 31 个简称的集合,解码结果第一位不在集合内时,用编辑距离找最近的合法简称替换。同理,第二位必须是字母,后面是字母数字组合,不符合的按位置做候选替换。这个规则层能把最终准确率再拉高 1~2 个百分点,成本几乎为零。
PROVINCES = set("京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新") def postprocess(text): if not text: return text # 第一位纠错 if text[0] not in PROVINCES: text = min(PROVINCES, key=lambda p: edit_distance(p, text[0])) + text[1:] # 第二位必须是字母 if len(text) > 1 and not text[1].isalpha(): text = text[0] + "A" + text[2:] return text def edit_distance(a, b): dp = list(range(len(b)+1)) for i, ca in enumerate(a, 1): prev, dp[0] = dp[0], i for j, cb in enumerate(b, 1): prev, dp[j] = dp[j], min(dp[j]+1, dp[j-1]+1, prev + (ca != cb)) return dp[-1]逻辑说明:edit_distance是标准动态规划实现,字符集小所以开销可忽略。纠错只在第一位和第二位做,因为这两位的先验最强,后面的字符自由度高,强行纠错反而可能把对的改错。
我自己的习惯是:每接手一个车牌识别压缩包,先花 20 分钟把检测和识别拆开单独测。检测单独跑一批图,看框的召回和矫正质量;识别拿裁剪好的标准车牌图单独跑,看字符准确率。两级分开测,出问题时能立刻定位是哪一级的锅,比端到端瞎调快得多。这个习惯帮我省下了大量「改了识别参数结果发现是检测框歪了」的无效时间。希望帮到你。
本文还有配套的精品资源,点击获取