使用 TensorRtSharp4.0 在 C# 中运行 YOLOv8n 图像分类

图像分类的输出不是检测框,而是一组“整张图片属于哪个类别”的概率。本教程从一个空的 .NET 控制台项目开始,演示如何获取官方 YOLOv8n-cls 权重、转换并暂存 ONNX、生成 C# 实际预处理 tensor、建立独立 ONNX Runtime 参考、通过公开 NuGet 包运行 TensorRT,最后把 Top-5 分类结果叠加到原图并保存程序窗口截图。

本文使用的图片是一辆伦敦双层巴士。最终 Top-1 是 fire_engine,而不是人工期望的 bus。这不是隐藏掉的坏结果:本文验证的是 C# 预处理、TensorRT 输出和独立 CPU reference 是否一致,不把单张图片的分类结果包装成模型精度证明。

本文使用的项目与库

项目或库 本文职责
TensorRtSharp4.0 提供 owner-safe 的 TensorRT/CUDA 托管接口、ONNX 构建和推理执行。
YoloVision 提供图像预处理、YOLO 分类解码、Top-K、JSON 和 SVG 可视化。
TensorRT 10.11 在 NVIDIA GPU 上构建并执行静态 ONNX 网络。
CUDA 12.9 / cuDNN 9.22 由用户自行安装,作为 TensorRT 的系统依赖。
Ultralytics 提供 YOLOv8n-cls 权重、ImageNet 类别表和 ONNX 导出工具。
ONNX Runtime CPU 使用 C# 实际输入 tensor 生成独立的 1,000 值参考输出。

本地消费项目只引用以下三个包:

  1. JYPPX.TensorRT.CSharp.API
  2. applications/YoloVision
  3. JYPPX.TensorRT.CSharp.API.Runtime.win-x64.trt10.11.cuda12.9.cudnn9.22.Bridge

第三个包只包含 jyppxtrtbridge.dll。CUDA、cuDNN、TensorRT 和 NVRTC 不会被打进 NuGet 包。

完整流程如下:

flowchart LR
    A["获取 YOLOv8n-cls 权重"] --> B["导出 ONNX"]
    B --> C["暂存到外层 models"]
    D["下载 CC0 巴士图片"] --> E["C# center-crop 预处理"]
    E --> F["ORT 生成 C# tensor reference"]
    C --> G["外部三包消费项目"]
    F --> G
    G --> H["TensorRT GPU 推理"]
    H --> I["全 1,000 概率比较"]
    H --> J["原图 Top-5 与终端截图"]

准备环境与工作目录

需要准备:

  • Windows x64 和支持 CUDA 的 NVIDIA GPU;
  • .NET 8 SDK;
  • TensorRT 10.11、CUDA 12.9、cuDNN 9.22;
  • 含 PyTorch、Ultralytics、ONNX、ONNX Runtime、OpenCV 和 Pillow 的 Python 环境。

以下命令假设当前目录是仓库根目录。所有大文件都写到 Git 仓库外层:

$repoRoot = (Resolve-Path '.').Path
$workspaceRoot = Split-Path $repoRoot -Parent
$python = '<python-with-ultralytics>'

$assetRoot = Join-Path $workspaceRoot 'downloads/yolov8n-cls-ultralytics-v8.3.0'
$modelRoot = Join-Path $workspaceRoot 'models/YoloVision/Classification/yolov8n-cls-ultralytics-v8.3.0'
$articleInputRoot = Join-Path $workspaceRoot 'downloads/article-assets/yolovision-yolov8n-cls-cc0-bus'
$demoRoot = Join-Path $workspaceRoot 'consumer-workspaces/yolov8n-cls-article'
$artifactRoot = Join-Path $workspaceRoot 'downloads/article-results/yolov8n-cls'

