基于Python的车牌识别:从OpenCV传统方案到深度学习模型实战
2026/9/15 4:21:02 网站建设 项目流程

简介:基于Python的车牌识别参考项目源码包,面向希望学习图像处理、模式识别与PyQt界面开发的Python开发者,以及需要快速搭建车牌识别实验环境的在校学生和研究人员。项目基于Python 3.6/3.7,整合了PyQt5与OpenCV,实现了从车牌定位、字符分割到字符识别的完整流程,并针对不同OpenCV版本进行了适配,方便对比算法表现。资源共2000个文件,其中1987个JPEG文件为车牌样本与中间过程图像,便于验证识别效果和调试;7个py文件为核心算法与界面逻辑,3个xml为配置或分类器数据,1个ui为可视化界面,另有2个md说明文档,压缩包整体约25.53MB。目录层级清晰,携带大量真实场景与合成样本图,适合在课程设计、算法复现或二次开发中直接参考。已有309人学习,能够帮助开发者快速掌握车牌识别中图像预处理、边缘检测、字符切分等关键环节,是理解整套识别管线的高质量入门素材。

1. 车牌识别为什么值得用 Python 从头跑一遍

你手上的这个名为“基于 Python 车牌识别参考项目”的源码压缩包,表面看是一堆.py文件和模型权重,实际拆开之后只有两件事:先在一张图里找到“哪里是车牌”,再把那块区域里的字符转成文本。很多人以为车牌识别是一个端到端黑盒,其实它更像一个流水线——定位、矫正、分割、识别各管一段,而 Python 生态让每一段都有现成库可以拼装。用这个参考项目入门,比直接调用商业 API 更能看清瓶颈在哪,也方便后续替换成更准的开源检测模型。这篇内容会按“选型 → 跑通 → 调参 → 验证”的顺序,把它讲成一套自己也能复刻的方案。

2. 车牌识别的两条路线:OpenCV 传统处理与深度学习模型

2.1 传统路线:灰度、二值化与轮廓检测定位车牌

先从参考项目里最常见的传统方案说起。车牌区域有相对固定的长宽比、边缘密集、字符和底色对比度高,因此可以用 OpenCV 做形态学处理。基本流程是读图转灰度、高斯模糊降噪、Sobel 算子找垂直边缘、二值化后再做闭运算把相邻边缘连成块,最后用cv2.findContours筛出长宽比在 2.5 到 5.5 之间的候选轮廓。

import cv2 import numpy as np def locate_plate_by_contour(img): gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯模糊,去掉路面纹理这类高频噪声 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # Scharr 算子对垂直边缘更敏感,车牌字符基本都是竖笔划 grad_x = cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=-1) abs_grad = cv2.convertScaleAbs(grad_x) # 自适应阈值比固定阈值更抗光照变化 _, binary = cv2.threshold(abs_grad, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 闭运算:先膨胀再腐蚀,把距离近的字符边缘连成一个矩形块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) ratio = w / h # 国内蓝牌常见比例约 3.0~4.5,这里放宽到 2.5~5.5 if 2.5 < ratio < 5.5 and w > 80 and h > 20: return img[y:y + h, x:x + w] return None

这里的参数不是随便拍的。ksize=-1表示 Scharr 算子,比普通 Sobel 更能突出边缘;(17, 5)的核宽度在 1080p 图上能把相邻字符的竖向笔画连起来,核太大容易把整辆车连成一块,太小又连不到一起。轮廓筛选里w > 80h > 20是为了过滤图片远处的小目标,参考项目里原始分辨率如果是 720p 以上,这两个下限可以按比例缩。

传统路线的缺点是抗干扰差:车身贴纸、深色玻璃、路灯杆都可能被当成车牌候选。它适合做快速预筛,给后续深度学习模型省计算量。

2.2 深度学习路线:把车牌检测做成目标检测任务

