fix(ocr-pipeline): dewarp visibility, grid on both sides, session persistence
- Fix dewarp method selection: prefer methods with >5px curvature over
higher confidence (vertical_edge 79px was being ignored for text_baseline 2px)
- Add grid overlay on left image in Dewarp step for side-by-side comparison
- Add GET /sessions/{id} endpoint to reload session data
- StepDeskew accepts sessionId prop to restore state when navigating back
- SessionInfo type extended with optional deskew_result and dewarp_result
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,7 @@ export default function OcrPipelinePage() {
|
|||||||
const renderStep = () => {
|
const renderStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 0:
|
case 0:
|
||||||
return <StepDeskew onNext={handleDeskewComplete} />
|
return <StepDeskew sessionId={sessionId} onNext={handleDeskewComplete} />
|
||||||
case 1:
|
case 1:
|
||||||
return <StepDewarp sessionId={sessionId} onNext={handleNext} />
|
return <StepDewarp sessionId={sessionId} onNext={handleNext} />
|
||||||
case 2:
|
case 2:
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export interface SessionInfo {
|
|||||||
image_width: number
|
image_width: number
|
||||||
image_height: number
|
image_height: number
|
||||||
original_image_url: string
|
original_image_url: string
|
||||||
|
deskew_result?: DeskewResult
|
||||||
|
dewarp_result?: DewarpResult
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeskewResult {
|
export interface DeskewResult {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ImageCompareViewProps {
|
|||||||
originalUrl: string | null
|
originalUrl: string | null
|
||||||
deskewedUrl: string | null
|
deskewedUrl: string | null
|
||||||
showGrid: boolean
|
showGrid: boolean
|
||||||
|
showGridLeft?: boolean
|
||||||
showBinarized: boolean
|
showBinarized: boolean
|
||||||
binarizedUrl: string | null
|
binarizedUrl: string | null
|
||||||
leftLabel?: string
|
leftLabel?: string
|
||||||
@@ -77,6 +78,7 @@ export function ImageCompareView({
|
|||||||
originalUrl,
|
originalUrl,
|
||||||
deskewedUrl,
|
deskewedUrl,
|
||||||
showGrid,
|
showGrid,
|
||||||
|
showGridLeft,
|
||||||
showBinarized,
|
showBinarized,
|
||||||
binarizedUrl,
|
binarizedUrl,
|
||||||
leftLabel,
|
leftLabel,
|
||||||
@@ -95,12 +97,15 @@ export function ImageCompareView({
|
|||||||
<div className="relative bg-gray-100 dark:bg-gray-900 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700"
|
<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' }}>
|
style={{ aspectRatio: '210/297' }}>
|
||||||
{originalUrl && !leftError ? (
|
{originalUrl && !leftError ? (
|
||||||
<img
|
<>
|
||||||
src={originalUrl}
|
<img
|
||||||
alt="Original Scan"
|
src={originalUrl}
|
||||||
className="w-full h-full object-contain"
|
alt="Original Scan"
|
||||||
onError={() => setLeftError(true)}
|
className="w-full h-full object-contain"
|
||||||
/>
|
onError={() => setLeftError(true)}
|
||||||
|
/>
|
||||||
|
{showGridLeft && <MmGridOverlay />}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400">
|
<div className="flex items-center justify-center h-full text-gray-400">
|
||||||
{leftError ? 'Fehler beim Laden' : 'Noch kein Bild'}
|
{leftError ? 'Fehler beim Laden' : 'Noch kein Bild'}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import type { DeskewGroundTruth, DeskewResult, SessionInfo } from '@/app/(admin)/ai/ocr-pipeline/types'
|
import type { DeskewGroundTruth, DeskewResult, SessionInfo } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||||
import { DeskewControls } from './DeskewControls'
|
import { DeskewControls } from './DeskewControls'
|
||||||
import { ImageCompareView } from './ImageCompareView'
|
import { ImageCompareView } from './ImageCompareView'
|
||||||
@@ -8,10 +8,11 @@ import { ImageCompareView } from './ImageCompareView'
|
|||||||
const KLAUSUR_API = '/klausur-api'
|
const KLAUSUR_API = '/klausur-api'
|
||||||
|
|
||||||
interface StepDeskewProps {
|
interface StepDeskewProps {
|
||||||
|
sessionId?: string | null
|
||||||
onNext: (sessionId: string) => void
|
onNext: (sessionId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StepDeskew({ onNext }: StepDeskewProps) {
|
export function StepDeskew({ sessionId: existingSessionId, onNext }: StepDeskewProps) {
|
||||||
const [session, setSession] = useState<SessionInfo | null>(null)
|
const [session, setSession] = useState<SessionInfo | null>(null)
|
||||||
const [deskewResult, setDeskewResult] = useState<DeskewResult | null>(null)
|
const [deskewResult, setDeskewResult] = useState<DeskewResult | null>(null)
|
||||||
const [uploading, setUploading] = useState(false)
|
const [uploading, setUploading] = useState(false)
|
||||||
@@ -22,6 +23,42 @@ export function StepDeskew({ onNext }: StepDeskewProps) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [dragOver, setDragOver] = useState(false)
|
const [dragOver, setDragOver] = useState(false)
|
||||||
|
|
||||||
|
// Reload session data when navigating back from a later step
|
||||||
|
useEffect(() => {
|
||||||
|
if (!existingSessionId || session) return
|
||||||
|
|
||||||
|
const loadSession = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}`)
|
||||||
|
if (!res.ok) return
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
const sessionInfo: SessionInfo = {
|
||||||
|
session_id: data.session_id,
|
||||||
|
filename: data.filename,
|
||||||
|
image_width: data.image_width,
|
||||||
|
image_height: data.image_height,
|
||||||
|
original_image_url: `${KLAUSUR_API}${data.original_image_url}`,
|
||||||
|
}
|
||||||
|
setSession(sessionInfo)
|
||||||
|
|
||||||
|
// Reconstruct deskew result from session data
|
||||||
|
if (data.deskew_result) {
|
||||||
|
const dr: DeskewResult = {
|
||||||
|
...data.deskew_result,
|
||||||
|
deskewed_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}/image/deskewed`,
|
||||||
|
binarized_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}/image/binarized`,
|
||||||
|
}
|
||||||
|
setDeskewResult(dr)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to reload session:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSession()
|
||||||
|
}, [existingSessionId, session])
|
||||||
|
|
||||||
const handleUpload = useCallback(async (file: File) => {
|
const handleUpload = useCallback(async (file: File) => {
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|||||||
@@ -123,9 +123,10 @@ export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
|
|||||||
originalUrl={deskewedUrl}
|
originalUrl={deskewedUrl}
|
||||||
deskewedUrl={dewarpedUrl}
|
deskewedUrl={dewarpedUrl}
|
||||||
showGrid={showGrid}
|
showGrid={showGrid}
|
||||||
|
showGridLeft={showGrid}
|
||||||
showBinarized={false}
|
showBinarized={false}
|
||||||
binarizedUrl={null}
|
binarizedUrl={null}
|
||||||
leftLabel="Begradigt (nach Deskew)"
|
leftLabel={`Begradigt (nach Deskew)${showGrid ? ' + Raster' : ''}`}
|
||||||
rightLabel={`Entzerrt${showGrid ? ' + Raster (mm)' : ''}`}
|
rightLabel={`Entzerrt${showGrid ? ' + Raster (mm)' : ''}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -627,12 +627,24 @@ def dewarp_image(img: np.ndarray) -> Tuple[np.ndarray, Dict[str, Any]]:
|
|||||||
f"curv={result_b['curvature_px']:.1f}px "
|
f"curv={result_b['curvature_px']:.1f}px "
|
||||||
f"({duration:.2f}s)")
|
f"({duration:.2f}s)")
|
||||||
|
|
||||||
# Pick method with higher confidence
|
# Pick best method: prefer significant curvature over high confidence
|
||||||
if result_a["confidence"] >= result_b["confidence"]:
|
# If one method found real curvature (>5px) and the other didn't (<3px),
|
||||||
|
# prefer the one with real curvature regardless of confidence.
|
||||||
|
a_has_curvature = result_a["curvature_px"] >= 5.0 and result_a["displacement_map"] is not None
|
||||||
|
b_has_curvature = result_b["curvature_px"] >= 5.0 and result_b["displacement_map"] is not None
|
||||||
|
|
||||||
|
if a_has_curvature and not b_has_curvature:
|
||||||
|
best = result_a
|
||||||
|
elif b_has_curvature and not a_has_curvature:
|
||||||
|
best = result_b
|
||||||
|
elif result_a["confidence"] >= result_b["confidence"]:
|
||||||
best = result_a
|
best = result_a
|
||||||
else:
|
else:
|
||||||
best = result_b
|
best = result_b
|
||||||
|
|
||||||
|
logger.info(f"dewarp: selected {best['method']} "
|
||||||
|
f"(curv={best['curvature_px']:.1f}px, conf={best['confidence']:.2f})")
|
||||||
|
|
||||||
if best["displacement_map"] is None or best["curvature_px"] < 2.0:
|
if best["displacement_map"] is None or best["curvature_px"] < 2.0:
|
||||||
return img, no_correction
|
return img, no_correction
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,32 @@ async def create_session(file: UploadFile = File(...)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sessions/{session_id}")
|
||||||
|
async def get_session_info(session_id: str):
|
||||||
|
"""Get session info including deskew/dewarp results for step navigation."""
|
||||||
|
session = _get_session(session_id)
|
||||||
|
img_bgr = session["original_bgr"]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"session_id": session["id"],
|
||||||
|
"filename": session["filename"],
|
||||||
|
"image_width": img_bgr.shape[1],
|
||||||
|
"image_height": img_bgr.shape[0],
|
||||||
|
"original_image_url": f"/api/v1/ocr-pipeline/sessions/{session_id}/image/original",
|
||||||
|
"current_step": session.get("current_step", 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include deskew result if available
|
||||||
|
if session.get("deskew_result"):
|
||||||
|
result["deskew_result"] = session["deskew_result"]
|
||||||
|
|
||||||
|
# Include dewarp result if available
|
||||||
|
if session.get("dewarp_result"):
|
||||||
|
result["dewarp_result"] = session["dewarp_result"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sessions/{session_id}/deskew")
|
@router.post("/sessions/{session_id}/deskew")
|
||||||
async def auto_deskew(session_id: str):
|
async def auto_deskew(session_id: str):
|
||||||
"""Run both deskew methods and pick the best one."""
|
"""Run both deskew methods and pick the best one."""
|
||||||
|
|||||||
Reference in New Issue
Block a user