ARTICLE DETAIL

资讯详情

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

基于PaddleOCR的车牌识别算法:从通用OCR到专用任务的改造实践

基于PaddleOCR的车牌识别算法:从通用OCR到专用任务的改造实践 简介本资源面向计算机视觉初学者与进阶开发者提供一套基于PaddleOCR的车牌识别完整项目源码帮助读者从零搭建可运行的车牌检测与识别系统解决车牌定位、字符识别及模型部署等实际问题。压缩包共416个文件约37MB涵盖90个Python脚本、59张jpg与46张png图像样本、49份md说明文档以及C、Java、模型配置与Docker部署文件覆盖训练、推理与跨平台适配全流程。已有253人学习下载适合作为课程设计或工程练手参考。项目围绕DB文字检测与CRNN字符识别展开包含数据预处理、模型训练脚本、超参数配置、量化压缩与推理代码读者可据此理解车牌识别从数据准备到部署的完整链路并掌握调参、数据增强与轻量化等优化思路。1. 车牌识别项目拆解PaddleOCR 到底能不能直接拿来识别车牌很多人第一次接触车牌识别脑子里想的是「我拿 PaddleOCR 直接跑一张车头照片它就该把车牌号吐出来」。真跑一遍就会发现通用 OCR 模型对车牌这种「小目标 强透视 字符间距被压缩」的场景并不友好检测框经常把整个车牌当成一行文字识别结果里混进省份简称的误判、边框被当成字符、新能源绿牌直接漏检。这个标题里的「基于 PaddleOCR 实现的车牌识别算法」本质上不是把 PaddleOCR 当黑盒调用而是把它拆成检测和识别两段再针对车牌做数据、后处理和训练三处改造。这篇文章面向的是手里有项目源码、想真正跑通并改出可用效果的从业者。我会按「先搞清楚 PaddleOCR 在车牌场景里哪一段能用、哪一段必须换」的思路把环境搭建、数据准备、检测模型微调、识别模型微调、后处理规则、部署推理这条链路讲清楚。新手可以照着命令一步步复现熟手可以直接跳到参数和避坑部分看边界。整套方案的核心不是某个玄学 trick而是把车牌当成一个「有固定字符集、有固定版式、有强几何约束」的专用 OCR 任务来处理而不是通用文字识别任务。2. 为什么通用 PaddleOCR 在车牌上会翻车检测与识别的分工2.1 车牌识别和通用 OCR 的三个本质差异通用 OCR 面对的是文档、路牌、商品包装这类文字字符大小相对均匀、排列规整、背景干扰可控。车牌不一样它有三个硬约束。第一是字符集封闭中国大陆车牌只包含省份简称、字母、数字和少量特殊字符一共几十个类别通用 OCR 的几千类字典在这里是负担不是优势。第二是版式固定蓝牌是「省份 字母 五位」绿牌是「省份 字母 六位」黄牌和新能源牌长度不同识别模型如果不知道这个先验就会在长度上乱猜。第三是成像条件恶劣卡口相机拍出来的车牌往往有俯仰角、有运动模糊、有夜间补光过曝检测框稍微偏一点识别就全错。PaddleOCR 的 PP-OCR 系列本身是「检测 方向分类 识别」三段式。检测用的是 DB 算法输出文本区域的多边形识别用的是 CRNN 或 SVTR 结构把检测框裁出来送进识别网络。这个架构对车牌是适用的问题出在预训练权重上。官方权重是在通用中英文数据集上训的检测头对「密集小文字」敏感识别头对「车牌字符分布」陌生。所以正确做法是保留架构替换数据和训练目标。2.2 检测和识别分别该动哪里检测阶段DB 算法的输出是概率图加阈值二值化。车牌在整张图里占比很小如果直接 resize 到 960 长边车牌可能只剩几十像素宽检测概率图会糊掉。常见做法是把检测输入尺寸调大或者先做一次粗定位把车牌区域裁出来再送检测。项目源码里如果直接调用PaddleOCR(detTrue, recTrue)一把梭大概率在远距离小车牌上漏检。识别阶段CRNN 的 CTC 解码依赖字符字典。通用字典里「京」「沪」「粤」这些字是有的但「使」「领」「学」这类特殊车牌字符不一定在训练分布里。更关键的是车牌字符的宽高比和通用文字不同识别网络的输入高度一般设 32 或 48车牌裁切后如果直接拉伸字符会变形。我一般会把识别输入高度设成 48宽度按比例 padding 到固定值保持字符不变形。2.3 一个最小可跑的验证脚本在动训练之前先用官方权重跑一张图看看基线到底差在哪。下面这段代码用 PaddleOCR 的 Python API 做一次推理把检测框和识别结果都打出来。from paddleocr import PaddleOCR import cv2 # 初始化关闭方向分类先用默认检测和识别模型 ocr PaddleOCR(use_angle_clsFalse, langch, show_logFalse) img_path test_car.jpg result ocr.ocr(img_path, clsFalse) # result 是列表每个元素是 [box, (text, score)] for line in result[0]: box, (text, score) line print(f检测框: {box}) print(f识别结果: {text}, 置信度: {score:.4f}) # 把检测框画出来肉眼确认框是否贴合车牌 img cv2.imread(img_path) for line in result[0]: box line[0] pts [(int(p[0]), int(p[1])) for p in box] for i in range(4): cv2.line(img, pts[i], pts[(i1) % 4], (0, 0, 255), 2) cv2.imwrite(det_result.jpg, img)这段代码的逻辑是先跑一次完整 OCR把检测框坐标和识别文本都拿到。参数上use_angle_clsFalse是因为车牌基本是水平方向方向分类反而会增加误判langch加载中文模型。跑完之后重点看两件事检测框是不是把整个车牌框成一个框还是把每个字符单独框出来识别结果里有没有把边框、螺丝孔当成字符。如果检测框碎成多个说明 DB 的阈值需要调如果识别结果里混入非车牌字符说明识别模型需要微调。这个基线结果决定了后面要投入多少精力在检测和识别上。3. 数据准备车牌数据集的标注格式与增强策略3.1 检测数据用 PPOCRLabel 标注成四点框PaddleOCR 的检测训练需要标注文件格式是「图片路径 四点坐标 文本内容」。车牌是矩形理论上两点框就够但 PaddleOCR 的检测训练脚本默认吃四点框所以标注时最好直接标四个角点。PPOCRLabel 是官方提供的半自动标注工具可以先跑一遍自动检测再人工修正。车牌场景下自动检测经常把车牌和车身文字混在一起人工修正这一步省不掉。标注文件每行格式如下坐标是顺时针四个点文本是车牌号train_images/0001.jpg [{transcription: 京A12345, points: [[100, 200], [300, 200], [300, 260], [100, 260]]}]注意points的顺序要和训练脚本里的解析逻辑一致PaddleOCR 默认按顺时针从左上角开始。如果顺序乱了训练时裁出来的区域会旋转 90 度识别阶段直接崩。3.2 识别数据裁切成单行文本图识别模型的训练数据是「车牌裁切图 车牌号」的配对。可以从检测标注里把车牌区域裁出来保存成单独图片再生成一个标签文件。标签文件格式是「图片路径 空格 车牌号」车牌号里不要有空格。import cv2 import json import os label_lines [] with open(det_annotations.txt, r, encodingutf-8) as f: for idx, line in enumerate(f): img_path, anno_str line.strip().split(\t) anno json.loads(anno_str)[0] pts anno[points] text anno[transcription] img cv2.imread(img_path) # 按四点坐标裁切这里简化为外接矩形 xs [p[0] for p in pts] ys [p[1] for p in pts] crop img[min(ys):max(ys), min(xs):max(xs)] out_path frec_images/{idx:06d}.jpg cv2.imwrite(out_path, crop) label_lines.append(f{out_path}\t{text}) with open(rec_labels.txt, w, encodingutf-8) as f: f.write(\n.join(label_lines))裁切时如果车牌有倾斜外接矩形会带进背景识别模型会学到背景噪声。更稳的做法是用透视变换把四点框拉成正矩形OpenCV 的getPerspectiveTransform加warpPerspective两行就能搞定。裁切后的图片高度建议统一到 48 像素宽度按比例缩放这样识别网络输入尺寸一致。3.3 车牌场景该做哪些数据增强通用 OCR 的增强策略是随机旋转、模糊、颜色抖动但车牌不能随便旋转。车牌字符有严格的水平排列旋转超过 10 度就会让字符粘连。我一般会开这几类增强亮度对比度扰动模拟夜间和过曝高斯模糊模拟运动模糊随机遮挡模拟泥点遮挡透视变换模拟不同拍摄角度。PaddleOCR 的配置文件里Transforms段可以逐项开关透视变换的distort参数不要开太大否则车牌会被拉成梯形字符比例失真。提示数据增强的目的是让模型见过真实卡口的恶劣成像不是把数据变得越花越好。每加一种增强最好留一小部分原始图做验证确认增强没有把车牌号本身改掉。4. 检测模型微调DB 算法的参数与训练命令4.1 检测配置文件里必须改的四个参数PaddleOCR 的检测训练用configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml这类配置文件。车牌场景下我一般会改这几处。第一是dataset里的data_dir和label_file_list指向自己的车牌检测数据。第二是image_shape默认是[3, 960, 960]车牌小目标多的话可以调到[3, 1280, 1280]但显存占用会上去。第三是batch_size_per_card根据显存调8G 显存一般设 4 到 8。第四是epoch_num车牌数据量通常几千张训 100 到 200 轮就够太多会过拟合。DB 算法本身有两个关键阈值det_db_thresh和det_db_box_thresh。前者控制概率图二值化后者控制检测框筛选。车牌区域通常比通用文字更「实」det_db_thresh可以设 0.3 左右det_db_box_thresh设 0.5 到 0.6。如果漏检多先把det_db_box_thresh降到 0.4 试试如果误检多就往上调。4.2 启动训练和断点续训训练命令用 PaddleOCR 的tools/train.py指定配置文件路径。如果是从官方预训练模型开始微调加上-o Global.pretrained_modelxxx参数。python tools/train.py \ -c configs/det/ch_PP-OCRv3/ch_PP-OCRv3_det_student.yml \ -o Global.pretrained_model./pretrain_models/ch_PP-OCRv3_det_train/best_accuracy \ -o Train.dataset.data_dir./train_data \ -o Train.dataset.label_file_list./train_data/det_train.txt \ -o Eval.dataset.data_dir./train_data \ -o Eval.dataset.label_file_list./train_data/det_val.txt \ -o Train.loader.batch_size_per_card4 \ -o Global.epoch_num150 \ -o Global.save_model_dir./output/det_carplate命令里-o是覆盖配置项优先级高于 yml 文件。pretrained_model指向官方检测模型这样即使车牌数据只有几千张也能在几十轮内收敛。训练过程中看output/det_carplate下的日志重点盯precision和recall车牌检测的 recall 比 precision 更重要漏检一个车牌比多检一个框的代价大。4.3 检测效果不好时先看可视化结果训练完导出推理模型后用tools/infer_det.py跑一批验证图把检测结果画出来。如果发现车牌被拆成两个框通常是 DB 的后处理参数问题调det_db_unclip_ratio默认 1.5调大到 2.0 可以让框更完整。如果发现车身文字被误检成车牌说明训练数据里负样本不够需要加一些没有车牌的车辆图作为背景负样本。这一步的可视化比看指标数字更直接指标高但框不准的情况很常见。5. 识别模型微调字典、输入尺寸与 CTC 解码5.1 自定义车牌字典识别模型的第一步是改字典。PaddleOCR 的识别字典文件在ppocr/utils/下每行一个字符。车牌场景下我一般会建一个精简字典只保留省份简称、字母、数字和少量特殊字符把通用字典里的几千个汉字去掉。字典变小有两个好处CTC 解码的搜索空间变小识别速度提升模型输出层维度降低小数据集上更容易收敛。字典文件示例京 津 冀 晋 蒙 辽 吉 黑 沪 苏 浙 皖 闽 赣 鲁 豫 鄂 湘 粤 桂 琼 渝 川 贵 云 藏 陕 甘 青 宁 新 0 1 2 3 4 5 6 7 8 9 A B C D E F G H J K L M N P Q R S T U V W X Y Z注意车牌里没有字母 I 和 O避免和数字 1、0 混淆字典里不要放。字典文件建好后在识别配置文件的character_dict_path里指向它同时把use_space_char设成 False车牌号里没有空格。5.2 识别训练配置和启动命令识别配置文件用configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml。需要改的地方和检测类似数据路径、字典路径、batch size、epoch。识别模型的输入高度在Train.dataset.transforms里的RecResizeImg中设置image_shape默认是[3, 48, 320]车牌裁切图高度 48 正好宽度 320 对七位车牌够用新能源八位车牌可以调到 360。python tools/train.py \ -c configs/rec/PP-OCRv3/ch_PP-OCRv3_rec.yml \ -o Global.pretrained_model./pretrain_models/ch_PP-OCRv3_rec_train/best_accuracy \ -o Global.character_dict_path./ppocr/utils/carplate_dict.txt \ -o Train.dataset.data_dir./train_data \ -o Train.dataset.label_file_list./train_data/rec_train.txt \ -o Eval.dataset.data_dir./train_data \ -o Eval.dataset.label_file_list./train_data/rec_val.txt \ -o Train.loader.batch_size_per_card32 \ -o Global.epoch_num200 \ -o Global.save_model_dir./output/rec_carplate识别训练的 batch size 可以比检测大因为输入图片小。epoch_num设 200 左右车牌数据量小训太久会记住训练集。验证时看acc指标但更要看具体错例尤其是省份简称和字母数字的混淆。5.3 CTC 解码与后处理规则识别模型输出的是 CTC 序列解码后得到字符序列。车牌的后处理可以加几条规则长度校验蓝牌 7 位、绿牌 8 位长度不对的直接丢弃或重新识别字符集校验第一位必须是省份简称第二位必须是字母混淆修正把识别结果里的0和O、1和I按位置规则互换。这些规则看起来简单但在实际卡口场景里能把准确率拉高好几个点。PROVINCES set(京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新) LETTERS set(ABCDEFGHJKLMNPQRSTUVWXYZ) DIGITS set(0123456789) def postprocess(text): if len(text) not in (7, 8): return None if text[0] not in PROVINCES: return None if text[1] not in LETTERS: return None # 把容易混淆的字符按位置修正 text text.replace(O, 0).replace(I, 1) # 第一位如果是数字误判成省份直接丢弃 return text后处理规则要克制不要过度修正。比如把所有的O都换成0是错的因为车牌第二位可能是字母O吗实际上中国大陆车牌字母里没有O所以这个替换是安全的。但不同地区的特殊车牌规则有差异规则最好从实际数据里统计出来而不是拍脑袋写。6. 避坑与排查车牌识别项目里最容易翻车的五件事6.1 检测框把车牌和车身文字框在一起现象是识别结果里出现「京A12345」后面跟着一串车身贴纸文字。原因是 DB 检测模型在训练时没见过「车牌紧邻车身文字」的负样本把两者当成一个文本区域。解决办法是在检测训练数据里加一批「车牌 邻近文字」的图标注时只框车牌让模型学会区分。另外可以在推理时加一个规则检测框的宽高比如果超过 5:1大概率是车牌和文字连在一起可以按固定宽度切分后再识别。6.2 新能源绿牌漏检或识别成蓝牌绿牌是八位蓝牌是七位识别模型如果训练数据里绿牌少就会把绿牌当蓝牌识别少一位。检测阶段绿牌和蓝牌的颜色差异大但 DB 是灰度检测颜色信息用不上所以漏检主要靠数据增强里的颜色抖动来缓解。识别阶段一定要在训练数据里保证绿牌样本占比至少 20% 以上。如果项目源码里只用了蓝牌数据绿牌效果差是必然的。6.3 训练 loss 下降但验证准确率不涨这是典型的过拟合。车牌数据集通常只有几千张识别模型参数量不小训到后面训练 loss 一直降验证 acc 卡住甚至下降。解决办法是加数据增强、加 dropout、减小模型宽度或者直接用官方预训练模型冻结 backbone 只训 head。我一般会先冻结 backbone 训 20 轮再解冻全量微调这样收敛更稳。6.4 推理速度慢单张图要几百毫秒检测输入尺寸调到 1280 后推理耗时会上来。如果部署在边缘设备上需要做模型裁剪或量化。PaddleOCR 支持导出 ONNX 和 Paddle Inference 格式用 Paddle Inference 的 TensorRT 加速能快不少。另一个容易忽略的点是方向分类模型如果开了use_angle_clsTrue每张图会多跑一次分类网络车牌场景下直接关掉。6.5 字典里放了空格导致识别结果带空格PaddleOCR 的识别配置里use_space_char默认是 True如果字典文件里没有空格但配置没关解码时可能输出空格。车牌号里没有空格所以一定要把use_space_char设成 False并且检查字典文件末尾有没有多余空行。这个坑很隐蔽因为训练时不会报错只有推理时才会发现结果里多了一个空格后处理长度校验直接失败。7. 把检测和识别串成完整推理管线一个可复用的封装训练完检测和识别模型后最后一步是把它们串起来。PaddleOCR 支持用det_model_dir和rec_model_dir分别指定自己训练的模型但默认的串联逻辑对车牌不一定最优。我一般会自己写一个封装类检测和识别分开调用中间加裁切和后处理。from paddleocr import PaddleOCR import cv2 class CarPlateOCR: def __init__(self, det_dir, rec_dir, dict_path): self.det PaddleOCR( det_model_dirdet_dir, rec_model_dirrec_dir, rec_char_dict_pathdict_path, use_angle_clsFalse, use_space_charFalse, show_logFalse ) def predict(self, img_path): result self.det.ocr(img_path, clsFalse, detTrue, recFalse) plates [] img cv2.imread(img_path) for box in result[0]: pts [(int(p[0]), int(p[1])) for p in box] xs [p[0] for p in pts] ys [p[1] for p in pts] crop img[min(ys):max(ys), min(xs):max(xs)] # 单独送识别 rec_res self.det.ocr(crop, detFalse, recTrue, clsFalse) text, score rec_res[0][0] text self.postprocess(text) if text: plates.append({text: text, score: score, box: pts}) return plates def postprocess(self, text): text text.replace( , ).replace(O, 0).replace(I, 1) if len(text) not in (7, 8): return None return text这个封装的关键点是检测和识别分两次调用检测只出框识别只吃裁切图。这样做的好处是可以在裁切时做透视变换而不是直接把外接矩形送识别。参数上use_space_charFalse和自定义字典路径是必须的否则识别结果会带空格或字典不匹配。实际部署时检测可以批量跑识别按裁切图批量送吞吐能提升不少。验证整套管线是否可用我一般会准备三类测试图白天正拍、夜间过曝、倾斜角度。每类至少 50 张统计端到端的车牌号完全正确率。如果白天能到 95% 以上夜间和倾斜能到 85% 以上这套方案就具备落地价值了。达不到就先回去看检测框的可视化结果大部分错误都出在检测阶段识别阶段的问题反而好定位。这套方案我从头跑过几遍最大的教训是不要一上来就调模型结构先把数据和后处理做扎实。车牌识别的瓶颈往往不在网络多深而在标注质量、字典设计和长度校验这些「脏活」上。希望帮到你。本文还有配套的精品资源点击获取
返回列表