RAG sur images : Vision models et recherche visuelle
Guide complet pour intégrer des images dans votre système RAG : modèles de vision, embeddings multimodaux, indexation et recherche visuelle avec GPT-4V, Claude Vision et CLIP.
RAG sur images : Vision models et recherche visuelle
Les systèmes RAG traditionnels se limitent au texte. Pourtant, une grande partie de l'information d'entreprise est visuelle : photos produits, captures d'écran, graphiques, schémas techniques. L'Image RAG permet d'indexer et de rechercher dans ces contenus visuels avec la même précision qu'un RAG textuel.
Pourquoi intégrer les images au RAG ?
Les données visuelles en entreprise
- E-commerce : 70% des décisions d'achat sont influencées par les images produits
- Support technique : Les captures d'écran accélèrent de 60% la résolution des tickets
- Documentation : Un schéma vaut souvent mieux qu'une page de texte
- Conformité : Photos de chantier, états des lieux, preuves visuelles
Cas d'usage concrets
| Secteur | Usage | Exemple de requête |
|---|---|---|
| E-commerce | Recherche visuelle | "Trouve des robes similaires à cette photo" |
| Immobilier | Analyse de biens | "Montre-moi des cuisines équipées modernes" |
| Support IT | Diagnostic | "Quel est ce message d'erreur ?" |
| Manufacturing | Contrôle qualité | "Cette pièce présente-t-elle un défaut ?" |
Architecture d'un Image RAG
Vue d'ensemble
┌─────────────────────────────────────────────────────────────┐
│ IMAGE RAG PIPELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Image │───▶│ Vision │───▶│ Embedding │ │
│ │ Input │ │ Model │ │ (CLIP/SigLIP) │ │
│ └──────────┘ └──────────────┘ └──────────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────┐ ┌──────────────────┐ │
│ │ │ Description │ │ Vector Store │ │
│ │ │ textuelle │ │ (Qdrant/Pine) │ │
│ │ └──────────────┘ └──────────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌─────────────────────────────────┐ │
│ └────────▶│ Retrieval multimodal │ │
│ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Génération (VLM/LLM) │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Les deux approches
1. Description + RAG textuel
- Le vision model décrit l'image en texte
- Le texte est indexé classiquement
- Plus simple mais perd des informations visuelles
2. Embeddings multimodaux natifs
- L'image est convertie directement en vecteur
- Préserve l'information visuelle complète
- Permet la recherche image-vers-image
Modèles de vision pour le RAG
Modèles propriétaires
| Modèle | Résolution max | Coût | Forces |
|---|---|---|---|
| GPT-4V | 2048x2048 | $0.00765/image (low) | Raisonnement complexe, OCR excellent |
| Claude 3.5 Sonnet Vision | 8192x8192 | $0.003/image | Analyse détaillée, sécurité |
| Gemini 1.5 Pro | Illimité | $0.001315/image | Multi-images, contexte long |
Modèles open source
| Modèle | Params | VRAM | Usage |
|---|---|---|---|
| LLaVA 1.6 | 34B | 24GB | Description générale |
| CogVLM2 | 19B | 16GB | Compréhension fine |
| InternVL2 | 76B | 48GB | Performance SOTA |
| Qwen-VL-Max | 72B | 48GB | Multilingue |
Modèles d'embeddings multimodaux
| Modèle | Dimension | Langues | Open source |
|---|---|---|---|
| CLIP (OpenAI) | 512/768 | EN principalement | Oui |
| SigLIP | 384-1152 | Multilingue | Oui |
| Jina CLIP v2 | 1024 | 89 langues | Oui |
| Cohere Embed v3 | 1024 | 100+ langues | Non |
Implémentation pratique
Étape 1 : Extraction et description des images
DEVELOPERpythonimport base64 from openai import OpenAI def describe_image_for_rag(image_path: str, context: str = "") -> dict: """ Génère une description RAG-optimisée d'une image. """ client = OpenAI() # Encoder l'image en base64 with open(image_path, "rb") as f: image_data = base64.b64encode(f.read()).decode("utf-8") # Déterminer le type MIME mime_type = "image/jpeg" if image_path.endswith((".jpg", ".jpeg")) else "image/png" prompt = """Analyse cette image pour un système RAG. Fournis: 1. **Description générale** (2-3 phrases) 2. **Éléments clés** (liste à puces des objets/concepts importants) 3. **Texte visible** (tout texte lisible dans l'image) 4. **Métadonnées suggérées** (catégorie, tags pertinents) Sois exhaustif mais concis. L'objectif est de permettre la recherche textuelle sur cette image.""" if context: prompt += f"\n\nContexte additionnel: {context}" response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": f"data:{mime_type};base64,{image_data}", "detail": "high" } } ] } ], max_tokens=1000 ) description = response.choices[0].message.content return { "image_path": image_path, "description": description, "model": "gpt-4o", "tokens_used": response.usage.total_tokens }
Étape 2 : Embeddings multimodaux avec CLIP
DEVELOPERpythonimport torch from PIL import Image from transformers import CLIPProcessor, CLIPModel class MultimodalEmbedder: def __init__(self, model_name: str = "openai/clip-vit-large-patch14"): self.device = "cuda" if torch.cuda.is_available() else "cpu" self.model = CLIPModel.from_pretrained(model_name).to(self.device) self.processor = CLIPProcessor.from_pretrained(model_name) def embed_image(self, image_path: str) -> list[float]: """Génère un embedding pour une image.""" image = Image.open(image_path).convert("RGB") inputs = self.processor(images=image, return_tensors="pt").to(self.device) with torch.no_grad(): embedding = self.model.get_image_features(**inputs) # Normaliser pour la similarité cosinus embedding = embedding / embedding.norm(dim=-1, keepdim=True) return embedding.cpu().squeeze().tolist() def embed_text(self, text: str) -> list[float]: """Génère un embedding pour du texte (même espace que les images).""" inputs = self.processor(text=[text], return_tensors="pt", padding=True).to(self.device) with torch.no_grad(): embedding = self.model.get_text_features(**inputs) embedding = embedding / embedding.norm(dim=-1, keepdim=True) return embedding.cpu().squeeze().tolist() def compute_similarity(self, image_path: str, text: str) -> float: """Calcule la similarité image-texte.""" img_emb = torch.tensor(self.embed_image(image_path)) txt_emb = torch.tensor(self.embed_text(text)) return (img_emb @ txt_emb).item()
Étape 3 : Indexation dans Qdrant
DEVELOPERpythonfrom qdrant_client import QdrantClient from qdrant_client.models import ( VectorParams, Distance, PointStruct, Filter, FieldCondition, MatchValue ) class ImageRAGIndex: def __init__(self, collection_name: str = "image_rag"): self.client = QdrantClient(url="http://localhost:6333") self.collection_name = collection_name self.embedder = MultimodalEmbedder() def create_collection(self, vector_size: int = 768): """Crée la collection avec deux espaces vectoriels.""" self.client.recreate_collection( collection_name=self.collection_name, vectors_config={ # Embedding visuel (CLIP) "visual": VectorParams( size=vector_size, distance=Distance.COSINE ), # Embedding textuel (description) "textual": VectorParams( size=1536, # Ada-002 ou similaire distance=Distance.COSINE ) } ) def index_image( self, image_id: str, image_path: str, description: str, text_embedding: list[float], metadata: dict = None ): """Indexe une image avec ses deux embeddings.""" visual_embedding = self.embedder.embed_image(image_path) point = PointStruct( id=hash(image_id) % (2**63), vector={ "visual": visual_embedding, "textual": text_embedding }, payload={ "image_id": image_id, "image_path": image_path, "description": description, **(metadata or {}) } ) self.client.upsert( collection_name=self.collection_name, points=[point] ) def search_by_text(self, query: str, limit: int = 5) -> list[dict]: """Recherche par requête textuelle.""" query_embedding = self.embedder.embed_text(query) results = self.client.search( collection_name=self.collection_name, query_vector=("visual", query_embedding), # CLIP text -> visual limit=limit ) return [ { "image_path": r.payload["image_path"], "description": r.payload["description"], "score": r.score } for r in results ] def search_by_image(self, image_path: str, limit: int = 5) -> list[dict]: """Recherche d'images similaires.""" query_embedding = self.embedder.embed_image(image_path) results = self.client.search( collection_name=self.collection_name, query_vector=("visual", query_embedding), limit=limit ) return [ { "image_path": r.payload["image_path"], "description": r.payload["description"], "score": r.score } for r in results ]
Étape 4 : Génération avec contexte visuel
DEVELOPERpythondef generate_with_images( query: str, retrieved_images: list[dict], client: OpenAI ) -> str: """ Génère une réponse en utilisant les images récupérées comme contexte. """ # Préparer le contenu multimodal content = [ { "type": "text", "text": f"""Tu es un assistant qui répond aux questions en utilisant les images fournies comme source d'information. Question de l'utilisateur: {query} Images disponibles:""" } ] # Ajouter chaque image avec sa description for i, img in enumerate(retrieved_images[:3], 1): # Max 3 images with open(img["image_path"], "rb") as f: img_data = base64.b64encode(f.read()).decode("utf-8") content.append({ "type": "text", "text": f"\n**Image {i}** (score: {img['score']:.2f}):\n{img['description']}" }) content.append({ "type": "image_url", "image_url": { "url": f"data:image/jpeg;base64,{img_data}", "detail": "low" # Économiser les tokens } }) content.append({ "type": "text", "text": "\n\nRéponds à la question en te basant uniquement sur ces images. Si les images ne permettent pas de répondre, dis-le clairement." }) response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": content}], max_tokens=1000 ) return response.choices[0].message.content
Optimisations avancées
Chunking d'images haute résolution
Pour les grandes images (plans, schémas), découpez en tuiles :
DEVELOPERpythonfrom PIL import Image def tile_large_image(image_path: str, tile_size: int = 512, overlap: int = 64): """Découpe une grande image en tuiles avec chevauchement.""" img = Image.open(image_path) width, height = img.size tiles = [] for y in range(0, height - overlap, tile_size - overlap): for x in range(0, width - overlap, tile_size - overlap): box = (x, y, min(x + tile_size, width), min(y + tile_size, height)) tile = img.crop(box) tiles.append({ "tile": tile, "position": (x, y), "original_size": (width, height) }) return tiles
Hybrid search image + texte
DEVELOPERpythondef hybrid_image_search( query: str, text_embedding: list[float], index: ImageRAGIndex, alpha: float = 0.7 # Poids du visuel vs textuel ) -> list[dict]: """Combine recherche visuelle et textuelle.""" # Recherche visuelle (CLIP) visual_results = index.search_by_text(query, limit=20) # Recherche textuelle (sur les descriptions) text_results = index.client.search( collection_name=index.collection_name, query_vector=("textual", text_embedding), limit=20 ) # Fusion des scores avec RRF combined_scores = {} for rank, r in enumerate(visual_results): img_id = r["image_path"] combined_scores[img_id] = combined_scores.get(img_id, 0) + alpha / (rank + 60) for rank, r in enumerate(text_results): img_id = r.payload["image_path"] combined_scores[img_id] = combined_scores.get(img_id, 0) + (1 - alpha) / (rank + 60) # Trier par score combiné sorted_results = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True) return [{"image_path": path, "score": score} for path, score in sorted_results[:5]]
Benchmarks et coûts
Performance de retrieval
| Méthode | Precision@5 | Recall@10 | Latence |
|---|---|---|---|
| Description seule | 0.72 | 0.81 | 50ms |
| CLIP seul | 0.78 | 0.85 | 30ms |
| Hybride | 0.84 | 0.91 | 80ms |
Coûts par image indexée
| Étape | Coût estimé | Notes |
|---|---|---|
| Description GPT-4V | $0.01-0.03 | Selon taille et détail |
| Embedding CLIP | $0 (local) | GPU recommandé |
| Stockage Qdrant | ~$0.0001 | Par vecteur/mois |
Comparaison des modèles d'embedding
| Modèle | Zero-shot accuracy | Multilingue | Vitesse |
|---|---|---|---|
| CLIP ViT-L/14 | 75.5% | Non | Rapide |
| SigLIP So400m | 83.1% | Oui | Moyen |
| Jina CLIP v2 | 81.2% | Oui | Rapide |
Pièges et solutions
Problème 1 : Images avec peu de contenu visuel
Symptôme : Les captures d'écran de texte sont mal indexées par CLIP.
Solution : OCR explicite + indexation textuelle.
DEVELOPERpythonimport pytesseract def extract_text_from_image(image_path: str) -> str: """Extrait le texte d'une image via OCR.""" img = Image.open(image_path) text = pytesseract.image_to_string(img, lang='fra+eng') return text.strip()
Problème 2 : Doublons visuels
Symptôme : Plusieurs images quasi-identiques polluent les résultats.
Solution : Déduplication par similarité.
DEVELOPERpythondef deduplicate_images(embeddings: list, threshold: float = 0.95): """Supprime les images trop similaires.""" keep = [] for i, emb in enumerate(embeddings): is_duplicate = False for j in keep: similarity = cosine_similarity(emb, embeddings[j]) if similarity > threshold: is_duplicate = True break if not is_duplicate: keep.append(i) return keep
Problème 3 : Contexte visuel vs textuel contradictoire
Symptôme : La description générée contredit l'image.
Solution : Validation croisée et score de confiance.
Intégration avec Ailog
Ailog supporte nativement l'indexation d'images dans vos knowledge bases :
- Upload : Glissez-déposez vos images dans l'interface
- Analyse automatique : Vision model pour extraction de contenu
- Indexation hybride : Embeddings visuels + textuels
- Recherche unifiée : Une seule requête pour texte et images
Essayez l'Image RAG sur Ailog - Aucune configuration requise.
FAQ
Guides connexes
Tags
Articles connexes
RAG Multimodal : Images, PDFs et au-delà du texte
Étendez votre RAG au-delà du texte : indexation d'images, extraction de PDFs, tableaux et graphiques pour un assistant vraiment complet.
Fondamentaux du Retrieval : Comment fonctionne la recherche RAG
Maîtrisez les bases du retrieval dans les systèmes RAG : embeddings, recherche vectorielle, chunking et indexation pour des résultats pertinents.
Magento : Assistant catalogue intelligent
Deployer un assistant IA sur Magento pour naviguer dans les catalogues complexes, recommander des produits et ameliorer l'experience B2B et B2C.