New-Item -ItemType Directory -Force `
  -Path $assetRoot, $modelRoot, $articleInputRoot, $artifactRoot | Out-Null

不要把 $assetRoot、$modelRoot、$articleInputRoot 或 $artifactRoot 放进仓库目录。

模型获取与许可证

本文固定使用 Ultralytics v8.3.0 资产和源码提交:

项目 固定值
权重 URL https://github.com/ultralytics/assets/releases/download/v8.3.0/yolov8n-cls.pt
源码 revision ultralytics-v8.3.0@6e43d1e1e5db72afbf686dee6745669bcb124b0a
权重 SHA256 11fa19f2aea79bc960d680a13f82f22105982b325eb9e17a4a5e1a9f8245980a
ImageNet 类别表 固定提交中的 ultralytics/cfg/datasets/ImageNet.yaml
许可证 AGPL-3.0-only

使用仓库脚本下载并校验权重、许可证和类别表:

pwsh -NoProfile -ExecutionPolicy Bypass `
  -File (Join-Path $repoRoot 'eng/Acquire-YoloV8ClassificationOfficialAssets.ps1') `
  -AssetRoot $assetRoot `
  -PythonPath $python

脚本固定 release asset ID、长度与 SHA256,下载后不执行 TensorRT,也不发布任何文件。权重、类别表和转换后的 ONNX 没有获得本项目重新分发授权,因此只保留在本地工作区。

ONNX 转换与暂存

进入权重目录后执行固定导出命令:

Push-Location (Join-Path $assetRoot 'source')
yolo export model=yolov8n-cls.pt format=onnx imgsz=224 opset=17 simplify=True dynamic=False batch=1 device=cpu
Pop-Location

Copy-Item `
  -LiteralPath (Join-Path $assetRoot 'source/yolov8n-cls.onnx') `
  -Destination (Join-Path $modelRoot 'yolov8n-cls.onnx') `
  -Force

转换后的文件暂存路径为:

models/YoloVision/Classification/yolov8n-cls-ultralytics-v8.3.0/yolov8n-cls.onnx

这里的 models 是仓库同级目录,不是仓库内目录。后续 Model Zoo 建立前,所有演示 ONNX 都按这个规则暂存。

校验模型:

$onnx = Join-Path $modelRoot 'yolov8n-cls.onnx'
Get-Item $onnx | Select-Object Length
Get-FileHash $onnx -Algorithm SHA256

期望结果:

属性 固定值
长度 10,911,331 字节
SHA256 630c022a99885d59f633ab5a614738f8a49be7f361e340fd3ff89b8c19b0768f
输入 images:float32[1,3,224,224]
输出 output0:float32[1,1000]
最后一个 ONNX 节点 Softmax

准备可公开展示的输入图片

模型资产中的 bus.jpg 未获得随项目文章重新分发的批准,因此结果图使用 Wikimedia Commons 的 Liverpool Street Bus station 2025。作者为 UK bus Image,许可证为 CC0 1.0。

下载 1280 像素版本并转换成内置预处理器支持的 PPM:

$inputJpg = Join-Path $articleInputRoot 'liverpool-street-bus-station-1280.jpg'
$inputPpm = Join-Path $articleInputRoot 'liverpool-street-bus-station-1280.ppm'

Invoke-WebRequest `
  -Uri 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Liverpool_Street_Bus_station_2025.jpg/1280px-Liverpool_Street_Bus_station_2025.jpg' `
  -OutFile $inputJpg

& $python -c "from PIL import Image; Image.open(r'$inputJpg').convert('RGB').save(r'$inputPpm')"
Get-FileHash $inputJpg, $inputPpm -Algorithm SHA256

期望值:

文件 尺寸 SHA256
JPEG 1280x961 52b889d4fc9baea772ba2d9bbdfdef8b70710f993d7f27d193a965b11e708bcb
PPM 1280x961 80715af66669147b049fec9386152bd45505f4e494a65079fdc55404d4589b8e

