模型部署不再卡在环境上:DeploySharp 四大推理引擎与 Windows/Linux 离线交付实战

文章目录

    • [一、DeploySharp 项目简介](#一、DeploySharp 项目简介)
    • [二、2.0 新特性与本篇部署实战](#二、2.0 新特性与本篇部署实战)
      • [2.1 从开发机运行,走到目标设备运行](#2.1 从开发机运行,走到目标设备运行)
      • [2.2 本篇会完成什么](#2.2 本篇会完成什么)
    • 三、先选模型和设备,再选推理后端
      • [3.1 应用交付,需要准备哪些部分](#3.1 应用交付,需要准备哪些部分)
      • [3.2 按实际场景选择](#3.2 按实际场景选择)
    • [四、NuGet 与原生运行库,按场景分开准备](#四、NuGet 与原生运行库,按场景分开准备)
      • [4.1 图像输入部分先保持一致](#4.1 图像输入部分先保持一致)
      • [4.2 普通 CPU:ONNX Runtime](#4.2 普通 CPU:ONNX Runtime)
      • [4.3 NVIDIA GPU:ONNX Runtime CUDA](#4.3 NVIDIA GPU:ONNX Runtime CUDA)
      • [4.4 CPU 的另一种选择:OpenVINO](#4.4 CPU 的另一种选择:OpenVINO)
      • [4.5 共用 OpenCV 图像库:OpenCV DNN](#4.5 共用 OpenCV 图像库:OpenCV DNN)
      • [4.6 NVIDIA GPU 调优:TensorRT](#4.6 NVIDIA GPU 调优:TensorRT)
    • 五、准备离线案例:模型、图片和配置一起管理
      • [5.1 创建工程,按本地路径使用模型](#5.1 创建工程,按本地路径使用模型)
      • [5.2 用配置文件记录模型约定](#5.2 用配置文件记录模型约定)
    • 六、编写可以随目录迁移的推理程序
      • [6.1 项目文件:按目标系统选择 native,并复制资产](#6.1 项目文件:按目标系统选择 native,并复制资产)
      • [6.2 主程序:路径从应用目录解析,推理直接用本地文件](#6.2 主程序:路径从应用目录解析,推理直接用本地文件)
    • 七、发布为完整目录,再交给目标设备
      • [7.1 先区分两种 .NET 发布方式](#7.1 先区分两种 .NET 发布方式)
      • [7.2 Windows x64:发布后直接启动 EXE](#7.2 Windows x64:发布后直接启动 EXE)
      • [7.3 Ubuntu 22.04 x64:同一份代码,发布另一套 native](#7.3 Ubuntu 22.04 x64:同一份代码,发布另一套 native)
      • [7.4 换成 GPU 应用,交付时还要加什么](#7.4 换成 GPU 应用,交付时还要加什么)
    • [八、实际运行:Windows 与 Ubuntu 的可迁移目录](#八、实际运行:Windows 与 Ubuntu 的可迁移目录)
      • [8.1 本次验证条件](#8.1 本次验证条件)
      • [8.2 输出与结果图](#8.2 输出与结果图)
    • 九、模型较多时:在线准备与严格离线缓存
      • [9.1 本地模型目录与下载缓存,按需求选择](#9.1 本地模型目录与下载缓存,按需求选择)
      • [9.2 一个可以直接运行的 PaddleOCR 缓存示例](#9.2 一个可以直接运行的 PaddleOCR 缓存示例)
      • [9.3 缓存怎样带到离线设备](#9.3 缓存怎样带到离线设备)
    • 十、换机失败时,按哪个方向检查
      • [10.1 先看失败发生在哪一层](#10.1 先看失败发生在哪一层)
      • [10.2 从教程运行走向业务部署](#10.2 从教程运行走向业务部署)
    • 十一、总结与后续

一、DeploySharp 项目简介

DeploySharp 是一个面向 C#/.NET 开发者 的开源模型部署框架,由「椒颜皮皮虾」开发,项目源码遵循 Apache License 2.0 。它基于 ONNX Runtime、OpenVINO、TensorRT、OpenCV DNN 等推理引擎,提供模型加载、图像前处理、推理执行和结果后处理接口,方便大家在桌面软件、工业视觉和文档应用中接入模型。

项目已经接入目标检测、分类、分割、关键点、旋转框、异常检测和背景移除等视觉任务,也提供 PP-OCRv4/v5/v6、PP-Structure 文档处理和部分图文、本地语言模型的使用入口。常用模型有配套前后处理,自有模型可以通过扩展接口接入。

项目地址:

text 复制代码
https://github.com/guojin-yan/DeploySharp/tree/DeploySharpV2.0

**NuGet 包:**在包管理器中搜索 JYPPX.DeploySharp,按任务与推理后端选择组件。

text 复制代码
https://www.nuget.org/packages?q=JYPPX.DeploySharp

二、2.0 新特性与本篇部署实战

2.1 从开发机运行,走到目标设备运行

前两篇实战已经完成 YOLO26 检测和 PaddleOCR。开发机上运行之后,接下来就要把程序交给目标设备,准备模型、运行库和配置,并让它在离线环境中运行。

2.0 版本把这些组件分开管理,部署时可以按需组合:

新版能力 交付应用时怎样使用
视觉任务与推理后端分开 保留配套前后处理,按已验证模型与设备选择执行引擎
托管适配器与 native runtime 分开 按操作系统和架构准备 DLL / .so,明确最终应用的依赖
本地模型与按需获取并存 自有模型直接用路径;公开模型可先下载、再缓存和离线复用
配置与任务流程可组合 把模型、类别、输入输出和业务参数一起交付,减少换机后的手工改代码

本篇先完成运行环境和目录交付。Batch、独立 Session 池、异步预取、ROI 和大图切片,可以在此基础上继续加入。

2.2 本篇会完成什么

下面先介绍 ORT CPU / ORT CUDA / OpenVINO / OpenCV DNN / TensorRT 的依赖和使用场景,再把一个 YOLO26 程序发布成完整目录,分别在 Windows 和 Ubuntu 运行,最后介绍 PaddleOCR 模型的在线下载和离线缓存。

主案例使用 ORT CPU,直接加载本地模型和图片;GPU 应用则在相同交付流程上增加对应的驱动、运行库和 Engine。

三、先选模型和设备,再选推理后端

3.1 应用交付,需要准备哪些部分

Visual.OpenCV 负责图片输入和前处理,Backend.* 负责调用推理引擎。NuGet 解析了托管引用之后,执行计算仍需要对应的原生库,模型文件和配套字典也需要随应用准备。

下面把这些关系放在一起。应用调用沿中间向下执行,模型与任务资产从左侧参与加载;GPU 场景还要满足设备驱动和厂商运行库的条件。

.NET Runtime 负责运行 C# 应用,原生运行库提供 OpenCV、ORT、OpenVINO、TensorRT 等计算能力。后面的自包含发布会带上 .NET Runtime,图像和推理原生库则通过 NuGet 包或厂商文件一起部署。

3.2 按实际场景选择

设备与需求 可以优先评估 准备重点
普通 x64 CPU,先完成部署 ONNX Runtime CPU 匹配的 ORT CPU 原生包和图像 runtime
希望继续使用 ONNX,在 NVIDIA GPU 执行 ONNX Runtime CUDA ORT 原生发行包、CUDA/cuDNN 与驱动;代码显式选择 CUDA
评估 CPU 上的模型执行效果 OpenVINO CPU OpenVINO runtime;当前 2.0.0 适配器使用 CPU 设备入口
已经依赖 OpenCV,模型图可以被 DNN 导入 OpenCV DNN CPU OpenCV 原生库、实际输出合同及 Shape/importer 兼容性
NVIDIA GPU,需要进一步调优 TensorRT 匹配的 bridge、TensorRT/CUDA/cuDNN、输入 Profile 和目标机 Engine

选型时先看目标设备,再确认模型的输入输出、算子和动态尺寸是否能被后端处理,最后比较速度与部署成本。选择过程如下:

第二篇在 Windows 上运行了 YOLO26n + ORT CPU / ORT CUDA / OpenVINO / OpenCV DNN / TensorRT ;第三篇在 Ubuntu 上运行了 PP-OCRv5 Mobile 完整流水线 + ORT CPU / OpenVINO / OpenCV DNN / TensorRT。可以先参考这两组配置,其他模型的具体状态见验证矩阵:

text 复制代码
模型支持指南:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/model-support.md

模型与后端验证矩阵:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/model-backend-verification-matrix.md

四、NuGet 与原生运行库,按场景分开准备

4.1 图像输入部分先保持一致

下面以 Windows x64 视觉应用为例。先安装公共图像组件,再从后续几节选择一组推理依赖。ORT CPU 和 GPU 原生包分别使用独立输出目录,避免同名库冲突。

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Visual.OpenCV --version 2.0.0
dotnet add package JYPPX.OpenCV.runtime.win-x64 --version 5.0.0

Visual、Core、OpenCV 托管接口等依赖由 NuGet 自动引入。这里的 OpenCV 用于读图和图像处理,模型可以交给其他引擎执行。

4.2 普通 CPU:ONNX Runtime

在公共图像组件之外,增加后端适配器和 CPU runtime:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.OnnxRuntime --version 2.0.0
dotnet add package Microsoft.ML.OnnxRuntime --version 1.28.0

注册后端并请求 CPU:

csharp 复制代码
backends.UseOnnxRuntime();
var request = new BackendRequest(BackendCapabilities.TensorInference,
    OnnxRuntimeBackendProvider.BackendId, "cpu");

后面的完整离线案例采用这一组。所有 DeploySharp 包统一使用 2.0.0;OpenCV 和 ORT 使用各自的版本号。

4.3 NVIDIA GPU:ONNX Runtime CUDA

下面是 Windows CUDA 13 的安装组合,还需配套的驱动、CUDA 和 cuDNN。本系列实测使用的是下文的 CUDA 12 组合:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.OnnxRuntime --version 2.0.0
dotnet add package Microsoft.ML.OnnxRuntime.Gpu.Windows --version 1.28.0

已有 CPU 工程改用 GPU 时,先移除显式的 Microsoft.ML.OnnxRuntime CPU 包,再选择 GPU 原生来源。Microsoft.ML.OnnxRuntime.Managed 作为托管依赖继续保留。初始化时还要显式启用 CUDA:

csharp 复制代码
backends.UseOnnxRuntime(new OnnxRuntimeOptions(
    executionProvider: OnnxRuntimeExecutionProvider.Cuda,
    cudaDeviceId: 0));
var request = new BackendRequest(BackendCapabilities.TensorInference,
    OnnxRuntimeBackendProvider.BackendId, "cuda");

已有 CUDA 12 环境时,可以使用对应的官方原生发行包。第二篇在 RTX 3060 Laptop、驱动 576.02 上,使用 Managed 1.28.0 加 onnxruntime-win-x64-gpu_cuda12-1.28.0.zip,配套 CUDA 12.9 和 cuDNN 9.22.0,完成了 YOLO26 推理。

这种方式使用下面的托管组合,再部署官方包中的三份 ORT 原生 DLL;它与上面的 Gpu.Windows 原生来源二选一:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.OnnxRuntime --version 2.0.0
dotnet add package Microsoft.ML.OnnxRuntime.Managed --version 1.28.0
text 复制代码
官方 CUDA 12 发行包:
https://github.com/microsoft/onnxruntime/releases/download/v1.28.0/onnxruntime-win-x64-gpu_cuda12-1.28.0.zip

同一 lib 目录中成套部署:
onnxruntime.dll
onnxruntime_providers_shared.dll
onnxruntime_providers_cuda.dll

完整下载、发布、搜索路径与实跑命令,见第二篇 8.4 节:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/blog/2.0.0/2026-10/2026-10-28-02-yolo26-quickstart.md

选择 CUDA 主版本时,同时核对原生发行包和驱动要求。三份 ORT DLL 使用同一发行包中的文件,并与其他 CPU/GPU 输出目录分开。

4.4 CPU 的另一种选择:OpenVINO

公共图像组件之外,增加:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.OpenVINO --version 2.0.0
dotnet add package OpenVINO.runtime.win --version 2026.2.1

初始化关键部分如下,文件顶部增加 using JYPPX.DeploySharp.Backends.OpenVINO;:

csharp 复制代码
backends.UseOpenVino(new OpenVinoOptions(
    performanceHint: OpenVinoPerformanceHint.Latency));
var request = new BackendRequest(BackendCapabilities.TensorInference,
    OpenVinoBackendProvider.BackendId, "CPU");

这一组加载 ONNX,前后处理沿用对应 Profile,当前适配器使用 CPU 设备入口。

4.5 共用 OpenCV 图像库:OpenCV DNN

在公共图像组件之外增加:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.OpenCV --version 2.0.0

DNN 后端还需要模型输入输出合同。例如第二篇的 YOLO26n,声明 Float32 images: [1,3,640,640] 与 output0: [1,-1,6],再使用 UseOpenCvDnn(...) 注册,请求 OpenCvDnnBackendProvider.BackendId 和 "cpu"。完整代码已经放在第二篇后端工程中。

OpenCV DNN 对动态宽度、辅助输入和不同 opset 的支持存在差异,部署前先运行对应模型,确认网络导入和输出结果。

4.6 NVIDIA GPU 调优:TensorRT

Windows TensorRT 10.11 安装以下组件:

powershell 复制代码
dotnet add package JYPPX.DeploySharp.Backend.TensorRT --version 2.0.0
dotnet add package JYPPX.TensorRT.CSharp.API.Runtime.win-x64.trt10.11.cuda12.9.cudnn9.22.Bridge --version 4.0.0

然后准备 NVIDIA 驱动、TensorRT 10.11.0.33、CUDA 12.9 和 cuDNN 9.22.0 。bridge 将托管 API 连接到这些厂商库。需要 CUDA 图像前后处理时,再按任务增加 JYPPX.DeploySharp.Visual.TensorRT 2.0.0;普通图像读取仍使用 OpenCV 组件。

与 ONNX 直接加载不同,TensorRT 通常先在目标 GPU 上从 ONNX 构建 Engine。第二篇的构建代码使用 TensorRtOnnxEngineBuilder,同时把 Profile 与 ModelArtifact 的格式都设为 tensorrt-engine ,并请求 TensorRT 后端的 cuda 设备。

交付 TensorRT 应用时,需要把这些文件分清楚:

文件或环境 应该怎样准备
ONNX 源模型 保留用于目标机转换,所有 external data 一起准备
Engine 与动态输入 Profile 在目标 GPU 和对应运行时上构建,覆盖实际 Batch、尺寸与宽度范围
bridge + TensorRT/CUDA/cuDNN 版本、系统、架构成套匹配;路径指向实际包含 DLL / .so 的目录
NVIDIA 驱动 在目标机安装,确认设备及所选 CUDA 版本可用

Engine 与构建时的 GPU、TensorRT 和输入配置有关,换设备或运行时后重新构建。本系列分别在 Windows TensorRT 10 和 Ubuntu TensorRT 11 环境生成各自的 Engine。

上面介绍了各后端的安装和初始化。完整工程可以参考 Windows YOLO26 和 Ubuntu OCR 两个示例,其中包含后端注册、设备选择和模型格式配置:

text 复制代码
Windows YOLO26 后端对照工程:
https://github.com/guojin-yan/DeploySharp/tree/DeploySharpV2.0/docs/blog/2.0.0/2026-10/examples/02-yolo26-backends

Ubuntu PaddleOCR 后端对照工程:
https://github.com/guojin-yan/DeploySharp/tree/DeploySharpV2.0/docs/blog/2.0.0/2026-10/examples/03-paddleocr-backends

五、准备离线案例:模型、图片和配置一起管理

5.1 创建工程,按本地路径使用模型

下面回到 YOLO26n + ORT CPU 主线。创建控制台项目:

shell 复制代码
dotnet new console -n OfflineInference -f net10.0
cd OfflineInference

本文也提供完整工程,可直接下载其中的 .csproj、Program.cs 和 deploysharp.json,再准备模型与图片:

text 复制代码
https://github.com/guojin-yan/DeploySharp/tree/DeploySharpV2.0/docs/blog/2.0.0/2026-10/examples/04-offline-inference

在 Windows PowerShell 中获取公开示例:

powershell 复制代码
New-Item -ItemType Directory -Force -Path .\models, .\images | Out-Null
Invoke-WebRequest -Uri 'https://github.com/guojin-yan/DeploySharp/releases/download/models-visual.1/yolo-v26-detect-n.model.onnx' -OutFile .\models\yolo26n.onnx
Invoke-WebRequest -Uri 'https://github.com/guojin-yan/DeploySharp/releases/download/test-assets.1/bus.jpg' -OutFile .\images\bus.jpg

在 Ubuntu Bash 中准备源码工程时,使用:

bash 复制代码
mkdir -p models images
curl -fL 'https://github.com/guojin-yan/DeploySharp/releases/download/models-visual.1/yolo-v26-detect-n.model.onnx' -o models/yolo26n.onnx
curl -fL 'https://github.com/guojin-yan/DeploySharp/releases/download/test-assets.1/bus.jpg' -o images/bus.jpg

已有本地模型和图片时,直接复制到目录中即可。后面的程序从本地路径加载,可以通过参数换成自己的输入。

5.2 用配置文件记录模型约定

创建 deploysharp.json:

json 复制代码
{
  "ModelPath": "models/yolo26n.onnx",
  "ImagePath": "images/bus.jpg",
  "ClassNamesPath": null,
  "OutputDirectory": "results",
  "InputName": "images",
  "OutputName": "output0",
  "ModelWidth": 640,
  "ModelHeight": 640,
  "ScoreThreshold": 0.25
}

本文模型是 COCO 80 类的 YOLO26n 端到端检测导出,输入为 Float32 NCHW 1×3×640×640,RGB、Letterbox、填充值 114、像素除以 255;输出 [1,N,6],每行依次是 x1/y1/x2/y2/score/class_id,解码时不重复执行 NMS。

ClassNamesPath 为 null 时使用 COCO 标签。使用自己的同结构模型时,将它改为 "models/classes.txt",每行填写一个类别,保持训练索引顺序,再按导出配置调整尺寸、张量名和阈值。输出结构或前处理不同的模型,需要同时替换对应 Profile。

最终源码目录如下:

text 复制代码
OfflineInference/
  OfflineInference.csproj
  Program.cs
  deploysharp.json
  models/
    yolo26n.onnx
    classes.txt        # 自有类别时才需要
  images/
    bus.jpg

发布时,.NET 和原生依赖会一起进入输出目录。下图展示完整目录结构及启动时的加载关系,交付时按这个结构整体复制:

六、编写可以随目录迁移的推理程序

6.1 项目文件:按目标系统选择 native,并复制资产

将 OfflineInference.csproj 替换为下面的完整内容。DeploymentRid 选择图像原生包,发布命令中的 -r 选择 .NET 目标运行时,两者保持一致;从 Windows 交叉发布 Linux 时,后文会显式传入这两个值。

xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PlatformTarget>x64</PlatformTarget>
    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
    <RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
    <GenerateDocumentationFile>false</GenerateDocumentationFile>
    <IsPackable>false</IsPackable>
    <DefaultItemExcludes>$(DefaultItemExcludes);publish/**</DefaultItemExcludes>
    <DeploymentRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</DeploymentRid>
    <DeploymentRid Condition="'$(DeploymentRid)' == '' and '$(OS)' == 'Windows_NT'">win-x64</DeploymentRid>
    <DeploymentRid Condition="'$(DeploymentRid)' == '' and $([MSBuild]::IsOSPlatform('Linux'))">linux-x64</DeploymentRid>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="JYPPX.DeploySharp.Visual.OpenCV" Version="2.0.0" />
    <PackageReference Include="JYPPX.DeploySharp.Backend.OnnxRuntime" Version="2.0.0" />
    <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.28.0" />
    <PackageReference Include="JYPPX.OpenCV.runtime.win-x64" Version="5.0.0" Condition="'$(DeploymentRid)' == 'win-x64'" />
    <PackageReference Include="JYPPX.OpenCV.runtime.ubuntu.22.04-x64" Version="5.0.0" Condition="'$(DeploymentRid)' == 'linux-x64'" GeneratePathProperty="true" />
    <None Update="deploysharp.json;models/**/*;images/**/*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
  </ItemGroup>
  <!-- .NET 10 的通用 linux-x64 RID 不会自动选择发行版专用的 OpenCV native 资产。 -->
  <ItemGroup Condition="'$(DeploymentRid)' == 'linux-x64'">
    <Content Include="$(PkgJYPPX_OpenCV_runtime_ubuntu_22_04-x64)/runtimes/ubuntu.22.04-x64/native/*">
      <Link>native/opencv/%(Filename)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </Content>
  </ItemGroup>
  <Target Name="ValidateDeploymentRid" BeforeTargets="PrepareForBuild">
    <Error Condition="'$(DeploymentRid)' != 'win-x64' and '$(DeploymentRid)' != 'linux-x64'" Text="本教程仅配置了 Windows x64 与 Ubuntu 22.04 x64 的原生依赖。" />
    <Error Condition="'$(DeploymentRid)' == 'linux-x64' and !Exists('$(PkgJYPPX_OpenCV_runtime_ubuntu_22_04-x64)/runtimes/ubuntu.22.04-x64/native/libJYPPX.OpenCV.Native.so')" Text="缺少 Ubuntu OpenCV 原生包;交叉发布请同时指定 -r linux-x64 -p:DeploymentRid=linux-x64。" />
  </Target>
</Project>

CopyToPublishDirectory 将模型、图片和配置带入发布目录。Linux OpenCV 包中的整组 .so 另行复制到 native/opencv/。GeneratePathProperty 用于获取包路径,也适用于自定义 NUGET_PACKAGES 的环境。

这里按普通目录发布,保留独立的原生库和模型文件,方便替换模型与排查加载问题。

6.2 主程序:路径从应用目录解析,推理直接用本地文件

把下面代码放入 Program.cs。YOLO26 的 Profile 与第二篇相同,本篇增加了 JSON 配置、相对路径解析和运行环境输出。

csharp 复制代码
using System.Runtime.InteropServices;
using System.Text.Json;
using JYPPX.DeploySharp;
using JYPPX.DeploySharp.Backends.OnnxRuntime;
using JYPPX.DeploySharp.Models;
using JYPPX.DeploySharp.Registry;
using JYPPX.DeploySharp.Results.Vision;
using JYPPX.DeploySharp.Tensors;
using JYPPX.DeploySharp.Visual;
using JYPPX.DeploySharp.Visual.Models.Yolo;
using JYPPX.DeploySharp.Visual.OpenCV;
using JYPPX.OpenCvSharp.Core;
using JYPPX.OpenCvSharp.ImgProc;
using ImageCodecs = JYPPX.OpenCvSharp.ImgCodecs.Cv2;
using Drawing = JYPPX.OpenCvSharp.ImgProc.Cv2;

// 相对路径统一从程序目录解析,从其他目录启动也能找到模型、配置和图片。
string baseDirectory = AppContext.BaseDirectory;
string Resolve(string value) => Path.GetFullPath(value, baseDirectory);
var config = JsonSerializer.Deserialize<DeploymentConfig>(
    File.ReadAllText(Resolve("deploysharp.json")))
    ?? throw new InvalidDataException("deploysharp.json 不能为空。");
string imagePath = Resolve(args.Length > 0 ? args[0] : config.ImagePath);
string modelPath = Resolve(args.Length > 1 ? args[1] : config.ModelPath);
string outputDirectory = Resolve(args.Length > 2 ? args[2] : config.OutputDirectory);
foreach (string path in new[] { imagePath, modelPath })
    if (!File.Exists(path)) throw new FileNotFoundException("找不到输入文件。", path);
IReadOnlyList<string> labels = string.IsNullOrWhiteSpace(config.ClassNamesPath)
    ? YoloLabelSets.Coco80
    : File.ReadAllLines(Resolve(config.ClassNamesPath));
if (labels.Count == 0 || labels.Any(string.IsNullOrWhiteSpace))
    throw new InvalidDataException("类别文件需要每行一个名称,按训练索引排列,不含空行。");
Directory.CreateDirectory(outputDirectory);

var size = new VisualSize(config.ModelWidth, config.ModelHeight);
var modelId = new ModelId("local/yolo26-offline");
var profile = new VisualModelProfile(
    "yolo26.detect.offline", modelId, VisualTaskId.ObjectDetection, "1", "onnx",
    new VisualInputBinding(config.InputName, TensorElementType.Float32,
        new TensorShape(1, 3, size.Height, size.Width), VisualTensorLayout.Nchw),
    new[] { new VisualOutputBinding(config.OutputName, TensorElementType.Float32,
        new TensorShape(1, -1, 6)) },
    labels.Select((name, index) => new VisualLabel(index, name)),
    new YoloDetectionDecoder(
        new YoloDetectionOutputContract(config.OutputName,
            YoloDetectionOutputKind.EndToEnd, labels.Count),
        new YoloDetectionDecoderOptions(scoreThreshold: config.ScoreThreshold)),
    preprocessing: new VisualPreprocessingOptions(
        size, VisualResizeMode.Letterbox, VisualColorOrder.Rgb,
        VisualNormalizationOptions.Scale(255f),
        paddingColor: new VisualRgbColor(114, 114, 114)));

using var backends = new BackendRegistry();
backends.UseOnnxRuntime();
var profiles = new VisualProfileRegistry();
profiles.Register(profile);
profiles.Freeze();
var request = new BackendRequest(BackendCapabilities.TensorInference,
    OnnxRuntimeBackendProvider.BackendId, "cpu");
var artifact = new ModelArtifact(modelId, "onnx", modelPath);
using var pipeline = new VisualPipeline(backends,
    profiles.Select(artifact, backends, request, VisualTaskId.ObjectDetection), request);
using PreparedVisualInput input = new OpenCvVisualInputFactory()
    .CreateFromFile(imagePath, profile);
DetectionResult result = pipeline.Run(input).GetValue<DetectionResult>();

using Mat canvas = ImageCodecs.ImRead(imagePath);
foreach (Detection item in result.Detections)
{
    var box = item.Box;
    string caption = FormattableString.Invariant($"{item.Label.Label} {item.Label.Score:F3}");
    Size textSize = Drawing.GetTextSize(caption, HersheyFonts.HersheySimplex, 0.75, 2, out _);
    var captionPoint = new Point(
        Math.Clamp((int)box.X, 3, Math.Max(3, canvas.Cols - textSize.Width - 12)),
        Math.Max(textSize.Height + 4, (int)box.Y - 8));
    Drawing.Rectangle(canvas, new Point((int)box.X, (int)box.Y),
        new Point((int)box.Right, (int)box.Bottom), new Scalar(80, 210, 30), 3);
    Drawing.PutText(canvas, caption, captionPoint,
        HersheyFonts.HersheySimplex, 0.75, new Scalar(20, 20, 20), 5);
    Drawing.PutText(canvas, caption, captionPoint,
        HersheyFonts.HersheySimplex, 0.75, new Scalar(255, 255, 255), 2);
}
if (!ImageCodecs.ImWrite(Path.Combine(outputDirectory, "result.jpg"), canvas))
    throw new IOException("无法写入结果图片,请使用有写权限的输出目录。");

var report = new
{
    backend = "onnxruntime", device = "cpu", deploySharp = "2.0.0",
    os = RuntimeInformation.OSDescription,
    framework = RuntimeInformation.FrameworkDescription,
    runtimeDirectory = RuntimeEnvironment.GetRuntimeDirectory(),
    baseDirectory, modelPath, imagePath,
    detections = result.Detections.Select(item => new
    {
        label = item.Label.Label, score = item.Label.Score,
        x = item.Box.X, y = item.Box.Y, width = item.Box.Width, height = item.Box.Height
    })
};
File.WriteAllText(Path.Combine(outputDirectory, "result.json"),
    JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine($"backend=onnxruntime;device=cpu;detections={result.Detections.Count}");
Console.WriteLine("labels=" + string.Join(",", result.Detections.Select(item => item.Label.Label)));
Console.WriteLine("framework=" + report.framework);
Console.WriteLine("runtime_directory=" + report.runtimeDirectory);
Console.WriteLine("saved=" + outputDirectory);

internal sealed class DeploymentConfig
{
    public string ModelPath { get; set; } = "models/yolo26n.onnx";
    public string ImagePath { get; set; } = "images/bus.jpg";
    public string? ClassNamesPath { get; set; }
    public string OutputDirectory { get; set; } = "results";
    public string InputName { get; set; } = "images";
    public string OutputName { get; set; } = "output0";
    public int ModelWidth { get; set; } = 640;
    public int ModelHeight { get; set; } = 640;
    public float ScoreThreshold { get; set; } = 0.25f;
}

AppContext.BaseDirectory 是程序所在目录,Environment.CurrentDirectory 是启动时的工作目录。从快捷方式或计划任务启动时,工作目录可能变化,因此这里统一按程序目录解析配置、模型、图片和默认输出路径。

三个可选参数依次是 图片、模型、输出目录。相对路径仍按程序目录解析,绝对路径直接使用。例如把输出参数指向一个有写权限的目录,就不必向安装目录写文件。自定义类别文件通过 JSON 指定。

输入、OpenCV 图像、pipeline 和后端注册表通过 using 释放,输入在推理结束后释放,pipeline 最后释放 Session。绘图时另外读取一次原图。

七、发布为完整目录,再交给目标设备

7.1 先区分两种 .NET 发布方式

发布方式 目录是否包含 .NET Runtime 目标机需要什么
框架依赖:--self-contained false 不包含 安装匹配的 .NET Runtime,再准备同样的模型和 native 依赖
自包含:--self-contained true 包含目标 RID 的运行时 不需要另装 .NET SDK/Runtime,仍需满足原生库与系统依赖

本篇使用 自包含、非单文件、不裁剪。联网准备与离线运行分别在两个阶段完成:构建时可以还原 NuGet、获取模型,目标机直接运行发布的程序。

7.2 Windows x64:发布后直接启动 EXE

在源码项目目录执行:

powershell 复制代码
dotnet publish -c Release -r win-x64 -p:DeploymentRid=win-x64 `
  --self-contained true -p:PublishSingleFile=false -p:PublishTrimmed=false `
  -o .\publish\win-x64

& .\publish\win-x64\OfflineInference.exe

发布目录包含 OfflineInference.exe、应用 DLL、.NET 运行时、DeploySharp 和第三方组件,以及模型、图片和配置。将整个目录复制到目标机,保留子目录结构,直接运行 EXE。

例如已经进入复制后的程序目录,想使用自己的图片、模型和输出位置:

powershell 复制代码
.\OfflineInference.exe .\images\my-image.jpg .\models\best.onnx .\results

相对路径按 EXE 所在目录解析。安装目录不可写时,第三个参数传入有写权限的绝对路径。如果 OpenCV 或 ORT 提示缺少 MSVC 等系统依赖,再按异常安装对应的 x64 运行库。

7.3 Ubuntu 22.04 x64:同一份代码,发布另一套 native

这份项目可以在 Windows 构建 Linux 目录,也可以直接在 Ubuntu 构建。执行下面的发布命令,区别是目标 RID 和对应 OpenCV 原生包:

shell 复制代码
dotnet publish -c Release -r linux-x64 -p:DeploymentRid=linux-x64 --self-contained true -p:PublishSingleFile=false -p:PublishTrimmed=false -o ./publish/linux-x64

DeploymentRid=linux-x64 同时参与 NuGet 还原,让 Windows 交叉发布时也能选择 Linux OpenCV 包。

把 publish/linux-x64 的全部内容传到目标机。进入该目录后,在 Bash 中运行:

bash 复制代码
chmod +x ./OfflineInference
app_dir="$(pwd)"
LD_LIBRARY_PATH="$app_dir/native/opencv" "$app_dir/OfflineInference"

发布目录已包含 .NET、ORT Linux 原生库和 OpenCV .so。启动时为当前进程设置 OpenCV 搜索路径;使用其他后端时,再合并对应的库目录。

Ubuntu 系统本身的动态加载器、glibc、libstdc++ 等依赖仍需具备。遇到加载问题,可以检查:

bash 复制代码
LD_LIBRARY_PATH="$app_dir/native/opencv" ldd "$app_dir/native/opencv/libJYPPX.OpenCV.Native.so"

ldd 输出中的 not found 可以定位缺失依赖。OpenCV 按 native 目录成组复制,chmod 则恢复文件传输后可能丢失的执行权限。

7.4 换成 GPU 应用,交付时还要加什么

上面的自包含目录只配置了 ORT CPU + OpenCV。使用 GPU 方案时,继续在相应后端工程中准备完整依赖:

GPU 路径 除应用和模型外,还需要
ORT CUDA 成套 ORT GPU 原生库、匹配 CUDA/cuDNN、目标机驱动;启动前配置实际 DLL / .so 目录
TensorRT bridge、TensorRT/CUDA/cuDNN、目标机驱动;匹配 Engine,或 ONNX 与目标机转换流程
TensorRT CUDA 前后处理 对应 Visual.TensorRT 组件及其任务支持条件,并保留实际使用的输入与缓冲配置

dotnet publish 复制项目中声明的文件。依赖开发机 PATH 的厂商库,需要另外声明为发布资产,或者在目标机安装并设置搜索路径。

八、实际运行:Windows 与 Ubuntu 的可迁移目录

8.1 本次验证条件

这次使用上面的同一份源码与模型,分别发布为两套自包含目录:

项目 Windows 运行 Ubuntu 运行
系统 Windows 11 x64,10.0.26200 Ubuntu 22.04.5 LTS x64,内核 6.8.0-138-generic
CPU AMD Ryzen 7 5800H Intel Core i7-1165G7
构建工具 .NET SDK 10.0.401 由左侧 Windows SDK 交叉发布后传输
发布与执行 win-x64 自包含 EXE linux-x64 自包含可执行文件
执行引擎 ORT CPU 1.28.0 ORT CPU 1.28.0
图像 runtime OpenCV 5.0.0 Windows x64 OpenCV 5.0.0 Ubuntu 22.04 x64
模型与输入 YOLO26n、640×640 输入;bus.jpg 原图 810×1080 相同模型与图片

我分别从程序目录之外 启动,两套程序都能找到配置和模型。JSON 中的 runtimeDirectory 指向各自发布目录,运行的是随包携带的 .NET。

两台设备已具备原生库所需的系统依赖。新设备部署时,可以按前面的 ldd 或加载异常补齐缺失项。

8.2 输出与结果图

两条路径都返回 4 个行人和 1 辆公交车。控制台共同输出如下,实际保存位置随目标目录变化:

text 复制代码
backend=onnxruntime;device=cpu;detections=5
labels=person,bus,person,person,person
framework=.NET 10.0.12

程序还会打印 runtime_directory 与 saved 的完整路径,并生成 result.json 和 result.jpg。下面是本篇 Windows 自包含程序实际生成的图片:

JSON 保存了框、类别、分数、模型路径、运行环境和程序目录,可以随业务日志一起保留,方便定位设备上的实际配置。

九、模型较多时:在线准备与严格离线缓存

9.1 本地模型目录与下载缓存,按需求选择

只有一两个自有模型时,前面的 models/ 目录就够用了。需要在应用中按模型 ID 获取公开模型,或者管理多组 OCR/文档模型时,再使用 JYPPX.DeploySharp.ModelFactory。

这个包里有两类入口:

入口 使用范围
ModelFactoryClient 依据模型目录快照选择工件,获取对应 ModelPack 并复用缓存
PaddleOcrReleaseClient 面向 models-paddleocr Release,按需下载独立 PP-OCR / PP-Structure 模型及配套文件

ModelFactoryClient 使用随包提供的模型目录快照,PaddleOcrReleaseClient 使用 PaddleOCR Release 的专用目录。两类入口各有对应的缓存结构,按所用客户端管理即可。

下载客户端负责准备文件并返回本地路径,应用再用对应 Profile 和后端创建推理会话。自有模型则继续按前面的本地路径方式加载。

9.2 一个可以直接运行的 PaddleOCR 缓存示例

创建一个单独的控制台工程,只安装下载组件:

shell 复制代码
dotnet new console -n ModelCache -f net10.0
cd ModelCache
dotnet add package JYPPX.DeploySharp.ModelFactory --version 2.0.0

将下面代码写入 Program.cs。默认准备一个 v5 Mobile 文本行方向模型,--offline 控制是否允许联网:

csharp 复制代码
using JYPPX.DeploySharp.ModelFactory;

bool offline = args.Contains("--offline");
string[] positional = args.Where(value => value != "--offline").ToArray();
string cacheRoot = Path.GetFullPath(positional.Length > 0 ? positional[0] : "model-cache");
string modelId = positional.Length > 1 ? positional[1] : "paddleocr/ppocrv5/mobile-cls";
using var cancellation = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
    eventArgs.Cancel = true;
    cancellation.Cancel();
};
using var client = new PaddleOcrReleaseClient(
    new PaddleOcrReleaseClientOptions(cacheRoot, offline: offline));
try
{
    PaddleOcrReleaseModelMaterialization model =
        await client.GetModelAsync(modelId, cancellationToken: cancellation.Token);
    Console.WriteLine($"offline={offline};model={modelId}");
    Console.WriteLine("model_path=" + model.ModelPath);
    Console.WriteLine("dictionary_path=" + (model.DictionaryPath ?? "(none)"));
}
catch (ModelFactoryException exception)
{
    foreach (var diagnostic in exception.Diagnostics)
        Console.Error.WriteLine($"{diagnostic.Code}: {diagnostic.Message}");
    Environment.ExitCode = 1;
}
catch (OperationCanceledException)
{
    Console.Error.WriteLine("模型准备已取消。");
    Environment.ExitCode = 2;
}

第一次在联网环境运行,之后用同一个目录离线读取:

shell 复制代码
dotnet run -c Release -- ./model-cache paddleocr/ppocrv5/mobile-cls
dotnet run -c Release -- ./model-cache paddleocr/ppocrv5/mobile-cls --offline

本次实际输出中,第一次是 offline=False,第二次是 offline=True;两次均返回 ppocrv5-mobile-cls.onnx 的本地路径。CLS 没有识别字典,所以 dictionary_path=(none)。

需要第三篇的 v5 Mobile 全流程时,再分别准备 DET 与 REC:

shell 复制代码
dotnet run -c Release -- ./model-cache paddleocr/ppocrv5/mobile-det
dotnet run -c Release -- ./model-cache paddleocr/ppocrv5/mobile-rec

REC 会同时取得对应字典,应用用返回的 ModelPath 和 DictionaryPath 继续创建相应 OCR Profile。Ctrl+C 可取消正在进行的调用;程序等待调用结束后再释放下载客户端。

完整缓存示例源码:

text 复制代码
https://github.com/guojin-yan/DeploySharp/tree/DeploySharpV2.0/docs/blog/2.0.0/2026-10/examples/04-model-cache

9.3 缓存怎样带到离线设备

客户端先在线准备缓存,离线时直接返回本地路径;缺少文件或元数据则报告缓存未命中。处理过程如下:

离线交付时,复制整个 model-cache 根目录 ,保留 .deploysharp-paddleocr/models-paddleocr/ 下的模型、字典和目录元数据,客户端需要这些信息定位缓存。

缓存程序也按第七节发布后交付。目标机已安装 .NET Runtime 时,可以用框架依赖发布;否则选择对应 RID 的自包含发布,直接启动可执行文件。

用空目录运行 --offline 时,程序返回 model-factory.offline-cache-miss。应用可以据此提示用户先准备所选模型,或选择已有的本地文件。

十、换机失败时,按哪个方向检查

10.1 先看失败发生在哪一层

现象 优先检查 处理方向
编译通过,启动找不到 OpenCV/ORT DLL 原生包、x64、发布子目录与同目录依赖 复制完整发布目录;根据异常检查实际缺失文件
自包含程序仍提示找不到入口文件 是否只复制了 DLL/EXE、RID 是否正确 使用对应系统的整个发布目录,Windows 执行 EXE,Linux 恢复可执行权限
从快捷方式启动后找不到模型 相对路径是否依赖工作目录 按 AppContext.BaseDirectory 解析,或在配置中填写实际绝对路径
可以加载模型,框或文字却不对 模型的输入输出、颜色、缩放、字典或类别是否配套 对照示例输入,核对模型与 Profile
ORT CUDA 初始化失败或报错 801 原生包的 CUDA 主版本、驱动、CUDA/cuDNN 与文件搜索路径 使用与设备匹配的完整官方发行包;不混放 CPU/CUDA 12/CUDA 13 文件
OpenCV DNN 报导入或动态 Shape 错误 导出图、opset、输入规格和 importer 使用兼容的模型导出,或选择支持该模型的其他后端
TensorRT 反序列化失败或动态尺寸超范围 Engine 与目标设备/版本、输入 Profile 在目标环境重新构建,检查真实 Batch 和所有动态维度
Linux 报 .so 或版本化依赖缺失 native/opencv 是否完整、LD_LIBRARY_PATH、系统库 用 ldd 查缺失项,补齐实际依赖,保持整组 .so
离线缓存未命中 缓存根目录、元数据、所选模型 ID 与文件是否完整 在联网准备端补齐后重新复制,或直接使用已有本地模型路径
结果无法写出 程序安装目录是否可写 第三个参数指定应用有权限的输出目录

排障时按加载顺序检查:发布目录、原生库、模型输入输出,再到设备和输出权限。下图可以作为快速参考:

后端和设备在启动时选择,并写入日志。业务需要 CPU 降级时,也在启动逻辑中处理,记录最终使用的设备,方便排查现场问题。

10.2 从教程运行走向业务部署

交付前,先确认程序目录中具备所选模型、配套文件和 native 依赖;再从实际启动入口运行一张业务输入,检查图像/文字结果与保存位置。模型或后端升级时,重新核对对应输入输出与结果;TensorRT 还要处理 Engine 的构建与缓存。

各后端的细节可以按问题继续查阅:

text 复制代码
NuGet 安装组合:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/package-combinations.md

安装与运行时:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/installation.md

ONNX Runtime:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/onnxruntime-getting-started.md

OpenVINO:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/openvino-getting-started.md

ModelFactory:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/articles/modelfactory-getting-started.md

十一、总结与后续

本篇介绍了五种后端的安装组合,并将同一份 YOLO26 CPU 程序发布为 Windows 和 Ubuntu 自包含目录。模型较多时,可以用 ModelFactory 在线准备文件,再将完整缓存带到离线设备。

大家可以先用随文工程生成发布目录,再替换自己的模型和输入。部署到 GPU 时,在对应后端工程中补齐运行库和 Engine,沿用相同的目录管理与发布方式。

下一篇继续回到视觉任务,介绍分类、分割、关键点与旋转框:根据任务选模型、配置前处理,读取不同的结果类型,并把它们正确展示到原图上。

text 复制代码
本文主稿与配图:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/blog/2.0.0/2026-10/2026-10-28-04-multi-backend-offline.md

系列目录:
https://github.com/guojin-yan/DeploySharp/blob/DeploySharpV2.0/docs/blog/2.0.0/README.md

问题反馈:
https://github.com/guojin-yan/DeploySharp/issues

DeploySharp 库代码采用 Apache License 2.0;示例 YOLO26 模型、PaddleOCR 模型与第三方运行库遵循各自许可。反馈部署问题时,附上系统、进程架构、模型、后端、原生库版本与完整异常,方便定位。

相关推荐
Kobebryant-Manba1 小时前
IEKT向量矩阵流程&评估
人工智能·算法·矩阵
看浪的路人1 小时前
第2讲:LLM API 统一网关——把 OpenAI/Claude/文心一言 管起来
人工智能·文心一言
奈落241 小时前
工程界讲角色边界,控制论讲博弈与稳定性:从 Swarms 的实践清单到《自动化学报》的协同综述,多智能体‘协同‘这件事有两套语言
人工智能·深度学习·自动化·ai 编程
小王R语言学习1 小时前
PaddleNLP:基于飞桨的大模型开发套件
人工智能·其他·paddlepaddle
codebolt1 小时前
C++程序crash分析和内存检测
开发语言·c++
椒颜皮皮虾྅1 小时前
让 C# 读懂整张图:DeploySharp 驱动 PaddleOCR v4/v5/v6,打通 OCR 全流程
开发语言·人工智能·深度学习·学习·c#·ocr·cuda
roman_日积跬步-终至千里1 小时前
【数据建模入门】从一家小店看懂数仓与机器学习:先理业务,再串技术
数据仓库·人工智能·机器学习·数据分析·数据建模
昂好的day1 小时前
C# 同步异步与多线程:Thread / Task / 线程池一次讲透(上篇)
开发语言·windows·c#
Java后端的Ai之路1 小时前
01、Java并发编程:学习攻略如何才能学好并发编程?
java·开发语言·学习