ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Python的车牌识别系统:OpenCV与轻量CNN实战

基于Python的车牌识别系统:OpenCV与轻量CNN实战 简介这份资源是一套基于Python的车辆牌照识别系统设计源码面向计算机视觉入门者、课程设计或毕业设计开发者帮助解决车牌自动检测与识别这一典型工程问题。压缩包共48个文件约30.78MB包含7个Python源文件、17个PNG与8个JPG图像样本、2个DAT训练数据文件以及UI界面、图标、配置脚本和演示视频等辅助素材覆盖从界面到算法再到测试数据的完整链路。项目围绕车牌定位、字符分割与识别展开配有SVM训练脚本与中文识别模型数据可直接运行或二次开发。目前已有461人学习下载适合希望快速搭建车牌识别原型、理解图像处理与机器学习结合流程的读者参考借鉴。1. 基于Python的车辆牌照识别系统从一张车牌图到可运行源码的完整路径手头有一批停车场抓拍图想用 Python 把里面的车牌号自动抠出来这是很多做智慧交通、门禁管理甚至课程设计的开发者都会碰到的需求。基于 Python 的车辆牌照识别系统设计源码核心要解决的就是三件事从复杂背景里定位车牌、把倾斜或模糊的车牌矫正成标准矩形、再逐字符识别出号码。它适合有 Python 基础、想快速跑通一套可演示可二次开发的识别流程的人也适合需要把识别模块嵌进自己后台服务的工程师。整套方案不依赖昂贵的商业 SDK用 OpenCV 做图像处理、用轻量级深度学习模型做字符识别一台普通笔记本就能跑起来。下面按“先立住原理、再动手复现、最后避坑”的顺序把这条链路拆开讲清楚每一步都给出能直接抄的代码和参数说明。2. 车牌识别系统的技术选型与整体链路拆解2.1 为什么用 OpenCV 轻量模型而不是端到端大模型做车牌识别第一反应可能是直接上一个端到端的目标检测加 OCR 大模型一步到位输出车牌号。这条路在服务器上确实省事但落到实际项目里尤其是需要本地部署、要控制延迟、要跑在边缘设备上的场景端到端大模型的显存占用和推理时间往往让人肉疼。我一般会把链路拆成“检测 矫正 识别”三段检测用 OpenCV 的传统形态学方法或轻量级检测网络矫正用透视变换识别用专门训练过的字符分类模型。这样每一段都能单独调、单独换哪一段效果差就优化哪一段不会牵一发动全身。从热搜词里能看到大量“python安装教程”“vscode python环境配置”“python入门”这类需求说明很多读者是刚接触 Python 不久。这套拆解式方案对新手更友好因为每一段都有明确的输入输出调试时能直观看到中间结果。比如检测阶段输出的是车牌四个角点坐标矫正阶段输出的是拉正后的车牌小图识别阶段才输出字符串。哪一步不对一眼就能定位。选型上OpenCV 负责颜色分割、边缘检测、形态学操作和透视变换这些是 CPU 上毫秒级完成的事。字符识别部分如果追求轻量可以用一个小的 CNN 分类网络把车牌字符分成 65 类左右数字、字母、省份简称。如果不想自己训练也可以接一个开源的轻量 OCR 模型但要注意车牌字符集和通用 OCR 的差异通用模型对车牌里容易混淆的字符比如 0 和 O、1 和 I往往分不清需要额外做后处理。2.2 从原图到车牌号的四步流水线整条链路可以拆成四步每一步都有明确的输入输出和可调参数。第一步是预处理。输入是原始抓拍图输出是增强后的灰度图或边缘图。常见做法是转灰度、高斯模糊去噪、Sobel 或 Canny 提取边缘。高斯核大小一般取 5×5Canny 的双阈值可以设成 100 和 200具体根据图片亮度和对比度微调。这一步的目标是让车牌区域的边缘更突出同时压掉背景里的纹理干扰。第二步是车牌定位。输入是边缘图输出是候选车牌区域的矩形框。国内蓝牌和新能源绿牌的颜色特征很明显可以用 HSV 颜色空间做分割。蓝色车牌的 H 分量大致在 100 到 124 之间S 和 V 可以设得宽一些。分割后做闭运算把车牌区域连成一片再根据长宽比蓝牌约 3:1 到 4:1和面积范围过滤掉明显不是车牌的连通域。第三步是透视矫正。输入是倾斜的车牌区域输出是拉正后的矩形车牌图。用 cv2.minAreaRect 拿到最小外接矩形再用 cv2.getPerspectiveTransform 和 cv2.warpPerspective 做透视变换。目标尺寸可以统一设成 240×80 或 440×140取决于后续识别模型需要的输入大小。第四步是字符识别。输入是矫正后的车牌图输出是字符串。如果自己训练 CNN需要先做字符分割把车牌图切成单个字符再逐个分类。如果用车牌专用 OCR 模型可以直接整图输入输出字符串。字符分割的常见做法是二值化后找垂直投影的波谷但新能源车牌有 8 个字符分割时要注意最后一个字符和边框的距离。提示这四步里定位和矫正的精度直接决定识别上限。如果车牌都没抠准后面识别模型再强也救不回来。建议先把前两步的中间结果可视化出来确认车牌区域被完整框住再往下走。3. 用 Python 跑通车牌检测与矫正的最小可运行代码3.1 环境准备与依赖安装先把环境搭起来。Python 版本建议 3.8 到 3.10太新的版本有些 OpenCV 预编译包可能还没跟上。用 conda 或 venv 建一个独立环境避免和系统里的其他包打架。# 创建虚拟环境 python -m venv plate_env # 激活环境Windows plate_env\Scripts\activate # 激活环境Linux / macOS source plate_env/bin/activate # 安装核心依赖 pip install opencv-python4.8.1.78 pip install numpy1.24.3 pip install pillow10.0.1opencv-python 选 4.8 系列是因为这个版本在 Windows 和 Linux 上的预编译包都比较稳定API 也没有大改动。numpy 锁 1.24 是为了避开 1.25 之后某些和 OpenCV 的兼容性小问题。pillow 用来做图片读写和格式转换不是必须但调试时保存中间结果很方便。如果后面要跑深度学习识别模型还需要装 PyTorch 或 TensorFlow。这里先不展开等确定识别方案后再补。新手如果对环境配置不熟可以先用 pip 装最小依赖跑通检测和矫正部分看到车牌被正确框出来再决定要不要上模型。3.2 车牌定位HSV 颜色分割加形态学过滤下面这段代码完成从读图到输出候选车牌框的全过程。核心思路是先转 HSV 做颜色分割再用形态学操作把车牌区域连成块最后按长宽比和面积筛掉干扰。import cv2 import numpy as np def locate_plate(image_path): # 读图并转 HSV img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图片: {image_path}) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 蓝色车牌 HSV 范围可根据实际图片微调 lower_blue np.array([100, 80, 80]) upper_blue np.array([124, 255, 255]) mask cv2.inRange(hsv, lower_blue, upper_blue) # 闭运算连接断裂区域核大小 17x5 适合横向车牌 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) candidates [] for cnt in contours: x, y, w, h cv2.boundingRect(cnt) aspect_ratio w / float(h) area w * h # 蓝牌长宽比约 3:1 到 4:1面积过滤掉太小和太大的 if 2.5 aspect_ratio 5.0 and 2000 area 50000: candidates.append((x, y, w, h)) # 按面积从大到小排序取前几个 candidates.sort(keylambda b: b[2] * b[3], reverseTrue) return img, candidates # 使用示例 img, boxes locate_plate(car.jpg) for i, (x, y, w, h) in enumerate(boxes[:3]): cv2.rectangle(img, (x, y), (x w, y h), (0, 255, 0), 2) print(f候选 {i}: 位置({x},{y}), 尺寸{w}x{h}, 长宽比{w/h:.2f}) cv2.imwrite(located.jpg, img)这段代码里lower_blue 和 upper_blue 是 HSV 分割的阈值。H 分量 100 到 124 覆盖了大部分蓝色车牌但如果图片偏暗或偏亮S 和 V 的下限需要调。比如夜间抓拍图 V 分量普遍偏低可以把 V 的下限从 80 降到 50。闭运算的核大小 (17, 5) 是横向的因为车牌是扁长形状横向连接比纵向连接更重要。如果车牌在图中倾斜严重这个核可能连不起来需要先做倾斜校正或者换用更通用的检测方法。长宽比过滤设成 2.5 到 5.0是因为蓝牌标准比例约 3.14:1但实际拍摄有透视变形比例会浮动。面积范围 2000 到 50000 是根据常见分辨率估算的如果图片是 4K 大图面积上限要相应放大。排序后取前三个候选是因为有时候保险杠上的蓝色贴纸或广告牌也会被框进来多留几个候选给后面矫正阶段筛选。3.3 透视矫正把倾斜车牌拉成标准矩形拿到候选框后不能直接裁下来就送去识别因为拍摄角度会让车牌产生透视变形。下面这段代码用最小外接矩形和透视变换把车牌拉正。def correct_plate(img, box): x, y, w, h box # 适当外扩避免裁掉车牌边缘 pad 5 roi img[max(0, y-pad):yhpad, max(0, x-pad):xwpad] if roi.size 0: return None gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 二值化突出车牌区域 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 找最小外接矩形 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None cnt max(contours, keycv2.contourArea) rect cv2.minAreaRect(cnt) box_pts cv2.boxPoints(rect) box_pts np.int0(box_pts) # 确定目标宽高统一到 240x80 width, height 240, 80 dst_pts np.float32([[0, 0], [width, 0], [width, height], [0, height]]) src_pts np.float32(box_pts) # 透视变换 M cv2.getPerspectiveTransform(src_pts, dst_pts) warped cv2.warpPerspective(roi, M, (width, height)) return warped # 对第一个候选做矫正 if boxes: plate correct_plate(img, boxes[0]) if plate is not None: cv2.imwrite(plate_corrected.jpg, plate) print(矫正后车牌尺寸:, plate.shape)这里有几个关键点。pad 外扩 5 个像素是为了防止 boundingRect 把车牌边缘切掉尤其是车牌有边框的时候。二值化用 OTSU 自动阈值比手动设固定值更适应不同光照。minAreaRect 返回的是带角度的矩形boxPoints 拿到四个角点顺序可能不是标准的左上、右上、右下、左下但 getPerspectiveTransform 对点的顺序敏感顺序错了变换结果会翻转或扭曲。稳妥的做法是先对 boxPoints 按坐标排序确保左上角是 xy 最小的点右下角是 xy 最大的点。目标尺寸 240×80 是蓝牌的一个常用比例实际蓝牌尺寸 440×140按比例缩放到 240×80 左右。如果后面识别模型需要更大输入可以改成 440×140。warpPerspective 的输出就是拉正后的车牌图可以直接送去字符识别。注意如果候选框本身就不准矫正阶段会把错误放大。建议在矫正前先可视化候选框确认框住的是完整车牌再往下走。如果候选框偏了回头调 HSV 阈值或形态学核大小。4. 字符识别从分割到分类的落地细节4.1 字符分割的垂直投影法与参数调整矫正后的车牌图要变成字符串中间绕不开字符分割。垂直投影法是最直观的做法把车牌二值化后统计每一列的白色像素数字符所在列的投影值高字符之间的间隙投影值低找波谷就能切出单个字符。def segment_characters(plate_img): gray cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) # 自适应二值化应对光照不均 binary cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 19, 5) # 垂直投影 projection np.sum(binary, axis0) / 255 # 找波谷作为分割点 char_imgs [] in_char False start 0 threshold 2 # 投影值低于此认为进入间隙 for i, val in enumerate(projection): if val threshold and not in_char: in_char True start i elif val threshold and in_char: in_char False if i - start 5: # 过滤太窄的噪声 char_imgs.append(binary[:, start:i]) # 处理最后一个字符贴着右边界的情况 if in_char and len(projection) - start 5: char_imgs.append(binary[:, start:]) return char_imgs chars segment_characters(plate) print(f分割出 {len(chars)} 个字符) for i, c in enumerate(chars): cv2.imwrite(fchar_{i}.png, c)adaptiveThreshold 比全局阈值更适合车牌因为车牌表面可能有反光或污渍全局阈值容易把某些字符切碎。blockSize 设 19 是经验值太小会保留噪声太大会把字符笔画连在一起。C 值设 5 是常数项用来微调阈值。垂直投影的 threshold 设 2 是个保守值意思是某列白色像素少于 2 个就认为是间隙。如果车牌字符粘连严重这个值要调大如果字符被切碎要调小。i - start 5 是过滤掉宽度小于 5 像素的噪声块正常字符宽度在 15 到 30 像素之间240 宽的车牌图。新能源车牌有 8 个字符最后一个字符和边框之间可能没有明显间隙代码里单独处理了 in_char 为 True 且延伸到末尾的情况。如果分割结果多出或少出字符优先检查二值化效果和 threshold 值。4.2 用轻量 CNN 做字符分类的输入输出约定分割出单个字符后需要一个分类模型把每个字符图映射到具体字符。自己训练一个 CNN 是最可控的做法输入统一到 20×40 或 32×64输出 65 类左右。import torch import torch.nn as nn class PlateCNN(nn.Module): def __init__(self, num_classes65): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d((4, 8)) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 4 * 8, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x # 输入约定单通道灰度图尺寸 32x64归一化到 [0,1] model PlateCNN() dummy torch.randn(1, 1, 32, 64) out model(dummy) print(输出维度:, out.shape) # torch.Size([1, 65])这个网络很小参数量在百万级别CPU 上单张推理几毫秒。输入是单通道灰度图因为车牌字符识别对颜色不敏感灰度图能减少计算量。尺寸 32×64 是字符的常见比例如果分割出的字符宽高比差异大可以先 resize 再送进网络。65 类怎么来的10 个数字 24 个英文字母去掉 I 和 O 避免和 1、0 混淆 31 个省份简称。实际类别数根据你的数据集调整。训练时要注意样本均衡数字和字母的出现频率远高于省份简称如果直接拿原始数据训模型会对省份简称欠拟合。常见做法是对省份简称做数据增强或过采样。如果不想自己训练也可以用一个预训练的车牌 OCR 模型但要注意输入尺寸和字符集是否匹配。通用 OCR 模型对车牌的识别率通常不如专用模型尤其是中文省份简称和易混淆字符。4.3 后处理把分类结果拼成合法车牌号模型输出的是每个字符的类别概率取 argmax 得到字符后还需要后处理才能拼成合法车牌。后处理主要做三件事按车牌规则校验格式、修正常见混淆、处理置信度低的字符。def postprocess(chars, probs): # chars: 字符列表, probs: 对应置信度 result [] for ch, p in zip(chars, probs): if p 0.6: # 低置信度字符根据上下文修正 if ch 0: ch O if len(result) 0 else ch elif ch 1: ch I if len(result) 0 else ch result.append(ch) plate_str .join(result) # 简单格式校验第一位是省份简称第二位是字母 if len(plate_str) 2: if not (A plate_str[1] Z): # 第二位不是字母尝试修正 pass return plate_str # 假设模型输出 chars [京, A, 1, 2, 3, 4, 5] probs [0.95, 0.92, 0.88, 0.91, 0.87, 0.93, 0.90] print(车牌号:, postprocess(chars, probs))置信度阈值 0.6 是经验值低于这个值的字符大概率有问题。修正常见混淆时0 和 O、1 和 I 是最典型的但修正规则要看位置车牌第二位是字母后面是数字和字母混合具体规则按当地车牌规范来。格式校验可以先做简单版确认第一位是省份简称、第二位是字母复杂校验可以查车牌号正则。后处理不是万能的如果模型本身识别率低后处理只能修个别错误。核心还是把分割和分类做好后处理作为最后一道防线。5. 车牌识别系统落地避坑5 个血泪教训5.1 现象白天识别率 95%晚上直接掉到 40%原因HSV 颜色分割的阈值是按白天光照调的夜间图片整体偏暗V 分量普遍低于 80蓝色车牌区域被漏掉。另外夜间车灯造成的局部过曝会让车牌反光二值化后字符断裂。解决不要用一套固定阈值跑所有场景。按图片平均亮度分档暗图降低 V 的下限亮图提高 V 的上限。更稳的做法是加一个光照补偿步骤用 CLAHE 对灰度图做自适应直方图均衡再转 HSV 分割。如果夜间场景占比高建议单独收集夜间数据调阈值或者直接上基于深度学习的检测模型对光照变化更鲁棒。5.2 现象车牌定位框到了保险杠上的蓝色贴纸原因HSV 颜色分割只认颜色不认形状和内容。保险杠贴纸、蓝色广告牌、甚至某些车身颜色都会产生蓝色连通域如果长宽比和面积刚好落在过滤范围内就会被当成车牌候选。解决在颜色分割后加一层纹理或边缘密度校验。车牌区域有密集的字符边缘而纯色贴纸边缘稀疏。可以用 Canny 边缘检测后统计候选区域内的边缘像素占比低于阈值的丢弃。另一个办法是提高候选框的筛选标准比如要求候选区域内至少有 5 个字符宽度的垂直边缘。如果还是误检可以在矫正后加一个简单的字符存在性判断比如二值化后连通域数量是否在合理范围。5.3 现象透视变换后车牌是镜像的或者旋转了 180 度原因cv2.boxPoints 返回的四个角点顺序不固定getPerspectiveTransform 对源点顺序敏感。如果源点顺序和目标点顺序不匹配变换结果就会翻转或旋转。解决在调用 getPerspectiveTransform 之前对 boxPoints 做排序。按 xy 的值排序最小的作为左上角最大的作为右下角剩下的两个点按 x 坐标区分右上和左下。排序后再对应到目标矩形的四个角。如果车牌本身是倒置的比如某些特殊拍摄角度还需要额外判断并旋转 180 度可以通过字符的上下方向来检测。5.4 现象字符分割把“川”字切成三块或者把“8”切成两个圈原因垂直投影法对笔画不连续的字符不友好。“川”字三竖之间间隙大投影波谷明显容易被切成三块。“8”字上下两个圈在垂直投影上中间细也可能被误切。解决分割后加一个合并逻辑如果相邻两个字符块的宽度都小于正常字符宽度的一半且合并后的宽度在正常范围内就把它们合并。正常字符宽度可以从车牌总宽度除以字符数估算。另外二值化时用形态学闭运算把同一字符的笔画连起来核大小取 3×3 或 5×5能缓解笔画断裂问题。对于“川”这类特殊字符可以在后处理阶段用规则修正比如检测到连续三个窄块且总宽度接近一个字符就合并。5.5 现象模型在训练集上 99%换一批图就崩原因训练数据太单一可能全是同一角度、同一光照、同一相机拍的。模型学到了背景特征而不是字符特征。另外训练集和测试集的字符分布不一致比如训练集里某个省份简称特别多模型对这个简称过拟合。解决训练数据要覆盖多角度、多光照、多相机。如果自己采集成本高可以用数据增强扩充随机旋转 ±15 度、随机调整亮度和对比度、随机加高斯噪声、随机做透视变换。增强时注意不要改变字符的可识别性比如旋转角度太大把字符转得认不出来就适得其反。另外训练集和验证集要按字符类别分层采样确保每个类别都有足够样本。如果某个类别样本太少过采样或合成少数类样本。6. 把识别率再往上推一个可复用的调参习惯整套系统跑通之后真正决定好不好用的往往不是模型结构而是调参的耐心。我自己的习惯是建一个“错题本”每次识别错的图都存下来按错误类型分类定位错、矫正错、分割错、分类错。每周花半小时翻一遍错题本看哪类错误最多就集中调那一环的参数。这个习惯比盲目换模型有用得多。举个具体例子。之前做停车场项目发现新能源绿牌的识别率一直上不去。翻错题本发现大部分错误集中在定位阶段绿牌的 HSV 范围和蓝牌差异很大用蓝牌的阈值去套绿牌漏检率很高。后来单独给绿牌加了一组 HSV 范围H 分量在 35 到 85 之间S 和 V 的下限也重新调了识别率从 70% 出头提到 90% 以上。这个改动本身不复杂但没有错题本可能一直以为是识别模型的问题在错误的方向上浪费时间。另一个习惯是给每个环节留一个“后悔药”。比如矫正阶段除了输出拉正后的车牌图还把原始候选框和变换矩阵存下来。如果后面识别错了可以回溯到矫正阶段手动调整角点重新变换看是不是矫正的问题。分割阶段同理把二值化图和投影曲线存下来分割错了能直接看到是阈值问题还是字符粘连问题。这些中间结果平时占不了多少空间但排查问题时能省大量时间。参数调优没有一劳永逸的配置。不同场景、不同相机、不同车牌类型最优参数都不一样。与其追求一套万能参数不如把调参流程标准化先可视化中间结果定位问题出在哪一环再针对那一环调 1 到 2 个关键参数调完在验证集上跑一遍看整体指标。每次只动一个参数避免多个参数一起改导致无法归因。最后说一个容易被忽略的点车牌识别的输出不只是一个字符串还应该带置信度和原始图。置信度低的识别结果在实际系统里可以转人工复核而不是直接放行。原始图存下来方便后续追溯和继续训练。这套方案的价值不在于一次做到 99%而在于有一个能持续迭代的闭环错题本加中间结果留存加标准化调参这三样坚持下来识别率会一点点往上走。希望帮到你。本文还有配套的精品资源点击获取
返回列表