近几年参考项目里已经很少纯用传统方法做定位,更多是接开源目标检测模型,比如 YOLOv5、YOLOv8 或者专门做车牌检测的轻量网络。原因很简单:检测模型对倾斜、模糊、暗光、新能源绿色车牌的鲁棒性远高于轮廓搜索。所谓“开源车牌检测识别模型”,通常拆成两块——检测头输出车牌框,识别头输出字符序列。

识别部分常见两种设计:一种是直接把车牌区域缩放后送进 CRNN + CTC,输出“省份简称+字母+数字”的序列;另一种是先做字符分割,再用分类器逐个识别。参考项目为了防止文件名乱码,经常把第二步写成ocr_plate_chinese(plate_image)这样的函数,内部要么加载一个*.pth模型,要么用 ONNX Runtime 跑plate_recognition.onnx。如果要在 CPU 上跑,优先找带fp16int8量化的 ONNX 版本,速度差距能达到 3 倍以上。

2.3 参考项目里两种代码的典型分工

如果你把压缩包内的文件拆开看,大概率会看到这样的目录结构:detect.py负责找车牌框,recognize.py负责识别字符,utils/里放着img_utils.pyplate_utils.pydetect.py可能同时包含 OpenCV 兜底逻辑和模型推理逻辑:先用模型检测,如果置信度低于阈值,就退回传统轮廓法再试一次。这种双保险设计在参考项目里很常见,目的是让 Demo 在任何图片上都至少能跑出一个结果,而不是直接报错。

recognize.py里通常有两套字符集:一个CHINESE_CHARS列表,包含 31 个省份简称;一个ALL_CHARS列表,包含大写英文字母和数字。注意这里有个坑:很多公开数据集会把“O”和“0”、“I”和“1”混在一起标,导致模型的字符集里同时存在两个相似字符,推理时容易互相抢。好的参考项目会在后处理时强制做混淆映射,比如把模型输出的 “O” 在车牌第二位之后一律替换为 “0”。

3. 用 Python 复现一个最小车牌识别流程

3.1 环境准备:安装 OpenCV 与 PyTorch(或 ONNX Runtime)

不管参考项目里用了什么框架,最稳妥的做法是建一个干净的虚拟环境再装依赖。Python 版本建议固定到 3.9 或 3.10,太新的 3.12 有时候会遇到 OpenCV 预编译 wheel 不匹配的问题。安装时先把框架独立装好,再装别的库,避免 pip 自动解析出冲突版本。

# 创建虚拟环境,项目隔离是基本习惯 python -m venv veh_env source veh_env/bin/activate # Windows 下用 veh_env\Scripts\activate # 装图像处理与深度学习框架,CPU 也能跑 pip install opencv-python opencv-contrib-python pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 如果参考项目提供的是 onnx 模型,就不需要装 torch,改成这个 # pip install onnxruntime

这里的参数说明:--index-url只对 PyTorch 生效,指定 CPU 版可以让安装包体积小很多,推理速度在车牌这种小目标场景上反而因为省去 CUDA 初始化而更快。如果你的机器有 N 卡,可以去掉--index-url装默认的 CUDA 版,但要注意 PyTorch 版本和显卡驱动要匹配。

3.2 车牌定位:从读图到候选框的完整代码

下面这段代码把前面 2.1 的轮廓法升级成“模型优先、OpenCV 兜底”的混合定位。假设参考项目里提供了best_plate_detect.onnx,我们就用 ONNX Runtime 加载。

