简介:这份资源面向计算机视觉方向的毕业设计、课程设计学生及入门开发者,提供一套基于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 和 pytorch,paddleocr 依赖 paddlepaddle,这两个框架对 numpy、opencv 的版本要求经常不一致。我一般用 conda 建两个独立环境,推理的时候通过子进程调用,或者干脆用 onnx 把两个模型都导出成统一格式。如果非要装在一个环境里,先装 paddlepaddle 再装 ultralytics,让 pip 自己去解依赖冲突,但要做好心理准备,大概率要手动降级 numpy。
# 创建主环境,Python 版本选 3.9,兼容性最好 conda create -n plate_recog python=3.9 -y conda activate plate_recog # 先装 paddlepaddle,CPU 版本足够跑推理 pip install paddlepaddle==2.5.2 -i https://mirror.baidu.com/pypi/simple # 再装 paddleocr pip install paddleocr==2.7.0.3 # 最后装 ultralytics,让它自己处理 torch 依赖 pip install ultralytics==8.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", encoding="utf-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(f"0 {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), encoding="utf-8")img_w 和 img_h 是原图尺寸,labelme 的坐标是像素绝对值,必须除以宽高做归一化。如果标注的时候图片被缩放显示过,要确认 json 里存的坐标是基于原图的,否则框会偏。转换完随机抽几张图用cv2.rectangle画出来看一眼,确认框的位置和大小对得上。
2.3 用预训练权重起步,别从零训
ultralytics 提供了 yolov8n.pt 这种在 COCO 上预训练的权重,虽然 COCO 没有车牌类别,但底层特征提取能力是通用的。我一般用 yolov8n 或者 yolov8s 做迁移学习,冻结前几层,只训检测头。数据量在两千张以内的话,yolov8n 足够,推理速度还快。训练命令:
yolo detect train \ data=datasets/plate/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ device=0data.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_cls=True, lang="ch", show_log=False) def recognize_plate(img_path): img = cv2.imread(img_path) # 第一步:检测车牌 results = det_model(img, conf=0.5, iou=0.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):y2+pad, max(0, x1-pad):x2+pad] # 第三步:OCR 识别 ocr_result = ocr.ocr(crop, cls=True) 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 platesconf=0.5 是检测置信度阈值,低于这个值的框直接丢掉,宁可漏检也不要误检,因为误检会把背景裁进去导致 OCR 出乱码。iou=0.45 是 NMS 的阈值,车牌之间一般不会重叠,这个值设小一点没关系。裁剪时留 5 像素边距是为了让 OCR 模型看到完整的字符边缘,不留边距的话首尾字符容易被切掉一半。
3.2 paddleocr 参数怎么调:识别中文车牌的三个关键设置
paddleocr 的 PaddleOCR 类有几个参数直接影响车牌识别效果。lang="ch" 是必须的,因为车牌里有省份简称。use_angle_cls=True 开启角度分类,处理倾斜车牌。但还有一个隐藏参数 rec_image_shape,默认是 "3,48,320",对于车牌这种长条形的图,宽度 320 可能不够,特别是新能源车牌有 8 个字符。我一般改成 "3,48,480"。
ocr = PaddleOCR( use_angle_cls=True, lang="ch", rec_image_shape="3,48,480", # 加宽识别输入 det=False, # 裁剪图不需要再检测 rec_batch_num=1, # 单张推理,避免 padding 干扰 drop_score=0.5 # 低于 0.5 的识别结果丢弃 )det=False 是因为我们已经裁好了车牌区域,不需要 paddleocr 再做一次文字检测,省掉这一步能减少误检。rec_batch_num=1 在单张推理时避免 batch 内 padding 导致识别结果偏移。drop_score=0.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 model=best.pt format=onnx dynamic=True,paddleocr 导出 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 回流慢慢磨。希望帮到你。
本文还有配套的精品资源,点击获取