YOLOv8+PaddleOCR车牌识别实战:从环境搭建到端到端调优
简介这份资源面向计算机视觉方向的毕业设计、课程设计学生及入门开发者提供一套基于YOLOv8与PaddleOCR融合的智能车牌识别系统完整工程。系统覆盖图像预处理、车牌定位、字符分割与OCR识别全流程可应用于车辆监控、停车场管理与交通流量控制等场景。压缩包共77个文件约58.61MB包含9个Python功能脚本、4个pt模型权重、PaddleOCR的pdmodel与pdiparams推理文件、yaml配置、ttf字体及大量jpg、png测试图片并附README部署说明与CITATION引用文件目录按数据集、模型、测试模块分层组织。已有30人学习下载。读者可直接获得可运行的检测与识别代码、轻量级yolov8n权重、摄像头与视频及静态图片多场景测试脚本以及训练曲线、混淆矩阵等评估图表便于快速复现实验、理解检测与识别串联思路并在此基础上完成二次开发。1. 从一张糊掉的车牌照片说起yolov8_paddleocr 这套组合到底能干什么地下车库出口逆光车牌一半在阴影里一半被远光灯打爆。传统基于边缘检测加模板匹配的方案在这种场景下基本就废了字符粘连、倾斜、反光随便一个都能让识别率掉到六成以下。我最早做车牌识别的时候就是栽在这种图上后来换成 yolov8 做检测、paddleocr 做字符识别同一批图识别率直接拉到九成五以上。这套组合的核心思路很清晰yolov8 负责在整张图里把车牌框出来paddleocr 负责把框里的字符读出来。两个模型各干各的活中间用坐标裁剪衔接。这个方案适合谁做智能停车、门禁管理、交通卡口这类场景的开发者手头有几百到几千张标注图想快速搭一个能跑的车牌识别系统。也适合拿来做毕业设计或者技术验证因为 yolov8 和 paddleocr 的生态都很成熟文档多、预训练权重好找、社区踩坑记录全。但要注意这套方案不是开箱即用的车牌检测需要自己训练或者找现成的车牌检测权重paddleocr 的通用模型对车牌字符的识别也需要做微调否则中文省份简称和数字字母混排的时候容易出错。下面从环境搭建开始一步步把这条路走通。2. 环境搭建与数据准备从零把 yolov8 和 paddleocr 装进同一台机器2.1 用 conda 隔离环境避开依赖打架yolov8 依赖 ultralytics 和 pytorchpaddleocr 依赖 paddlepaddle这两个框架对 numpy、opencv 的版本要求经常不一致。我一般用 conda 建两个独立环境推理的时候通过子进程调用或者干脆用 onnx 把两个模型都导出成统一格式。如果非要装在一个环境里先装 paddlepaddle 再装 ultralytics让 pip 自己去解依赖冲突但要做好心理准备大概率要手动降级 numpy。# 创建主环境Python 版本选 3.9兼容性最好 conda create -n plate_recog python3.9 -y conda activate plate_recog # 先装 paddlepaddleCPU 版本足够跑推理 pip install paddlepaddle2.5.2 -i https://mirror.baidu.com/pypi/simple # 再装 paddleocr pip install paddleocr2.7.0.3 # 最后装 ultralytics让它自己处理 torch 依赖 pip install ultralytics8.0.200这里 paddlepaddle 选 2.5.2 是因为 2.6 之后对某些旧显卡的兼容性有变化CPU 推理场景下 2.5.2 足够稳定。ultralytics 选 8.0.200 是一个经过大量项目验证的版本API 没有大改。安装顺序很重要先 paddle 后 ultralytics因为 ultralytics 会尝试装最新版 opencv而 paddleocr 对 opencv 版本有上限要求反过来装容易把 paddleocr 的依赖冲掉。提示如果装完 import paddle 报 libgomp 相关错误执行conda install -c conda-forge libgomp补上系统库。2.2 车牌检测数据集标注格式和目录结构yolov8 支持的数据格式是 YOLO txt每张图对应一个 txt每行是class_id x_center y_center width height全部归一化到 0 到 1。车牌检测通常只有一个类别class_id 就是 0。我一般用 labelme 标注然后写脚本转成 YOLO 格式。目录结构按 ultralytics 的要求来datasets/ plate/ images/ train/ (存放训练图片) val/ (存放验证图片) labels/ train/ (存放对应的 txt) val/转换脚本核心逻辑就三步读 labelme 的 json取 rectangle 的四个角点算归一化中心点和宽高。import json import os from pathlib import Path def labelme_to_yolo(json_dir, output_dir, img_w, img_h): 将 labelme 标注转为 YOLO 格式只处理矩形框 for json_file in Path(json_dir).glob(*.json): with open(json_file, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: if shape[shape_type] ! rectangle: continue points shape[points] x1, y1 points[0] x2, y2 points[1] # 计算归一化中心点和宽高 x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h width abs(x2 - x1) / img_w height abs(y2 - y1) / img_h lines.append(f0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) # 写入同名 txt out_path Path(output_dir) / (json_file.stem .txt) out_path.write_text(\n.join(lines), encodingutf-8)img_w 和 img_h 是原图尺寸labelme 的坐标是像素绝对值必须除以宽高做归一化。如果标注的时候图片被缩放显示过要确认 json 里存的坐标是基于原图的否则框会偏。转换完随机抽几张图用cv2.rectangle画出来看一眼确认框的位置和大小对得上。2.3 用预训练权重起步别从零训ultralytics 提供了 yolov8n.pt 这种在 COCO 上预训练的权重虽然 COCO 没有车牌类别但底层特征提取能力是通用的。我一般用 yolov8n 或者 yolov8s 做迁移学习冻结前几层只训检测头。数据量在两千张以内的话yolov8n 足够推理速度还快。训练命令yolo detect train \ datadatasets/plate/data.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ device0data.yaml 里写清楚 train、val 路径和 nc: 1、names: [plate]。epochs 设 100 是给模型足够时间收敛patience 20 表示验证集损失 20 轮不降就早停。lr0 初始学习率 0.01 对迁移学习偏大如果 loss 震荡厉害就降到 0.001。batch 16 在 8G 显存上跑 640 尺寸刚好显存不够就降到 8 或者用 yolov8n。注意训练完看 results.png 里的 mAP50 曲线如果验证集 mAP 一直上不去但训练集在涨说明过拟合了加数据增强或者减模型复杂度。3. 把检测和识别串起来yolov8 出框、paddleocr 读字符的完整推理链路3.1 推理流程拆解从一张图到一串车牌号整个链路分四步yolov8 推理得到所有车牌框的坐标和置信度按置信度过滤掉低分框用 NMS 去掉重叠框然后根据坐标从原图裁剪出车牌区域最后把裁剪图送进 paddleocr 做文字识别。这里有个细节paddleocr 默认的检测模型会自己在裁剪图里再找一次文字区域对于车牌这种已经裁好的图可以直接用识别模型跳过检测步骤速度更快。from ultralytics import YOLO from paddleocr import PaddleOCR import cv2 import numpy as np # 加载模型 det_model YOLO(runs/detect/train/weights/best.pt) ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) def recognize_plate(img_path): img cv2.imread(img_path) # 第一步检测车牌 results det_model(img, conf0.5, iou0.45)[0] plates [] for box in results.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) conf float(box.conf[0]) # 第二步裁剪车牌区域留一点边距 pad 5 crop img[max(0, y1-pad):y2pad, max(0, x1-pad):x2pad] # 第三步OCR 识别 ocr_result ocr.ocr(crop, clsTrue) if ocr_result and ocr_result[0]: text ocr_result[0][0][1][0] plates.append({bbox: [x1, y1, x2, y2], conf: conf, text: text}) return platesconf0.5 是检测置信度阈值低于这个值的框直接丢掉宁可漏检也不要误检因为误检会把背景裁进去导致 OCR 出乱码。iou0.45 是 NMS 的阈值车牌之间一般不会重叠这个值设小一点没关系。裁剪时留 5 像素边距是为了让 OCR 模型看到完整的字符边缘不留边距的话首尾字符容易被切掉一半。3.2 paddleocr 参数怎么调识别中文车牌的三个关键设置paddleocr 的 PaddleOCR 类有几个参数直接影响车牌识别效果。langch 是必须的因为车牌里有省份简称。use_angle_clsTrue 开启角度分类处理倾斜车牌。但还有一个隐藏参数 rec_image_shape默认是 3,48,320对于车牌这种长条形的图宽度 320 可能不够特别是新能源车牌有 8 个字符。我一般改成 3,48,480。ocr PaddleOCR( use_angle_clsTrue, langch, rec_image_shape3,48,480, # 加宽识别输入 detFalse, # 裁剪图不需要再检测 rec_batch_num1, # 单张推理避免 padding 干扰 drop_score0.5 # 低于 0.5 的识别结果丢弃 )detFalse 是因为我们已经裁好了车牌区域不需要 paddleocr 再做一次文字检测省掉这一步能减少误检。rec_batch_num1 在单张推理时避免 batch 内 padding 导致识别结果偏移。drop_score0.5 过滤掉低置信度的识别结果防止输出乱码。如果发现识别结果里混入了非车牌字符比如把车牌框旁边的螺丝孔识别成数字可以在裁剪的时候把边距调小或者用颜色过滤车牌底色通常是蓝、黄、绿背景色不对的直接跳过。3.3 后处理车牌号格式校验和纠错OCR 出来的原始文本经常有错比如把 0 识别成 O把 1 识别成 I把 8 识别成 B。车牌号的格式是固定的普通车牌 7 位新能源 8 位第一位是省份简称第二位是字母后面是数字和字母组合。写一个简单的规则校验和纠错函数能再拉高一两个点的准确率。import re # 省份简称列表 PROVINCES 京津冀晋蒙辽吉黑沪苏浙皖闽赣鲁豫鄂湘粤桂琼渝川贵云藏陕甘青宁新 # 易混淆字符映射 CONFUSE_MAP {O: 0, I: 1, B: 8, S: 5, Z: 2} def correct_plate(text): 对 OCR 结果做格式校验和字符纠错 text text.strip().replace( , ) if len(text) 7: return None # 第一位必须是省份简称 if text[0] not in PROVINCES: # 尝试从易混淆映射里找 if text[0] in CONFUSE_MAP: text CONFUSE_MAP[text[0]] text[1:] else: return None # 第二位必须是字母 if not text[1].isalpha(): return None # 后续位做易混淆替换 corrected text[:2] for ch in text[2:]: corrected CONFUSE_MAP.get(ch, ch) return corrected这个函数先检查长度再检查省份简称和字母位最后对数字位做易混淆替换。注意 CONFUSE_MAP 只对数字位生效字母位不能替换否则会把合法的字母改成数字。实际用的时候如果 correct_plate 返回 None说明 OCR 结果格式完全不对直接丢弃比强行纠错更安全。4. 避坑与排查车牌识别从能跑到好用之间隔着的五个坑4.1 坑一检测框偏大或偏小导致 OCR 乱码现象车牌检测框看起来没问题但 OCR 出来的字符缺胳膊少腿或者混入了车牌框外的内容。原因通常是训练数据的标注框和实际推理时的框有偏差或者裁剪时边距设得不对。解决方法是先用验证集的可视化脚本把检测框画出来和原图对比确认框的紧贴程度。如果框偏大检查标注时是不是把车牌外的边框也框进去了如果框偏小检查 NMS 的 iou 阈值是不是太低导致框被裁掉了边缘。我一般把裁剪边距设成车牌高度的 5% 左右比如车牌高 40 像素边距就留 2 像素。4.2 坑二paddleocr 把新能源车牌识别成普通车牌现象8 位新能源车牌只识别出 7 位最后一位丢失。原因是 rec_image_shape 的宽度不够默认 320 在 8 字符车牌上会截断。解决方法是把宽度改成 480 甚至 640同时检查裁剪图的宽高比如果裁剪图被 resize 到固定尺寸时压扁了字符会变形。可以在裁剪后先按比例 resize 到高度 48宽度按原比例算再 padding 到 480。4.3 坑三逆光和夜间场景检测框丢失现象白天识别率九成五一到晚上或者地下车库就掉到六成。原因是训练数据里缺少暗光样本模型没学过这种光照条件下的车牌特征。解决方法是收集夜间和逆光场景的图片至少补 200 张到训练集里重新训练。如果没法补数据可以在推理前做直方图均衡化或者 CLAHE把暗部提亮但效果有限根治还是要靠数据。4.4 坑四多车牌场景下 OCR 结果串行现象一张图里有两辆车OCR 把两辆车的字符混在一起输出。原因是裁剪的时候没有按检测框逐个裁剪而是一次性把整张图送进了 OCR。解决方法是严格按每个检测框单独裁剪、单独识别不要图省事把多个框拼成一张图。另外检测框的排序也要注意按 x 坐标从左到右排避免输出顺序混乱。4.5 坑五模型导出 ONNX 后精度下降现象PyTorch 模型推理正常导出 ONNX 后识别率掉了好几个点。原因是导出时的动态轴设置不对或者 paddleocr 的预处理在 ONNX 里没有对齐。解决方法是导出 yolov8 时用yolo export modelbest.pt formatonnx dynamicTruepaddleocr 导出 ONNX 要用 paddle2onnx 工具并且对比导出前后的预处理代码确保归一化参数一致。如果精度还是掉检查 ONNX Runtime 的版本有些版本对某些算子的实现有差异。5. 把识别率从九成五推到九成九三个我反复验证过的调优习惯第一个习惯是建立错误样本回流机制。每次线上跑出来的识别错误不管是检测框偏了还是 OCR 读错了都自动存到一个 badcase 文件夹里每周把新增的 badcase 标注一遍补进训练集重新训一轮。这个习惯听起来笨但效果最实在。我做过统计第一轮训练 mAP50 大概 0.92补三轮 badcase 之后能到 0.97 以上OCR 的准确率也跟着涨因为检测框更准了OCR 的输入质量就高了。第二个习惯是给 OCR 结果加一个置信度阈值和格式校验的双重过滤。paddleocr 返回的识别结果里带一个置信度分数我一般把 drop_score 设成 0.5低于这个值的直接丢。然后再过一遍 correct_plate 函数做格式校验格式不对的也丢。这样虽然会漏掉一些真实车牌但能保证输出的车牌号都是可信的。在停车收费这种场景下漏检可以人工补录误检会导致扣错费宁可漏不可错。第三个习惯是定期用验证集跑一遍全链路评估不只看检测的 mAP还要看端到端的车牌号准确率。评估脚本很简单遍历验证集图片跑一遍 recognize_plate和标注的真值对比统计完全正确的比例。这个指标才是最终用户感知到的指标。我见过太多项目检测 mAP 刷到 0.98但端到端准确率只有 0.85问题就出在裁剪和 OCR 的衔接上。def evaluate_end2end(val_img_dir, val_label_dir): 端到端评估检测识别全链路准确率 total, correct 0, 0 for img_path in Path(val_img_dir).glob(*.jpg): label_path Path(val_label_dir) / (img_path.stem .txt) if not label_path.exists(): continue # 读真值 gt_text label_path.read_text().strip() # 跑全链路 plates recognize_plate(str(img_path)) pred_text plates[0][text] if plates else total 1 if pred_text gt_text: correct 1 print(f端到端准确率: {correct}/{total} {correct/total:.4f})这个脚本跑一次就能看出问题出在哪个环节。如果检测框位置对但 OCR 结果错就去调 OCR 参数如果检测框都没了就去补检测训练数据。我一般每周跑一次记录准确率变化掉点了就查原因。这套流程跑顺之后车牌识别系统基本可以稳定在九成八以上的端到端准确率剩下的长尾问题靠 badcase 回流慢慢磨。希望帮到你。本文还有配套的精品资源点击获取