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

资讯详情

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

从零实现车牌识别:OpenCV定位分割+PyTorch轻量CNN识别全流程

从零实现车牌识别:OpenCV定位分割+PyTorch轻量CNN识别全流程 简介面向计算机专业大作业与期末设计的初级车牌识别项目基于PyTorch和OpenCV实现包含完整源码与训练模型。评测得分98分属于导师认可的高分项目源码均经本地编译与严格调试下载后可直接运行。资源压缩包大小25.38MB共7个文件其中3个Python脚本分别负责字符识别、车牌识别与整体流程2个pth权重文件用于字符和车牌模型推理另有README说明文档与图片数据压缩包结构清晰便于快速上手。目前已有64人学习使用适合正在完成课程设计、毕业设计或需要项目实战练习的开发者参考。通过该资源可以了解车牌定位、字符分割与识别的初步实现思路掌握PyTorch模型加载与OpenCV图像处理的基本配合方式并可直接基于源码扩展训练数据或调整网络结构对快速落地一个视觉识别小项目具有实用价值。1. 车牌识别不是一个模型而是一条流水线很多人第一次接触车牌识别时会下意识地以为这是一个“端到端”的深度学习任务——输入一张图片输出一个车牌号。但在实际做初级项目时这条路往往是最难走通的你需要大量标注数据、足够深的网络结构、以及动辄数小时的GPU训练时间。反观工程上更常见、也更好拿结果的做法是把车牌识别拆成“定位→分割→识别”三件套OpenCV做前两步PyTorch训练一个轻量CNN做第三步。标题里那个“源码模型”本质就是这套管线的工程化落地。这个项目解决的痛点很明确不是研究车牌识别在极端场景下的SOTA精度而是让你在有限算力、有限数据、有限时间内跑通一条从“一张车辆图片”到“一个车牌字符串”的完整链路。它适合三类人刚学完PyTorch基础框架想找个综合练手项目的学生、需要应付课程设计或毕业设计答辩的开发者、以及想快速搭建一个车辆管理原型系统的从业者。动手之前先想清楚一点车牌识别系统的瓶颈从来不在单点模型精度而在各环节之间的错误累积——定位歪了分割就错分割错了识别再准也是白搭。带着这个视角去看下面每一步的设计和参数选择你才不会把项目做成“能用但不知道为什么不稳”。2. OpenCV 车牌定位用颜色和形状先把候选区裁出来2.1 为什么定位环节用 OpenCV 而不是让模型去做车牌的物理特征非常鲜明统一的蓝色或绿色背景、长宽比约 3:1 的矩形、高对比度的白色字符。这些特征用传统图像处理的方式去捕捉既不贵又稳定远比训练一个目标检测模型来得划算。初级项目里最常踩的坑是有人一上来就挂一个 YOLO 模型做端到端定位结果训练数据不够、漏检一堆反而连踏实的 OpenCV 方案都不如。我一般会把定位逻辑拆成两条并行路径一是基于颜色阈值HSV 空间筛选出蓝色/绿色的像素区域二是基于边缘检测Sobel 形态学闭运算找出文字密集的候选区域。两条路径的结果各自做轮廓筛选再取并集。这样做的原因是颜色路径在曝光正常时很准一旦逆光或夜色下容易失效边缘路径虽然在背景杂乱的场景会误检多但恰好能补上颜色失效的缺口。两者结合再用长宽比和面积做硬过滤初级项目里这个方案的召回率已经相当能看。2.2 用 HSV 颜色阈值圈出车牌底色HSV 空间的优势在于把“颜色”拆成了色相H、饱和度S和明度V颜色筛选直接基于 H 通道即可不受光照强度直接影响。下面是标准蓝色车牌的筛选代码import cv2 import numpy as np def find_plate_by_color(img_bgr): # 转 HSV 色彩空间色相、饱和度、明度三通道分离 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 蓝色车牌阈值标准蓝调 H 在 100~124S 和 V 做宽松处理 blue_lower np.array([100, 80, 80]) blue_upper np.array([124, 255, 255]) # 绿色新能源车牌阈值H 在 35~77 之间 green_lower np.array([35, 80, 80]) green_upper np.array([77, 255, 255]) mask_blue cv2.inRange(hsv, blue_lower, blue_upper) mask_green cv2.inRange(hsv, green_lower, green_upper) mask cv2.bitwise_or(mask_blue, mask_green) # 闭运算先膨胀后腐蚀把车牌上的字符镂空区域补上 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) return contours, mask这段代码的核心在于两个参数HSV 阈值和闭运算核。HSV 阈值决定了颜色筛选的严格程度标准蓝的 H 值集中在 105~115 之间设为 100~124 是给不同相机色偏留出余量S饱和度的下限调到 80是为了在夜间低饱和度场景下还能保留部分候选。闭运算核设为 17×5匹配的是车牌的横向长条特征——核的宽度要大于字符笔画之间的空隙才能把“京A·12345”这种字间断口连成一个整体。2.3 轮廓筛选的三个硬性参数拿到候选轮廓之后不能直接裁图。大部分轮廓是车身贴纸、天空反光、路面标线之类的噪声。筛选时我会固定检查三个量参数过滤规则设定理由面积候选轮廓面积必须大于整幅图面积的 0.5%车牌在画面中不会太小过小是噪点宽高比1.8 ≤ w/h ≤ 4.5标准车牌宽高比约 3.14留出自身倾斜和形变余量填充度轮廓面积占外接矩形面积 ≥ 0.4排除细长条、L形等非矩形轮廓宽高比区间要留宽是因为车牌在画面中可能有透视倾斜投影到图像平面上宽高比会偏离 3.14但如果放宽到 5 以上很容易把车身侧面的一长条装饰线圈进来。填充度这个参数很多人会漏掉——它是用cv2.contourArea(contour) / (w * h)算的专门用来干掉那些形状奇怪但面积恰好达标的轮廓。三轮过滤下来候选区通常从几十个降到两三个再按面积排序取最大一个即可。边缘检测路径Sobel 闭运算 同样筛选逻辑在此不赘述跑通颜色路径后再加不迟。3. 字符分割从车牌图到 7 张子图的关键一跳3.1 为什么分割比定位更影响最终准确率定位拿到的是整块车牌区域识别需要的却是单个字符。分割如果切偏了哪怕偏一个像素后面的 CNN 分类器都会拿半截字符去推理。初级项目最容易在这里翻车的原因是很多人直接用固定等宽切分——假设车牌七位字符均等分布。实际拍摄时汉字和字母的宽度差异很大第二位字母后还有一个间隔点就是那个小圆点固定切分必然错位。正确的做法是“垂直投影法”把车牌图像的每一列做二值化后求和字符区域因为白色像素多会留下高柱字符间的空隙几乎为零投影图上的波峰和波谷天然把每个字符切开了。这个思路简单、稳定不引入额外模型是实现分割的首选方案。3.2 二值化与连通域切分代码分割之前要先把颜色变成二值信息。通常将定位输出的车牌图转灰度再找一个自适应阈值做二值化。注意不用全局固定阈值因为不同车牌的底色深浅和曝光不一固定阈值会把字符和背景糊在一起def split_plate_chars(plate_bgr): # 灰度化 大津法二值化自动计算阈值区分前后景 gray cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU) # 垂直投影统计每列上的白色像素个数 h, w binary.shape col_sum np.sum(binary, axis0) # 找到连续非零区间即候选字符的左右边界 chars [] in_char False start 0 for i in range(w): if col_sum[i] 0 and not in_char: in_char True start i elif col_sum[i] 0 and in_char: in_char False if i - start 10: # 排除噪声导致的极窄区间 chars.append((start, i)) area binary[:, start:i] # 统一裁剪到字符高度便于后续送入 CNN chars.append(area) # 按字符个数和宽度做合法性校验 if len(chars) ! 7: return None # 交给上层做失败处理 return chars这段代码里的i - start 10是排除小噪点区间的关键最小宽度要按字符实际尺寸来定——你可以在调试时把这些区间宽度打印出来如果是 7 个字符各自的宽度应当落在某个合理范围内。字符数校验是必须有的很多初级项目忽略这一层直接在分割出错时把非法的“字符图”送进 CNN让模型去猜一个根本不存在的类别。3.3 第二位的“间隔点”问题中国车牌第二位后面有个小圆点垂直投影时它也会形成一个小波峰。处理办法有两种一是宽度过滤小圆点的投影宽度远小于字符宽度二是位置过滤它出现在第二位和第三位之间且高度只占字符高度的一半以下。更稳的做法是在二值图上做连通域分析把高度明显小于整体平均高度的连通域直接剔除。分割完成后把 7 张子图统一 resize 到 32×32 或者 64×32宽×高的固定尺寸灰度图即可入模型——颜色信息在那个阶段已经没有意义了。4. PyTorch 训练轻量车牌字符识别模型4.1 分类任务的类别空间为什么是 65 而不是 36车牌字符由汉字、字母、数字三部分组成。汉字是 31 个省直辖市简称字母是 24 个I 和 O 不用因为和数字 1/0 容易混淆数字 10 个合计 65 类。这里有一个关键设计决定对车牌做定长 7 字符整体识别而不是逐字符单独训练模型。整体识别模型把 7 个字符当作一个 65^7 的组合分类问题需要的输出维度接近天文数字数据量根本喂不够。逐字符分类则简单得多——7 个字符位置共享同一个分类器模型只输出 7 组 softmax每组 65 个概率。训练数据也不需要把所有组合收集齐只要覆盖“某一位置上出现过这个字符”即可。两者的本质区别是前者建模的是字符序列的联合分布后者假设各位置独立。初级项目里后者的精度和可训练性都远胜前者。4.2 构建一个两卷积层加双全连接层的 CNN对于单字符 32×32 的灰度图一个重型 ResNet 是大炮打蚊子。更合理的做法是手动搭一个轻量 CNN——两个卷积层提取边缘和纹理特征两个全连接层做分类。PyTorch 里定义网络结构的代码并不长import torch.nn as nn class PlateCharCNN(nn.Module): def __init__(self, num_classes65): super(PlateCharCNN, self).__init__() # 第一层输入 1 通道灰度图输出 32 个特征图 self.conv1 nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 32x32 - 16x16 ) # 第二层特征图加深到 64尺寸降到 8x8 self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 16x16 - 8x8 ) # 全连接层64*8*8 展平后进入分类器 self.fc nn.Sequential( nn.Linear(64 * 8 * 8, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): x self.conv1(x) x self.conv2(x) x x.view(x.size(0), -1) return self.fc(x)MaxPool2d(2)把特征图尺寸逐层减半32×32 经过两次池化变成 8×8扁平化后是 64×8×84096 维。Dropout 0.5 在训练时随机屏蔽一半全连接输入防止模型对训练集过拟合。这个网络结构参数量大概 50 万左右CPU 上训练一个 epoch 也就几十秒——你完全不需要 GPU 就能完成整个项目。4.3 训练循环中必须处理的四个细节训练代码本身不复杂与标准图像分类训练几乎一致。区别在于数据构造。字符级数据有两种来源公开数据集如 CCPD 经分割后得到和合成数据。手动收集真实车牌分割图通常只有几千张而用字体渲染的方式可以生成任意规模的合成数据集——把 65 个字符用不同字体、不同字号、不同噪声水平渲染成 32×32 图片。合成数据虽然与真实拍摄图有域差异但对于字符分类这种任务shape 信息已经足够初级项目里效果完全可用。训练循环里四个细节决定了最终能跑多好优化器设为 Adam初始学习率 1e-3别用 SGD初项目调学习率频率太高不值得每 10 个 epoch 将学习率乘 0.5用torch.optim.lr_scheduler.StepLR实现防止后期在局部最小值周围震荡交叉熵损失直接接在全连接输出上注释掉手动 softmax——nn.CrossEntropyLoss内部已经做了 log_softmax重复加 softmax 会破坏梯度数值稳定性保存模型时同时保存state_dict和类别映射表类别映射表是从索引到字符的字典没有它你根本不知道输出的第 37 类到底是“A”还是“B”。这个映射表很多人会忘记存推理时再回头找数据集的类别顺序非常痛苦。torch.save({ model_state_dict: model.state_dict(), class_idx: class_idx, # {0: 京, 1: 津, ..., 64: 9} input_size: 32, }, plate_char_cnn.pth)训练完成的判定标准不是训练集准确率到 100%而是验证集上每个字符位都达到 98% 以上的单字符准确率。按经验在 3 万张合成数据上训练 30 个 epoch 左右能达到这个水平。低于 98% 时先别调网络结构去查数据——合成字体过少、字符图像没有居中对齐、二值化后白色像素太稀疏这三个问题占比超过八成。5. 模型推理管线与分数提升的实战技巧5.1 组装端到端推理脚本模型训练完毕把定位、分割、识别串成一条 pipeline。输入一张含车辆的照片输出车牌字符串。串接时要特别注意中间结果在函数间的传递类型——定位返回 BGR 的numpy.ndarray分割返回 7 张单通道的numpy.ndarrayCNN 需要的是torch.Tensor且 shape 为(1, 1, 32, 32)、dtype 为float32、像素值归一化到 0~1。这里最容易出 bug 的是维度顺序和数值范围不一致。import torch import cv2 def recognize_plate(img_bgr, model, class_idx, devicecpu): # 1. 定位 contours, _ find_plate_by_color(img_bgr) if not contours: return None plate_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(plate_contour) plate_img img_bgr[y:yh, x:xw] # 2. 分割 char_areas split_plate_chars(plate_img) if char_areas is None or len(char_areas) ! 7: return None # 3. 识别 model.eval() result [] with torch.no_grad(): for gray_char in char_areas: gray_char cv2.resize(gray_char, (32, 32)) # 归一化 增加通道维和 batch 维 tensor torch.from_numpy(gray_char).float().div(255.0) tensor tensor.unsqueeze(0).unsqueeze(0).to(device) logits model(tensor) pred logits.argmax(dim1).item() result.append(class_idx[pred]) return .join(result)注意torch.no_grad()所在位置它必须包住整段 for 循环而不是只包住模型调用否则每次循环都会重新构建计算图增加不必要的内存开销和推理延迟。argmax取的是概率最大的类别索引再通过class_idx映射回真实字符。推理速度在我的测试环境下纯 CPU 推理单张图约 80~120ms其中定位占了大半——如果性能吃紧把findContours的输入 mask 先缩小到一半分辨率再放大就是最直接的优化手段。5.2 一个把分数从 85 拉到 95 的细节識別置信度回退最后一个直接可用的技巧是给识别结果加一个置信度阈值和回退机制。CNN 输出的 65 个类别的 softmax 概率最大值就是模型对这个字符的置信度。当置信度低于某个阈值比如 0.6时说明这个字符分割得可能不对——与其强行给出一个错误答案不如返回“无法识别”这在课程设计和答辩里比硬报一个“汏A·12345”要体面得多。在实际车辆管理系统中这个回退机制还能触发二次拍照或人工复核而不是让错误数据直接落库。我一般会在 pipeline 最后加一个 7 元素置信度数组的输出让调用方自己决定截断策略。本文还有配套的精品资源点击获取
返回列表