ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Python和OpenCV的深度学习车牌识别系统:从图像预处理到视频流实战

基于Python和OpenCV的深度学习车牌识别系统:从图像预处理到视频流实战 简介这是一套面向高校学生的车牌识别毕业设计完整资料基于Python、OpenCV与深度学习技术实现适合计算机视觉入门者、毕业设计或课程设计选题人群使用。系统通过卷积神经网络完成车牌定位与字符识别流程涵盖灰度化、二值化、滤波去噪、字符分割等环节并配有详细代码注释便于新手理解算法原理与工程实现。资源包共18个文件包含5个py源码、5个png与3个jpg示例图片、2个dat模型文件、1个pptx演示文稿及README说明文档压缩包约4.73MB部署简单下载后按文档指引即可运行。目前已有86人学习关注。整套资料不仅提供可运行的源码与文档还完整呈现了从图像预处理到字符识别的技术链路读者可借此掌握OpenCV图像处理与深度学习模型的实际应用并在此基础上扩展至其他图像识别任务为后续研究或工作积累项目经验。1. 车牌识别毕设从调通一张图到跑通一条流水线很多计算机视觉方向的毕业设计卡住的地方从来不是模型本身而是从「一张图能识别」到「一段视频能稳定输出」之间的工程断层。基于 Python 和 OpenCV 的深度学习车牌识别系统恰好是这条断层上最典型的题目它同时踩中了图像预处理、目标检测、字符分割、序列识别四个环节任何一个环节参数没调好最终结果就是车牌框歪了、字符切碎了、识别置信度掉到 0.3 以下。我带过几届本科毕设也帮不少同学做过代码 review发现一个反直觉的结论大部分车牌识别项目失败不是深度学习模型不行而是 OpenCV 预处理阶段就把图像信息毁掉了。比如高斯模糊核设成 15×15边缘全糊了后面 Canny 检测出来的轮廓全是噪声再比如透视变换的四个角点顺序搞反矫正后的车牌直接镜像翻转CNN 再强也认不出来。这篇文章面向正在做这个题目的同学也面向想快速搭一套可演示系统的工程师把从环境配置、预处理、检测、分割到识别的完整链路拆开讲每一步给出可复现的代码和参数依据同时把那些「跑得通但结果不对」的坑提前标出来。你不需要先精通深度学习但需要愿意动手调参数、看中间结果图。2. 环境搭建与 OpenCV 图像预处理别让第一步就翻车2.1 Python 环境与 OpenCV 安装的版本选择车牌识别项目对 OpenCV 版本并不挑剔但有几个版本组合是经过大量项目验证比较稳的。我一般推荐 Python 3.8 到 3.10 之间搭配 OpenCV 4.5.x 或 4.6.x。Python 3.11 以上在部分 Windows 环境下安装 opencv-python 时会出现 wheel 不匹配的问题虽然能解决但对毕设来说没必要给自己找麻烦。安装命令很简单但要注意区分 opencv-python 和 opencv-contrib-python前者只包含主模块后者包含 SIFT、SURF 等扩展模块。车牌识别通常不需要 contrib 里的东西但如果你打算用 SIFT 做角点辅助定位那就装 contrib 版本。# 创建虚拟环境避免污染系统 Python python -m venv plate_env # Windows 激活 plate_env\Scripts\activate # Linux / macOS 激活 source plate_env/bin/activate # 安装核心依赖指定版本范围避免自动升级到不兼容版本 pip install opencv-python4.6.0.66 pip install numpy1.23.5 pip install torch1.13.1 torchvision0.14.1 pip install matplotlib3.7.1这里把 torch 也一起装了因为后面字符识别要用 CNN。版本锁定很重要我见过有同学用 pip install 不带版本号结果 numpy 升到 2.xOpenCV 直接报 ABI 不兼容报错信息是numpy.core.multiarray failed to import查半天查不出来。如果你已经装了高版本 numpy先卸载再装指定版本。提示安装完成后用python -c import cv2; print(cv2.__version__)验证能打印出版本号才算成功。如果报ModuleNotFoundError: No module named opencv大概率是虚拟环境没激活或者 pip 装到了系统 Python 而不是虚拟环境里。2.2 车牌定位前的图像预处理灰度、滤波、边缘检测的参数逻辑车牌识别的第一步是从整张图里找到车牌区域。传统做法是用颜色特征加边缘检测深度学习做法是用 YOLO 或 SSD 直接回归车牌框。毕设里两种都常见我建议先用传统方法跑通再用深度学习替换检测模块这样你能理解每一步在做什么。预处理的核心流程是灰度化 → 高斯滤波 → Sobel 或 Canny 边缘检测 → 形态学闭运算 → 轮廓筛选。每一步的参数都有物理含义不是随便填的。import cv2 import numpy as np def preprocess_plate(image_path): # 读取图像注意 OpenCV 默认是 BGR 通道 img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图像: {image_path}) # 转灰度减少计算量同时保留亮度信息 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 高斯滤波核大小必须是奇数3x3 或 5x5 适合车牌这种中等纹理 # 核太大如 15x15会把车牌字符边缘也模糊掉后面边缘检测就废了 blurred cv2.GaussianBlur(gray, (5, 5), 0) # Sobel 水平方向差分车牌字符在水平方向有强烈边缘 # 这里用 Scharr 核替代默认 Sobel 核因为 Scharr 对小边缘响应更好 sobel_x cv2.Sobel(blurred, cv2.CV_16S, 1, 0, ksize-1) abs_x cv2.convertScaleAbs(sobel_x) # 二值化阈值 120 是经验值光照均匀时可用 Otsu 自动阈值 _, binary cv2.threshold(abs_x, 120, 255, cv2.THRESH_BINARY) # 形态学闭运算先膨胀后腐蚀把断裂的字符边缘连成块 # 核用 17x5 是因为车牌是扁长形状水平方向需要更大的连接力度 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return img, gray, closed # 调用并保存中间结果方便调试 img, gray, closed preprocess_plate(car.jpg) cv2.imwrite(debug_gray.jpg, gray) cv2.imwrite(debug_closed.jpg, closed)这段代码里最关键的参数是高斯核大小和形态学核形状。高斯核 (5,5) 是平衡去噪和保留边缘的常用值如果你处理的是夜间低照度图像可以先用直方图均衡化再滤波。形态学核 (17,5) 里的 17 是水平方向5 是垂直方向这个比例来自中国蓝牌车牌的宽高比大约 440:140约等于 3:1所以水平核要比垂直核长。如果你用 (5,17) 就反了会把垂直方向的噪声连成片轮廓筛选时全是误检。注意调试阶段一定要把中间结果图保存下来看。很多同学直接跑完整流程结果不对就改模型其实问题出在二值化那一步——阈值设成 200车牌区域全黑了后面再怎么做都没用。2.3 轮廓筛选与透视矫正从找到车牌到摆正车牌形态学闭运算之后图像里会有很多白色块车牌只是其中之一。筛选轮廓的依据通常是宽高比、面积、矩形度。中国蓝牌和新能源绿牌的宽高比在 2.5 到 4.5 之间面积根据图像分辨率不同而变化但一般不会小于整图面积的 1%。def find_plate_contour(closed_img, original_img): # 找外轮廓RETR_EXTERNAL 只取最外层避免嵌套轮廓干扰 contours, _ cv2.findContours(closed_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] img_area original_img.shape[0] * original_img.shape[1] for cnt in contours: # 计算最小外接矩形带旋转角度 rect cv2.minAreaRect(cnt) (cx, cy), (w, h), angle rect # 统一宽高让 w 始终是长边 if w h: w, h h, w angle 90 aspect_ratio w / h if h 0 else 0 area_ratio (w * h) / img_area # 宽高比 2.5~4.5面积占比 0.5%~15%这两个范围覆盖绝大多数车牌 if 2.5 aspect_ratio 4.5 and 0.005 area_ratio 0.15: candidates.append((cnt, rect, area_ratio)) # 按面积从大到小排序取最大的作为车牌 candidates.sort(keylambda x: x[2], reverseTrue) if not candidates: return None # 获取最小外接矩形的四个角点 box cv2.boxPoints(candidates[0][1]) box np.int0(box) return box def perspective_correct(img, box): # 对四个角点排序左上、右上、右下、左下 # 排序逻辑先按 x 坐标分左右再按 y 坐标分上下 rect np.zeros((4, 2), dtypenp.float32) s box.sum(axis1) rect[0] box[np.argmin(s)] # 左上角 xy 最小 rect[2] box[np.argmax(s)] # 右下角 xy 最大 diff np.diff(box, axis1) rect[1] box[np.argmin(diff)] # 右上角 x-y 最小 rect[3] box[np.argmax(diff)] # 左下角 x-y 最大 # 目标尺寸中国车牌标准 440x140这里按比例缩放到 240x80 减少计算量 dst np.array([[0, 0], [240, 0], [240, 80], [0, 80]], dtypenp.float32) # 计算透视变换矩阵并应用 M cv2.getPerspectiveTransform(rect, dst) warped cv2.warpPerspective(img, M, (240, 80)) return warped角点排序是透视矫正里最容易出错的地方。如果你不排序直接把 boxPoints 返回的四个点传给 getPerspectiveTransform变换后的图像可能是旋转 90 度甚至镜像的。上面用的 sum 和 diff 排序法是最稳定的适用于大多数凸四边形。矫正后的尺寸我设成 240×80这是 440×140 的近似比例同时把宽度控制在 240 像素后面 CNN 输入不用再大幅缩放。3. 深度学习检测与字符分割YOLO 微调还是传统分割3.1 用 YOLOv5 做车牌检测的微调流程传统边缘检测在光照均匀、背景简单的场景下够用但毕设答辩时老师往往会拿一张复杂背景的图来试这时候传统方法就容易翻车。用 YOLOv5 做车牌检测是当前比较成熟的做法模型小、推理快、微调数据量要求不高。微调流程分四步准备数据集、配置 YOLOv5、训练、导出推理。数据集方面你需要收集至少 500 张包含车牌的图片用 labelImg 标注成 YOLO 格式每张图对应一个 txt 文件内容格式是类别 x_center y_center width height坐标都归一化到 0 到 1 之间。# 克隆 YOLOv5 仓库注意这里只写通用做法不指定具体 commit git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt # 准备数据配置文件 plate_data.yaml # 内容如下 # train: ./data/train/images # val: ./data/val/images # nc: 1 # names: [plate] # 开始训练batch size 根据显存调整8G 显存用 16 python train.py --img 640 --batch 16 --epochs 100 --data plate_data.yaml --weights yolov5s.pt训练完成后用detect.py做推理或者用 torch.hub 加载模型。YOLOv5s 在 500 张图微调 100 轮后车牌检测的 mAP 通常能到 0.9 以上前提是标注质量过关。如果 mAP 卡在 0.5 上不去先检查标注框有没有把车牌完整框住很多同学标的时候只框了字符区域没框蓝色底牌模型学到的特征就不对。提示YOLOv5 训练时如果报CUDA out of memory先把 batch size 降到 8 或 4或者把 img 尺寸从 640 降到 416。毕设演示不要求实时 30 帧416 尺寸推理速度更快精度损失在可接受范围内。3.2 字符分割投影法、连通域法和 CTC 的取舍检测到车牌并矫正后下一步是把车牌里的字符一个个切出来。常见做法有三种垂直投影法、连通域法、CTC 序列识别。毕设里我建议用垂直投影法因为它逻辑直观、代码量少、可解释性强答辩时老师问起来你能说清楚每一步在干什么。垂直投影法的原理是对二值化后的车牌图像统计每一列白色像素的个数字符所在列的投影值高字符之间的间隙投影值低找到波谷就找到了分割点。def segment_characters(plate_img): # 转灰度并二值化车牌字符是白色背景是深色 gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 自适应阈值应对光照不均blockSize 必须是奇数 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) # 垂直投影对每一列求和 col_sum np.sum(binary, axis0) / 255 # 找波谷投影值低于阈值的列认为是字符间隙 threshold np.max(col_sum) * 0.15 gaps col_sum threshold # 根据波谷切分字符 characters [] start None for i, is_gap in enumerate(gaps): if not is_gap and start is None: start i elif is_gap and start is not None: if i - start 5: # 忽略太窄的噪声区域 char_img binary[:, start:i] # 统一缩放到 20x40作为 CNN 输入 char_resized cv2.resize(char_img, (20, 40)) characters.append(char_resized) start None return characters投影法的坑在于车牌上的铆钉、边框、污渍都会在投影图上产生假波峰或假波谷。解决办法是在投影前先做水平投影把车牌上下边框裁掉只保留字符区域。另外字符「1」的投影宽度很窄容易被当成噪声过滤掉所以i - start 5这个阈值不能设太大5 到 8 之间比较合适。如果字符分割总是切不准可以考虑用 CTCConnectionist Temporal Classification做端到端识别跳过分割步骤。但 CTC 需要更多训练数据和更复杂的解码逻辑毕设时间紧的话不建议一开始就上 CTC先把投影法调通实在不行再换。3.3 CNN 字符识别模型的结构与训练参数字符识别用一个小型 CNN 就够了不需要 ResNet 或 VGG 这种大模型。车牌字符类别数是 65 左右数字 10 字母 24 省份简称 31输入 20×40 的灰度图三层卷积加两层全连接就能到 95% 以上的准确率。import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_classes65): super(PlateCNN, self).__init__() # 输入 1x40x20灰度图高 40宽 20 self.conv1 nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2) # 输出 32x20x10 ) self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2) # 输出 64x10x5 ) self.conv3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.MaxPool2d(2) # 输出 128x5x2 ) self.fc nn.Sequential( nn.Flatten(), nn.Linear(128 * 5 * 2, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x self.fc(x) return x # 训练参数 model PlateCNN(num_classes65) criterion nn.CrossEntropyLoss() # 学习率 1e-3 配合 Adam 是字符识别的常用起点 optimizer torch.optim.Adam(model.parameters(), lr1e-3) # 每 20 轮衰减一次学习率防止后期震荡 scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5)训练数据方面每个字符类别至少需要 200 张样本总共 65 类就是 13000 张字符图。这些图可以从标注好的车牌里用投影法切出来也可以直接用公开的字符数据集。训练时 batch size 设 64跑 50 到 80 轮验证集准确率能到 97% 以上。如果准确率卡在 90% 左右上不去检查一下省份简称的样本是不是太少比如「藏」「青」这些车牌本来就少模型见得不多了自然认不准。4. 避坑与排查那些让毕设进度卡三天的典型问题4.1 现象OpenCV 读图返回 None后面全部报错原因图片路径包含中文或空格OpenCV 的 imread 在 Windows 下对中文路径支持不好。另外如果路径写的是相对路径但脚本工作目录和图片目录不一致也会读不到。解决用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)替代cv2.imread这样能正确处理中文路径。或者把图片路径改成全英文并且用绝对路径。4.2 现象YOLOv5 训练 loss 不下降mAP 一直是 0原因标注文件格式不对。YOLO 要求坐标归一化到 0 到 1很多同学标的时候直接填了像素坐标比如120 340 200 80模型根本学不到东西。另外类别索引要从 0 开始如果只有一类车牌类别号必须是 0不能写 1。解决用 labelImg 标注时选择 YOLO 格式它会自动归一化。标完后随机抽几张图用cv2.rectangle把标注框画出来肉眼确认框的位置和大小对不对。4.3 现象字符分割把「1」和「I」切没了或者把「京」切成两半原因投影阈值设得太高窄字符的投影值低于阈值被当成间隙。或者车牌有倾斜垂直投影时字符列不对齐波谷位置偏移。解决先做透视矫正确保车牌水平再把投影阈值从 0.15 降到 0.08 试试。对于「京」这种左右结构的字投影图中间会有波谷需要在分割后加一步合并逻辑如果相邻两个字符块的宽度都小于 15 像素且间距小于 3 像素就合并成一个字符。4.4 现象模型在训练集上准确率 99%测试集只有 60%原因过拟合。毕设数据集通常只有几百张图模型参数量相对数据量偏大训练轮数多了就会记住训练样本。解决加数据增强对车牌图做随机旋转-5 到 5 度、随机亮度调整0.8 到 1.2 倍、随机高斯噪声。另外把 Dropout 从 0.5 提到 0.6或者加 L2 正则化权重衰减设 1e-4。4.5 现象推理时 GPU 显存够但速度很慢单张图要 2 秒原因没有用torch.no_grad()上下文模型在推理时还在计算梯度。另外如果每次推理都重新加载模型加载时间会占大头。解决推理代码包在with torch.no_grad():里模型加载一次后缓存起来。如果还是慢把模型转成 ONNX 或 TensorRTYOLOv5s 转 ONNX 后推理速度能提升 2 到 3 倍。5. 从单张图到视频流把毕设做成能演示的完整系统5.1 用 OpenCV VideoCapture 接入摄像头或视频文件单张图跑通后下一步是接入视频流。OpenCV 的 VideoCapture 既能读视频文件也能接摄像头接口统一切换只需要改一个参数。import cv2 import torch from plate_detector import detect_plate # 假设你已经封装了检测函数 from plate_recognizer import recognize_chars # 加载模型只加载一次 device torch.device(cuda if torch.cuda.is_available() else cpu) model PlateCNN(num_classes65).to(device) model.load_state_dict(torch.load(plate_cnn.pth, map_locationdevice)) model.eval() # 视频源0 表示默认摄像头也可以传视频文件路径 cap cv2.VideoCapture(0) # 设置分辨率太高会影响帧率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) frame_count 0 while True: ret, frame cap.read() if not ret: break frame_count 1 # 每 3 帧处理一次减少计算量显示仍然流畅 if frame_count % 3 ! 0: cv2.imshow(Plate Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break continue # 检测车牌 plate_img, box detect_plate(frame) if plate_img is not None: # 识别字符 text recognize_chars(plate_img, model, device) # 画框和文字 cv2.drawContours(frame, [box], -1, (0, 255, 0), 2) cv2.putText(frame, text, (box[0][0], box[0][1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Plate Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里有两个工程细节值得注意。第一每 3 帧处理一次而不是每帧都处理因为车牌识别模型推理需要时间如果每帧都跑视频会卡成幻灯片。跳帧处理后显示帧率能保持在 20 帧以上肉眼看起来是流畅的。第二cv2.waitKey(1)里的参数是 1 毫秒不能设太大否则视频会一顿一顿的。5.2 多帧投票用时间换准确率视频流相比单张图有一个天然优势同一个车牌会在连续多帧里出现。利用这个特性可以对多帧的识别结果做投票把置信度低的错误结果过滤掉。具体做法是维护一个字典key 是识别出的车牌号value 是出现次数。每处理一帧如果识别结果置信度大于 0.8就把对应 key 的计数加一。当某个车牌号的计数超过 5 次就认为它是稳定结果显示在画面上。如果连续 30 帧没有新的稳定结果就清空字典重新开始。from collections import defaultdict plate_votes defaultdict(int) stable_plate None frames_without_new 0 def update_votes(text, confidence): global stable_plate, frames_without_new if confidence 0.8: plate_votes[text] 1 frames_without_new 0 # 找到票数最高的车牌 best max(plate_votes.items(), keylambda x: x[1]) if best[1] 5: stable_plate best[0] else: frames_without_new 1 if frames_without_new 30: plate_votes.clear() stable_plate None return stable_plate这个投票机制能把单帧识别率从 90% 提升到 98% 以上代价是响应延迟增加了几帧。对于毕设演示来说这个延迟完全可接受而且答辩时你可以把这个设计作为一个亮点讲用时间冗余换空间准确率。5.3 系统集成与答辩演示的检查清单把检测、分割、识别、投票串起来后整个系统就成型了。答辩前按下面这个清单过一遍能避免大部分现场翻车。检查项验证方法常见问题模型文件路径用绝对路径或os.path.dirname(__file__)拼接换电脑后相对路径失效GPU 可用性torch.cuda.is_available()打印确认答辩电脑没装 CUDA代码要能自动回退 CPU摄像头权限提前在答辩电脑上跑一次 VideoCapture(0)有些教室电脑摄像头被禁用视频文件备份准备一段车牌视频作为备选输入现场摄像头拍不清车牌中间结果图保存预处理、分割的中间图老师问细节时能展示推理速度用time.time()测单帧耗时超过 500ms 要跳帧或降分辨率我自己的习惯是答辩前一天把整个系统在一台没配过环境的电脑上从零跑一遍。这一步能暴露 90% 的路径问题和依赖缺失问题。另外代码里所有硬编码的路径都改成命令行参数或配置文件这样换数据集不用改代码。5.4 一个容易被忽略的细节车牌颜色判断中国车牌有蓝牌、黄牌、绿牌新能源三种字符识别模型如果只训练了蓝牌数据遇到绿牌就会翻车。解决办法是在检测到车牌后先判断颜色蓝色底、黄色底、绿色底分别对应不同的字符集和预处理参数。绿牌是 8 位字符蓝牌是 7 位这个位数差异可以在后处理阶段用来校验识别结果。def judge_plate_color(plate_img): # 转 HSV 空间颜色判断更稳定 hsv cv2.cvtColor(plate_img, cv2.COLOR_BGR2HSV) # 计算色调直方图 hist cv2.calcHist([hsv], [0], None, [180], [0, 180]) # 找到主色调 dominant_hue np.argmax(hist) if 100 dominant_hue 130: return blue elif 20 dominant_hue 35: return yellow elif 40 dominant_hue 80: return green else: return unknown这个函数返回颜色标签后识别阶段就可以根据颜色选择对应的字符映射表。绿牌的第二位是字母 D 或 F蓝牌没有这个限制这些先验知识加进去准确率还能再提一两个点。做毕设最深的体会是不要等到全部写完再调试每写一个函数就用一张图验证一次。我见过太多同学把检测、分割、识别全写完才跑结果报错都不知道是哪一步的问题。把中间结果可视化把参数调优的过程记录下来这些不仅是调试手段也是答辩时展示工作量最好的材料。希望帮到你。本文还有配套的精品资源点击获取
返回列表