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
| Enjeu | Description | Impact 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é algorithmique | Modèles de deep learning (CNN, Transformers) très gourmands en calcul | Né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érationnel | Facturation à la consommation du cloud GPU, licences logicielles | Recherche 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
| Atout | Pourquoi c’est pertinent |
|---|---|
| Interopérabilité | Un même modèle ONNX peut être réutilisé sur des serveurs CPU, GPU ou FPGA sans recompilation. |
| Performance | Optimisations de graphe et exécution multi‑threadée native, souvent 2–3× plus rapide que l’exécution pure Python. |
| Footprint réduit | Runtime 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
| Étape | Rôle | Implémentation Rust typique |
|---|---|---|
| Capture | Acquisition depuis caméra, fichier ou broker (Kafka) | Crate opencv ou gstreamer-rs pour le flux vidéo |
| Pré‑traitement | Redimensionnement, conversion en tenseur, normalisation des canaux | image + ndarray : Array3::<f32>::from_shape_vec((c,h,w), data)? |
| Session ONNX | Chargement du modèle et sélection de l’EP (CPU/CUDA) | let session = SessionBuilder::new(&env)?.with_model_from_file("model.onnx")?.with_cuda()?; |
| Inference | Exécution du graphe, récupération des sorties | let outputs = session.run(vec![input_tensor])?; |
| Post‑traitement | Décodage de masques, application d’un seuil, génération d’annotations | Opérations sur ndarray ou appel à opencv::imgproc |
| Persist/Export | Enregistrement au format PNG/JPEG, mise à jour de base de données, réponse HTTP | Crate 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
| Axe | Technique | Effet attendu |
|---|---|---|
| Quantisation | DynamicQuantization ou QDQ (quantize‑dequantize) lors de la conversion du modèle | Ré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érateurs | Activation fusionnée (Conv + Relu) via le graphe optimizer | Diminution du nombre d’appels kernel, amélioration de la latence de ~ 15 %. |
| Batching adaptatif | Regroupement dynamique (batch size 1–8) selon la charge CPU/GPU | Meilleure utilisation du GPU, débit ↑ jusqu’à 3× pour les modèles CNN. |
| Pinning de mémoire | Allocation d’une zone « pinned » pour le transfert H2D/D2H | Ré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 souveraine → Solution → Produit → Demander une démonstration