- Authors

- Name
- Phillip Pham
- @ddppham
TL;DR
- Vision Language Models (VLMs) uebertreffen klassische OCR deutlich bei komplexen Layouts, Tabellen und handschriftlichen Dokumenten.
- Eine produktive Pipeline kombiniert Preprocessing (Super-Resolution, Denoising), VLM-Analyse und Post-Processing in containerisierten Microservices.
- GPU-Workloads auf Kubernetes erfordern den NVIDIA Device Plugin, passende Resource Requests und durchdachtes Scheduling.
- Startet mit einem FastAPI-Service hinter einem Kubernetes Deployment mit HPA. Skaliert GPU-Pods horizontal basierend auf Queue-Laenge.
- Messt Qualitaet quantitativ: Word Accuracy, BLEU-Score und Layout-Preservation sind die relevanten Metriken.
Das Problem mit klassischer OCR
Wer schon mal Tesseract auf ein eingescanntes Formular aus den 90ern losgelassen hat, kennt das Ergebnis: Buchstabensalat, kaputte Tabellen, fehlende Absaetze. Klassische OCR arbeitet zeichenbasiert und versteht weder Layout noch Kontext.
Typische Schwaechen:
- Handschriftliche oder schlecht gedruckte Texte werden falsch erkannt
- Tabellenstrukturen gehen verloren (Zellen werden zu Fliesstext)
- Mehrspaltige Layouts werden in falscher Reihenfolge gelesen
- Bilder, Diagramme und Formulare werden ignoriert
Vision Language Models loesen diese Probleme, weil sie Dokumente als Bild verstehen und gleichzeitig Text-Semantik anwenden koennen.
VLM-basierte Pipeline: Architektur
Eine produktive PDF-Pipeline besteht aus drei Stufen:
1. Preprocessing: Bildqualitaet verbessern, bevor das VLM ueberhaupt sieht. 2. VLM-Analyse: Layout-Erkennung, Textextraktion, Tabellen-Parsing. 3. Post-Processing: Ergebnisse validieren, Format aufbereiten, Konfidenz berechnen.
Preprocessing: Super-Resolution und Denoising
Schlechte Scan-Qualitaet ist der haeufigste Grund fuer schlechte Ergebnisse. Bevor ihr ein teures VLM drauf ansetzt, verbessert das Eingabebild:
# preprocessing.py
import fitz # PyMuPDF
from PIL import Image, ImageFilter, ImageEnhance
import numpy as np
def extract_page_as_image(pdf_path: str, page_num: int, zoom: float = 2.0) -> Image.Image:
"""Extrahiert eine PDF-Seite als hochaufgeloestes Bild."""
doc = fitz.open(pdf_path)
page = doc.load_page(page_num)
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
doc.close()
return img
def enhance_for_ocr(image: Image.Image) -> Image.Image:
"""Optimiert ein Bild fuer nachfolgende OCR/VLM-Verarbeitung."""
# Kontrast erhoehen
enhancer = ImageEnhance.Contrast(image)
image = enhancer.enhance(1.5)
# Schaerfen
image = image.filter(ImageFilter.SHARPEN)
# In Graustufen konvertieren und zurueck (reduziert Rauschen)
gray = image.convert("L")
# Adaptive Binarisierung per Schwellwert
threshold = np.mean(np.array(gray))
binary = gray.point(lambda x: 255 if x > threshold else 0)
return binary.convert("RGB")
VLM-Analyse mit TrOCR und LayoutLMv3
Fuer die eigentliche Textextraktion kombinieren wir zwei Modelle:
- TrOCR (microsoft/trocr-base-handwritten): Transformer-basierte OCR, besonders stark bei Handschrift
- LayoutLMv3 (microsoft/layoutlmv3-base): Versteht Dokumenten-Layouts und kann Regionen klassifizieren
# vlm_processor.py
from transformers import TrOCRProcessor, VisionEncoderDecoderModel
from transformers import LayoutLMv3Processor, LayoutLMv3ForSequenceClassification
import torch
from PIL import Image
from typing import List, Dict
class DocumentProcessor:
def __init__(self, device: str = None):
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
# TrOCR fuer Texterkennung
self.trocr_processor = TrOCRProcessor.from_pretrained(
"microsoft/trocr-base-handwritten"
)
self.trocr_model = VisionEncoderDecoderModel.from_pretrained(
"microsoft/trocr-base-handwritten"
).to(self.device)
# LayoutLMv3 fuer Dokumentenverstaendnis
self.layout_processor = LayoutLMv3Processor.from_pretrained(
"microsoft/layoutlmv3-base"
)
self.layout_model = LayoutLMv3ForSequenceClassification.from_pretrained(
"microsoft/layoutlmv3-base"
).to(self.device)
def extract_text(self, image: Image.Image) -> str:
"""Extrahiert Text aus einem Bild per TrOCR."""
pixel_values = self.trocr_processor(
image, return_tensors="pt"
).pixel_values.to(self.device)
with torch.no_grad():
generated_ids = self.trocr_model.generate(pixel_values, max_new_tokens=512)
return self.trocr_processor.batch_decode(
generated_ids, skip_special_tokens=True
)[0]
def classify_regions(self, image: Image.Image) -> List[Dict]:
"""Klassifiziert Regionen eines Dokuments (Header, Table, Text, etc.)."""
encoding = self.layout_processor(
image, return_tensors="pt", truncation=True
)
encoding = {k: v.to(self.device) for k, v in encoding.items()}
with torch.no_grad():
outputs = self.layout_model(**encoding)
predictions = torch.softmax(outputs.logits, dim=1)
labels = ["header", "footer", "title", "text", "table",
"figure", "list", "form", "signature"]
return [
{"label": labels[i], "confidence": predictions[0][i].item()}
for i in range(len(labels))
if predictions[0][i].item() > 0.1
]
Kubernetes-Deployment fuer GPU-Workloads
KI-Modelle brauchen GPUs. Auf Kubernetes managt ihr GPU-Zugang ueber den NVIDIA Device Plugin und Resource Requests.
Deployment-Manifest
# pdf-processor-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pdf-processor
namespace: document-ai
spec:
replicas: 2
selector:
matchLabels:
app: pdf-processor
template:
metadata:
labels:
app: pdf-processor
spec:
containers:
- name: processor
image: registry.example.com/pdf-processor:v1.2.0
ports:
- containerPort: 8000
resources:
requests:
cpu: "2"
memory: "8Gi"
nvidia.com/gpu: "1"
limits:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
env:
- name: MODEL_CACHE_DIR
value: "/models"
- name: WORKERS
value: "2"
volumeMounts:
- name: model-cache
mountPath: /models
- name: upload-volume
mountPath: /data/uploads
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
volumes:
- name: model-cache
persistentVolumeClaim:
claimName: model-cache-pvc
- name: upload-volume
persistentVolumeClaim:
claimName: upload-pvc
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
nodeSelector:
accelerator: nvidia-a100
---
apiVersion: v1
kind: Service
metadata:
name: pdf-processor
namespace: document-ai
spec:
selector:
app: pdf-processor
ports:
- port: 80
targetPort: 8000
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: pdf-processor-hpa
namespace: document-ai
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: pdf-processor
minReplicas: 1
maxReplicas: 6
metrics:
- type: Pods
pods:
metric:
name: queue_depth
target:
type: AverageValue
averageValue: "5"
Wichtige Details:
- nvidia.com/gpu: "1" in den Resource Requests stellt sicher, dass der Pod auf einem Node mit GPU scheduliert wird.
- tolerations und nodeSelector sorgen dafuer, dass GPU-Pods nur auf GPU-Nodes landen.
- Der HPA skaliert basierend auf einer Custom Metric (Queue-Tiefe), nicht auf CPU. Das ist bei GPU-Workloads sinnvoller.
- Model-Cache per PVC: Modelle werden nur einmal heruntergeladen und persistent gespeichert. Das spart Startup-Zeit bei Pod-Restarts.
API-Service mit FastAPI
Der eigentliche Service exponiert eine REST-API und verarbeitet PDFs asynchron:
# api.py
from fastapi import FastAPI, UploadFile, File, BackgroundTasks
from fastapi.responses import JSONResponse
import uuid
import os
import redis
import json
app = FastAPI(title="PDF Quality Enhancement API")
cache = redis.Redis(host=os.getenv("REDIS_HOST", "redis"), port=6379, db=0)
processor = DocumentProcessor()
@app.post("/process")
async def process_pdf(background_tasks: BackgroundTasks, file: UploadFile = File(...)):
"""Nimmt ein PDF entgegen und startet asynchrone Verarbeitung."""
task_id = str(uuid.uuid4())
upload_path = f"/data/uploads/{task_id}.pdf"
with open(upload_path, "wb") as f:
content = await file.read()
f.write(content)
cache.set(f"task:{task_id}", json.dumps({"status": "queued"}))
background_tasks.add_task(run_pipeline, upload_path, task_id)
return {"task_id": task_id, "status": "queued"}
@app.get("/status/{task_id}")
async def get_status(task_id: str):
"""Gibt den Verarbeitungsstatus zurueck."""
result = cache.get(f"task:{task_id}")
if not result:
return JSONResponse(status_code=404, content={"error": "Task nicht gefunden"})
return json.loads(result)
@app.get("/health")
async def health():
return {"status": "ok", "gpu_available": torch.cuda.is_available()}
async def run_pipeline(pdf_path: str, task_id: str):
"""Verarbeitet ein PDF durch die gesamte Pipeline."""
try:
cache.set(f"task:{task_id}", json.dumps({"status": "processing"}))
from preprocessing import extract_page_as_image, enhance_for_ocr
doc = fitz.open(pdf_path)
results = []
for page_num in range(len(doc)):
image = extract_page_as_image(pdf_path, page_num)
enhanced = enhance_for_ocr(image)
text = processor.extract_text(enhanced)
regions = processor.classify_regions(enhanced)
results.append({
"page": page_num + 1,
"text": text,
"regions": regions
})
doc.close()
output = {"status": "completed", "pages": len(results), "results": results}
cache.set(f"task:{task_id}", json.dumps(output))
except Exception as e:
cache.set(f"task:{task_id}", json.dumps({"status": "failed", "error": str(e)}))
Qualitaet messen
Subjektive Bewertung reicht nicht. Definiert quantitative Metriken:
| Metrik | Was sie misst | Zielwert |
|---|---|---|
| Word Accuracy | Anteil korrekt erkannter Woerter | > 95% |
| Character Error Rate (CER) | Fehlerrate auf Zeichenebene | < 3% |
| BLEU Score | Aehnlichkeit zum Referenztext | > 0.85 |
| Table Structure Accuracy | Korrekte Zeilen/Spalten-Zuordnung | > 90% |
| Layout Preservation | Strukturelle Aehnlichkeit zum Original | > 85% |
Erstellt ein Ground-Truth-Dataset mit 50-100 manuell transkribierten Dokumenten und evaluiert automatisiert nach jedem Modell-Update.
Vergleich: Klassische OCR vs. VLM
| Kriterium | Tesseract (klassisch) | TrOCR + LayoutLMv3 |
|---|---|---|
| Gedruckter Text (sauber) | 95%+ Accuracy | 98%+ Accuracy |
| Handschrift | 40-60% | 85%+ |
| Tabellen | Struktur geht verloren | Struktur bleibt erhalten |
| Mehrspaltig | Falsche Lesereihenfolge | Korrekte Regionen-Erkennung |
| Formulare | Nur Text, keine Felder | Feld-Label-Zuordnung |
| Geschwindigkeit | Sehr schnell (CPU) | Langsamer (GPU empfohlen) |
| Infrastruktur-Kosten | Minimal | GPU-Nodes noetig |
VLMs sind nicht immer die richtige Wahl. Fuer saubere, gedruckte Dokumente ist Tesseract schneller und guenstiger. Die Staerke der VLMs zeigt sich bei schwierigen Dokumenten: schlechte Scans, Handschrift, komplexe Layouts.
Typische Fehler beim Deployment
Modelle im Container-Image baken. KI-Modelle sind mehrere GB gross. Wenn sie im Image liegen, wird jeder Build und Pull langsam. Nutzt stattdessen ein PersistentVolume oder einen Model-Registry-Service.
Keine GPU-Limits setzen. Ohne explizite GPU-Limits kann ein Pod alle GPUs eines Nodes belegen. Setzt immer nvidia.com/gpu: "1" (oder den gewuenschten Wert).
Synchrone Verarbeitung. PDFs koennen Hunderte Seiten haben. Synchrone API-Calls fuehren zu Timeouts. Nutzt asynchrone Verarbeitung mit einer Queue (Redis, RabbitMQ) und einem separaten Worker-Deployment.
Weitergehende Artikel
- GPU-Workloads auf Kubernetes
- Kubernetes AI und Machine Learning
- MLOps mit Kubernetes
- Kubernetes Storage und Persistent Volumes
Ihr wollt eine KI-basierte Dokumentenverarbeitung aufsetzen und braucht Unterstuetzung beim Kubernetes-Setup, GPU-Scheduling oder der Pipeline-Architektur? Meldet euch unter /kontakt.
Kubernetes-Beratung gesucht?
Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.
📖 Verwandte Artikel
Weitere interessante Beiträge zu ähnlichen Themen
Intelligente Dokumentenverarbeitung mit Kubernetes in Deutschland: IDP-Pipelines für den Mittelstand
Revolutionieren Sie die Dokumentenverarbeitung in Ihrem deutschen Mittelstandsunternehmen! Erfahren Sie, wie skalierbare IDP-Pipelines mit OCR und KI auf Kubernetes-Plattformen in Deutschland manuelle Prozesse automatisieren, Kosten senken und die Datenqualität signifikant verbessern – DSGVO-konform und effizient.
Kubernetes Deutschland: KI-gestützte Dokumentzusammenfassung effizient verarbeiten
Automatisieren Sie die KI-gestützte Dokumentzusammenfassung mit Kubernetes Deutschland und steigern Sie die Effizienz Ihrer Textverarbeitung im Mittelstand.
KI-Qualitätskontrolle auf Kubernetes mit YOLO
KI-gestützte Qualitätskontrolle auf Kubernetes: Kamera-zu-Inference-Pipeline mit YOLO und Triton Inference Server für automatisierte Defekterkennung.
YOLO Inference auf Kubernetes: GPU-Scheduling und Autoscaling
YOLO-Modelle als skalierbare Inference-Services auf Kubernetes deployen: NVIDIA Device Plugin, GPU-Scheduling, HPA mit Custom Metrics und Triton Serving.
Kubeflow auf Kubernetes: MLOps-Pipelines einrichten
Kubeflow auf Kubernetes deckt den gesamten ML-Lifecycle ab: von Jupyter Notebooks über Pipelines bis Model Serving mit KServe und Katib.