创建 WinML 加载项

本指南介绍如何在 Electron 应用中创建使用 Windows 机器学习(WinML)的 C# 本机加载项。 使用 WinML,可以在 Windows 设备上本地运行machine learning模型(ONNX 格式),以便执行图像分类、对象检测等任务。

先决条件

在开始本指南之前,请确保已:

注释

WinML 在任何Windows 10(1809+)或Windows 11设备上运行。 为了获得最佳性能,建议使用 GPU 或 NPU 的设备,但 API 也适用于 CPU。

Important

WinML 加载项需要 experimental Windows 应用 SDK。 如果在安装指南的winapp init期间选择了“稳定 SDKs”,则需要更新 SDK 版本。 编辑 winapp.yaml并将 Microsoft.WindowsAppSDK 版本更改为 2.0.0-experimental3,然后运行 npx winapp restore 进行更新。

步骤 1:创建 C# 原生插件

让我们创建一个将使用 WinML API 的本机加载项。 我们将使用利用 node-api-dotnet 桥接 JavaScript 和 C# 的 C# 模板。

npx winapp node create-addon --template cs --name winMlAddon

这将创建一个 winMlAddon/ 文件夹,其中包含:

  • addon.cs - 将调用 WinML API 的 C# 代码
  • winMlAddon.csproj - 引用 Windows SDK 和 Windows 应用 SDK 的项目文件
  • README.md - 有关如何使用加载项的文档

该命令还会将一个build-winMlAddon脚本添加到您的package.json中,用于构建加载项,同时添加一个clean-winMlAddon脚本用于清理构建产物。

{
  "scripts": {
    "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release",
    "clean-winMlAddon": "dotnet clean ./winMlAddon/winMlAddon.csproj"
  }
}

该模板会自动包含对两个 SDK 的引用,因此你可以立即开始调用Windows API!

让我们通过构建加载项来验证所有设置是否正确。

# Build the C# addon
npm run build-winMlAddon

注释

还可以使用 npx winapp node create-addon (不使用 --template 标志)创建 C++ 加载项。 C++ 加载项使用 node-addon-api并提供对具有最佳性能的 Windows API 的直接访问。 请参阅 C++ 通知加载项指南 以了解演练,或查看 完整的命令文档 以获取更多选项。

步骤 2:下载 SqueezeNet 模型并获取示例代码

我们将使用 AI 开发库中分类图像示例作为参考。 此示例使用 SqueezeNet 1.1 模型进行图像分类。

2.1. 下载模型

  1. 安装 AI 开发画廊
  2. 导航到 分类图像示例
  3. 下载 SqueezeNet 1.1 模型(它支持 CPU、GPU 和 NPU)
  4. 单击“ 打开包含文件夹 ”以找到 .onnx 该文件

从 AI 开发库下载 SqueezeNet

  1. squeezenet1.1.onnx 文件复制到 project 根目录中的 models/ 文件夹中

步骤 3:添加所需的 NuGet 包

在添加 WinML 代码之前,我们需要添加图像处理、ONNX 运行时和 GenAI 支持所需的其他 NuGet 包。

3.1. 更新 Directory.packages.props

将以下包版本添加到 Directory.packages.props 项目的根目录中的文件(应在创建加载项时创建):

<Project>
  <PropertyGroup>
    <!-- Enable central package versioning -->
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Microsoft.JavaScript.NodeApi" Version="0.9.17" />
    <PackageVersion Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.17" />
    <!-- Add these packages for WinML -->
+   <PackageVersion Include="Microsoft.ML.OnnxRuntime.Extensions" Version="0.14.0" />
+   <PackageVersion Include="System.Drawing.Common" Version="9.0.9" />
+   <PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
+   <PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.Managed" Version="0.10.1" />
+   <PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.WinML" Version="0.10.1" />
    
    <!-- These versions may be updated automatically during restore to match yaml -->
    <PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental3" />
    <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
  </ItemGroup>
</Project>

3.2. 更新 winMlAddon.csproj

打开 winMlAddon/winMlAddon.csproj 包,并将包引用添加到 <ItemGroup>

<ItemGroup>
  <PackageReference Include="Microsoft.JavaScript.NodeApi" />
  <PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" />
  <!-- Add these packages for WinML -->
+ <PackageReference Include="Microsoft.ML.OnnxRuntime.Extensions" />
+ <PackageReference Include="System.Drawing.Common" />
+ <PackageReference Include="Microsoft.Extensions.AI" />
+ <PackageReference Include="Microsoft.ML.OnnxRuntimeGenAI.Managed" />
+ <PackageReference Include="Microsoft.ML.OnnxRuntimeGenAI.WinML" />
  
  <PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
  <PackageReference Include="Microsoft.WindowsAppSDK" />
</ItemGroup>

这些包执行的操作:

  • Microsoft.ML.OnnxRuntime.Extensions - 为 ONNX 运行时提供其他运算符和实用工具
  • System.Drawing.Common - 为预处理启用图像加载和操作
  • Microsoft。Extensions.AI - 适用于.NET的 AI 抽象
  • Microsoft.ML.OnnxRuntimeGenAI.Managed - ONNX Runtime GenAI 的托管绑定
  • Microsoft.ML.OnnxRuntimeGenAI.WinML - ONNX Runtime GenAI 的 WinML 集成

