ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

街景门牌号识别实战:基于深度卷积神经网络与CRNN的端到端方案

街景门牌号识别实战:基于深度卷积神经网络与CRNN的端到端方案 简介面向深度学习、机器学习、计算机视觉与模式识别领域的研究者和学生这份PDF专业文献聚焦自然场景中街景门牌号识别难题。针对背景复杂、字符风格多样、人工特征难以适用的问题作者基于AlexNet改进网络结构加深网络深度并增加卷积核数量在激活函数后引入批归一化BN于全连接层采用低比例Dropout策略同时将图像灰度化以弱化背景和光照干扰。在SVHN街景门牌号数据集上该网络训练约13小时识别率达94.58%在精度与训练成本之间实现了良好平衡。资源为单篇PDF共1个文件压缩包仅1.78MB包含论文全文、网络结构图、实验对比数据及参考文献可完整还原从数据预处理、网络设计到实验验证的整个研究过程。文中还梳理了CNN、深度学习、AlexNet、BN、Dropout、OCR、LeNet-5及GPU加速等关键知识点适合作为课程设计、科研入门或算法复现的参考资料。目前已有480人学习下载是相关领域学习者值得参考的专业资源。1. 街景门牌号识别为什么深度学习比传统OCR更靠谱街景门牌号识别说白了就是让算法看懂街景照片里那串门牌数字。过去用传统OCR在干净扫描件上还能跑一遇到倾斜、模糊、反光、遮挡的实拍门牌就翻车——阈值分割对光照敏感模板匹配扛不住形变字符切分在粘连数字面前直接哑火。基于深度卷积神经网络的街景门牌号识别方法把特征提取、序列建模和字符映射端到端塞进同一个网络在SVHN这类基准集上通常能到95%以上的准确率关键是它对真实街景的退化鲁棒性要好得多。这篇笔记不讲论文复现的虚话直接从任务拆解、模型搭建、训练调参到落地加速把每一步的参数和坑都摆出来适合正在做车牌识别、门牌识别、快递单号识别这类实拍数字识别项目的工程师和学生。2. 门牌号识别任务拆解从SVHN到真实街景的差异2.1 先搞清SVHN数据集与真实街景的边界SVHNStreet View House Number是从谷歌街景里裁剪出来的门牌号图片每张图含有1到5位数字。它的标注是每个数字的边界框和类别标签所以很多人把它当多目标检测或分类任务来做。但要注意SVHN里的图片已经经过预处理门牌号大致在画面中心背景干扰相对可控光照分布也相对均匀。真实街景完全不同——门牌可能出现在画面角落被树枝或车灯遮挡字体可能是艺术字墙面反光、阴影、夜间照明都会让数字纹理彻底改变。所以严格来说门牌号识别在工程上分两条路线一条是检测识别级联先用目标检测把门牌区域框出来再对区域做数字识别另一条是端到端序列识别直接输入整张街景图输出数字序列。后者对数据量和模型容量的要求更高但省去了检测框的误差传递。我一般建议如果你手里的街景图是全景扫描件门牌占比小且背景复杂优先用检测识别如果门牌已经裁切好或者门牌在画面中占据主要区域直接上端到端序列识别更省事。下面的代码是常见的数据预处理从SVHN的mat格式标注中提取数字序列和bounding box并生成训练用的图片列表。SVHN官方提供digitStruct.mat里面存了每个样本的bbox和label用Python读出来转成自己的格式。import h5py import numpy as np from pathlib import Path # 读取SVHN的digitStruct.mat # 该文件结构每个样本有name, bbox包含height,left,top,width,label def load_svhn_labels(mat_path): with h5py.File(mat_path, r) as f: names f[digitStruct][name] bboxes f[digitStruct][bbox] records [] for i in range(len(names)): # 逐样本解析处理单个数字和多个数字的存储差异 name .join(chr(c) for c in f[names[i]][()].flatten()) bbox_group f[bboxes[i]][()].flatten() bbox_list [] for item in bbox_group: if item.ndim 0: # 单个数字时是标量 item np.array([item]) for j in item: bbox_list.append({k: f[j][k][()].reshape(-1)[0] for k in [height,left,top,width,label]}) labels [int(b[label]) % 10 for b in bbox_list] # 10代表0 records.append({name: name, labels: labels}) return records records load_svhn_labels(digitStruct.mat) print(f样本总数: {len(records)}, 示例标签: {records[0]})这段代码的逻辑是h5py读取SVHN官方mat文件names和bboxes都是对象引用需要逐级解除引用。特别注意单个数字和多个数字的存储结构不同ndim 0时得手动包一层数组。label里用10表示数字0取模可以还原成0-9。参数说明mat_path是digitStruct.mat的路径返回的records列表里每个元素包含原始文件名和数字序列后续按name去图片目录里找对应png即可。2.2 门牌号识别是序列标注问题不是简单分类对一张包含 502 三个数字的门牌图如果只做单标签分类你得预定义所有可能的组合这显然不现实。正确做法是把它当序列标注输入图像输出一个不定长的数字序列。常用的建模方式有两种CTCConnectionist Temporal Classification和Attention。CTC不需要逐字符对齐标注只需要最终的序列标签所以训练省事Attention需要在训练时让模型逐步生成字符解码更灵活但容易漂移。工程上我倾向CTC尤其数字序列长度不超过5位时CTC的稳定性和速度都更优。既然要做序列识别图像输入就要有序列的形态。常见做法是把图片缩放成固定高度比如32宽度按比例缩放但不超过某个上限比如256然后沿着宽度方向把特征图切成一列一列的时间步。深度卷积神经网络在这里的作用是提取每列的特征向量再用循环网络BiLSTM建模列与列之间的上下文关系最后接入CTC损失。这个过程可以用一个公式概括CNN提取视觉特征 - 特征图按列展开成序列 - BiLSTM建模列间依赖 - CTC解码出数字串。在动手写模型之前先要把训练数据整理成统一格式。下面是一个轻量的数据加载器把图片高度固定到32宽度padding到固定值同时生成对应的标签序列和序列长度。import torch from torch.utils.data import Dataset from torchvision import transforms from PIL import Image class HouseNumberDataset(Dataset): def __init__(self, records, img_dir, fixed_height32, max_width128): self.records records self.img_dir img_dir self.fixed_height fixed_height self.max_width max_width self.tf transforms.Compose([ transforms.Resize((fixed_height, max_width)), # 高度固定宽度强制缩放 transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) self.char_map {str(i): i1 for i in range(10)} # 0-9映射到1-100位留给CTC blank def __len__(self): return len(self.records) def __getitem__(self, idx): rec self.records[idx] img Image.open(f{self.img_dir}/{rec[name]}).convert(RGB) img self.tf(img) labels [self.char_map[str(d)] for d in rec[labels]] label_len len(labels) return { img: img, labels: torch.tensor(labels, dtypetorch.long), label_len: label_len }这里有一个容易被忽略的参数Resize((fixed_height, max_width))直接把所有图压成 32x128。这样做的好处是张量形状统一方便batch训练坏处是长宽比失真尤其把细长门牌横向拉宽后数字会变形。更稳的做法是保持高度32宽度按原图比例等比缩放不够max_width的部分用0填充右边加padding这个坑我们放到第4章详细说。char_map里用0作为CTC的blank符号所以数字1-9映射到1-9数字0映射到10这样模型输出通道数设为1110个数字1个blank。3. 搭建深度卷积神经网络模型选型与训练配置3.1 从LeNet到CRNN门牌号识别的主流模型骨架深度卷积神经网络在门牌号识别上的进化路线很清晰最早是LeNet这种浅层CNN直接输出数字类别只适合单数字后来SVHN比赛带火了多数字识别大家开始用多个CNN分支或滑动窗口再后来序列识别思路普及CNNRNNCTC的组合成为标配也就是常说的CRNN结构。对街景门牌号来说CRNN比纯CNN多了一个关键能力它能建模数字之间的顺序关系。比如 12 和 21纯CNN把整张图当成一个整体特征容易混淆CRNN通过时间序列把左右位置的自然顺序编码进特征里准确率明显提升。卷积层怎么选我在实际项目里用过ResNet18、VGG16和MobileNetV3做特征提取结论是参数规模不是第一位的感受野和特征图宽度才是。门牌数字笔画细卷积核不能太大3x3足够网络深度至少要有4个下采样把32高图片缩到2x2或1x1的feature map否则序列长度不够。推荐一个实用的backbone配置4个卷积阶段每阶段两个3x3卷积加一个2x2 maxpool通道数从64翻倍到512。这样输入32x128的图最终特征图是4x32高4宽32序列长度就是32。循环层用BiLSTM还是GRU门牌号序列短用双向LSTM一层或两层都够。隐藏单元数一般取256或512。如果怕过拟合可以加dropout。关键参数是batch_firstTrue这样tensor形状是(batch, seq_len, feature_dim)方便和CNN输出对齐。CTC损失函数在PyTorch里是torch.nn.CTCLoss需要传入三个东西模型的logits、目标标签序列、每个样本的序列长度和目标长度。3.2 最小可复现训练脚本数据加载、损失函数与优化器下面这段代码是一个完整的CRNN训练循环PyTorch版注释里标了每个参数的推荐值和调整方向。它可以直接跑通SVHN数据集的子集但你要把它当成模板根据自己的数据规模改batch size和迭代轮数。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader class CRNN(nn.Module): CNN BiLSTM CTC 的门牌号识别网络 def __init__(self, num_classes11, hidden_size256): super().__init__() # 卷积特征提取输入3x32x128输出512x4x32 self.cnn nn.Sequential( nn.Conv2d(3, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 16x64 nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 8x32 nn.Conv2d(128, 256, 3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, (2, 1)), # 池化时不缩宽度保持序列长度 4x32 ) self.lstm nn.LSTM(256*4, hidden_size, bidirectionalTrue, num_layers2, batch_firstTrue, dropout0.3) self.fc nn.Linear(hidden_size*2, num_classes) def forward(self, x): x self.cnn(x) # [B, 256, 4, 32] b, c, h, w x.size() x x.view(b, c*h, w) # 合并高度和通道变成 [B, 1024, 32] x x.permute(0, 2, 1) # [B, 32, 1024] 序列长度32 x, _ self.lstm(x) # [B, 32, 512] x self.fc(x) # [B, 32, 11] return x # logits后面接CTCLoss # 训练参数设置 batch_size 32 epochs 20 learning_rate 1e-3 device torch.device(cuda if torch.cuda.is_available() else cpu) model CRNN(num_classes11, hidden_size256).to(device) optimizer optim.Adam(model.parameters(), lrlearning_rate, weight_decay1e-5) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) criterion nn.CTCLoss(blank0, zero_infinityTrue) # 假设 dataset 是上一节的 HouseNumberDataset dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue, num_workers4, collate_fncollate_fn) for epoch in range(epochs): model.train() total_loss 0 for batch in dataloader: imgs, targets, target_lengths batch[img].to(device), batch[labels], batch[label_len] logits model(imgs) # [B, T, C] log_probs logits.log_softmax(2).permute(1, 0, 2) # CTC需要[T, B, C] input_lengths torch.full((batch_size,), logits.size(1), dtypetorch.long) loss criterion(log_probs, targets, input_lengths, target_lengths) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 5.0) # 梯度裁剪防止LSTM梯度爆炸 optimizer.step() total_loss loss.item() scheduler.step() print(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(dataloader):.4f})这段代码的逻辑拆解如下CRNN前向时CNN输出特征图是[B, C, H, W]其中H4W32。为了把特征图转成序列把C和H合并成一个维度得到[B, 1024, 32]再permute成[B, 32, 1024]这样32个时间步每步1024维特征。BiLSTM双向输出是512维hidden_size256双向翻倍最后接线性层映射到11类。几个参数值得单独说。nn.MaxPool2d(2, (2, 1))是故意让高度缩减一半、宽度不变因为高度从8缩到4宽度保持32这样序列长度仍是32。如果你把宽度也缩了比如变成16序列长度就太短数字密集时会丢信息。CTCLoss的blank0必须和char_map里为blank预留的位置一致否则解码会错位。zero_infinityTrue的作用是当loss出现inf比如某样本目标长度大于输入长度时置零避免训练崩溃但这种情况应该靠数据检查杜绝。collate_fn没有写全实际需要把不同长度的标签序列pad到同一长度并记录真实长度。常见做法是用torch.nn.utils.rnn.pad_sequence加上左边padding并在标签前插入一个值比如-1供CTCLoss忽略。这里不再展开第4章会提到标签对齐的坑。学习率策略建议用warmup余弦退火。上面的脚本里直接用余弦退火但前几个epoch可能因为学习率过大导致振荡。一个更稳的做法是前3个epoch用线性warmup从1e-4升到1e-3之后再按余弦衰减。优化器选Adam没问题但weight_decay别太大1e-5足够太大会把卷积核的权重压得过小。batch size在显存允许范围内越大越好但BatchNorm的参数会受影响建议用32到64之间。4. 训练与调参避坑5条真实踩坑记录4.1 现象验证集loss不降精度卡在80%上不去最常见的原因是学习率设置不当。我见过有人用默认的1e-3跑SVHN一开始loss下降很快到了第5个epoch就开始震荡验证集准确率一直在80%左右徘徊。查下来发现是学习率太大模型在最优解附近来回跳跃。另一个原因是数据归一化不一致训练时用ImageNet的mean/std但推理时忘了转成相同预处理。解决方法是先跑两三个epoch观察loss曲线。如果loss在初始值附近波动剧烈把学习率降到1e-4或1e-5。如果loss下降极慢说明学习率太小适当上调。更系统的方式是使用学习率预热前三个epoch用1/10的目标学习率然后线性涨到目标值之后自然衰减。另外固定一套预处理pipeline训练和推理都使用完全相同的Resize和Normalize参数不要混用。4.2 现象模型永远输出空序列或只输出重复数字CTC训练时很容易出现模型倾向输出blank字符导致预测结果全是空的。尤其当训练集中很多图片只有一个数字而blank通道的概率远大于数字通道时模型学到的捷径就是全输出blank。另一个表现是输出一串相同的数字比如111而不是12。原因是BiLSTM的时间步之间相关性过强或者梯度在时间维度上传播不均匀。我当时的处理办法有三步第一步检查标签字符映射确认blank索引是0且数字标签从1开始第二步在损失函数上动刀CTCLoss的blank参数不能错同时可以用一个简单的正则项惩罚blank概率的平均值过大第三步调整模型结构把BiLSTM的隐藏单元数从512降到256增加dropout强迫模型不过度依赖时序记忆。还有一个经验如果训练数据里不同长度的样本分布不均尽量按长度分batch避免一个batch里全是单数字样本。4.3 现象训练时随机裁剪效果好但推理时结果变差我踩过最深的坑是数据增强和推理预处理不一致。训练时我用了RandomResizedCrop把门牌号随机缩放裁剪使得模型对尺度变化更鲁棒但推理时我用的是直接Resize((32, 128))把整张图压扁。结果训练指标93%推理时面对同样来源的图片只有85%差了一大截。原因很简单模型学到的特征对长宽比和绝对位置敏感推理时破坏了这种分布。解决方法是让推理和训练的尺寸策略统一。要么训练时也强制所有图片Resize((32, 128))放弃尺度增强要么推理时保持高度32宽度按比例缩放后padding到128。我推荐后者写一个inference_transform先用Resize只传高度再手动计算宽度并pad这样既保持了宽高比也兼容了batch输入。另外对门牌这种小字符水平翻转增强千万别用数字翻转后语义全变。4.4 现象整图输入模型小门牌根本认不出来如果输入图片是整条街景门牌号只占几十个像素上面那种整图识别模型基本会翻车。因为卷积网络下采样后小目标特征几乎丢失。有人试过把输入分辨率从32x128调成64x256精度提升有限显存却翻倍。正确的做法是先用目标检测定位门牌区域再对裁剪区域做识别。检测器可以用YOLO或Faster R-CNN门牌类别的anchor长宽比偏扁需要调整anchor尺寸。我做过一个方案用YOLOv8检测门牌置信度阈值设0.3NMS IoU阈值设0.5把检测框扩10%再送CRNN识别。这样整图识别准确率从70%涨到91%。代价是两阶段延迟大约多15ms但收益明显。如果你不想引入检测器可以尝试在CNN中使用特征金字塔FPN让浅层高分辨率特征图也能接触到最后分类但实现复杂度会上升不一定划算。4.5 现象SVHN上表现很好换真实街景数据就崩这是所有公开数据集训练的宿命SVHN虽然来自街景但已经过裁剪和筛选背景多样性远小于真实场景。我的真实测试集里有夜间长曝光、玻璃反光、绿色藤蔓遮挡、倾斜角度接近45°的门牌模型直接崩溃。这说明模型学到的特征是“SVHN风格”的不是通用的数字纹理。后悔药有三丸。第一丸是收集至少500张真实街景图片用半自动标注检测模型预标记人工修正加入训练集做finetune。第二丸是合成数据增强找一批不同风格的字体随机旋转、加模拟阴影、贴到真实街景背景上生成大量伪样本。第三丸是使用灰度图或边缘图作为额外输入通道减少对颜色的依赖。我最后靠75%真实25%合成数据把真实街景的精度从62%拉到了84%。别指望模型自己泛化数据上必须下功夫。5. 从单张图片到街景视频流推断加速与后处理技巧门牌号识别在实际产品里往往要处理连续视频流而不是单张静态图。我的经验是先做batch推断再把同一门牌在多帧中的识别结果做时序融合。CTC解码时常用贪婪搜索每帧输出一个数字序列但街道上车辆角度变化单帧可能截到残缺门牌。一种轻量做法是滑动窗口收集最近5帧的识别结果对每个位置的数字做投票票数最高且超过3票才输出否则等待下一帧。这个技巧能把误检率降低一半代价只是延迟5帧。模型加速方面CRNN的瓶颈通常在BiLSTM因为LSTM是串行计算。可以把它替换成BiGRU或直接用因果卷积TCN推理速度能快一截。如果精度损失可接受可以把模型导出成ONNX用TensorRT或OpenVINO做FP16量化。我在Jetson Nano上把CRNN从TensorFlow转到TensorRT后单帧推断从45ms降到18ms显存占用也降了一半。注意量化时对BatchNorm层的处理最好是先折叠BN再量化不然误差会放大。验证模型是否真正落地我有个习惯不看单张图准确率而是录一段真实街景视频跑一个端到端demo统计“门牌号连续5帧识别一致”的比例。这个比例比单帧精度更能反映产品体验。另外别忘了做置信度校准——CTC输出的概率往往过于自信稍微加一个温度系数temperature scaling会让阈值调节更靠谱。最后说个教训别急着优化模型结构先把数据管线搞干净。我见过太多人花一个月调网络最后发现是训练和推理的预处理不一致白白浪费时间。希望这篇笔记能帮你少走这几步冤枉路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表