首页 / 资讯中心 / 文章详情

街景门牌号数据集解析:从标注处理到OCR/文本检测实战

街景门牌号数据集解析:从标注处理到OCR/文本检测实战 ★ FEATURED ARTICLE
简介这份街景门牌号数据集专为计算机视觉与图像识别任务设计面向AI研究人员、算法工程师及参加相关竞赛的开发者可用于训练和验证门牌号自动识别模型支撑智能导航、自动驾驶、智慧城市等真实场景。包体共7个文件、约885MB包含3个zip图像压缩包、2个csv表格与2个json标注文件其中图像数据按训练、验证、测试划分便于隔离测试集避免过拟合csv清单与样例提交文件可辅助数据预处理和结果格式化json标注为模型提供门牌号位置与类别信息。配套数据加载思路能高效完成批量读取、归一化与裁剪等操作配合CNN等网络结构进行训练调参帮助使用者建立从数据准备、模型训练到精度评估的完整流程。目前已有519人学习尤其适合需要真实街景图像数据来做深度学习实战练习或算法比对的开发者。1. 街景门牌号数据集做OCR和文本检测前先把手上的标注数据吃透做文字检测的人最怕的不是模型不收敛而是手上没有带边界框标注的真实街景图。街景门牌号数据集恰好就是这类“解剖级”的数据每张图像都是从街景画面里裁出来的门牌号区域上面有1到5位数字伴随一份精确到像素的边界框标注。拿它入门OCR、做文本检测、或者验证目标检测模型的锚框设计都比用合成图来得踏实。这份资源解决的是“有标注的街景文本数据从哪来”的问题适合刚接触检测任务的开发者也适合想快速验证模型结构的熟手。数据量大、标注完整是少数下载下来就能直接进训练流程的开源资源之一。2. 数据集内部结构与标注格式把 mat/h5 文件变成能直接喂给模型的样本2.1 目录结构train、test、extra 三份数据的定位差异解压后你会看到三个子目录和几个标注文件常见版本的结构如下表所示。不同渠道打包的名称可能略有差异但核心就是这三份数据和一个总标注文件。子集常见图像数标注质量典型用途train7万级人工标注相对干净主训练集模型的主体来源test2万级人工标注质量稳定固定评估集报告 mAP 和正确率extra50万级自动标注噪声较多扩充训练建议先清洗再合并这张表值得多看两眼。很多开发者第一次打开压缩包直接把三个目录扔进训练脚本结果要么 epoch 时间翻倍要么指标不升反降。问题往往出在 extra 子集上它的标注是程序自动生成的漏标和错标比例高于 train。我一般会先把 extra 放一边等主干模型在 train 上跑通、能输出候选框之后再用模型去清洗 extra 的标签最后才合并训练。顺序反了血泪经验告诉你指标大概率会很难看。2.2 标注格式拆解digitStruct.mat 里到底存了什么标注文件通常叫 digitStruct.mat是一个 MATLAB 格式的结构体数组。顶层是一个数组每个元素对应一张图像每个元素包含两个字段name 和 bbox。name 是图像文件名bbox 又是一个结构体里面有五个字段——height、label、left、top、width。这里就是第一个“玄学”集中地。五个字段的值可能是标量也可能是数组。当图像上只有一个数字时这些字段是单个数值当门牌号是“325”这种三位数时每个字段就成了 1x3 的数组。用 scipy.io.loadmat 直接读默认返回的是嵌套的 object array很多人到这里就懵了打印出来一堆 array([[[...]]], dtypeobject)根本不知道如何下手。第二个坑藏在坐标的坐标系里。left 和 top 表示包围盒左上角相对图像左上角的像素偏移width 和 height 是框的宽和高单位都是像素。但部分版本导出的标注里这些值可能已经被归一化过也就是除以了图像宽高。你拿到手的第一件事就是用 PIL 读一张图把 left、top、width、height 打印出来和图像尺寸对比确认是绝对像素值还是相对比例。2.3 解压后第一件事校验图像与标注能否对上不要急着训练先把数据校验脚本跑一遍。这个脚本的目标很简单读标注文件的头部样本检查框是否越界、文件名是否存在、数字标签是否在 0 到 9 之间。from pathlib import Path from PIL import Image import numpy as np from scipy.io import loadmat raw loadmat(digitStruct.mat, simplify_cellsTrue) items raw[digitStruct] bad 0 for i, item in enumerate(items[:5000]): name str(item[name]) bbox item[bbox] img_path Path(train) / name if not img_path.exists(): print(f[missing] {name}) bad 1 continue w, h Image.open(img_path).size lefts bbox[left] # 兼容标量和数组两种情况 lefts [lefts] if np.isscalar(lefts) else np.atleast_1d(lefts).tolist() tops [bbox[top]] if np.isscalar(bbox[top]) else np.atleast_1d(bbox[top]).tolist() widths [bbox[width]] if np.isscalar(bbox[width]) else np.atleast_1d(bbox[width]).tolist() heights [bbox[height]] if np.isscalar(bbox[height]) else np.atleast_1d(bbox[height]).tolist() for left, top, width, height in zip(lefts, tops, widths, heights): if left 0 or top 0 or left width w or top height h: bad 1 if bad 10: print(f[bounds] {name}: box({left}, {top}, {width}, {height}), img({w}, {h})) print(fchecked 5000 samples, bad{bad})这段代码的重点在 np.isScalar 和 np.atleast_1d 的兼容处理。MATLAB 结构体单值字段在 loadmat 后可能是 0 维数组直接取下标会报错统一转成列表才是稳妥做法。校验逻辑只有一条——框必须完整落在图像范围内。越界框如果不处理后续转 YOLO 格式时负坐标和超出边界的宽高会让 loss 直接变成 NaN。3. 把标注转成可训练样本解析脚本、坐标归一化与可视化验证3.1 解析 digitStruct.mat一个能处理单数字和多数字框的脚本抓握数据结构之后需要写一个完整的解析函数把每条记录转成 Python 字典方便后续 train/test/extra 三个子集统一处理。这里用 simplify_cellsTrue 可以让 loadmat 返回嵌套 dict 而不是可怕的 object array省掉很多解引用操作。import numpy as np from scipy.io import loadmat def load_digit_struct(mat_path): raw loadmat(mat_path, simplify_cellsTrue) items raw[digitStruct] records [] for item in items: bbox item[bbox] name str(item[name]) def to_list(v): # 兼容标量、一维数组、二维数组三种情况 if np.isscalar(v): return [float(v)] arr np.atleast_1d(np.squeeze(v)) return [float(x) for x in arr] labels to_list(bbox[label]) lefts to_list(bbox[left]) tops to_list(bbox[top]) widths to_list(bbox[width]) heights to_list(bbox[height]) boxes list(zip(lefts, tops, widths, heights)) records.append({ name: name, labels: [int(x) for x in labels], boxes: boxes, num_digits: len(labels), }) return records train_records load_digit_struct(digitStruct.mat) print(floaded {len(train_records)} records) print(train_records[0])代码里最关键的 to_list 函数解决了大部分解析异常。bbox 里的字段在 MATLAB 里可能是 1x1、1xN 甚至 Nx1 的矩阵loadmat 转过来维度常常带多余的一层np.squeeze 先去掉单维度再 atleast_1d 保证能循环。labels 里可能出现 10它表示数字 0因为原始数据集用 label10 代表数字 0、用 label1 到 9 代表 1 到 9。如果不做转换直接拿 10 训练分类头就会多出一个永远不会出现的“第 11 类”。3.2 坐标转换与归一化绝对像素、相对比例与 YOLO 格式检测模型通常需要相对坐标YOLO 格式要求的是 cx、cy、w、h并且全部归一化到 0 到 1。直接把 left、top、width、height 除以图像宽高即可。这个步骤本身不难难的是确认图像尺寸。一定要用 PIL 打开原图拿宽高不要用训练时 resize 之后的尺寸。from PIL import Image def normalize_boxes(record, image_dir): img Image.open(f{image_dir}/{record[name]}) img_w, img_h img.size norm_boxes [] for left, top, width, height in record[boxes]: cx (left width / 2.0) / img_w cy (top height / 2.0) / img_h bw width / img_w bh height / img_h norm_boxes.append((cx, cy, bw, bh)) return norm_boxes, img_w, img_h这段函数返回的元组顺序是 cx、cy、bw、bh对应 YOLO txt 里的一行目标。注意 left、top 在原始标注里可能是 float 类型不要随手 int() 截断否则框会整体偏移 1 到 2 个像素。门牌号数字通常只占图像高度的 10% 到 30%1 个像素的偏移在缩放后会被放大最终影响定位精度。3.3 可视化验证画框看标注把“以为对”变成“看得到对”解析和归一化做完之后必须做一次可视化验证。这一步相当于给标注数据做“体检”能一次性发现坐标坐标系错误、标签错位、多框错乱等问题。from PIL import Image, ImageDraw import matplotlib.pyplot as plt def visualize_record(record, image_dir, axNone): img Image.open(f{image_dir}/{record[name]}).convert(RGB) draw ImageDraw.Draw(img) for (left, top, width, height), label in zip(record[boxes], record[labels]): draw.rectangle( [left, top, left width, top height], outline(255, 0, 0), width2 ) draw.text((left, max(0, top - 12)), str(label), fill(0, 255, 0)) if ax is None: plt.imshow(img) plt.axis(off) plt.show() else: ax.imshow(img) ax.axis(off)我一般会随机抽 100 张图把 train 和 extra 各画 50 张按记录顺序排成 10x10 网格。重点看三类现象框是否紧贴数字、多位数框之间是否重叠、数字标签是否和框内内容一致。如果框把整个门牌背景都包进去了说明标注框生成策略偏保守训练时可以用略微收缩框的增强策略来补偿。如果框之间大面积重叠说明多位数字被过度切分这类样本在检测阶段需要 NMS 后合并才能正确解码。3.4 转成 YOLO/VOC 目录结构从原始包到工程组织为了直接对接训练框架常见做法是组织成如下目录结构dataset/ ├── images/ │ ├── train/ 00001.png 00002.png ... │ ├── test/ 00001.png ... │ └── extra/ 00001.png ... ├── labels/ │ ├── train/ 00001.txt 00002.txt ... │ ├── test/ 00001.txt ... │ └── extra/ 00001.txt ... ├── train.txt ├── val.txt └── test.txt其中 labels 下每个 txt 文件和图像同名每行记录一个框格式是“类别 cx cy w h”。这里类别就是数字本身0 到 9 共 10 类。转换脚本只有十几行把 normalize_boxes 的输出格式化写文件即可。def to_yolo_line(class_id, cx, cy, bw, bh): return f{class_id} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f} def convert_record(record, image_dir, label_dir): norm_boxes, img_w, img_h normalize_boxes(record, image_dir) label_path f{label_dir}/{record[name].rsplit(., 1)[0]}.txt with open(label_path, w) as f: for box, label in zip(norm_boxes, record[labels]): cls 0 if label 10 else label f.write(to_yolo_line(cls, *box) \n)逻辑不复杂却有两个值得注意的参数。一是文件命名的扩展名替换图像是 .png标签就用同名 .txt不要用图像全名加 .txt 后缀否则训练框架读取时容易重复拼接扩展名。二是 cls 0 if label 10 else label 这一行的处理数字 0 在分类时映射为类 0类别 1 到 9 对应数字 1 到 9这样 10 分类才能完整覆盖 0 到 9 的所有门牌号。4. 实战用这份数据集训练一个门牌号检测与识别模型4.1 自定义 Dataset 加载器图像增强和标签整理一起做转好格式后直接写一个 PyTorch Dataset。因为门牌号最多 5 位数字每张图的目标数不是固定的加载器要把框补齐到统一长度否则 collate 阶段会报错。import torch from PIL import Image from torch.utils.data import Dataset MAX_DIGITS 5 class StreetNumberDataset(Dataset): def __init__(self, records, image_dir, img_size(96, 96), transformNone): self.records records self.image_dir image_dir self.img_size img_size self.transform transform def __len__(self): return len(self.records) def __getitem__(self, idx): rec self.records[idx] img Image.open(f{self.image_dir}/{rec[name]}).convert(RGB) orig_w, orig_h img.size boxes torch.zeros((MAX_DIGITS, 4), dtypetorch.float32) labels torch.full((MAX_DIGITS,), 10, dtypetorch.long) for i, (box, label) in enumerate(zip(rec[boxes], rec[labels])): if i MAX_DIGITS: break left, top, width, height box cx (left width / 2) / orig_w * self.img_size[0] cy (top height / 2) / orig_h * self.img_size[1] bw width / orig_w * self.img_size[0] bh height / orig_h * self.img_size[1] boxes[i] torch.tensor([cx, cy, bw, bh], dtypetorch.float32) labels[i] 0 if label 10 else label if self.transform is not None: img self.transform(img) return img, boxes, labels参数上有几处需要解释。boxes 的填充值是 0labels 的填充值是 1010 在分类头里表示“无数字”。配合一个自定义的 collate_fn 即可完成 batch 组装。resize 到 96x96 是为了训练速度但真实推理场景门牌号往往更小最后评估时用 192 或 256 效果会好一截。增强方面建议用 albumentations 的 Compose 配合 BBoxParams纯 torch 的 transforms 无法同步变换框这是很多人训练到一半发现检测框漂移的根源。4.2 网络选型检测分支与数字分类分支怎么搭方案主干输出设计适用场景方案AYOLOv8n 这类通用检测框架10 类检测直接回归框和类别想快速跑通、部署成熟场景方案BResNet18 主干 自定义头部回归 5 个框 每框 11 类分类想练手、追求轻量方案CCNN 序列解码按位输出数字序列对连续多位数鲁棒性好方案 A 最省事把前面转好的 YOLO 格式数据直接丢进去训练。方案 B 更适合理解“检测 定位 分类”的本质主干共享特征一个头回归框坐标一个头输出 11 分类0 到 9 加“无数字”。方案 C 本质是 CRNN 的变体适合门牌号长度固定、数字紧凑的场景但实现复杂度高一些。我一般会先用方案 B 跑通数据管道因为它的损失函数好拆定位和分类的梯度可以分别观察。主干可以用 ResNet18检测头输出 5x420 个数值分类头输出 5x1155 个 logits两个头都接在全局平均池化后面。注意这里把门牌号当成“最多 5 位、位置固定顺序”来建模对图像中数字从右到左的情况容易退化大规模投影时要加一个方向校正逻辑。4.3 训练参数与效果评估学习率、锚框尺寸与指标训练参数上数据集的特殊性在于目标小、数量多。以下是几个常见的推荐起点实际效果会随数据版本和图像分辨率浮动。参数推荐值说明输入分辨率96x96 热身最终 192x192门牌数字占图比例小分辨率太低会丢细节学习率1e-3AdamWwarmup 5 epoch用 SGD 时为 1e-2 起步但收敛慢批量大小32显存不足降到 16配合梯度累积也可以锚框按数据集聚类k6不要沿用 COCO 的默认框目标尺寸差异大训练轮次主干冻结 10 epoch 再解冻 20 epoch先让检测头稳定再微调主干损失函数定位用 CIoU分类用 CrossEntropy分类里有大量“无数字”填充需要类权重评估指标mAP0.5 和数字序列准确率前者看检测后者看整体门牌识别是否正确锚框这一点值得展开。COCO 数据集的锚框偏大而这组数据里数字框普遍又小又扁。直接用默认锚框常见的翻车现场是两位数字只检出一个框或者三位数中间的某个数字被漏掉。解决办法是拿训练集所有框宽高做一次 k-means 聚类把 k 设置为 6让锚框匹配实际目标分布。数字序列准确率是比 mAP 更严格的指标它要求图像中每个数字都被正确检测且分类正确才算整张图正确最终评估时两个指标都要报。5. 避坑与排查街景门牌号数据集上的五个高频问题与处理方案5.1 画框颜色发蓝数字和背景全部“掉色”现象用 OpenCV 的 imread 读图后画框可视化发现数字颜色与原始街景照片明显不同红色框也变成了偏蓝的色调。原因数据集里的 PNG 是 RGB 通道顺序而 cv2.imread 默认以 BGR 读取。直接把 BGR 数据交给 matplotlib 显示红蓝通道互换看起来像整体掉色。更隐蔽的是如果用 cv2 读图去算图像均值做归一化通道顺序错误会影响模型训练收敛。解决读图统一走 PIL或者 cv2 读完后加一行 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。在训练脚本里最好固定一个图像读取模块不要让 train 脚本用 PIL、验证脚本用 cv2这种不一致往往要等到模型部署时才暴露出来。5.2 框整体偏移坐标和图像错位现象可视化时框画出来了但位置明显偏离数字区域向左下角或右上角偏移了固定的一段距离。原因最常见的是坐标系理解错误。left、top 是相对图像左上角的原点但有些版本在导出标注时把 top 坐标换成了 bottom 坐标或者导出时做了翻转。另一种常见原因是 loadmat 后字段顺序搞错把 height 当成了 top 来读取。解决先随机挑 5 张图把原始 left、top、width、height 打印出来和图像宽高对比。如果 left 接近 0 且框还在画面内说明坐标体系正常如果所有 left 都整体多了几十像素说明还有偏移量要减掉。校正完再重新可视化验证一遍。5.3 三位数门牌漏检成两位mAP 卡在 70% 上下现象模型训练后 mAP0.5 在 70% 附近徘徊查看具体样本发现 325 这类三位数字只检测出了两个框中间数字被吞掉。原因锚框与目标尺寸不匹配是主因。门牌号的数字框宽度和高度比例比较极端三位数框宽高比可能超过 3:1默认锚框无法覆盖。其次输入分辨率太低resize 到 96x96 后单个数字可能只有 10 像素宽特征图上的响应太弱。解决第一步做锚框聚类把 k 增大到 6 或 8第二步把输入分辨率提到 192 以上第三步在损失函数里对高宽比超过阈值的框单独加权。三步都做了mAP 通常能提升到 80% 以上。如果还卡着就去检查 NMS 阈值是否太高把多个重叠框合并成一个大框了。5.4 合并 extra 子集后指标反而下降现象把 extra 子集和 train 合并训练数据量涨了 5 倍以上mAP 不升反降训练日志里的 loss 也在震荡。原因extra 子集的标注是自动生成的漏标和错标比例高。模型学到这些错误标注后相当于在一堆“假标签”上过拟合自然带偏梯度。某开发者把 extra 全部灌进去后验证集上多检出了大量背景框就是这个原因。解决采用两阶段清洗。先用 train 训练一个基线模型对 extra 做推理把每个标注框和模型预测框的 IoU 算出来IoU 低于 0.3 的样本直接剔除IoU 在 0.3 到 0.7 之间的人工复核一批确认策略后再批量过滤。清洗后 extra 的样本量可能减少 10% 到 20%但训练曲线会稳很多。5.5 数字“4”和“9”、“7”和“1”识别混淆现象检测框位置很准mAP 也不错但数字序列准确率低了几个点混淆矩阵里 4 和 9、7 和 1 大量互认。原因这些数字在门牌字体下形态接近加上图像模糊、透视畸变单靠整图分类头很难区分。另一个原因是标签 10 表示 0训练时如果样本数少类别分布不均衡分类头会倾向于预测高频数字。解决分类阶段按框裁剪数字区域做自适应二值化后再进分类头相当于把“端到端识别”拆成“检测 预处理 分类”三段。同时在损失函数里给低频数字加类别权重或者用 Focal Loss 让模型多关注难分样本。最后加一个先验约束门牌号长度在 1 到 5 位之间解码时用字典约束掉不可能的序列组合。6. 进阶玩法把街景门牌号数据用到极限的几种验证与增强思路6.1 用 extra 子集做预训练再用 train 微调如果不想清洗 extra还有一种折中用法先在 extra 上做无监督预训练比如用对比学习让主干学到街景数字的通用特征再拿 train 的干净标注做 10 轮微调。这个方案对显存要求更低也能规避 extra 标注噪声问题。我在实际实验里预训练后用 train 只微调 5 轮效果比直接 train 训 30 轮还稳定。关键点是预训练阶段的图像增强要强一些随机裁剪、旋转、颜色扰动都开让主干对街景光照变化不敏感。6.2 按数字位数分组微调另一种提升手段是把训练样本按 num_digits 分成 1 到 5 位五组先训一个全量模型再对 3 位和 4 位门牌号单独微调最后几层。因为实际街景门牌号大量集中在 2 到 4 位这个分布不均衡靠普通训练很难学到位。分组微调后我通常会让分类头单独跑几个 epoch学习率降到 1e-5 以下避免破坏前面学到的检测特征。6.3 一套通用的增强顺序给后来者一个直接可用的增强配置先做几何变换再做光学变换。几何包括水平翻转、±15 度旋转、±10% 缩放光学包括高斯模糊、亮度扰动、随机遮挡。顺序必须是先几何后光学否则模糊后的图像再做旋转会产生不必要的插值噪声。增广强度上训练初期用弱增强第 15 个 epoch 后增强强度逐步拉满模拟大模型训练里的退火策略。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.7), A.GaussianBlur(blur_limit(3, 7), p0.3), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), ], bbox_paramsA.BboxParams(formatpascal_voc, min_visibility0.3))参数上GaussianBlur 的 blur_limit 不要超过 7门牌数字本来就细模糊太狠会把笔画糊成一团。RandomBrightnessContrast 的 brightness_limit 在 0.2 以内户外光照变化剧烈但还不至于把暗部细节完全压掉。min_visibility0.3 的意思是增强后如果框被裁剪到可视面积不足 30%就丢弃这个目标避免模型拿着一堆残缺框训练。从那以后我每次拿到带标注的街景数据都会先抽 100 张图做可视化校验再决定要不要训练。这个习惯帮我躲过了至少三次坐标错位和标签错位的坑也让我对 extra 这类自动标注子集始终保持警惕。数据集的解析脚本、转换脚本和训练参数都整理在压缩包里解压后按文章顺序跑一遍就能看到效果。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站