OCR constrémenté Fuzzy - Les trois -Tier OCR Pipeline
Partie 4: Intelligence de l'image Introduit l’architecture des ondes ImageSummarizer et les modèles plus généraux. Cet article se plonge dans la profondeur Sous-système OCR—trois niveaux d'extraction de texteM SK1 roulement intelligent, et l'optimisation des pistes de films qui permet une réduction du symbole pour les GIF animés 30×
Pourquoi un article distinct? Le pipeline OCR s’est évolué de "Tesseract avec Vision LLM fallback" à un système à trois niveaux sophistiquéM SK2avec MLMSC3 basé sur le OCR , multiMNK5vote encadré MNK6 texte MMK7extraction uniquement des bandes MBK8 et coûtsMMK9routage conscientMRK10 Il est suffisamment complexe pour justifier sa propre décomposition détailléeMEK12
Articles connexes:
- Partie 1: Modèle de fuzziness restreint - Le modèle fondamental
- Partie 2: MoM Fuzzy restreint - Coordination de modèles multiples
- Partie 3: Traçage du contexte - Mémoire et temps
- Partie 4: Intelligence de l'image - Aperçu complet de l'architecture des ondes
- DocSummarizer - Analyse de documents avec des patrons similaires
- Système de gestion des données - Profilage des données avec le déterminisme - première approche
Le problème: L'extraction du texte est difficile
OCR sur les images réelles-les images du monde échouent de façon prévisibleM SK1
- Fonts stylisés: Tesseract formé sur des caractères standardsM SK1 ne fonctionne pas sur le texte décoratif
- Noisy GIFs: Artifacts de compression des cadresM SK1 jitter, changements de sous-titres
- Faible contraste: Texte foncé sur fond foncé
- Texte rotatif: NonM SK1angles de texte horizontaux
- Contenu mixte: Screenshots avec plusieurs régions de texte
- Coûts de l'API: Les appels de vision LLM sont coûteux ($0.001-0.01 par image
Approche traditionnelle: M SK1Run Tesseract, s’il ne peut pas utiliser Vision LLM "
Problème: Il faut soit manquer de texte stylisé (Tesseract échoue) ou coûter trop cher ( toujours utiliser Vision LLMM SK4
Solution: Ajouter un niveau intermédiaire (Florence-2 ONNXM SK3 qui gère les caractères stylisés localementMSC4 escalant à Vision LLM seulement lorsque les deux méthodes locales échouent
L’architecture des trois -Tier OCR
Le système gère les ondes dans l'ordre prioritaire (numéro supérieur =exécution ultérieure):
Wave Priority Order:
40: TextLikelinessWave → Heuristic text detection
50: OcrWave → Tesseract OCR (if text-likely)
51: MlOcrWave → Florence-2 ML OCR (if Tesseract low confidence)
55: Florence2Wave → Florence-2 captions (optional)
80: VisionLlmWave → Vision LLM (escalation)
Niveau 1: Tesseract (Traditional OCRM SK2
| Priorité | Accroître la vitesse MSC2 Coût MSC3 Le meilleur pour SMC4 Limites SMC5 | ||
|---|---|---|---|
| 50 | ~50ms | gratuit SMK3 texte propre, contraste élevéMS K5 caractères standard | caractèrees stylisées , qualité médiocreMSC8 texte rotatif MESK9 |
Signales émises:
ocr.text- Texte extraitocr.confidence- Note de confiance moyenne Tesseract
Niveau 2: FlorenceM SK1 ONNX (ML OCR)
| Priorité | Accroître la vitesse MSC2 Coût MSC3 Le meilleur pour SMC4 Limites SMC5 |
|---|---|
| 51 |
Signales émises:
ocr.ml.text- Single -frame FlorenceM SK2 OCRocr.ml.multiframe_text- MultiM SK1texte GIF encadré (préféré pour les animations)ocr.ml.confidence- Cote de confiance du modèle
Niveau 3: Vision LLM (Cloud Fallback)
| Priorité | Accroître la vitesse | Coût | Le meilleur pour SMK4 Constraints SMK5 | |||
|---|---|---|---|---|---|---|
| 80 | ~1-5s | $0.001-0.01 | Toute chose \ , surtout des scènes complexes \ | Il faut respecter les signaux déterministes | |
Signales émises:
ocr.vision.text- Extraction du texte de Vision LLM OCRocr.vision.confidence- Confiance en LLM (typiquement 0.95)caption.text- Caption descriptive facultative séparée du OCR
The ONNX Arsenal: Local ML Models
Avant de plonger dans les trois niveaux OCR, il faut laisser le Modèles ML déterministes qui alimentent le système. Tous les modèles fonctionnent localement par l’entremise de l’ONNX Runtime—pas d’appels API , pas de dépendance à la nuage
Pourquoi ONNX?
- Fonctionne localement: Aucune clé API, aucune latence de réseauM SK2 aucun coût renouvelable
- Déterministe: Les mêmes entrées = les mêmes sorties ≥(pas d'échantillonnageM SK3randomité de température)*
- Soutien rapide:HardwareM SK1accélérée (CPU/GPU≥),inférence optimisée
- Portable: fonctionne sur Windows, LinuxM SK2 macOS
- Auto-achevé: Démarrer le téléchargement des modèles automatiquement
* Avertissement mineur: Les fournisseurs d'exécution GPU peuvent introduire un float négligeable-un non-déterminisme de pointeM SK2 Le contrat de signalisation (les seuils de confiance
Les cinq modèles ONNX
Remarque: Les tailles sont approximatives et varient selon la variante/quantisationM SK2 Les versions de téléchargement typiques présentées ci-dessous
| Modèle | approximativement MSC2 Taille MSC3 But SMC4 vitesse MMC5 Type de modèle MESC6 | ||
|---|---|---|---|
| OUEST | ~100MB SMK2 Détection du texte en scène | ≥~20ms | Détermination du texte SKM6 |
| CRAFT | ~150MB SMK2 Caractéristique-détection du texte dans la région | ||
| Florence-2 | ~250MB SMK2 OCR + sous-titrage | ||
| Real-ESRGAN | ~60MB | ||
| CLIP | ~600MB SMK2 Emplacements sémantiques | ≥~100ms MMK5 Em placement multimodal PMK6 |
Total de l'espace de disque: ~1.0-1.5GB selon les variantes de modèle choisiesM SK2
1. EAST: Détection du texte de la scène
Détecteur de texte de scène efficace et exact - trouve des régions de texte dans les paysages naturels
// EAST detects text bounding boxes with confidence scores
var result = await textDetector.RunEastDetectionAsync(imagePath);
// Output: List of BoundingBox with coordinates + confidence
// Example: [BoundingBox(x1:50, y1:100, x2:300, y2:150, confidence:0.92)]
Comment cela fonctionne:
- Modèle d'apprentissage approfondi formé sur des ensembles de données de texte en scène
- Carte des résultats (confidenceM SK1 + carte géométrique (coordonnées de la caseMSC4
- Dispositifs de texte rotatif, multi-texte à grande échelle
- Utilise Non -Maximum Suppression (NMS) pour fusionner les boîtes chevauchantes
Pourquoi le determinisme?
- Aucune randomité dans l'inférence (poids congelésM SK1
- La même image → les mêmes boîtes bouillantes
- Les notes de confiance sont reproduites.
- Les seuils d'évacuation sont config-driven (eM SK2g.,
< 0.5 → escalate)
Détails techniques:
// EAST preprocessing (from implementation)
- Input size: 320×320 (must be multiple of 32)
- Format: BGR with mean subtraction [123.68, 116.78, 103.94]
- Output stride: 4 (downsampled 4×)
- Score threshold: 0.5
- NMS IoU threshold: 0.4
Exemple de sortie:
Input: meme.png (800×600)
EAST detection: 15 text regions found
Region 1: (50, 480, 750, 580) - confidence 0.87 [bottom subtitle area]
Region 2: (100, 50, 300, 90) - confidence 0.62 [top text]
Region 3: ...
Route decision: ANIMATED (subtitle pattern in bottom 30%)
2. CRAFTM SK1 Caractéristique Sensibilisation de la région
Character-détection de texte à niveau - excelle dans les textes courbesM SK1 artistiques, et stylisés .
// CRAFT finds character-level regions, then groups into words
var result = await textDetector.RunCraftDetectionAsync(imagePath);
// Better than EAST for: decorative fonts, curved text, logos
Comment cela fonctionne:
- Détecte les régions individuelles de caractères (plus granulaires que l'ESTM SK1
- Utilise la note d'affinité pour regrouper les caractères en mots
- Flood-fill algorithme trouve des composants de texte connectés
- Applique un texte courbé que l'Est manque
Lorsque CRAFT est utilisé:
- EAST n'est pas disponible ou a échoué
- L'image a des caractères artistiques/decoratifs (autoM SK2détecté)
- Utilisateur sélectionne explicitement le détecteur CRAFT
Détails techniques:
// CRAFT preprocessing
- Max dimension: 1280px (maintains aspect ratio)
- Format: RGB normalized with ImageNet stats
- Mean: [0.485, 0.456, 0.406]
- Std: [0.229, 0.224, 0.225]
- Output stride: 2 (downsampled 2×)
- Threshold: 0.4 for character regions
ÉAST vs CRAFT comparativement:
| Caractéristiques | ÉAST | CRAFT S |
|---|---|---|
| Niveau de détection | MotM SK2ligne | Character S |
| Accroître la vitesse | MSC2ms MSC3 ≥MSC4ms | |
| Suivant le mieux pour | Texte standard, sous-titres MSC3 Fonts décoratifsMSC4 logos SMC5 | |
| Texte courbé | Limité MSC2 Excellent MSC3 | |
| Taille du modèle : |
3. RéelM SK1ESRGAN: Super -Upscaling de la résolution
Améliore les images de qualité inférieure-avant l'OCR -
// Upscale low-quality image before running OCR
if (quality.Sharpness < 30) // Laplacian variance threshold
{
var upscaled = await esrganService.UpscaleAsync(imagePath, scale: 4);
// Now run OCR on the enhanced image
}
Quand il est utilisé:
- Accuracité de l'image < 30 ≥(Variance laplacianeM SK3
- Régions de texte décelées mais très petites (< 20 hauteur pxM SK2
- La confiance en OCR est faible, mais les régions de texte sont présentes
- Utilisateur demande explicitement l'agrandissement
Exemple ::
Input: 100×75 screenshot with tiny text
Laplacian variance: 18 (very blurry)
ESRGAN: Upscale to 400×300 (~500ms)
New Laplacian variance: 87 (sharp)
OCR: Tesseract confidence: 0.92 (vs 0.42 before upscaling)
Text: "Click here to continue" (vs garbled before)
Détails techniques:
// Real-ESRGAN processing
- Input: Any size (processed in 128×128 tiles if large)
- Output: 4× scaled (200×150 → 800×600)
- Model: x4plus variant (general photos)
- Processing: ~500ms for 800×600 image
- Memory: ~2GB peak (tiles reduce this)
Économie des symboles:
Scenario: Screenshot with tiny text
Option 1: Send low-res to Vision LLM
Image: 100×75 = ~20 tokens
LLM can't read tiny text → fails
Cost: $0.0002 (wasted)
Option 2: Upscale with ESRGAN, use Tesseract
ESRGAN: Free (local), 500ms
Tesseract: Free (local), 50ms
Success: 92% confidence
Cost: $0
Result: ESRGAN + local OCR beats Vision LLM for low-res images
4. CLIP: Embeddings sémantiques
Emplacements multimodaux pour la recherche sémantique d'images - projeter des images et du texte dans un espace vectoriel partagé
// Generate embedding for semantic search
var embedding = await clipService.GenerateEmbeddingAsync(imagePath);
// Returns: float[512] vector
// Later: semantic search across thousands of images
var similarImages = await vectorDb.SearchAsync(queryEmbedding, topK: 10);
Comment cela fonctionne:
- CLIP ViT-BM SK1 encoder visuel (350MB )
- Projecter les images à des vecteurs 512-dimensionnels
- Formé à s'aligner sur les descriptions de texte
- Enable "Trouver des images comme celle-ci" sans mots clés
Utiliser des cas:
- Recherche sémantique d'images dans les systèmes RAG
- Détection du duplicat (même si elle a été modifiéeM SK1cropped)
- Contexte-groupement fondé
- Recommandations d'image similaires
Détails techniques:
// CLIP visual encoder
- Model: ViT-B/32 (Vision Transformer)
- Input: 224×224 RGB (center crop + resize)
- Output: 512-dimensional embedding
- Normalized: L2 norm = 1.0
- Speed: ~100ms per image
Exemple ::
Input images:
cat_on_couch.jpg → [0.23, -0.51, 0.88, ...]
dog_on_couch.jpg → [0.19, -0.48, 0.91, ...]
car_photo.jpg → [-0.67, 0.33, -0.12, ...]
Query: "animals on furniture"
Text embedding → [0.21, -0.50, 0.89, ...]
Cosine similarity:
cat_on_couch: 0.94 (very similar!)
dog_on_couch: 0.91 (similar)
car_photo: 0.12 (not similar)
Result: Returns cat and dog images
Modèle de langage (Covered in Tier 2)
Voir la section Niveau 2 pour obtenir des détails détaillés sur FlorenceM SK1 ONNX OCR et les sous-titrages.
Auto-Système de téléchargement
Tous les modèles sont téléchargés automatiquement à la première utilisation:
$ imagesummarizer image.png --pipeline auto
[First run]
Downloading EAST scene text detector (~100MB)...
Progress: ████████████████████ 100% (102.4 MB)
Downloading Florence-2 base model (~250MB)...
Progress: ████████████████████ 100% (248.7 MB)
Downloading CLIP ViT-B/32 visual (~350MB)...
Progress: ████████████████████ 100% (347.2 MB)
Models saved to: ~/.mostlylucid/models/
Total disk space: 1.16 GB
[Subsequent runs]
All models cached, analysis starts immediately
Dégradation gracieuse:
// If ONNX model download fails, system falls back gracefully
EAST unavailable → Try CRAFT → Fall back to Tesseract PSM
Real-ESRGAN unavailable → Skip upscaling, use original image
CLIP unavailable → Skip embeddings, OCR still works
Florence-2 unavailable → Use Tesseract → Vision LLM escalation
Chaque défaillance du modèle ONNX est enregistrée avec le chemin de chute , pour s’assurer que le système n’éclatera jamais en raison des modèles manquants.
Pourquoi cela importe?
Note de prix: Les exemples de coût ci-dessous utilisent des prix illustrables (~$0.005/image pour Vision LLMM SK2 Les coûts réels d’API varient selon le fournisseur et le modèle. L’influence fondamentaleMSC4le traitement local élimine la plupart des appels APIMST5les retenues indépendamment du prix précisM ST6
Sans les modèles ONNX (ligne de baseM SK1
Every image → Send to Vision LLM
Cost: ~$0.005/image (example pricing)
Time: ~2s network + inference
100 images = ~$0.50, ~200s
Avec les modèles ONNX (local-premièreM SK2
85 images → EAST + Florence-2 (local)
Cost: $0
Time: ~200ms
10 images → EAST + Tesseract (local)
Cost: $0
Time: ~50ms
5 images → EAST + Vision LLM (escalation)
Cost: ~$0.025 (5 × $0.005)
Time: ~2s each
100 images = ~$0.025, ~30s total
Épargnes: ~95% réduction des coûtsM SK2 ≥~85% plus rapide, routage déterministe.
Les modèles ONNX transforment le système de "probabilistic jusqu’à la base déterministe " à "deterministic foundation
Niveau 1: Tesseract OCR
La ligne de base. FastM SK1 deterministe, fonctionne bien pour un texte propreMSC3
public class OcrWave : IAnalysisWave
{
public string Name => "OcrWave";
public int Priority => 60; // After color/identity
public async Task<IEnumerable<Signal>> AnalyzeAsync(
string imagePath,
AnalysisContext context,
CancellationToken ct)
{
var signals = new List<Signal>();
// Get preprocessed image from cache
var image = context.GetCached<Image<Rgba32>>("image");
// Run Tesseract OCR
using var engine = new TesseractEngine(@"./tessdata", "eng", EngineMode.Default);
using var page = engine.Process(image);
var text = page.GetText();
var confidence = page.GetMeanConfidence();
signals.Add(new Signal
{
Key = "ocr.text", // Tesseract OCR result
Value = text,
Confidence = confidence,
Source = Name,
Tags = new List<string> { "ocr", "text" },
Metadata = new Dictionary<string, object>
{
["engine"] = "tesseract",
["mean_confidence"] = confidence,
["word_count"] = text.Split(' ').Length
}
});
signals.Add(new Signal
{
Key = "ocr.confidence",
Value = confidence,
Confidence = 1.0,
Source = Name
});
return signals;
}
}
Signaux clés:
ocr.full_text- Le texte extraitocr.early_exit- Signal pour passer à niveau 2/3 si la confiance est élevée- Le score de confiance favorise l'escalade des décisions
Niveau 2: Florence-2 ONNX
Microsoft's FlorenceM SK1 est un modèle linguistique de vision - qui excelle en captioning dense et en OCR. La version ONNX fonctionne localement sans frais d’APIMSC4
Pourquoi Florence-2?
- Meilleure que Tesseract pour les caractères stylisés: Encadre les textes décoratifs
- Plus rapide que Vision LLM: ~200ms vs 1-5s
- Droits d'auteur: Exécute localement, Aucune clé API n'est requise
- Compréhension multimodale: Peut extraire le texte dans un contexte (e.gM SK3 boucles de paroleMSC4
Mise en œuvre
public class MlOcrWave : IAnalysisWave
{
private readonly Florence2OnnxModel _model;
public string Name => "MlOcrWave";
public int Priority => 51; // Runs AFTER Tesseract (priority 50)
public async Task<IEnumerable<Signal>> AnalyzeAsync(
string imagePath,
AnalysisContext context,
CancellationToken ct)
{
var signals = new List<Signal>();
// Check if Tesseract already succeeded with high confidence
var tesseractConfidence = context.GetValue<double>("ocr.confidence");
if (tesseractConfidence >= 0.95)
{
signals.Add(new Signal
{
Key = "ocr.ml.skipped", // Consistent namespace: ocr.ml.*
Value = true,
Confidence = 1.0,
Source = Name,
Metadata = new Dictionary<string, object>
{
["reason"] = "tesseract_high_confidence",
["tesseract_confidence"] = tesseractConfidence
}
});
return signals;
}
// Run Florence-2 OCR
var result = await _model.ExtractTextAsync(imagePath, ct);
signals.Add(new Signal
{
Key = "ocr.ml.text", // Florence-2 ML OCR text
Value = result.Text,
Confidence = result.Confidence,
Source = Name,
Tags = new List<string> { "ocr", "text", "ml" },
Metadata = new Dictionary<string, object>
{
["model"] = "florence2-base",
["inference_time_ms"] = result.InferenceTime,
["token_count"] = result.TokenCount
}
});
// For animated GIFs, extract all unique frames
if (context.GetValue<int>("identity.frame_count") > 1)
{
var frameResults = await ExtractMultiFrameTextAsync(
imagePath,
maxFrames: 10,
ct);
signals.Add(new Signal
{
Key = "ocr.ml.multiframe_text",
Value = frameResults.CombinedText,
Confidence = frameResults.AverageConfidence,
Source = Name,
Metadata = new Dictionary<string, object>
{
["frames_processed"] = frameResults.FrameCount,
["unique_text_segments"] = frameResults.UniqueSegments,
["deduplication_method"] = "levenshtein_85"
}
});
}
return signals;
}
}
Multi-Procédure GIF du cadre
Pour les GIF animés, Florence-2 procédés parallèles jusqu’à des cadres d’échantillonnage 10
private async Task<MultiFrameResult> ExtractMultiFrameTextAsync(
string imagePath,
int maxFrames,
CancellationToken ct)
{
// Load GIF and extract frames
using var image = await Image.LoadAsync<Rgba32>(imagePath, ct);
var frames = new List<Image<Rgba32>>();
int frameCount = image.Frames.Count;
int step = Math.Max(1, frameCount / maxFrames);
for (int i = 0; i < frameCount; i += step)
{
frames.Add(image.Frames.CloneFrame(i));
}
// Process all frames in parallel (bounded concurrency to avoid thrashing)
var semaphore = new SemaphoreSlim(4); // Max 4 concurrent inferences
var tasks = frames.Select(async frame =>
{
await semaphore.WaitAsync(ct);
try
{
var result = await _model.ExtractTextAsync(frame, ct);
return result;
}
finally
{
semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
semaphore.Dispose();
// Deduplicate using Levenshtein distance
var uniqueTexts = DeduplicateByLevenshtein(
results.Select(r => r.Text).ToList(),
threshold: 0.85);
return new MultiFrameResult
{
CombinedText = string.Join("\n", uniqueTexts),
FrameCount = frames.Count,
UniqueSegments = uniqueTexts.Count,
AverageConfidence = results.Average(r => r.Confidence)
};
}
private List<string> DeduplicateByLevenshtein(
List<string> texts,
double threshold)
{
var unique = new List<string>();
foreach (var text in texts)
{
bool isDuplicate = false;
foreach (var existing in unique)
{
var distance = LevenshteinDistance(text, existing);
var maxLen = Math.Max(text.Length, existing.Length);
var similarity = 1.0 - (distance / (double)maxLen);
if (similarity >= threshold)
{
isDuplicate = true;
break;
}
}
if (!isDuplicate)
{
unique.Add(text);
}
}
return unique;
}
Exemple :: 93-frame GIF MSC2 | | 10 cadres échantillonnés MSC4 | MSC5 | résultats uniques de texte
Frame 1-45: "I'm not even mad."
Frame 46-93: "That's amazing."
La décision de routage
Détection du texte de l'OpenCV (~5-20ms) détermine le chemin à suivre
public class TextDetectionService
{
public TextDetectionResult DetectText(Image<Rgba32> image)
{
// Use OpenCV EAST text detector
var (regions, confidence) = RunEastDetector(image);
return new TextDetectionResult
{
HasText = regions.Count > 0,
RegionCount = regions.Count,
Confidence = confidence,
Route = SelectRoute(regions, confidence, image)
};
}
private ProcessingRoute SelectRoute(
List<TextRegion> regions,
double confidence,
Image<Rgba32> image)
{
// No text detected
if (regions.Count == 0)
return ProcessingRoute.NoOcr;
// Animated GIF with subtitle pattern
if (image.Frames.Count > 1 && HasSubtitlePattern(regions))
return ProcessingRoute.AnimatedFilmstrip;
// High confidence, standard text
if (confidence >= 0.8 && HasStandardTextCharacteristics(regions))
return ProcessingRoute.Fast; // Florence-2 only
// Moderate confidence
if (confidence >= 0.5)
return ProcessingRoute.Balanced; // Florence-2 + Tesseract voting
// Low confidence, complex image
return ProcessingRoute.Quality; // Full pipeline + Vision LLM
}
private bool HasSubtitlePattern(List<TextRegion> regions)
{
// Subtitles are typically in bottom 30% of frame
var bottomRegions = regions.Where(r =>
r.BoundingBox.Y > r.ImageHeight * 0.7);
return bottomRegions.Count() >= regions.Count * 0.5;
}
}
Rendement des routes
| Route | Triggers quand MSC2 Traitement MSC3 Temps SMC4 Coût SMC5 | ||||
|---|---|---|---|---|---|
| FAST | Haute confiance (>0.8), texte standard MSC2 FlorenceMSC3 seulement SMC4 SMC5ms ESMC6 gratuits SSMC7 | ||||
| BALANCED | Confiance modérée | ||||
| QUALITÉ | Faible confiance MSC1 complexe MSC2 Multi SMC3 cadre SMC4 Vision LLM | SMCC6s SMCC7 | 8 | 9 | |
| ANIMATED | GIF avec un patron de sous-titre | TexteM SK2carte à bandes cinématographiques seulement |
Texte-Extraction de bandes seulement
L'optimisation des sous-titres GIF: extrait seulement les régions de texte, sans cadres completsM SK1
Le problème
Approche traditionnelle pour un GIF 93-frame avec sous-titres:
Option 1: Process every frame
93 frames × 300×185 × ~150 tokens/frame = 13,950 tokens
Cost: ~$0.14 @ $0.01/1K tokens
Time: ~27 seconds
Option 2: Sample 10 frames
10 frames × 300×185 × ~150 tokens/frame = 1,500 tokens
Cost: ~$0.015
Time: ~3 seconds
Problem: Might miss subtitle changes
La solution: Texte- Seulement les bandes
Extraire uniquement les cases de bounding du texte, supprimant les pixels arrière-plan:
2 text regions × 250×50 × ~25 tokens/region = 50 tokens
Cost: ~$0.0005
Time: ~2 seconds
Token reduction: 30×
Mise en œuvre
public class FilmstripService
{
public async Task<TextOnlyStrip> CreateTextOnlyStripAsync(
string imagePath,
CancellationToken ct)
{
using var gif = await Image.LoadAsync<Rgba32>(imagePath, ct);
// 1. Detect subtitle region (bottom 30% of frames)
var subtitleRegion = DetectSubtitleRegion(gif);
// 2. Extract frames with text changes
var uniqueFrames = ExtractUniqueTextFrames(gif, subtitleRegion);
// 3. Extract tight bounding boxes around text
var textRegions = ExtractTextBoundingBoxes(uniqueFrames);
// 4. Create horizontal strip of text-only regions
var strip = CreateHorizontalStrip(textRegions);
return new TextOnlyStrip
{
Image = strip,
RegionCount = textRegions.Count,
TotalTokens = EstimateTokens(strip),
OriginalTokens = EstimateTokens(gif),
Reduction = CalculateReduction(strip, gif)
};
}
private Rectangle DetectSubtitleRegion(Image<Rgba32> gif)
{
// Analyze bottom 30% of frame for text patterns
int subtitleHeight = (int)(gif.Height * 0.3);
int subtitleY = gif.Height - subtitleHeight;
return new Rectangle(0, subtitleY, gif.Width, subtitleHeight);
}
private List<Image<Rgba32>> ExtractUniqueTextFrames(
Image<Rgba32> gif,
Rectangle subtitleRegion)
{
var uniqueFrames = new List<Image<Rgba32>>();
Image<Rgba32>? previousFrame = null;
for (int i = 0; i < gif.Frames.Count; i++)
{
var frame = gif.Frames.CloneFrame(i);
var subtitleCrop = frame.Clone(ctx =>
ctx.Crop(subtitleRegion));
// Compare with previous frame
if (previousFrame == null ||
HasTextChanged(subtitleCrop, previousFrame, threshold: 0.05))
{
uniqueFrames.Add(subtitleCrop);
previousFrame = subtitleCrop;
}
}
return uniqueFrames;
}
private bool HasTextChanged(
Image<Rgba32> current,
Image<Rgba32> previous,
double threshold)
{
// Threshold bright pixels (white/yellow text on dark background)
var currentBright = CountBrightPixels(current);
var previousBright = CountBrightPixels(previous);
// Calculate Jaccard similarity of bright pixels
var intersection = currentBright.Intersect(previousBright).Count();
var union = currentBright.Union(previousBright).Count();
var similarity = union > 0 ? intersection / (double)union : 1.0;
// Text changed if similarity drops below threshold
return similarity < (1.0 - threshold);
}
// Helper type for bounding box + crop
private record TextCrop
{
public required Image<Rgba32> CroppedImage { get; init; }
public required Rectangle Bounds { get; init; }
}
private List<TextCrop> ExtractTextBoundingBoxes(
List<Image<Rgba32>> frames)
{
var textCrops = new List<TextCrop>();
foreach (var frame in frames)
{
// Threshold to get text mask
var mask = ThresholdBrightPixels(frame, minValue: 200);
// Find connected components (text regions)
var components = FindConnectedComponents(mask);
// Get tight bounding box around all components
var bbox = GetTightBoundingBox(components);
// Add padding
bbox.Inflate(5, 5);
// Clone the region (dispose properly in production!)
var cropped = frame.Clone(ctx => ctx.Crop(bbox));
textCrops.Add(new TextCrop
{
CroppedImage = cropped,
Bounds = bbox
});
}
return textCrops;
}
private Image<Rgba32> CreateHorizontalStrip(
List<TextCrop> textCrops)
{
// Calculate strip dimensions
int totalWidth = textCrops.Sum(c => c.Bounds.Width);
int maxHeight = textCrops.Max(c => c.Bounds.Height);
// Create blank canvas
var strip = new Image<Rgba32>(totalWidth, maxHeight);
// Paste text regions horizontally
int xOffset = 0;
foreach (var crop in textCrops)
{
strip.Mutate(ctx => ctx.DrawImage(
crop.CroppedImage,
new Point(xOffset, 0),
opacity: 1.0f));
xOffset += crop.Bounds.Width;
// Dispose crop after use (important!)
crop.CroppedImage.Dispose();
}
return strip;
}
}
Exemple visuelle
Résultats obtenus: anchorman-not-even-mad.gif (93 frames, 300×185)
Traitement:
1. Detect subtitle region: bottom 30% (300×55)
2. Extract unique frames: 93 frames → 2 text changes
3. Extract tight bounding boxes:
- Frame 1-45: "I'm not even mad." → 252×49 bbox
- Frame 46-93: "That's amazing." → 198×49 bbox
4. Create horizontal strip: 450×49 total
Résultats: TexteM SK1seulement bande (450×49)

Économie des symboles:
- Encadrés complets (10 échantillonnésM SK1 300×185 | | 3 | 4 | 5 | 6 | Tokens
- Graphique OCR (2 cadresM SK1 300×185 | | 3 | 4 | 5 | 6 | tokens
- Texte-seulement bande: 450×49 ≥= ≤~50 tokens
Réduction 30× tout en préservant le texte des sous-titres.
Niveau 3: Vision LLM Escalation
Lorsque Tesseract et Florence -2 échouent ou produisent des résultats faibles - résultats de confiance M SK2 s’élargent à un Vision LLM (GPT-4oMSC5 Claude MSC6 SonnetMST7 Gemini Pro VisionMSC8 ou les modèles Ollama comme minicpmMSM9vMSS10
La porte de la qualité
public class OcrQualityWave : IAnalysisWave
{
private readonly SpellChecker _spellChecker;
public string Name => "OcrQualityWave";
public int Priority => 58; // After Florence-2 and Tesseract
public async Task<IEnumerable<Signal>> AnalyzeAsync(
string imagePath,
AnalysisContext context,
CancellationToken ct)
{
var signals = new List<Signal>();
// Get best OCR result from earlier waves (priority order)
string? ocrText =
context.GetValue<string>("ocr.ml.text") ?? // Florence-2 (priority 51)
context.GetValue<string>("ocr.text"); // Tesseract (priority 50)
if (string.IsNullOrWhiteSpace(ocrText))
{
signals.Add(new Signal
{
Key = "ocr.quality.no_text",
Value = true,
Confidence = 1.0,
Source = Name
});
return signals;
}
// Run spell check (deterministic quality assessment)
var spellResult = _spellChecker.CheckTextQuality(ocrText);
// Additional quality signals to avoid false positives
var alphanumRatio = CalculateAlphanumericRatio(ocrText); // Letters/digits vs junk
var avgTokenLength = CalculateAverageTokenLength(ocrText);
signals.Add(new Signal
{
Key = "ocr.quality.spell_check_score",
Value = spellResult.CorrectWordsRatio,
Confidence = 1.0,
Source = Name,
Metadata = new Dictionary<string, object>
{
["total_words"] = spellResult.TotalWords,
["correct_words"] = spellResult.CorrectWords,
["garbled_words"] = spellResult.GarbledWords,
["alphanum_ratio"] = alphanumRatio,
["avg_token_length"] = avgTokenLength
}
});
// Deterministic escalation threshold
// NOTE: Spellcheck alone can false-trigger on proper nouns, memes, brand names.
// Use additional signals (alphanum ratio, token length) to reduce false escalations.
bool isGarbled = spellResult.CorrectWordsRatio < 0.5 &&
alphanumRatio > 0.7; // Mostly valid characters, just not in dictionary
signals.Add(new Signal
{
Key = "ocr.quality.is_garbled",
Value = isGarbled,
Confidence = 1.0,
Source = Name
});
// Signal Vision LLM escalation
if (isGarbled)
{
signals.Add(new Signal
{
Key = "ocr.quality.escalation_required",
Value = true,
Confidence = 1.0,
Source = Name,
Tags = new List<string> { "action_required", "escalation" },
Metadata = new Dictionary<string, object>
{
["reason"] = "spell_check_below_threshold",
["quality_score"] = spellResult.CorrectWordsRatio,
["threshold"] = 0.5,
["target_tier"] = "vision_llm"
}
});
// Cache garbled text for Vision LLM to access
context.SetCached("ocr.garbled_text", ocrText);
}
return signals;
}
}
L'évacuation est déterministe: cote de vérification de l'orthographe < 50% ≥→ escaladeM SK4 Aucun jugement probabiliste.
Vision LLM avec Filmstrip
Lorsque l'escalade est déclenchée pour les GIF animés, utiliser le texte
public class VisionLlmWave : IAnalysisWave
{
private readonly IVisionLlmClient _client;
public string Name => "VisionLlmWave";
public int Priority => 50;
public async Task<IEnumerable<Signal>> AnalyzeAsync(
string imagePath,
AnalysisContext context,
CancellationToken ct)
{
var signals = new List<Signal>();
// Check if escalation is required
var escalationRequired = context.GetValue<bool>(
"ocr.quality.escalation_required");
if (!escalationRequired)
{
signals.Add(new Signal
{
Key = "vision.llm.skipped",
Value = true,
Confidence = 1.0,
Source = Name,
Metadata = new Dictionary<string, object>
{
["reason"] = "no_escalation_required"
}
});
return signals;
}
// For animated GIFs, use text-only strip
string imageToProcess = imagePath;
bool usedFilmstrip = false;
if (context.GetValue<int>("identity.frame_count") > 1)
{
var filmstrip = await CreateTextOnlyStripAsync(imagePath, ct);
imageToProcess = filmstrip.Path;
usedFilmstrip = true;
signals.Add(new Signal
{
Key = "vision.filmstrip.created",
Value = true,
Confidence = 1.0,
Source = Name,
Metadata = new Dictionary<string, object>
{
["mode"] = "text_only",
["region_count"] = filmstrip.RegionCount,
["token_reduction"] = filmstrip.Reduction,
["original_tokens"] = filmstrip.OriginalTokens,
["final_tokens"] = filmstrip.TotalTokens
}
});
}
// Build constrained prompt
var prompt = BuildConstrainedPrompt(context);
// Call Vision LLM
var result = await _client.ExtractTextAsync(
imageToProcess,
prompt,
ct);
// Emit OCR text signal (Vision LLM tier)
signals.Add(new Signal
{
Key = "ocr.vision.text", // Vision LLM OCR result
Value = result.Text,
Confidence = 0.95, // High but not 1.0 - still probabilistic
Source = Name,
Tags = new List<string> { "ocr", "vision", "llm" },
Metadata = new Dictionary<string, object>
{
["model"] = result.Model,
["used_filmstrip"] = usedFilmstrip,
["inference_time_ms"] = result.InferenceTime,
["token_count"] = result.TokenCount,
["cost_usd"] = result.Cost
}
});
// Optionally emit caption if requested (separate from OCR)
if (result.Caption != null)
{
signals.Add(new Signal
{
Key = "caption.text", // Descriptive caption, not OCR
Value = result.Caption,
Confidence = 0.90,
Source = Name,
Tags = new List<string> { "caption", "description" }
});
}
return signals;
}
private string BuildConstrainedPrompt(AnalysisContext context)
{
var sb = new StringBuilder();
sb.AppendLine("Extract all text from this image.");
sb.AppendLine();
sb.AppendLine("CONSTRAINTS:");
sb.AppendLine("- Only extract text that is actually visible");
sb.AppendLine("- Preserve formatting and line breaks");
sb.AppendLine("- If no text is present, return empty string");
sb.AppendLine();
// Add context from earlier waves
var garbledText = context.GetCached<string>("ocr.garbled_text");
if (!string.IsNullOrEmpty(garbledText))
{
sb.AppendLine("CONTEXT:");
sb.AppendLine("Traditional OCR detected garbled text:");
sb.AppendLine($" \"{garbledText}\"");
sb.AppendLine("Use this as a hint for stylized or unusual fonts.");
sb.AppendLine();
}
sb.AppendLine("Return only the extracted text, no commentary.");
return sb.ToString();
}
}
La chaîne des priorités
Lorsque tous les niveaux sont terminés,, la sélection finale du texte utilise un ordre de priorité strict:
public static string? GetFinalText(DynamicImageProfile profile)
{
// Priority chain (highest to lowest quality)
// NOTE: This selects ONE source, but the ledger exposes ALL sources
// with confidence scores for downstream inspection
// 1. Vision LLM OCR (best for complex/garbled text)
var visionText = profile.GetValue<string>("ocr.vision.text");
if (!string.IsNullOrEmpty(visionText))
return visionText;
// 2. Florence-2 multi-frame GIF OCR (best for animations)
var florenceMultiText = profile.GetValue<string>("ocr.ml.multiframe_text");
if (!string.IsNullOrEmpty(florenceMultiText))
return florenceMultiText;
// 3. Florence-2 single-frame ML OCR (good for stylized fonts)
var florenceText = profile.GetValue<string>("ocr.ml.text");
if (!string.IsNullOrEmpty(florenceText))
return florenceText;
// 4. Tesseract OCR (reliable for clean standard text)
var tesseractText = profile.GetValue<string>("ocr.text");
if (!string.IsNullOrEmpty(tesseractText))
return tesseractText;
// 5. Fallback (empty)
return string.Empty;
}
Chaque niveau possède des caractéristiques connues:
| Source | clé de signalisation | ||||||
|---|---|---|---|---|---|---|---|
| Vision LLM OCR | ocr.vision.text Tableaux complexes |
||||||
| Florence-2 (GIFM SK3 | ocr.ml.multiframe_text |
GIF animés avec des sous-titres | 0.85-0.92 | ||||
| FlorenceM SK1 (single) | ocr.ml.text |
Fonts stylisésM SK1 memes , texte décoratif | 0.85-0.90 | gratuit SSK6 \ | SSK7ms SSK8 | ||
| Tesseract | ocr.text |
Texte standard propreM SK1 contraste élevé | Variations | Libre | S~50ms M |
Analyse des coûts
Avant trois-Tier System
100 images , tous utilisant Vision LLM:
100 images × $0.005/image = $0.50
Total time: 100 × 2s = 200 seconds
Après le système Three-Tier
Répartition des routes (typique):
- 60 images
- 25 images → itinéraire BALANCED MSC2FlorenceMSc3 MSC4 TesseractMScs5 gratuitsMS C6 MSc7ms\MSc8
- 10 images → Route de qualité (+ Vision LLMM SK3 M$0.005, ≥~2s)
- 5 images → route ANIMATE MSC2 piste cinématographique SMC3 ESC4
Cost:
60 × $0 = $0
25 × $0 = $0
10 × $0.005 = $0.05
5 × $0.002 = $0.01
Total: $0.06
Time:
60 × 0.1s = 6s
25 × 0.3s = 7.5s
10 × 2s = 20s
5 × 2.5s = 12.5s
Total: 46 seconds
Savings:
Cost: 88% reduction ($0.50 → $0.06)
Time: 77% reduction (200s → 46s)
Le niveau moyen (FlorenceM SK1 gère 85% d’images à prix zéro.
Mettre tout ensemble
Voici le flux complet d’un meme GIF avec sous-titres.
1. Load image: anchorman-not-even-mad.gif (93 frames)
2. IdentityWave (priority 10):
→ identity.frame_count = 93
→ identity.format = "gif"
→ identity.is_animated = true
3. TextLikelinessWave (priority 40, ~10ms):
→ Heuristic text detection: 15 regions in bottom 30%
→ Subtitle pattern: DETECTED
→ text.likeliness = 0.85
4. OcrWave (priority 50, ~60ms):
→ Run Tesseract OCR on first frame
→ ocr.text = "I'm not emn mad." (garbled)
→ ocr.confidence = 0.62
5. MlOcrWave (priority 51, ~180ms):
→ Tesseract confidence < 0.95, run Florence-2
→ Sample 10 frames (animated GIF)
→ Run Florence-2 on each frame (parallel)
→ Deduplicate: 10 results → 2 unique texts
→ ocr.ml.multiframe_text = "I'm not even mad.\nThat's amazing."
→ ocr.ml.confidence = 0.91
6. OcrQualityWave (priority 58, ~5ms):
→ Check Florence-2 result
→ Spell check: 6/6 words correct (100%)
→ ocr.quality.is_garbled = false
→ ocr.quality.escalation_required = false
7. VisionLlmWave (priority 80, SKIPPED):
→ No escalation required (Florence-2 succeeded)
Final output:
Text: "I'm not even mad.\nThat's amazing."
Source: ocr.ml.multiframe_text
Confidence: 0.91
Cost: $0 (local processing)
Time: ~250ms total (Tesseract + Florence-2)
Si Florence-2 avait échoué M SK1confidence < 0.5), le flux se poursuivrait
6. OcrQualityWave:
→ Spell check: 2/6 words correct (33%)
→ ocr.quality.is_garbled = true
→ ocr.quality.escalation_required = true
7. VisionLlmWave:
→ Create text-only filmstrip (2 regions, 450×49)
→ Send to Vision LLM: "Extract all text from this strip"
→ vision.llm.text = "I'm not even mad.\nThat's amazing."
→ Confidence: 0.95
→ Cost: ~$0.002 (30× token reduction vs full frames)
→ Time: ~2.3s
Configuration
Le système à trois niveaux est entièrement configurable:
{
"DocSummarizer": {
"Ocr": {
"Tesseract": {
"Enabled": true,
"DataPath": "/usr/share/tesseract-ocr/4.00/tessdata",
"Languages": ["eng"],
"EarlyExitThreshold": 0.95
},
"Florence2": {
"Enabled": true,
"ModelPath": "models/florence2-base",
"ConfidenceThreshold": 0.85,
"MaxFrames": 10,
"DeduplicationMethod": "levenshtein",
"LevenshteinThreshold": 0.85
},
"Quality": {
"SpellCheckThreshold": 0.5,
"EscalationEnabled": true
}
},
"VisionLlm": {
"Enabled": true,
"Provider": "ollama",
"OllamaUrl": "http://localhost:11434",
"Model": "minicpm-v:8b",
"MaxRetries": 3,
"TimeoutSeconds": 30
},
"Filmstrip": {
"TextOnlyMode": true,
"SubtitleRegionPercent": 0.3,
"BrightPixelThreshold": 200,
"TextChangeThreshold": 0.05
},
"Routing": {
"FastRouteConfidence": 0.8,
"BalancedRouteConfidence": 0.5,
"TextDetectionEnabled": true
}
}
}
Modes de défaillance
| Défaut | Détection MSC2 Réponse MSC3 | ||||||
|---|---|---|---|---|---|---|---|
| Tesseract échoue | Confidence < MSC2 OU vérification de l'orthographe SMC3 | 4 | 5 | Évacuer à Florence | SMC6 | 7 | |
| Florence-2 échec | Confidentialité < 0.5 OU vérification de l'orthographe | 3 | 4 | 5 | Escalier à la vision LLM | 6 | |
| Démarrage de Vision LLM | Demande excède 30s | ||||||
| Tous les niveaux ont échoué | Tous les résultats sont vides ou bloqués | Retournez la chaîne vide avec confiance | |||||
| Limite du coût de l'API | Budget quotidien excédentaire | Disable Vision LLMM SK2 utiliser Florence-2 seulement MSC4 | |||||
| Modèle non disponible | FlorenceM SK1Vision LLM offline | Passer niveau, aller de l'avant |
Chaque défaillance est déterministe et enregistrée avec la provenance complète.
Comparaison avec d'autres approches
Traditionnel: Tesseract + Fallback manuel
For each image:
1. Run Tesseract
2. If looks wrong, manually fix or skip
Problems:
- No middle tier (binary: works or doesn't)
- Manual intervention required
- No cost optimization
Cloud-First: Utiliser toujours Vision LLM
For each image:
1. Send to GPT-4o/Claude
2. Pay $0.005-0.01 per image
Problems:
- Expensive (85% of images could be free)
- Slow (network latency)
- Still hallucinates without constraints
Trois-TierM SK1 Local-Premièrement avec l'é Escalation intelligente
For each image:
1. OpenCV text detection (5-20ms, free)
2. Route to appropriate tier
3. Florence-2 handles 85% locally (200ms, free)
4. Vision LLM only for complex cases (2-5s, $0.001-0.01)
Benefits:
- 88% cost reduction
- 77% faster (most images process locally)
- Deterministic escalation (auditable)
- Filmstrip optimization (30× token reduction)
- Constrained by deterministic signals
Conclusion
Le pipeline OCR de trois niveaux - prouve que coût-aware routing et local-premier traitement peut améliorer considérablement le rendement et l’économie sans sacrifier la qualité.
Principaux points de vue:
- Florence-2 ONNX est le lieu idéal: meilleur que Tesseract pour les caractères stylisés, plus rapide et moins coûteux que Vision LLM
- Texte-seulement les bandes atteignent la réduction du symbole M SK1: Extraire des boîtes de bouillage, sans cadres complets
- Le routage est déterministe: Détection d'un CV ouvert + seuils de confiance
- L'évacuation est vérifiable: Chaque niveau émet des signaux de provenance
- L'échec est gracieux: La chaîne de priorité assure le retour à la meilleure source disponible
Les échelles du modèle: Analyse déterministe locale → Modèle local de ML → escalation nuageuse, chaque niveau ayant des caractéristiques connues et un échange de coûts
Il s’agit de la constraint Fuzziness appliquée aux signaux déterministes OCR: ( vérification du langageM SK2 détection des textes) modèles probabilistes de contrainte MSC4FlorenceMST5 Vision LLMMst6 et le résultat final regroupe les sources par qualitéM st7
Ressources
Documentation LucidRAG
- Bibliothèque ImageSummarizer - Code de source
- Vision Intégration de l'OCR - RoutingM SK1 filmstrips, token economics
- Guide de l'architecture - Évacuation des ondes
- Documentation du pipeline - AutoM SK1 équilibrés , routes de qualité
- Référence sur les signaux - Catalogue complet des signaux
Outils CLI
- ImageSummarizer CLI - Commande-outil de ligne
- CLI README - Usage et configuration
- Images de démonstration - Échantillons de GIF et bandes-cadre
Documents de recherche
- Florence-2 Papier Modèle linguistique
- Détecteur de texte de l'Est - Détection efficace du texte de la scène
- Papier CRAFT - Sensibilisation à la région du caractère
- Real-ESRGAN - Super pratiqueM SK1résolution
- CLIP Papier - Apprentissage de modèles visuels transférables
Articles connexes
- Partie 4: Intelligence de l'image - Vue d'ensemble de l'architecture des ondes
- DocSummarizer - Pipeline d'analyse des documents
- Système de gestion des données - Approche de profilage des données
La série
| Partie | Modèle MSC2 Concentration MSC3 | |
|---|---|---|
| 1 | Fuzziness restreinte Composante unique | |
| 2 | MoM Fuzzy restreint | Composantes multiples |
| 3 | Traçage du contexte | Temps / mémoire |
| 4 | Intelligence de l'image | Architecture des vaguesM SK1 patrons |
| 4.1 | The Three-Tier OCR Pipeline (this articleM SK2 | OCR, modèles ONNX, bandes de films |
Suivant: Partie 5 montrera comment ImageSummarizer DocSummarizer, et Système de gestion des données composer en graphe multi-modal RAG avec LucidRAG.
Toutes les parties suivent la même invariante: Les composantes probabilistes proposent; les systèmes déterministes persistent.