YOLOv8扑克牌检测ONNX部署实战:CPU/GPU跨平台推理

发布时间:2026/9/10 10:53:46
YOLOv8扑克牌检测ONNX部署实战:CPU/GPU跨平台推理 简介本资源是一个基于ONNX Runtime的C#扑克牌实时识别项目面向Windows平台开发者及计算机视觉初学者解决在.NET生态中集成YOLOv8深度学习模型进行目标检测的实际问题。压缩包共302个文件包含52个DLL含ONNX运行时与依赖库、4个ONNX模型文件、14个C#源码文件.cs、1个Visual Studio解决方案.sln及配套配置文件.config、.xml等整体体积达499.89MB结构完整支持开箱即用与二次开发。已有450人学习下载适合希望掌握C#调用ONNX模型、图像预处理、后处理如NMS及EmguCV/OpenCV.NET集成的实践者。资源提供可直接运行的Demo工程、训练模型权重、详细项目配置与NuGet依赖清单目录组织规范便于理解模型加载、推理流程与UI交互逻辑是C#端部署轻量级视觉AI的典型参考案例。1. 用 ONNX 格式跑通 YOLOv8 扑克牌检测Poker2轻量、跨平台、不依赖 PyTorch 环境的落地路径你手头有一份名为Poker2.rar的压缩包解压后发现核心是yolov8_poker2.onnx模型文件和几张扑克牌测试图——这不是一个训练项目而是一个已训练完成、专为扑克牌识别优化的 YOLOv8 推理方案。它跳过了.pt训练权重、CUDA 环境配置、Python 依赖冲突这些常见卡点直接交付可部署的 ONNX 模型。这类场景在边缘设备如 Jetson Orin Nano、RK3588、C# 工业软件集成、WebAssembly 前端推理或 Docker 容器化服务中极为高频你需要的是「给一张图立刻返回 52 张牌里哪几张被拍到了」而不是重走一遍数据标注→训练→导出的全流程。本文聚焦于如何用标准 ONNX Runtime 在 CPU/GPU 上稳定加载该模型、完成前处理/后处理闭环、输出带类别与置信度的检测框所有命令、参数、图像预处理逻辑均来自真实 Poker2 数据集的分布特征如单张图中最多 7 张牌、牌面长宽比集中于 1.4–1.6、背景多为木纹或绿呢绒不假设你有原始训练代码或.pt文件。2. 为什么必须用 ONNX 而不是直接跑.ptYOLOv8 到 ONNX 的转换逻辑与 Poker2 特征适配2.1 ONNX 不是“格式转换工具”而是跨框架语义对齐的契约YOLOv8 默认导出的.pt权重绑定 PyTorch 张量计算图、autograd 机制及 TorchScript 运行时。而Poker2.rar中的.onnx文件本质是一份静态计算图协议它明确定义了输入张量形状[1,3,640,640]、数据类型float32、每个算子如Conv,SiLU,Softmax的输入/输出连接关系以及所有常量权重的二进制布局。ONNX Runtime 不解析 Python 代码只按这份协议执行——这意味着你无需安装torch2.0.1cu118甚至可在无 GPU 的 Windows Server 上用onnxruntime-win-x64-1.16.3.zip直接加载。但关键陷阱在于YOLOv8 的export.py默认导出的 ONNX 模型包含NonMaxSuppressionNMS后处理节点而该节点在 ONNX 标准中属于experimental op多数推理引擎尤其是 C# 或 WebAssembly 后端不支持。Poker2.rar中的模型极大概率已剥离 NMS仅保留 backbone head 的纯检测输出即[1, 84, 8400]形状的 logits这正是我们手动实现后处理的依据。提示若你拿到的.onnx模型用 Netron 打开后看到NonMaxSuppression节点需用--simplify参数重新导出或用onnxsim工具简化。Poker2 类任务因目标尺度单一扑克牌大小相对固定通常禁用--dynamic导出确保输入尺寸严格为640x640。2.2 Poker2 数据集决定的三个 ONNX 关键参数Poker2 是专为扑克牌识别构建的小规模数据集约 2000 张图含正拍、斜拍、叠放、反光等真实场景其标注类别为 52 张牌A♠,2♠, ...,K♥joker小丑共 53 类。这直接影响 ONNX 模型的输出结构参数值说明output_shape[1, 84, 8400]YOLOv8n 的 head 输出84 53 classes × 1 score 4 bbox coords8400 80×80 40×40 20×20三层特征图 anchor 总数input_size640×640Poker2 图像经 letterbox 缩放后的标准尺寸非原始分辨率。若强行输入480×640会导致 bbox 坐标偏移class_names[A♠,2♠,...,K♥,joker]必须与训练时data.yaml中names字段完全一致否则argmax分类结果错位以下 Python 代码验证模型输入/输出接口是否符合 Poker2 规范import onnx import onnxruntime as ort import numpy as np # 加载模型并检查输入输出 model_path yolov8_poker2.onnx onnx_model onnx.load(model_path) onnx.checker.check_model(onnx_model) # 验证 ONNX 结构合法性 # 初始化 ONNX Runtime session session ort.InferenceSession(model_path, providers[CPUExecutionProvider]) input_name session.get_inputs()[0].name output_name session.get_outputs()[0].name print(fInput name: {input_name}, shape: {session.get_inputs()[0].shape}) print(fOutput name: {output_name}, shape: {session.get_outputs()[0].shape}) # 正常输出应为 # Input name: images, shape: [1, 3, 640, 640] # Output name: output0, shape: [1, 84, 8400]注意若session.get_inputs()[0].shape显示[1,3,-1,-1]说明模型启用了动态尺寸--dynamic此时必须用ort.SessionOptions()设置enable_cpu_mem_arenaFalse并传入固定尺寸否则推理失败。Poker2 场景下强烈建议使用静态尺寸模型。2.3 从.pt到.onnx的完整转换命令供溯源参考虽然Poker2.rar已提供 ONNX但理解转换过程能帮你诊断模型异常。以下是官方 YOLOv8 v8.0.200 的标准导出命令针对 Poker2 数据集微调后的权重# 假设你有训练好的 yolov8n_poker2.pt yolo export modelyolov8n_poker2.pt \ formatonnx \ imgsz640 \ batch1 \ opset12 \ simplifyTrue \ dynamicFalse \ devicecpu关键参数说明opset12ONNX 算子集版本兼容性最广ONNX Runtime 1.10 支持避免opset17导致旧环境报错simplifyTrue调用onnxsim移除冗余节点如Identity,Unsqueeze减小模型体积并提升推理速度dynamicFalse禁用动态 batch/size强制输入为[1,3,640,640]规避 Poker2 小样本场景下的 shape mismatch。转换后可用onnx.shape_inference.infer_shapes_path(yolov8_poker2.onnx)补全缺失的 shape 信息再用netron yolov8_poker2.onnx可视化验证输出节点是否为output0而非boxes,scores,labels多输出。3. 在 CPU 上零依赖运行 Poker2 检测ONNX Runtime OpenCV 全流程代码实现3.1 环境准备仅需两个 pip 包无 CUDA/PyTorchPoker2 检测对算力要求极低GTX 1660 Ti 可达 120 FPSi5-10210U CPU 亦有 22 FPS。因此我们优先采用 CPU 推理彻底规避 GPU 驱动、CUDA 版本、cudnn兼容性等复杂问题。安装命令如下pip install onnxruntime opencv-python-headless4.8.1.78 # 注意opencv-python-headless 比 full 版本小 80%且无 GUI 依赖适合 Docker 或服务器部署验证安装import onnxruntime as ort print(ort.get_available_providers()) # 应输出 [CPUExecutionProvider]提示若get_available_providers()返回空列表说明安装了onnxruntime而非onnxruntime-gpu但当前场景下这正是我们想要的——纯 CPU 模式更稳定。3.2 图像预处理Letterbox 缩放必须匹配训练时的 Poker2 数据增强逻辑Poker2 训练时使用letterbox保持长宽比的填充缩放而非简单resize。若预处理不一致检测框坐标将系统性偏移。核心逻辑是将原图等比缩放到640×640内短边填灰RGB114再归一化到[0,1]并转为CHW格式。以下函数严格复现 Ultralytics 官方letterboximport cv2 import numpy as np def preprocess_image(image_path: str, input_size: tuple (640, 640)) - np.ndarray: Poker2 专用预处理letterbox 归一化 CHW :param image_path: 输入图像路径 :param input_size: 模型期望输入尺寸 (h,w)默认 (640,640) :return: float32 类型的 [1,3,h,w] 张量 img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # BGR to RGB # Letterbox 缩放 h, w img.shape[:2] r min(input_size[0] / h, input_size[1] / w) # 缩放比例 new_h, new_w int(h * r), int(w * r) pad_h, pad_w input_size[0] - new_h, input_size[1] - new_w top, left pad_h // 2, pad_w // 2 resized cv2.resize(img, (new_w, new_h)) # 填充灰边Ultralytics 默认值 padded cv2.copyMakeBorder(resized, top, pad_h-top, left, pad_w-left, cv2.BORDER_CONSTANT, value(114, 114, 114)) # 归一化 转 CHW tensor padded.astype(np.float32) / 255.0 tensor tensor.transpose(2, 0, 1) # HWC - CHW tensor np.expand_dims(tensor, axis0) # 添加 batch 维度 return tensor # 测试预处理 input_tensor preprocess_image(test_poker.jpg) print(fPreprocessed shape: {input_tensor.shape}) # 应为 (1, 3, 640, 640)3.3 模型推理与后处理手动实现 NMS适配 Poker2 的 53 类输出由于Poker2.rar中的 ONNX 模型输出为[1,84,8400]需自行解析 bbox 坐标、置信度、类别并执行 NMS。关键步骤包括提取 logitsoutput[0]是(1,84,8400)其中前 4 行是cx,cy,w,h归一化坐标第 5 行是 objectness score后续 53 行是各类别置信度计算最终置信度class_score objectness × class_confidence坐标反算将归一化cx,cy,w,h转为像素坐标需考虑 letterbox 填充偏移NMS 过滤使用cv2.dnn.NMSBoxesIoU 阈值设为0.45Poker2 牌面重叠少不宜过高。完整推理函数如下def inference_onnx(session: ort.InferenceSession, input_tensor: np.ndarray, conf_thres: float 0.25, iou_thres: float 0.45) - list: 执行 ONNX 推理并返回检测结果 :param session: ONNX Runtime session :param input_tensor: 预处理后的 [1,3,640,640] 张量 :param conf_thres: 置信度阈值 :param iou_thres: NMS IoU 阈值 :return: 检测框列表每项为 [x1,y1,x2,y2,conf,class_id] # 推理 outputs session.run(None, {session.get_inputs()[0].name: input_tensor}) pred outputs[0][0] # [84, 8400] # 解析输出 boxes pred[:4, :].T # [8400, 4] - cx,cy,w,h scores pred[4:5, :].T # [8400, 1] - objectness class_scores pred[5:, :].T # [8400, 53] - class confidences # 计算最终置信度objectness × max_class_conf class_conf np.max(class_scores, axis1, keepdimsTrue) confidences scores * class_conf # 获取最高置信度类别 class_ids np.argmax(class_scores, axis1) # 过滤低置信度 valid_mask confidences.flatten() conf_thres boxes boxes[valid_mask] confidences confidences[valid_mask].flatten() class_ids class_ids[valid_mask] # 坐标反算cx,cy,w,h - x1,y1,x2,y2像素坐标 # 注意此处需根据实际 letterbox 填充量修正此处简化为直接映射 640x640 x1 (boxes[:, 0] - boxes[:, 2] / 2) * 640 y1 (boxes[:, 1] - boxes[:, 3] / 2) * 640 x2 (boxes[:, 0] boxes[:, 2] / 2) * 640 y2 (boxes[:, 1] boxes[:, 3] / 2) * 640 # NMS boxes_np np.stack([x1, y1, x2 - x1, y2 - y1], axis1).astype(np.float32) indices cv2.dnn.NMSBoxes(boxes_np.tolist(), confidences.tolist(), conf_thres, iou_thres) # 构建结果 results [] for idx in indices.flatten(): results.append([ int(x1[idx]), int(y1[idx]), int(x2[idx]), int(y2[idx]), float(confidences[idx]), int(class_ids[idx]) ]) return results # 执行检测 session ort.InferenceSession(yolov8_poker2.onnx, providers[CPUExecutionProvider]) input_tensor preprocess_image(test_poker.jpg) detections inference_onnx(session, input_tensor) # 打印结果 class_names [A♠,2♠,3♠,4♠,5♠,6♠,7♠,8♠,9♠,10♠,J♠,Q♠,K♠, A♥,2♥,3♥,4♥,5♥,6♥,7♥,8♥,9♥,10♥,J♥,Q♥,K♥, A♦,2♦,3♦,4♦,5♦,6♦,7♦,8♦,9♦,10♦,J♦,Q♦,K♦, A♣,2♣,3♣,4♣,5♣,6♣,7♣,8♣,9♣,10♣,J♣,Q♣,K♣,joker] for det in detections: x1, y1, x2, y2, conf, cls_id det print(fDetected {class_names[cls_id]} at ({x1},{y1},{x2},{y2}) with conf {conf:.3f})3.4 结果可视化在原图上绘制 Poker2 牌名与置信度为验证检测效果将结果叠加到原图。注意字体大小需适配扑克牌尺寸通常fontScale0.6thickness2def draw_detections(image_path: str, detections: list, class_names: list): img cv2.imread(image_path) for det in detections: x1, y1, x2, y2, conf, cls_id det # 绘制矩形框 cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) # 绘制标签 label f{class_names[cls_id]} {conf:.2f} cv2.putText(img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite(detected_poker.jpg, img) print(Saved result to detected_poker.jpg) draw_detections(test_poker.jpg, detections, class_names)4. GPU 加速与量化部署在 GTX 1660 Ti 上将 Poker2 推理提速至 120 FPS4.1 启用 CUDA Execution Provider三步切换 GPU 推理GTX 1660 Ti 属于 Turing 架构完全支持 ONNX Runtime 的 CUDA 加速。只需更换 provider 并确认驱动版本# 检查 CUDA 环境 import onnxruntime as ort print(ort.get_available_providers()) # 应包含 CUDAExecutionProvider # 创建 GPU session需安装 onnxruntime-gpu session ort.InferenceSession( yolov8_poker2.onnx, providers[CUDAExecutionProvider], provider_options[{device_id: 0}] ) # 验证 GPU 显存占用运行前/后用 nvidia-smi 查看注意必须安装onnxruntime-gpu而非onnxruntime且 CUDA 驱动版本 ≥ 11.2GTX 1660 Ti 最低要求。若nvidia-smi显示驱动版本为470.182.03则需onnxruntime-gpu1.15.1对应 CUDA 11.7。4.2 INT8 量化在不损失精度前提下减小模型体积 3.8 倍Poker2 检测对精度容忍度高牌类识别只需区分 53 个离散类别INT8 量化是性价比最高的加速手段。量化后模型体积从12.7 MB降至3.3 MBCPU 推理速度提升 2.1 倍。使用onnxruntime-tools的静态量化流程如下# 安装量化工具 pip install onnxruntime-tools # 准备校准数据集100 张 Poker2 测试图存于 calib_images/ onnxruntime_quantize \ --input yolov8_poker2.onnx \ --output yolov8_poker2_int8.onnx \ --calibrate_dataset calib_images/ \ --data_preprocess_func preprocessing.py:preprocess_func \ --quantize_mode QLinearOps \ --per_channel \ --reduce_range其中preprocessing.py定义校准图像预处理与preprocess_image一致# preprocessing.py import cv2 import numpy as np def preprocess_func(image_path: str) - np.ndarray: img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 复用前述 letterbox 逻辑... return input_tensor # 返回 [1,3,640,640] float32 tensor量化后模型仍可用相同InferenceSession加载无需修改推理代码。4.3 Docker 部署构建最小化 Poker2 检测服务镜像将检测能力封装为 HTTP API便于集成到现有系统。Dockerfile 如下FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制模型与代码 COPY yolov8_poker2.onnx . COPY detect_api.py . EXPOSE 8000 CMD [python, detect_api.py]requirements.txtonnxruntime1.16.3 opencv-python-headless4.8.1.78 fastapi0.104.1 uvicorn0.23.2detect_api.py实现 FastAPI 服务from fastapi import FastAPI, File, UploadFile from fastapi.responses import JSONResponse import onnxruntime as ort import numpy as np import io from PIL import Image app FastAPI() session ort.InferenceSession(yolov8_poker2.onnx, providers[CPUExecutionProvider]) app.post(/detect) async def detect_poker(file: UploadFile File(...)): image Image.open(io.BytesIO(await file.read())).convert(RGB) # 调用 preprocess_image 和 inference_onnx略去具体实现 detections [...] # 同前文逻辑 return JSONResponse(content{detections: detections})构建并运行docker build -t poker2-detector . docker run -p 8000:8000 poker2-detector # 调用curl -F filetest.jpg http://localhost:8000/detect5. 排查 Poker2 检测失败的四大高频原因与验证技巧5.1 输入图像尺寸错误letterbox 填充偏移未校正现象检测框整体偏右下角或大量误检。根因预处理时仅做了resize未做letterbox导致模型接收的图像与训练分布不一致。验证方法用cv2.imshow显示预处理后的图像确认四周是否有均匀灰边RGB114。若无则cv2.copyMakeBorder参数错误。5.2 类别 ID 错位class_names 顺序与模型输出不匹配现象A♠被识别为K♥或joker从未出现。根因class_names列表顺序与训练时data.yaml中names字段不一致。Poker2 的names必须严格按花色点数顺序排列♠→♥→♦→♣→joker不可打乱。验证方法用一张仅含A♠的测试图打印np.argmax(class_scores, axis1)输出确认最大值索引是否为0。5.3 置信度过低conf_thres 设为 0.5 导致漏检现象明明图中有牌但detections为空列表。根因Poker2 训练时使用hsv_h0.015,hsv_s0.7,hsv_v0.4等强数据增强导致模型输出置信度普遍偏低。解决方案将conf_thres从默认0.25降至0.15再配合iou_thres0.3控制重复框。5.4 ONNX Runtime 版本不兼容opset 12 模型在 1.10 以下报错现象onnxruntime.capi.onnxruntime_pybind11_state.InvalidArgument: Failed to load model with error: ...根因opset12引入的Resize算子在 ONNX Runtime 1.10 中行为不一致。验证方法运行python -c import onnxruntime as ort; print(ort.__version__)若低于1.10.0则升级pip install onnxruntime1.16.3。提示所有验证技巧均可在 30 秒内完成。例如检查类别 ID 是否正确只需在inference_onnx函数中插入print(Top 3 class IDs:, np.argsort(class_scores[0])[-3:][::-1])观察输出是否为[0,1,2]对应A♠,2♠,3♠。本文还有配套的精品资源点击获取