feat: Words-First Grid Builder (bottom-up alternative zu cell_grid_v2)
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 54s
CI / test-go-edu-search (push) Successful in 47s
CI / test-python-klausur (push) Failing after 2m31s
CI / test-python-agent-core (push) Successful in 23s
CI / test-nodejs-website (push) Successful in 32s

Neuer Algorithmus in cv_words_first.py: Clustert Tesseract word_boxes
direkt zu Spalten (X-Gap) und Zeilen (Y-Proximity), baut Zellen an
Schnittpunkten. Kein Spalten-/Zeilenerkennung noetig.

- cv_words_first.py: _cluster_columns, _cluster_rows, _build_cells, build_grid_from_words
- ocr_pipeline_api.py: grid_method Parameter (v2|words_first) im /words Endpoint
- StepWordRecognition.tsx: Dropdown Toggle fuer Grid-Methode
- OCR-Pipeline.md: Doku v4.3.0 mit Words-First Algorithmus
- 15 Unit-Tests fuer cv_words_first

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-12 06:46:05 +01:00
parent 2fdf3ff868
commit ced5bb3dd3
6 changed files with 854 additions and 34 deletions

View File

@@ -1,6 +1,6 @@
# OCR Pipeline - Schrittweise Seitenrekonstruktion
**Version:** 4.1.0
**Version:** 4.3.0
**Status:** Produktiv (Schritte 110 implementiert)
**URL:** https://macmini:3002/ai/ocr-pipeline
@@ -22,7 +22,7 @@ Jeder Schritt kann individuell geprueft, korrigiert und mit Ground-Truth-Daten v
| 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 |
| 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 |
@@ -82,28 +82,29 @@ flowchart TD
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
│ │
└───────────────────────────┴────────────────────┘
┌──────────────────┼──────────────────┐
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.)
@@ -147,6 +148,8 @@ klausur-service/backend/
│ └── 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_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
@@ -169,7 +172,8 @@ admin-lehrer/
├── StepRowDetection.tsx # Schritt 6: Zeilenerkennung
├── StepWordRecognition.tsx # Schritt 7: Worterkennung
├── StepLlmReview.tsx # Schritt 8: Korrektur (SSE-Stream)
├── StepReconstruction.tsx # Schritt 9: Rekonstruktion (Canvas)
├── StepReconstruction.tsx # Schritt 9: Rekonstruktion (Canvas + Overlay)
├── usePixelWordPositions.ts # Shared Hook: Pixel-basierte Wortpositionierung
├── FabricReconstructionCanvas.tsx # Fabric.js Editor
└── StepGroundTruth.tsx # Schritt 10: Validierung
```
@@ -257,10 +261,20 @@ Alle Endpoints unter `/api/v1/ocr-pipeline/`.
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `POST` | `/sessions/{id}/words` | Wort-Grid aus Spalten x Zeilen erstellen |
| `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` |
| `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: Korrektur
| Methode | Pfad | Beschreibung |
@@ -513,6 +527,12 @@ Horizontale Projektionsprofile finden Zeilen-Luecken; word-level Validierung ver
der entstehenden Luecke ausgedehnt, damit kein Zeileninhalt durch schrumpfende Grenzen
abgeschnitten wird.
3. **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 um `max(border_thickness, 5px)`
geschrumpft, sodass nur Zeilen **innerhalb** der Box ausgeschlossen werden.
```python
def _is_artifact_row(row: RowGeometry) -> bool:
"""Zeile ist Artefakt wenn alle Tokens <= 1 Zeichen."""
@@ -524,13 +544,128 @@ def _heal_row_gaps(rows, top_bound, bottom_bound):
...
```
### 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`):
1. Box-Zonen identifizieren, `box_ranges_inner` berechnen (geschrumpft um Border-Dicke)
2. Content-Strips = Seitenbereiche **ohne** Box-Inneres, vertikal gestapelt
3. Zeilenerkennung auf gestapeltem Bild, Y-Koordinaten zurueckgemappt
4. 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 — Hybrid-Grid (Detail)
## Schritt 7: Worterkennung (Detail)
### Algorithmus: `build_cell_grid_v2()`
Schritt 7 bietet zwei Grid-Strategien, auswaehlbar per `grid_method`-Parameter:
Schritt 5 nutzt eine **Hybrid-Strategie**: Breite Spalten verwenden die Full-Page-Tesseract-Woerter,
| 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
1. Alle Woerter nach X-Mitte sortieren
2. Aufeinanderfolgende X-Gaps berechnen
3. Adaptiver Schwellwert: `median_word_height × 3` (min 3% Bildbreite)
4. Gaps > Schwellwert = Spaltengrenzen
5. Kein Gap gefunden → 1 Spalte (`column_text`)
6. Spaltentypen: `column_1`, `column_2`, ... (generisch, positionsbasiert)
#### Zeilen-Clustering
1. Woerter zu visuellen Zeilen gruppieren (Y-Toleranz: halbe Worthoehe)
2. Jede visuelle Zeile = eine Zeile im Grid
3. 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?"
@@ -692,7 +827,7 @@ Change-Format:
## Schritt 9: Rekonstruktion (Detail)
Zwei Modi verfuegbar:
Drei Modi verfuegbar:
### Einfacher Modus
@@ -709,6 +844,73 @@ angezeigt, alle Grid-Zellen (auch leere!) werden als editierbare Textfelder daru
- Zoom 50200 %
- 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:**
1. **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
```
2. **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`, danach `reverse()`
3. **Pixel-Wortpositionierung:** Der `usePixelWordPositions` Hook 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 (30120%)
- 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).
```typescript
function usePixelWordPositions(
imageUrl: string,
cells: GridCell[],
active: boolean,
rotation: 0 | 180 = 0,
): Map<string, WordPosition[]>
```
**Algorithmus:**
1. Bild in offscreen Canvas laden (optional 180° gedreht)
2. Pro Zelle: `getImageData()` → vertikale Projektion (dunkle Pixel pro Spalte)
3. Cluster-Erkennung (Schwelle: 3% der Zellhoehe, Gap: 2% der Zellbreite)
4. Bei Rotation: Cluster zurueck ins Original-Koordinatensystem spiegeln
5. Text-Gruppen (split bei 3+ Leerzeichen) auf Cluster matchen
6. Auto-Schriftgroesse per `measureText()` + `fontRatio`
7. Mode-Normalisierung: Haeufigste `fontRatio` (gerundet auf 0.02) auf alle anwenden
**Rueckgabe:** `Map<cell_id, WordPosition[]>` mit `xPct`, `wPct`, `text`, `fontRatio`
### Fabric.js Editor
Erweiterter Canvas-Editor (`FabricReconstructionCanvas.tsx`):
@@ -861,6 +1063,8 @@ ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/brea
| Datum | Version | Aenderung |
|-------|---------|----------|
| 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 |