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
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:
@@ -1,6 +1,6 @@
|
||||
# OCR Pipeline - Schrittweise Seitenrekonstruktion
|
||||
|
||||
**Version:** 4.1.0
|
||||
**Version:** 4.3.0
|
||||
**Status:** Produktiv (Schritte 1–10 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 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:**
|
||||
|
||||
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 (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).
|
||||
|
||||
```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 |
|
||||
|
||||
Reference in New Issue
Block a user