
简介这份资源是一个基于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、哪套识别方案CRNNCTC 还是 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 python3.9 -y conda activate lpr # 先装 torch注意和 CUDA 版本对应没有 GPU 就用 cpu 版 pip install torch2.0.1 torchvision0.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 里写的是torch1.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, conf0.25, iou0.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]], dtypenp.float32) M cv2.getPerspectiveTransform(pts, dst) warped cv2.warpPerspective(img, M, (w, h)) plates.append(warped) return plates def order_points(pts): # 按 xy 和 x-y 区分四个角避免角点顺序错乱导致图像翻转 rect np.zeros((4,2), dtypenp.float32) s pts.sum(axis1) rect[0] pts[np.argmin(s)] rect[2] pts[np.argmax(s)] d np.diff(pts, axis1) rect[1] pts[np.argmin(d)] rect[3] pts[np.argmax(d)] return rect逻辑说明conf0.25是检测置信度阈值车牌场景建议比通用检测低一些因为远距离车牌目标小、得分偏低设太高会漏检。iou0.45控制重叠框合并车牌一般不会密集堆叠这个值够用。order_points是血泪经验——不做角点排序透视变换后车牌可能是上下颠倒或左右镜像的识别模型直接懵掉。参数调整上如果发现车牌框偏大包含太多背景把conf提到 0.4 左右如果漏检严重降到 0.15 并检查输入分辨率YOLO 默认 640远距离车牌可能需要imgsz1280。3.2 识别阶段CRNN 解码与字符集对齐识别阶段把矫正后的车牌图转成文字。CRNNCTC 是车牌识别里最稳的方案因为它不需要字符级标注且对车牌这种定长字符序列很友好。关键点是字符集必须和训练时一致否则解码出来的索引对不上输出全是乱码。import torch from crnn import CRNN # 假设项目里有这个定义 # 字符集顺序必须和训练时完全一致差一个字符全盘错 CHARS 京沪津渝冀晋蒙辽吉黑苏浙皖闽赣鲁豫鄂湘粤桂琼川贵云藏陕甘青宁新0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ rec_model CRNN(num_classeslen(CHARS)1) # 1 是 CTC blank rec_model.load_state_dict(torch.load(weights/rec.pth, map_locationcpu)) 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(dim2)[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(clipLimit2.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逻辑说明clipLimit2.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, keylambda 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 分钟把检测和识别拆开单独测。检测单独跑一批图看框的召回和矫正质量识别拿裁剪好的标准车牌图单独跑看字符准确率。两级分开测出问题时能立刻定位是哪一级的锅比端到端瞎调快得多。这个习惯帮我省下了大量「改了识别参数结果发现是检测框歪了」的无效时间。希望帮到你。本文还有配套的精品资源点击获取