AI‑Assisted Image Processing avec ONNX Runtime en Rust

Par Emmanuel Forgues - 11 août 2025

Publié initialement le 11 août 2025.

Mis à jour le 5 mai 2026.

Migré vers StratoSentry le 5 mai 2026.

Chapô

L’inférence de modèles d’intelligence artificielle (IA) sur des images est aujourd’hui un pilier des applications métiers – détection d’anomalies, amélioration de la qualité visuelle, reconnaissance d’objets ou segmentation médicale. Le recours à ONNX Runtime, moteur d’exécution open‑source compatible avec le format interopérable ONNX, combiné au langage système Rust promet une performance native, une sûreté mémoire et un coût opérationnel maîtrisé. Cet article décrypte les principes techniques, les bénéfices organisationnels et les contraintes à anticiper lorsqu’on construit des pipelines de traitement d’image IA en Rust avec ONNX Runtime, afin d’aider décideurs, architectes et développeurs à prendre des décisions éclairées.

Introduction

Imaginez une plateforme de contrôle qualité dans l’industrie agroalimentaire qui doit analyser plusieurs dizaines de mégapixels par seconde pour repérer des défauts visuels. La solution repose sur un modèle de segmentation entraîné sous PyTorch, exporté au format ONNX et exécuté en production sur des serveurs Linux équipés de GPU. Le choix du moteur d’inférence, du langage d’implémentation et de l’orchestration influence directement la latence, le coût énergétique, la robustesse face aux pannes et la conformité aux exigences de protection des données (RGPD, ISO 27001).

ONNX Runtime offre une abstraction unifiée pour exploiter les accélérateurs matériels (CPU, GPU, TensorRT, DirectML…), tandis que Rust, grâce à son système de typage strict et son modèle d’emprunt, élimine les classes classiques de bugs mémoire qui pèsent sur la disponibilité des services critiques. L’enjeu est double : tirer parti du potentiel algorithmique de l’IA tout en garantissant une infrastructure fiable, sécurisée et économiquement viable.

1️⃣ Contexte et enjeux du traitement d’image assisté par IA

EnjeuDescriptionImpact métier
Volume & vélocitéCroissance exponentielle des flux d’images (vidéo surveillance, IoT visuel)Besoin de latence < 100 ms pour décision en temps réel
Complexité algorithmiqueModèles de deep learning (CNN, Transformers) très gourmands en calculNécessité d’accélérateurs matériels et d’optimisations logicielles
Sécurité & conformitéDonnées potentiellement sensibles (visages, informations médicales)Obligations RGPD, chiffrement au repos et en transit
Coût opérationnelFacturation à la consommation du cloud GPU, licences logiciellesRecherche d’une solution « pay‑as‑you‑go » sans surprovisionnement

Ces facteurs poussent les organisations à repenser leurs architectures de traitement d’image pour passer d’un modèle monolithique Python/Flask vers des services légers, compilés et résilients.

2️⃣ ONNX Runtime : un moteur d’inférence universel

2.1 Le format ONNX

ONNX (Open Neural Network Exchange) est une spécification ouverte qui décrit les graphes de calcul d’un modèle IA indépendamment du framework d’origine (PyTorch, TensorFlow, Scikit‑Learn). Un fichier .onnx contient :

  • la topologie du graphe (opérations, connexions),
  • les poids des paramètres (tensors),
  • les métadonnées (versions d’opérateurs, domaine).

Cette portabilité évite le verrouillage propriétaire et facilite le déploiement sur différents environnements.

2.2 Architecture d’ONNX Runtime

+-------------------+
|   ONNX Model      |
|   (.onnx)         |
+--------+----------+
         |
         v
+--------+----------+    +------------------+
|  Session Builder  |--> | Execution Provider|
+--------+----------+    +------------------+
         |                     |
         v                     v
+--------+----------+    +------------------+
|   Graph Optimizer |    |   Hardware (CPU,|
+--------+----------+    |   GPU, TensorRT)|
         |               +------------------+
         v
+-------------------+
|  Inference Engine |
+-------------------+
  • Session Builder crée une instance de session qui charge le modèle et prépare les ressources.
  • Execution Provider (EP) est l’abstraction du matériel ; ONNX Runtime fournit des EP pour CPU, CUDA, TensorRT, DirectML, etc.
  • Graph Optimizer applique des transformations (fusion d’opérateurs, quantisation) afin de réduire la charge computationnelle.

Le moteur expose une API C/C++ et plusieurs bindings (Python, .NET, Java, Rust). La version 1.18 (avril<0xE2><0x80><0xAF>2024) introduit le Dynamic Quantization côté client et l’optimisation du memory planner pour les scénarios à faible latence.

2.3 Avantages pour le traitement d’image

