ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Python的车牌识别:从OpenCV传统方案到深度学习模型实战

基于Python的车牌识别:从OpenCV传统方案到深度学习模型实战 简介基于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.04.5这里放宽到 2.55.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 80和h 20是为了过滤图片远处的小目标参考项目里原始分辨率如果是 720p 以上这两个下限可以按比例缩。传统路线的缺点是抗干扰差车身贴纸、深色玻璃、路灯杆都可能被当成车牌候选。它适合做快速预筛给后续深度学习模型省计算量。2.2 深度学习路线把车牌检测做成目标检测任务近几年参考项目里已经很少纯用传统方法做定位更多是接开源目标检测模型比如 YOLOv5、YOLOv8 或者专门做车牌检测的轻量网络。原因很简单检测模型对倾斜、模糊、暗光、新能源绿色车牌的鲁棒性远高于轮廓搜索。所谓“开源车牌检测识别模型”通常拆成两块——检测头输出车牌框识别头输出字符序列。识别部分常见两种设计一种是直接把车牌区域缩放后送进 CRNN CTC输出“省份简称字母数字”的序列另一种是先做字符分割再用分类器逐个识别。参考项目为了防止文件名乱码经常把第二步写成ocr_plate_chinese(plate_image)这样的函数内部要么加载一个*.pth模型要么用 ONNX Runtime 跑plate_recognition.onnx。如果要在 CPU 上跑优先找带fp16或int8量化的 ONNX 版本速度差距能达到 3 倍以上。2.3 参考项目里两种代码的典型分工如果你把压缩包内的文件拆开看大概率会看到这样的目录结构detect.py负责找车牌框recognize.py负责识别字符utils/里放着img_utils.py和plate_utils.py。detect.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, dtypenp.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, iterations1) 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(keylambda b: b[0]) # 模板匹配部分省略核心是把每个字符图与模板库里所有图做归一化相关性比较 return [match_template(plate[y:yh, x:xw]) 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 - 5和y - 5是外扩像素防止检测框刚好卡掉车牌边缘的字符score是识别器的置信度后续可以拿它过滤低质量结果。把定位和识别解耦后续想换更强的检测器只需要改动locator部分不影响识别逻辑。4. 车牌识别模型效果不稳先调这三个参数4.1 目标检测置信度阈值宁漏勿错还是宁错勿漏车牌识别系统一旦部署到停车场或道路监控误检和漏检的成本完全不同。如果后续有车道闸机拦截漏检导致抬不了杆用户会投诉误检把广告牌当车牌可能把分钟级账单算错。参考项目里默认的conf_threshold0.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(clipLimit3.0, tileGridSize(8, 8)) l clahe.apply(l) enhanced cv2.merge((l, a, b)) return cv2.cvtColor(enhanced, cv2.COLOR_LAB2BGR)这里clipLimit3.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 个错例后你能清楚地看到瓶颈集中在“汉字识别”还是“数字混淆”这一条经验比任何调参攻略都实用。本文还有配套的精品资源点击获取
返回列表