ITSkillsCenter
Intelligence Artificielle

Détection YOLO v11 dans le navigateur via WebGPU et ONNX Runtime Web

9 min de lecture

📍 Guide principal : Détection d’objets en 2026 : pipeline YOLO v11 + Ultralytics + Roboflow. Prérequis : exporter le modèle en ONNX.

Faire tourner YOLO v11 directement dans le navigateur, sans serveur d’inférence, n’est plus de la science-fiction. WebGPU, l’API graphique standardisée par le W3C et activée par défaut dans Chrome depuis 2023, dans Firefox sur Windows depuis 2025 et dans Safari 26, donne aux pages web l’accès au GPU pour des calculs généraux. Couplé à ONNX Runtime Web, le modèle YOLO devient une bibliothèque JS qu’on charge comme n’importe quel SDK et qui s’exécute sur le GPU local de l’utilisateur. Avantages : aucune donnée image ne quitte la machine, pas de coût d’infrastructure d’inférence, latence réseau zéro. Limites : taille du modèle limitée par la connexion (préférer YOLO11n ou YOLO11s), incompatibilités sur les vieux navigateurs ou téléphones bas de gamme.

Prérequis

  • Un modèle YOLO v11 exporté en ONNX (cf. tutoriel d’export, format léger en FP16 recommandé).
  • Node.js 20 LTS ou supérieur installé pour bundler la démo.
  • Un navigateur supportant WebGPU : Chrome 113+, Edge 113+, Firefox 141+ (Windows), Safari 26+.
  • Un éditeur de code (VS Code par exemple).
  • Niveau attendu : à l’aise avec JavaScript moderne, ES modules, npm.
  • Temps estimé : 60 minutes pour la première démo, plus court ensuite.

Étape 1 — Vérifier la disponibilité de WebGPU

Avant tout, vérifier que le navigateur cible expose bien l’API WebGPU. La méthode officielle, recommandée par la spec W3C, consiste à interroger navigator.gpu et tenter de récupérer un adaptateur graphique :

// À coller dans la console du navigateur
if (!navigator.gpu) {
  console.log("WebGPU non supporté sur ce navigateur.");
} else {
  navigator.gpu.requestAdapter().then(adapter => {
    console.log("Adaptateur GPU :", adapter ? adapter.info : "indisponible");
  });
}

Sur un Chrome récent avec un GPU actif, vous obtenez un objet contenant le nom du GPU, le vendor, et l’architecture. Si navigator.gpu est undefined, le navigateur est trop ancien ou WebGPU est désactivé. Sur Firefox, vérifier le flag dom.webgpu.enabled dans about:config. Sur les machines sans GPU (CPU uniquement), requestAdapter peut échouer ; dans ce cas, basculer sur le backend WASM d’ONNX Runtime Web qui fonctionne sans GPU mais avec une latence sensiblement plus élevée.

Étape 2 — Initialiser le projet et installer ONNX Runtime Web

On part d’un projet npm vide pour avoir un setup propre, isolé et reproductible. Dans un terminal :

mkdir yolo-webgpu-demo && cd yolo-webgpu-demo
npm init -y
npm install onnxruntime-web

Le package onnxruntime-web contient le runtime ONNX compilé en WebAssembly avec les bindings WebGPU. Il pèse environ 30 Mo (mais grâce au tree-shaking et au lazy-loading des backends, le bundle final déployé sera bien plus léger). Vérifier l’installation :

node -e "const ort = require('onnxruntime-web'); console.log(ort.env.versions);"

La sortie affiche les versions du runtime, du modèle d’opérations et des backends disponibles. Avec une version 1.18+, le backend WebGPU est inclus par défaut.

Pour servir la démo sans configurer un bundler complexe, on utilisera un petit serveur Vite qui gère ESM, hot reload et les chemins relatifs vers les assets WASM :

npm install --save-dev vite

Étape 3 — Préparer le modèle ONNX et les ressources