import cv2 import numpy as np import onnxruntime as ort class PlateLocator: def __init__(self, onnx_path): self.session = ort.InferenceSession(onnx_path) self.input_name = self.session.get_inputs()[0].name self.output_name = self.session.get_outputs()[0].name def detect(self, img): # 模型输入一般是 640x640 的 RGB 图,需要做 letterbox 填充 h, w = img.shape[:2] scale = 640 / max(h, w) new_w, new_h = int(w * scale + 0.5), int(h * scale + 0.5) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((640, 640, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized # YOLO 输出是 [batch, 6 + num_classes, num_anchors],先转成正常坐标 blob = canvas[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob = np.expand_dims(blob, 0) preds = self.session.run([self.output_name], {self.input_name: blob})[0] # 这里假设只有一个输出,取第一个 batch 并且置信度 > 0.5 的框 dets = preds[0].transpose(1, 0) boxes = [] for d in dets: if d[4] < 0.5: continue x, y, x2, y2 = d[:4] # 还原到原始坐标 x, y, x2, y2 = x / scale, y / scale, x2 / scale, y2 / scale boxes.append((int(x), int(y), int(x2 - x), int(y2 - y))) return boxes

这段代码里最容易被忽略的是letterbox填充而不是直接resize。因为直接拉伸会改变车牌长宽比,后续字符识别会对这个比例敏感。114是 YOLO 系列常用的填充灰色值,和训练时保持一致。置信度0.5是一个初始值,实际项目中需要根据测试集调整,这个放到第 4 章专门讲。

3.3 字符分割与 OCR 识别:模板匹配与 CRNN 两种做法

拿到车牌框后,下一步是识别字符串。最简单的参考实现是字符分割加模板匹配:先对车牌区域做灰度化、二值化,用轮廓找到每个字符的包围盒,切出来和模板库里的字符图片做像素匹配。

def recognize_plate_by_template(plate_img): # 先放大车牌,提高分割稳定性 plate = cv2.resize(plate_img, (360, 110)) gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 180, 255, cv2.THRESH_BINARY_INV) # 用固定核做一次竖向膨胀,让汉字笔画合并成一个连通域 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 5)) merged = cv2.dilate(binary, kernel, iterations=1) contours, _ = cv2.findContours(merged, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) char_boxes = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) # 过滤掉明显不是字符的小噪点,以及超高或超矮的条状物 if 5 < w < 80 and 20 < h < 100 and h / w < 4: char_boxes.append((x, y, w, h)) char_boxes.sort(key=lambda b: b[0]) # 模板匹配部分省略,核心是把每个字符图与模板库里所有图做归一化相关性比较 return [match_template(plate[y:y+h, x:x+w]) for (x, y, w, h) in char_boxes]

但模板匹配对字体变化很脆弱,参考项目里真正的识别器大多用 CRNN。CRNN 的核心思想是先把图像变成特征序列,再用循环网络或 Transformer 逐帧预测字符,最后通过 CTC 对齐。代码里通常会写这样一段加载逻辑:

import cv2 import numpy as np def preprocess_for_crnn(plate_img): # CRNN 输入高度固定为 32,宽度按比例缩放,宽度最好是 16 的倍数 h, w = plate_img.shape[:2] target_h = 32 scale = target_h / h new_w = int(w * scale) # 宽度对齐到 16 的倍数,方便 CNN 下采样 new_w = (new_w // 16) * 16 resized = cv2.resize(plate_img, (new_w, target_h)) # 转灰度、归一化、加 batch 维度 resized = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) img = resized.astype(np.float32) / 255.0 img = np.expand_dims(img, axis=(0, 1)) return img

注意new_w必须能整除 16,否则模型最后的全连接层维度对不上。这是新手最容易报shape mismatch的地方。如果参考项目的模型是从 PaddleOCR 转出来的,前处理还得加一次归一化均值0.5、方差0.5,不同训练框架的处理细节不一样,拿到源码后先看preprocess函数,别直接套 OpenCV 默认值。

3.4 把检测与识别串成函数

整个流程最终会汇聚成一个plate_recognition(image_path)函数,方便批量调用。常见写法是下面这样:

