Creazione di un componente aggiuntivo WinML

Questa guida illustra come creare un componente aggiuntivo nativo C# che usa Windows Machine Learning (WinML) nell'app Electron. WinML consente di eseguire machine learning modelli (formato ONNX) in locale nei dispositivi Windows per attività come la classificazione delle immagini, il rilevamento degli oggetti e altro ancora.

Prerequisiti

Prima di iniziare questa guida, assicurarsi di avere:

Annotazioni

WinML viene eseguito in qualsiasi dispositivo Windows 10 (1809+) o Windows 11. Per ottenere prestazioni ottimali, i dispositivi con GPU o NPU sono consigliati, ma l'API funziona anche sulla CPU.

Importante

Il componente aggiuntivo WinML richiede il experimental SDK per app di Windows. Se hai selezionato "SDK stabili" nella guida alla configurazione durante winapp init, dovrai aggiornare la versione dell'SDK. Modificare winapp.yaml e modificare la versione di Microsoft.WindowsAppSDK in 2.0.0-experimental3, quindi eseguire npx winapp restore per eseguire l'aggiornamento.

Passaggio 1: Creare un componente aggiuntivo nativo C#

Verrà ora creato un componente aggiuntivo nativo che userà le API WinML. Si userà un modello C# che sfrutta node-api-dotnet per collegare JavaScript e C#.

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

Verrà creata una winMlAddon/ cartella con:

  • addon.cs - Il codice C# che chiamerà le API WinML
  • winMlAddon.csproj - File di Project con riferimenti a Windows SDK e SDK per app di Windows
  • README.md - Documentazione su come usare il componente aggiuntivo

Il comando aggiunge anche uno script al tuo build-winMlAddon per la creazione del componente aggiuntivo e uno script al tuo clean-winMlAddon per la pulizia degli artefatti di compilazione:

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

Il modello include automaticamente riferimenti a entrambi gli SDK, quindi è possibile iniziare immediatamente a chiamare Windows API.

Verificare che tutto sia configurato correttamente creando il componente aggiuntivo:

# Build the C# addon
npm run build-winMlAddon

Annotazioni

È anche possibile creare un componente aggiuntivo C++ usando npx winapp node create-addon (senza il --template flag). I componenti aggiuntivi C++ usano node-addon-api e forniscono l'accesso diretto alle API Windows con prestazioni massime. Per una procedura dettagliata o la documentazione completa dei comandi per altre opzioni, vedere la guida per il componente aggiuntivo per le notifiche C++.

Passaggio 2: Scaricare il modello SqueezeNet e ottenere il codice di esempio

Come riferimento si userà l'esempio Classifica immagine da AI Dev Gallery . Questo esempio usa il modello SqueezeNet 1.1 per la classificazione delle immagini.

2.1. Scaricare il modello

  1. Installare AI Dev Gallery
  2. Passare all'esempio di classificazione dell'immagine
  3. Scaricare il modello SqueezeNet 1.1 (supporta CPU, GPU e NPU)
  4. Fare clic su Apri cartella contenitore per individuare il .onnx file

Scaricamento di SqueezeNet dalla Galleria Dev AI

  1. Copiare il file squeezenet1.1.onnx in la cartella models/ nella cartella principale del progetto

Annotazioni

Il modello può anche essere scaricato direttamente dal repository GitHub ONNX Model Zoo

Passaggio 3: Aggiungere pacchetti NuGet necessari

Prima di aggiungere il codice WinML, è necessario aggiungere altri pacchetti NuGet necessari per l'elaborazione delle immagini, il runtime ONNX e il supporto genAI.

3.1. Aggiornare Directory.packages.props

Aggiungere le versioni del pacchetto seguenti al Directory.packages.props file nella radice del progetto (dovrebbe essere stato creato al momento della creazione del componente aggiuntivo):

<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. Aggiornare winMlAddon.csproj

Aprire winMlAddon/winMlAddon.csproj e aggiungere i riferimenti al pacchetto a <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>

Operazioni eseguite da questi pacchetti:

  • Microsoft.ML.OnnxRuntime.Extensions : fornisce operatori e utilità aggiuntivi per il runtime ONNX
  • System.Drawing.Common - Abilita il caricamento e la manipolazione delle immagini per la pre-elaborazione
  • Microsoft. Extensions.AI - Astrazioni di intelligenza artificiale per .NET
  • Microsoft.ML.OnnxRuntimeGenAI.Managed - Associazioni gestite per ONNX Runtime GenAI
  • Microsoft.ML.OnnxRuntimeGenAI.WinML - Integrazione WinML per ONNX Runtime GenAI

Passaggio 4: Aggiungere il codice di esempio

La Galleria di sviluppo AI mostra l'implementazione completa per la classificazione delle immagini con SqueezeNet.

Codice di esempio di SqueezeNet

