feat: add border ghost filter + graphic detection tests + structure overlay
- Add _filter_border_ghost_words() to remove OCR artefacts from box borders (vertical + horizontal edge detection, column cleanup, re-indexing) - Add 20 tests for border ghost filter (basic filtering + column cleanup) - Add 24 tests for cv_graphic_detect (color detection, word overlap, boxes) - Clean up cv_graphic_detect.py logging (per-candidate → DEBUG) - Add structure overlay layer to StepReconstruction (boxes + graphics toggle) - Show border_ghosts_removed badge in StepStructureDetection - Update MkDocs with structure detection documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { GridResult, GridCell, ColumnResult, RowResult, PageZone, PageRegion, RowItem } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||
import type { GridResult, GridCell, ColumnResult, RowResult, PageZone, PageRegion, RowItem, StructureResult, StructureBox, StructureGraphic } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||
import { usePixelWordPositions } from './usePixelWordPositions'
|
||||
|
||||
const KLAUSUR_API = '/klausur-api'
|
||||
@@ -60,6 +60,9 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
const [fontScale, setFontScale] = useState(0.7)
|
||||
const [globalBold, setGlobalBold] = useState(false)
|
||||
const [imageRotation, setImageRotation] = useState<0 | 180>(0)
|
||||
const [structureBoxes, setStructureBoxes] = useState<StructureBox[]>([])
|
||||
const [structureGraphics, setStructureGraphics] = useState<StructureGraphic[]>([])
|
||||
const [showStructure, setShowStructure] = useState(true)
|
||||
const reconRef = useRef<HTMLDivElement>(null)
|
||||
const [reconWidth, setReconWidth] = useState(0)
|
||||
|
||||
@@ -92,12 +95,15 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId])
|
||||
|
||||
// Track image natural height for font scaling
|
||||
// Track image natural dimensions for font scaling and structure layer
|
||||
const handleImageLoad = useCallback(() => {
|
||||
if (imageRef.current) {
|
||||
setImageNaturalH(imageRef.current.naturalHeight)
|
||||
if (!imageNaturalSize) {
|
||||
setImageNaturalSize({ w: imageRef.current.naturalWidth, h: imageRef.current.naturalHeight })
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [imageNaturalSize])
|
||||
|
||||
const loadSessionData = async () => {
|
||||
if (!sessionId) return
|
||||
@@ -132,6 +138,13 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
setUndoStack([])
|
||||
setRedoStack([])
|
||||
|
||||
// Load structure result (boxes, graphics, colors)
|
||||
const structureResult: StructureResult | undefined = data.structure_result
|
||||
if (structureResult) {
|
||||
setStructureBoxes(structureResult.boxes || [])
|
||||
setStructureGraphics(structureResult.graphics || [])
|
||||
}
|
||||
|
||||
// Check for parent with boxes (sub-sessions + zones)
|
||||
const columnResult: ColumnResult | undefined = data.column_result
|
||||
const rowResult: RowResult | undefined = data.row_result
|
||||
@@ -517,6 +530,65 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
return bboxPct
|
||||
}
|
||||
|
||||
// Structure layer: boxes and graphic elements as background
|
||||
const renderStructureLayer = (imgW: number, imgH: number) => {
|
||||
if (!showStructure) return null
|
||||
const hasElements = structureBoxes.length > 0 || structureGraphics.length > 0
|
||||
if (!hasElements) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Structure boxes */}
|
||||
{structureBoxes.map((box, i) => {
|
||||
const bgColor = box.bg_color_hex || '#6b7280'
|
||||
return (
|
||||
<div
|
||||
key={`sbox-${i}`}
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
left: `${(box.x / imgW) * 100}%`,
|
||||
top: `${(box.y / imgH) * 100}%`,
|
||||
width: `${(box.w / imgW) * 100}%`,
|
||||
height: `${(box.h / imgH) * 100}%`,
|
||||
border: `${Math.max(1, box.border_thickness)}px solid ${bgColor}40`,
|
||||
backgroundColor: `${bgColor}0a`,
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Graphic elements */}
|
||||
{structureGraphics.map((g, i) => (
|
||||
<div
|
||||
key={`sgfx-${i}`}
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
left: `${(g.x / imgW) * 100}%`,
|
||||
top: `${(g.y / imgH) * 100}%`,
|
||||
width: `${(g.w / imgW) * 100}%`,
|
||||
height: `${(g.h / imgH) * 100}%`,
|
||||
border: `1px dashed ${g.color_hex}60`,
|
||||
backgroundColor: `${g.color_hex}08`,
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="absolute text-[8px] leading-none opacity-50"
|
||||
style={{
|
||||
top: '1px',
|
||||
left: '2px',
|
||||
color: g.color_hex,
|
||||
}}
|
||||
>
|
||||
{g.shape === 'illustration' ? 'Illust' : 'Bild'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Overlay rendering helper
|
||||
const renderOverlayMode = () => {
|
||||
const imgW = imageNaturalSize?.w || 1
|
||||
@@ -597,6 +669,9 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Structure elements (boxes, graphics) */}
|
||||
{renderStructureLayer(imgW, imgH)}
|
||||
|
||||
{/* Pixel-positioned words / editable inputs */}
|
||||
{cells.map((cell) => {
|
||||
const displayText = getDisplayText(cell)
|
||||
@@ -831,6 +906,19 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
>
|
||||
180°
|
||||
</button>
|
||||
{(structureBoxes.length > 0 || structureGraphics.length > 0) && (
|
||||
<button
|
||||
onClick={() => setShowStructure(v => !v)}
|
||||
className={`px-2 py-1 text-xs border rounded transition-colors ${
|
||||
showStructure
|
||||
? 'border-violet-300 bg-violet-50 text-violet-600 dark:border-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title="Strukturelemente anzeigen"
|
||||
>
|
||||
Struktur
|
||||
</button>
|
||||
)}
|
||||
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||
</>
|
||||
)}
|
||||
@@ -851,6 +939,21 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
Leer
|
||||
</button>
|
||||
|
||||
{/* Structure toggle */}
|
||||
{(structureBoxes.length > 0 || structureGraphics.length > 0) && (
|
||||
<button
|
||||
onClick={() => setShowStructure(v => !v)}
|
||||
className={`px-2 py-1 text-xs border rounded transition-colors ${
|
||||
showStructure
|
||||
? 'border-violet-300 bg-violet-50 text-violet-600 dark:border-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title="Strukturelemente anzeigen"
|
||||
>
|
||||
Struktur
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||
|
||||
{/* Zoom controls */}
|
||||
@@ -915,6 +1018,9 @@ export function StepReconstruction({ sessionId, onNext }: StepReconstructionProp
|
||||
onLoad={handleImageLoad}
|
||||
/>
|
||||
|
||||
{/* Structure elements (boxes, graphics) */}
|
||||
{imageNaturalSize && renderStructureLayer(imageNaturalSize.w, imageNaturalSize.h)}
|
||||
|
||||
{/* Empty field markers */}
|
||||
{showEmptyHighlight && cells
|
||||
.filter(c => emptyCellIds.has(c.cellId))
|
||||
|
||||
@@ -165,6 +165,11 @@ export function StepStructureDetection({ sessionId, onNext }: StepStructureDetec
|
||||
{result.word_count} Woerter
|
||||
</span>
|
||||
)}
|
||||
{(result.border_ghosts_removed ?? 0) > 0 && (
|
||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 text-xs font-medium">
|
||||
{result.border_ghosts_removed} Rahmenlinien entfernt
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-400 text-xs ml-auto">
|
||||
{result.image_width}x{result.image_height}px | {result.duration_seconds}s
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user