尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python车牌识别实战:OpenCV定位与CNN字符识别全流程

Python车牌识别实战:OpenCV定位与CNN字符识别全流程 简介这份资源面向计算机相关专业的本科生提供一套可直接用于毕业设计、期末大作业或课程设计的车牌识别与管理系统完整方案技术栈覆盖OpenCV图像处理与深度学习字符识别适合具备一定Python基础、希望快速完成高分项目的学生。压缩包共2000个文件约33.47MB其中1987个jpg为车牌与字符样本图像另有xml标注文件、py源码、sh脚本、pptx汇报材料、docx项目报告及md说明文档覆盖数据、代码与文档全流程。资源已有371人学习下载代码含详细注释新手也能看懂。内容按数据收集与预处理、基于Canny边缘检测与形态学操作的车牌定位、垂直投影字符分割、CNN字符识别等模块组织并附项目工作总结报告与演示文稿下载后简单部署即可运行便于理解车牌识别系统的完整实现思路与工程结构。1. 从一张模糊车位照说起Python 车牌识别系统到底在做什么地下车库入口摄像头拍到的车牌往往只有 100 来像素宽还带着运动模糊和顶灯反光。用纯 OpenCV 的模板匹配去认白天勉强能出结果一到晚上或者车牌倾斜就集体翻车。这正是「Python 基于 OpenCV 和深度学习的车牌识别与管理系统」要解决的核心问题把传统图像处理负责的「定位」和深度学习负责的「认字」拆开各干各擅长的事。这套方案适合三类人正在做 Python 毕业设计、需要一套能跑通又有技术含量的完整项目想入门 OpenCV 图像处理和深度学习 CNN 的开发者以及需要给停车场、小区门禁做一套轻量车牌管理后台的工程师。它不追求工业级识别率但胜在结构清晰、依赖少、能在一台普通笔记本上从零复现。整条链路是图像预处理 → 车牌定位 → 字符分割 → CNN 字符识别 → 数据入库与后台管理。下面按这条链路逐段拆开讲。2. 车牌定位用 OpenCV 把车牌从整张图里抠出来车牌识别的第一步不是识别是定位。整张图里车牌可能只占 5% 的面积直接丢给 CNN 既慢又容易被背景干扰。传统做法是用颜色和边缘特征先把候选区域框出来再交给后续模块。这一步用 OpenCV 就够了不需要深度学习。2.1 颜色分割与形态学定位的第一道筛子国内蓝牌的主色调是蓝底白字HSV 空间下蓝色有比较集中的分布区间。先把 BGR 转成 HSV用inRange卡出蓝色区域再做闭运算把字符间的缝隙填上车牌就会变成一个连通的白色块。import cv2 import numpy as np def locate_plate_by_color(image_path): img cv2.imread(image_path) # 高斯模糊压掉噪点核大小 5x5 是经验值太大车牌边缘会糊 blurred cv2.GaussianBlur(img, (5, 5), 0) hsv cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) # 蓝色车牌 HSV 区间H 在 100~124 之间覆盖绝大多数蓝牌 lower_blue np.array([100, 80, 80]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算先膨胀再腐蚀把车牌内部字符的孔洞填平 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (17, 5)) closed cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 找外轮廓按面积和长宽比筛掉明显不是车牌的块 contours, _ cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) ratio w / float(h) area w * h # 蓝牌长宽比约 3.14放宽到 2.2~4.5面积下限 2000 像素 if 2.2 ratio 4.5 and area 2000: candidates.append((x, y, w, h)) return img, candidates这段代码的逻辑是「先粗筛再精筛」。HSV 区间负责粗筛把蓝色像素全捞出来形态学闭运算负责把离散的蓝色像素连成块轮廓筛选负责精筛用长宽比和面积两个几何特征排除掉蓝色车身、广告牌这类干扰。参数上最需要调的是lower_blue和upper_blue的 H 通道范围不同摄像头白平衡不一样H 的上下限可能要各挪 5 到 10。kernel的尺寸也有讲究宽度 17 是为了横向连接字符高度 5 是为了不把上下边缘连到别的区域这个比例是根据蓝牌字符间距反推的。2.2 边缘检测兜底颜色失效时怎么办颜色分割有个硬伤黄牌、绿牌、白牌它都认不出来夜间偏色时蓝牌也可能漏检。这时候用 Sobel 做垂直边缘检测兜底因为车牌区域字符密集垂直边缘数量远高于普通车身区域。def locate_plate_by_edge(image_path): img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Sobel 只取 x 方向车牌字符的竖直笔画会形成强响应 sobel_x cv2.Sobel(gray, cv2.CV_16S, 1, 0, ksize3) abs_x cv2.convertScaleAbs(sobel_x) # 二值化阈值 120低于它的边缘直接丢掉 _, binary cv2.threshold(abs_x, 120, 255, cv2.THRESH_BINARY) # 横向闭运算把字符边缘连成矩形区域 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) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) if 2.2 w / float(h) 4.5 and w * h 2000: candidates.append((x, y, w, h)) return img, candidates边缘法的关键参数是ksize和阈值。ksize3是标准 Sobel 核对细笔画响应好阈值 120 是经验值图像整体偏暗时要降到 80 左右否则边缘全被砍掉。实际工程里我一般把颜色法和边缘法的候选框合并用 IoU 去重这样白天靠颜色、夜间靠边缘召回率能明显提上来。这一步不需要深度学习OpenCV 的算子在 CPU 上单帧只要几毫秒比直接上检测网络轻量得多。3. 字符分割把定位框里的字符一个个切开定位框拿到之后里面是一整串字符。CNN 识别的是单个字符所以必须先把它们切开。这一步的难点在于铆钉、边框、字符粘连切歪一个后面全错。3.1 灰度化、二值化与垂直投影标准流程是裁剪定位区域 → 灰度化 → 自适应二值化 → 垂直投影找字符间隙。垂直投影就是统计每一列白色像素的个数字符所在列白点多字符间隙列白点少投影曲线会呈现明显的波峰波谷。def split_characters(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 自适应阈值应对车牌局部光照不均 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 19, 5) # 垂直投影按列求和 col_sum np.sum(binary 255, axis0) # 投影值低于阈值认为是间隙阈值取列高的 1/10 threshold binary.shape[0] // 10 chars [] in_char False start 0 for i, v in enumerate(col_sum): if v threshold and not in_char: in_char True start i elif v threshold and in_char: in_char False # 宽度太窄的当噪点丢掉太宽的可能是粘连 if 8 i - start 60: chars.append(binary[:, start:i]) return charsadaptiveThreshold的 blockSize 取 19、C 取 5是针对车牌这种小尺寸图像调出来的。blockSize 必须是奇数太小会把字符内部也二值化出空洞太大又退化成全局阈值。投影阈值取列高的十分之一是为了过滤掉车牌上下边框残留的少量白点。字符宽度限制在 8 到 60 像素之间是因为标准蓝牌单个字符宽度大约占车牌宽度的 1/8超出这个范围的基本是粘连或噪点。3.2 粘连字符的二次切分投影法最怕字符粘连尤其是「川」「湘」这类笔画多的字或者二值化过头导致相邻字符连在一起。遇到宽度超过 60 像素的块我一般用垂直投影的局部极小值再切一刀。def split_wide_block(block): col_sum np.sum(block 255, axis0) # 在中间 1/3 区域找投影最小值作为切分点 mid_start len(col_sum) // 3 mid_end len(col_sum) * 2 // 3 if mid_end mid_start: return [block] split_idx mid_start np.argmin(col_sum[mid_start:mid_end]) return [block[:, :split_idx], block[:, split_idx:]]这个二次切分只在块宽超过阈值时触发切分点选在中间三分之一的投影最小值处避免切到字符笔画上。实际跑下来粘连字符能救回七成左右剩下的靠 CNN 的鲁棒性兜底——因为 CNN 输入是固定尺寸轻微粘连对识别影响没那么致命。这一步的坑在于切分点如果落在「1」这种窄字符上会把一个字符切成两半所以切完还要检查两半的宽度是否都大于 8 像素不满足就放弃切分。4. 深度学习字符识别CNN 怎么认字分割出来的字符是二值图尺寸不一。CNN 需要固定输入所以先统一缩放到 20×40再送进网络。这一步是整个系统里唯一用到深度学习的地方也是识别率的分水岭。4.1 网络结构选型为什么不用现成的大模型字符识别是个小分类问题类别数就是省份简称加字母数字总共 65 类左右。用 ResNet、VGG 这种大模型属于杀鸡用牛刀参数量大、训练慢、部署重。我一般用三层卷积加两层全连接的小网络参数量控制在 50 万以内CPU 推理单字符 2 毫秒左右。import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_classes65): super().__init__() # 输入 1x40x20灰度、高40、宽20 self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 20x10 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 10x5 nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 5x2 ) self.classifier 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): return self.classifier(self.features(x))三层卷积的感受野刚好覆盖 40×20 的字符图再深就过拟合了。每层卷积后接 ReLU 和最大池化池化把特征图逐步缩小最后展平成 1280 维向量。全连接层加 Dropout 0.5 是防过拟合的关键字符数据集通常只有几万张不加 Dropout 训练集准确率能到 99% 但验证集只有 85%。类别数 65 对应31 个省份简称 24 个字母去掉 I、O 10 个数字。4.2 训练数据准备与增强公开的字符数据集不好找我一般从车牌数据集里自己切。CCPD、CRPD 这类车牌数据集下载后用第 3 章的投影法批量切字符再按字符内容分文件夹存。每个类别至少准备 500 张65 类就是 3 万多张够小网络收敛了。from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_tf transforms.Compose([ transforms.Grayscale(), transforms.Resize((40, 20)), # 随机旋转 ±5 度模拟车牌倾斜 transforms.RandomRotation(5), # 随机调整亮度对比度模拟光照变化 transforms.ColorJitter(brightness0.3, contrast0.3), transforms.ToTensor(), transforms.Normalize([0.5], [0.5]), ]) dataset ImageFolder(chars_dataset, transformtrain_tf) loader DataLoader(dataset, batch_size64, shuffleTrue)增强策略里RandomRotation(5)对应车牌在图像里的轻微倾斜ColorJitter对应不同时段的光照差异。归一化用 0.5 均值和标准差是因为字符图二值化后像素集中在 0 和 255映射到 [-1, 1] 区间能让网络训练更稳。batch_size 取 64 是显存和收敛速度的折中小网络用 32 也行但训练轮次要相应增加。训练时用交叉熵损失加 Adam 优化器学习率 1e-3跑 30 个 epoch 基本收敛。验证集准确率能到 97% 以上剩下的 3% 主要是「0」和「O」、「1」和「I」这种形近字符实际车牌里这些字符有位置约束后处理能纠正一部分。5. 管理系统与避坑从识别结果到能用的后台识别出车牌字符串只是半成品真正交付的是一个能查、能存、能管的后台。这部分用 Flask 加 SQLite 就够不需要上重型框架。5.1 数据入库与查询接口识别结果要落库字段包括车牌号、识别时间、置信度、抓拍图路径。Flask 提供两个接口一个接收识别结果写入一个按车牌号或时间段查询。from flask import Flask, request, jsonify import sqlite3 app Flask(__name__) def init_db(): conn sqlite3.connect(plate.db) conn.execute(CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, plate_no TEXT NOT NULL, confidence REAL, capture_time TEXT, image_path TEXT)) conn.commit() conn.close() app.route(/api/record, methods[POST]) def add_record(): data request.json conn sqlite3.connect(plate.db) conn.execute(INSERT INTO records (plate_no, confidence, capture_time, image_path) VALUES (?,?,?,?), (data[plate_no], data[confidence], data[capture_time], data[image_path])) conn.commit() conn.close() return jsonify({status: ok}) app.route(/api/query, methods[GET]) def query_record(): plate request.args.get(plate, ) conn sqlite3.connect(plate.db) rows conn.execute(SELECT * FROM records WHERE plate_no LIKE ?, (f%{plate}%,)).fetchall() conn.close() return jsonify(rows)表结构里plate_no加索引能显著提升查询速度数据量上万后不加索引查询会明显变慢。confidence字段存 CNN 输出的最大概率低于 0.8 的记录可以标记为待人工复核。接口用LIKE做模糊查询方便按车牌前缀筛选但要注意 SQL 注入参数化查询已经处理了这个问题。5.2 避坑与常见问题排查现象一白天识别正常夜间识别率断崖式下跌。原因是颜色分割在低照度下 HSV 分布偏移蓝色区域卡不出来。解决方法是夜间切换到边缘检测分支或者对图像先做 CLAHE 直方图均衡再进颜色分割实测能挽回大部分夜间样本。现象二CNN 训练准确率 99%实际部署只有 70%。原因是训练集和实际场景的字符尺寸、字体差异大。解决方法是把实际场景切出来的字符人工标注后加入训练集做一轮微调通常再训 10 个 epoch 就能把差距补上。现象三ModuleNotFoundError: No module named cv2。这是新手最常遇到的装了opencv-python但环境不对。解决方法是确认 pip 和 python 是同一个环境用python -m pip install opencv-python而不是直接pip install避免装到系统 Python 而项目跑在虚拟环境里。现象四字符分割把「1」切成两半。原因是「1」的笔画细投影值在字符内部也有低谷。解决方法是在切分后加宽度校验宽度小于 8 像素的块合并回相邻块或者对窄字符单独用固定宽度切分。现象五多辆车同时进画面定位框选错。原因是轮廓筛选只按面积和长宽比没考虑位置。解决方法是按候选框面积排序取最大的或者结合车道线位置做区域约束只保留画面下半部分的候选框。6. 把识别率再往上推一截CTC 与端到端思路前面这套「定位 分割 单字符识别」的流水线优点是每一段都可解释、可单独调优缺点是误差会逐级累积——定位偏一点分割就歪识别就错。如果想把识别率从 90% 推到 97% 以上值得了解 CTCConnectionist Temporal Classification这条路线。CTC 的思路是跳过字符分割直接把整张车牌图送进 CNN RNN输出一个字符序列网络自己学会对齐。PyTorch 里可以用nn.CTCLoss实现标签是字符序列不需要标注每个字符的位置。class PlateCRNN(nn.Module): def __init__(self, num_classes65): super().__init__() self.cnn nn.Sequential( nn.Conv2d(1, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) # 输入高 40 宽 160经过两次池化后高 10 宽 40 self.rnn nn.LSTM(128 * 10, 256, bidirectionalTrue, batch_firstTrue) self.fc nn.Linear(256 * 2, num_classes) def forward(self, x): feat self.cnn(x) # B,128,10,40 b, c, h, w feat.shape feat feat.permute(0, 3, 1, 2).reshape(b, w, c * h) out, _ self.rnn(feat) return self.fc(out) # B,40,65CTC 的关键参数是输入图的宽高比。车牌整体宽高比约 3.14输入设成 160×40 比较合适池化后时间步 40足够覆盖 7 个字符。CTCLoss的blank标签设成 0其余类别从 1 开始编号。训练时input_lengths填 40target_lengths填实际字符数。CTC 的坑在于收敛慢通常要 100 个 epoch 以上而且对学习率敏感建议用 1e-4 起步配合梯度裁剪。我自己的习惯是如果只是毕业设计或者小规模部署第 2 到第 4 章的流水线方案足够代码量可控、调试直观如果场景里车牌倾斜严重、字符粘连频繁再上 CTC。两条路线不冲突CTC 的 CNN 部分可以直接复用前面训好的字符特征。踩过的最大坑是 CTC 训练时忘了设zero_infinityTrue遇到无法对齐的样本 loss 直接变 inf整个训练崩掉这个参数一定要加上。希望帮到你。本文还有配套的精品资源点击获取
返回列表