步骤 4:添加示例代码

AI 开发库显示了使用 SqueezeNet 进行图像分类的完整实现:

SqueezeNet 示例代码

我们已为 Electron 改编了此代码,你可以在 电子 winml 示例中找到完整的实现。 该 winMlAddon/ 文件夹包含 AI 开发库中修改的代码。

将整个 winMlAddon/ 文件夹从 samples/electron-winml/winMlAddon/ 复制到项目根目录,替换步骤 1 中创建的文件夹。 示例包括多于addon.cs的多个文件(Utils/中的帮助类、聊天客户端等),这些文件是构建和运行插件所必需的。

Important

必须复制 整个文件夹,而不仅仅是 addon.cs。 加载项依赖于子文件夹中的帮助程序文件 Utils/Prediction.csImageNet.csBitmapFunctions.cs 等等)。

关键实现详细信息

重点介绍实现的重要部分,以及与 AI Dev Gallery 代码的主要区别:

1. Project根路径要求

与 AI 开发库代码不同,我们的 Electron 加载项需要 JavaScript 代码才能传递 项目根路径。 这是必要的,因为:

  • 加载项需要在文件夹中找到 ONNX 模型文件models/
  • 需要从特定目录加载本机依赖项(DLL)
[JSExport]
public static async Task<Addon> CreateAsync(string projectRoot)
{
    if (!Path.Exists(projectRoot))
    {
        throw new Exception("Project root is invalid.");
    }

    var addon = new Addon(projectRoot);
    addon.PreloadNativeDependencies();

    string modelPath = Path.Join(projectRoot, "models", @"squeezenet1.1-7.onnx");
    await addon.InitModel(modelPath, ExecutionProviderDevicePolicy.DEFAULT, null, false, null);

    return addon;
}

这会根据设备功能自动选择最佳执行提供程序(CPU、GPU 或 NPU)。

2. 预加载原生依赖项

该加载项包含一种 PreloadNativeDependencies() 方法,用于加载所需的 DLL。 此方法适用于 开发和生产 方案,无需将 DLL 复制到项目根目录:

private void PreloadNativeDependencies()
{
    // Loads required DLLs from the winMlAddon build output
    // This ensures dependencies are available regardless of the execution context
}

在加载模型之前,在初始化期间调用此方法,确保所有本机库都可用。

3. 配置 Electron Forge 以进行打包

若要确保加载项在生产版本中正常工作,需要将打包程序配置为:

  1. 解压本地文件 - DLL、ONNX模型及.node文件必须在ASAR存档之外可访问
  2. 排除不必要的文件 - 通过排除生成项目和临时文件来保持包大小较小

对于 Electron Forge,请更新您的 forge.config.js

// From samples/electron-winml/forge.config.js
module.exports = {
  packagerConfig: {
    asar: {
      // Unpack native files so they can be accessed by the addon
      unpack: "**/*.{dll,exe,node,onnx}"
    },
    ignore: [
      // Exclude .winapp folder (SDK packages and headers)
      /^\/.winapp\//,
      // Exclude MSIX packages
      "\\.msix$",
      // Exclude winMlAddon source files, but keep the dist folder
      /^\/winMlAddon\/(?!dist).+/
    ]
  },
  // ... rest of your config
};

此功能的作用:

  1. asar.unpack - 将 DLL、可执行文件、.node 二进制文件和 ONNX 模型提取到 app.asar.unpacked/

    • 这使得它们在运行时可通过文件系统路径进行访问
    • JavaScript 代码自动调整路径(请参阅 app.asar 上面的→ app.asar.unpacked 替换)
  2. ignore - 从最终包中排除:

    • .winapp/ - SDK 包和标头(运行时不需要)
    • .msix 文件 - 打包输出
    • winMlAddon/ 源文件 - 仅保留包含已编译二进制文件的 dist/ 文件夹

注释

如果使用其他打包工具(电子生成器等),则需要配置类似的设置来解压缩本机依赖项和排除开发文件。 查看打包程序的文档,了解 ASAR 解包选项。

4. 图像分类

该方法 ClassifyImage 处理图像并返回预测:

[JSExport]
public async Task<Prediction[]> ClassifyImage(string imagePath)
{
    // Loads the image, preprocesses it, and runs inference
    // Returns top predictions with labels and confidence scores
}

完整的实现管理以下内容:

  • 图像加载和预处理(调整大小、规范化)
  • 运行模型推理
  • 处理后结果以获取具有标签和置信度分数的顶级预测

注释

完整的源代码包括图像预处理、张量创建和结果分析。 检查 示例实现 以了解所有详细信息。

了解代码

加载项提供以下主要函数:

  1. CreateAsync - 初始化加载项并加载 SqueezeNet 模型
  2. ClassificationImage - 获取图像路径并返回分类预测

WinML 根据可用性自动选择最佳执行设备(CPU、GPU 或 NPU)。

