建立 WinML 外掛

本指南將教你如何在 Electron 應用程式中建立一個使用 Windows 機器學習(WinML) 的 C# 原生外掛。 WinML 允許你在 Windows 裝置上本地執行machine learning model(ONNX 格式),用於影像分類、物件偵測等任務。

先決條件

在開始本指南前,請確保你已經:

備註

WinML 可在任何 Windows 10(1809+)或 Windows 11 裝置上執行。 為了最佳效能,建議使用有 GPU 或 NPU 的裝置,但 API 也能支援 CPU。

Important

WinML 外掛需要 experimental Windows 應用程式 SDK。 如果你在設定指南中 winapp init 選擇了「穩定 SDK」,你需要更新你的 SDK 版本。 編輯 winapp.yaml,並將 Microsoft.WindowsAppSDK 版本改為 2.0.0-experimental3,然後執行 npx winapp restore 來更新。

步驟 1:建立 C# 原生附加元件

我們來建立一個原生外掛,使用 WinML API。 我們將使用 C# 模板,利用 node-api-dotnet 來橋接 JavaScript 和 C#。

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

這會建立一個 winMlAddon/ 資料夾,內容包括:

  • addon.cs - 你的 C# 程式碼,能呼叫 WinML API
  • 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 開發者畫廊中的 Classify Image 範例作為參考。 本範例使用 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/ 資料夾

備註

此模型也可直接從 ONNX Model Zoo GitHub 倉庫

步驟 3:新增必要的 NuGet 套件

在加入 WinML 程式碼之前,我們還需要新增額外的 NuGet 套件,以支援影像處理、ONNX 執行時及生成式人工智慧(GenAI)。

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 執行環境 GenAI 的 WinML 整合

步驟 4:加入範例程式碼

AI 開發者畫廊展示了 SqueezeNet 影像分類的完整實作:

SqueezeNet 範例程式碼

我們已將此程式碼改編為 Electron,你可以在 electron-winml 範例中找到完整的實作。 winMlAddon/ 資料夾包含 AI 開發者畫廊中修改過的程式碼。

將整個 winMlAddon/ 資料夾從 samples/electron-winml/winMlAddon/ 複製到你的專案根目錄,替換步驟 1 中建立的資料夾。 範例中包含多個檔案,除了 addon.cs 之外,還有 Utils/ 中的輔助類別、聊天客戶端等,這些都屬於插件建置與執行所需的檔案。

Important

你必須複製 整個資料夾,而不是只複製 addon.cs。 外掛依賴子資料夾中的Utils/輔助檔案(Prediction.csImageNet.csBitmapFunctions.cs等)。

主要實作細節

讓我們來強調實作中的重要部分以及與 AI 開發者畫廊程式碼的主要差異:

1. 專案根目錄路徑需求

與 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/ 資料夾

備註

如果你用的是不同的封裝工具(像是 electron-builder 等),你需要設定類似的設定來解壓原生相依並排除開發檔案。 請查閱包裝商的文件,了解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. ClassifyImage - 取得影像路徑並回傳分類預測

WinML 會根據可用性自動選擇最佳執行裝置(CPU、GPU 或 NPU)。

步驟 5:建立 C# 外掛

現在製作附加元件:

npm run build-winMlAddon

這會用 Native AOT (提前編譯)編譯你的 C# 程式碼,該程式:

  • 建立一個 .node 二進位檔(原生附加元件格式)
  • 為較小的套件尺寸修剪未使用的程式碼
  • 目標機器不需要 .NET 執行環境
  • 提供本地效能

編譯後的附加元件會安裝在 winMlAddon/dist/winMlAddon.node

步驟 6:測試附加元件

現在讓我們測試外掛是否能正常運作,方法是從主程序呼叫它。 打開 src/main.js 並遵循以下步驟:

6.1. 載入附加元件

在最上面加上需求陳述:

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

備註

這個指令已經是我們在設定指南中加入的腳本的一部分,所以它會在之後自動執行。 不過,你需要手動操作每當你:

  • 修改 Package.appxmanifest (變更能力、身份或屬性)
  • 更新應用程式素材(圖示、標誌等)

現在啟動你的應用程式:

npm start

檢查主機輸出——你應該會看到 WinML 測試結果!

⚠️ 已知問題:應用程式當機或視窗空白(點擊展開)

Windows 有一個已知的 Electron 應用程式封裝稀疏錯誤,會導致應用程式啟動時當機或無法渲染網頁內容。 這個問題在 Windows 上已經修正,但尚未擴散到所有裝置。

請參考 開發環境設定 以取得解決方法。

後續步驟

祝賀! 你成功建立了一個原生外掛,能用 WinML 執行機器學習模型! 🎉

現在你準備好:

或者探索其他指南:

為您的車型客製化

要完整整合你的 ONNX 模型,你需要:

  1. 了解你的模型輸入—— 影像、張量、序列等等。
  2. 建立正確的輸入綁定 ——將資料轉換成 WinML 預期的格式
  3. 處理輸出 ——解析並解讀模型的預測
  4. 優雅地處理錯誤 ——模型載入與推論可能會失敗

其他資源

Troubleshooting

NU1010 建置失敗:PackageReference 項目未定義對應的 PackageVersion

確保winMlAddon.csproj中參考的所有套件在Directory.packages.props中都有相應的條目。 請參閱步驟3以了解所需套件的完整清單。

載入外掛時出現「not a valid Win32 application」錯誤

這表示這個外掛是為與你的 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

取得說明

祝你機器學習愉快! 🤖