Copier le fichier best.onnx exporté précédemment dans un dossier public/models/ du projet. Vite servira ce dossier tel quel à la racine. Vérifier la taille : YOLO11n.onnx en FP16 fait environ 5 Mo, YOLO11s.onnx environ 20 Mo. Au-delà, le téléchargement initial deviendra long pour les utilisateurs en connexion mobile et il vaut mieux rester sur YOLO11n pour le navigateur.

Récupérer aussi la liste des classes du modèle. Si vous avez entraîné via Roboflow, le fichier data.yaml contient les noms ; les copier dans un fichier JS exporté :

// public/models/labels.js
export const LABELS = ["stop", "cedez", "limitation-50"];
// ou pour COCO :
// export const LABELS = ["person", "bicycle", "car", ...];

Étape 4 — Charger le modèle et lancer une inférence

Le code d’inférence ressemble à du PyTorch côté API : on charge le modèle, on prépare un tensor d’entrée, on appelle session.run, on lit la sortie. La vraie subtilité tient au pré-traitement de l’image (resize 640×640, normalisation, conversion HWC→CHW) qui doit reproduire exactement ce que fait Ultralytics côté Python pour ne pas dégrader la mAP. Voici le squelette principal :

// src/main.js
import * as ort from 'onnxruntime-web/webgpu';

async function loadModel() {
  // Choisir explicitement WebGPU comme backend
  const session = await ort.InferenceSession.create(
    '/models/best.onnx',
    { executionProviders: ['webgpu'] }
  );
  console.log('Modèle chargé sur WebGPU');
  return session;
}

async function preprocess(imgEl) {
  const canvas = new OffscreenCanvas(640, 640);
  const ctx = canvas.getContext('2d');
  ctx.drawImage(imgEl, 0, 0, 640, 640);
  const data = ctx.getImageData(0, 0, 640, 640).data;

  // Conversion RGBA → RGB normalisé [0,1] et passage HWC → CHW
  const float32Data = new Float32Array(3 * 640 * 640);
  for (let i = 0; i < 640 * 640; i++) {
    float32Data[i] = data[i * 4] / 255.0;                    // R
    float32Data[i + 640 * 640] = data[i * 4 + 1] / 255.0;     // G
    float32Data[i + 2 * 640 * 640] = data[i * 4 + 2] / 255.0; // B
  }
  return new ort.Tensor('float32', float32Data, [1, 3, 640, 640]);
}

async function detect(session, imgEl) {
  const tensor = await preprocess(imgEl);
  const output = await session.run({ images: tensor });
  return output;
}

L'objet output contient le tensor brut de sortie YOLO de forme [1, 84, 8400] pour 80 classes COCO (84 = 4 coords + 80 classes). Pour des classes personnalisées, c'est [1, 4 + num_classes, 8400]. Il reste à appliquer le NMS (Non-Maximum Suppression) côté JS pour ne garder que les meilleures boîtes non-redondantes — voir l'étape suivante.

Étape 5 — Appliquer le post-traitement et afficher les boîtes

Le NMS supprime les détections redondantes qui pointent sur le même objet avec un seuil de chevauchement (IoU). En vanilla JS, on peut implémenter une version compacte basée sur le tri par score, ou utiliser une bibliothèque tierce. Pour rester minimal :