def plate_recognition(image_path, locator, recognizer): img = cv2.imread(image_path) boxes = locator.detect(img) results = [] for x, y, w, h in boxes: # 稍微向外扩一点边缘,避免裁剪掉车牌的白色边框 x1, y1 = max(0, x - 5), max(0, y - 5) x2, y2 = min(img.shape[1], x + w + 5), min(img.shape[0], y + h + 5) plate = img[y1:y2, x1:x2] text, score = recognizer(plate) results.append((text, score, (x, y, w, h))) return results

这个函数的参数含义要明确:x - 5y - 5是外扩像素,防止检测框刚好卡掉车牌边缘的字符;score是识别器的置信度,后续可以拿它过滤低质量结果。把定位和识别解耦,后续想换更强的检测器,只需要改动locator部分,不影响识别逻辑。

4. 车牌识别模型效果不稳?先调这三个参数

4.1 目标检测置信度阈值:宁漏勿错还是宁错勿漏

车牌识别系统一旦部署到停车场或道路监控,误检和漏检的成本完全不同。如果后续有车道闸机拦截,漏检导致抬不了杆,用户会投诉;误检把广告牌当车牌,可能把分钟级账单算错。参考项目里默认的conf_threshold=0.5没有区分场景。

调参时不要直接拍脑袋。建议先用 20 张包含车牌的图片跑一遍,打印每个候选框的置信度,画分布直方图。常见现象的阈值是 0.3 附近还有不少非车牌框,0.7 以上开始有漏检。用手头数据画出一条“置信度-召回率”曲线,再决定取哪个值。通常停车场固定机位可以拉到 0.6,因为相机角度固定,不会出现极端形变;移动手持设备建议降到 0.4,宁可多裁一块也不能把车牌丢掉。

4.2 OCR 识别器的序列解码参数:beam width 与 blank 惩罚

CRNN 系列模型在解码时有几个参数直接影响字符准确率。beam width控制搜索宽度,设为 1 就是贪心解码,设为 5 可以考虑多个候选路径。车牌字符最长也就 8 个,beam width 不必太大,5 和 10 的效果差距很小,但耗时能差一倍。

更关键的是blank惩罚。CTC 算法会引入一个blanktoken 代表没有字符,模型输出时长串里如果 blank 概率过高,会导致字符被吞掉。很多参考项目给出的后处理代码里只写了一句argmax+去重,根本没做 blank 惩罚,所以识别“粤B12345”时经常漏掉中间某个数字。正确的做法是在 CTC 解码的天机上给 blank 路径加一个负的偏置,比如把每个时间步的 blank 概率乘以0.4,让模型更倾向于输出真实字符。这个系数在不同数据集上的最优值在0.3~0.7之间,需要小批量验证。

还有一个容易被忽略的参数是vocab顺序。PaddleOCR 之类的中文识别模型,字符表是按中文、英文、数字顺序排列的,直接拿来当车牌识别器,会把省份简称识别成常见汉字。参考项目如果自带字典文件,先检查里面有没有“粤、沪、京”这些省份简称,没有的话得往字符表里加,并重新训练最后一层分类头。

4.3 预处理尺寸与颜色空间转换的隐藏影响

很多拿参考项目直接跑的用户都会问:为什么同一张图,换一个resize尺寸结果就变了?这不是玄学。检测模型输入是 640x640,但如果车牌在图中只占很小的面积,直接缩小后车牌字符已经被压成几个像素,模型天然看不见。解决办法是在检测前对图像分块或做瓦片预测,再用 NMS 合并结果。

颜色空间也是个大坑。车牌识别模型一般用 RGB 训练,而 OpenCV 默认读进来是 BGR。如果参考项目的detect.py里漏了cv2.cvtColor(img, cv2.COLOR_BGR2RGB),那么模型看到的颜色通道是反的,对蓝色车牌的响应会显著变弱。另一个隐蔽问题是灰度化后的对比度归一化。有些车牌图片对比度很低,直接用cv2.imread读进来,字符和底色几乎分不开,这时需要对图片做 CLAHE 自适应直方图均衡化:

