refactor: redesign debug panel with overlay visualization

Much better UX for identifying correct rotation:

- Shows ORIGINAL image scaled to fit the modal
- Draws GREEN bounding box showing the crop area
- Draws ORANGE rotated rectangle showing rotation effect
- Darkens everything outside the crop area (visual focus)
- Rotation slider updates the overlay in real-time
- Quick preset buttons for common angles
- Detailed debug logs showing all calculations
- Better styling: dark theme for readability

User can now visually see what will be extracted with the current
rotation angle. Report the angle that makes the label readable.
This commit is contained in:
2026-04-22 11:06:01 +03:00
parent 1a774088a1
commit f708fb7768
2 changed files with 120 additions and 108 deletions

View File

@@ -2,6 +2,7 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Item } from '@/lib/db'; import { Item } from '@/lib/db';
import { X } from 'lucide-react';
interface DebugRotationPanelProps { interface DebugRotationPanelProps {
item: Item; item: Item;
@@ -9,6 +10,7 @@ interface DebugRotationPanelProps {
originalCropBounds?: { x: number; y: number; width: number; height: number }; originalCropBounds?: { x: number; y: number; width: number; height: number };
originalRotation?: number; originalRotation?: number;
imageProcessing?: any; imageProcessing?: any;
onClose: () => void;
} }
export function DebugRotationPanel({ export function DebugRotationPanel({
@@ -17,13 +19,14 @@ export function DebugRotationPanel({
originalCropBounds, originalCropBounds,
originalRotation = 0, originalRotation = 0,
imageProcessing, imageProcessing,
onClose,
}: DebugRotationPanelProps) { }: DebugRotationPanelProps) {
const [rotation, setRotation] = useState(originalRotation); const [rotation, setRotation] = useState(originalRotation);
const [cropBounds, setCropBounds] = useState(originalCropBounds);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const [logs, setLogs] = useState<string[]>([]); const [logs, setLogs] = useState<string[]>([]);
const imageRef = useRef<HTMLImageElement | null>(null);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
const imageRef = useRef<HTMLImageElement>(null); const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
const commonAngles = [ const commonAngles = [
{ label: '0°', value: 0 }, { label: '0°', value: 0 },
@@ -36,138 +39,140 @@ export function DebugRotationPanel({
{ label: '±180°', value: 180 }, { label: '±180°', value: 180 },
]; ];
const addLog = (message: string) => {
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
};
useEffect(() => { useEffect(() => {
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; img.crossOrigin = 'anonymous';
img.onload = () => { img.onload = () => {
imageRef.current = img; imageRef.current = img;
setImageLoaded(true); setImageLoaded(true);
addLog(`Image loaded: ${img.width}×${img.height}px`); addLog(`📸 Image loaded: ${img.width}×${img.height}px`);
// Calculate scaled dimensions to fit canvas
const maxWidth = 600;
const maxHeight = 500;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
const scaledW = img.width * scale;
const scaledH = img.height * scale;
setScaledDimensions({ width: scaledW, height: scaledH, scale });
addLog(`📐 Scaled to fit: ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (scale ${scale.toFixed(2)}x)`);
}; };
img.onerror = () => addLog('❌ Failed to load image'); img.onerror = () => addLog('❌ Failed to load image');
img.src = imageUrl; img.src = imageUrl;
}, [imageUrl]); }, [imageUrl]);
const addLog = (message: string) => {
setLogs((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${message}`]);
};
useEffect(() => { useEffect(() => {
if (!imageLoaded || !imageRef.current || !canvasRef.current) return; if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
const img = imageRef.current;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
addLog(`\n--- Processing Step ---`); const img = imageRef.current;
addLog(`Original image: ${img.width}×${img.height}px`); const { scale } = scaledDimensions;
if (cropBounds) { // Set canvas size to match scaled image
addLog(`Crop bounds: x=${cropBounds.x}, y=${cropBounds.y}, w=${cropBounds.width}, h=${cropBounds.height}`); canvas.width = scaledDimensions.width;
addLog(`Crop rect: (${cropBounds.x}, ${cropBounds.y}, ${cropBounds.x + cropBounds.width}, ${cropBounds.y + cropBounds.height})`); canvas.height = scaledDimensions.height;
}
// Set canvas size to match cropped result (or full image if no crop) addLog(`\n🎨 --- Drawing Preview ---`);
const finalWidth = cropBounds ? cropBounds.width : img.width; addLog(`Rotation: ${rotation}°`);
const finalHeight = cropBounds ? cropBounds.height : img.height;
// Account for rotation expanding the canvas // Draw scaled image
let displayWidth = finalWidth; ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
let displayHeight = finalHeight;
const radians = (rotation * Math.PI) / 180; // Draw crop bounding box on the ORIGINAL image
const cos = Math.abs(Math.cos(radians)); const cropX = originalCropBounds.x * scale;
const sin = Math.abs(Math.sin(radians)); const cropY = originalCropBounds.y * scale;
const cropW = originalCropBounds.width * scale;
const cropH = originalCropBounds.height * scale;
if (Math.abs(rotation % 90) !== 0) { // Save state
displayWidth = Math.ceil(finalWidth * cos + finalHeight * sin);
displayHeight = Math.ceil(finalWidth * sin + finalHeight * cos);
}
canvas.width = displayWidth;
canvas.height = displayHeight;
addLog(`After crop: ${finalWidth}×${finalHeight}px`);
addLog(`Applying rotation: ${rotation}°`);
addLog(`Canvas with expansion: ${displayWidth}×${displayHeight}px`);
// Draw: Crop → Rotate
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Save context state
ctx.save(); ctx.save();
// Move to center, rotate, move back // Draw semi-transparent overlay (darken everything outside crop)
ctx.translate(displayWidth / 2, displayHeight / 2); ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.rotate((rotation * Math.PI) / 180); ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.translate(-finalWidth / 2, -finalHeight / 2);
// Draw cropped portion of image // Clear the crop area (show full brightness)
if (cropBounds) { ctx.clearRect(cropX, cropY, cropW, cropH);
ctx.drawImage(
img, // Draw crop bounds rectangle
cropBounds.x, ctx.strokeStyle = '#00FF00';
cropBounds.y, ctx.lineWidth = 3;
cropBounds.width, ctx.strokeRect(cropX, cropY, cropW, cropH);
cropBounds.height,
0, // Draw rotation indicator (rotated rectangle inside crop area)
0, if (Math.abs(rotation) > 0.5) {
cropBounds.width, ctx.strokeStyle = '#FFB800';
cropBounds.height ctx.lineWidth = 2;
); ctx.setLineDash([5, 5]);
} else {
ctx.drawImage(img, 0, 0); // Draw a rotated rectangle showing where text will be after rotation
ctx.save();
ctx.translate(cropX + cropW / 2, cropY + cropH / 2);
ctx.rotate((rotation * Math.PI) / 180);
ctx.strokeRect(-cropW / 2, -cropH / 2, cropW, cropH);
ctx.restore();
} }
ctx.restore(); ctx.restore();
addLog(`✅ Preview rendered`); // Logs
addLog(`Final image: ${displayWidth}×${displayHeight}px`); addLog(`Crop area: (${originalCropBounds.x}, ${originalCropBounds.y}) ${originalCropBounds.width}×${originalCropBounds.height}px`);
}, [imageLoaded, rotation, cropBounds]); addLog(`Scaled crop: (${cropX.toFixed(0)}, ${cropY.toFixed(0)}) ${cropW.toFixed(0)}×${cropH.toFixed(0)}px`);
if (Math.abs(rotation) > 0.5) {
addLog(`✅ Rotation overlay shown in ORANGE`);
}
}, [imageLoaded, rotation, originalCropBounds, scaledDimensions]);
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 overflow-auto"> <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-5xl w-full my-8"> <div className="bg-slate-900 border border-slate-700 rounded-lg shadow-2xl max-w-4xl w-full">
<div className="p-6 border-b"> {/* Header */}
<h2 className="text-2xl font-bold">🔧 Debug Rotation Panel</h2> <div className="p-4 border-b border-slate-700 flex items-center justify-between bg-slate-800">
<p className="text-gray-600 text-sm mt-1">Item: {item.name}</p> <h2 className="text-xl font-bold text-white">🔧 Debug Rotation & Crop</h2>
<button
onClick={onClose}
className="p-1 hover:bg-slate-700 rounded text-gray-400 hover:text-white transition"
>
<X size={20} />
</button>
</div> </div>
<div className="grid grid-cols-2 gap-6 p-6"> <div className="grid grid-cols-3 gap-4 p-6">
{/* Left: Controls */} {/* Left: Controls */}
<div className="space-y-6"> <div className="space-y-4 bg-slate-800 p-4 rounded">
{/* Rotation Slider */} {/* Rotation Slider */}
<div> <div>
<label className="block text-sm font-semibold mb-2"> <label className="block text-sm font-semibold text-white mb-2">
Rotation: <span className="text-blue-600 font-bold">{rotation}°</span> Rotation: <span className="text-yellow-400 font-bold">{rotation}°</span>
</label> </label>
<input <input
type="range" type="range"
min="-180" min="-180"
max="180" max="180"
value={rotation} value={rotation}
onChange={(e) => { onChange={(e) => setRotation(Number(e.target.value))}
const val = Number(e.target.value);
setRotation(val);
}}
className="w-full" className="w-full"
/> />
</div> </div>
{/* Quick Presets */} {/* Quick Presets */}
<div> <div>
<label className="block text-sm font-semibold mb-2">Quick Angles</label> <label className="block text-sm font-semibold text-white mb-2">Quick Angles</label>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-1">
{commonAngles.map((angle) => ( {commonAngles.map((angle) => (
<button <button
key={angle.value} key={angle.value}
onClick={() => setRotation(angle.value)} onClick={() => setRotation(angle.value)}
className={`py-2 px-3 rounded text-xs font-semibold transition ${ className={`py-1 px-2 rounded text-xs font-semibold transition ${
rotation === angle.value rotation === angle.value
? 'bg-blue-600 text-white' ? 'bg-yellow-600 text-white'
: 'bg-gray-200 hover:bg-gray-300' : 'bg-slate-700 text-gray-300 hover:bg-slate-600'
}`} }`}
> >
{angle.label} {angle.label}
@@ -177,15 +182,18 @@ export function DebugRotationPanel({
</div> </div>
{/* Original Values */} {/* Original Values */}
{(originalRotation !== undefined || originalCropBounds) && ( {originalRotation !== undefined && (
<div className="bg-gray-100 p-4 rounded"> <div className="bg-slate-700 p-3 rounded text-xs">
<h3 className="font-semibold text-sm mb-2">Original Values</h3> <h3 className="font-semibold text-white mb-2">Original Values</h3>
<div className="text-xs space-y-1 font-mono"> <div className="text-gray-300 space-y-1 font-mono">
<div>Rotation: {originalRotation}°</div> <div>Rotation: {originalRotation}°</div>
{originalCropBounds && ( {originalCropBounds && (
<div> <>
Crop: ({originalCropBounds.x}, {originalCropBounds.y}) {originalCropBounds.width}×{originalCropBounds.height} <div>Crop X: {originalCropBounds.x}</div>
</div> <div>Crop Y: {originalCropBounds.y}</div>
<div>Width: {originalCropBounds.width}</div>
<div>Height: {originalCropBounds.height}</div>
</>
)} )}
{imageProcessing?.confidence && ( {imageProcessing?.confidence && (
<div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div> <div>Confidence: {(imageProcessing.confidence * 100).toFixed(0)}%</div>
@@ -193,34 +201,37 @@ export function DebugRotationPanel({
</div> </div>
</div> </div>
)} )}
{/* Save Button */}
<button className="w-full bg-green-600 text-white py-2 rounded font-semibold hover:bg-green-700">
Save Corrected Rotation
</button>
</div> </div>
{/* Right: Preview + Logs */} {/* Center: Canvas Preview */}
<div className="space-y-4"> <div className="flex flex-col items-center justify-center bg-slate-800 p-4 rounded">
{/* Canvas Preview */} <h3 className="text-sm font-semibold text-white mb-2">Original Image with Crop Box</h3>
<div className="border rounded bg-gray-50 p-4"> <div className="border border-slate-600 rounded flex items-center justify-center bg-black/50">
<h3 className="font-semibold text-sm mb-2">Processed Preview</h3>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="border bg-white max-w-full h-auto mx-auto rounded" className="max-w-full max-h-96 rounded"
/> />
</div> </div>
<div className="text-xs text-gray-400 mt-2 text-center">
{/* Logs */} <div>🟢 Green = Crop area</div>
<div className="border rounded bg-gray-50 p-4"> <div>🟠 Orange = Rotation effect</div>
<h3 className="font-semibold text-sm mb-2">Debug Logs</h3>
<div className="bg-black text-green-400 p-3 rounded font-mono text-xs h-32 overflow-y-auto">
{logs.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
</div> </div>
</div> </div>
{/* Right: Logs */}
<div className="bg-slate-800 p-4 rounded flex flex-col">
<h3 className="text-sm font-semibold text-white mb-2">Debug Logs</h3>
<div className="bg-black text-green-400 p-3 rounded font-mono text-xs flex-1 overflow-y-auto border border-slate-700">
{logs.map((log, i) => (
<div key={i}>{log}</div>
))}
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-700 bg-slate-800 text-center text-sm text-gray-300">
Find the rotation angle that makes the label text readable, then report the value back.
</div> </div>
</div> </div>
</div> </div>

View File

@@ -201,6 +201,7 @@ export default function ItemDetailModal({
originalCropBounds={item.labels_data ? JSON.parse(item.labels_data).image_processing?.crop_bounds : undefined} originalCropBounds={item.labels_data ? JSON.parse(item.labels_data).image_processing?.crop_bounds : undefined}
originalRotation={item.labels_data ? JSON.parse(item.labels_data).image_processing?.rotation_degrees : undefined} originalRotation={item.labels_data ? JSON.parse(item.labels_data).image_processing?.rotation_degrees : undefined}
imageProcessing={item.labels_data ? JSON.parse(item.labels_data).image_processing : undefined} imageProcessing={item.labels_data ? JSON.parse(item.labels_data).image_processing : undefined}
onClose={() => setShowDebugPanel(false)}
/> />
)} )}
</div> </div>