资讯动态

PaddleOCR车牌识别实战:微调通用OCR模型实现高精度车牌号输出

发布时间:2026/9/12 22:15:10 来源:尧图企业网站定制
简介基于PaddleOCR的车牌号识别精简源码包面向具备基础Python知识、想快速上手OCR车牌识别的开发者与学习者。它在PaddleOCR官方源码基础上做了针对性裁剪保留检测、识别、分类等核心模块配合作者文章中提供的模型权重即可直接调用免去从零搭建复杂环境的成本。压缩包共33个文件以15个Python脚本为主涵盖推理预测、后处理与工具函数另有14个pyc缓存文件、2个文本字典/参数文件及2张JPG样例图整体体积仅273KB结构紧凑便于下载后快速查看与调试。资源内含det、rec、cls三类预测入口及结果可视化样例目录组织方式贴近PaddleOCR原生工程方便理解OCR推理流程或在此基础上扩展自己的车牌数据。当前已有417人学习使用适合用于课程设计、毕业设计或工业车牌识别前的效果验证是一份轻量实用的参考代码包。1. 车牌识别这件事PaddleOCR 解决了一半用 PP-OCR 通用模型直接跑车牌图片检测基本能框住车牌区域但识别结果往往败在中文省份简称上——鲁A识别成兽A、京N变成示N这类错误不是模型不够强而是通用 OCR 的字符集是全量中文字符车牌只用了 31 个省级简称两者概率分布天然不匹配。车牌识别本质上是一个约束极强的问题字符位置固定、长度固定或接近固定、字体单一、字符集小但对比度差、大角度、反白字和金属反光又让图像质量远不如文档扫描件。本文要讲的是怎么在 PaddleOCR 的检测识别双阶段架构上用一套可复现的源码方案把通用 OCR 改造成能输出合法车牌号的专用识别链路覆盖数据准备、微调、后处理到部署排错的完整路径。2. 从 PaddleOCR 检测识别结构拆车牌识别需求2.1 检测、识别分离结构对车牌场景的三个关键影响PaddleOCR 的默认流水线是 DB 文本检测 PP-OCRv4 识别也可以是 v3 或更早的 v2两个模型独立训练、独立推理。这个结构对车牌场景有三个直接影响。第一检测模型输出的四边形框是文本行级别的不是车牌级别的。车牌虽然有明确边框但检测器只认字符纹理如果车牌上有污渍、拖车钩、保险杠横条框会略大或略歪。框歪了识别模型拿到的就是带背景的裁剪图字符切分起点就偏了。第二识别模型按整行文本的逻辑做序列预测对字符间粘连非常敏感。京A·12345里的小圆点、间隔符模型有时会吞掉有时会输出成标点。第三中文省份简称是低频字符通用训练集里京沪浙这类字出现频率远低于的了一模型对它们的特征记忆不深迁移到车牌这种强反白、低分辨率的场景时特征响应会漂移。理解了这三点后续所有改造动作都是围绕缩小输入多样性、补足字符分布展开的。一个常见的误区是直接用 PaddleOCR 官方提供的车牌识别模型或者第三方权重。这些模型在标准蓝牌上表现尚可但遇到新能源绿牌、警车白牌、使馆黑牌或者摄像头俯拍的大角度图像时字符集外字符和几何畸变会同时触发错误。自己微调不是可选项是必经步骤。2.2 在本地把 PaddleOCR 车牌识别源码环境跑起来先搭环境。PaddleOCR 的源码仓和 Python 包是分开的源码仓用于训练和二次开发paddleocr 包用于快速推理。两者最好都装上。# 创建虚拟环境Python 3.8 - 3.10 均可 conda create -n paddleocr python3.10 -y conda activate paddleocr # 安装 PaddlePaddle GPU 版以 CUDA 11.8 为例CPU 版去掉 cu118 后缀即可 python -m pip install paddlepaddle-gpu2.6.1.post118 -i https://www.paddlepaddle.org.cn/packages/stable/cu118/ # 克隆源码仓并安装依赖 git clone https://github.com/PaddlePaddle/PaddleOCR.git cd PaddleOCR pip install -r requirements.txt pip install -e .# 用自带模型快速推理一张车牌图 paddleocr --image_dir test_license.jpg --use_angle_cls false --use_gpu true上面命令里--use_angle_cls false是关掉方向分类器。方向分类器用于区分 0/180 度旋转文本车牌这种强先验目标用不到关掉能省一次推理开销。输出会以 JSON 形式打印检测框、识别文本和置信度。先跑通这一步确认环境无异常再进入微调阶段。组件版本建议作用Python3.10Paddle 官方 wheel 覆盖最稳paddlepaddle-gpu2.6.x训练和推理后端PaddleOCR 源码release/2.7含训练脚本和 PPOCRLabelopencv-python4.8图像预处理依赖2.3 用现成模型先看基线结果环境通之后别急着训练先拿 20 张不同类型的车牌照跑一轮基线把 JSON 结果落盘逐张看失败模式。from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsFalse, langch, show_logFalse) def run_baseline(image_paths): for path in image_paths: result ocr.predict(path) for res in result: texts [line[text] for line in res[rec_texts]] scores res[rec_scores] boxes res[det_polys] print(f{path}: {texts} | scores: {scores} | boxes: {boxes}) run_baseline([car1.jpg, car2.jpg, car3.jpg])这段代码把检测框坐标、识别文本、置信度一次打出来。重点观察两类问题如果boxes坐标明显偏出车牌边界是检测问题后续要靠数据增强或者检测微调解决如果boxes正常但texts里的中文是错的是识别问题这占大多数。统计一下 20 张图里检测框正常但识别错的比例如果超过 30%说明当前模型对这张风格的车牌完全不敏感微调识别模型是最高优先级如果检测框本身就飘那要先处理检测。提示车牌识别场景下检测框的质量直接决定识别上限。框歪 2 个像素识别准确率就可能掉 5 个点后处理补不回来。3. 车牌数据是识别精度的分水岭微调前先做两类数据3.1 真实车牌数据和合成车牌的取舍车牌识别微调的数据来源有三类现场采集、网络爬取、程序合成。三类的成本和效果差异巨大。现场采集最理想但涉及个人隐私和数据合规问题普通开发者拿不到大批量真实车牌能拿到几十张做验证已经算不错。网络爬取的车牌图质量参差不齐而且很多是文章配图分辨率低、角度单一实际帮助有限。程序合成是性价比最高的方式——用 PIL 或 OpenCV 把字符渲染到背景图上加透视变换、噪声、模糊、光照变化。合成数据的核心技巧是字符字体要接近真实车牌字体。真实车牌用的是特殊字体中文是等线体变种数字和字母是 DIN 类的无衬线字体。常见的免费替代方案是用开源的FS_MA_字体或汽车牌照专用字体找不到就用思源黑体加粗再配合一定程度的形态学腐蚀膨胀模拟喷墨打印效果。合成时必做的增强项包括随机饱和度偏移营造不同新旧程度、随机多边形遮挡模拟泥污、随机透视角度模拟摄像头安装高度。import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont def synthesize_plate(plate_text, font_path, output_size(440, 140)): # 蓝牌底色新增度随机 bg np.full((output_size[1], output_size[0], 3), (220, 130, 30), dtypenp.uint8) # 转换为 PIL 绘制字符 img Image.fromarray(bg) draw ImageDraw.Draw(img) font ImageFont.truetype(font_path, 80) # 字符间距按真实车牌比例设定 draw.text((20, 20), plate_text, fontfont, fill(255, 255, 255)) # 随机透视变换 pts1 np.float32([[0, 0], [output_size[0], 0], [0, output_size[1]], [output_size[0], output_size[1]]]) pts2 np.float32([[5, 3], [output_size[0]-8, 2], [2, output_size[1]-6], [output_size[0]-5, output_size[1]-9]]) matrix cv2.getPerspectiveTransform(pts1, pts2) result cv2.warpPerspective(np.array(img), matrix, output_size) # 加高斯模糊模拟摄像头对焦不准 result cv2.GaussianBlur(result, (3, 3), 0) return result合成代码里的关键参数是pts2的四个坐标偏移量偏移从 2 到 10 像素模拟不同角度的轻微透视。GaussianBlur的核大小固定为 3×3不要用大核否则字符边缘会糊成一团模型学到的特征是模糊而不是字符结构。合成数据生成量建议 5000 张起步覆盖 31 个省份简称、字母 I/O 的混淆场景、数字 0/O、1/I 的形近组合。3.2 用 PPOCRLabel 产出 PaddleOCR 可训的标注格式合成数据可以自动生成标注文件但真实采集的图片需要人工标注。PaddleOCR 官方提供了 PPOCRLabel 工具它位于源码仓的PPOCRLabel目录。启动它会打开一个 GUI 界面可以画框、写转写文本。cd PaddleOCR/PPOCRLabel python PPOCRLabel.py --lang ch --save_to_txt标注界面里按住鼠标左键画四边形框框住整个车牌区域然后在弹出的对话框里输入车牌内容例如京A12345。保存后生成两个重要文件Label.txt包含检测框坐标和文本crop_img/目录下存放裁剪后的车牌小图。注意 PPOCRLabel 的多边形点序是顺时针还是逆时针PaddleOCR 训练脚本能自动处理但如果你写脚本读标注文件必须检查坐标点序点序错了检测 loss 会不收敛。标注规范上要统一几点蓝牌和绿牌分开标注绿牌的字符比蓝牌多一位新能源牌是 8 位省份简称 字母 6 位数字/字母如果标注时把位数标错识别模型的序列长度分布会被污染。遮挡严重的车牌该删就删不要因为这张图难强行标进去噪声样本会把模型带偏。3.3 识别模型微调的 yaml 改法与训练命令数据齐了进入微调。PaddleOCR 的识别模型训练入口是tools/train.py配置文件在configs/rec/PP-OCRv4/下。微调不是从头训而是加载官方在公开数据集上训好的权重做 finetune这样可以大幅减少数据和训练时间。# 下载官方 PP-OCRv4 识别模型权重 wget -P ./pretrain_models/ https://paddleocr.bj.bcebos.com/PP-OCRv4/chinese/ch_PP-OCRv4_rec_train.tar tar -xf ./pretrain_models/ch_PP-OCRv4_rec_train.tar -C ./pretrain_models/ # 修改配置文件 ch_PP-OCRv4_rec_hgnet.yml 中的关键项后启动训练 python tools/train.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec_hgnet.yml \ -o Global.pretrained_model./pretrain_models/ch_PP-OCRv4_rec_train/best_accuracy \ Global.epoch_num50 \ Train.dataset.data_dir./train_data \ Train.dataset.label_file_list[./train_data/rec_gt.txt] \ Train.dataset.data_transforms[0].RecAug.prob0.5配置参数拆开看pretrained_model指向官方预训练权重的路径不包含.pdparams后缀epoch_num这里设 50 轮因为车牌字符集小、数据量不大轮数太多会过拟合在验证集上看到字符准确率不再提升时就该停Train.dataset.data_transforms[0].RecAug.prob是数据增强概率车牌场景建议降低到 0.3-0.5增强太强会把字符的几何结构改变太多反而破坏车牌字符的硬先验。还有一个必改项是字典文件。PaddleOCR 默认的中文字典有 6623 个字符车牌识别只需要其中 31 个省份简称加数字字母。在配置文件的character_dict_path里指定一个只含相关字符的字典文件能显著减小模型输出头的维度加快训练收敛同时避免模型在推理时输出鲁以外的奇怪汉字。# 生成车牌专用字典 python -c chars 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新 digits 0123456789 letters ABCDEFGHJKLMNPQRSTUVWXYZ unique list(dict.fromkeys(chars digits letters)) with open(plate_dict.txt, w) as f: f.write(\n.join(unique) \n) 注意dict 文件里不能有重复字符每行一个字符文件末尾保留换行。字母表里我刻意去掉了I和O因为国内车牌号不使用这两个字母留着只会让模型在遇到相似字形时多一个容易混淆的输出。训练完的模型在output/目录下best_accuracy.pdparams是验证集表现最好的权重。评估命令单独跑不要用训练时输出的 loss 当作识别精度。python tools/eval.py -c configs/rec/PP-OCRv4/ch_PP-OCRv4_rec_hgnet.yml \ -o Global.pretrained_model./output/best_accuracy \ Global.eval_batch_step[0, 100] \ Eval.dataset.data_dir./eval_data \ Eval.dataset.label_file_list[./eval_data/rec_gt.txt]4. 把源码推理改造成能输出合法车牌号4.1 后处理管线置信度阈值 省简称表 定长补全模型微调完推理侧不能直接把rec_texts当成车牌号输出。模型输出的是原始字符串里面可能有中间空格、置信度低的字符、甚至模型幻觉出的非法字符。需要在后处理里做三层过滤。第一层是字符置信度过滤PaddleOCR 的结果里有每个字符的置信度吗严格来说没有rec_scores是整行文本的平均置信度不是逐字符的。所以要用 PaddleOCR 暴露的rec_texts和rec_scores做一个粗筛低于阈值的整行直接丢弃。第二层是合法性校验用正则把字符串里的非法字符剔除、把字母转大写、把中文限制在省简称表内。第三层是定长逻辑蓝牌 7 位不含点号绿牌 8 位如果识别结果长度和期望不一致做补全或截断。import re PROVINCE_SHORT 京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼 PLATE_PATTERN re.compile(r^([%s])([A-Z])([A-Z0-9]{5,6})$ % PROVINCE_SHORT) def normalize_plate(text, score, expected_len7): # 去空格和分隔符 text text.replace( , ).replace(·, ).replace(-, ) # 字母统一大写排除 I/O text text.upper().replace(I, 1).replace(O, 0) if score 0.75: return None, score m PLATE_PATTERN.match(text) if m: plate m.group(1) m.group(2) m.group(3) if len(plate) expected_len: return plate, score # 长度不对时尝试截断尾部 if len(text) expected_len: candidate text[:expected_len] if PLATE_PATTERN.match(candidate): return candidate, score return None, scorenormalize_plate里的几个处理逻辑说明I转成1、O转成0是因为车牌不包含这两个字母识别模型容易把数字 1 认成 I、把 0 认成 O这个映射对蓝牌准确率提升明显expected_len蓝牌传 7绿牌传 8在调用方根据检测结果的颜色先做一次判断或者传两个候选值分别尝试匹配。score阈值 0.75 只是初值如果实际场景漏检多往下调到 0.65如果误检多往上调到 0.85阈值在验证集上扫一遍取最优点。4.2 一批图片的平均字符准确率和整牌准确率怎么算模型调完要用客观指标评估不能在几张测试图上目测还行就结束。车牌识别领域有两个常用指标整牌准确率严格匹配和字符准确率宽容匹配。整牌准确率是指整个车牌号完全正确才算对字符准确率是逐字符比对结果适合定位到底是哪个位置的字符容易错。def evaluate_predictions(ground_truth, predictions): total_chars 0 correct_chars 0 correct_plates 0 per_position {i: [0, 0] for i in range(8)} for gt, pred in zip(ground_truth, predictions): if gt pred: correct_plates 1 for i, (g, p) in enumerate(zip(gt, pred)): per_position[i][1] 1 if g p: correct_chars 1 else: per_position[i][0] 1 total_chars len(gt) char_acc correct_chars / total_chars plate_acc correct_plates / len(ground_truth) return char_acc, plate_acc, per_positionper_position统计的是第几位字符的出错次数这个信息非常值钱。常见规律是第 2 位省份简称后的字母和第 3、4 位数字区域错误率最高第 2 位是因为字母区和数字区交界处字符间隔小第 3、4 位是因为车牌中间的点号或防伪标识会在字符边缘制造噪点。如果某个位置错误率明显偏高针对这个位置做卡阈值或者在合成数据里加大该位置的字体形变是最直接的优化手段。5. 剩下的精度坑用预处理和部署细节填平车牌识别做到 95% 整牌准确率后再往上走的每一步都是抠细节。第一个坑是大角度车牌。摄像头俯拍或侧面抓拍时车牌是梯形甚至不规则的四边形直接裁剪送入识别模型字符会被压缩变形。处理方法是先做透视矫正把检测出的四个角点映射到标准矩形。def perspective_fix(image, quad): # quad 是检测模型输出的四个角点顺序为左上、右上、右下、左下 dst_pts np.float32([[0, 0], [440, 0], [440, 140], [0, 140]]) quad np.float32(quad).reshape(4, 2) # 保证点序正确按左上、右上、右下、左下排列 mat cv2.getPerspectiveTransform(quad, dst_pts) return cv2.warpPerspective(image, mat, (440, 140))点序错了矫正会变成翻转或错位映射所以从检测结果拿角点时先按 y 坐标排序分成上下两组各组内按 x 坐标排序这是最常见的四角点排序方式。第二个坑是双层黄牌和新能源绿牌的交替字符排列这类车牌的字符不是单行的检测模型可能把两行字共用一个框识别模型只能识别单行文本输出必然乱。常见做法是对检测框的高度做判断如果宽高比小于 2.5就按上下两半拆开分别识别再把结果拼接。第三个坑是夜间低照度蓝牌在低光下呈深灰色字符对比度骤降可以在预处理里先做 CLAHE 局部直方图均衡再送入识别。部署侧的常见诉求是把模型导出成 ONNX 格式绕开 Paddle 的 Python 运行时在 C 或者 Java 端做推理这对摄像头抓拍场景的延迟和内存占用都有好处。# 导出 ONNX输入尺寸 3x48x320按权重实际尺寸填 paddle2onnx --model_dir ./output/inference \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ./output/plate_rec.onnx \ --opset_version 11部署方式单张推理延迟CPU, i5-1240P依赖体积适用场景Paddle Python35-55ms大原型验证、离线批处理Paddle Inference C18-25ms中后端服务ONNX Runtime C15-22ms小边缘盒子、嵌入式ONNX 导出后配合 OpenCV 做图像预处理在 C 里集成时格外注意输入张量的归一化参数PaddleOCR 用的是[0.5, 0.5, 0.5]均值和[0.5, 0.5, 0.5]方差直接用其他模型的[0.485, 0.456, 0.406]那一套会导致识别精度骤降。归一化参数错了模型输出全部跑偏但报错还查不出来是最容易埋坑的地方。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价