import cv2 def enhance_plate(img): lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) l = clahe.apply(l) enhanced = cv2.merge((l, a, b)) return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)

这里clipLimit=3.0控制对比度增强强度,值太小没效果,值太大车牌上的白字会过曝。tileGridSize表示把图像分成多少块做局部均衡,8x8 适合 720p 以上的图;如果车牌区域很小,建议改成 4x4,避免局部网格被单一底色占据。

5. 验证车牌识别模型的两种实战技巧

5.1 建立一个带标注的图片文件夹做批量识别率统计

很多人拿到参考项目后只跑单张图,看到输出“粤B12345”就认为成功了,换成一张倾斜的车牌马上打回原形。正确的做法是从项目里抽出批量测试脚本,把所有测试图片放进一个目录,图片文件名直接标注真实车牌号,比如粤B12345_01.jpg。然后写一个脚本,自动对比预测结果和文件名前缀。

import os import plate_recognition def batch_eval(test_dir): total = 0 correct = 0 correct_chars = 0 total_chars = 0 for f in os.listdir(test_dir): if not f.lower().endswith(('.jpg', '.png', '.jpeg')): continue true_text = os.path.splitext(f)[0].split('_')[0] img_path = os.path.join(test_dir, f) results = plate_recognition.plate_recognition(img_path, locator, recognizer) if not results: print(f'{f}: 未检测到车牌') continue pred_text, score = results[0][0], results[0][1] total += 1 # 整串完全匹配才算正确 if pred_text == true_text: correct += 1 # 同时统计字符级准确率,方便定位是“汉字错了”还是“数字错了” for p, t in zip(pred_text, true_text): if p == t: correct_chars += 1 total_chars += len(true_text) print(f'{f}: 预测={pred_text}, 真值={true_text}, 置信度={score:.3f}') print(f'整串准确率: {correct / total:.2%}') print(f'字符级准确率: {correct_chars / total_chars:.2%}')

这个脚本输出的两个指标很有用:如果整串准确率远低于字符级准确率,说明模型大多数字符都认得对,只是偶尔错一两个字,这种场景更适合做后续纠错而不是换模型;如果字符级准确率都低,那就得回头处理 4.3 节的图像增强问题。

5.2 对模糊、倾斜、多车牌场景做二次修正

参考项目一般只处理单张正视角车牌,但真实场景里总有车辆转弯压线、手机抓拍抖动的情况。针对倾斜车牌,可以在识别前加一个仿射变换矫正。常见做法是检测车牌的四个角点,然后把四边形透视矫正成矩形,OpenCV 里有cv2.getPerspectiveTransform可以用。很多源码里会先检测车牌上下边缘的直线,计算倾斜角angle,再用cv2.warpAffine旋转。

对于模糊车牌,不要直接把低分辨率图送去识别,试试先做一次超分辨率预处理。参考项目通常不会自带超分模型,但可以调用现成的opencv-contrib-python里的dnn_superres模块,加载一个轻量的 EDSR 模型,把车牌区域放大 2 倍再识别。这个操作能把字符识别的准确率从 60% 拉到 80% 以上,代价只是每张图多几十毫秒。

多车牌场景则要增加一个 NMS 后处理。检测器可能对同一个车牌输出多个重叠框,必须在locator.detect返回结果后先按 IoU 做一次非极大值抑制,保留置信度最高的那个。可以复用cv2.dnn.NMSBoxes,它接受(x, y, w, h)格式的框列表和对应的置信度列表。参考项目如果输出置信度排序混乱,最好自己按score降序排一遍再做 NMS,否则可能留下一个边角框。

最后建议维护一个badcase.txt,把每张错图的文件名、预测结果、真值、置信度写进去。持续收集 100 个错例后,你能清楚地看到瓶颈集中在“汉字识别”还是“数字混淆”,这一条经验比任何调参攻略都实用。

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

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

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

立即咨询