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>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
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: (scale: number) => void
|
||||
onManualDewarp: (shearDegrees: number) => void
|
||||
onGroundTruth: (gt: DewarpGroundTruth) => void
|
||||
onNext: () => void
|
||||
isApplying: boolean
|
||||
@@ -15,7 +15,6 @@ interface DewarpControlsProps {
|
||||
|
||||
const METHOD_LABELS: Record<string, string> = {
|
||||
vertical_edge: 'Vertikale Kanten',
|
||||
text_baseline: 'Textzeilen-Baseline',
|
||||
manual: 'Manuell',
|
||||
none: 'Keine Korrektur',
|
||||
}
|
||||
@@ -29,11 +28,18 @@ export function DewarpControls({
|
||||
onNext,
|
||||
isApplying,
|
||||
}: DewarpControlsProps) {
|
||||
const [manualScale, setManualScale] = useState(100)
|
||||
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) {
|
||||
@@ -45,7 +51,7 @@ export function DewarpControls({
|
||||
const handleGroundTruthIncorrect = () => {
|
||||
onGroundTruth({
|
||||
is_correct: false,
|
||||
corrected_scale: manualScale !== 0 ? manualScale : undefined,
|
||||
corrected_shear: manualShear !== 0 ? manualShear : undefined,
|
||||
notes: gtNotes || undefined,
|
||||
})
|
||||
setGtSaved(true)
|
||||
@@ -58,8 +64,8 @@ export function DewarpControls({
|
||||
<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">Kruemmung:</span>{' '}
|
||||
<span className="font-mono font-medium">{dewarpResult.curvature_px} px</span>
|
||||
<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>
|
||||
@@ -91,25 +97,25 @@ export function DewarpControls({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual scale slider */}
|
||||
{/* 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">Korrekturstaerke</div>
|
||||
<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-8 text-right">0%</span>
|
||||
<span className="text-xs text-gray-400 w-10 text-right">-2.0°</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
min={-200}
|
||||
max={200}
|
||||
step={5}
|
||||
value={manualScale}
|
||||
onChange={(e) => setManualScale(parseInt(e.target.value))}
|
||||
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">200%</span>
|
||||
<span className="font-mono text-sm w-14 text-right">{manualScale}%</span>
|
||||
<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(manualScale / 100)}
|
||||
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"
|
||||
>
|
||||
@@ -117,7 +123,7 @@ export function DewarpControls({
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
100% = automatisch erkannte Korrektur, 0% = keine, 200% = doppelt so stark
|
||||
Scherung der vertikalen Achse in Grad. Positiv = Spalten nach rechts kippen, negativ = nach links.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -126,8 +132,9 @@ export function DewarpControls({
|
||||
{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">
|
||||
Korrekt entzerrt?
|
||||
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">
|
||||
|
||||
@@ -47,7 +47,7 @@ export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
|
||||
runDewarp()
|
||||
}, [sessionId, dewarpResult])
|
||||
|
||||
const handleManualDewarp = useCallback(async (scale: number) => {
|
||||
const handleManualDewarp = useCallback(async (shearDegrees: number) => {
|
||||
if (!sessionId) return
|
||||
setApplying(true)
|
||||
setError(null)
|
||||
@@ -56,7 +56,7 @@ export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
|
||||
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/dewarp/manual`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scale }),
|
||||
body: JSON.stringify({ shear_degrees: shearDegrees }),
|
||||
})
|
||||
if (!res.ok) throw new Error('Manuelle Entzerrung fehlgeschlagen')
|
||||
|
||||
@@ -66,7 +66,7 @@ export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
|
||||
? {
|
||||
...prev,
|
||||
method_used: data.method_used,
|
||||
scale_applied: data.scale_applied,
|
||||
shear_degrees: data.shear_degrees,
|
||||
dewarped_image_url: `${KLAUSUR_API}${data.dewarped_image_url}?t=${Date.now()}`,
|
||||
}
|
||||
: null,
|
||||
|
||||
Reference in New Issue
Block a user