Files
breakpilot-lehrer/admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx
Benjamin Admin 589d2f811a feat: Dewarp-Korrektur als Schritt 2 in OCR Pipeline (7 Schritte)
Implementiert Buchwoelbungs-Entzerrung mit zwei Methoden:
- Methode A: Vertikale-Kanten-Analyse (Sobel + Polynom 2. Grades)
- Methode B: Textzeilen-Baseline (Tesseract + Baseline-Kruemmung)
Beste Methode wird automatisch gewaehlt, manueller Slider (-3 bis +3).

Backend: 3 neue Endpoints (auto/manual dewarp, ground truth)
Frontend: StepDewarp + DewarpControls, Pipeline von 6 auf 7 Schritte

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:46:41 +01:00

139 lines
4.0 KiB
TypeScript

'use client'
import { useState } from 'react'
const A4_WIDTH_MM = 210
const A4_HEIGHT_MM = 297
interface ImageCompareViewProps {
originalUrl: string | null
deskewedUrl: string | null
showGrid: boolean
showBinarized: boolean
binarizedUrl: string | null
leftLabel?: string
rightLabel?: string
}
function MmGridOverlay() {
const lines: React.ReactNode[] = []
// Vertical lines every 10mm
for (let mm = 0; mm <= A4_WIDTH_MM; mm += 10) {
const x = (mm / A4_WIDTH_MM) * 100
const is50 = mm % 50 === 0
lines.push(
<line
key={`v-${mm}`}
x1={x} y1={0} x2={x} y2={100}
stroke={is50 ? 'rgba(59, 130, 246, 0.4)' : 'rgba(59, 130, 246, 0.15)'}
strokeWidth={is50 ? 0.12 : 0.05}
/>
)
// Label every 50mm
if (is50 && mm > 0) {
lines.push(
<text key={`vl-${mm}`} x={x} y={1.2} fill="rgba(59,130,246,0.6)" fontSize="1.2" textAnchor="middle">
{mm}
</text>
)
}
}
// Horizontal lines every 10mm
for (let mm = 0; mm <= A4_HEIGHT_MM; mm += 10) {
const y = (mm / A4_HEIGHT_MM) * 100
const is50 = mm % 50 === 0
lines.push(
<line
key={`h-${mm}`}
x1={0} y1={y} x2={100} y2={y}
stroke={is50 ? 'rgba(59, 130, 246, 0.4)' : 'rgba(59, 130, 246, 0.15)'}
strokeWidth={is50 ? 0.12 : 0.05}
/>
)
if (is50 && mm > 0) {
lines.push(
<text key={`hl-${mm}`} x={0.5} y={y + 0.6} fill="rgba(59,130,246,0.6)" fontSize="1.2">
{mm}
</text>
)
}
}
return (
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="absolute inset-0 w-full h-full pointer-events-none"
style={{ zIndex: 10 }}
>
<g style={{ pointerEvents: 'none' }}>{lines}</g>
</svg>
)
}
export function ImageCompareView({
originalUrl,
deskewedUrl,
showGrid,
showBinarized,
binarizedUrl,
leftLabel,
rightLabel,
}: ImageCompareViewProps) {
const [leftError, setLeftError] = useState(false)
const [rightError, setRightError] = useState(false)
const rightUrl = showBinarized && binarizedUrl ? binarizedUrl : deskewedUrl
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: Original */}
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">{leftLabel || 'Original (unbearbeitet)'}</h3>
<div className="relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700"
style={{ aspectRatio: '210/297' }}>
{originalUrl && !leftError ? (
<img
src={originalUrl}
alt="Original Scan"
className="w-full h-full object-contain"
onError={() => setLeftError(true)}
/>
) : (
<div className="flex items-center justify-center h-full text-gray-400">
{leftError ? 'Fehler beim Laden' : 'Noch kein Bild'}
</div>
)}
</div>
</div>
{/* Right: Deskewed with Grid */}
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
{rightLabel || `${showBinarized ? 'Binarisiert' : 'Begradigt'}${showGrid ? ' + Raster (mm)' : ''}`}
</h3>
<div className="relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700"
style={{ aspectRatio: '210/297' }}>
{rightUrl && !rightError ? (
<>
<img
src={rightUrl}
alt="Begradigtes Bild"
className="w-full h-full object-contain"
onError={() => setRightError(true)}
/>
{showGrid && <MmGridOverlay />}
</>
) : (
<div className="flex items-center justify-center h-full text-gray-400">
{rightError ? 'Fehler beim Laden' : 'Begradigung laeuft...'}
</div>
)}
</div>
</div>
</div>
)
}