diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx
index 340555d..981cd76 100644
--- a/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx
+++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx
@@ -49,7 +49,7 @@ export default function OcrPipelinePage() {
const renderStep = () => {
switch (currentStep) {
case 0:
- return
+ return
case 1:
return
case 2:
diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts
index a727661..0ffe7ff 100644
--- a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts
+++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts
@@ -13,6 +13,8 @@ export interface SessionInfo {
image_width: number
image_height: number
original_image_url: string
+ deskew_result?: DeskewResult
+ dewarp_result?: DewarpResult
}
export interface DeskewResult {
diff --git a/admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx b/admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx
index 774da28..2d448a7 100644
--- a/admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx
+++ b/admin-lehrer/components/ocr-pipeline/ImageCompareView.tsx
@@ -9,6 +9,7 @@ interface ImageCompareViewProps {
originalUrl: string | null
deskewedUrl: string | null
showGrid: boolean
+ showGridLeft?: boolean
showBinarized: boolean
binarizedUrl: string | null
leftLabel?: string
@@ -77,6 +78,7 @@ export function ImageCompareView({
originalUrl,
deskewedUrl,
showGrid,
+ showGridLeft,
showBinarized,
binarizedUrl,
leftLabel,
@@ -95,12 +97,15 @@ export function ImageCompareView({
{originalUrl && !leftError ? (
-

setLeftError(true)}
- />
+ <>
+

setLeftError(true)}
+ />
+ {showGridLeft &&
}
+ >
) : (
{leftError ? 'Fehler beim Laden' : 'Noch kein Bild'}
diff --git a/admin-lehrer/components/ocr-pipeline/StepDeskew.tsx b/admin-lehrer/components/ocr-pipeline/StepDeskew.tsx
index cb3bb9b..14786ce 100644
--- a/admin-lehrer/components/ocr-pipeline/StepDeskew.tsx
+++ b/admin-lehrer/components/ocr-pipeline/StepDeskew.tsx
@@ -1,6 +1,6 @@
'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 { DeskewControls } from './DeskewControls'
import { ImageCompareView } from './ImageCompareView'
@@ -8,10 +8,11 @@ import { ImageCompareView } from './ImageCompareView'
const KLAUSUR_API = '/klausur-api'
interface StepDeskewProps {
+ sessionId?: string | null
onNext: (sessionId: string) => void
}
-export function StepDeskew({ onNext }: StepDeskewProps) {
+export function StepDeskew({ sessionId: existingSessionId, onNext }: StepDeskewProps) {
const [session, setSession] = useState(null)
const [deskewResult, setDeskewResult] = useState(null)
const [uploading, setUploading] = useState(false)
@@ -22,6 +23,42 @@ export function StepDeskew({ onNext }: StepDeskewProps) {
const [error, setError] = useState(null)
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) => {
setUploading(true)
setError(null)
diff --git a/admin-lehrer/components/ocr-pipeline/StepDewarp.tsx b/admin-lehrer/components/ocr-pipeline/StepDewarp.tsx
index daccf50..54fc7de 100644
--- a/admin-lehrer/components/ocr-pipeline/StepDewarp.tsx
+++ b/admin-lehrer/components/ocr-pipeline/StepDewarp.tsx
@@ -123,9 +123,10 @@ export function StepDewarp({ sessionId, onNext }: StepDewarpProps) {
originalUrl={deskewedUrl}
deskewedUrl={dewarpedUrl}
showGrid={showGrid}
+ showGridLeft={showGrid}
showBinarized={false}
binarizedUrl={null}
- leftLabel="Begradigt (nach Deskew)"
+ leftLabel={`Begradigt (nach Deskew)${showGrid ? ' + Raster' : ''}`}
rightLabel={`Entzerrt${showGrid ? ' + Raster (mm)' : ''}`}
/>
diff --git a/klausur-service/backend/cv_vocab_pipeline.py b/klausur-service/backend/cv_vocab_pipeline.py
index 708a6b4..6b05018 100644
--- a/klausur-service/backend/cv_vocab_pipeline.py
+++ b/klausur-service/backend/cv_vocab_pipeline.py
@@ -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"({duration:.2f}s)")
- # Pick method with higher confidence
- if result_a["confidence"] >= result_b["confidence"]:
+ # Pick best method: prefer significant curvature over high 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
else:
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:
return img, no_correction
diff --git a/klausur-service/backend/ocr_pipeline_api.py b/klausur-service/backend/ocr_pipeline_api.py
index 4eee01a..54eebac 100644
--- a/klausur-service/backend/ocr_pipeline_api.py
+++ b/klausur-service/backend/ocr_pipeline_api.py
@@ -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")
async def auto_deskew(session_id: str):
"""Run both deskew methods and pick the best one."""