function postprocess(outputTensor, threshold = 0.5, iouThreshold = 0.45) {
  const data = outputTensor.data; // Float32Array de taille 84*8400
  const numAnchors = 8400;
  const numClasses = 80; // adapter selon votre modèle
  const boxes = [];

  for (let i = 0; i < numAnchors; i++) {
    let maxScore = 0, classId = -1;
    for (let c = 0; c < numClasses; c++) {
      const score = data[(4 + c) * numAnchors + i];
      if (score > maxScore) { maxScore = score; classId = c; }
    }
    if (maxScore > threshold) {
      const cx = data[0 * numAnchors + i];
      const cy = data[1 * numAnchors + i];
      const w  = data[2 * numAnchors + i];
      const h  = data[3 * numAnchors + i];
      boxes.push({
        x1: cx - w / 2, y1: cy - h / 2,
        x2: cx + w / 2, y2: cy + h / 2,
        score: maxScore, classId
      });
    }
  }

  // NMS simple par tri par score décroissant
  boxes.sort((a, b) => b.score - a.score);
  const keep = [];
  for (const box of boxes) {
    let suppressed = false;
    for (const k of keep) {
      const iou = computeIoU(box, k);
      if (iou > iouThreshold) { suppressed = true; break; }
    }
    if (!suppressed) keep.push(box);
  }
  return keep;
}

function computeIoU(a, b) {
  const x1 = Math.max(a.x1, b.x1), y1 = Math.max(a.y1, b.y1);
  const x2 = Math.min(a.x2, b.x2), y2 = Math.min(a.y2, b.y2);
  const inter = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
  const areaA = (a.x2 - a.x1) * (a.y2 - a.y1);
  const areaB = (b.x2 - b.x1) * (b.y2 - b.y1);
  return inter / (areaA + areaB - inter);
}

Côté affichage, dessiner les boîtes filtrées sur un canvas par-dessus l'image source, avec un libellé qui combine le nom de classe (depuis LABELS[classId]) et le score. Pour une démo continue sur webcam, encadrer le tout dans une boucle requestAnimationFrame qui appelle detect à chaque frame.

Étape 6 — Mesurer la latence et choisir la taille du modèle

La latence d'inférence varie énormément selon le GPU local de l'utilisateur. Pour profiler, encadrer l'appel à session.run avec performance.now() et calculer la moyenne sur 50 frames pour lisser les variations :

const N = 50;
const times = [];
for (let i = 0; i < N; i++) {
  const t0 = performance.now();
  await session.run({ images: tensor });
  times.push(performance.now() - t0);
}
console.log("Latence moyenne :", times.reduce((a,b)=>a+b)/N, "ms");

Sur un laptop équipé d'une intégrée Intel récente, YOLO11n.onnx en FP16 tourne à 30-60 ms par frame (15-30 FPS), assez fluide pour une démo. Sur GPU dédié (RTX 30xx/40xx), la même inférence descend à 5-15 ms. Si vous visez une exécution sur smartphone, tester sur un appareil de milieu de gamme — les téléphones Android d'entrée de gamme peuvent monter à 200-400 ms par frame, ce qui rend l'expérience inutilisable. Dans ce cas, basculer sur le backend WASM SIMD avec quantification INT8.

Problèmes courants côté navigateur

Symptôme Cause probable Action
« No available backend found » WebGPU non supporté ou flag désactivé Vérifier navigator.gpu, mettre à jour le navigateur, activer le flag dans Firefox.
Inférence qui retourne tout à zéro Pré-traitement incorrect (canal, normalisation, ordre HWC/CHW) Comparer pixel par pixel avec le pré-traitement Ultralytics, ajuster.
Latence x10 par rapport au benchmark Backend WASM utilisé silencieusement Forcer executionProviders: ['webgpu'] et vérifier dans la console.
Page qui crashe au chargement du modèle Modèle trop volumineux pour la mémoire mobile Réduire à YOLO11n FP16, ou activer la quantification INT8.
Boîtes décalées par rapport aux objets Image non resizée à 640×640 avant tensor Resizer dans un OffscreenCanvas avant getImageData.
WASM files non trouvés (404) Vite n'a pas servi le dossier node_modules/onnxruntime-web/dist Configurer vite.config.js avec publicDir incluant ces assets.

Pages liées

Ressources externes

Avec une démo WebGPU fonctionnelle, vous disposez d'un mode de déploiement complémentaire au backend serveur — utile pour les démos commerciales, les outils internes confidentiels, ou les cas où le RGPD interdit toute remontée d'image vers un serveur central.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité