OCR constrémenté Fuzzy - Les trois -Tier OCR Pipeline (Français (French))

OCR constrémenté Fuzzy - Les trois -Tier OCR Pipeline

Wednesday, 07 January 2026

//

31 minute read

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:


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 extrait
  • ocr.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 OCR
  • ocr.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 OCR
  • ocr.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é:

  1. EAST n'est pas disponible ou a échoué
  2. L'image a des caractères artistiques/decoratifs (autoM SK2détecté)
  3. 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 extrait
  • ocr.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)

Texte-Exemple de bande seulement

É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:

  1. 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
  2. Texte-seulement les bandes atteignent la réduction du symbole M SK1: Extraire des boîtes de bouillage, sans cadres complets
  3. Le routage est déterministe: Détection d'un CV ouvert + seuils de confiance
  4. L'évacuation est vérifiable: Chaque niveau émet des signaux de provenance
  5. 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

Outils CLI

Documents de recherche

Articles connexes


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.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.