Files
breakpilot-lehrer/admin-lehrer/components/ocr-pipeline/DewarpControls.tsx
Benjamin Admin 09b820efbe refactor(dewarp): replace displacement map with affine shear correction
The old displacement-map approach shifted entire rows by a parabolic
profile, creating a circle/barrel distortion. The actual problem is
a linear vertical shear: after deskew aligns horizontal lines, the
vertical column edges are still tilted by ~0.5°.

New approach:
- Detect shear angle from strongest vertical edge slope (not curvature)
- Apply cv2.warpAffine shear to straighten vertical features
- Manual slider: -2.0° to +2.0° in 0.05° steps
- Slider initializes to auto-detected shear angle
- Ground truth question: "Spalten vertikal ausgerichtet?"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 18:23:04 +01:00

202 lines
7.7 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import type { DewarpResult, DewarpGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types'
interface DewarpControlsProps {
dewarpResult: DewarpResult | null
showGrid: boolean
onToggleGrid: () => void
onManualDewarp: (shearDegrees: number) => void
onGroundTruth: (gt: DewarpGroundTruth) => void
onNext: () => void
isApplying: boolean
}
const METHOD_LABELS: Record<string, string> = {
vertical_edge: 'Vertikale Kanten',
manual: 'Manuell',
none: 'Keine Korrektur',
}
export function DewarpControls({
dewarpResult,
showGrid,
onToggleGrid,
onManualDewarp,
onGroundTruth,
onNext,
isApplying,
}: DewarpControlsProps) {
const [manualShear, setManualShear] = useState(0)
const [gtFeedback, setGtFeedback] = useState<'correct' | 'incorrect' | null>(null)
const [gtNotes, setGtNotes] = useState('')
const [gtSaved, setGtSaved] = useState(false)
// Initialize slider to auto-detected value when result arrives
useEffect(() => {
if (dewarpResult && dewarpResult.shear_degrees !== undefined) {
setManualShear(dewarpResult.shear_degrees)
}
}, [dewarpResult?.shear_degrees])
const handleGroundTruth = (isCorrect: boolean) => {
setGtFeedback(isCorrect ? 'correct' : 'incorrect')
if (isCorrect) {
onGroundTruth({ is_correct: true })
setGtSaved(true)
}
}
const handleGroundTruthIncorrect = () => {
onGroundTruth({
is_correct: false,
corrected_shear: manualShear !== 0 ? manualShear : undefined,
notes: gtNotes || undefined,
})
setGtSaved(true)
}
return (
<div className="space-y-4">
{/* Results */}
{dewarpResult && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="flex flex-wrap items-center gap-3 text-sm">
<div>
<span className="text-gray-500">Scherung:</span>{' '}
<span className="font-mono font-medium">{dewarpResult.shear_degrees}°</span>
</div>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
<div>
<span className="text-gray-500">Methode:</span>{' '}
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300">
{METHOD_LABELS[dewarpResult.method_used] || dewarpResult.method_used}
</span>
</div>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
<div>
<span className="text-gray-500">Konfidenz:</span>{' '}
<span className="font-mono">{Math.round(dewarpResult.confidence * 100)}%</span>
</div>
</div>
{/* Toggle */}
<div className="flex gap-3 mt-3">
<button
onClick={onToggleGrid}
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
showGrid
? 'bg-teal-100 border-teal-300 text-teal-700 dark:bg-teal-900/40 dark:border-teal-600 dark:text-teal-300'
: 'border-gray-300 text-gray-500 dark:border-gray-600 dark:text-gray-400'
}`}
>
Raster anzeigen
</button>
</div>
</div>
)}
{/* Manual shear angle slider */}
{dewarpResult && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Scherwinkel (manuell)</div>
<div className="flex items-center gap-3">
<span className="text-xs text-gray-400 w-10 text-right">-2.0°</span>
<input
type="range"
min={-200}
max={200}
step={5}
value={Math.round(manualShear * 100)}
onChange={(e) => setManualShear(parseInt(e.target.value) / 100)}
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500"
/>
<span className="text-xs text-gray-400 w-10">+2.0°</span>
<span className="font-mono text-sm w-16 text-right">{manualShear.toFixed(2)}°</span>
<button
onClick={() => onManualDewarp(manualShear)}
disabled={isApplying}
className="px-3 py-1.5 text-sm bg-teal-600 text-white rounded-md hover:bg-teal-700 disabled:opacity-50 transition-colors"
>
{isApplying ? '...' : 'Anwenden'}
</button>
</div>
<p className="text-xs text-gray-400 mt-1">
Scherung der vertikalen Achse in Grad. Positiv = Spalten nach rechts kippen, negativ = nach links.
</p>
</div>
)}
{/* Ground Truth */}
{dewarpResult && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Spalten vertikal ausgerichtet?
</div>
<p className="text-xs text-gray-400 mb-2">Pruefen ob die Spaltenraender jetzt senkrecht zum Raster stehen.</p>
{!gtSaved ? (
<div className="space-y-3">
<div className="flex gap-2">
<button
onClick={() => handleGroundTruth(true)}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
gtFeedback === 'correct'
? 'bg-green-100 text-green-700 ring-2 ring-green-400'
: 'bg-gray-100 text-gray-600 hover:bg-green-50 dark:bg-gray-700 dark:text-gray-300'
}`}
>
Ja
</button>
<button
onClick={() => handleGroundTruth(false)}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
gtFeedback === 'incorrect'
? 'bg-red-100 text-red-700 ring-2 ring-red-400'
: 'bg-gray-100 text-gray-600 hover:bg-red-50 dark:bg-gray-700 dark:text-gray-300'
}`}
>
Nein
</button>
</div>
{gtFeedback === 'incorrect' && (
<div className="space-y-2">
<textarea
value={gtNotes}
onChange={(e) => setGtNotes(e.target.value)}
placeholder="Notizen zur Korrektur..."
className="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md p-2 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200"
rows={2}
/>
<button
onClick={handleGroundTruthIncorrect}
className="text-sm px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
>
Feedback speichern
</button>
</div>
)}
</div>
) : (
<div className="text-sm text-green-600 dark:text-green-400">
Feedback gespeichert
</div>
)}
</div>
)}
{/* Next button */}
{dewarpResult && (
<div className="flex justify-end">
<button
onClick={onNext}
className="px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 font-medium transition-colors"
>
Uebernehmen & Weiter &rarr;
</button>
</div>
)}
</div>
)
}