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>
38 KiB
OCR Pipeline - Schrittweise Seitenrekonstruktion
Version: 4.1.0 Status: Produktiv (Schritte 1–10 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.7–0.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.5–0.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:
boundingRecteiner 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:
- Woerter werden nach X-Position in vertikale Spaltencluster gruppiert
- Pro Cluster: Lineare Regression
x = a*y + b→a = dx/dy = tan(shear_angle) - Ensemble aus drei Methoden: Textzeilen (1.5× Gewicht), Projektionsprofil (2-Pass), Vertikalkanten
- 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-Grenzeright_x), damit Woerter am rechten Rand der letzten Spalte nicht uebersehen werden. - Letzte Spalte: Wird immer bis zur vollen Bildbreite
wausgedehnt, 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)
- Horizontales Projektionsprofil berechnen: Zeilensummen ueber den Content-Bereich
- Leere Zeilen erkennen: Zeilen mit < 2% Tinten-Dichte (
H_GAP_THRESH = 0.02) - Gaps sammeln: Zusammenhaengende leere Zeilen zu Gaps buendeln (Mindestlaenge:
max(5, h/200)) - Grosse Gaps identifizieren: Gaps > 1.8× Median-Gap-Hoehe = Sub-Header-Trennungen
- Segmente bilden: Seite an grossen Gaps aufteilen
- 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:
- X-Achse in 2px-Bins aufteilen
- Pro Bin zaehlen, wie viele Segment-Woerter ihn ueberdecken
- Zusammenhaengende Bins mit 0 Woertern = Gap-Kandidaten
- Nur Gaps im inneren 90%-Bereich beruecksichtigen (Raender ignorieren)
- 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):
- Fuer jede Spalte mit
width_ratio >= 0.15undword_count >= 5: - Left-Edges aller Woerter mit
conf >= 30sammeln - In Alignment-Bins clustern (8px Toleranz)
- Linkester Bin mit >= 10% der Woerter = wahrer Spaltenanfang
- Woerter links davon = Sub-Spalte, wenn >= 2 und < 35% Anteil
- 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:
-
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. -
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 20–40% Bildbreite, typische schmale bei 3–12%. Die 15%-Grenze trennt diese Gruppen sauber.
!!! note "Offener Punkt: Schwellen-Validierung" Die 15%-Schwelle wurde an Vokabeltabellen mit 3–5 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):
- Crop: Exakte Spalten- × Zeilengrenzen mit 3px internem Padding
- Density-Check: Ueberspringe leere Zellen (
dark_ratio < 0.005) - Upscaling: Kleine Crops (Hoehe < 80px) werden 3× vergroessert
- OCR: Engine-spezifisch (Tesseract, TrOCR, RapidOCR, LightON)
- Fallback: Bei leerem Ergebnis → PSM 7 (Einzelzeile) statt PSM 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, 1 → I, 8 → B) 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 en → B |
1 want |
I want |
Alleinstehendes 1 → I (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.5g → p.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 50–200 %
- 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 3–5) |
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 6–7 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 |