PPM 提供推理像素,JPEG 嵌入 SVG 结果图。YoloVision 会校验两者与预处理源图尺寸一致。

使用公开包准备应用

applications/YoloVision 是完整应用并设置为 IsPackable=false。它通过共享 props 引用已发布的 JYPPX.TensorRT.CSharp.API 4 系列包,以及作者维护的 OpenCV-CSharp-API。应用本身不发布 YoloVision 案例 NuGet 包。

新建仓库外项目时,可以让 NuGet 获取当前公开预览版,而不在文章中写死具体版本:

dotnet add package JYPPX.TensorRT.CSharp.API --version "4.0.0-*"
dotnet add package JYPPX.OpenCV.CSharp.API --prerelease
dotnet add package JYPPX.OpenCV.runtime.win-x64 --prerelease
dotnet add package JYPPX.TensorRT.CSharp.API.Runtime.win-x64.trt10.11.cuda12.9.cudnn9.22.Bridge --version "4.0.0-*"

4.0.0-* 只跟随当前 4.0.0 预览线,避免误选 API 不兼容的历史 4.0.6170。最后一个包 ID 必须按目标机器环境替换。它只包含项目自有 bridge;CUDA、cuDNN、TensorRT 和 NVRTC 继续由用户安装。仓库中的 YoloVision 项目直接运行当前源码,但 TensorRT/CUDA API 来自公开 NuGet 包。

编写程序入口

将 Program.cs 写成:

using YoloVisionSample;

return YoloVisionCommand.Run(args);

YoloVisionCommand 负责参数解析、图像预处理、TensorRT engine 生命周期、binding、执行、分类解码、reference 比较、JSON 与 SVG 输出。消费程序不接触裸 IntPtr,也不需要自己释放 TensorRT/CUDA owner。

编译并运行

还原并编译使用公开包的 YoloVision 应用:

dotnet restore ./applications/YoloVision/YoloVision.csproj
dotnet build ./applications/YoloVision/YoloVision.csproj -c Release --no-restore

1. 生成 C# 实际输入 tensor

YOLOv8n-cls 使用 shorter-side-center-crop,不是 detection 的 letterbox。先执行预处理-only:

$inputTensor = Join-Path $artifactRoot 'classification-csharp-input.fp32.bin'

dotnet run --project ./applications/YoloVision -c Release --no-build -- `
  --task cls `
  --family v8 `
  --image $inputPpm `
  --preprocessed-output $inputTensor `
  --input-shape 1x3x224x224 `
  --preprocess-only

固定预处理合同为:

  • RGB、NCHW、float32[1,3,224,224];
  • 短边缩放到 224,得到 298x224;
  • 从水平方向 x=37 开始中心裁剪;
  • 像素乘以 1/255;
  • mean 为 0,0,0,std 为 1,1,1。

本次 C# tensor SHA256 为 931ba0af3ea36ad344551f5dbe850dcbe800f464f4d98324a3e8f0447dd6a7a3。

2. 为 C# tensor 生成独立参考

C# 抗锯齿缩放与 Pillow/Ultralytics 可能有逐像素差异,因此 reference 必须基于 C# 实际 tensor 生成,不能直接复用另一个预处理器的输出:

$referenceRoot = Join-Path $artifactRoot 'reference'

& $python (Join-Path $repoRoot 'eng/Invoke-YoloVisionClassificationReference.py') `
  --weights (Join-Path $assetRoot 'source/yolov8n-cls.pt') `
  --imagenet-yaml (Join-Path $assetRoot 'source/ImageNet.yaml') `
  --image $inputJpg `
  --onnx $onnx `
  --output-directory $referenceRoot `
  --csharp-tensor $inputTensor

脚本会同时验证 PyTorch 与 ONNX Runtime,随后写出:

  • output0-csharp-input-onnxruntime.fp32.bin;
  • output0-csharp-input.reference.json;
  • 1,000 类 labels;
  • Top-5 与所有输入、输出 SHA256。

