Files
breakpilot-lehrer/docs-src/services/klausur-service/OCR-Pipeline.md
Benjamin Admin e60254bc75
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Failing after 1m59s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 24s
fix: alle Post-Crop-Schritte nutzen cropped statt dewarped Bild
Spalten-, Zeilen-, Woerter-Overlay und alle nachfolgenden Steps
(LLM-Review, Rekonstruktion) lesen jetzt image/cropped mit Fallback
auf image/dewarped. Tests fuer page_crop.py hinzugefuegt (25 Tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 09:10:10 +01:00

38 KiB
Raw Blame History

OCR Pipeline - Schrittweise Seitenrekonstruktion

Version: 4.1.0 Status: Produktiv (Schritte 110 implementiert) URL: https://macmini:3002/ai/ocr-pipeline

Uebersicht

Die OCR Pipeline zerlegt den OCR-Prozess in 10 einzelne Schritte, um eingescannte Seiten aus mehrspaltig gedruckten Schulbuechern Wort fuer Wort zu rekonstruieren. Jeder Schritt kann individuell geprueft, korrigiert und mit Ground-Truth-Daten versehen werden.

Ziel: 10 Vokabelseiten fehlerfrei rekonstruieren.

Pipeline-Schritte

Schritt Name Beschreibung Status
1 Orientierung 90/180/270° Drehungen von Scannern korrigieren Implementiert
2 Begradigung (Deskew) Scan begradigen (Hough Lines + Word Alignment) Implementiert
3 Entzerrung (Dewarp) Buchwoelbung entzerren (Vertikalkanten-Analyse) Implementiert
4 Zuschneiden (Crop) Content-basierter Crop: Buchruecken-Schatten + Ink-Projektion Implementiert
5 Spaltenerkennung Unsichtbare Spalten finden (Projektionsprofile + Wortvalidierung) Implementiert
6 Zeilenerkennung Horizontale Zeilen + Kopf-/Fusszeilen-Klassifikation + Luecken-Heilung Implementiert
7 Worterkennung Hybrid-Grid: Breite Spalten full-page, schmale cell-crop Implementiert
8 Korrektur Zeichenverwirrung + regel-basierte Rechtschreibkorrektur (SSE-Stream) Implementiert
9 Rekonstruktion Interaktive Zellenbearbeitung auf Bildhintergrund (Fabric.js) Implementiert
10 Validierung Ground-Truth-Vergleich und Qualitaetspruefung Implementiert

!!! note "Reihenfolge-Aenderung (v4.1)" Crop wurde hinter Deskew/Dewarp verschoben. Das Bild ist dann bereits gerade, was den Content-basierten Crop deutlich zuverlaessiger macht — insbesondere bei Buchscans mit Ruecken-Schatten und weissem Scanner-Hintergrund.


Dokumenttyp-Erkennung und Pipeline-Pfade

Automatische Weiche: detect_document_type()

Nicht jedes Dokument durchlaeuft denselben Pfad. Nach den gemeinsamen Vorverarbeitungsschritten (Orientierung, Deskew, Dewarp, Crop) analysiert detect_document_type() die Seitenstruktur ohne OCR — rein ueber Projektionsprofile und Textdichte-Analyse (< 2 Sekunden).

detect_document_type(ocr_img, img_bgr) → DocumentTypeResult

Entscheidungslogik

flowchart TD
    A[Bild-Input] --> B[Vertikales Projektionsprofil]
    B --> C{Interne Spalten-Gaps >= 2?}
    C -->|Ja| D{Zeilen-Gaps >= 5?}
    D -->|Ja| E["vocab_table<br/>pipeline = cell_first<br/>confidence 0.70.95"]
    D -->|Nein| F{Zeilen-Gaps >= 3?}
    C -->|Nein| G{Interne Spalten-Gaps >= 1?}
    G -->|Ja| F
    G -->|Nein| H["full_text<br/>pipeline = full_page<br/>skip: columns, rows"]
    F -->|Ja| I["generic_table<br/>pipeline = cell_first<br/>confidence 0.50.85"]
    F -->|Nein| H
Dokumenttyp Spalten-Gaps Zeilen-Gaps Pipeline Beispiel
vocab_table ≥ 2 ≥ 5 cell_first 3-spaltige Schulbuch-Vokabeltabelle
generic_table ≥ 1 ≥ 3 cell_first 2-spaltiges Glossar
full_text 0 egal full_page Fliesstext, Aufsatz, Buchseite

Komplett-Flussdiagramm

┌─────────────────────────────────────────────────────────────────────┐
│ GEMEINSAME VORVERARBEITUNG (alle Dokumente)                         │
│                                                                     │
│ Schritt 1: Orientierung (90/180/270° Drehung korrigieren)           │
│ Schritt 2: Deskew (Hough Lines + Iterative Projektion + Ensemble)   │
│ Schritt 3: Dewarp (Vertikalkanten-Drift, Ensemble Shear)            │
│ Schritt 4: Crop (Content-basiert: Schatten + Ink-Projektion)        │
└─────────────────────────────────────┬───────────────────────────────┘
                                      │
                            detect_document_type()
                                      │
                    ┌─────────────────┴──────────────────┐
                    ▼                                     ▼
            FULL-TEXT PFAD                        CELL-FIRST PFAD
            (pipeline='full_page')                (pipeline='cell_first')
                    │                                     │
            Keine Spalten/Zeilen                  Spaltenerkennung
            analyze_layout_by_words()             detect_column_geometry()
            Lese-Reihenfolge                      _detect_sub_columns()
                    │                             expand_narrow_columns()
                    │                             Zeilenerkennung
                    │                             detect_row_geometry()
                    │                                     │
                    │                            build_cell_grid_v2()
                    │                                     │
                    │                           ┌─────────┴──────────┐
                    │                           ▼                    ▼
                    │                     Breite Spalten       Schmale Spalten
                    │                     (>= 15% Breite)     (< 15% Breite)
                    │                     Full-Page Words     Cell-Crop OCR
                    │                     word_lookup          cell_crop_v2
                    │                           │                    │
                    └───────────────────────────┴────────────────────┘
                                                │
                                    Post-Processing Pipeline
                                    (Lautschrift, Komma-Split, etc.)
                                                │
                                    Schritt 8: Korrektur (Spell)
                                    Schritt 9: Rekonstruktion
                                    Schritt 10: Validierung

Architektur

Admin-Lehrer (Next.js)          klausur-service (FastAPI :8086)
┌────────────────────┐          ┌─────────────────────────────┐
│ /ai/ocr-pipeline   │          │ /api/v1/ocr-pipeline/       │
│                    │  REST    │                             │
│ PipelineStepper    │◄────────►│ Sessions CRUD               │
│ StepDeskew         │          │ Image Serving               │
│ StepDewarp         │   SSE    │ Deskew/Dewarp/Columns/Rows  │
│ StepColumnDetection│◄────────►│ Word Recognition            │
│ StepRowDetection   │          │ Correction (Spell-Checker)  │
│ StepWordRecognition│          │ Reconstruction              │
│ StepLlmReview      │          │ Ground Truth                │
│ StepReconstruction │          └─────────────────────────────┘
│ StepGroundTruth    │                    │
└────────────────────┘                    ▼
                               ┌─────────────────────┐
                               │ PostgreSQL           │
                               │ ocr_pipeline_sessions│
                               │ (Images + JSONB)     │
                               └─────────────────────┘

Dateistruktur

klausur-service/backend/
├── services/
│   └── cv_vocab_pipeline.py            # Computer Vision + NLP Algorithmen
├── ocr_pipeline_api.py                 # FastAPI Router (Schritte 2-10)
├── orientation_crop_api.py             # FastAPI Router (Schritte 1 + 4)
├── page_crop.py                        # Content-basierter Crop-Algorithmus
├── ocr_pipeline_session_store.py       # PostgreSQL Persistence
├── layout_reconstruction_service.py    # Fabric.js JSON + PDF/DOCX Export
└── migrations/
    ├── 002_ocr_pipeline_sessions.sql   # Basis-Schema
    ├── 003_add_row_result.sql          # Row-Result Spalte
    └── 004_add_word_result.sql         # Word-Result Spalte

admin-lehrer/
├── app/(admin)/ai/ocr-pipeline/
│   ├── page.tsx                        # Haupt-Page mit Session-Management
│   └── types.ts                        # TypeScript Interfaces
└── components/ocr-pipeline/
    ├── PipelineStepper.tsx              # Fortschritts-Stepper
    ├── StepOrientation.tsx             # Schritt 1: Orientierung
    ├── StepDeskew.tsx                   # Schritt 2: Begradigung
    ├── StepDewarp.tsx                   # Schritt 3: Entzerrung
    ├── StepCrop.tsx                     # Schritt 4: Zuschneiden
    ├── StepColumnDetection.tsx          # Schritt 5: Spaltenerkennung
    ├── StepRowDetection.tsx             # Schritt 6: Zeilenerkennung
    ├── StepWordRecognition.tsx          # Schritt 7: Worterkennung
    ├── StepLlmReview.tsx               # Schritt 8: Korrektur (SSE-Stream)
    ├── StepReconstruction.tsx           # Schritt 9: Rekonstruktion (Canvas)
    ├── FabricReconstructionCanvas.tsx   # Fabric.js Editor
    └── StepGroundTruth.tsx             # Schritt 10: Validierung

API-Referenz

Alle Endpoints unter /api/v1/ocr-pipeline/.

Sessions

Methode Pfad Beschreibung
POST /sessions Neue Session erstellen (Bild hochladen)
GET /sessions Alle Sessions auflisten
GET /sessions/{id} Session-Info mit allen Step-Results
PUT /sessions/{id} Session umbenennen
DELETE /sessions/{id} Session loeschen
POST /sessions/{id}/detect-type Dokumenttyp erkennen

Bilder

Methode Pfad Beschreibung
GET /sessions/{id}/image/original Originalbild
GET /sessions/{id}/image/oriented Orientiertes Bild
GET /sessions/{id}/image/deskewed Begradigtes Bild
GET /sessions/{id}/image/dewarped Entzerrtes Bild
GET /sessions/{id}/image/cropped Zugeschnittenes Bild
GET /sessions/{id}/image/binarized Binarisiertes Bild
GET /sessions/{id}/image/columns-overlay Spalten-Overlay
GET /sessions/{id}/image/rows-overlay Zeilen-Overlay
GET /sessions/{id}/image/words-overlay Wort-Grid-Overlay

Schritt 1: Orientierung

Methode Pfad Beschreibung
POST /sessions/{id}/orientation 90/180/270° Drehung erkennen und korrigieren

Schritt 2: Begradigung

Methode Pfad Beschreibung
POST /sessions/{id}/deskew Automatische Begradigung
POST /sessions/{id}/deskew/manual Manuelle Winkelkorrektur
POST /sessions/{id}/ground-truth/deskew Ground Truth speichern

Schritt 3: Entzerrung

Methode Pfad Beschreibung
POST /sessions/{id}/dewarp Automatische Entzerrung
POST /sessions/{id}/dewarp/manual Manueller Scherbungswinkel
POST /sessions/{id}/adjust-combined Kombinierte Rotation + Shear Feinabstimmung
POST /sessions/{id}/ground-truth/dewarp Ground Truth speichern

Schritt 4: Zuschneiden

Methode Pfad Beschreibung
POST /sessions/{id}/crop Automatischer Content-Crop
POST /sessions/{id}/crop/manual Manueller Crop (Prozent-Koordinaten)
POST /sessions/{id}/crop/skip Crop ueberspringen

Schritt 5: Spalten

Methode Pfad Beschreibung
POST /sessions/{id}/columns Automatische Spaltenerkennung
POST /sessions/{id}/columns/manual Manuelle Spalten-Definition
POST /sessions/{id}/ground-truth/columns Ground Truth speichern

Schritt 6: Zeilen

Methode Pfad Beschreibung
POST /sessions/{id}/rows Automatische Zeilenerkennung
POST /sessions/{id}/rows/manual Manuelle Zeilen-Definition
POST /sessions/{id}/ground-truth/rows Ground Truth speichern
GET /sessions/{id}/ground-truth/rows Ground Truth abrufen

Schritt 7: Worterkennung

Methode Pfad Beschreibung
POST /sessions/{id}/words Wort-Grid aus Spalten x Zeilen erstellen
POST /sessions/{id}/ground-truth/words Ground Truth speichern
GET /sessions/{id}/ground-truth/words Ground Truth abrufen

Schritt 8: Korrektur

Methode Pfad Beschreibung
POST /sessions/{id}/llm-review?stream=true SSE-Stream Korrektur starten
POST /sessions/{id}/llm-review/apply Ausgewaehlte Korrekturen speichern

Schritt 9: Rekonstruktion

Methode Pfad Beschreibung
POST /sessions/{id}/reconstruction Zellaenderungen speichern
GET /sessions/{id}/reconstruction/fabric-json Fabric.js Canvas-Daten
GET /sessions/{id}/reconstruction/export/pdf PDF-Export (reportlab)
GET /sessions/{id}/reconstruction/export/docx DOCX-Export (python-docx)
POST /sessions/{id}/reconstruction/detect-images Bildbereiche per VLM erkennen
POST /sessions/{id}/reconstruction/generate-image Bild per mflux generieren
POST /sessions/{id}/reconstruction/validate Validierung speichern (Step 10)
GET /sessions/{id}/reconstruction/validation Validierungsdaten abrufen

Schritt 4: Zuschneiden/Crop (Detail)

Warum Crop nach Deskew/Dewarp?

In frueheren Versionen lief Crop als Schritt 2 (vor Deskew). Das fuehrte zu Problemen:

  • Schiefes Bild: boundingRect einer schiefen Seite schliesst viel Scanner-Hintergrund ein
  • Buchscans: Otsu-Binarisierung versagt bei weiss-auf-weiss (Seite auf weissem Scanner)
  • Buchruecken: Gradueller Schatten-Uebergang wird nicht als Kante erkannt

Loesung (v4.1): Crop laeuft jetzt nach Dewarp — das Bild ist dann gerade.

Algorithmus: Content-basierte 4-Kanten-Erkennung

Datei: page_crop.py

Input: Entzerrtes BGR-Bild
  │
  ├─ Adaptive Threshold (Gauss, blockSize=51)
  │   → binary (Text=255, Hintergrund=0)
  │
  ├─ Linker Rand (Buchruecken-Schatten):
  │   1. Grauwert-Spaltenmittel in linken 25%
  │   2. Glaetten mit Boxcar-Kernel
  │   3. Transition hell→dunkel finden (> 60% des Helligkeitsbereichs)
  │   4. Fallback: Binaere Vertikal-Projektion
  │
  ├─ Rechter Rand: Binaere Vertikal-Projektion (letzte Ink-Spalte)
  │
  ├─ Oben/Unten: Binaere Horizontal-Projektion (erste/letzte Ink-Zeile)
  │
  ├─ Rausch-Filter: Runs < 0.5% der Dimension ignorieren
  │
  ├─ Sanity-Checks:
  │   - Mindestens eine Kante > 2% Border
  │   - Crop-Flaeche >= 40% des Originals
  │
  └─ Crop + konfigurierbarer Rand (default 1%)

Vergleich alt vs. neu

Eigenschaft Alt (Otsu + Kontur) Neu (Content-basiert)
Binarisierung Otsu (global) Adaptive Threshold
Methode Groesste Kontur → boundingRect 4-Kanten Ink-Projektion
Buchruecken Nicht erkannt Schatten-Gradient-Erkennung
Weiss-auf-weiss Versagt Funktioniert (adaptive)
Format-Matching A4/Letter erzwungen Kein Format-Matching (Content-Bounds)
Position in Pipeline Vor Deskew (Schritt 2) Nach Dewarp (Schritt 4)

Schritt 3: Entzerrung/Dewarp (Detail)

Algorithmus: Vertikalkanten-Drift

Die Dewarp-Erkennung misst die vertikale Spaltenkippung (dx/dy) statt Textzeilen-Neigung:

  1. Woerter werden nach X-Position in vertikale Spaltencluster gruppiert
  2. Pro Cluster: Lineare Regression x = a*y + ba = dx/dy = tan(shear_angle)
  3. Ensemble aus drei Methoden: Textzeilen (1.5× Gewicht), Projektionsprofil (2-Pass), Vertikalkanten
  4. Qualitaetspruefung: Horizontale Projektionsvarianz vor/nach Korrektur

Schwellenwerte:

Parameter Wert Beschreibung
Min. Korrekturwinkel 0.08° Unter 0.08° wird nicht korrigiert
Ensemble Min-Confidence 0.35 Mindest-Konfidenz fuer Korrektur
Quality-Gate Skip < 0.5° Kleine Korrekturen ueberspringen Quality-Gate

Feinabstimmung (Combined Adjust)

Der Endpoint POST /sessions/{id}/adjust-combined erlaubt die kombinierte Feinabstimmung von Rotation und Shear in einem Schritt. Im Frontend stehen 7 Schieberegler zur Verfuegung:

Rotation (3 Paesse):

Slider Bereich Beschreibung
P1 Iterative ±5° Erster Deskew-Pass (Hough Lines)
P2 Word-Alignment ±3° Zweiter Pass (Wort-Ausrichtung)
P3 Textline ±3° Dritter Pass (Textzeilen-Regression)

Die Summe aller drei ergibt den finalen Rotationswinkel.

Shear (4 Methoden, Radio-Auswahl):

Slider Bereich Beschreibung
A: Textline Drift ±5° Textzeilen-Drift
B: Projection Profile ±5° 2-Pass Projektionsprofil
C: Vertical Edges ±5° Vertikalkanten-Analyse
D: Ensemble ±5° Gewichteter Ensemble-Wert

Nur der per Radio-Button ausgewaehlte Shear-Wert wird verwendet.

POST /sessions/{id}/adjust-combined
Body: {"rotation_degrees": 1.23, "shear_degrees": -0.45}
Response: {"method_used": "manual_combined", "shear_degrees": -0.45, "dewarped_image_url": "..."}

Schritt 5: Spaltenerkennung (Detail)

Algorithmus: detect_column_geometry()

Mehrstufige Erkennung: Seite segmentieren, vertikale Projektionsprofile finden Luecken, Wort-Bounding-Boxes validieren.

Bild → Binarisierung → Seiten-Segmentierung → Vertikalprofil → Lueckenerkennung → Wort-Validierung → ColumnGeometry

Wichtige Implementierungsdetails:

  • Initialer Tesseract-Scan: Laeuft auf der vollen Bildbreite [left_x : w] (nicht nur bis zur Content-Grenze right_x), damit Woerter am rechten Rand der letzten Spalte nicht uebersehen werden.
  • Letzte Spalte: Wird immer bis zur vollen Bildbreite w ausgedehnt, nicht nur bis zur erkannten Content-Grenze.
  • Phantom-Spalten-Filter (Step 9): Spalten mit Breite < 3 % der Content-Breite UND < 3 Woerter werden als Artefakte entfernt; die angrenzenden Spalten schliessen die Luecke.
  • Spaltenzuweisung: Woerter werden anhand des groessten horizontalen Ueberlappungsbereichs einer Spalte zugeordnet.

Seiten-Segmentierung an Sub-Headern

Farbige Zwischenueberschriften (z.B. „Unit 4: Bonnie Scotland" mit blauem Hintergrund) erzeugen nach Binarisierung Tinte ueber die gesamte Seitenbreite. Diese Baender fuellen Spaltenluecken im vertikalen Projektionsprofil auf und fuehren zu fragmentierten Spalten (z.B. 11 statt 5).

Loesung: Horizontale Gap-Segmentierung (Step 2b)

  1. Horizontales Projektionsprofil berechnen: Zeilensummen ueber den Content-Bereich
  2. Leere Zeilen erkennen: Zeilen mit < 2% Tinten-Dichte (H_GAP_THRESH = 0.02)
  3. Gaps sammeln: Zusammenhaengende leere Zeilen zu Gaps buendeln (Mindestlaenge: max(5, h/200))
  4. Grosse Gaps identifizieren: Gaps > 1.8× Median-Gap-Hoehe = Sub-Header-Trennungen
  5. Segmente bilden: Seite an grossen Gaps aufteilen
  6. Groesstes Segment waehlen: Das hoechste Segment wird fuer die vertikale Projektion verwendet
┌─────────────────────────────────┐
│  Header / Titel                 │  ─── grosser Gap ───
├─────────────────────────────────┤
│  EN  │  DE  │ Example │ Page   │  ← Segment 1 (groesster)
│  ... │  ... │ ...     │ ...    │
├─────────────────────────────────┤
│  Unit 4: Bonnie Scotland        │  ─── grosser Gap ───
├─────────────────────────────────┤
│  EN  │  DE  │ Example │ Page   │  ← Segment 2
│  ... │  ... │ ...     │ ...    │
└─────────────────────────────────┘

Segment-gefilterte Wort-Validierung:

Die Wort-Validierung (Step 5) nutzt nur Tesseract-Woerter innerhalb des gewaehlten Segments. Woerter aus Sub-Header-Bereichen (die die volle Breite einnehmen) werden so ausgeschlossen und koennen die Spaltenluecken-Validierung nicht verfaelschen.

Word-Coverage Gap Detection (Fallback)

Wenn die pixel-basierte Projektion keine ausreichenden Spaltenluecken findet (z.B. bei Seiten mit Illustrationen, die Spaltenluecken teilweise verdecken), greift ein Fallback auf Basis der Tesseract-Wort-Bounding-Boxes:

  1. X-Achse in 2px-Bins aufteilen
  2. Pro Bin zaehlen, wie viele Segment-Woerter ihn ueberdecken
  3. Zusammenhaengende Bins mit 0 Woertern = Gap-Kandidaten
  4. Nur Gaps im inneren 90%-Bereich beruecksichtigen (Raender ignorieren)
  5. Gaps mit Mindestbreite (max(8px, content_w * 0.5%)) werden als Spaltenluecken akzeptiert

Sub-Spalten-Erkennung: _detect_sub_columns()

Erkennt versteckte Sub-Spalten innerhalb breiter Spalten (z.B. Seitenzahl-Spalte links neben EN-Vokabeln).

Algorithmus (Left-Edge Alignment Clustering):

  1. Fuer jede Spalte mit width_ratio >= 0.15 und word_count >= 5:
  2. Left-Edges aller Woerter mit conf >= 30 sammeln
  3. In Alignment-Bins clustern (8px Toleranz)
  4. Linkester Bin mit >= 10% der Woerter = wahrer Spaltenanfang
  5. Woerter links davon = Sub-Spalte, wenn >= 2 und < 35% Anteil
  6. Neue ColumnGeometry-Objekte mit korrekten Indizes erzeugen

Koordinatensystem: Word left-Werte sind relativ zum Content-ROI (left_x), ColumnGeometry.x ist absolut. left_x wird als Parameter durchgereicht.

Spalten-Erweiterung: expand_narrow_columns()

Laeuft nach _detect_sub_columns(). Erweitert sehr schmale Spalten (< 10% Content-Breite, z.B. page_ref, marker) in den Weissraum zum Nachbar-Spalte hinein, aber nie ueber die naechsten Woerter im Nachbarn hinaus (4px Sicherheitsabstand).

Spaltentyp-Klassifikation: classify_column_types()

Spaltentyp Beschreibung Erkennung
column_en Englische Vokabeln EN-Funktionswoerter (the, a, is...)
column_de Deutsche Uebersetzung DE-Funktionswoerter (der, die, das...)
column_example Beispielsaetze Abkuerzungen, Grammatik-Marker
page_ref Seitenzahlen Schmal (< 20% Breite), wenige Woerter
column_marker Dekorative Markierungen Sehr schmal, spezielle Zeichen
column_text Generischer Text Fallback

Konfigurierbare Parameter

# Mindestbreite fuer echte Spalten (automatisch: max(20px, 3% content_w))
min_real_col_w = max(20, int(content_w * 0.03))

Schritt 6: Zeilenerkennung (Detail)

Algorithmus: detect_row_geometry()

Horizontale Projektionsprofile finden Zeilen-Luecken; word-level Validierung verhindert Fehlschnitte.

Zusaetzliche Post-Processing-Schritte:

  1. Artefakt-Zeilen entfernen (_is_artifact_row): Zeilen, in denen alle erkannten Tokens nur 1 Zeichen lang sind (Scan-Schatten, leere Zeilen), werden als Artefakte klassifiziert und aus dem Grid entfernt.

  2. Luecken-Heilung (_heal_row_gaps): Nach dem Entfernen leerer/Artefakt-Zeilen werden die verbleibenden Zeilen auf die Mitte der entstehenden Luecke ausgedehnt, damit kein Zeileninhalt durch schrumpfende Grenzen abgeschnitten wird.

def _is_artifact_row(row: RowGeometry) -> bool:
    """Zeile ist Artefakt wenn alle Tokens <= 1 Zeichen."""
    if row.word_count == 0: return True
    return all(len(w.get('text','').strip()) <= 1 for w in row.words)

def _heal_row_gaps(rows, top_bound, bottom_bound):
    """Verbleibende Zeilen auf Mitte der Luecken ausdehnen."""
    ...

Schritt 7: Worterkennung — Hybrid-Grid (Detail)

Algorithmus: build_cell_grid_v2()

Schritt 5 nutzt eine Hybrid-Strategie: Breite Spalten verwenden die Full-Page-Tesseract-Woerter, schmale Spalten werden isoliert per Cell-Crop OCR verarbeitet.

!!! success "Warum Hybrid?" Full-Page OCR liefert gute Ergebnisse fuer breite Spalten (Saetze, IPA-Klammern, Interpunktion). Aber bei schmalen Spalten (Seitenzahlen, Marker) „bluten" Woerter aus Nachbar-Spalten ein. Cell-Crop isoliert jede Zelle und verhindert dieses Bleeding.

Broad vs. Narrow — Die 15%-Schwelle

_NARROW_COL_THRESHOLD_PCT = 15.0  # cv_vocab_pipeline.py
Eigenschaft Breite Spalten (>= 15%) Schmale Spalten (< 15%)
OCR-Quelle Full-Page Tesseract (vorher gelaufen) Isolierter Cell-Crop
Wort-Zuweisung _assign_row_words_to_columns() Direktes Zell-OCR
Confidence-Filter conf >= 30 conf >= 30
Text-Bereinigung _clean_cell_text() (mittel) _clean_cell_text_lite() (aggressiv)
Neighbour-Bleeding Risiko vorhanden Verhindert (isoliert)
Parallelisierung Sequentiell Parallel (max_workers=4)
OCR-Engine Label word_lookup cell_crop_v2
Typische Spalten EN-Vokabeln, DE-Uebersetzung, Beispielsaetze Seitenzahlen, Marker

Empirische Grundlage: Typische breite Spalten liegen bei 2040% Bildbreite, typische schmale bei 312%. Die 15%-Grenze trennt diese Gruppen sauber.

!!! note "Offener Punkt: Schwellen-Validierung" Die 15%-Schwelle wurde an Vokabeltabellen mit 35 Spalten validiert. Fuer eine breitere Validierung werden diverse Schulbuchseiten mit unterschiedlichen Layouts (2-, 3-, 4-, 5-spaltig, verschiedene Verlage) benoetigt. Aktuell gibt es in der Datenbank nur Sessions mit demselben Arbeitsblatt-Typ.

Cell-Crop OCR: _ocr_cell_crop()

Isolierte OCR einer einzelnen Zelle (Spalte × Zeile Schnittflaeche):

  1. Crop: Exakte Spalten- × Zeilengrenzen mit 3px internem Padding
  2. Density-Check: Ueberspringe leere Zellen (dark_ratio < 0.005)
  3. Upscaling: Kleine Crops (Hoehe < 80px) werden 3× vergroessert
  4. OCR: Engine-spezifisch (Tesseract, TrOCR, RapidOCR, LightON)
  5. Fallback: Bei leerem Ergebnis → PSM 7 (Einzelzeile) statt PSM 6
  6. Bereinigung: _clean_cell_text_lite() (aggressives Noise-Filtering)

Ablauf von build_cell_grid_v2()

Eingabe: ocr_img, column_regions, row_geometries
                    │
        ┌───────────┴───────────┐
        │ Filter                 │
        │ • Phantom-Zeilen       │
        │ • Artefakt-Zeilen      │
        │ • Irrelevante Spalten  │
        │   (header, footer,     │
        │    margin, ignore)     │
        └───────────┬───────────┘
                    │
        ┌───────────┴───────────┐
        │ Klassifizierung        │
        │ Spalte.width / img_w   │
        │ >= 15% → broad         │
        │ < 15%  → narrow        │
        └───────────┬───────────┘
                    │
        ┌───────────┴────────────────┐
        │                            │
   Phase 1: Broad               Phase 2: Narrow
   (sequentiell)                 (parallel, max_workers=4)
        │                            │
   Pro (row, col):               Pro (row, col):
   1. Words aus Full-Page        1. _ocr_cell_crop()
   2. Filter conf >= 30         2. Isoliertes Zell-Bild
   3. _words_to_reading_order   3. Upscale wenn noetig
   4. _clean_cell_text()        4. _clean_cell_text_lite()
        │                            │
        └───────────┬────────────────┘
                    │
            Merge + Sortierung
            (row_index, col_index)
            Leere Zeilen entfernen
                    │
               Ausgabe: cells[], columns_meta[]

Post-Processing Pipeline (in build_vocab_pipeline_streaming)

# Schritt Funktion Beschreibung
0a Lautschrift-Fortsetzung _merge_phonetic_continuation_rows IPA-only Folgezeilen zusammenfuehren
0b Zeilen-Fortsetzung _merge_continuation_rows Zeilen mit Kleinbuchstaben-Anfang zusammenfuehren
2 Lautschrift-Fix _fix_phonetic_brackets OCR-Lautschrift mit Woerterbuch-IPA ersetzen
3 Komma-Split _split_comma_entries break, broke, broken → 3 Eintraege
4 Beispielsaetze _attach_example_sentences Beispielsatz-Zeilen an vorangehenden Eintrag haengen

!!! info "Zeichenkorrektur in Schritt 6" Die Zeichenverwirrungskorrektur (|I, 1I, 8B) laeuft nicht in Schritt 5, sondern als erstes in Schritt 6 (Korrektur), damit die Aenderungen im UI sichtbar und rueckgaengig machbar sind.


Schritt 8: Korrektur (Detail)

Korrektur-Engine

Schritt 6 kombiniert drei Korrektur-Stufen, alle als SSE-Stream:

Stufe 1 — Zeichenverwirrungskorrektur (_fix_character_confusion):

OCR-Fehler Korrektur Regel
|ch Ich | am Wortanfang vor Kleinbuchstaben → I
| want I want Alleinstehendes |I
8en Ben 8 am Wortanfang vor enB
1 want I want Alleinstehendes 1I (NICHT vor . oder ,)
1. Kreuz unveraendert 1. = Listennummer, wird nicht korrigiert

Stufe 2 — Regel-basierte Rechtschreibkorrektur (spell_review_entries_streaming):

Nutzt pyspellchecker (MIT-Lizenz) mit EN+DE-Woerterbuch. Pro Token mit verdaechtigem Zeichen (0, 1, 5, 6, 8, |) werden Kandidaten geprueft:

_SPELL_SUBS = {
    '0': ['O', 'o'], '1': ['l', 'I'], '5': ['S', 's'],
    '6': ['G', 'g'], '8': ['B', 'b'], '|': ['I', 'l', '1'],
}

Stufe 3 — Seitenzahl-Korrektur (page_ref-Felder):

Korrigiert haeufige OCR-Fehler in Seitenverweisen (z.B. p.5gp.59).

Umgebungsvariablen

Variable Default Beschreibung
REVIEW_ENGINE spell Korrektur-Engine: spell oder llm
OLLAMA_REVIEW_MODEL qwen3:0.6b Ollama-Modell (nur wenn REVIEW_ENGINE=llm)
OLLAMA_REVIEW_BATCH_SIZE 20 Eintraege pro LLM-Aufruf

SSE-Protokoll

POST /sessions/{id}/llm-review?stream=true

Events:
  data: {"type": "meta", "total_entries": 96, "to_review": 80, "skipped": 16, "model": "spell"}
  data: {"type": "batch", "changes": [...], "entries_reviewed": [0,1,2,...], "progress": {...}}
  data: {"type": "complete", "duration_ms": 234}
  data: {"type": "error", "detail": "..."}

Change-Format:
  {"row_index": 5, "field": "english", "old": "| want", "new": "I want"}

Schritt 9: Rekonstruktion (Detail)

Zwei Modi verfuegbar:

Einfacher Modus

Das entzerrte Originalbild wird mit 30 % Opazitaet als Hintergrund angezeigt, alle Grid-Zellen (auch leere!) werden als editierbare Textfelder darueber gelegt.

Features:

  • Alle Zellen editierbar — auch leere Zellen (kein Filter mehr)
  • Farbkodierung nach Spaltentyp (Blau=EN, Gruen=DE, Orange=Beispiel)
  • Leere Pflichtfelder (EN/DE) rot gestrichelt markiert
  • Undo/Redo (Ctrl+Z / Ctrl+Shift+Z)
  • Tab-Navigation durch alle Zellen (inkl. leerer)
  • Zoom 50200 %
  • Per-Zell-Reset-Button bei geaenderten Zellen

Fabric.js Editor

Erweiterter Canvas-Editor (FabricReconstructionCanvas.tsx):

  • Drag & Drop fuer Zellen
  • Freie Positionierung auf dem Canvas
  • Export als PDF (reportlab) oder DOCX (python-docx)
POST /sessions/{id}/reconstruction
Body: {"cells": [{"cell_id": "r5_c2", "text": "corrected text"}]}

Wichtige Konstanten

Konstante Wert Datei Beschreibung
_NARROW_COL_THRESHOLD_PCT 15.0% cv_vocab_pipeline.py Schwelle breit/schmal fuer Hybrid-OCR
_NARROW_THRESHOLD_PCT 10.0% cv_vocab_pipeline.py Schwelle fuer Spalten-Erweiterung
_MIN_WORD_CONF 30 cv_vocab_pipeline.py Mindest-Confidence fuer OCR-Woerter
_PAD 3px cv_vocab_pipeline.py Internes Padding bei Cell-Crop
PDF_ZOOM 3.0 cv_vocab_pipeline.py PDF-Rendering (= 432 DPI)
_MIN_WORD_MARGIN 4px cv_vocab_pipeline.py Sicherheitsabstand bei Spalten-Erweiterung

Datenbank-Schema

CREATE TABLE ocr_pipeline_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255),
    filename VARCHAR(255),
    status VARCHAR(50) DEFAULT 'active',
    current_step INT DEFAULT 1,

    -- Dokumenttyp-Erkennung
    doc_type VARCHAR(50),        -- 'vocab_table', 'generic_table', 'full_text'
    doc_type_result JSONB,       -- Vollstaendiges DetectionResult

    -- Bilder (BYTEA)
    original_png BYTEA,
    deskewed_png BYTEA,
    binarized_png BYTEA,
    dewarped_png BYTEA,

    -- Step-Results (JSONB)
    deskew_result JSONB,
    dewarp_result JSONB,
    column_result JSONB,
    row_result JSONB,
    word_result JSONB,   -- enthaelt vocab_entries, cells, llm_review

    -- Ground Truth + Meta
    ground_truth JSONB,
    auto_shear_degrees REAL,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

word_result JSONB-Struktur:

{
  "vocab_entries": [...],
  "cells": [{"cell_id": "r0_c0", "text": "hello", "bbox_pct": {...}, "ocr_engine": "word_lookup", ...}],
  "columns_used": [...],
  "llm_review": {
    "changes": [{"row_index": 5, "field": "english", "old": "...", "new": "..."}],
    "model_used": "spell",
    "duration_ms": 234
  }
}

Abhaengigkeiten

Python (klausur-service)

Paket Version Lizenz Zweck
pytesseract ≥0.3.10 Apache-2.0 Haupt-OCR (Schritt 35)
opencv-python-headless ≥4.8.0 Apache-2.0 Bildverarbeitung, Projektionsprofile
Pillow ≥10.0.0 HPND (MIT-kompatibel) Bildkonvertierung
rapidocr latest Apache-2.0 Schnelles OCR (ARM64 via ONNX)
onnxruntime latest MIT ONNX-Inferenz fuer RapidOCR
pyspellchecker ≥0.8.1 MIT Regel-basierte OCR-Korrektur (Schritt 6)
eng-to-ipa latest MIT IPA-Lautschrift-Lookup (Schritt 5)
reportlab latest BSD PDF-Export (Schritt 7)
python-docx ≥1.1.0 MIT DOCX-Export (Schritt 7)
fabric (JS) ^6 MIT Canvas-Editor (Frontend)

!!! info "pyspellchecker (neu seit 2026-03)" pyspellchecker (MIT-Lizenz) ersetzt die LLM-basierte Korrektur als Standard-Engine. EN+DE-Woerterbuch, ~134k Woerter. Kein Ollama noetig. Umschaltbar via REVIEW_ENGINE=llm fuer den LLM-Pfad.


Bekannte Einschraenkungen

Problem Ursache Workaround
Schraeg gedruckte Seiten Deskew erkennt Text-Rotation, nicht Seiten-Rotation Manueller Winkel
Sehr kleine Schrift (< 8pt) Tesseract PSM 7 braucht min. Zeichengroesse Vorher zoomen
Handgeschriebene Eintraege Tesseract/RapidOCR sind fuer Druckschrift optimiert TrOCR-Engine
Mehr als 5 Spalten Projektionsprofil kann verschmelzen (Segmentierung hilft) Manuelle Spalten
Farbige Marker (rot/blau) HSV-Erkennung erzeugt False Positives Manuell im Rekonstruktions-Editor
15%-Schwelle nicht breit validiert Nur an einem Arbeitsblatt-Typ getestet Diverse Schulbuchseiten testen

Deployment

# 1. Git push
git push origin main

# 2. Mac Mini pull + build
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-lehrer pull --no-rebase origin main"

# klausur-service (Backend)
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml build klausur-service"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml up -d klausur-service"

# admin-lehrer (Frontend)
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml build admin-lehrer"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/docker-compose.yml up -d admin-lehrer"

# 3. Testen unter:
# https://macmini:3002/ai/ocr-pipeline

!!! warning "Base-Image bei neuen Python-Paketen" Wenn requirements.txt geaendert wird (z.B. neues Paket hinzugefuegt), muss zuerst das Base-Image neu gebaut werden: bash ssh macmini "/usr/local/bin/docker build -f /Users/benjaminadmin/Projekte/breakpilot-lehrer/klausur-service/Dockerfile.base \ -t klausur-base:latest /Users/benjaminadmin/Projekte/breakpilot-lehrer/klausur-service/"


Aenderungshistorie

Datum Version Aenderung
2026-03-05 3.1.0 Spalten: Seiten-Segmentierung an Sub-Headern, Word-Coverage Fallback, Segment-gefilterte Validierung
2026-03-05 3.0.1 Dewarp: Feinabstimmung mit 7 Schiebereglern (3 Rotation + 4 Shear), Combined-Adjust-Endpoint
2026-03-05 3.0.0 Doku-Update: Dokumenttyp-Erkennung, Hybrid-Grid, Sub-Column-Detection, Pipeline-Pfade
2026-03-04 2.2.0 Dewarp: Vertikalkanten-Drift statt Textzeilen-Neigung, Schwellenwerte gesenkt
2026-03-04 2.1.0 Sub-Column-Detection, expand_narrow_columns, Fabric.js Editor, PDF/DOCX-Export
2026-03-03 2.0.0 Schritte 67 implementiert; Spell-Checker, Rekonstruktions-Canvas
2026-03-03 1.5.0 Spaltenerkennung: volle Bildbreite fuer initialen Scan, Phantom-Filter
2026-03-03 1.4.0 Zeilenerkennung: Artefakt-Zeilen entfernen + Luecken-Heilung
2026-03-03 1.3.0 Zeichenkorrektur: 1./|. Listenpraefixe werden nicht zu I.
2026-03-03 1.2.0 LLM-Engine durch Spell-Checker ersetzt (REVIEW_ENGINE=spell)
2026-02-28 1.0.0 Schritt 5 (Worterkennung) implementiert
2026-02-22 0.4.0 Schritt 4 (Zeilenerkennung) implementiert
2026-02-20 0.3.0 Schritt 3 (Spaltenerkennung) mit Typ-Klassifikation
2026-02-15 0.2.0 Schritt 2 (Entzerrung/Dewarp)
2026-02-12 0.1.0 Schritt 1 (Begradigung/Deskew) + Session-Management