AtoutPourquoi c’est pertinent
InteropérabilitéUn même modèle ONNX peut être réutilisé sur des serveurs CPU, GPU ou FPGA sans recompilation.
PerformanceOptimisations de graphe et exécution multi‑threadée native, souvent 2–3× plus rapide que l’exécution pure Python.
Footprint réduitRuntime léger (~ 12 Mo) comparé aux bibliothèques complètes des frameworks d’origine.
Licence permissive (MIT)Aucun coût de licence, compatible avec les projets propriétaires ou open‑source.

3️⃣ Pourquoi choisir Rust pour l’inférence ?

3.1 Sécurité mémoire et absence de GC

Rust applique le principe du borrow checker à la compilation : chaque référence a un propriétaire unique ou est partagée en lecture seule, ce qui élimine les fuites de mémoire, les débordements de tampon et les data races. Dans des services d’inférence où les entrées sont des flux d’images continus, ces garanties assurent une disponibilité élevée et évitent les redémarrages intempestifs.

3.2 Performances proches du C/C++

Le compilateur LLVM génère du code natif optimisé, sans overhead de runtime (garbage collector). Les benchmarks de la Rust‑Python interopérabilité montrent que le passage d’un tableau NumPy à une fonction Rust via pyo3 réduit le temps de copie de 30 % en moyenne. Pour les pipelines d’image où chaque milliseconde compte, cela se traduit par un gain net.

3.3 Écosystème croissant pour l’IA

Le crate onnxruntime (v0.15) fournit des bindings sûrs et idiomatiques à ONNX Runtime : création de session, injection d’inputs sous forme de tensors Rust (ndarray::Array) et récupération des outputs sans conversions coûteuses. D’autres crates (image, tch-rs, tract-onnx) enrichissent l’écosystème pour le pré‑traitement et la post‑analyse.

3.4 Déploiement statique & portabilité

Rust compile en binaire statique, facilitant la containerisation (Docker, OCI) ou le déploiement sur des systèmes embarqués où les dépendances dynamiques sont limitées (ex : edge devices ARM). Cela réduit la surface d’attaque et simplifie la conformité aux exigences de software bill of materials (SBOM).

4️⃣ Architecture typique d’une chaîne de traitement d’image IA en Rust

4.1 Étapes détaillées

ÉtapeRôleImplémentation Rust typique
CaptureAcquisition depuis caméra, fichier ou broker (Kafka)Crate opencv ou gstreamer-rs pour le flux vidéo
Pré‑traitementRedimensionnement, conversion en tenseur, normalisation des canauximage + ndarray : Array3::<f32>::from_shape_vec((c,h,w), data)?
Session ONNXChargement du modèle et sélection de l’EP (CPU/CUDA)let session = SessionBuilder::new(&env)?.with_model_from_file("model.onnx")?.with_cuda()?;
InferenceExécution du graphe, récupération des sortieslet outputs = session.run(vec![input_tensor])?;
Post‑traitementDécodage de masques, application d’un seuil, génération d’annotationsOpérations sur ndarray ou appel à opencv::imgproc
Persist/ExportEnregistrement au format PNG/JPEG, mise à jour de base de données, réponse HTTPCrate rocket ou actix-web pour l’API RESTful

4.2 Gestion du parallélisme

  • Thread‑per‑request : chaque requête crée une session clonée (ONNX Runtime utilise le session cache).
  • Batching : accumulation de N images avant appel run, améliore l’utilisation du GPU (généralement 4–8× plus rapide que un traitement séquentiel).

Rust offre la bibliothèque tokio pour gérer les tâches asynchrones et le pool de threads dédié aux opérations CPU‑intensives.

5️⃣ Mise en œuvre concrète : exemple de code complet

Objectif : Service HTTP qui reçoit une image JPEG, applique un modèle ONNX de super‑résolution (ESRGAN) et renvoie l’image upscalée. Le service s’appuie sur le CUDA Execution Provider lorsqu’un GPU est disponible, sinon il bascule sur le CPU.
// Cargo.toml (extraits)
// -------------------------------------------------
[dependencies]
onnxruntime = { version = "0.15", features = ["cuda"] }
tokio = { version = "1", features = ["full"] }
warp = "0.3"
image = "0.24"
ndarray = "0.15"
anyhow = "1.0"
// -------------------------------------------------

use onnxruntime::{environment::Environment, session::SessionBuilder, GraphOptimizationLevel};
use warp::Filter;
use image::io::Reader as ImageReader;
use ndarray::Array4;
use std::sync::Arc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialisation de l’environnement ONNX Runtime
    let env = Arc::new(Environment::builder()
        .with_name("rust-onnx")
        .build()?);

// Création d’une session (CUDA si présent)
    let mut sess_builder = SessionBuilder::new(&env)?
        .with_optimization_level(GraphOptimizationLevel::Basic)?;
    #[cfg(feature = "cuda")]
    {
        sess_builder = sess_builder.with_cuda(0)?; // GPU 0
    }
    let session = Arc::new(sess_builder.with_model_from_file("esrgan.onnx")?);

