ARTICLE DETAIL

资讯详情

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

全志开发板部署LPRNet车牌识别:数据集制作全流程指南

全志开发板部署LPRNet车牌识别:数据集制作全流程指南 先说个结论在嵌入式开发板上做车牌识别最难的反而不是模型训练而是训练数据和设备实际看到的数据根本不在一个分布里。我自己在全志V3s上跑LPRNet时就栽过跟头——PC上测试精度看着还行一上板子摄像头天色一变、角度偏一点、车牌有点遮挡识别率直接掉到没法用。后来复盘发现问题不在模型结构在我拿去做训练的数据集和开发板真实输入之间差了太多。所以这个系列的第一篇我打算先把数据集制作的完整思路整理出来。毕竟这是整个链路的地基数据不对后面训练、量化、移植做得再漂亮都是白搭。这篇文章会围绕全志开发板部署LPRNet这个目标把采集规范、标注方式、合成数据方法、格式转换、质量校验这些环节逐一讲清楚。适合正准备在V3s、T113、H618这类板子上做车牌识别但还没想明白数据怎么做的朋友参考。1. 为什么是LPRNet全志开发板的算力与模型选型逻辑1.1 全志开发板的性能边界全志这个家族的开发板有个共同特点便宜、集成度高、但算力比主流手机SoC差一大截。做部署选型之前必须先把板子的性能边界搞清楚否则后面对数据处理方式的要求就无从谈起。型号核心典型规格部署定位V3s单核Cortex-A7主频约1.2GHz内置64MB DDR2低端方案适合单路识别内存紧张T113双核Cortex-A7主频约1.2GHz常见模组带128MB DDR3比V3s稍宽裕仍属于低预算场景H618四核Cortex-A53主频约1.5GHz外置DDR可跑完整Linux可以承载识别加简单业务逻辑F1C200sARM926EJ-S主频较低资源极有限基本只能跑极简任务LPRNet很吃力V3s是典型代表单核A7、内置64MB内存。这种板子跑LPRNet前向推理不是不行但你不能指望它同时跑一个大目标检测网络加一个大识别网络。我的做法是识别部分用LPRNet前面接一个轻量车牌检测器两边都做INT8量化。这种场景下训练数据必须跟量化后的模型行为对齐后面我会专门说校准集怎么做。1.2 LPRNet的结构特点决定了数据集规范LPRNet的完整名称是License Plate Recognition Network它的核心思路是不做字符分割直接把整张车牌图映射成一个字符序列。主干网络是卷积层堆叠后面接CTC解码有些变体会加轻量循环层。因为去掉了字符分割这一步它对字符粘连、倾斜、模糊的容忍度明显更高这对嵌入式场景太重要了——摄像头拍到的车牌很少有端端正正的。这个结构对数据集有一个直接影响样本标签必须是完整的车牌字符串而不是每个字符的边界框位置。也就是说我们标注时写的是“京A12345”不是告诉模型“文字在x12到x28位置”。LPRNet的CTC训练方式天然支持变长序列所以训练数据可以有一些车牌字符间距不一样的样本这反而能提升泛化能力。但如果标签里没有字符级别的信息你就不要指望模型能学会精确的字间关系。1.3 数据集在整条部署链路里的位置很多人把数据集制作当成“先找一堆车牌图片跑一下训练就完事”。实际上在全志开发板部署这个具体目标下数据集制作要提前考虑三件事板子上的预处理能力有限输入尺寸通常会被压缩到很小比如24像素高的灰度图摄像头采集到的噪声、压缩伪影、光照变化需要训练数据里就有类似分布后期INT8量化需要一份覆盖各字符类型的校准数据集。也就是说数据集不是给模型“喂饱”就结束它还要为部署侧的量化、预处理、边框裁剪服务。这个思路贯穿整个系列本篇文章先解决数据怎么来、怎么标、怎么组织的问题。2. 动手做数据前先定死LPRNet的输入规范2.1 固定高度24像素背后的逻辑是什么LPRNet对输入图像有一个约定俗成的处理方式高度固定为24像素宽度可以变化大多数工程实现为了省事直接固定为94像素。24这个数字不是随机定的它是网络深度和特征图分辨率之间的折中——24像素高的图经过几层步长为2的卷积后特征图高度还能维持在几个像素级别既保留序列特征又不会让计算量爆掉。我在全志开发板上实测如果输入从24x94提升到32x128前向推理时间大概会增加30%到50%。对于V3s这种单核A7来说这个差距已经足够影响实用帧率。所以制作数据集时所有样本无论原始来源是1080p抓拍还是500万像素手机图最终都要统一裁剪并缩放到24x94左右。这里有个容易被忽视的细节缩放不是直接拉伸要按车牌区域的长宽比做resize。直接拉伸会让字符比例变形模型学到的是变形的字形特征遇到正常视角的车牌反而会误判。我的做法是从采集图中裁剪车牌区域时保持原始的宽高比先resize到高度24然后中心裁剪或padding到宽度94。2.2 字符集设计不是所有常用字都要进字符集车牌识别字符集的设计直接决定分类头的输出维度也决定数据生成的随机规则。以国内普通蓝牌为例最常见的基础方案是省份简称汉字京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新共31个字母字母表有26个但车牌上基本不出现O和I因为这两个字和数字0、1实在太容易混淆。所以有效字母是24个数字0到9共10个。合起来就是31加24加10共65类。如果你的场景涉及使领馆牌照、警用牌照、新能源牌照字符集会有些差异。新能源绿牌的结构和蓝牌不太一样字符集通常还是一样的字母数字汉字但颜色和排版不同。我的建议是第一版先做65类把管道跑通后续再按业务需要扩充。设计字符集时还有一个经验不要迷信“越多越全”。有人为了保险把什么“使”“领”“学”全塞进去结果训练数据里这些字符出现频率极低模型对这些类的拟合基本靠猜反而拉低了整体精度。低频字符要么不做要么单独准备充足的样本。这一点在后面的数据均衡部分还会展开。2.3 标签到底标什么LPRNet的标签是整串字符比如“京A12345”这样一整个label而不是目标检测里的边界框。如果你手头的现成数据集只有车牌框标注它可以直接用来训练检测器但不能直接用来训练LPRNet必须重新标注字符序列。有一种情况比较讨巧你可以从真实车牌图片里通过字符分割算法半自动生成标签。比如先用连通域分析把字符区域切出来再用单字符识别模型打底人工校对一遍。但这个方案在模糊、倾斜样本上错误率偏高人工校对量并不少。我实际用的更多的是“文件名当标签”的方式后面在第5节详细说。3. 真实样本采集从全志开发板到数据集的通路3.1 摄像头接口与采集方式全志V3s、T113这类板子通常带CSI接口可以接OV2640、OV5640之类的裸摄像头模块H618开发板则更灵活USB免驱摄像头也能用。采集真实样本时我的建议是直接用开发板本来的摄像头去拍而不是用手机拍完再传进电脑。原因很简单开发板往上的视频流经过了特定的ISP处理、缩放、JPEG压缩这些信号损失和PC端摄像头完全不同。训练数据里如果有这些痕迹模型上板之后才能对齐。采集分辨率不需要太高720p甚至VGA已经足够。车牌识别在部署时一般会先做检测裁剪车牌在整幅画面里占比不会很大盲目追求高清只会让后端处理负担加重而且采集下来的大图转小图时还会引入额外的缩放噪声。我自己习惯把采集帧保存为JPEG质量85左右刻意保留压缩伪影。3.2 视频抽帧与车牌区域裁剪管道真实样本不是一张张按快门拍出来的最靠谱的办法是录一段视频然后抽帧。我常用的流程是在开发板上用v4l2接口连续抓帧或者直接录MP4每5到10帧抽一帧保存抽出的帧先用一个轻量检测模型或用程序按车牌颜色特征粗定位车牌区域人工把每张检测结果里的车牌区域裁剪并保存裁剪时保留车牌四周10到20像素的背景余量避免把字符顶在图像边缘。裁剪这一步有个坑很多车牌周边有螺丝、边框、装饰线如果裁剪区域包含大量非车牌元素LPRNet会把这些背景当成特征的一部分。训练数据里如果背景杂乱模型在部署时遇到类似背景可能会误激活。所以真实样本尽量保持裁剪一致性要么都有少量余量要么都精确切到车牌边界。3.3 真实样本的预处理与存档格式所有真实样本收集完成后统一做一次预处理管道读取原始裁剪图按车牌长宽比resize到高度24宽度按比例得到大约94左右转成灰度图保存为PNG或高画质JPEG把对应的车牌字符串作为标签写入一个CSV或保存为文件名。关于通道问题多说一句。LPRNet原论文用的是灰度输入但不少开源实现会直接喂RGB三通道。我的处理方式是存档时保存彩色图训练脚本里再统一转灰度。这样后面如果想对比“灰度输入和RGB输入哪个在全志板子上精度更好”不需要重新做数据集只需要改训练脚本。在这里把彩色原图保留下来未来还可能出现彩色增强数据的需求不要一次性把信息丢了。4. 合成数据扩充用程序制造数万张可用样本4.1 字符级渲染生成的基本方法真实样本的量通常不够尤其是一个城市场景下车牌字符分布极不均匀。常见的操作是用合成数据补量。合成数据不是说在蓝底上把字排上去就行那样模型会学到“人造车牌”的纹理泛化能力很差。我的做法是字符级渲染加物理扰动。先准备一张空的车牌画布以普通蓝牌为例底色是蓝色渐变字符是白色。用PIL的ImageDraw或者OpenCV绘制但不要用默认字体要去下载几个开源的中文字体文件比如思源黑体或者文泉驿系列按车牌字形的长宽比例稍微变形。车牌字体通常比较窄长直接用系统黑体字往往偏宽。生成一个字符的流程随机选一个省份汉字、一个字母作为发牌机关代码、再随机生成5位字母数字序列拼成一个合法车牌字符串逐个字符渲染成小图像按车牌排版间距拼接在画布上对“车牌底板”的颜色、纹理做随机扰动不要一种蓝色用到底保存整图时同时把字符串标签写入标签文件。这里的一个关键点是随机规则要合法不能出现完全不合规的车牌组合。比如第一位省份汉字后的字母其实是发牌机关所在地代码在真实世界里不是所有字母都会出现。如果合成数据里随机生成大量不存在的组合模型会学到不存在的特征偏差部署时看到真实车牌反而困惑。虽然不要求合成数据绝对合法但至少要限制在常见规则范围内。4.2 透视变换与背景融合让合成图看起来像摄像头拍的合成车牌最容易被识别出“假”的地方在于它是正对着镜头的平面图。真实摄像头拍到的车牌都带有一定透视畸变所以合成图必须做透视变换。用OpenCV可以做非常简单的随机四点变换import cv2 import numpy as np src np.float32([[0, 0], [w, 0], [w, h], [0, h]]) dst np.float32([ [random.uniform(-20, 20), random.uniform(-15, 15)], [w random.uniform(-20, 20), random.uniform(-15, 15)], [w random.uniform(-20, 20), h random.uniform(-20, 20)], [random.uniform(-20, 20), h random.uniform(-20, 20)] ]) M cv2.getPerspectiveTransform(src, dst) warped cv2.warpPerspective(plate, M, (w, h))变换之后再贴到背景图上。背景图怎么来我习惯从采集的真实视频里截取不包含车牌的局部画面比如路面、墙面、植物区域这些背景与部署场景的纹理分布天然接近。合成时把变换后的车牌用alpha融合贴到背景上然后对整张图做亮度抖动、颜色抖动。如果不用真实背景用纯色渐变背景也不是不行但模型在训练时会依赖“车牌周围是平滑区域”这个不存在的特征。一旦部署时车牌周边有复杂的纹理识别率就会下降。所以我强烈建议合成数据的背景一定要用真实采集的背景块至少保证一半以上的样本是真实背景。4.3 光照、噪声与压缩模拟真实摄像头拍到车牌会受到光照、雨雾、运动模糊、传感器噪声、JPEG压缩等影响。合成数据阶段需要把这些因素模拟进去。我常用的扰动手段有亮度线性调整模拟顺光和逆光gamma变换模拟暗光环境gamma值在0.6到1.6之间随机HSV色偏扰动模拟白平衡漂移高斯模糊和运动模糊模糊核大小在0到2.5之间随机高斯白噪声sigma在0到10之间随机最后统一用JPEG质量70到95保存模拟摄像头编码损失。有个细节值得专门说很多合成pipeline是先在原始分辨率下做扰动再缩放到24x94。如果你的扰动是在最终的小尺寸图上做的那么模糊和噪声的尺度看起来会很假。先在大图上渲染、加特效、缩小这一连串顺序不要搞反。这也是合成数据和真实数据外观不一致的原因之一。真实样本和合成样本的数量配比我的经验是1比20到1比50。比如真实样本500张合成样本1.5万到2.5万张。合成数据主要用来让模型见过足够丰富的字符形态和背景变化真实数据用来校准域差异两者缺一不可。5. 标注、格式转换与数据集划分的细节5.1 标注工具选择能少用鼠标就少用鼠标LPRNet的标注不是画框、不是点关键点就是输入一串字符。这种任务不适合硬套LabelImg这类目标检测标注工具效率很低。我的做法很简单文件名即标签。比如采集到一张真实车牌图保存时直接命名为“京A12345_001.jpg”然后写一个Python脚本扫描目录里的文件名自动拆分出标签。这种方式的优势是管理成本极低后续换卡、换相机也方便。如果你有大量图片需要标注且文件名没法直接反映标签可以建一个CSV两列是路径和标签。标注时用文本浏览器看图键盘输入字符串回车下一张。用任何表格工具都能实现真没必要上重型标注平台。需要提醒一个坑开发板或Linux服务器上的文件系统对中文文件名支持不一定好。在PC上命名“京A12345.jpg”没问题拷贝到某台Linux主机上可能显示乱码训练脚本读路径时也会出问题。我的经验是文件名用ASCII编码比如“train_000123.jpg”把标签单独放在一个txt或csv里。保存标签文件时务必用UTF-8编码并且清晰地写好字符集定义。这个不起眼的细节能替你省掉后面大量的排查时间。5.2 LPRNet训练所需的目录结构数据集制作完最终要落成一个训练代码能直接读的格式。目前常见LPRNet开源实现都接受类似下面的标注文件data/train/train_0001.png 京A12345 data/train/train_0002.png 苏B88888 data/train/train_0003.png 粤C66666也就是每行是“图片路径 空格 标签字符串”。如果你把所有图片放在一个大目录里标签写在txt里训练脚本按行读取拆分完全够用。目录结构我一般这样组织lpr_dataset/ ├── train/ │ ├── images/ │ │ ├── train_0001.png │ │ ├── train_0002.png │ │ └── ... │ └── label_train.txt ├── val/ │ ├── images/ │ │ ├── val_0001.png │ │ └── ... │ └── label_val.txt └── classes.txtclasses.txt里每行一个字符类训练代码会把它映射成一个整数索引。顺序有讲究最好把高频字符放在前面低频字符放后面因为有些训练脚本在做类别平衡时会对排在前面的类更友好。这个不是硬性规定但我在实践里发现这样调整能减少一些训练不稳的问题。5.3 训练验证集划分防止车牌“泄漏”划分训练集和验证集时最容易犯的错误是随机按图片切分。比如同一个真实车牌的连续帧一部分进了train、一部分进了val验证集的精度会虚高到一个假象水平。原因很简单模型已经把这块车牌的纹理特征记住了验证时遇到同牌不同帧等于开卷考试。正确做法是按车牌字符串去重后划分。先把全部标签读进来按字符串分组每组内的图片要么全进train、要么全进val不能跨组。这样划分出的验证集才能真正反映模型对陌生车牌的识别能力。训练阶段我的切分比例是9比1如果数据量超过5万张验证集有个3000到5000张也就足够了。除了按标签去重还要注意时间维度。如果真实样本是同一天、同一地点拍的随机划分仍可能让train和val共享强烈的光照条件、背景环境。这种情况下真实样本最好按时间段切分前一周的数据进train后一周的数据进val这才更贴近部署时遇到的“未来数据”。6. 数据集质量校验与部署前适配6.1 字符频率统计别让低频字符毁掉精度不管数据是真实采集还是合成生成做完整后都要做一次字符频率统计。我用下面这段脚本快速检查from collections import Counter import codecs counter Counter() label_file train/label_train.txt with codecs.open(label_file, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) 2: counter.update(parts[1]) for char, count in counter.most_common(): print(char, count)统计的结果通常很扎心直辖市和常见省份简称出现频率极高而一些西部省份简称可能只有个位数。这种极不均衡的数据就算模型总体精度再高实际部署时遇到训练样本少的那几个省份准确率会明显拉胯。应对办法有两个层面。第一是生成端加权合成数据脚本里给低频汉字提高生成概率让它至少和其他字符平齐。第二是训练端处理比如对低频类做过采样或者在使用CTC损失时给低频类更高的权重。这一篇不展开训练细节但数据集层面就先把分布调平衡后面训练阶段能省力很多。6.2 人工抽检用“网格图”看一遍所有样本自动统计只能检查标签格式和字符分布看不出图像内容与标签是否匹配。我会把数据集里的图片缩略图拼成一张大网格图每张缩略图下角写上对应标签然后整张看一遍。这个方法土但非常有效。合成数据里偶尔会出现渲染错误比如字符重叠、字体缺失变成方框、透视变换导致字符被裁掉这些在网格图上扫一眼就能发现。更严格一点的话把验证集里每张图的真实标签和模型预测标签做对比专门挑出预测错误的样本看。错误集中在哪类字符、哪种光照、哪种变形后续往数据集里补什么样样本就一清二楚了。这一步其实应该反复进行因为数据集本身也是一个“迭代产物”没有一次性做到完美这回事。6.3 为后期量化准备一份校准集这个问题系列后篇讲部署时还会细说但我在数据集制作阶段就会把它考虑进去。全志开发板跑LPRNet通常要把模型转成INT8量化模型。量化需要一个校准数据集用来统计激活值的数值范围。这个校准集不是随便找几张测试图就行它必须覆盖出现频率最高的字符组合各种光照条件下的样本真实的开发板摄像头采集图至少100到300张。我通常会在数据集Build完成后单独切出一个calib目录放200到500张兼顾均衡性的图像。注意校准集里的图像不应该从训练集里复制最好用独立的真实采集图。如果校准集和训练集完全重叠量化校准得到的统计值会偏向训练分布部署时的输入分布稍有偏移量化误差就会被放大。6.4 模拟开发板输入的数据通路验证在板子上跑模型之前还有一道重要的验证把开发板摄像头实际采集的帧保存为图片然后走一遍和训练数据完全相同的预处理确认预处理后的图像和训练样本外观一致。这一步用实验数据说话别凭感觉。做法很简单先在PC上写一个预处理脚本把开发板传来的原始帧裁出车牌区域resize到24x94灰度化。把处理后的图保存下来和训练样本放在一起看格子图。如果发现亮度差异、对比度差异、车牌边缘被裁掉等问题就能在训练阶段前调整预处理而不是等模型部署到板上后才发现识别率低。另外开发板上的推理结果输出后应该能反向在原始帧上标出对应的车牌位置和识别文本。把这个可视化结果保存成图片或视频片段和PC端的预测结果做对比。遇到识别不出来的样本要能追溯它对应的原始帧判断是训练数据覆盖不足还是预处理不一致。这个闭环链路是部署一个可靠系统的核心数据集的格式和组织方式从第一天起就要为这个闭环服务。7. 写在数据制作之后的话手感说得差不多了聊聊个人体会。做一次数据集不难做一份和部署链路完全对齐的数据集才是最花时间的。我在全志开发板上踩过最深的坑就是在数据集阶段图省事、用网上现成图片凑数结果后面量化、移植、调试的时间全花在填补数据分布差异上比重新做数据还要痛苦。后来改成“以部署场景为起点倒推数据需求”每一步都问自己“板子上的摄像头看见的图像是这样的吗”整个链条就顺了很多。最后分享一个实用小技巧做真实采集时不要只拍晴天、正对车尾的角度。多拍几个时段清晨逆光、中午顶光、夜间辅助照明、雨后天晴每个场景都留一小批样本。哪怕初始训练时用不上后面量化校验和bad case分析时这些边缘场景样本的价值远超统一规整的“好看数据”。这个系列后面我会继续写LPRNet在全志开发板上的训练细节、模型转换和INT8量化以及最终部署的完整踩坑记录。如果你正准备在自己的板子上做车牌识别先把这份数据集的功夫花到位后面会轻松很多。
返回列表