本次 C# tensor reference SHA256 为 2e80e6b389f8b68022bcaada3a5958784739c1224571c461b8cfb4fa44317a4f。

3. 执行 TensorRT 正例

让运行时找到用户安装的 TensorRT,并确认没有指定源码树 bridge:

$env:JYPPX_TENSORRT_ROOT = $env:TENSORRT_PATH
$env:JYPPX_ENABLE_DEVELOPMENT_PROBING = 'true'
Remove-Item Env:JYPPX_NATIVE_BRIDGE_PATH -ErrorAction SilentlyContinue

$labels = Join-Path $referenceRoot 'imagenet-yolov8n-cls.names'
$reference = Join-Path $referenceRoot 'output0-csharp-input.reference.json'
$outputJson = Join-Path $artifactRoot 'classification-output.json'
$outputSvg = Join-Path $artifactRoot 'classification-annotated.svg'

dotnet run --project ./applications/YoloVision -c Release --no-build -- `
  --model $onnx `
  --labels $labels `
  --image $inputPpm `
  --preprocessed-output $inputTensor `
  --visualization-background $inputJpg `
  --output-json $outputJson `
  --visualization $outputSvg `
  --input-shape 1x3x224x224 `
  --input-name images `
  --output-name output0 `
  --tensor-rt-line 10 `
  --noTF32 `
  --family v8 `
  --task cls `
  --classification-output output0 `
  --class-count 1000 `
  --classification-score-mode probabilities `
  --no-nms `
  --nms-mode none `
  --confidence 0 `
  --top-k 5 `
  --reference-outputs "output0:$reference" `
  --reference-abs-tolerance 0.0001 `
  --reference-rel-tolerance 0.0001

下面是仓库外项目仅通过三个 PackageReference 执行时的真实窗口:

YoloVision YOLOv8n 分类真实程序运行窗口

终端截图来自本次真实运行的 stdout,不是手工填写的结果卡片。窗口显示输入输出合同、预处理、TensorRT binding、全向量比较、Top-5、OutputValidated=True、YoloVision Passed=True 和退出码 0。

4. 查看原图 Top-5

分类任务没有目标框。正确的结果展示方式是在原图上叠加整张图片的 Top-5 类别和概率:

YoloVision YOLOv8n 分类 Top-5 原图结果

图中结果来自程序生成的 SVG,不是后期手工添加文字。Top-5 为:

排名 ImageNet 类别 概率
1 fire_engine 0.58679134
2 streetcar 0.33616993
3 trolleybus 0.022817276
4 recreational_vehicle 0.0072083427
5 garbage_truck 0.0068488526

红色双层巴士被排到 fire_engine,说明这个轻量 ImageNet 模型对该图片存在类别混淆。TensorRT 与 ORT 得到同一 Top-5,只证明部署实现一致,不证明预测语义一定正确。

已验证结果

本次运行环境为 Windows x64、NVIDIA GeForce RTX 3060 Laptop GPU、TensorRT 10.11、CUDA 12.9、FP32 且关闭 TF32。

验证项 结果
外部项目包引用 3
ProjectReference 0
手工程序集引用 0
JYPPX_NATIVE_BRIDGE_PATH 未设置
输入 tensor 150,528 个 float32
输出 tensor output0:[1,1000]
比较值数量 1,000
mismatch 0
first mismatch -1
最大绝对误差 8.568168E-08
最大相对误差 1.04507835E-05
Top-5 数量 5
退出码 0

发布前历史证据中的三个包都锚定源码提交 be2e507ae2d34836982eadc4d18a71d9d6655ab0:

包 长度 SHA256
managed API 15,230,357 140d7cc4f3c2842b5bf601650b955f2ebe9951f910858fc823da2ef6d38f54d8
YoloVision 119,261 79fcf8469ccf0b49102f55752acd0fde24a4e07a7d654b8938b349aa3c031c31
bridge-only 376,997 968e54765bc51228f5893223f8799ab0decb75405df8c765980440a98e9f960c

