简介:这是一套面向计算机视觉初学者与课程设计者的车牌检测识别完整项目源码,基于Python实现,覆盖图像预处理、车牌定位、字符分割与字符识别四大核心环节,适合用于毕业设计、课程作业或算法练手。压缩包共48个文件,约22.11MB,包含7个py脚本承载检测与SVM训练逻辑,17个png与8个jpg提供测试图片和界面素材,另有dat模型数据、ui界面文件、ico图标及mp4演示视频等,结构完整可直接运行。项目整合OpenCV图像处理、NumPy数据操作与深度学习字符识别思路,并涉及灰度化、直方图均衡、边缘检测、连通组件分析等具体技术点,还附带训练数据与配置文件,便于读者理解从定位到识别的全流程。目前已有150人学习下载,适合希望快速上手车牌识别实战、对照代码梳理算法脉络的开发者参考。
1. 从一张违停抓拍图说起:Python 车牌检测和识别到底在做什么
路口相机拍下一张车尾图,人工要做的第一件事是找到那块蓝底白字的矩形,第二件事是把它上面的字符读出来。基于 Python 的车牌检测和识别系统,本质就是把这两件事串成一条自动流水线:检测负责回答“车牌在哪”,识别负责回答“车牌是多少”。它适合三类人:想给停车场、门禁、称重站做一套本地化识别的开发者;手里有几千张过车图、想批量结构化入库的数据同学;以及刚学完 Python 基础、想找一个能跑通全流程项目的入门者。这条流水线不依赖云端接口,一台带普通显卡的机器就能跑,难点不在模型多大,而在预处理、后处理和参数配合。下面按“先跑通、再调优、最后避坑”的顺序拆开讲。
2. 检测与识别两条链路怎么选:YOLO 定位加 CRNN 读字符
2.1 为什么把检测和识别拆成两个模型
车牌场景有一个很现实的特点:检测框里的内容高度结构化,永远是“省份汉字 + 字母 + 数字”的固定组合,长度有限、字体统一。如果用一个端到端模型直接输出字符串,训练数据要求高,出错后也难定位是框歪了还是字认错了。拆成两段之后,检测模型只关心“车牌矩形在哪”,识别模型只关心“框里的字符序列是什么”,两段可以分别换、分别调。
常见做法是检测用 YOLO 系列,识别用 CRNN + CTC。YOLO 负责回归边界框,速度快、对小目标友好;CRNN 用 CNN 提特征、RNN 建模字符顺序、CTC 解决不定长对齐,正好匹配车牌字符数不固定的情况。两段之间用透视变换把倾斜车牌摆正,这一步做不好,后面识别再强也白搭。
选型时还要考虑部署环境。如果只有 CPU,检测可以换轻量骨干,识别把 RNN 换成全卷积结构;如果有 NVIDIA 显卡,直接上常规版本即可。我一般会先确认推理设备,再决定模型规模,而不是先挑最大的模型。
2.2 用 OpenCV 做车牌定位的最小可跑脚本
在训练自己的模型之前,先用传统方法跑一遍定位,能帮你快速理解车牌的颜色和边缘特征。下面这段代码用 HSV 颜色分割加轮廓筛选,把蓝色车牌区域框出来。
import cv2 import numpy as np def locate_plate(image_path): img = cv2.imread(image_path) # 转 HSV,蓝色车牌在 H 通道约 100-124 hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) lower = np.array([100, 80, 80]) upper = np.array([124, 255, 255]) mask = cv2.inRange(hsv, lower, upper) # 形态学闭运算,把字符间的空隙连成整块 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) boxes = [] for c in contours: x, y, w, h = cv2.boundingRect(c) ratio = w / float(h) # 车牌宽高比大致在 2.5 到 5.5 之间 if 2.5 < ratio < 5.5 and w > 80: boxes.append((x, y, w, h)) return img, boxes if __name__ == "__main__": img, boxes = locate_plate("car.jpg") for (x, y, w, h) in boxes: cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imwrite("located.jpg", img)这段代码的逻辑是:颜色阈值先把蓝色区域挑出来,闭运算把分散的字符连成一块,再用宽高比和最小宽度过滤掉误检。参数里lower和upper的 H 范围决定颜色宽容度,光照偏黄时可以把 S 下限降到 60;kernel的尺寸决定闭运算强度,字符间距大就加宽。跑不通时先看mask图,如果车牌区域是断的,说明闭运算不够;如果整张图全白,说明阈值太松。
提示:传统方法只适合固定颜色和光照的场景,夜间、逆光、脏污车牌会大量漏检,它更适合用来生成候选框,再交给模型精修。
2.3 训练检测模型时的数据标注与增强参数
自己训练 YOLO 检测器,标注格式用 YOLO 的 txt:每行类别 x_center y_center width height,坐标都归一化到 0 到 1。车牌只有一个类别,所以类别号恒为 0。标注时框要贴紧车牌边缘,留白太多会让识别阶段的透视变换把背景也带进去。
数据增强是提升泛化的关键。常用组合是随机亮度对比度、随机透视、随机缩放加平移。透视变换的幅度不要太大,否则车牌被压成梯形后,检测框会包含大量非车牌区域。我一般把透视幅度控制在 0.05 以内,缩放范围 0.8 到 1.2。
训练参数上,输入尺寸建议 640,batch 根据显存调,学习率用余弦退火从 0.01 降到 0.0001。如果数据集里小目标多,可以把输入提到 960,但推理速度会明显下降。验证时重点看召回率,漏检比误检更致命,因为漏掉的车牌后面没有任何补救机会。
3. 把车牌读出来:CRNN 识别、字符集定义与后处理
3.1 字符集怎么定,直接决定识别上限
识别模型的输出空间由字符集决定。中国车牌常见字符包括省份汉字、字母 A-Z(去掉 I 和 O)、数字 0-9,再加上新能源车牌多一位。字符集一旦定死,模型就永远认不出集合外的字符。所以第一步是把业务里可能出现的车牌类型列全:蓝牌、黄牌、绿牌、白牌。如果只做蓝牌,字符集可以精简到 31 个汉字加 34 个字母数字;如果要做新能源,得把绿牌多出来的字母位也加进去。
字符集建议单独存成一个 txt,每行一个字符,训练和推理都读同一个文件。这样换字符集时不用改代码,只改文件。索引 0 通常留给 CTC 的 blank,实际字符从 1 开始编号。
3.2 CRNN 推理代码与 CTC 解码
下面这段是识别阶段的推理骨架,输入是已经摆正的车牌小图,输出是字符串。
import torch import cv2 import numpy as np def decode_ctc(logits, charset): # logits: (T, C),C 含 blank indices = logits.argmax(axis=1) result = [] prev = -1 for idx in indices: if idx != prev and idx != 0: result.append(charset[idx - 1]) prev = idx return "".join(result) def recognize(plate_img, model, charset, img_h=32, img_w=100): img = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) img = cv2.resize(img, (img_w, img_h)) img = img.astype(np.float32) / 255.0 img = (img - 0.5) / 0.5 tensor = torch.from_numpy(img).unsqueeze(0).unsqueeze(0) with torch.no_grad(): logits = model(tensor) # (1, T, C) logits = logits.squeeze(0).cpu().numpy() return decode_ctc(logits, charset)解码逻辑是 CTC 的标准做法:先取每个时间步的最大概率索引,然后合并连续重复,再去掉 blank。img_h和img_w必须和训练时一致,改了尺寸模型就认不准。charset的顺序必须和训练时完全一致,差一位就会把“京”认成“津”。如果输出全是空或重复单字,先检查输入归一化是否和训练一致,再检查字符集有没有错位。
3.3 透视变换摆正车牌的三个参数
检测框给的是倾斜矩形,识别前要把它摆正。用cv2.getPerspectiveTransform加cv2.warpPerspective,关键是四个角点的顺序。常见做法是先对检测框内的车牌区域做边缘检测,找最小外接矩形,再按左上、右上、右下、左下排序。
def align_plate(img, box): x, y, w, h = box roi = img[y:y+h, x:x+w] gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return cv2.resize(roi, (100, 32)) rect = cv2.minAreaRect(max(contours, key=cv2.contourArea)) box_pts = cv2.boxPoints(rect) # 按 x+y 和 x-y 排序,得到左上、右上、右下、左下 s = box_pts.sum(axis=1) d = np.diff(box_pts, axis=1).ravel() ordered = np.array([box_pts[np.argmin(s)], box_pts[np.argmin(d)], box_pts[np.argmax(s)], box_pts[np.argmax(d)]], dtype=np.float32) dst = np.array([[0, 0], [99, 0], [99, 31], [0, 31]], dtype=np.float32) M = cv2.getPerspectiveTransform(ordered, dst) return cv2.warpPerspective(roi, M, (100, 32))三个关键参数:阈值方法用 OTSU 自动找分割点,比固定阈值稳;minAreaRect拿到的角度决定旋转量;目标尺寸(100, 32)必须和识别模型输入一致。如果摆正后字符还是斜的,多半是角点排序错了,检查argmin和argmax对应的点是否真的在左上和右下。
4. 避坑与排查:车牌识别翻车的五个真实原因
4.1 检测框贴太紧导致字符被切
现象:识别结果总是少一位,比如“京A12345”读成“京A1234”。原因:检测框比车牌实际区域小,边缘字符被裁掉。解决:训练标注时框往外扩 2 到 3 个像素,推理时把检测框按比例放大 1.05 倍再裁剪。这个改动很小,但能救回大量边缘样本。
4.2 字符集顺序错位导致系统性认错
现象:所有车牌的第一个汉字都错,且错得很有规律。原因:训练时字符集按拼音排序,推理时按笔画排序,索引对不上。解决:字符集文件只保留一份,训练和推理都从同一个路径读取,改完必须重新导出模型。血泪经验是,这种错不会报异常,只会安静地输出错误结果。
4.3 输入归一化不一致导致置信度整体偏低
现象:模型能出字,但置信度普遍低于 0.5,偶尔跳字。原因:训练时用了(x-0.5)/0.5,推理时只做了/255。解决:把预处理写成一个函数,训练和推理共用,不要在两处各写一遍。归一化参数是黑匣子里最容易忽略的一环。
4.4 夜间红外图颜色失真导致颜色定位失效
现象:白天正常,夜间大量漏检。原因:红外补光下蓝色车牌在 HSV 里偏灰,颜色阈值分割直接失效。解决:夜间链路不要依赖颜色,改用检测模型直接回归,或者把红外图转成灰度后做边缘检测。如果必须用颜色,把 S 下限降到 30 并配合亮度判断。
4.5 批量推理时显存溢出
现象:单张图正常,一次传 32 张就报显存不足。原因:识别模型输入尺寸固定,但检测阶段不同图片产生的车牌数量不同,拼 batch 时没有做动态填充。解决:按车牌数量分组,每组内填充到同一宽度,或者把 batch 降到 8。后悔药是提前在推理脚本里加显存监控,别等上线才发现。
5. 把单张推理变成可用的批量流水线:三个提速技巧
5.1 检测和识别解耦成两个进程
单进程串行跑,检测完一张再识别一张,GPU 利用率很低。常见做法是把检测和识别拆成两个进程,中间用一个队列传车牌小图。检测进程只管出框,识别进程只管读字,两边各自 batch。这样检测的 batch 和识别的 batch 互不干扰,整体吞吐能提升接近一倍。队列长度设 64 左右,太长会吃内存,太短会空转。
5.2 用 ONNX Runtime 做 CPU 端加速
没有显卡的机器上,PyTorch 直接推理偏慢。把检测和识别模型都导出成 ONNX,用 ONNX Runtime 的 CPU 执行提供者跑,开启图优化和线程数设置。
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("plate_rec.onnx", providers=["CPUExecutionProvider"]) sess.set_providers(["CPUExecutionProvider"], [{"intra_op_num_threads": 4}]) def infer_onnx(img_tensor): input_name = sess.get_inputs()[0].name out = sess.run(None, {input_name: img_tensor}) return out[0]intra_op_num_threads设成物理核心数,别超过,超了反而抢线程。导出 ONNX 时注意 opset 版本,CRNN 里的 LSTM 在低版本 opset 上可能不支持,建议用 12 以上。如果导出后结果和 PyTorch 对不上,先检查输入名字和维度顺序。
5.3 用置信度阈值做二次过滤
识别模型对模糊车牌会输出低置信度结果。把每个字符的概率取平均,低于 0.6 的整条结果标记为“待人工复核”,不要直接入库。这个阈值不是拍脑袋定的,拿一批标注好的验证集跑一遍,画准确率随阈值变化的曲线,选准确率开始明显下降的那个点。我一般会留 5% 左右的复核量,既能挡住大部分错读,又不会让复核队列爆掉。
5.4 一个我常用来验证全链路的小习惯
每次改完检测或识别的任何参数,我都会固定拿 20 张图跑一遍全链路,记录检测召回、识别准确率和单张耗时三个数。这三个数放在一起看,才能判断改动是赚了还是亏了。只看识别准确率涨了,可能检测召回掉了,整体反而更差。这个习惯帮我省下过好几次“改完以为变好、上线才发现变差”的返工。希望帮到你。
本文还有配套的精品资源,点击获取