步骤 5:生成 C# 加载项

现在生成加载项:

npm run build-winMlAddon

这将使用 本机 AOT(提前编译)来编译 C# 代码:

  • 创建 .node 二进制(原生加载项格式)
  • 剪裁未使用的代码以缩小捆绑包大小
  • 目标计算机上不需要.NET 运行时
  • 提供原生性能

编译的加载项将位于 winMlAddon/dist/winMlAddon.node.

步骤 6:测试加载项

现在,让我们通过从主进程调用加载项来测试加载项的工作原理。 打开 src/main.js 并按照以下步骤操作:

6.1. 加载加载项

在顶部添加 require 语句:

const winMlAddon = require('../winMlAddon/dist/winMlAddon.node');

6.2。 创建测试函数

添加此函数以测试图像分类:

const testWinML = async () => {
  console.log('Testing WinML addon...');
  
  try {
    let projectRoot = path.join(__dirname, '..');
    // Adjust path for packaged apps
    if (projectRoot.includes('app.asar')) {
      projectRoot = projectRoot.replace('app.asar', 'app.asar.unpacked');
    }
    
    const addon = await winMlAddon.Addon.createAsync(projectRoot);
    console.log('Model loaded successfully!');
    
    // Classify a sample image
    const imagePath = path.join(projectRoot, 'test-images', 'sample.jpg');
    const predictions = await addon.classifyImage(imagePath);
    
    console.log('Top predictions:');
    predictions.slice(0, 5).forEach((pred, i) => {
      console.log(`${i + 1}. ${pred.label}: ${(pred.confidence * 100).toFixed(2)}%`);
    });
  } catch (error) {
    console.error('Error testing WinML:', error.message);
  }
};

要点

  • 路径调整(app.asarapp.asar.unpacked)可确保代码在开发和打包应用中正常工作
  • 这会访问forge.config.js中配置的未打包本机文件。

6.3. 调用测试函数

在函数末尾 createWindow() 添加此行:

testWinML();

6.4. 准备测试映像

测试图像分类:

  1. test-images/在项目根目录中创建文件夹
  2. 添加名为 sample.jpg 的测试图像(代码需要此确切文件名)
  3. SqueezeNet 模型可识别 1000 个不同的 ImageNet 类(动物、对象、场景等)

运行应用时,控制台中会显示分类结果!

Tip

有关包含 IPC 处理程序、文件选择对话框和用户界面的完整实现,请参阅 electron-winml 示例

步骤 7:更新调试标识

为了确保加载Windows 应用 SDK并可供使用,我们需要确保设置调试标识,以确保每当应用运行时都会加载框架。 同样,每当修改 Package.appxmanifest 或更改清单中引用的资产(如应用图标),都需要更新应用的调试标识。 运行:

npx winapp node add-electron-debug-identity

此命令:

  1. 读取您的 Package.appxmanifest 以获取应用的详细信息和功能
  2. 在你的electron.exe中以临时身份注册node_modules
  3. 使你无需完全 MSIX 打包即可测试身份验证所需的 API

注释

此命令已经是我们在安装指南中添加的 postinstall 脚本的一部分,因此它在 npm install 之后自动运行。 但是,无论何时,都需要手动运行它:

  • 修改 Package.appxmanifest (更改功能、标识或属性)
  • 更新应用资产(图标、徽标等)

现在运行应用:

npm start

检查控制台输出 - 应会看到 WinML 测试结果!

⚠️ 已知问题:应用崩溃或空白窗口(单击以展开)

已知存在与稀疏打包相关的Windows bug,会导致Electron应用程序启动时崩溃或无法呈现Web内容。 此问题已在Windows中修复,但尚未传播到所有设备。

有关解决方法,请参阅 开发环境设置

后续步骤

祝贺! 你已成功创建一个本机加载项,可以使用 WinML 运行机器学习模型! 🎉

现在,你已准备好:

或者浏览其他指南:

为您的模型进行自定义

若要完全集成 ONNX 模型,需要:

  1. 了解模型的输入 - 图像、张量、序列等。
  2. 创建正确的输入绑定 - 将数据转换为 WinML 所需的格式
  3. 处理输出 - 分析和解释模型的预测
  4. 正常处理错误 - 模型加载和推理可能会失败

其他资源

故障排除

生成失败,错误 NU1010:PackageReference 项未定义相应的 PackageVersion

确保在winMlAddon.csproj中引用的所有包都在Directory.packages.props中有相应的匹配项。 有关所需包的完整列表,请参阅步骤 3。

加载加载项时,“不是有效的 Win32 应用程序”

这意味着加载项是为与 Node.js/Electron 运行时不同的体系结构构建的。 检查 Node.js 体系结构:

node -e "console.log(process.arch)"

然后使用匹配的目标重新生成加载项:

# For x64 Node.js:
dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-x64

# For ARM64 Node.js:
dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-arm64

如果最近更改了 Node.js 安装,请重新安装 node_modules 以获取匹配的 Electron 二进制文件:

rm -rf node_modules package-lock.json
npm install

获取帮助

快乐的机器学习! 🤖