ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv8+LPRNet车牌识别系统实战:检测与识别解耦方案

YOLOv8+LPRNet车牌识别系统实战:检测与识别解耦方案 简介本资源是一套基于YOLOv8目标检测与LPRNet端到端车牌识别的完整Python实现系统面向计算机、电子信息、人工智能等专业的本科生及入门级开发者适用于课程设计、期末大作业与毕业设计等实践场景帮助学习者掌握多模型协同的视觉识别工程落地方法。压缩包共60个文件包含13个核心Python脚本如main.py、train.py、split_dataset.py、3个PyTorch模型文件.pt/.pth、22张示例图像.jpg/.png、3个配置文件.yaml/.yml/.json以及Vue前端模块.vue/.js/.html整体体积36.15MB结构清晰涵盖数据预处理、模型训练、推理部署与Web交互全流程。目前已有1171人学习下载。用户可直接运行源码完成车牌定位与字符识别获取含Flask后端服务、LPRNet轻量识别模型、YOLOv8训练配置及批量图像处理工具的全栈方案并参考目录中generate_lpr_data.py、batch_resize_images.py等实用脚本快速构建自有数据集。1. 为什么用 YOLOv8 LPRNet 搭建车牌识别系统比单模型方案更稳、更快、更准你手头有一段监控视频想自动提取所有经过车辆的车牌号——不是只检测框出车牌位置而是真正读出“粤B12345”“沪A6789X”这样的完整字符。如果只用一个端到端模型比如某些轻量级 CRNN 或直接回归字符的网络在夜间低照度、雨雾遮挡、车牌反光或角度倾斜超过25°时识别率常跌破60%。而 YOLOv8 LPRNet 的组合本质是把「定位」和「识别」两个强耦合但技术路径迥异的任务解耦YOLOv8 专注在复杂背景中以高召回率、低误检率框出车牌区域mAP0.5 达 92.3%LPRNet 则在裁剪后的规整 ROI 上做字符级序列识别准确率超 96.5%推理耗时仅 8–12ms/图。这套方案不依赖 GPU 高算力GTX 1660 Ti 即可实时处理 1080p15fps代码结构清晰、模块可替换性强特别适合安防集成商、智慧停车系统开发者、高校课程设计及边缘部署场景。本文不讲论文复现只聚焦如何用 Python 源码包快速跑通、调参、定位失败样本并把识别结果稳定接入业务逻辑。2. YOLOv8 车牌检测模块从模型加载、预处理到坐标后处理的全链路实现YOLOv8 在车牌检测任务中胜出核心在于其 C2f 结构对小目标车牌宽高比约 3:1占画面比例常低于 2%的特征融合能力更强且 Neck 层的 PANet 路径聚合能有效缓解多尺度车牌漏检。本方案采用yolov8n.ptnano 版作为基础检测模型兼顾速度与精度在 1280×720 输入下平均推理时间 18msRTX 3060mAP0.5 达 91.7%。2.1 模型加载与输入预处理适配车牌长宽比的 resize 策略YOLOv8 默认使用letterbox等比缩放灰边填充保证长宽比但车牌区域本身是细长矩形直接填满会导致水平方向压缩失真影响后续 LPRNet 的字符分割。我们改用自适应宽高比保持缩放aspect-aware resizeimport cv2 import numpy as np from ultralytics import YOLO def preprocess_for_plate(img, target_size(640, 640)): h, w img.shape[:2] # 计算缩放比例优先满足宽度 ≤ target_w高度按比例缩放 scale min(target_size[0] / w, target_size[1] / h) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h)) # 填充至 target_size但只在底部/右侧补零避免车牌被顶部/左侧灰边干扰 pad_w target_size[0] - new_w pad_h target_size[1] - new_h padded cv2.copyMakeBorder(resized, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT, value(114, 114, 114)) # 归一化 增加 batch 维度 tensor padded.astype(np.float32) / 255.0 tensor np.transpose(tensor, (2, 0, 1)) # HWC → CHW tensor np.expand_dims(tensor, axis0) # → (1, 3, H, W) return tensor, (scale, pad_w, pad_h) # 加载模型需提前下载 yolov8n.pt 至当前目录 model YOLO(yolov8n.pt)提示cv2.copyMakeBorder的BORDER_CONSTANT值设为(114, 114, 114)是 YOLOv8 官方训练时的灰边均值与模型权重对齐若用自定义数据集训练应替换为训练时实际使用的 padding 值。2.2 推理与原始坐标还原处理 NMS 后的 bbox 并映射回原图YOLOv8 输出的 bbox 坐标是归一化到[0,1]的且已过 NMS默认conf0.25,iou0.7。关键在于将预测框精准还原至原始图像尺寸def detect_plates(model, img_path): img cv2.imread(img_path) tensor, (scale, pad_w, pad_h) preprocess_for_plate(img) # YOLOv8 推理返回 Results 对象 results model(tensor, verboseFalse) boxes results[0].boxes.xyxy.cpu().numpy() # (N, 4) xyxy 格式 confs results[0].boxes.conf.cpu().numpy() # (N,) 置信度 # 还原坐标先减去 padding再除以 scale if len(boxes) 0: # 减去右侧/底部 padding boxes[:, [0, 2]] - pad_w boxes[:, [1, 3]] - pad_h # 除以缩放比例 boxes / scale # 截断到图像边界防止浮点误差越界 boxes[:, [0, 2]] np.clip(boxes[:, [0, 2]], 0, img.shape[1]) boxes[:, [1, 3]] np.clip(boxes[:, [1, 3]], 0, img.shape[0]) return img, boxes, confs # 示例调用 orig_img, bboxes, scores detect_plates(model, test_car.jpg) print(f检测到 {len(bboxes)} 个车牌候选框)表YOLOv8 检测参数对车牌场景的影响实测 GTX 1660 Ti参数默认值推荐值效果说明验证方式conf0.250.35提升高置信度框占比减少误检如车灯、反光带统计scores 0.35的框数占比iou0.70.5降低 NMS 阈值避免相邻车牌如并行车被合并目视检查双车同框是否漏检imgsz6401280大尺寸提升小车牌召回但推理12mstimeit测速 mAP0.5 变化halfFalseTrueFP16 推理提速 1.8×精度损失 0.3% AP对比results[0].boxes.conf.mean()注意imgsz1280时需确保显存 ≥ 6GB若显存不足可改用model.export(formatonnx, halfTrue)导出 ONNX 后用 ONNX Runtime 推理内存占用降低 40%。3. LPRNet 字符识别模块从 ROI 裁剪、归一化到序列解码的端到端流程LPRNet 是专为车牌字符识别设计的轻量级 CNNCTC 模型无 RNN 层纯卷积结构使其在嵌入式设备上也能达到 10ms 级延迟。其输入固定为94×24宽×高输出为 68 类字符含数字、字母、省份简称及空白符的序列概率通过 CTC 解码得到最终车牌号。3.1 ROI 裁剪与标准化解决车牌倾斜、透视畸变的关键预处理YOLOv8 输出的是轴对齐矩形框AABB但真实车牌存在旋转与透视。直接裁剪会导致字符拉伸或截断。我们采用四点透视校正Perspective Warpdef warp_plate_roi(img, bbox): x1, y1, x2, y2 map(int, bbox) roi img[y1:y2, x1:x2].copy() # 估算车牌四角简化版假设车牌为刚性矩形用 bbox 中心宽高比推算 h, w roi.shape[:2] plate_ratio 3.0 # 车牌宽高比 ≈ 3:1 if w / h 2.5: # 若裁剪框过窄说明倾斜严重启用角点检测 # 使用霍夫直线检测上下边缘仅示意生产环境建议用 PnP 或关键点回归 gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) edges cv2.Canny(gray, 50, 150) lines cv2.HoughLinesP(edges, 1, np.pi/180, threshold30, minLineLength20, maxLineGap5) if lines is not None and len(lines) 2: # 取最长的两条线作为上下边拟合四角此处省略具体几何计算 pass # 简化策略对 bbox 做 ±5° 微调生成 3 组候选 warp选 CTC 置信度最高者 candidates [] for angle in [-5, 0, 5]: M cv2.getRotationMatrix2D((w//2, h//2), angle, 1.0) rotated cv2.warpAffine(roi, M, (w, h)) candidates.append(rotated) return candidates[1] # 默认取 0°进阶可并行推理三张选最优 def preprocess_lprnet(roi): # 调整大小至 94x24双三次插值保细节 resized cv2.resize(roi, (94, 24), interpolationcv2.INTER_CUBIC) # 灰度化 直方图均衡化增强对比度 gray cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY) equalized cv2.equalizeHist(gray) # 归一化至 [-1, 1]LPRNet 训练时的输入范围 tensor (equalized.astype(np.float32) / 127.5) - 1.0 tensor np.expand_dims(tensor, axis0) # (1, 24, 94) tensor np.expand_dims(tensor, axis0) # (1, 1, 24, 94) return tensor3.2 LPRNet 模型加载与 CTC 解码解析 logits 并过滤低置信字符本方案使用 PyTorch 实现的 LPRNetlprnet.pth输入 shape(1,1,24,94)输出 shape(1,68,18)18 为最大字符数68 为字符类别数import torch import torch.nn as nn class LPRNet(nn.Module): def __init__(self, class_num68, dropout_rate0.5): super().__init__() # ... 定义 LPRNet 结构此处省略源码包中已提供 def forward(self, x): # ... 前向传播 return logits # (batch, class_num, seq_len) # 加载模型 device torch.device(cuda if torch.cuda.is_available() else cpu) lprnet LPRNet(class_num68).to(device) lprnet.load_state_dict(torch.load(lprnet.pth, map_locationdevice)) lprnet.eval() # 字符映射表按 LPRNet 训练时的顺序 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, -] # 共 68 类 def decode_ctc(logits, blank0): CTC greedy decode返回字符串及各字符置信度 probs torch.nn.functional.softmax(logits, dim1) # (class_num, seq_len) pred torch.argmax(probs, dim0).cpu().numpy() # (seq_len,) # 合并重复 移除 blank prev -1 decoded [] confs [] for p in pred: if p ! blank and p ! prev: decoded.append(p) confs.append(probs[p, len(decoded)-1].item()) prev p # 映射为字符 text .join([CHARS[i] for i in decoded if i len(CHARS)]) return text, confs # 完整识别函数 def recognize_plate(lprnet, roi): tensor preprocess_lprnet(roi) input_tensor torch.from_numpy(tensor).to(device) with torch.no_grad(): logits lprnet(input_tensor) # (1, 68, 18) text, confs decode_ctc(logits[0]) # 取 batch 第 0 张 # 过滤低置信字符0.6 if confs and min(confs) 0.6: text text[:len(confs)] # 截断至可靠部分 return text # 示例对第一个检测框识别 if len(bboxes) 0: roi warp_plate_roi(orig_img, bboxes[0]) plate_text recognize_plate(lprnet, roi) print(f识别结果: {plate_text})提示decode_ctc中blank0对应 CHARS[0]京需确认你的lprnet.pth是否使用相同索引顺序若识别结果首字符总为‘京’大概率是索引错位应检查CHARS定义与模型训练时一致。4. 系统级联与鲁棒性增强多帧融合、置信度加权与失败样本诊断单帧识别易受瞬时噪声干扰如雨滴遮挡、闪光过曝。本方案引入时间维度冗余校验并对识别失败样本提供可追溯的诊断路径。4.1 多帧投票机制基于置信度加权的车牌号融合对连续 N 帧推荐 N5的同一车牌 ROI收集所有识别结果及其 CTC 置信度均值按加权投票确定最终车牌号from collections import defaultdict import numpy as np def fuse_plate_results(results_list): results_list: [{text: 粤B12345, conf: 0.92}, ...] 返回最高票文本及综合置信度 if not results_list: return , 0.0 # 统计文本出现次数及对应置信度 vote_count defaultdict(float) vote_conf defaultdict(list) for r in results_list: text r[text] conf r[conf] vote_count[text] conf # 置信度加权计票 vote_conf[text].append(conf) # 选加权和最高的文本 best_text max(vote_count.keys(), keylambda x: vote_count[x]) avg_conf np.mean(vote_conf[best_text]) return best_text, avg_conf # 视频流处理伪代码 cap cv2.VideoCapture(traffic.mp4) frame_count 0 plate_buffer [] # 存储最近 5 帧的识别结果 while cap.isOpened(): ret, frame cap.read() if not ret: break # YOLOv8 检测 _, bboxes, scores detect_plates(model, frame) # 此处需传入 frame 而非路径 if len(bboxes) 0 and scores[0] 0.4: roi warp_plate_roi(frame, bboxes[0]) text recognize_plate(lprnet, roi) conf 0.85 # 此处应为 CTC 解码时的平均置信度简化示意 plate_buffer.append({text: text, conf: conf}) # 每 5 帧融合一次 if len(plate_buffer) 5: final_plate, final_conf fuse_plate_results(plate_buffer) print(f[Frame {frame_count}] Final plate: {final_plate} (conf: {final_conf:.3f})) plate_buffer [] # 清空缓冲区 frame_count 14.2 失败样本诊断自动保存低置信 ROI 与可视化分析当某帧识别置信度0.5或字符数!7/8蓝牌7位、黄牌8位、新能源9位自动保存 ROI 图像及原始检测框供人工复核def save_failure_case(orig_img, bbox, roi, text, conf, reason): timestamp int(time.time()) # 保存原始图检测框 vis_img orig_img.copy() x1, y1, x2, y2 map(int, bbox) cv2.rectangle(vis_img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(vis_img, f{reason}: {text}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) cv2.imwrite(ffailures/{timestamp}_detect.jpg, vis_img) # 保存 ROI cv2.imwrite(ffailures/{timestamp}_roi.jpg, roi) # 保存日志 with open(failures/log.txt, a) as f: f.write(f{timestamp}, {reason}, {text}, {conf:.3f}\n) # 在 recognize_plate 后添加判断 if len(text) not in [7, 8, 9] or (confs and min(confs) 0.5): save_failure_case(orig_img, bboxes[0], roi, text, min(confs) if confs else 0.0, LENGTH_OR_CONF_LOW)注意failures/目录需提前创建日志文件可用于统计高频失败模式如“雨天模糊”“夜间过曝”指导后续数据增强策略。5. 部署优化与常见问题排查从 Windows 本地调试到 Linux 服务化本方案源码包.zip已包含requirements.txt、config.py及预训练模型但实际部署常遇环境兼容性问题。以下为高频问题与解决方案。5.1 环境配置避坑指南Python、CUDA、PyTorch 版本黄金组合组件推荐版本关键原因验证命令Python3.9.16YOLOv8 8.0.200 与 PyTorch 2.0 兼容最佳避免 3.11 的typing模块变更python --versionCUDA11.8支持 RTX 30/40 系显卡且与 PyTorch 2.0.1 官方 wheel 匹配nvcc --versionPyTorch2.0.1cu118torch2.0.1 torchvision0.15.2 torchaudio2.0.2python -c import torch; print(torch.__version__, torch.version.cuda)OpenCV4.8.1修复了cv2.warpPerspective在 ARM 设备上的内存泄漏python -c import cv2; print(cv2.__version__)安装命令Linux# 创建虚拟环境 python3.9 -m venv venv_lpr source venv_lpr/bin/activate # 安装 PyTorchCUDA 11.8 pip3 install torch2.0.1cu118 torchvision0.15.2cu118 torchaudio2.0.2cu118 -f https://download.pytorch.org/whl/torch_stable.html # 安装其他依赖 pip install -r requirements.txt # 若报 cv2 错误单独重装 pip uninstall opencv-python -y pip install opencv-python4.8.1.785.2 服务化封装用 Flask 暴露 REST API支持 HTTP 图片上传将识别逻辑封装为 Web 服务便于前端或 IoT 设备调用from flask import Flask, request, jsonify import base64 import io app Flask(__name__) app.route(/recognize, methods[POST]) def api_recognize(): try: # 解析 base64 图片 data request.get_json() img_bytes base64.b64decode(data[image]) img_array np.frombuffer(img_bytes, np.uint8) img cv2.imdecode(img_array, cv2.IMREAD_COLOR) # 执行检测识别 _, bboxes, _ detect_plates(model, img) # 此处需修改 detect_plates 以接受 img 数组 if len(bboxes) 0: return jsonify({plate: , confidence: 0.0, error: no_plate_detected}) roi warp_plate_roi(img, bboxes[0]) text recognize_plate(lprnet, roi) return jsonify({ plate: text, confidence: float(np.mean(confs)) if confs in locals() else 0.0, bbox: bboxes[0].tolist() }) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境禁用 debug调用示例curlcurl -X POST http://localhost:5000/recognize \ -H Content-Type: application/json \ -d {image:base64_encoded_string_here}5.3 三个必查的识别失败根因与修复动作表现象根因快速验证方法修复动作检测框完全丢失YOLOv8 输入尺寸与模型训练尺寸不匹配检查preprocess_for_plate中target_size是否与yolov8n.pt训练时一致默认 640修改target_size(640,640)并确认模型未被重新导出为其他尺寸识别结果为空字符串LPRNet 输入 tensor shape 错误如(1,3,24,94)误传在recognize_plate中打印input_tensor.shape确保preprocess_lprnet返回(1,1,24,94)用cv2.cvtColor(..., cv2.COLOR_BGR2GRAY)保证单通道字符顺序错乱如“粤B12345”→“粤1B2345”CTC 解码时未正确处理重复字符合并打印pred数组原始 argmax 序列观察是否出现A,A,B,B类模式检查decode_ctc中prev初始化与更新逻辑确保if p ! blank and p ! prev:条件成立提示所有修复后务必用test_samples/下的 10 张典型失败图含雨天、夜间、倾斜进行回归测试确保问题真正解决而非掩盖。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进