Questo codice è stato adattato per Electron ed è possibile trovare l'implementazione completa nell'esempio electron-winml. La winMlAddon/ cartella contiene il codice modificato da AI Dev Gallery.

Copiare l'intera winMlAddon/ cartella da samples/electron-winml/winMlAddon/ alla radice del progetto, sostituendo quella creata nel passaggio 1. L'esempio include più file oltre a addon.cs (classi helper in Utils/, un client di chat e così via) necessari per la creazione e l'esecuzione dell'addon.

Importante

È necessario copiare l'intera cartella, non solo addon.cs. Il componente aggiuntivo dipende dai file helper nella Utils/ sottocartella (Prediction.cs, ImageNet.cs, BitmapFunctions.cse così via).

Dettagli dell'implementazione chiave

Verranno ora evidenziate le parti importanti dell'implementazione e le differenze principali rispetto al codice di AI Dev Gallery:

Requisito percorso radice progetto

A differenza del codice di AI Dev Gallery, il componente aggiuntivo Electron richiede che il codice JavaScript passi il percorso radice del progetto. Questa operazione è necessaria perché:

  • Il componente aggiuntivo deve individuare il file del modello ONNX nella models/ cartella
  • Le dipendenze native (le DLL) devono essere caricate da cartelle specifiche
[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;
}

In questo modo viene selezionato automaticamente il provider di esecuzione migliore (CPU, GPU o NPU) in base alle funzionalità del dispositivo.

2. Precaricamento delle dipendenze native

Il componente aggiuntivo include un PreloadNativeDependencies() metodo per caricare le DLL necessarie. Questo approccio funziona sia per gli scenari di sviluppo che per gli scenari di produzione senza dover copiare DLL nella radice del progetto:

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

Questa operazione viene chiamata durante l'inizializzazione prima del caricamento del modello, assicurando che tutte le librerie native siano disponibili.

3. Configurazione di Electron Forge per l'impacchettamento

Per garantire che il componente aggiuntivo funzioni correttamente nelle build di produzione, è necessario configurare il packager per:

  1. Decomprimere i file nativi : le DLL, i modelli ONNX e i file con estensione node devono essere accessibili all'esterno dell'archivio ASAR
  2. Escludere file non necessari : mantenere le dimensioni del pacchetto ridotte escludendo gli artefatti di compilazione e i file temporanei

Per Electron Forge aggiornare :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
};

Scopo di questa funzione:

  1. asar.unpack - Estrae DLL, eseguibili, binari .node e modelli ONNX in app.asar.unpacked/

    • In questo modo è possibile accedervi in fase di esecuzione tramite percorsi del file system
    • Il codice JavaScript regola automaticamente i percorsi (vedere la app.asar sostituzione → app.asar.unpacked precedente)
  2. ignore - Esclude dal pacchetto finale:

    • .winapp/ - Pacchetti e intestazioni SDK (non necessari in fase di esecuzione)
    • .msix files - Output confezionati in pacchetto
    • winMlAddon/ file di origine : mantiene solo la dist/ cartella con file binari compilati

Annotazioni

Se si usa un diverso strumento per la creazione di pacchetti (generatore di elettroni e così via), è necessario configurare impostazioni simili per decomprimere le dipendenze native ed escludere i file di sviluppo. Controllare la documentazione del pacchetto per le opzioni di decompressione ASAR.

4. Classificazione delle immagini

Il ClassifyImage metodo elabora un'immagine e restituisce stime:

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

La completa implementazione gestisce:

  • Caricamento e pre-elaborazione delle immagini (ridimensionamento, normalizzazione)
  • Esecuzione dell'inferenza del modello
  • Risultati post-elaborazione per ottenere stime principali con etichette e punteggi di attendibilità

Annotazioni

Il codice sorgente completo include la pre-elaborazione delle immagini, la creazione di tensor e l'analisi dei risultati. Controllare l'implementazione di esempio per tutti i dettagli.

Informazioni sul codice

Il componente aggiuntivo fornisce queste funzioni principali:

  1. CreateAsync : inizializza il componente aggiuntivo e carica il modello SqueezeNet
  2. ClassifyImage : accetta un percorso di immagine e restituisce stime di classificazione

WinML seleziona automaticamente il dispositivo di esecuzione migliore (CPU, GPU o NPU) in base alla disponibilità.

Passaggio 5: Compilare il componente aggiuntivo C#

Compilare ora il componente aggiuntivo:

npm run build-winMlAddon

Questo compila il codice C# usando native AOT (compilazione ahead-of-time), che:

  • Crea un .node file binario (formato di componente aggiuntivo nativo)
  • Riduce il codice inutilizzato per diminuire la dimensione del bundle
  • Non richiede runtime .NET sui computer di destinazione
  • Offre prestazioni native

Il componente aggiuntivo compilato sarà in winMlAddon/dist/winMlAddon.node.

