简介本资源面向计算机视觉学习者与智能交通方向开发者提供一套基于YOLOv8的车牌检测与识别完整实战方案帮助读者从零搭建可运行的车牌识别系统解决车牌定位、字符分割与字符识别等核心问题。压缩包共55个文件约32.93MB包含23张jpg与13张png图像样本、3个ipynb训练与推理笔记、3个yaml配置、2个pt权重、2段mp4演示视频以及pb模型、txt说明、csv数据与md文档等覆盖数据准备、模型训练、性能评估到部署测试的完整链路。已有298人学习下载适合希望快速上手YOLOv8工程实践的中高级读者。项目不仅给出可直接运行的源码还通过详细流程教程讲解多尺度特征融合、损失函数设计等原理并涉及不同光照、角度与遮挡条件下的识别处理思路便于读者理解算法细节、优化识别率并迁移到实际场景。1. 车牌识别项目为什么总在“检测对了、识别错了”上翻车车牌识别这套东西外行看是“拍张照读出号码”内行知道它其实是两段完全不同的任务拼起来的先用 YOLOv8 把车牌从整张图里框出来再把框里那一小块图交给字符识别模型逐位读出来。标题里写的“基于 YOLOv8 实现车牌检测车牌识别算法”核心就在这个“”号上——检测和识别是两条独立的链路各自有各自的模型、数据集和调参逻辑任何一段拉胯整条链路的结果就是错的。我见过太多人拿着一个 YOLOv8 车牌检测的权重检测框画得漂漂亮亮结果识别出来的号码缺一位、把 0 认成 O、把 8 认成 B。问题往往不在检测而在识别那一段被草草带过。这个方案适合谁适合已经会跑 YOLOv8 目标检测、想把它落到一个完整可交付场景的工程师也适合做停车场、门禁、卡口这类项目、需要自己掌控整条链路而不是买一体机的人。下面我按“检测怎么训、识别怎么做、整条链路怎么拼、坑在哪”的顺序把能复现的细节讲清楚。2. 车牌检测用 YOLOv8 训一个只认车牌的检测器2.1 为什么检测和识别必须拆成两个模型先说选型理由这决定了后面所有工作量。车牌识别本质上是一个“先定位、后分类”的级联问题。检测阶段要解决的是“车牌在哪、有多大、有没有倾斜”输出的是坐标框识别阶段要解决的是“框里这串字符是什么”输出的是字符序列。这两件事对模型的要求完全不同检测需要感受野覆盖整张图、对尺度变化鲁棒识别需要把一个小区域放大到固定尺寸、对字符切分和序列建模敏感。如果你硬用一个模型端到端出车牌号等于让网络同时学定位和序列解码训练难度陡增数据量要求也高得多。常见做法就是拆开YOLOv8 负责检测识别单独用一个轻量 CRNN 或者专门的车牌识别网络。拆开的好处是每一段都能单独调、单独换、单独评估检测不准就补检测数据识别不准就补识别数据互不干扰。这也是标题里“检测识别”这个加号的真实含义。2.2 车牌检测数据集的准备与标注格式转换YOLOv8 训练需要 YOLO 格式的标注每张图对应一个 txt每行是类别 中心x 中心y 宽 高全部归一化到 0~1。如果你手上是 LabelImg 存的 VOC xml或者 Labelme 存的 json得先转。labelme 标注用于 yolov8 是很常见的起点但 labelme 默认存的是多边形点转成矩形框要取外接矩形。下面这个脚本把 Labelme 的 json 批量转成 YOLO txt只保留矩形框标注import json import os from pathlib import Path # 类别映射本项目只有一类——车牌 CLASS_MAP {plate: 0} def labelme_to_yolo(json_path, out_txt_path, img_w, img_h): with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: label shape[label] if label not in CLASS_MAP: continue pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] # 取外接矩形兼容多边形标注 xmin, xmax min(xs), max(xs) ymin, ymax min(ys), max(ys) # 归一化并转成中心点宽高 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{CLASS_MAP[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_txt_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: json_dir Path(labels_json) out_dir Path(labels_yolo) out_dir.mkdir(exist_okTrue) # 图片尺寸需要和 json 对应这里假设你已知尺寸或从图片读取 IMG_W, IMG_H 1920, 1080 for jp in json_dir.glob(*.json): labelme_to_yolo(jp, out_dir / (jp.stem .txt), IMG_W, IMG_H)逻辑说明CLASS_MAP只留车牌一类多标签场景可以扩展。取min/max是为了把多边形压成外接矩形车牌一般是矩形这样最稳。归一化用图片真实宽高IMG_W/IMG_H必须和图片一致否则框会整体偏移——这是新手最容易翻车的地方标注看着对训练出来框全飘。参数说明cx/cy/w/h都是相对值YOLOv8 内部会再乘回特征图尺寸所以不要在这里乘像素。如果你的数据集图片尺寸不统一建议先统一 resize 到固定尺寸再标注或者转换时逐张读取真实尺寸。2.3 YOLOv8 训练命令与关键参数怎么设数据组织成images/train、images/val、labels/train、labels/val再写一个plate.yamlpath: /data/plate_dataset train: images/train val: images/val nc: 1 names: [plate]然后一条命令开训yolo detect train \ modelyolov8n.pt \ dataplate.yaml \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/plate \ nameexp1逻辑说明modelyolov8n.pt用 nano 版起步车牌是单类、目标特征明显n 版通常够用想提精度再换 s 或 m。imgsz640是默认值车牌在整图里占比小的话可以提到 960 或 1280但显存和速度要权衡。patience20表示 20 轮没提升就早停省时间。参数说明lr0初始学习率单类小数据集 0.01 一般稳loss 震荡就降到 0.005。batch按显存调GTX1660Ti 跑 yolov8n 640 大概能到 16。训练完看runs/plate/exp1/results.png重点看mAP50和mAP50-95车牌检测 mAP50 上 0.95 才算能用。想看损失曲线YOLOv8 自带results.csv用 pandas 画一下就行yolov8 画损失函数曲线图本质就是读这个 csv。3. 车牌识别从检测框到字符序列的完整链路3.1 识别模型选型CRNN 还是专用车牌网络检测框出来之后识别这一段的选择直接决定最终准确率。主流两条路一是通用 OCR 路线CRNNCTC适合字符集不固定、样式多变的场景二是专用车牌识别网络输入固定为单行车牌、输出固定位数蓝牌 7 位、新能源 8 位结构更轻、精度更高。我一般会选专用网络因为车牌字符集是封闭的——省份汉字 字母 数字一共几十类没必要上通用 OCR 那么重的模型。专用网络通常是一个 CNN 提特征 序列解码头输入把检测框裁出来 resize 到94x24或128x32输出每一位的分类概率。这样训练快、推理快单张车牌几毫秒。选型理由讲清楚如果你的场景只有蓝牌和新能源绿牌专用网络是最优解如果还要兼容黄牌、使馆牌、各种异形牌那 CRNN 的泛化更省心。标题里的“车牌识别算法”没有限定牌型我按最常见的蓝牌绿牌来写其他牌型靠扩充识别数据集解决。3.2 检测框裁剪与透视矫正的预处理检测框是带角度的直接裁矩形会把背景带进来字符被拉伸。正确做法是先做透视矫正把倾斜车牌拉正。YOLOv8 如果只训了水平框输出的是正矩形倾斜车牌会被框大要拿四个角点得用 OBB旋转框或者自己加关键点回归。实操里更省事的办法是检测框裁出来后用 OpenCV 做一次最小外接矩形 仿射变换。import cv2 import numpy as np def crop_and_rectify(img, box): x1, y1, x2, y2 map(int, box) roi img[y1:y2, x1:x2] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) # 二值化后找车牌轮廓做最小外接矩形 _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return cv2.resize(roi, (128, 32)) rect cv2.minAreaRect(max(contours, keycv2.contourArea)) box_pts cv2.boxPoints(rect) # 按左上、右上、右下、左下排序 s box_pts.sum(axis1) d np.diff(box_pts, axis1) ordered np.zeros((4, 2), dtypefloat32) ordered[0] box_pts[np.argmin(s)] ordered[2] box_pts[np.argmax(s)] ordered[1] box_pts[np.argmin(d)] ordered[3] box_pts[np.argmax(d)] dst np.array([[0, 0], [128, 0], [128, 32], [0, 32]], dtypefloat32) M cv2.getPerspectiveTransform(ordered, dst) return cv2.warpPerspective(roi, M, (128, 32)) # 调用box 来自 YOLOv8 的 xyxy # plate_img crop_and_rectify(frame, [x1, y1, x2, y2])逻辑说明先灰度 OTSU 二值化把车牌区域和背景分开再找最大轮廓做minAreaRect得到带角度的矩形。boxPoints拿到四个角点后按坐标排序映射到128x32的目标图透视变换把倾斜拉正。这一步做不做识别准确率能差 10 个点以上。参数说明目标尺寸128x32要和识别模型输入一致改模型输入就得同步改这里。OTSU 对光照不均的图可能失效可以换成自适应阈值cv2.adaptiveThreshold。如果轮廓找不准先做一次高斯模糊去噪。3.3 识别模型训练与字符集定义识别数据集就是一张张裁好的车牌小图 对应的字符串标签。字符集要提前定死比如CHARS [京,沪,粤,苏,浙,皖,闽,赣,鲁,豫,鄂,湘,津,冀,晋,蒙, 辽,吉,黑,桂,琼,渝,川,贵,云,藏,陕,甘,青,宁,新, 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 混淆字符集里直接不放模型就不会输出它们。训练用 CTC 或者逐位交叉熵都行逐位分类要求车牌位数固定蓝牌 7 位、绿牌 8 位可以按牌型分两个头。训练脚本核心就是DataLoader读图 标签转索引 交叉熵这里不展开重点说评估识别准确率要按“整串全对”算不能按单字符算单字符 99% 整串可能只有 90%。4. 把检测和识别拼成一条可跑的推理链路4.1 单张图片的端到端推理脚本两段模型都训好后推理就是串起来YOLOv8 出框 → 裁剪矫正 → 识别模型出字符。下面是一个最小可跑的推理脚本import cv2 import numpy as np from ultralytics import YOLO import torch det_model YOLO(runs/plate/exp1/weights/best.pt) rec_model torch.load(plate_rec.pth, map_locationcpu) rec_model.eval() def decode(pred): # pred: [T, num_classes] 或 [num_classes] idx pred.argmax(dim-1).tolist() return .join([CHARS[i] for i in idx if i ! 0]) # 0 为 CTC blank def infer(img_path): img cv2.imread(img_path) results det_model(img, conf0.4, iou0.5)[0] for box in results.boxes.xyxy.cpu().numpy(): plate crop_and_rectify(img, box) tensor torch.from_numpy(plate).permute(2, 0, 1).float().unsqueeze(0) / 255.0 with torch.no_grad(): pred rec_model(tensor) text decode(pred[0]) print(车牌号:, text) infer(test.jpg)逻辑说明conf0.4是检测置信度阈值太低会框到非车牌区域太高会漏检0.4 是常用起点。iou0.5做 NMS 去重。识别模型输出经过decode转成字符串CTC blank 索引为 0 要过滤掉。参数说明conf和iou要根据你的验证集调漏检多就降 conf误检多就升 conf。识别输入必须和训练时一致包括归一化方式这里除以 255和通道顺序BGR 还是 RGB训练用什么推理就用什么。4.2 批量测试与准确率统计单张跑通不算数要拿一批图统计端到端准确率。做法是准备一个测试集每张图有真值车牌号跑完对比import glob total, correct 0, 0 for img_path in glob.glob(testset/*.jpg): gt open(img_path.replace(.jpg, .txt)).read().strip() pred infer_and_return(img_path) # 改成返回字符串 total 1 if pred gt: correct 1 print(f端到端准确率: {correct/total:.4f})逻辑说明端到端准确率 检测对且识别全对的比例这个指标才是项目能不能上线的依据。检测 mAP 高不代表端到端高识别错一位整串就废。参数说明测试集要覆盖不同光照、角度、车牌类型否则统计出来的数字虚高。至少 200 张起步样本太少波动大。5. 车牌识别落地避坑5 个我踩过的真实坑5.1 检测框偏一点识别全错现象检测框肉眼看着挺准识别结果却经常错一两位。原因识别模型对输入裁剪很敏感框偏几个像素字符就被切掉边缘或者带进相邻字符。解决裁剪时在检测框基础上向外扩 5%~10%再做透视矫正给识别留余量同时训练识别模型时加入随机平移增强提升鲁棒性。5.2 字符集里混进 I 和 O现象模型把 1 认成 I、0 认成 O输出非法车牌号。原因字符集定义时把字母 I、O 也放进去了而真实车牌根本不用这两个字母模型学了多余的类。解决字符集里直接删掉 I 和 O从源头杜绝如果历史数据里有误标清洗掉。5.3 训练集和推理预处理不一致现象训练时准确率 98%部署后掉到 70%。原因训练用 RGB、除以 255推理用 BGR、没归一化输入分布完全变了。解决把预处理写成一个函数训练和推理共用同一份代码杜绝两套逻辑。这是最隐蔽也最致命的坑。5.4 小目标车牌检测漏检严重现象远处车牌、小尺寸车牌检测不到。原因imgsz640下小目标特征被下采样丢掉了。解决训练和推理都提到imgsz960或1280数据增强里加 mosaic 和 scale让模型见过小目标必要时用 P2 小目标检测头。5.5 新能源绿牌识别率低现象蓝牌识别很好绿牌经常错。原因绿牌 8 位、底色不同、字符排布不同训练集里绿牌样本太少。解决单独统计绿牌样本量至少补到和蓝牌同量级或者按牌型分两个识别头各自训练。6. 进阶把车牌识别跑到边缘设备上的取舍前面整套链路在服务器上跑没问题但真实项目经常要落到 RK3588、Hi3516 这类边缘板子上这时候取舍就来了。RK3588 部署 YOLOv8 有官方 RKNN 工具链检测模型转 RKNN 后能跑 NPU速度从 CPU 的几百毫秒降到几十毫秒。但识别模型如果结构太花哨转换时算子不支持就得换更朴素的 CNN 结构。我的习惯是检测用 YOLOv8n 转 RKNN识别用一个纯卷积 全连接的轻量网络避免 LSTM 这类转换麻烦的算子。量化用 INT8校准集准备 200 张真实场景图量化后精度掉超过 2 个点就回退 FP16。板端推理时检测和识别串行跑中间裁剪矫正用 OpenCV 的 ARM 优化版本整体单帧控制在 100ms 以内就算能打。验证方法很简单拿同一批测试图在服务器和板端各跑一遍对比端到端准确率差距在 1 个点以内说明量化没伤到关键特征。如果差距大先查预处理是否一致再查量化校准集是否覆盖了各种光照。最后说个我自己的教训别一上来就追求端到端大模型车牌识别这种封闭场景拆成检测 识别两段每段都能单独优化、单独替换才是工程上最稳的路。我早期图省事想用一个模型搞定结果调了两周还不如拆开跑一天的效果。希望帮到你。本文还有配套的精品资源点击获取