简介:基于Python的车牌识别参考项目源码包,整合PyQt5与OpenCV技术栈,面向图像处理、模式识别方向的开发者与学习者,提供一套包含界面交互、图像预处理、车牌定位与识别在内的可运行参考框架,可用于智能交通场景下的算法研究与功能复现。包内共2000个文件,其中1987张jpg图片为车牌样本与调试中间结果,7个py脚本是核心实现,另有3个xml配置、2个md说明文档及1个Qt界面文件,压缩包约25.53MB,结构清晰便于按需查阅。项目适配Python 3.6/3.7,并兼容OpenCV 3.4.3与4.2.0,覆盖从车牌图像输入、区域提取到字符识别的完整流程,附带的界面源码和大量真实场景图片可帮助读者理解各环节技术要点,并作为二次开发的基础。目前已有309人学习,适合希望通过实战项目掌握PyQt5界面开发与OpenCV图像处理综合应用的入门及进阶用户。
1. 说是“参考项目”,其实它就是车牌识别的完整脑图
拿到“基于 python 车牌识别参考项目【源码】.zip ”这类资源,很多人的第一反应是:这又是一份跑不起来的玩具代码。但车牌识别这个方向,恰恰是所有计算机视觉入门者最该花两周时间复现一遍的项目——它的技术栈非常完整,却不至于复杂到让人放弃:图像采集、车牌定位、字符分割(或端到端识别)、结果后处理,每一环都是后续做安防、道闸、智慧停车时每天都在碰的问题。而一个“参考项目”的价值,不在于它识别率是 98% 还是 99%,而在于它把整套方案的骨架摆在了你面前。
这份工作里我经手过的车牌识别源码不少,从 OpenCV 传统方案到 YOLO + LPRNet 的深度学习组合都搭过。说实话,Python 车牌识别项目最大的劝退点从来不是算法本身,而是环境:OpenCV 版本冲突、PyTorch 装半天装不上、汉字字符集标注对不上、蓝牌识别还行绿牌一塌糊涂。这篇文章就按我实际踩坑的顺序,从环境搭建开始,讲到跑通最小识别,再讲用 ONNX 做提速部署,最后把坑和调优边界都交代清楚。零基础跟到最后一节,能自己写出一个能用的识别脚本;有经验的也能在部署那节找到点有用的东西。
2. 先把环境捣鼓明白:Python 版本、OpenCV 与深度学习框架的选择
2.1 Python 版本选择:为什么建议 Python 3.8~3.10
车牌识别参考项目十有八九是从 GitHub 或 CSDN 上传下来的,它们的依赖要求往往写在 requirements.txt 里,但这份文件只列了包名,很少告诉你 Python 版本。我在多台机器上踩出来的结论是:Python 3.8 到 3.10 是最稳的区间,3.6 及以下装新版 OpenCV 和 PyTorch 会疯狂报错,3.11 以上某些老项目的 tensorflow 版本直接装不上。如果你拿到的源码里用了 PyTorch,注意看它 import 的是 torch 还是 torchvision,这决定了你装的是 CPU 版还是 GPU 版。
# 建议用 conda 创建独立环境,避免把系统 Python 搞乱 conda create -n plate python=3.9 -y conda activate plate # 安装 OpenCV(注意 opencv-python 只含主模块,opencv-contrib-python 含扩展模块) pip install opencv-python==4.8.1.78 opencv-contrib-python==4.8.1.78 # 如果参考项目依赖 PyTorch 做字符识别,装 CPU 版就够了 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cpu参数说明:conda create -n plate python=3.9里的plate是环境名,可以随便起;python=3.9指定解释器版本。OpenCV 版本我特意锁在 4.8.x,是因为 4.9 之后cv2.findContours的参数行为没有变化,但某些传统形态学处理在 4.9 上有细微差异,参考项目里如果写了cv2.boundingRect这类接口,锁版本能省去大量排查时间。torch 的--index-url .../cpu会安装 CPU 版,对车牌识别场景 GPU 并非必需。
2.2 跑通参考项目的最小命令:加载模型与单张图片识别
环境装好后的第一个里程碑,是让参考项目能对单张图片输出车牌号。大多数 Python 车牌识别项目的主入口长这样:一个detect_plate.py文件,里面定义了recognize_plate(image_path)函数,或者在 main 函数里用argparse接收图片路径。你先不要急着改任何参数,直接按项目 README 的说明跑一次,记录下来三个东西:识别结果是什么、耗时多少毫秒、控制台有没有打印警告。
import argparse import cv2 # 假设参考项目的核心模块是 plate_recognition from plate_recognition import PlateRecognizer def main(): parser = argparse.ArgumentParser(description="车牌识别参考项目运行入口") parser.add_argument("--image", type=str, required=True, help="输入图片路径") parser.add_argument("--conf", type=float, default=0.5, help="置信度阈值") parser.add_argument("--cpu", action="store_true", help="强制使用 CPU 推理") args = parser.parse_args() # 实例化识别器,参考项目通常会在这里加载检测模型和识别模型 recognizer = PlateRecognizer(conf_thresh=args.conf, use_cpu=args.cpu) image = cv2.imread(args.image) if image is None: raise FileNotFoundError(f"无法读取图片: {args.image}") # 核心推理:返回车牌框坐标 + 识别文本 + 置信度 result = recognizer.recognize(image) print(f"识别结果: {result}") if __name__ == "__main__": main()这段代码的逻辑很直白:argparse负责接收命令行参数,PlateRecognizer是你需要重点阅读的类,它内部通常串联了目标检测和文本识别两个模型。--conf 0.5的意思是只有置信度超过一半的检测框才被保留,太低了会出现误检。--cpu参数是我建议参考项目里都应加的,因为很多人第一次跑用的是笔记本,GPU 环境没配好,如果源码默认走 CUDA,直接就会报AssertionError: torch.cuda.is_available() is False的错。
2.3 项目里常见的目录结构:别急着读算法,先读这三个文件
源码包解压后,你看到的往往是一堆 .py 文件和几个文件夹,新手容易一上来就打开主干算法文件开始啃,这是低效的。我建议按这个顺序快速摸清项目:先读requirements.txt,确认依赖有没有坑;再读README.md,看作者写的运行命令和数据说明;最后读main.py或入口脚本,把数据流捋清楚。如果项目里有config.py或config.yaml,那更是重点,因为参数都集中在那里。
# config.yaml 示例:参考项目常用的参数配置结构 detect_model: path: "weights/yolov5s_plate.pt" # 车牌检测模型权重 imgsz: 640 # 输入分辨率,越大越慢但小目标更准 conf_thresh: 0.45 # 检测置信度阈值 iou_thresh: 0.45 # NMS 的 IoU 阈值 recognize_model: path: "weights/lprnet.pth" # 车牌识别模型权重 input_height: 48 # 字符识别输入高度,不能随意改 input_width: 168 # 宽度根据训练时设定 class_dict: - "京" - "津" # ... 省份简称列表,顺序和训练时保持一致这里的核心参数就三个:conf_thresh和iou_thresh控制检测框的粒度,input_height和input_width是识别模型的输入尺寸。很多中文车牌项目识别不准,问题不在算法,而是class_dict里的省份简称顺序和模型训练时不一致,导致输出映射错乱。拿到源码后先比对这一块,最快耽误半小时,最慢耽误一整天。
3. 车牌识别的主流方案对比:传统 CV、深度学习与 ONNX 部署路径
3.1 传统方案:HSV 颜色定位 + 轮廓筛选 + 模板匹配,作为学习骨架仍然值得看
传统方案在深度学习普及前是主流,它不依赖 GPU,在配置低的机器上也能跑,缺点是泛化能力弱。参考项目如果走这条路,流程通常是:先做灰度化 + 高斯模糊 + Sobel 或 Canny 边缘检测,然后形态学闭运算把车牌区域连通,再用cv2.findContours找到候选轮廓,最后根据车牌的宽高比(蓝牌约为 440:140,即 3.14 左右)筛选出真正属于牌照的框。字符识别环节则用模板匹配,把截取的车牌图像切分成单个字符,和预先准备好的字符模板比对相似度。
import cv2 import numpy as np def locate_plate_traditional(image): """传统 CV 定位车牌:边缘检测 + 形态学 + 轮廓筛选""" # 1. 转为灰度图并做高斯模糊 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 2. 用 Sobel 算子检测垂直边缘 —— 车牌字符密集,垂直边缘响应强 sobel = cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize=3) abs_sobel = np.absolute(sobel) sobel_8u = np.uint8(abs_sobel) edges = cv2.normalize(sobel_8u, None, 0, 255, cv2.NORM_MINMAX) # 3. 阈值化 + 闭运算,把字符区域连成一片 _, binary = cv2.threshold(edges, 100, 255, cv2.THRESH_BINARY) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 4. 寻找外轮廓,按面积和宽高比筛选 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) if w > 80 and h > 20 and 2.5 < w / h < 4.5: # 面积占比过滤:车牌区域应占整图的一定比例 area = w * h if area > image.shape[0] * image.shape[1] * 0.001: candidates.append((x, y, w, h)) return candidates这段代码的筛选逻辑用的是经验参数:车牌宽高比在 2.5 到 4.5 之间,太窄或太宽的直接丢弃;面积占比不低于整图的千分之一,避免把远处的小噪点当车牌。Sobel的ksize=3是梯度算子的核大小,越大对噪声越敏感,这里 3 就够。闭运算的核(17, 5)表示水平方向扩展 17 像素、垂直方向 5 像素,因为车牌字符是横向排列的。
传统方案的参考价值在于:它把“定位”这个动作拆解得很清楚,每一步的输入输出都是肉眼可验证的中间图像。你会明白预处理从灰度到边缘再到形态学是怎么一步步逼近目标的。但它的天花板也很明显——光照不均、倾斜车牌、新能源汽车的渐变绿底车牌都会让阈值失效。所以我一般建议新手拿传统方案建立直觉,但要清楚这只是入门,不是能直接落地的方案。
3.2 深度学习方案:YOLO 检测车牌 + LPRNet/CRNN 识别字符
当前 Python 车牌识别参考项目的顶配组合是:YOLO 系模型负责检测车牌位置,LPRNet 或 CRNN 负责把裁剪出来的车牌图转成字符串。这两个模型职责不同,前者输出边界框[x, y, w, h, confidence],后者输出字符序列。LPRNet 是专门为车牌识别设计的轻量网络,不依赖 RNN,支持变长序列预测,所以中文车牌(省份汉字 + 字母 + 数字,共 7 或 8 个字符)用起来很顺。CRNN 则是 CNN + BiLSTM + CTC 的经典结构,优缺点都有:准确率上限更高,但模型体积更大、推理更慢。
import torch import cv2 import numpy as np def deep_plate_recognize(image, detect_model, ocr_model, device): """YOLO 检测车牌 + LPRNet 识别字符的完整流程""" # 检测阶段:用 YOLO 拿到车牌框 results = detect_model(image, size=640) boxes = results.xyxy[0].cpu().numpy() plate_texts = [] for box in boxes: x1, y1, x2, y2, conf, cls = box if conf < 0.5: # 置信度过滤,低分的检测框大多是多检或误检 continue # 裁剪车牌区域,LPRNet 要求输入高度 48,宽度按比例缩放 crop = image[int(y1):int(y2), int(x1):int(x2)] crop = cv2.resize(crop, (168, 48)) crop = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) crop = crop.astype(np.float32) / 255.0 # 转为 PyTorch Tensor 并增加 batch 和 channel 维度 tensor = torch.from_numpy(crop).unsqueeze(0).unsqueeze(0).to(device) # 识别阶段:模型输出的是 18 个时间步的字符概率分布 with torch.no_grad(): logits = ocr_model(tensor) # 这一步是 CTC 解码:去重 + 去空白符,得到最终字符串 text = ctc_decode(logits) plate_texts.append((text, conf)) return plate_texts这段流程里最重要的不是推理代码本身,而是预处理细节:LPRNet 的输入高度 48 是固定的,宽度却可以有一定弹性,但参考项目里大多固定为 168,因为训练时就用的这个尺寸。ctc_decode是一个函数,参考项目里通常也叫greedy_decode,核心逻辑是取每个时间步概率最高的字符,然后合并重复字符并去掉空白符。这个模型对灰度图更稳定,用cv2.COLOR_BGR2GRAY转灰度精度会略高于直接输入 RGB,因为训练数据就是这么处理的。
3.3 用 ONNX Runtime 做推理提速:不仅能脱离 PyTorch,还能跑在边缘设备上
参考项目如果用的是 PyTorch,直接部署到生产环境会有两个问题:一是 pytorch 包体积大,二是 GPU 环境依赖复杂。而转成 ONNX 格式后,可以用 onnxruntime 做 CPU 推理,速度比 PyTorch CPU 模式快不少,而且.onnx文件可以部署到《车牌识别》相关的边缘设备上,这也正是热词里“java onnx 车牌识别”背后大家常用的路径。转换过程看似简单,但有几个坑:YOLO 的torch.argmax操作不能直接转、动态输入尺寸要显式指定、批处理维度建议保持 1。
import torch import onnxruntime as ort # 以 YOLOv5 检测模型为例,导出 ONNX 格式 def export_onnx(model, output_path="plate_det.onnx"): model.eval() dummy_input = torch.randn(1, 3, 640, 640) # 输入张量:1 张图,RGB,640x640 with torch.no_grad(): torch.onnx.export( model, dummy_input, output_path, opset_version=11, input_names=["images"], output_names=["output"], dynamic_axes={"images": {0: "batch"}} # 仅 batch 维度动态 ) print(f"ONNX 导出完成: {output_path}") # 用 onnxruntime 推理 session = ort.InferenceSession("plate_det.onnx", providers=["CPUExecutionProvider"]) def onnx_infer(image_np): input_tensor = image_np.astype("float32") / 255.0 input_tensor = input_tensor.transpose(2, 0, 1)[None, ...] # HWC -> NCHW outputs = session.run(None, {"images": input_tensor}) return outputs[0]导出时最容易翻车的点是opset_version。版本太低不支持某些算子,版本太高在边缘设备上又可能跑不动,我常用的妥协值是 11,兼容性最广。dynamic_axes只把 batch 维度设为动态,而不要动态化宽高,因为车牌检测模型在训练时用的就是固定 640x640 输入,运行时动态宽高会显著掉点。providers参数在 CPU 机器上只能填CPUExecutionProvider,但也有 Java 程序接入 onnxruntime 的场景——这时参考项目的 Python 代码只负责离线导出模型,在线推理由 Java 端加载同一个 .onnx 文件完成。
3.4 三种方案的选型建议:到底哪种适合你的参考项目
做选型之前先明确应用场景,不问场景就推荐的都是在耍流氓。如果是课程设计或毕业设计,传统方案足够交差,因为它的工程量容易展示且算法逻辑能讲透;如果是实际停车场道闸项目,必须用深度学习方案或者臻识这类一体机设备,因为真实环境里车牌角度、光照变化剧烈;如果做的是嵌入式部署,比如树莓派或 Jetson Nano,那么 ONNX 方案最合适。你要做的判断不是哪个方案“更高级”,而是哪个方案在自己的时间预算和硬件条件下能稳定跑起来。
| 方案 | 定位能力 | 识别能力 | CPU 推理耗时 | 部署复杂度 | 适合场景 |
|---|---|---|---|---|---|
| 传统 CV | 中等 | 中低 | 50~150ms | 低 | 课程设计、固定机位演示 |
| YOLO + LPRNet | 高 | 高 | 100~300ms | 中 | 停车场、真实环境 |
| YOLO + ONNX | 高 | 高 | 60~200ms | 中 | 边缘设备、Java 集成 |
表格里的耗时是基于 640x640 输入在普通笔记本 CPU 上的估算值,实际数据取决于硬件。参考项目里如果是传统方案,你可以评估加一个测试脚本,在同一批图片上跑一个简单的混淆矩阵;如果是深度学习方案,建议先跑通 ONNX 这条路再谈优化,否则后面换设备时模型格式就是最大的绊脚石。
4. 车牌识别翻车避坑专场:5 个真实场景的踩坑记录
4.1 现象:蓝牌能识别,新能源绿牌完全认不出来
很多参考项目训练数据里没有绿牌或样本极少,导致定位阶段就把绿牌当成背景丢掉了。绿牌的特点是宽度比蓝牌大(新能源车牌 480x140,比蓝牌多一位字符),颜色是渐变绿色,HSV 空间里色相跨度大,如果传统方案用的是固定绿色范围筛选,必然漏检。
原因:训练集类别不平衡,detect 模型没见过足够多的绿牌;或者传统方案只针对蓝牌的蓝色区域做了颜色过滤。
解决:先看参考项目中检测模型训练的类别是否包含 green_plate,没有的话需要自己补数据微调。传统方案的话,把颜色筛选逻辑从单色扩展为蓝色 + 绿色两个区间,同时放宽宽高比范围到 2.5 到 5.0。如果只是临时演示,可以把输入图片先做一次颜色增强,但这不是长效解法。
4.2 现象:车牌是倾斜的,识别结果漏字符或乱码
当车辆在画面里带角度,检测框把车牌斜着裁出来,LPRNet 对倾斜图像很敏感,因为训练数据大多是正视角。尤其是两行式黄牌卡车车牌,裁剪后容易把第二行漏掉。
原因:检测模型输出的是水平矩形框,没有角度信息;识别模型对旋转失真泛化能力差。
解决:参考项目里如果有cv2.getPerspectiveTransform的代码,说明作者已经预留了矫正能力。没有的话你需要做一个透视变换矫正步骤:找到车牌的四个角点(可以用轮廓的外接矩形拟合),然后映射到标准 440x140 的尺寸。注意透视变换后要做一次锐化增强,因为变换会把边缘变模糊。
4.3 现象:同一个坐标在 Java 调用 ONNX 部署时不生效,识别框整体偏移
我遇到过一整个下午都在排查的问题:Python 脚本里识别准确,导出 ONNX 后用 Java 调用,检测框在画面偏左偏上,导致字符识别全错。后来发现是 Python 端预处理用了letterbox缩放,把图片从原始尺寸缩放并填充到 640x640,而 Java 端直接resize到 640x640 没有保持宽高比,导致物体位置在坐标系里整体漂移。
原因:推理前后处理不一致,检测框的坐标是在填充后的图上计算的,映射回原图时用的缩放因子不同。
解决:必须把letterbox的填充逻辑原样复制到 Java 端,或者导出 ONNX 时统一输入尺寸和缩放策略。我的习惯是把预处理和后处理封装成一个纯 Python 函数,并输出一份调用约定文档,注释里写明“输入为原始 BGR 图,输出坐标为原图坐标系”,这样跨语言调用时不会因理解偏差出问题。
4.4 现象:识别结果中英文字母 O 和数字 0 混淆,反复出现
车牌字体里字符“O”和“0”极像,“Z”和“2”在某些省份字体下也无法区分。这在字符识别任务中是经典难点,不是代码 bug。
原因:模型训练时如果标注错误,或者字符集里 O 和 0 都有,但训练图像中某个类别样本太少,模型就会倾向概率高的那一边。
解决:两个办法——第一,和后端业务系统约定:车牌文本里只允许出现数字 0,不允许字母 O,因为现实中大陆车牌不会出现字母 O 和 I(这是交通法规规定的);在做 post-processing 时写一个映射函数,把模型输出的 O/0 统一替换为 0,把 I/1 统一替换为 1,能减小 80% 的此类错误。第二,如果参考项目自带训练脚本,可以对字符做等比例过采样,用数据增强弥补样本不均衡。
4.5 现象:图片尺寸很大时推理崩溃,内存溢出
参考项目中的测试图片大多是网络下载的 1920x1080,而摄像头可能输出 3840x2160 或者更高。检测模型输入是 640x640,但如果在预处理时直接把大图一次性读入 numpy 数组并进行letterbox缩放,有时会出现内存峰值过高,尤其是同时处理多路视频流。
原因:加载大图、转换色彩空间、创建填充画布这三个操作同时占据内存,多线程时会叠加。
解决:在预处理前先做一次尺寸判断,对于长边超过 1600 的图片先等比缩放到长边 1280,再进行标准预处理。这样做的好处是检测精度几乎不掉,因为车牌本身是画面的小区域,过度放大会拖慢速度但不会增加有效信息。参考项目里如果没有这段保护逻辑,你自己补上即可。
5. 把识别率再往上顶一截:5 个必调参数与真实边界
5.1 置信度阈值:从 0.3 到 0.6 逐个试,不要永远用默认值
大部分参考项目的conf_thresh默认是 0.5,但真实场景下这个值需要根据相机位置调整。相机离车牌近,车牌在画面中大且清晰,阈值可以提高到 0.6 以上降低误检;相机角度偏低或画面模糊,阈值要降到 0.3~0.4 避免漏检。我一般用一段脚本在 20 张已知结果图上遍历不同阈值,画出准确率和召回率曲线,取曲线的拐点作为生产参数。
import cv2 import numpy as np evaluation_images = [...] # 20 张已标注图片的路径列表 best_conf, best_f1 = 0.0, 0.0 for conf in np.arange(0.3, 0.65, 0.05): tp = fp = fn = 0 for img_path in evaluation_images: # 执行你的识别管线 result = run_recognize_pipeline(img_path, conf_thresh=conf) gt = get_annotation(img_path) # 标注好的真实车牌 if result != "" and result == gt: tp += 1 elif result != "" and result != gt: fp += 1 elif result == "" and gt != "": fn += 1 precision = tp / max(tp + fp, 1) recall = tp / max(tp + fn, 1) f1 = 2 * precision * recall / max(precision + recall, 1e-6) if f1 > best_f1: best_f1, best_conf = f1, conf print(f"最优阈值: {conf:.2f}, F1: {f1:.3f}")这段就是最朴素的网格搜索:对每个阈值统计 TP、FP、FN,计算 F1 分数。为什么用 F1 而不是准确率?因为车牌识别对漏检和误检的代价不对等,漏检了要车主倒车重来,误检了至少要人工复核一遍,F1 能综合两者。如果参考项目里跑一次单张图要 200ms,20 图 7 个阈值就是 28 秒,完全可以接受。
5.2 NMS 的 IoU 阈值:多车牌场景的关键旋钮
参考项目默认 NMS 的iou_thresh=0.45,但如果画面里同时出现两辆车,检测模型输出了两个高度重叠的候选框,IoU 阈值太高会只保留一个框导致漏检另一张车牌。这里有个反直觉的点:不是阈值越高越好。IoU 阈值从 0.45 升到 0.7,会保留更多互相重叠的框,但如果两个框的重合度确实高到 0.7,那它们大概率是同一个车牌。
一个实际的经验值:抓拍单车道进出口用 0.45,多车道广场用 0.3——因为多车距离近,框与框之间天然有重叠,你要让模型容忍这种重叠。同时注意 NMS 前后的坐标映射,参考项目里如果对检测结果做了 padding 裁剪,NMS 处理的一定是原始检测框坐标,不要在裁剪后做 NMS。
5.3 识别模型的输入尺寸:48x168 是训练时定死的,不要乱改
LPRNet 类模型的输入高度固定为 24 或 48 的倍数,宽度可以是任意值但必须跟训练时一致。很多人为了“提速”把输入缩小到 96x48,结果车牌字符粘连,识别率直线下降。反过来加大到 240x48 也不会让精度提升,因为模型学到的特征尺度已经固定了。
正确的做法是:把模型输入尺寸恢复到训练时的设定值(参考项目源码里通常有常量定义),只在预处理阶段优化——例如保持高度 48,按检测框的真实宽高比动态生成宽度,而不是硬缩放到 168。动态宽度的实现需要在模型前向之前做一次resize,LPRNet 用全卷积结构可以接受这种尺寸变化,但 CRNN 会因为全连接层的固定维度直接报错。
5.4 字符字典顺序:动一个字符,识别率掉五个点
class_dict在参考项目里常以 json 或 txt 形式存储,它的顺序就是模型输出层每个神经元的索引映射。如果你从网上下载的模型权重配套的字典里“京”在第 3 位,而你的代码按字母排序把它放到了第 15 位,那么模型推理出的结果会被整体错位。这种错误非常隐蔽,因为识别流程完全不报错,只是输出乱码。
# 检查字符字典顺序是否和模型输出对齐 import json with open("class_dict.json", "r", encoding="utf-8") as f: class_dict = json.load(f) # 打印前 10 个字符和最后 10 个字符,肉眼核对 print("前 10:", class_dict[:10]) print("后 10:", class_dict[-10:]) # 常见正确字典的开头是各省简称,结尾是数字和字母 # 如果发现字典按 "0123456789..." 开头,输出错乱的可能性极大写这段代码的目的只是提醒你:拿到源码后,把损失函数输出层的维度打印出来,和class_dict的长度对比。如果长度一致但顺序不对,唯一正确的解法是把字典改成和模型训练时完全一致的文件,而不是重新训练模型——找到原始训练仓库,里面通常有labels.txt或class_indices.json。
5.5 图像增强的边界:直方图均衡不是万能的
车牌图像受光照影响大,参考项目里常有人用cv2.equalizeHist做直方图均衡来增强对比度,但对暗光条件下已经发白的车牌,均衡反而会把噪声放大,让字符边缘断裂。我测试过三种预处理工具的组合:灰度化 + CLAHE(限制对比度自适应直方图均衡) + 高斯模糊,在低照度场景下比直接均衡好很多。
import cv2 def preprocess_plate_crop(crop): """预处理车牌局部图:CLAHE + 轻度模糊 + 二值化""" gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) # CLAHE 参数:clipLimit 控制对比度限制,2.0 是经验值 clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) enhanced = clahe.apply(gray) # 注意这里不能用高斯模糊,会抹掉字符边缘,改用中值滤波 denoised = cv2.medianBlur(enhanced, 3) return denoised这段代码里的clipLimit=2.0是 CLAHE 的核心参数,太大局部对比度过强,太小则增强无效;tileGridSize=(8,8)把图像分成 64 个小块分别做直方图均衡,能保留局部细节。还有一个容易被忽略的点:中值滤波的核必须是奇数且不能大于 3,因为车牌字符笔画本身很细,核一大边缘就没了。这个预处理不保证每次都比原图好,所以在工期紧张时不妨先跑一版不加预处理的基线,再对比加预处理的版本,让数据说话而不是靠感觉。
6. 参考项目如何收进真实系统:封装成 HTTP 服务并做好速度与精度的权衡
6.1 用 FastAPI 把识别管线包成接口,让算法与业务解耦
参考项目跑通后,真正要用于现场时,通常需要把识别能力做成一个独立的服务,这样上层业务系统(Java、PHP 都行)通过 HTTP 调用,不需要关心 Python 环境的细节。这个步骤实际上是做服务封装,也是很多 MySQL 课程设计、PHP 项目中同样会用到的部署模式——把核心算法当成一个黑匣子,只暴露接口。
from fastapi import FastAPI, UploadFile, File from plate_recognition import PlateRecognizer import cv2 import numpy as np app = FastAPI() recognizer = PlateRecognizer(conf_thresh=0.45, use_cpu=True) @app.post("/recognize") async def recognize(file: UploadFile = File(...)): # 读取上传的图片文件并解码为 OpenCV 图像 content = await file.read() img_array = np.frombuffer(content, dtype=np.uint8) image = cv2.imdecode(img_array, cv2.IMREAD_COLOR) if image is None: return {"code": 1, "msg": "图片解码失败", "data": None} # 执行车牌识别 plates = recognizer.recognize(image) # 返回结果只保留可 JSON 序列化的字段 results = [] for plate in plates: results.append({ "bbox": plate["bbox"], # [x1, y1, x2, y2] "text": plate["text"], # 识别出的车牌字符串 "confidence": round(plate["confidence"], 4) }) return {"code": 0, "msg": "success", "data": results}FastAPI 的UploadFile是异步接口,但PlateRecognizer是同步阻塞的,这会导致并发请求时事件循环被卡住。要解决这个问题,用def而不是async def声明路由,FastAPI 会自动把同步函数丢到线程池里跑,这是一个非常隐蔽但重要的细节。cv2.imdecode的第二个参数IMREAD_COLOR会把图片强制转为三通道 BGR,如果前端上传的是 RGBA 图也不会有问题,imdecode会丢弃 alpha 通道。
6.2 提速三板斧:多线程推理、图像缩放下限控制、帧间复用
参考项目的识别耗时如果不达标,不要急着上 GPU,先尝试三个免费技巧。第一,OpenCV 的图像解码非常耗 CPU,如果同一路视频流里连续帧变化小,可以只对每三帧做一次识别,中间两帧用上一次的结果。第二,检测模型的输入尺寸从 640 降到 416,速度能提升约 40%,代价是远处小目标车牌检测率下降,这个取舍要看相机安装位置——近景抓拍 416 足够,远景监控必须 640。第三,把识别模型用 ONNX 替换 PyTorch,在纯 CPU 机器上通常能获得 1.5 到 2 倍的加速。
这里要泼一盆冷水:如果你在树莓派上跑深度学习车牌识别,不要指望流畅的视频流实时识别。树莓派 CPU 单帧推理 600ms 以上是正常的,哪怕上了 ONNX 也压不进 30ms 以内。周界安防项目里常见的做法是:树莓派做运动检测,有车辆进入时拍图,然后把图片发到服务端识别。参考项目如果演示时卡顿,不要急着优化代码,先考虑是不是选错了硬件平台。
6.3 精度的最后一公里:相机安装角度与补光
讲一个我在安防项目里从硬件侧获得的经验:算法调参优化空间有限时,改善相机的安装角度效果立竿见影。车牌识别相机的最佳俯仰角在 15 到 30 度之间,超过 45 度时车牌变形剧烈,检测模型就算框出来了,识别模型也很难读对。补光也有讲究,夜间要保证车牌区域照度在 200 lux 以上,不然再强的模型也扛不住噪声。
很多参考项目的 README 只讲软件怎么跑,不讲场景约束,这导致新手误以为模型是万能的。实际上,识别率是设备和算法协同的结果,你在测试现场摆放相机的角度、与车辆的距离、是否开启补光灯,这些变量对最终效果的影响可能比模型结构还要大。拿到源码测试时,第一步先建一个固定机位的测试集——同一位置拍 50 张不同车牌照片,跑出基准准确率,再调整相机,看准确率变化。这样的实验比盲目调参有意义得多。
6.4 关于资源方案的沉淀
这份参考项目解决的核心问题是“从零开始跑通识别链路”,它的落地路径已经清楚了:环境搭建 → 方案选型 → 参数调优 → 服务封装。如果源码里有训练集,那它还能延伸出数据标注和模型微调的方向;如果只有推理代码,那你就理解为一个可复用的推理框架。无论哪种,你都获得了一套能从图像到车牌的完整技术栈。这是我在做完不少路侧、园区项目后,觉得对后来者最值的那个入口。
我的习惯是:每次做完一个车牌识别方向的项目,都会把“踩坑记录”更新到自己的笔记里,特别是绿牌漏检和跨语言调用这两个坑,几乎每次换新项目都会再见一次。车牌的字体规范、颜色规范、字符集限制,这些领域知识是写在交通行业标准里的,不靠模型学,要靠工程人员主动补进去。希望你跟着这份思路跑通自己的参考项目时,少走点我当年的弯路,希望帮到你。
本文还有配套的精品资源,点击获取