受控负例

把 reference 第 0 个值增加 0.125 后再次运行,严格验证必须失败:

ReferenceOutputValidation ... Passed=False
ReferenceOutputTensor Tensor=output0 Compared=1000 Mismatches=1 FirstMismatch=0 MaxAbs=0.125
OutputValidated=False
YoloVision Passed=False

进程退出码为 1。这证明 reference 不匹配时程序会 fail closed,而不是仍然输出成功。

结果文件与复查方法

正例运行会产生:

文件 用途
classification-csharp-input.fp32.bin C# 实际预处理 tensor。
classification-output.json 模型合同、预处理、binding、reference 比较与 Top-5。
classification-annotated.svg 嵌入原图和 Top-5 的矢量结果。
output0-csharp-input.reference.json ORT 对同一 C# tensor 的 1,000 值参考。
classification-runtime-terminal-source.log 截图对应的完整 stdout。

建议复查:

Get-FileHash `
  (Join-Path $artifactRoot 'classification-output.json'), `
  (Join-Path $artifactRoot 'classification-annotated.svg'), `
  (Join-Path $artifactRoot 'classification-runtime-terminal-source.log') `
  -Algorithm SHA256

两张图都来自同一次真实 TensorRT 执行:结果图由该次执行写出的 SVG 渲染,终端图由显示该次 stdout 的 Windows Terminal 窗口直接捕获。图片来源、包哈希、运行哈希和两张 PNG 的 SHA256 记录在 samples/assets/yolovision-yolov8n-cls-article-visual-assets.json 与 samples/assets/yolovision-yolov8n-cls-article-runtime-evidence.json。

常见问题

output0 概率和为 1,但 Top-5 不对

检查 labels 是否严格按 ImageNet.yaml 的 map 插入顺序生成,并确认共有 1,000 行。类别表顺序错误不会改变概率,却会把 class ID 映射成错误名称。

TensorRT 与 ORT 有少量误差

确认使用 --noTF32,reference 来自同一个 C# tensor,且模型与 reference 的 SHA256 一致。不要通过扩大容差来掩盖输入 tensor 不同的问题。

可视化只有结果面板,没有原图

必须同时传入 --image、--visualization 和 --visualization-background。用于预处理的 PPM 与作为背景的 JPEG 尺寸必须完全相同。

bridge 已复制但 TensorRT DLL 找不到

bridge-only 包只负责项目自己的 native bridge。请安装与包键一致的 TensorRT/CUDA/cuDNN,并让 JYPPX_TENSORRT_ROOT 指向 TensorRT 安装根目录。

复查与边界

发布前 local-feed 证据复核

本文已经证明:

  • 官方 YOLOv8n-cls 可以转换为固定静态 ONNX 并暂存在外层 models;
  • C# 内置 center-crop 预处理能够生成可哈希 tensor;
  • 独立 ORT reference 与 TensorRT 全 1,000 概率零 mismatch;
  • 外部项目可以通过公开 managed/bridge 包完成真实 GPU 推理;
  • 原图 Top-5、JSON、完整日志和程序窗口可相互追溯;
  • 受控 reference 变更会返回非零退出码。

本文不代表:

  • 模型对这张图片的 Top-1 语义一定正确;
  • 权重、ONNX 或第三方模型资产获得公开再分发授权;
  • 包已经从 nuget.org 或 GitHub Packages 下载;
  • 已完成 public-package、post-publish、Owner acceptance 或正式 release proof;
  • 已创建 tag、GitHub Release 或执行任何包发布。

模型、reference、tensor、运行日志和中间 SVG 继续保留在仓库外。Git 只保存技术文章、可公开再分发的两张结果 PNG,以及不含模型本体的哈希证据。