fix: transform crop_bounds from EXIF-applied to raw image space

Gemini analyzes images with EXIF orientation applied (e.g., 90° CW rotation),
returning crop_bounds in that coordinate space. We need to transform them back
to raw image coordinates before cropping.

For EXIF orientation 6 (Rotate 90 CW):
- Raw: 4032×3024 (landscape)
- Displayed (after EXIF): 3024×4032 (portrait)
- Transform portrait coords back to landscape before cropping

This fixes the issue where crop was applied to wrong image region.
This commit is contained in:
2026-04-22 10:25:44 +03:00
parent ee1fcfbee6
commit 1425856af5

View File

@@ -88,6 +88,15 @@ class ImageProcessor:
# Extract EXIF orientation (but don't apply yet)
exif_orientation = self._extract_exif_orientation(image)
# Transform crop_bounds from EXIF-applied space to raw space
if crop_bounds and exif_orientation and exif_orientation > 1:
crop_bounds = self._transform_crop_bounds_by_exif(
crop_bounds, exif_orientation, original_size[0], original_size[1]
)
msg = f"[CROP] EXIF {exif_orientation} transform applied: {crop_bounds}"
self.logger.info(msg)
print(f">>> {msg}")
# Smart cropping (on raw, un-rotated image so crop_bounds match AI analysis)
cropped_image = image
crop_size = None
@@ -204,6 +213,44 @@ class ImageProcessor:
self.logger.debug(f"Could not extract EXIF orientation: {e}")
return None
def _transform_crop_bounds_by_exif(
self, crop_bounds: Dict, orientation: int, raw_width: int, raw_height: int
) -> Dict:
"""
Transform crop_bounds from EXIF-applied space to raw image space.
Gemini analyzes images with EXIF applied, returning crop_bounds in that space.
We need to transform them back to raw image coordinates before cropping.
Args:
crop_bounds: {x, y, width, height} in EXIF-applied space
orientation: EXIF orientation value (1-8)
raw_width: raw image width (before EXIF)
raw_height: raw image height (before EXIF)
Returns:
Transformed crop_bounds in raw image space
"""
if orientation == 1:
# No rotation, use as-is
return crop_bounds
elif orientation == 6:
# Rotate 90 CW: raw (w×h) appears as displayed (h×w)
# Transform from displayed (h×w) back to raw (w×h)
x, y, w, h = crop_bounds['x'], crop_bounds['y'], crop_bounds['width'], crop_bounds['height']
# After 90° CW: (x,y) in raw → (raw_height - y, x) in displayed
# Reverse: (x,y) in displayed → (y, raw_width - x) in raw
return {
'x': y,
'y': raw_width - x - w,
'width': h,
'height': w
}
else:
# Other orientations: return as-is (TODO: implement if needed)
self.logger.warning(f"EXIF orientation {orientation} not supported for crop_bounds transform, using as-is")
return crop_bounds
def _rotate_by_orientation(
self, image: Image.Image, orientation: int
) -> Image.Image: