ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于yolov5与CRNN的中文车牌识别系统:检测矫正到部署

基于yolov5与CRNN的中文车牌识别系统:检测矫正到部署 简介本资源是一套完整的中文车牌识别高分毕设项目面向人工智能、自动化、电子信息等专业学生及初学者解决车牌检测与OCR识别两大核心任务。系统基于YOLOv5实现车牌定位结合CRNN网络完成端到端中文字符识别支持单双层车牌、复杂光照与倾斜场景附带GUI可视化界面及测试视频演示。压缩包共92个文件含53个Python源码涵盖训练、推理、GUI、模型导出等模块、15个YAML配置文件含YOLOv5多版本模型参数与训练超参、9张效果展示图及7张测试样例图另有2个预训练权重.pth/.pt文件和完整README文档整体大小仅7.65MB结构清晰、模块解耦度高便于学习理解与二次开发。项目已通过导师评审答辩得分95分代码全部实测可运行配套详细技术文档与使用说明覆盖数据准备、模型训练、推理部署全流程适合课程设计、毕业设计及AI视觉入门实践。 看到这个标题我猜大多数人第一反应是又一套yolov5CRNN中文车牌识别源码还带“高分项目”四个字。这套基于yolov5和CRNN的中文车牌识别系统我完整跑通之后最直接的感受是——它确实是典型的教学级工程包代码结构清楚文档也全适合拿来学习、做毕业设计、或者在某个小型安防/道闸场景里快速起步。但“能跑通”和“跑得稳”之间隔着不少坑很多细节标题里看不出来真正动手才会暴露。这篇就把整条链路拆开讲清楚yolov5负责什么、CRNN负责什么、中间那块最容易翻车的环节是什么、以及怎么评估和改进。1. 先拆清楚yolov5和CRNN各自在这套系统里干什么1.1 为什么车牌识别要拆成“检测识别”两段车牌识别在工程上从来不是一个模型干完的事而是两个性质完全不同的任务。第一件事是在一张大图里找到车牌在哪里这是目标检测问题第二件事是把车牌区域里的字符读出来这是一个序列识别问题。把两件事混在一起会让模型既要做空间定位又要做字符分类训练数据、网络结构、评价指标都很难兼顾。这也是为什么绝大多数工业级车牌识别系统都是两阶段架构。yolov5在这个项目里负责第一阶段输出的是车牌区域的边界框通常是一个矩形框加上置信度。CRNN负责第二阶段输入是裁剪好的车牌图片输出是一串字符比如“粤A12345”。两个模型各自独立训练、独立调优整套系统的灵活性高很多。如果你采集了一批新的夜间数据发现检测漏了只需要优化yolov5如果发现字符老是认错只需要优化CRNN不需要动另一头。1.2 为什么选yolov5为什么选CRNNyolov5在车牌检测里的地位有点像“万能钥匙”。它最大的优势不是精度绝对领先而是生态太成熟了。从v6.0到v7.0网上能搜到的教程、踩坑记录、预训练权重多到看不过来训练、验证、导出onnx、转TensorRT的链路官方都给你铺好了。对于这种需要快速落地的项目你不需要折腾太多工程问题把精力留给数据更现实。CRNN则是因为它天生适合车牌这种“有限字符集、变长序列”的场景。它不像目标检测那样要把每个字符框出来只需要把整张车牌图片映射成一个字符串训练的时候只需要给整行文本的标签不需要字符级位置标注。中文字符、英文字母、数字混排的序列识别它就是正经路子。当然也有其他选择。有人用LPRNet做轻量端到端识别也有人直接套PaddleOCR的PP-OCR系列但各自有取舍。方案优点缺点适合场景yolov5 CRNN两阶段分工清晰检测和识别可独立优化中间多了裁剪和矫正步骤流程稍长教学项目、中小型工程落地LPRNet模型轻量端到端部署简单精度和鲁棒性一般中文字符细节弱资源受限的嵌入式设备PaddleOCR PP-OCR中文识别能力强工具链完整更偏通用文本行车牌这种强结构场景要调通用OCR场景车牌需要二次适配单模型端到端车牌识别一个网络直接输出字符串架构复杂数据要求高训练难度大大厂自研场景不太适合个人项目1.3 一条完整车牌识别的数据流把这套系统的数据流完整画出来大概是这样摄像头或图片 → yolov5检测车牌 → 得到bbox和置信度 → 按置信度过滤 → 裁剪车牌区域 → 角度矫正/方向归一化 → 归一化尺寸 → 送入CRNN → 输出车牌字符串和置信度 → 后处理纠错 → 交给业务系统。中间“角度矫正”那一步很多人忽略但这往往是识别率差的最大原因。yolov5给出的框不一定水平尤其是路侧停车、斜向摄像头、车身摆角大的时候裁剪出来的图片带着倾斜。CRNN对倾斜文字非常敏感不矫正直接喂进去识别率能掉十几个百分点。这一点后面专门展开说。2. yolov5车牌检测从基础配置到中文车牌特化2.1 用官方预训练权重还是从零训练很多初学者拿到车牌标注数据后第一件事就是yolov5s.pt直接开训但这里有个基本选择要做用COCO预训练权重做迁移学习还是从零开始训练。我建议直接用预训练权重原因很朴素——车牌检测本质上是一个“中大型物体检测”问题COCO预训练模型已经学会了边缘、纹理、物体边界等通用视觉特征你只需要在它的基础上微调让它把注意力聚焦到“车牌”这个类别上。从零训练需要的数据量和训练轮数都要大得多个人项目根本划不来。实际操作时yolov5的--weights参数填预训练权重路径训练代码默认会冻结前几层还是全部微调这取决于版本和配置。通常默认配置下模型会把所有层都参与训练对于车牌这种与自然图像差异比较大的目标全量微调反而效果更好不必担心过拟合只要数据别太少。2.2 数据标注与车牌长宽比陷阱车牌检测标注看着简单但有个隐藏陷阱车牌的长宽比和COCO里常见的目标很不一样。普通蓝牌长宽比大约3:1黄牌和新能源绿牌更长一些。如果你标注的时候框得不够紧模型会学到很多多余的背景干扰后续裁剪出来的区域就会带上大面积车漆或车灯直接影响识别。标注建议用labelme或CVAT最终转成YOLO格式的txt文件每行一个目标类别序号、中心点x、中心点y、宽度w、高度h全部归一化到0~1。注意框要尽量贴合车牌边缘四边不要留太多余量但也不要切掉字符。yolov5默认训练输入是640×640但对于车牌这种细长目标直接把整张图缩放到640×640会把车牌压得非常小。我的做法是先把默认输入尺寸调大比如用--img 960或者用640×384这种更接近横屏的长方形输入减少非等比压缩带来的形变。如果你的数据里车牌本来就比较大640也能用如果视频流里车离得远一定要上高分辨率。2.3 锚框重聚类、超参数和训练自己的数据集yolov5默认锚框是COCO数据集上聚出来的对车牌这种长条形目标并不友好。训练时yolov5会自动调用autoanchor逻辑根据你的标注重新聚类锚框。但有时候自动计算出的结果不一定最优你可以手动跑一下python utils/autoanchor.py --data plate.yaml看看聚类的Anchor和默认差异有多大。通常在车牌数据集里会聚出几个长宽比接近3:1甚至4:1的锚框这说明默认锚框确实不合适。训练命令大致是这样python train.py --data plate.yaml --cfg models/yolov5s.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100plate.yaml里主要配置训练集和验证集路径、类别数nc: 1、类别名称names: [plate]。超参数文件我一般会在hyp.scratch-low.yaml基础上调两处一是mosaic增强概率如果你用960输入尺寸来补偿小目标mosaic可以保留为默认1.0但如果显存不够只能用640建议把mosaic概率降到0.8左右避免车牌被缩到过小二是HSV颜色扰动车牌颜色在不同光照下变化大适当增大hsv_h、hsv_s、hsv_v的值让模型对颜色变化更鲁棒。有个容易被忽略的点和“yolov5训练单通道”这个热词相关检测阶段一般用三通道彩色图但如果你后续识别阶段用灰度图两者不要混着来。检测模型始终喂彩色图更稳因为蓝色车牌和绿色车牌的色相信息对检测也有帮助灰度化降的是识别端的计算量不是检测端。2.4 检测阶段容易翻车的地方小目标、模糊、夜间反光车牌检测在实际场景里最常见的问题不是模型结构不够好而是目标太小。监控画面里一辆车在50米开外车牌在整幅画面里可能只有二三十像素宽yolov5的neck层对这种小目标召回率明显不足。我的处理规则是全图先做一次低置信度检测比如阈值0.15把可能的候选框都捞出来对于那些置信度中等、尺寸很小的框再按原图位置放大裁出来单独用更高分辨率重检一轮。这种方法在停车场道闸这种机位固定的场景里非常管用。夜间是另一个重灾区。红外补光下字符对比度足够yolov5检测一般还有不错的效果怕的是强光反射和灯下眩光车漆反光会在画面里形成高亮斑块模型容易把高亮区域误判成车牌。治本的办法是收集负样本把没有车牌的车头、车尾、地面反光图片放进数据目录不加标注文件。yolov5训练时没有标注文件的图片会被当作背景样本帮助模型学会区分“像车牌但不是车牌”的干扰区域。3. 车牌矫正很多人忽略但直接影响识别率的一步3.1 为什么识别前必须矫正CRNN这类序列识别模型底层卷积核是按水平方向提取特征的。它对输入图片有一个隐含假设文字基本是水平排列的。一旦车牌旋转了20度、30度或者因为透视产生了梯形形变卷积特征图里的字符形状会发生明显改变LSTM建模出来的序列也会乱套。实测下来一张倾斜15度的车牌直接送进CRNN识别率可能从95%掉到70%左右。这不是模型不行而是输入分布变了。所以yolov5检测得到bbox之后绝不能直接交给CRNN必须先做方向归一化。这也是这套项目里决定“能看”和“好用”的关键一步。3.2 minAreaRect加仿射变换几行代码解决大部分倾斜问题最实用的矫正方法是用cv2.minAreaRect找到车牌区域的最小外接矩形再用旋转矩阵把图像掰正。代码大概长这样import cv2 import numpy as np def rotate_plate(plate_img, angle): h, w plate_img.shape[:2] center (w // 2, h // 2) matrix cv2.getRotationMatrix2D(center, angle, 1.0) cos np.abs(matrix[0, 0]) sin np.abs(matrix[0, 1]) new_w int((h * sin) (w * cos)) new_h int((h * cos) (w * sin)) matrix[0, 2] (new_w / 2) - center[0] matrix[1, 2] (new_h / 2) - center[1] rotated cv2.warpAffine(plate_img, matrix, (new_w, new_h)) return rotated # 假设pts是yolov5输出的四个角点或检测框的四个顶点 rect cv2.minAreaRect(pts.astype(np.float32)) angle rect[2] if rect[1][0] rect[1][1]: # 高度大于宽度需要转90度 angle - 90 plate_corrected rotate_plate(crop_img, angle)这里有个细节很多人会踩坑OpenCV的minAreaRect返回的角度范围是[-90, 0)它到底代表长边还是短边不同OpenCV版本可能有差异。判断标准很简单看rect[1]的宽高如果宽度小于高度说明矩形站起来了需要再转90度。矫正完之后用cv2.threshold或亮度归一化增强一下再resize到CRNN的输入尺寸。3.3 透视畸变什么时候要处理minAreaRect处理的是平面旋转遇到斜向摄像头产生的透视变形就不太够了。比如停车位侧方位摄像头车牌在画面里是一个梯形用旋转掰正只能把整体转到水平但左右两侧字符大小还是不一致。这种情况需要用透视变换src_pts np.float32([top_left, top_right, bottom_right, bottom_left]) dst_pts np.float32([[0, 0], [width, 0], [width, height], [0, height]]) matrix cv2.getPerspectiveTransform(src_pts, dst_pts) result cv2.warpPerspective(crop_img, matrix, (width, height))难点在于四个角点怎么来。如果项目里专门加了车牌角点检测网络那直接用如果只有yolov5的矩形框可以先用边缘检测找到车牌区域的四边形轮廓再取四个顶点。这套源码里多数场景用的是旋转矫正已经够用。我的建议是不要一开始就上透视变换先跑通旋转矫正版本在真实测试集上统计识别错误如果大量错误集中在斜侧视角再考虑加角点检测。还有一个反直觉的经验矫正不要“矫枉过正”。如果物体本身只有轻微透视强行拉成正规矩形反而会造成字符横向拉伸变形CRNN未必能适应。轻微倾斜交给数据增强去覆盖重度畸变再走透视变换。4. CRNN识别中文字符是这套系统的真正难点4.1 用一句话讲清楚CRNN怎么把图像变成车牌字符串CRNN的结构可以理解为三段CNN负责从图片里提取视觉特征RNN通常用双向LSTM负责建模字符之间的序列关系CTC负责把特征序列和最终字符串对齐。具体来说一张车牌图片经过CNN骨干网络后会输出一个特征图。这个特征图的高度被压缩到1行宽度被保留于是整张图变成了一串特征列。每一列就相当于“打字机扫过车牌的一个竖条”LSTM对这些特征列逐列建模最后在每个时间步输出一个字符概率分布。CTC Loss的巧妙之处在于它不需要我们标出每个字符在第几列只需要把预测序列和整行字符串做对齐训练时自动处理重复字符和空白位置。类比一下把CRNN想象成一个打字员它从左到右扫过车牌每个瞬间只看到一小条区域但它能把看到的片段串成一句话。这就是序列识别相对单字符分类的优势——它天然能处理字符之间的距离不均匀问题。4.2 中文字符类别表的坑中文车牌识别最难的不是技术架构而是字符集本身。车牌由省份简称汉字、发牌机关代号字母、序号数字/字母、以及少量特殊字符如警、学、挂、港、澳、使、领组成。汉字数量虽然只有31个省份简称但对模型来说汉字和字母数字混排会把分类空间的难度拉高不少。设计字符类别表时我的建议是只包含车牌上真实出现的字符不要贪全。省级简称就是那么几十个汉字字母这块有一个关键处理车牌上通常不用字母“I”和“O”以免和数字1、0混淆。所以类别表里可以直接去掉这两个字母减少分类压力。字符类别数量大致范围例子省份简称汉字约31个京、津、冀、晋、蒙、辽、吉、黑、沪、苏、浙、皖、闽、赣、鲁、豫、鄂、湘、粤、桂、琼、渝、川、贵、云、藏、陕、甘、青、宁、新英文字母24个去掉I/OA-Z中除I、O数字10个0-9特殊字8~10个警、学、挂、港、澳、使、领等CTC blank1个用于CTC对齐的空位总类别数一般在70个上下。这个规模不大但样本分布极度不均衡省份简称里人口大省出现的车牌远多于其他省份直接训练会让模型对低频省份简称过拟合风险低但召回差。训练时可以对样本做类别加权或者按省份简称做重采样。4.3 训练CRNN的数据来源与数据增强车牌真实数据是稀缺资源尤其是带各种复杂背景、光照变化的中文车牌。公开数据集里有一个常见的方案是用CCPD中国城市停车场数据集做预训练它包含几十万张真实中文车牌图片带字符级标注很适合做CRNN的初始训练。但CCPD的图片背景比较单一直接拿去部署到各种摄像头视角会掉点后期还是要用你自己的场景数据做微调。另一个绕不开的路子是合成数据。用字体渲染把字符画在背景上再加透视变换、模糊、噪声、光照遮盖生成大量带标签的车牌图片。合成数据的最大问题是“假”——字符边缘太干净背景太干净。我的经验是合成数据用来做预训练可以但最终必须用真实数据微调否则识别率看着虚高一上真实场景就崩。CRNN的输入规格通常是固定高度32像素宽度可以动态变化。蓝牌7个字符、新能源绿牌8个字符宽度差不少训练时batch内会做宽度等宽补齐。推理阶段需要注意动态shape对框架部署的约束如果导出onnx时固定了输入宽高长车牌有被截断的风险。4.4 CTC解码、置信度与易混淆字符CRNN解码最常用的贪心策略是每个时间步取概率最高的字符然后合并相邻重复字符、去掉blank。对于车牌这种长度确定蓝牌7位、绿牌8位的场景贪心解码基本够用。如果解码出来的长度和预期不符可以用beam search找更合理的候选序列再按长度过滤。置信度不能用最后一个softmax最大值更好的做法是计算所有字符概率的几何平均这样能反映整行字符的稳定度。实际应用中我会把置信度低的识别结果标记为“不确定”不直接参与开闸或报警避免误识别造成更大问题。易混淆字符主要集中在汉字之间。比如“浙”和“湘”、“赣”和“费”之间笔画细节相似模糊图片下非常容易错。另一个常见错误是字符有遮挡或脏污时模型把“8”认成“6”、把“B”认成“8”。这些只能靠数据增强和真实验证集迭代慢慢压下去没有什么一劳永逸的trick。5. 串起来跑通源码结构、训练流程与端到端评估5.1 拿到源码后的第一件事看目录与README“高分项目”这套源码文件夹里通常会有detect/和recognize/两个模块分别对应yolov5和CRNN的训练与推理代码外加若干工具脚本、权重文件和文档。我的建议是别急着跑train先把README从头到尾读一遍。重点看四件事环境依赖版本、目录说明、训练命令、推理命令。安装依赖时最容易翻车的是PyTorch和CUDA的版本不匹配。yolov5官方requirements会拉一堆包其中有些不是必须的。我的习惯是新建一个虚拟环境把torch版本锁定到和官方文档一致的版本例如Python 3.8、PyTorch 1.12左右openCV就用pip默认版本。先把detect.py跑一张测试图确认检测正常再跑CRNN的demo最后才动训练。5.2 从训练到推理的完整命令流整个项目从零跑通的命令流类似这样# 1. 训练yolov5检测模型 python train.py --data data/plate.yaml --weights yolov5s.pt --img 640 --batch-size 16 --epochs 100 # 2. 测试检测模型 python detect.py --weights weights/best.pt --source test_images/ --save-txt --save-conf # 3. 准备CRNN训练数据 # train.txt中每行格式图片路径 车牌号 cat data/train.txt | head -5 # 4. 训练CRNN识别模型 python train_crnn.py --train data/train.txt --val data/val.txt --batch-size 64 --lr 1e-3 --epochs 60 # 5. 端到端推理 python run.py --image demo.jpg --detect_weights weights/detect_best.pt --recognize_weights weights/crnn_best.pthCRNN训练脚本里的字符表、输入高度、序列长度都要和你的数据集对齐。如果你用的是别人改过的CRNN源码注意看它config里的字符集合是否覆盖了你需要的汉字否则训练时会因为字符越界报错或者静默识别错误。真正部署时要注意不要每次都把裁剪的车牌图写到磁盘再读取内存直接传数组给CRNN推理速度能快不少。视频处理时可以先把一帧检测完的所有车牌区域一次性送入CRNN利用batch推理比逐张处理快几倍。5.3 如何科学地评估“识别率”很多项目文档里只写一个总体准确率比如“识别率95%”但这是一个容易被数字骗的游戏。车牌识别至少要拆成三个指标看检测召回率车牌有没有被找出来、整牌准确率整个车牌字符串对不对、字符准确率每个字符对不对。整牌准确率要求最高一个字符错就是整牌错所以它通常比字符准确率低几个点。评估集还应该按场景分组不能混在一起只看一个数。场景样本数检测召回率字符准确率整牌准确率白天近距离200099.3%98.5%95.2%白天远距离80094.1%94.6%88.3%夜间100096.2%92.1%84.6%斜侧角度60093.8%90.4%80.1%按场景区分之后你才能知道模型短板在哪里。如果夜间整牌准确率只有85%那优化重点应该放在夜间数据增强、曝光补偿、CRNN输入归一化上而不是继续堆白天数据。还有一个容易被忽略的问题评估集不能从视频连续帧里密集抽帧。同一个场景同一辆车连续几十帧模型只要能推断这辆车准确率就会被刷得很高但这不能代表真实应用。采样时至少要隔几帧抽一帧或者按车辆ID去重。6. 部署优化与后续扩展6.1 模型压缩与边缘端落地这套源码如果只是跑在PC上yolov5s加标准CRNN已经够用。但真实场景经常是边缘盒子部署比如rk3568、rv1106这类国产嵌入式平台。这时候模型要跟着换yolov5s换成yolov5nCRNN的骨干网络换成轻量的CycleMLP或者缩减通道数。最近热词里“yolov5在rk3568上”“rv1106搭建yolov5模型”出现的频率很高说明很多人都在做这个方向的移植。移植流程一般是PyTorch模型导出ONNX注意固定输入尺寸或支持动态shape再用ONNX转成NCNN或RKNN最后做量化。这个过程中最容易出问题的是某些算子在目标平台上不支持比如yolov5里的Focus层在转ONNX时会有兼容问题。好在yolov5官方已经把这些都处理好v7.0版本导出onnx基本无痛。如果实在想省事检测和识别都统一输入尺寸用静态shape导出部署端会稳定很多。代价是输入图片的长宽比会被拉伸需要靠检测阶段letterbox和识别阶段等宽padding来缓解。6.2 实际项目中遇到的几个“玄学”问题跑得久了你会发现最影响体验的往往不是模型精度而是工程细节。视频流长时间运行后越跑越慢大概率是每帧创建了大量numpy数组没有释放或者PyTorch在GPU上累积了显存碎片。我的处理方式是每处理完一帧就del掉大数组torch.cuda.empty_cache()只在每500帧显存明显增长时调用一次否则频繁调用反而拖慢速度。另一个容易踩的坑是相机自动曝光导致同一辆车在不同帧的亮度差异巨大。CRNN对亮度的敏感度比检测模型高同一张车牌白天识别对、晚上识别错的情况很常见。一个低成本解法是在送进CRNN之前做一次简单归一化比如把整块车牌区域转灰度后做对比度拉伸。这比加数据增强更直接有效。车牌表面有泥、贴装饰边框、前车挡住一半这些corner case不会出现在教学数据里但真实数据里总会撞见。这类case模型的识别置信度会明显下降你需要在后处理里设计兜底策略置信度低于阈值的车牌不做最终判定而是记录现场图供人工复核或下一帧再试。6.3 后续可以怎么扩展这套项目跑通后往上加东西很方便。最简单的扩展是加车牌颜色识别从yolov5检测到的crop区域直接用颜色直方图判断蓝牌、绿牌、黄牌零成本且非常准能和字符识别结果互相校验。再进一步可以加车辆检测和跟踪把“识别到一张车牌”升级成“识别到一辆车在某个时间点进入/离开”这才是道闸和园区管理真正需要的语义。如果想让识别率再往上走可以考虑用视频多帧投票策略同一辆车连续几帧识别结果相同且置信度较高才作为最终结果。这样能把单帧偶发错误大量过滤掉。更重的做法是对CRNN输出做语言模型纠错利用车牌字符的合法组合规则比如省份简称后一位必须是字母从beam search候选里挑出最合理的序列。我的体会是这套基于yolov5和CRNN的中文车牌识别系统最大的价值不是“能识别车牌”这个结果而是它把你带进了从检测到序列识别再到工程部署的完整链路。把评估集建好、把错误case翻清楚这套系统会越用越顺手。本文还有配套的精品资源点击获取
返回列表