Passaggio 6: Testare il componente aggiuntivo

Adesso testiamo il funzionamento del componente aggiuntivo chiamandolo dal processo principale. Aprire src/main.js e seguire questa procedura:

6.1. Caricare il componente aggiuntivo

Aggiungi le istruzioni require nella parte superiore:

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

6.2. Creare una funzione di test

Aggiungere questa funzione per testare la classificazione delle immagini:

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);
  }
};

Punti principali:

  • La regolazione del percorso (app.asarapp.asar.unpacked) garantisce che il codice funzioni sia nelle app di sviluppo che nelle app in pacchetto
  • In questo modo si accede ai file nativi decompressi configurati in forge.config.js

6.3. Chiamare la funzione test

Aggiungere questa riga alla fine della createWindow() funzione:

testWinML();

6.4. Preparare le immagini di test

Per testare la classificazione delle immagini:

  1. Creare una test-images/ cartella nella radice del progetto
  2. Aggiungere un'immagine di test denominata sample.jpg (il codice prevede questo nome file esatto)
  3. Il modello SqueezeNet riconosce 1000 classi ImageNet diverse (animali, oggetti, scene e così via)

Quando si esegue l'app, nella console verranno visualizzati i risultati della classificazione.

Tip

Per un'implementazione completa con i gestori IPC, le finestre di dialogo di selezione dei file e un'interfaccia utente, vedi l'esempio electron-winml.

Passaggio 7: Aggiornare l'identità di debug

Per assicurarsi che il SDK per app di Windows venga caricato e disponibile per l'utilizzo, è necessario assicurarsi di configurare l'identità di debug che assicurerà che il framework venga caricato ogni volta che viene eseguita l'app. Analogamente, ogni volta che modifichi Package.appxmanifest o cambi gli asset a cui si fa riferimento nel manifesto (ad esempio le icone dell'app), devi aggiornare l'identità di debug dell'app. Corri!

npx winapp node add-electron-debug-identity

Questo comando:

  1. Legge il tuo Package.appxmanifest per ottenere i dettagli e le funzionalità dell'app
  2. Registra electron.exe nel node_modules con un'identità temporanea.
  3. Consente di testare le API necessarie per l'identità senza pacchetti MSIX completi

Annotazioni

Questo comando fa già parte dello postinstall script aggiunto nella guida all'installazione, quindi viene eseguito automaticamente dopo npm install. Tuttavia, è necessario eseguirlo manualmente ogni volta che:

  • Modifica Package.appxmanifest (modifica funzionalità, identità o proprietà)
  • Aggiornare gli asset dell'app (icone, logo e così via)

Ora esegui l'applicazione:

npm start

Controllare l'output della console. Verranno visualizzati i risultati del test WinML.

⚠️ Problema noto: arresto anomalo dell'app o finestra vuota (fare clic per espandere)

Esiste un bug noto di Windows con la creazione sparsa di pacchetti di applicazioni Electron che causa l'arresto anomalo dell'app all'avvio o al mancato renderizzazione del contenuto web. Il problema è stato risolto in Windows ma non è ancora stato propagato a tutti i dispositivi.

Per una soluzione alternativa, vedere Configurazione dell'ambiente di sviluppo .

Operazioni successive

Complimenti. È stato creato un componente aggiuntivo nativo in grado di eseguire modelli di Machine Learning con WinML. 🎉

A questo momento è possibile:

In alternativa, esplorare altre guide:

Personalizzazione per il tuo modello

Per integrare completamente il modello ONNX, è necessario:

  1. Comprendere gli input del modello : immagini, tensori, sequenze e così via.
  2. Creare associazioni di input appropriate - Convertire i dati nel formato richiesto da WinML
  3. Elaborare gli output : analizzare e interpretare le stime del modello
  4. Gestire gli errori elegantemente - il caricamento e l'inferenza del modello possono fallire

Risorse aggiuntive

Risoluzione dei problemi

Compilazione non riuscita con NU1010: gli elementi PackageReference non definiscono un PackageVersion corrispondente

Verificare che tutti i pacchetti a cui viene fatto riferimento in winMlAddon.csproj abbiano voci corrispondenti in Directory.packages.props. Per l'elenco completo dei pacchetti necessari, vedere Passaggio 3.

"non un'applicazione Win32 valida" durante il caricamento del componente aggiuntivo

Ciò significa che il componente aggiuntivo è stato creato per un'architettura diversa rispetto al runtime di Node.js/Electron. Controllare l'architettura Node.js:

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

Ricompilare quindi il componente aggiuntivo con la destinazione corrispondente:

# 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

Se di recente è stata modificata l'installazione Node.js, reinstallare node_modules anche per ottenere il file binario Electron corrispondente:

rm -rf node_modules package-lock.json
npm install

Ottenere informazioni

Buon apprendimento automatico! 🤖