// Définition du filtre HTTP (Warp)
    let infer_route = warp::post()
        .and(warp::path("super_res"))
        .and(warp::multipart::form().max_length(5_000_000))
        .and(with_session(session.clone()))
        .and_then(handle_request);

println!("🚀 Service démarré sur http://0.0.0.0:3030");
    warp::serve(infer_route).run(([0, 0, 0, 0], 3030)).await;
    Ok(())
}

// Passer la session dans les handlers
fn with_session(
    sess: Arc<onnxruntime::session::Session>,
) -> impl Filter<Extract = (Arc<onnxruntime::session::Session>,), Error = std::convert::Infallible> + Clone {
    warp::any().map(move || sess.clone())
}

// Handler principal
async fn handle_request(
    form: warp::multipart::FormData,
    session: Arc<onnxruntime::session::Session>,
) -> Result<impl warp::Reply, warp::Rejection> {
    // Extraction du fichier image depuis le multipart
    let parts: Vec<_> = form.try_collect().await.map_err(|e| warp::reject::custom(e))?;
    let mut img_bytes = Vec::new();
    for part in parts {
        if part.name() == "image" {
            let data = part.stream()
                .try_fold(Vec::new(), |mut acc, bytes| async move {
                    acc.extend_from_slice(&bytes);
                    Ok(acc)
                })
                .await
                .map_err(|e| warp::reject::custom(e))?;
            img_bytes = data;
        }
    }

// Décodage JPEG → ndarray (NCHW, f32, [1,3,H,W])
    let dyn_img = ImageReader::new(std::io::Cursor::new(&img_bytes))
        .with_guessed_format()?
        .decode()?;
    let rgb = dyn_img.to_rgb8();
    let (w, h) = rgb.dimensions();
    let tensor: Array4<f32> = ndarray::Array::from_shape_fn((1, 3, h as usize, w as usize), |(_, c, y, x)| {
        rgb[(x as u32, y as u32)][c] as f32 / 255.0
    });

// Exécution du modèle
    let input_tensor = onnxruntime::tensor::OrtOwnedTensor::from_array(tensor);
    let outputs = session.run(vec![input_tensor])?;
    let output: onnxruntime::tensor::OrtOwnedTensor<f32, _> = outputs[0].try_extract()?;

// Reconstruction de l’image upscalée
    let (out_c, out_h, out_w) = (output.dim().1, output.dim().2, output.dim().3);
    let mut buf = Vec::with_capacity((out_h * out_w * out_c) as usize);
    for y in 0..out_h {
        for x in 0..out_w {
            for c in 0..out_c {
                let v = (output[[0, c, y, x]] * 255.0).clamp(0.0, 255.0) as u8;
                buf.push(v);
            }
        }
    }
    let upscaled = image::ImageBuffer::<image::Rgb<u8>, _>::from_raw(
        out_w as u32,
        out_h as u32,
        buf,
    )
    .ok_or_else(|| warp::reject::custom("Failed to build output image"))?;

// 8️⃣ Encodage JPEG et réponse HTTP
    let mut out_buf = Vec::new();
    upscaled.write_to(&mut std::io::Cursor::new(&mut out_buf), image::ImageOutputFormat::Jpeg(90))?;
    Ok(warp::reply::with_header(out_buf, "Content-Type", "image/jpeg"))
}

Analyse du code :

  • Utilisation de Arc pour partager la session entre les requêtes sans re‑chargement.
  • Sélection dynamique de l’EP : le flag Cargo cuda active le provider si le GPU est présent, sinon le runtime bascule automatiquement sur le CPU.
  • Conversion zéro‑copy grâce à ndarray et aux tensors propriétaires d’ONNX Runtime.
  • Gestion asynchrone via Tokio et Warp, garantissant un débit élevé même sous forte concurrence.

6️⃣ Optimisation des performances

AxeTechniqueEffet attendu
QuantisationDynamicQuantization ou QDQ (quantize‑dequantize) lors de la conversion du modèleRéduction de la bande passante mémoire : 4× moins d’accès, perte de précision < 1 % sur les tâches de classification.
Fusion d’opérateursActivation fusionnée (Conv + Relu) via le graphe optimizerDiminution du nombre d’appels kernel, amélioration de la latence de ~ 15 %.
Batching adaptatifRegroupement dynamique (batch size 1–8) selon la charge CPU/GPUMeilleure utilisation du GPU, débit ↑ jusqu’à 3× pour les modèles CNN.
Pinning de mémoireAllocation d’une zone « pinned » pour le transfert H2D/D2HRéduction du temps de copie de 30–40 % sur les cartes NVIDIA.
Thread‑pool dédiérayon ou tokio::task::spawn_blocking pour les pré‑traitements CPU‑intensifsÉquilibrage des cœurs, évite la contention avec le thread d’inférence.

| **Compilation de kernels personnalisés

Poursuivre le parcours : Gouverner une IA souveraineSolutionProduitDemander une démonstration

Retour au blog

StratoSentry - 125 boulevard Saint-Denis, 92400 Courbevoie, France - contact@stratosentry.com