简介:车牌识别是计算机视觉领域的经典工程场景,常被选为课程设计与毕业设计题目。传统图像处理与深度学习各有局限:纯OpenCV方法依赖手工特征,对光照和角度敏感;端到端模型则需大量标注数据和算力。工程实践往往采用混合架构——以OpenCV完成灰度化、滤波、二值化、轮廓定位与字符分割,再将字符图像送入CNN分类器识别。这种设计兼顾鲁棒性与可调试性,也便于在有限数据集上完成模型训练与参数调优。理解预处理管道、卷积网络结构、字符分割与推理流程,能帮助快速搭建一套可运行的车牌识别系统,并为后续图像识别类项目提供可复用的方法论。本文以毕设场景为切入点,完整梳理了从图像输入到车牌号输出的全链路实现细节。
1. 车牌识别系统:CNN+OpenCV这条链路为什么是毕设首选
做毕设选车牌识别的人很多,但真正能用起来、敢站到答辩台上讲清楚的不多。问题往往不在模型有多深,而在整个链路里预处理、定位、分割、识别每一环都会出幺蛾子。这套基于CNN+OpenCV的Python实现,核心是把传统图像处理当作前置管道,把CNN当作识别引擎,训练好的模型对蓝底白字这类标准车牌效果很稳,配合答辩PPT和报告文档,属于那种“拿回去能改、改完能跑、跑完能答辩”的完整资源。适合正在做计算机视觉方向课设或毕设的同学,也适合想快速跑通一个端到端识别流程、但不想从零开始搓轮子的从业者。
2. 系统架构与模型选型:为什么是OpenCV预处理+CNN分类,而不是端到端硬怼
2.1 从传统图像处理到深度学习:这套架构的分工逻辑
车牌识别从技术演进上看有两条路线。一条是纯传统图像处理,边缘检测、颜色分割、形态学操作全用OpenCV搞定,好处是快、无需训练,坏处是对光照、倾斜、遮挡极其敏感,稍微换个拍摄角度识别率就崩。另一条是端到端深度学习,输入图像直接进网络输出车牌号,好处是鲁棒性强,坏处是你得准备海量标注数据,训练周期长,而且毕设场景下GPU资源往往不宽裕。
这套资源选了折中路线:OpenCV负责定位和字符分割,CNN负责分类。这个分工逻辑很实在——定位和分割是空间变换问题,传统算法在规则场景下又快又准;字符识别是模式分类问题,CNN在这个任务上的泛化能力远超模板匹配。实践下来,这种结构比纯端到端模型容易调试得多,哪一步出问题可以直接在中间结果图上看到,对答辩时的“你是怎么排查问题的”这类提问来说,是个加分项。
2.2 CNN模型结构设计:卷积层、池化层和全连接层的取舍
代码里的CNN模型不算深,因为车牌字符类别就那么多(汉字、字母、数字加起来几十类),不需要ResNet那种上百层的容量。常见做法是三层卷积+两层全连接:
import torch.nn as nn class LicensePlateCNN(nn.Module): def __init__(self, num_classes): super(LicensePlateCNN, self).__init__() self.conv_layers = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), # 输入单通道灰度图 nn.ReLU(), nn.MaxPool2d(2), # 28x28 -> 14x14 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 14x14 -> 7x7 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2) # 7x7 -> 3x3 ) self.fc_layers = nn.Sequential( nn.Flatten(), nn.Linear(128 * 3 * 3, 256), nn.ReLU(), nn.Dropout(0.5), # 防过拟合,训练时随机失活 nn.Linear(256, num_classes) ) def forward(self, x): return self.fc_layers(self.conv_layers(x))这里的参数有几个值得注意的地方。输入用单通道灰度图而不是三通道彩色图,因为车牌字符识别对颜色不敏感,灰度化能显著降低计算量,训练速度也更快。第一层卷积输出32个特征图,第二层64,第三层128,逐层翻倍是CNN的常规设计思路——浅层学边缘和纹理,深层学字符的部件级特征。Dropout层放在全连接层中间,目的是防止模型对训练集过拟合,毕设数据集通常只有几百张图,不加Dropout的话验证集准确率很容易在某个epoch之后反而往下掉。
2.3 OpenCV预处理管道:灰度化、去噪、二值化的顺序和参数
预处理管道是这套系统的地基。你可能会想,直接用原始RGB图喂给CNN不就行了?实际上OpenCV的预处理起着两个作用:一是在定位阶段提升车牌区域的对比度,二是给CNN提供更干净的字符输入。顺序一般固定为:灰度化 → 高斯滤波去噪 → 自适应二值化 → 形态学操作。
import cv2 import numpy as np def preprocess_image(image_path): # 读入图片并缩放:太大影响定位速度,太小丢失细节 img = cv2.imread(image_path) img = cv2.resize(img, None, fx=0.5, fy=0.5, interpolation=cv2.INTER_CUBIC) # 第一步:灰度化,把三通道压缩成单通道 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 第二步:高斯滤波去噪,核大小取5x5,sigma取0由函数自动算 blurred = cv2.GaussianBlur(gray, (5, 5), 0) # 第三步:自适应阈值二值化,blockSize取31,C取5 binary = cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 5 ) # 第四步:形态学闭运算,先膨胀后腐蚀,连回断裂的字符笔画 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) morphed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) return morphed高斯滤波的核大小直接影响去噪效果和边缘保留的平衡。核太大会把字符边缘也抹掉,核太小等于没滤。5x5是实测下来对车载摄像头噪声比较合适的值。自适应阈值比全局阈值在光照不均场景下稳得多,它按局部邻域计算阈值,每个像素的阈值都不一样。blockSize参数表示邻域大小,31的意思是看周围31x31像素范围;C是常数项,从阈值里减掉,增大C会让二值化结果更暗更“挑剔”。形态学闭运算主要解决字符笔画断裂的问题——车牌字符被污渍或反光影响时,膨胀能连上断点,再腐蚀还原轮廓。
2.4 车牌定位:轮廓检测与几何筛选的实战参数
车牌定位是整体准确率的最大变量。CNN再好,车牌框找错了也白搭。基于OpenCV的经典做法是找轮廓、筛轮廓,利用车牌的宽高比和面积占比作为硬性条件。
def locate_plate(binary_img): # 提取轮廓 contours, _ = cv2.findContours( binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) candidates = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) # 宽高比:标准车牌约3:1,放宽到2:1到4.5:1 aspect_ratio = w / h if aspect_ratio < 2.0 or aspect_ratio > 4.5: continue # 面积占比:车牌在整张图中通常占1%到10% area_ratio = (w * h) / (binary_img.shape[0] * binary_img.shape[1]) if area_ratio < 0.01 or area_ratio > 0.10: continue candidates.append((x, y, w, h)) # 按面积排序,取最大的作为车牌区域 if not candidates: return None return max(candidates, key=lambda rect: rect[2] * rect[3])RETR_EXTERNAL只提取最外层轮廓,这样能避开字符内部的孔洞轮廓。CHAIN_APPROX_SIMPLE压缩水平、垂直和对角方向的线段,减少存储点数。宽高比是筛选条件里最硬的一条——蓝底白字车牌长宽比在3.14左右,2.0到4.5的区间足够容纳拍摄角度带来的轻微透视变形。面积占比的下限设1%,低于这个值的噪声区域直接丢弃;上限10%,避免把车头大块区域误判成车牌。
3. 从图像到车牌号:字符分割与CNN推理全流程复现
3.1 字符分割:投影法与连通域方法的取舍
拿到车牌区域后,下一步是把“京A12345”这样的整体图像切成单个字符。如果直接在整张车牌上跑CNN分类,输出的维度会爆炸——你不知道一个车牌上有几个字符,而且字符间距不固定。所以必须先把字符位置找出来。两种主流做法:投影法和连通域分析法。
投影法的思路是把二值图像在垂直方向上做像素统计,统计值大的列就是字符区域,统计值接近零的列就是字符间隙。优点是对标准字符间距的车牌效果好,缺点是遇到铆钉、边框干扰时容易把噪声也当成分割边界。连通域法更好理解——把二值图中连在一起的白色像素块找出来,每个块就是一个候选字符。这套资源走的是投影法加的连通域校验,两者结合比单用一种稳。
def segment_characters(plate_img): # 先转为灰度并二值化,确保输入干净 gray = cv2.cvtColor(plate_img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # 垂直投影:统计每一列的白色像素数 col_sum = np.sum(binary, axis=0) # 找到字符起始和结束位置 in_char = False char_regions = [] start = 0 for i, val in enumerate(col_sum): if val > 0 and not in_char: in_char = True start = i elif val == 0 and in_char: in_char = False end = i # 宽度小于5个像素的视为噪声 if end - start >= 5: char_regions.append((start, end)) # 按位置从左到右排序 char_regions.sort(key=lambda r: r[0]) return char_regions这段代码的关键逻辑在阈值选择上。0和THRESH_OTSU连用是一种高级技巧——threshold函数会基于图像直方图自动计算最优阈值,替代硬编码的固定值。不同照片的明暗条件千差万别,固定阈值在暗光照片上会把字符糊成一片,Otsu在大多数场景下能自动找到区分前景和背景的分界点。
分割结果需要警惕两个问题:汉字可能被切成两半,因为“京”字的左右结构在投影上可能出现中间断开的假象;数字“1”和字母“I”太窄可能被当作噪声滤掉。实践中遇到第一种情况,常见做法是检查相邻字符间隔,如果间隔明显小于其他字符间距,就合并这两个区域;遇到第二种情况,把最小宽度阈值从5降到3,但这样也会引入更多虚假区域,需要后续用CNN的置信度来做二次过滤。
3.2 CNN推理:把字符图像转成模型输入张量
字符区域定好后,每张字符图要经过与训练时完全相同的预处理才能进模型。很多新手在这一点上翻车——训练时做了归一化,推理时忘了做;训练时图像尺寸是32x32,推理时直接resize成任意尺寸。这些都会导致推理结果完全不可用。
import torch from torchvision import transforms # 推理时的变换顺序必须与训练时一致,这是原则性问题 inference_transform = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) def predict_char(char_image, model, device): # char_image是numpy数组格式的单通道二值图 tensor = inference_transform(char_image).unsqueeze(0) tensor = tensor.to(device) model.eval() # 关键:切换到评估模式,关闭Dropout with torch.no_grad(): outputs = model(tensor) probs = torch.softmax(outputs, dim=1) confidence, pred = torch.max(probs, dim=1) return pred.item(), confidence.item()model.eval()这行很多人会漏掉。PyTorch默认是训练模式,Dropout层会随机丢弃神经元,同一个输入每次过模型结果都不一样。推理时如果忘了切到eval模式,你会在测试集上看到一种诡异的现象:同一张图识别两次,结果不一样。这个坑的血泪经验后面避坑章节还会再提。
transform的Normalize用了mean=0.5和std=0.5,对应把像素从[0,1]区间映射到[-1,1]区间。你可能会问为什么要做这一步,最直接的解释是:CNN训练时权重是在这个分布下收敛的,推理时输入分布不一致,权重就没法正常工作。
3.3 端到端整合:主流程代码与坐标可视化校验
把前面几块串在一起,整个系统的调用链路很清晰:读图 → 预处理 → 定位 → 分割 → 逐字符识别 → 拼接结果。
def recognize_license_plate(image_path, model, device): # 1. 预处理 morphed = preprocess_image(image_path) # 2. 定位车牌区域 plate_rect = locate_plate(morphed) if plate_rect is None: return None, None, "未检测到车牌区域" x, y, w, h = plate_rect original = cv2.imread(image_path) plate_img = original[y:y+h, x:x+w] # 3. 字符分割 char_regions = segment_characters(plate_img) # 4. 逐字符识别 result = "" confidences = [] for start, end in char_regions: char_img = plate_img[:, start:end] pred, conf = predict_char(char_img, model, device) result += str(pred) confidences.append(conf) # 5. 在原始图上画矩形框并标注结果,方便直观检查 cv2.rectangle(original, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(original, result, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) return original, result, min(confidences)这里最值得说的是最后一步的可视化输出。调试车牌识别系统时,如果只输出一个字符串,出错了你根本不知道错在哪一步——是定位框偏了、分割切错了,还是CNN分类错了。把矩形框和识别结果画回原图,一眼就能定位问题环节。这是我在调试视觉系统时的习惯做法:永远让中间结果可见,不要只输出最终结果。
4. 训练与调参:让CNN在自己的数据上收敛的关键配置
4.1 数据集组织与标签编码:从文件夹结构到One-Hot向量
CNN训练前要做的最基础的事情是数据整理。字符图片按类别放文件夹,文件夹名就是类别名,这是最不容易出错的方案。每个类别的图片数量要尽量均衡——如果“京”字有800张而字母“Z”只有50张,模型会严重偏向样本多的类别。
from torch.utils.data import Dataset from PIL import Image import os class CharDataset(Dataset): def __init__(self, root_dir, transform=None): self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {cls: i for i, cls in enumerate(self.classes)} self.samples = [] # 遍历每个类别文件夹,收集图片路径和标签 for cls in self.classes: cls_dir = os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith(('.png', '.jpg', '.jpeg')): path = os.path.join(cls_dir, fname) label = self.class_to_idx[cls] self.samples.append((path, label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert('L') # 统一转灰度 if self.transform: img = self.transform(img) return img, label标签编码方式这里没有直接做One-Hot,而是用整数索引class_to_idx,然后交给PyTorch的CrossEntropyLoss处理。CrossEntropyLoss内部会自动做one-hot形式的目标分布计算,手工做One-Hot反而多此一举还容易出错。Class_to_idx用sorted排序而不是直接用os.listdir的原始顺序,这是为了让类别索引在不同操作系统下保持一致——Windows和Linux的文件系统返回顺序不一样,不排序的话同一个类可能在两台机器上对应不同索引,训练好的模型换台机器推理就全错了。
4.2 训练超参数:学习率、批量大小、epoch与优化器选择
训练阶段有四个参数直接决定模型能不能收敛:学习率、batch size、epoch数和优化器。资源代码里的默认值一般是学习率0.001、batch size 32、epoch 30、Adam优化器。我给个实测观察,你根据自己的显卡显存来调。
import torch.optim as optim from torch.utils.data import DataLoader # 训练配置 num_epochs = 30 batch_size = 32 learning_rate = 0.001 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = LicensePlateCNN(num_classes=len(dataset.classes)).to(device) # Adam的默认参数betas=(0.9, 0.999)足够大多数场景 optimizer = optim.Adam(model.parameters(), lr=learning_rate) criterion = nn.CrossEntropyLoss() train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=2) for epoch in range(num_epochs): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() # 梯度裁剪:防止loss突变成NaN torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() running_loss += loss.item() avg_loss = running_loss / len(train_loader) print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}')学习率0.001是Adam优化器的经验安全值,高于这个值loss容易震荡,低于这个值收敛太慢。每轮训练打一次平均loss,如果看到loss在某个epoch后不再下降,说明模型容量到了上限,要么加卷积层,要么加数据量,改学习率基本没用。
梯度裁剪很多人不知道,但对这个小模型很重要。车牌字符图片里偶尔有极端噪声样本,一次异常梯度就能让权重飞到天上去,loss变成NaN再也回不来。clip_grad_norm_把梯度范数截断到1.0,相当于给反向传播加了安全阀,后续所有epoch的loss曲线都会平稳很多。
4.3 验证与混淆矩阵:准确率之外还要看哪些指标
训练集准确率98%不代表测试集也能到98%,所以数据要划出独立的验证集,常见比例是8:2或者9:1。验证集绝对不能参与训练,这是机器学习的铁律。资源里的报告模板大概率会要求你贴准确率曲线,但答辩时老师更可能问的问题是:哪几类字符最容易混淆?
from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt import numpy as np def evaluate_model(model, val_loader, class_names, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 计算混淆矩阵,行是真实标签,列是预测标签 cm = confusion_matrix(all_labels, all_preds) acc = np.mean(np.array(all_preds) == np.array(all_labels)) # 打印被错分最多的类别组合 print(f'Overall Accuracy: {acc:.4f}') for i in range(len(class_names)): for j in range(len(class_names)): if i != j and cm[i][j] > 0: print(f'{class_names[i]} -> {class_names[j]}: {cm[i][j]}次') # 画混淆矩阵热力图,答辩PPT里可以直接用 fig, ax = plt.subplots(figsize=(10, 8)) im = ax.imshow(cm, cmap='Blues') ax.set_xticks(range(len(class_names))) ax.set_yticks(range(len(class_names))) ax.set_xticklabels(class_names, rotation=45) ax.set_yticklabels(class_names) for i in range(len(class_names)): for j in range(len(class_names)): ax.text(j, i, cm[i][j], ha='center', va='center') plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')车牌字符识别里最容易混淆的几组是:“0”和“O”、“1”和“I”、“8”和“B”。汉字里“京”和“津”在分辨率低的时候也容易搞混。这些对拍照片的人(交警系统)可能不算大问题,但对你答辩来说,能主动说出“模型在哪类字符上表现弱、为什么弱、怎么改进”,比单报一个98%的准确率有说服力得多。
4.4 训练时常见翻车记录:loss不降、验证集准确率抖动
loss不降的最常见原因是学习率太大或数据没归一化。肉眼检查方法:把第一个batch的图片打印出来看,确认像素值范围是[-1,1]还是[0,255]。如果发现是[0,255],Normalize的mean和std设置铁定出问题了。验证集准确率抖动大,常见原因是batch size太小(比如4或8),梯度的方向噪声太大,增大到32或64一般会好一些。
验证集准确率比训练集低一大截,这说明模型过拟合了。毕设场景下数据量通常只有几百到一两千张,过拟合几乎必然发生。对策按优先级排:先加Dropout比例,从0.5提到0.7;再做数据增强,比如随机平移几个像素、轻微旋转、加椒盐噪声;最后才是考虑换更小的模型结构。
5. 避坑与排查:车牌识别项目最常见的五个翻车点
5.1 现象:OpenCV装好了但import cv2报ModuleNotFoundError
原因:Python环境中存在多个虚拟环境或Anaconda基础环境里的OpenCV装到了site-packages,但当前解释器指向的是另一个环境。很多人直接在系统Python里pip install opencv-python,然后又建了conda环境跑代码,两个环境的site-packages目录互不相通。
解决:先用which python和which pip确认当前命令指向的解释器路径,再用同一个python解释器执行python -m pip install opencv-python。我自己的习惯是项目一创建就建独立虚拟环境,所有依赖装在里面,不让它污染系统环境。
5.2 现象:cv2.error: OpenCV(4.4.0)报错,常见于读取视频或摄像头帧
原因:OpenCV在不同版本里对某些API的参数做了调整,4.x版本里VideoCapture的某些属性设置和3.x不兼容。另一类常见报错是imread返回None,路径里带了中文字符,OpenCV的imread在Windows上对中文路径支持不好。
解决:路径保持纯英文,图片放到images/这类简单目录下。读图后先加一行判断:
img = cv2.imread(path) if img is None: raise ValueError(f"无法读取图片: {path},请检查路径和文件名")这行代码能省掉大量排查时间,至少能快速排除是路径问题还是算法问题。
5.3 现象:训练后loss下降正常,但每个epoch跑完验证集准确率都在30%上下徘徊
原因:标签和类别索引没对齐。我在一个项目里遇到过这种情况——训练数据文件夹按字母排序,验证集却按字典序重新排了一遍,两个索引映射就错位了。更隐蔽的情况是数据增强过度,随机旋转角度设成了180度,字符都倒过来了。
解决:训练脚本里用统一的class_to_idx,不要在训练和验证阶段各自生成。数据增强的旋转角度控制在±10度以内,不要为追求“增强效果”把参数拉得过猛。先关掉数据增强跑一遍看baseline,再逐步加回来,这样能确认每一步对准确率的真实贡献。
5.4 现象:推理时同一张图片,每次运行识别结果都不同
原因:模型没有调用model.eval(),Dropout层在推理时仍然随机丢弃神经元。PyTorch默认模型在train模式,即便你只是做前向传播,Dropout也会产生随机性。
解决:推理前显式调用model.eval(),并用with torch.no_grad():包裹前向过程。net.eval()这行代码写进predict函数里,不要放在主流程里调用一次就以为万事大吉——如果代码里的某条分支在val模式下又触发了train(),随机性就回来了。
5.5 现象:车牌定位框把车灯、反光条也框进来,或者完全定位不到
原因:纯颜色分割在蓝色车牌上准确率还行,但在黄昏、夜间或车身贴了深色膜的场景下,车牌的蓝色特征会严重偏移。更常见的坑是轮廓筛选条件太窄,某些角度下车牌宽高比偏离3:1,面积占比也跌破1%,被过滤条件直接判死。
解决:把定位阶段的中间结果画出来检查——找一张模板图片,把findContours找到的所有候选轮廓全部画在原图上,看是候选太少还是筛选条件太严格。宽度比区间放到1.5到5.0,面积占比下限放到0.5%,多放几个候选进去,再用CNN做最终确认。车牌定位这事,宁可多框不可漏框,后续还可以用字符数来做二次校验——一个车牌应该切出7个或8个字符,少于这个数就说明定位或分割有问题。
6. 进阶用法:数据增强与模型集成,把准确率再往上推一截
车牌识别项目做到能跑通、能答辩,已经达到基本要求。但如果想让系统在更真实的场景下也站得住,有两个投入产出比很高的方向:数据增强和模型集成。
数据增强是应对数据量不足的第一手段。车牌字符图片的增强策略跟ImageNet那套不完全一样,字符识别对旋转和缩放最敏感,但对颜色变化不敏感——因为字符本来就是黑白二值图。我常用的增强组合是:±10度旋转、水平或垂直平移2个像素、0.9到1.1倍缩放。这些变换模拟的是拍摄角度轻微变化和车辆轻微移动的效果。要用PyTorch的transforms在训练时动态做,不要提前生成一万张增强图片存硬盘——训练时实时做省存储,还相当于每个epoch看到的是不同的数据,模型不容易背下来训练集。
另一种有效的增强是随机遮挡。车牌上常有泥点、反光斑,训练时在字符图中间随机抹掉一个小方块,模型被迫学会从残缺字符中提取特征。这个思路跟Cutout正则化一脉相承,实测对车牌识别这类局部特征任务提升很明显。缺点是不好解释给答辩老师听,如果导师比较传统,建议把随机遮挡说成“模拟车牌污损场景的数据增强手段”。
模型集成就更直接了——训练三个结构略有差异的CNN,比如一个三层卷积、一个四层卷积、一个加宽卷积通道的变体,推理时三个模型投票决定最终字符。单个模型在某个字符上可能犯错,三个模型同时犯同样错误的概率要低得多。代价是推理时间变成三倍,但毕设演示时一张图跑200毫秒和跑600毫秒,肉眼根本分辨不出来。集成用的三个模型不要从同一个随机种子训练出来——那样它们学到的东西几乎一模一样,集成没有意义。分别用42、2024、7这三个种子初始化,才能保证模型之间的差异性。
最后多提一句验证方法。不管你怎么折腾,最终都要回到一个朴素的习惯上:建一个固定的验证集,里面放二十张带有不同场景特征的车牌图——白天、傍晚、倾斜角度大、带泥点、汉字笔画复杂。每次改动完代码或训练完新模型,跑一遍这个验证集,把准确率和错例截图存档。我从那以后每次改完预处理参数都强制走一遍这个流程,哪怕只改了高斯滤波的核大小,也要把二十张图全过一遍。这样调参才有方向,答辩被问到“你这个参数是怎么定的”时,你能拿出对比数据而不是凭感觉。希望帮到你。
本文还有配套的精品资源,点击获取