feat: store original EXIF-stripped image for debugging
- Save original image before crop/rotation with '_debug_original' variant - Store original_photo_path in labels_data.image_processing for debug access - Update DebugRotationPanel to display original image instead of processed - Update ItemDetailModal to pass original image path to debug panel - Enables accurate crop/rotation visualization with true original image
This commit is contained in:
@@ -594,6 +594,23 @@ def _auto_save_photo_from_extraction(
|
|||||||
|
|
||||||
# All validation passed, proceed with processing
|
# All validation passed, proceed with processing
|
||||||
try:
|
try:
|
||||||
|
# Save original image (EXIF-stripped, unprocessed) for debugging
|
||||||
|
category = db_item.category or "items"
|
||||||
|
existing_files = []
|
||||||
|
cat_dir = Path("images") / category.lower()
|
||||||
|
if cat_dir.exists():
|
||||||
|
existing_files = [f.name for f in cat_dir.iterdir() if f.is_file()]
|
||||||
|
|
||||||
|
filename_base = db_item.name or f"item_{item_id}"
|
||||||
|
debug_filename = get_unique_filename(filename_base, category, existing_files, variant="debug_original")
|
||||||
|
|
||||||
|
try:
|
||||||
|
original_debug_path = save_image(image_bytes, category, debug_filename.replace("_debug_original.jpg", ""), variant="debug_original")
|
||||||
|
log.info(f"[AUTO_SAVE] Saved original image for debugging: {original_debug_path}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[AUTO_SAVE] Failed to save debug original image: {e}")
|
||||||
|
original_debug_path = None
|
||||||
|
|
||||||
# Process image (crop + rotation + compression + thumbnail)
|
# Process image (crop + rotation + compression + thumbnail)
|
||||||
processor = ImageProcessor()
|
processor = ImageProcessor()
|
||||||
log.info(f"[AUTO_SAVE] Processing photo: crop_bounds={crop_bounds_validated}, rotation_degrees={rotation_degrees or 0}")
|
log.info(f"[AUTO_SAVE] Processing photo: crop_bounds={crop_bounds_validated}, rotation_degrees={rotation_degrees or 0}")
|
||||||
@@ -664,6 +681,26 @@ def _auto_save_photo_from_extraction(
|
|||||||
db_item.photo_thumbnail_path = thumbnail_path
|
db_item.photo_thumbnail_path = thumbnail_path
|
||||||
db_item.photo_upload_date = datetime.now(timezone.utc)
|
db_item.photo_upload_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# Store image processing metadata in labels_data (including original debug image path)
|
||||||
|
if not db_item.labels_data:
|
||||||
|
db_item.labels_data = "{}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
labels = json.loads(db_item.labels_data)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
labels = {}
|
||||||
|
|
||||||
|
if "image_processing" not in labels:
|
||||||
|
labels["image_processing"] = {}
|
||||||
|
|
||||||
|
# Add the original image path for debugging
|
||||||
|
labels["image_processing"]["original_photo_path"] = original_debug_path
|
||||||
|
labels["image_processing"]["crop_bounds"] = crop_bounds_validated
|
||||||
|
labels["image_processing"]["rotation_degrees"] = rotation_degrees or 0
|
||||||
|
|
||||||
|
db_item.labels_data = json.dumps(labels)
|
||||||
|
log.info(f"[AUTO_SAVE] Updated labels_data with original_photo_path: {original_debug_path}")
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_item)
|
db.refresh(db_item)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { X } from 'lucide-react';
|
|||||||
interface DebugRotationPanelProps {
|
interface DebugRotationPanelProps {
|
||||||
item: Item;
|
item: Item;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
originalPhotoPath?: string;
|
||||||
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;
|
||||||
@@ -16,6 +17,7 @@ interface DebugRotationPanelProps {
|
|||||||
export function DebugRotationPanel({
|
export function DebugRotationPanel({
|
||||||
item,
|
item,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
originalPhotoPath,
|
||||||
originalCropBounds,
|
originalCropBounds,
|
||||||
originalRotation = 0,
|
originalRotation = 0,
|
||||||
imageProcessing,
|
imageProcessing,
|
||||||
@@ -28,6 +30,9 @@ export function DebugRotationPanel({
|
|||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
|
const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 });
|
||||||
|
|
||||||
|
// Use original photo path if available, otherwise fall back to processed image URL
|
||||||
|
const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl;
|
||||||
|
|
||||||
const commonAngles = [
|
const commonAngles = [
|
||||||
{ label: '0°', value: 0 },
|
{ label: '0°', value: 0 },
|
||||||
{ label: '+22°', value: 22 },
|
{ label: '+22°', value: 22 },
|
||||||
@@ -61,8 +66,8 @@ export function DebugRotationPanel({
|
|||||||
addLog(`📐 Scaled to fit: ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (scale ${scale.toFixed(2)}x)`);
|
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 = displayImageUrl;
|
||||||
}, [imageUrl]);
|
}, [displayImageUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
|
if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return;
|
||||||
|
|||||||
@@ -198,6 +198,19 @@ export default function ItemDetailModal({
|
|||||||
<DebugRotationPanel
|
<DebugRotationPanel
|
||||||
item={item}
|
item={item}
|
||||||
imageUrl={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
|
imageUrl={buildPhotoUrl(backendUrl, currentPhoto.full_url)}
|
||||||
|
originalPhotoPath={
|
||||||
|
item.labels_data
|
||||||
|
? (() => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(item.labels_data);
|
||||||
|
const origPath = parsed.image_processing?.original_photo_path;
|
||||||
|
return origPath ? buildPhotoUrl(backendUrl, origPath) : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
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}
|
||||||
|
|||||||
Reference in New Issue
Block a user