- Add _filter_border_ghost_words() to remove OCR artefacts from box borders (vertical + horizontal edge detection, column cleanup, re-indexing) - Add 20 tests for border ghost filter (basic filtering + column cleanup) - Add 24 tests for cv_graphic_detect (color detection, word overlap, boxes) - Clean up cv_graphic_detect.py logging (per-candidate → DEBUG) - Add structure overlay layer to StepReconstruction (boxes + graphics toggle) - Show border_ghosts_removed badge in StepStructureDetection - Update MkDocs with structure detection documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61 KiB
OCR Pipeline - Schrittweise Seitenrekonstruktion
Version: 4.3.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 (v2) oder Words-First (bottom-up) | 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 WORDS-FIRST PFAD CELL-FIRST PFAD
(pipeline= (grid_method= (grid_method=
'full_page') 'words_first') 'v2', default)
│ │ │
Keine Spalten/ Tesseract Full-Page Spaltenerkennung
Zeilen word_boxes detect_column_geometry()
analyze_layout_ _cluster_columns() _detect_sub_columns()
by_words() _cluster_rows() expand_narrow_columns()
│ _build_cells() Zeilenerkennung
│ │ detect_row_geometry()
│ build_grid_from_ │
│ words() 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)
├── cv_box_detect.py # Box-Erkennung + Zonen-Aufteilung
├── cv_graphic_detect.py # Grafik-/Bilderkennung (Region-basiert)
├── cv_color_detect.py # Farbtext-Erkennung (HSV-Analyse)
├── cv_words_first.py # Words-First Grid Builder (bottom-up)
├── 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
├── app/(admin)/ai/ocr-overlay/
│ ├── page.tsx # OCR Overlay: 3 Modi (Pipeline/Paddle/Kombi)
│ └── types.ts # OVERLAY_/PADDLE_DIRECT_/KOMBI_STEPS
├── components/ocr-overlay/
│ ├── PaddleDirectStep.tsx # Wiederverwendbar fuer Paddle Direct + Kombi
│ └── OverlayReconstruction.tsx # Overlay-Anzeige auf Bildhintergrund
└── 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
├── StepStructureDetection.tsx # Schritt 8: Strukturerkennung
├── StepLlmReview.tsx # Schritt 9: Korrektur (SSE-Stream)
├── StepReconstruction.tsx # Schritt 9: Rekonstruktion (Canvas + Overlay)
├── usePixelWordPositions.ts # Shared Hook: Pixel-basierte Wortpositionierung
├── 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 erstellen |
POST |
/sessions/{id}/ground-truth/words |
Ground Truth speichern |
GET |
/sessions/{id}/ground-truth/words |
Ground Truth abrufen |
Query-Parameter fuer /sessions/{id}/words:
| Parameter | Default | Beschreibung |
|---|---|---|
engine |
auto |
OCR-Engine: auto, tesseract, rapid, paddle |
pronunciation |
british |
IPA-Woerterbuch: british oder american |
stream |
false |
SSE-Streaming (nur bei grid_method=v2) |
skip_heal_gaps |
false |
Zeilen-Luecken nicht heilen (Overlay-Modus) |
grid_method |
v2 |
Grid-Strategie: v2 (top-down) oder words_first (bottom-up) |
Schritt 8: Strukturerkennung
| Methode | Pfad | Beschreibung |
|---|---|---|
POST |
/sessions/{id}/detect-structure |
Boxen, Zonen, Farben und Grafiken erkennen |
GET |
/sessions/{id}/image/structure-overlay |
Overlay mit allen Strukturelementen |
Schritt 9: 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 10: 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. -
Box-Boundary-Schutz (
box_ranges_inner, neu in v4.2): Bei Seiten mit Box-Zonen (Sub-Sessions) werden Zeilen am Box-Rand nicht faelschlich ausgeschlossen. Das Problem: Die letzte Textzeile ueber einer Box ueberlappt haeufig mit dem Box-Rahmen. Loesung: Die Exclusion-Zone wird ummax(border_thickness, 5px)geschrumpft, sodass nur Zeilen innerhalb der Box ausgeschlossen werden.
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."""
...
Box-Zonen und Content-Strips (Detail)
Seiten mit Box-Bereichen (z.B. Grammatik-Tipps, Uebungsboxen) werden in Zonen aufgeteilt:
┌──────────────────────────┐
│ Content Zone 0 (Zeilen) │ ← Vokabeltabelle oben
├──────────────────────────┤
│ ███ Box Zone (border) ███│ ← Sub-Session mit eigener OCR
├──────────────────────────┤
│ Content Zone 2 (Zeilen) │ ← Vokabeltabelle unten
└──────────────────────────┘
Content-Strip-Verfahren (detect_rows in ocr_pipeline_api.py):
- Box-Zonen identifizieren,
box_ranges_innerberechnen (geschrumpft um Border-Dicke) - Content-Strips = Seitenbereiche ohne Box-Inneres, vertikal gestapelt
- Zeilenerkennung auf gestapeltem Bild, Y-Koordinaten zurueckgemappt
- Wort-Filterung: Woerter in Box-Innerem werden ausgeschlossen
Wichtig: box_ranges_inner (nicht box_ranges) wird verwendet, damit
Zeilen am Box-Rand nicht abgeschnitten werden. Minimum 5px Margin.
Schritt 7: Worterkennung (Detail)
Schritt 7 bietet zwei Grid-Strategien, auswaehlbar per grid_method-Parameter:
| Strategie | Parameter | Ansatz | Benoetigt Spalten/Zeilen? |
|---|---|---|---|
| Hybrid-Grid v2 | grid_method=v2 (Default) |
Top-down: Spalten → Zeilen → Zellen → OCR | Ja (Schritte 5+6) |
| Words-First | grid_method=words_first |
Bottom-up: Woerter → Spalten clustern → Zeilen clustern → Zellen | Nein |
Words-First Grid Builder: build_grid_from_words()
Datei: cv_words_first.py
Der Words-First Builder arbeitet bottom-up: Er nimmt die pixelgenauen word_boxes aus einem
Tesseract Full-Page-Lauf und clustert sie direkt zu Spalten und Zeilen — ohne die
vorherige Spalten-/Zeilenerkennung (Schritte 5+6) zu benoetigen.
Algorithmus
Eingabe: word_dicts (flat list), img_w, img_h
│
┌───────────┴───────────┐
│ 1. Confidence-Filter │
│ conf >= 30 │
│ Whitespace entf. │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ 2. _cluster_columns() │
│ X-Gap-Analyse │
│ Schwelle: median_h │
│ × 3 (min 3% Breite)│
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ 3. _cluster_rows() │
│ Y-Proximity-Grupp. │
│ Toleranz: median_h │
│ / 2 │
└───────────┬───────────┘
│
┌───────────┴───────────┐
│ 4. _build_cells() │
│ Wort → (col, row) │
│ Text + bbox + conf │
│ word_boxes pro Zelle│
└───────────┬───────────┘
│
Ausgabe: cells[], columns_meta[]
(identisch zu build_cell_grid_v2)
Spalten-Clustering
- Alle Woerter nach X-Mitte sortieren
- Aufeinanderfolgende X-Gaps berechnen
- Adaptiver Schwellwert:
median_word_height × 3(min 3% Bildbreite) - Gaps > Schwellwert = Spaltengrenzen
- Kein Gap gefunden → 1 Spalte (
column_text) - Spaltentypen:
column_1,column_2, ... (generisch, positionsbasiert)
Zeilen-Clustering
- Woerter zu visuellen Zeilen gruppieren (Y-Toleranz: halbe Worthoehe)
- Jede visuelle Zeile = eine Zeile im Grid
- Sortiert von oben nach unten
Edge Cases
| Fall | Behandlung |
|---|---|
| Einzelne Spalte (Fliesstext) | Kein X-Gap → 1 Spalte column_text |
| Keine Woerter erkannt | Leeres Ergebnis ([], []) |
| Ueberschriften (grosse Schrift) | Eigene Zeile durch Y-Gap |
| Bilder/Grafiken | Keine Woerter → automatisch leerer Bereich |
| Schmale Spalten (Seitenzahlen) | Eigene Spalte durch X-Gap |
Vergleich v2 vs. Words-First
| Kriterium | v2 (Top-Down) | Words-First (Bottom-Up) |
|---|---|---|
| Abhaengigkeiten | Spalten + Zeilen noetig | Nur Tesseract-Woerter |
| Spaltentypen | Semantisch (EN, DE, ...) | Positionsbasiert (1, 2, ...) |
| OCR | Hybrid (full-page + cell-crop) | Nur full-page Tesseract |
| Robustheit | Abhaengig von Spalten-/Zeilenerkennung | Direkt aus Wortpositionen |
| Geschwindigkeit | Langsamer (cell-crop pro Zelle) | Schneller (kein OCR-Lauf) |
| Genauigkeit | Besser bei schmalen Spalten | Besser bei ungewoehnlichen Layouts |
Hybrid-Grid v2: build_cell_grid_v2()
Schritt 7 nutzt im Default 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, PaddleOCR)
- Fallback: Bei leerem Ergebnis → PSM 7 (Einzelzeile) statt PSM 6
- Bereinigung:
_clean_cell_text_lite()(aggressives Noise-Filtering)
PaddleOCR Remote-Engine (engine=paddle)
PaddleOCR (PP-OCRv5 Latin) laeuft als eigenstaendiger Microservice auf einem Hetzner x86_64 Server, da PaddlePaddle nicht auf ARM64 (Mac Mini) laeuft.
Mac Mini (klausur-service) Hetzner (paddleocr-service)
│ HTTPS POST + Bild │
│ ──────────────────────────▶ │ PP-OCRv5 Latin
│ │ FastAPI (Port 8095)
│ JSON word_boxes │ API-Key Auth
│ ◀────────────────────────── │
Besonderheiten:
- Erzwingt automatisch
grid_method=words_first(full-page OCR, kein cell-crop) - Async HTTP-Client (
paddleocr_remote.py) mit 30s Timeout - Koordinaten sind bereits absolut (kein content_bounds Offset noetig)
- API-Key Authentifizierung ueber
X-API-KeyHeader - Dateien:
paddleocr-service/main.py,services/paddleocr_remote.py,cv_ocr_engines.py:ocr_region_paddle()
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 8: Strukturerkennung (Detail)
Erkennt Boxen, Zonen, Farbregionen und grafische Elemente auf der Seite. Laeuft nach der Worterkennung (Schritt 7), damit OCR-Wortpositionen fuer die Unterscheidung von Text vs. Grafik zur Verfuegung stehen.
Teilschritte
- Box-Erkennung (
cv_box_detect.py): Linien-Rahmen und farbige Hintergruende - Zonen-Aufteilung (
split_page_into_zones): Seite in Box- und Content-Zonen aufteilen - Farb-Analyse (
cv_color_detect.py): HSV-basierte Erkennung farbiger Textbereiche - Grafik-Erkennung (
cv_graphic_detect.py): Nicht-Text-Grafiken identifizieren
Grafik-Erkennung: Region-basierter Ansatz
Zwei Paesse trennen farbige Grafiken von farbigem Text und erkennen schwarze Illustrationen:
Pass 1 — Farbige Bildregionen:
- HSV-Saturation-Kanal extrahieren (Schwelle > 40)
- Schwarzer Text hat Saettigung ≈ 0 → unsichtbar auf diesem Kanal
- Starke Dilation (25×25 Ellipse) verschmilzt nahe Farbpixel zu Regionen
- Fuer jede Region: Wort-Ueberlappung pruefen
- > 50 % Ueberlappung mit OCR-Woertern → farbiger Text → ueberspringen
- ≤ 50 % → farbige Grafik/Bild → behalten
- Minimum 200 Farbpixel erforderlich (kein Rauschen)
- Regionen > 50 % der Bildbreite oder -hoehe → Seitenumfassend → ueberspringen
Pass 2 — Schwarze Illustrationen:
- Otsu-Binarisierung fuer Tinten-Maske
- Ausschlusszonen: OCR-Woerter (5 px Padding) + erkannte Boxen (8 px Inset)
- Farbige Pixel aus Pass 1 ebenfalls ausschliessen
- Nur Konturen mit Flaeche > 5000 px und min(Breite, Hoehe) > 40 px
Deduplizierung: Ueberlappende Elemente (> 50 % IoU der kleineren Bounding-Box) werden zusammengefasst. Ergebnis nach Flaeche absteigend sortiert.
Response-Format
{
"boxes": [
{"x": 50, "y": 300, "w": 1100, "h": 200, "confidence": 0.85,
"border_thickness": 3, "bg_color_name": "blue", "bg_color_hex": "#2563eb"}
],
"zones": [
{"index": 0, "zone_type": "content", "x": 50, "y": 50, "w": 1100, "h": 250},
{"index": 1, "zone_type": "box", "x": 50, "y": 300, "w": 1100, "h": 200}
],
"graphics": [
{"x": 100, "y": 500, "w": 150, "h": 120, "area": 8500,
"shape": "image", "color_name": "red", "color_hex": "#dc2626",
"confidence": 0.72}
],
"color_pixel_counts": {"red": 1234, "blue": 5678},
"has_words": true,
"word_count": 96,
"duration_seconds": 0.45
}
Grafik-Shape-Typen
| Shape | Quelle | Beschreibung |
|---|---|---|
image |
Pass 1 | Farbige Grafik/Bild (Ballons, Pfeile, Icons) |
illustration |
Pass 2 | Grosse schwarze Zeichnung/Illustration |
Erkannte Farben
red, orange, yellow, green, blue, purple, black
— basierend auf dem Median-Hue der saturierten Pixel in der Region.
Frontend-Anzeige
StepStructureDetection.tsx zeigt:
- Boxen-Liste mit Position, Hintergrundfarbe und Confidence
- Zonen-Uebersicht (Content vs. Box)
- Farb-Zusammenfassung (Pixel-Counts)
- Grafik-Liste mit Shape, Abmessungen, Farbe und Confidence
Schritt 9: Rekonstruktion (Detail)
Drei 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
Overlay-Modus (neu in v4.2)
Ganzseitige Tabellenrekonstruktion mit Pixel-basierter Wortpositionierung. Nur verfuegbar bei Parent-Sessions mit Sub-Sessions (Box-Bereiche).
Funktionsweise:
-
Sub-Session-Merging: Zellen aus Sub-Sessions werden koordinaten-konvertiert und in die Parent-Session eingefuegt. Die Umrechnung laeuft ueber die Box-Zone:
parentCellX = boxXPct + (subCell.bbox_pct.x / 100) * boxWPct parentCellY = boxYPct + (subCell.bbox_pct.y / 100) * boxHPct -
180°-Rotation: Bei Parent-Sessions mit Boxen wird das Bild standardmaessig 180° gedreht, da der Scan haeufig kopfueber vorliegt. Die Pixel-Analyse arbeitet auf dem rotierten Bild:
- Canvas:
ctx.translate(W, H); ctx.rotate(Math.PI) - Zell-Koordinaten:
(100 - x - w, 100 - y - h)fuer rotiertes Space - Cluster-Ruecktransformation:
start → cw-1-end, danachreverse()
- Canvas:
-
Pixel-Wortpositionierung: Der
usePixelWordPositionsHook analysiert dunkle Pixel per vertikaler Projektion, findet Wortgruppen-Cluster und berechnet die exakte horizontale Position + Auto-Schriftgroesse.
Layout: 50/50 Grid (links Originalbild, rechts Rekonstruktion)
Toolbar:
- Schriftgroessen-Slider (30–120%)
- Bold-Toggle
- 180°-Rotations-Toggle
- Speichern-Button
Visuelle Elemente:
- Spaltenlinien (aus
column_result.columns) - Zeilenlinien (aus
row_result.rows) - Box-Zonen-Markierung (blau, halbtransparent)
- Editierbare Inputs an Pixel-Positionen
Shared Hook: usePixelWordPositions
Extrahierter Hook fuer die Pixel-basierte Wortpositionierung, genutzt in StepLlmReview (Schritt 8) und StepReconstruction (Schritt 9).
function usePixelWordPositions(
imageUrl: string,
cells: GridCell[],
active: boolean,
rotation: 0 | 180 = 0,
): Map<string, WordPosition[]>
Algorithmus:
- Bild in offscreen Canvas laden (optional 180° gedreht)
- Pro Zelle:
getImageData()→ vertikale Projektion (dunkle Pixel pro Spalte) - Cluster-Erkennung (Schwelle: 3% der Zellhoehe, Gap: 2% der Zellbreite)
- Bei Rotation: Cluster zurueck ins Original-Koordinatensystem spiegeln
- Text-Gruppen (split bei 3+ Leerzeichen) auf Cluster matchen
- Auto-Schriftgroesse per
measureText()+fontRatio - Mode-Normalisierung: Haeufigste
fontRatio(gerundet auf 0.02) auf alle anwenden
Rueckgabe: Map<cell_id, WordPosition[]> mit xPct, wPct, yPct, hPct, text, fontRatio
!!! note "Per-Word Y-Positionierung (v4.3.1)"
WordPosition enthaelt seit v4.3.1 auch yPct und hPct. Dadurch rendert jedes
Wort an seiner tatsaechlichen vertikalen Position, statt alle Woerter einer Zelle
auf der Zell-Mitte zu stapeln. Bei Zellen ohne word_boxes (Fallback) werden
yPct/hPct aus cell.bbox_pct uebernommen.
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/"
OCR Overlay — Alternative Pipelines
URL: https://macmini:3002/ai/ocr-overlay
Neben der vollen 10-Schritt-Pipeline gibt es die OCR Overlay-Seite mit vereinfachten Pfaden fuer schnelle Ergebnisse. Alle drei Modi teilen die gleichen Vorverarbeitungsschritte (Orient → Deskew → Dewarp → Crop).
Modus-Uebersicht
| Modus | Schritte | Engine | Endpoint | Beschreibung |
|---|---|---|---|---|
| Pipeline | 7 | Tesseract | /words (SSE) |
Volle Pipeline: Zeilen + Woerter + Overlay |
| Paddle Direct | 5 | PaddleOCR | /paddle-direct |
PaddleOCR ersetzt Zeilen + Woerter + Overlay |
| Kombi | 5 | PaddleOCR + Tesseract | /paddle-kombi |
Beide Engines, Ergebnisse gemittelt |
Flussdiagramm
┌──────────────────────────────────────────────────────────────┐
│ GEMEINSAME VORVERARBEITUNG (alle 3 Modi) │
│ │
│ Schritt 1: Orientierung │
│ Schritt 2: Deskew │
│ Schritt 3: Dewarp │
│ Schritt 4: Crop │
└──────────────────┬────────────────────┬───────────────────────┘
│ │
┌───────────┼────────────────────┼────────────────┐
▼ ▼ ▼ ▼
PIPELINE PADDLE DIRECT KOMBI-MODUS
(7 Schritte) (5 Schritte) (5 Schritte)
│ │ │
Zeilen- PaddleOCR PaddleOCR
erkennung word_boxes + Tesseract
│ │ parallel
Woerter- build_grid_ │
erkennung from_words() _merge_paddle_
│ │ tesseract()
Overlay Overlay │
│ │ build_grid_
▼ ▼ from_words()
Ergebnis Ergebnis │
Overlay
│
Ergebnis
Paddle Direct
PaddleOCR laeuft auf dem vorverarbeiteten Bild und erkennt Woerter direkt.
Endpoint: POST /api/v1/ocr-pipeline/sessions/{id}/paddle-direct
Ablauf:
- Cropped/dewarped Bild laden (Prioritaet: cropped > dewarped > original)
ocr_region_paddle(img_bgr, region=None)aufrufenbuild_grid_from_words(word_dicts, img_w, img_h)fuer Grid-Erstellung- Cells mit
ocr_engine="paddle_direct"taggen - In DB speichern (
current_step=8)
Frontend: PaddleDirectStep.tsx — wiederverwendbare Komponente mit konfigurierbaren Props.
Kombi-Modus (PaddleOCR + Tesseract)
!!! info "Motivation"
PaddleOCR liefert gute Texterkennung, positioniert Woerter aber manchmal falsch
(z.B. !Betonung als ein Wort, Bullet Points nicht erkannt). Tesseract erkennt
Sonderzeichen besser und liefert feinere Word-Level-Boxen. Der Kombi-Modus
nutzt beide Engines und mittelt die Koordinaten.
Endpoint: POST /api/v1/ocr-pipeline/sessions/{id}/paddle-kombi
Ablauf:
- Cropped/dewarped Bild laden
- Parallel beide Engines aufrufen:
ocr_region_paddle(img_bgr, region=None)→paddle_wordspytesseract.image_to_data(pil_img, lang='eng+deu', config='--psm 6 --oem 3')→tess_words
- Merge:
_merge_paddle_tesseract(paddle_words, tess_words) build_grid_from_words(merged_words, img_w, img_h)fuer Grid- Cells mit
ocr_engine="kombi"taggen - In DB speichern
Merge-Algorithmus (v2: Row-Based Sequence Alignment)
!!! info "Rewrite (v4.3)"
Der Merge wurde von IoU-basiertem Matching auf Row-Based Sequence Alignment umgestellt.
Multi-Word Paddle-Boxen werden vor dem Merge in Einzelwoerter aufgeteilt
(_split_paddle_multi_words).
Ablauf:
- Row Grouping: Woerter beider Engines nach Y-Position in Zeilen gruppieren (12px Toleranz)
- Row Matching: Paddle- und Tesseract-Zeilen ueber vertikale Naehe zuordnen
- Sequence Alignment: Innerhalb jeder gematchten Zeile links-nach-rechts durchlaufen:
- Gleicher Text oder Substring-Match: Zusammenfuehren (Paddle-Text, gemittelte Koordinaten)
- Raeumlicher Overlap >= 50%: Auch bei unterschiedlichem Text als Duplikat behandeln
- Nur bei einer Engine: Wort beibehalten (falls Confidence >= 30)
- Ungematchte Zeilen: Paddle-Zeilen behalten, Tesseract-Zeilen nur mit Confidence >= 40
flowchart TD
A[Beide Engines] --> B[Row Grouping<br/>Y-Toleranz 12px]
B --> C[Row Matching<br/>vertikale Naehe]
C --> D{Gleicher Text<br/>oder Overlap >= 50%?}
D -->|Ja| E[Deduplizieren:<br/>Paddle-Text + gemittelte Coords]
D -->|Nein| F{Wort nur bei<br/>einer Engine?}
F -->|Ja| G[Beibehalten<br/>falls conf >= 30]
F -->|Nein| H[Beide behalten<br/>verschiedene Positionen]
Koordinaten-Mittelung:
merged_left = (paddle_left × paddle_conf + tess_left × tess_conf) / (paddle_conf + tess_conf)
Gleiches Prinzip fuer top, width, height. Der Text kommt immer von PaddleOCR (bessere Texterkennung).
Raeumlicher Overlap-Check (v4.3.1):
Wenn zwei Woerter >= 50% horizontal ueberlappen, werden sie als dasselbe physische Wort behandelt — unabhaengig davon, ob die OCR-Texte unterschiedlich sind (z.B. "hello" vs "helo"). Dies verhindert, dass leicht unterschiedliche Erkennungen als separate Woerter uebereinander im Overlay erscheinen.
Dateien
| Datei | Aenderung |
|---|---|
ocr_pipeline_api.py |
_split_paddle_multi_words(), _group_words_into_rows(), _merge_row_sequences(), _merge_paddle_tesseract(), /paddle-kombi Endpoint |
admin-lehrer/.../ocr-overlay/types.ts |
KOMBI_STEPS Konstante |
admin-lehrer/.../ocr-overlay/useSlideWordPositions.ts |
Slide-Positionierung mit yPct/hPct |
admin-lehrer/.../ocr-overlay/usePixelWordPositions.ts |
Pixel-Cluster-Positionierung mit yPct/hPct |
admin-lehrer/.../ocr-overlay/OverlayReconstruction.tsx |
Rendering mit per-Word Y-Positionen |
admin-lehrer/.../PaddleDirectStep.tsx |
Wiederverwendbar mit endpoint/engineKey Props |
admin-lehrer/.../ocr-overlay/page.tsx |
3er-Toggle: Pipeline / Paddle Direct / Kombi |
Tests
cd klausur-service/backend && pytest tests/test_paddle_kombi.py -v # 36 Tests
Test-Klassen:
| Klasse | Tests | Beschreibung |
|---|---|---|
TestSplitPaddleMultiWords |
7 | Multi-Word-Box-Splitting |
TestGroupWordsIntoRows |
5 | Y-Position Row Clustering |
TestMergeRowSequences |
10 | Sequence Alignment innerhalb einer Zeile |
TestMergePaddleTesseract |
8 | Vollstaendiger Merge mit Row-Matching |
TestMergeRealWorldRegression |
1 | Regression mit Echtdaten |
TestSpatialOverlapDedup |
4 | Raeumliche Overlap-Deduplizierung |
TestSplitThenMerge |
1 | Split + Merge End-to-End |
| Testklasse | Tests | Beschreibung |
|---|---|---|
TestBoxIoU |
6 | IoU-Berechnung: identisch, kein Overlap, teilweise, enthalten, Kante, Null-Flaeche |
TestMergePaddleTesseract |
10 | Merge: Match-Averaging, kein Match, Low-Conf-Drop, leer, IoU-Schwelle, Text-Praeferenz, Zero-Conf |
TestMergePaddleTesseractBulletPoints |
2 | Bullet-Points und Sonderzeichen von Tesseract |
Aenderungshistorie
| Datum | Version | Aenderung |
|---|---|---|
| 2026-03-16 | 4.6.0 | Strukturerkennung (Schritt 8): Region-basierte Grafikerkennung (cv_graphic_detect.py) mit Zwei-Pass-Verfahren (Farbregionen + schwarze Illustrationen), Wort-Ueberlappungs-Filter, Box/Zonen/Farb-Analyse. Schritt laeuft nach Worterkennung. |
| 2026-03-12 | 4.5.0 | Kombi-Modus (PaddleOCR + Tesseract): Beide Engines laufen parallel, Koordinaten werden IoU-basiert gematcht und confidence-gewichtet gemittelt. Ungematchte Tesseract-Woerter (Bullets, Symbole) werden hinzugefuegt. 3er-Toggle in OCR Overlay. |
| 2026-03-12 | 4.4.0 | PaddleOCR Remote-Engine (engine=paddle): PP-OCRv5 Latin auf Hetzner x86_64. Neuer Microservice (paddleocr-service/), HTTP-Client (paddleocr_remote.py), Frontend-Dropdown-Option. Nutzt words_first Grid-Methode. |
| 2026-03-12 | 4.3.0 | Words-First Grid Builder (cv_words_first.py): Bottom-up-Algorithmus clustert Tesseract word_boxes direkt zu Spalten/Zeilen/Zellen. Neuer grid_method Parameter im /words Endpoint. Frontend-Toggle in StepWordRecognition. |
| 2026-03-10 | 4.2.0 | Rekonstruktion: Overlay-Modus mit Pixel-Wortpositionierung, 180°-Rotation, Sub-Session-Merging, usePixelWordPositions Hook, Box-Boundary-Schutz (box_ranges_inner) |
| 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 |