From f2a374b363cb49a414271bce1c08e9a1e2cc9bd1 Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Sat, 18 Jul 2026 15:46:01 -0400 Subject: [PATCH] Make face-aware crop minimal-shift instead of full re-centering _face_aware_crop_box() previously always centered the crop on the union of all detected faces' centroid, even when the plain center-crop already kept every face fully on screen -- unnecessarily moving a composition that didn't need fixing. Now starts from the plain center-crop and only shifts it the minimum amount needed to bring an otherwise-cropped-out face back into frame; already-fine framing is left untouched (falls back to centering on the faces' midpoint only if they're spread too wide for any single shift to contain them all, which is unchanged from before). Verified: a face safely inside the plain center-crop now produces byte- identical output to the no-shift case (previously it still would have been re-centered); an edge face gets a 100px shift instead of the 1050px a full re-center would have applied. Re-ran against the real 4-face test photo from earlier -- all four were already fully visible, so the refined box now exactly matches the plain center-crop instead of shifting unnecessarily. --- server/app/image_pipeline.py | 37 +++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/server/app/image_pipeline.py b/server/app/image_pipeline.py index 35812f3..36dbe62 100644 --- a/server/app/image_pipeline.py +++ b/server/app/image_pipeline.py @@ -38,10 +38,13 @@ def _face_aware_crop_box( img_width: int, img_height: int, target_width: int, target_height: int, faces: list[dict] ) -> tuple[int, int, int, int]: """Largest crop window matching target_width:target_height that fits - inside the source image, centered on the union of all face bounding - boxes instead of the image's geometric center. Doesn't guarantee every - face survives if they're spread wider than the crop window allows -- - just biases toward keeping them on screen, best-effort. + inside the source image. Starts from the plain center crop and only + shifts it the minimum amount needed to bring any faces that would + otherwise be cut off back on screen -- an already-fine composition + (faces already fully inside the center crop) is left untouched rather + than re-centered on the faces. If the faces themselves span wider than + the crop window allows, centers on their midpoint as best-effort, + since there's no shift that fits them all regardless. Each face's box is given relative to its own imageWidth/imageHeight (the resolution Immich ran detection on), which may differ from the @@ -60,9 +63,6 @@ def _face_aware_crop_box( min_y = min(min_y, face["boundingBoxY1"] * scale_y) max_y = max(max_y, face["boundingBoxY2"] * scale_y) - faces_cx = (min_x + max_x) / 2 - faces_cy = (min_y + max_y) / 2 - target_ratio = target_width / target_height if img_width / img_height > target_ratio: crop_h = img_height @@ -71,8 +71,27 @@ def _face_aware_crop_box( crop_w = img_width crop_h = int(crop_w / target_ratio) - left = max(0, min(faces_cx - crop_w / 2, img_width - crop_w)) - top = max(0, min(faces_cy - crop_h / 2, img_height - crop_h)) + left = (img_width - crop_w) / 2 + top = (img_height - crop_h) / 2 + + if max_x - min_x <= crop_w: + if min_x < left: + left = min_x + elif max_x > left + crop_w: + left = max_x - crop_w + else: + left = (min_x + max_x) / 2 - crop_w / 2 + + if max_y - min_y <= crop_h: + if min_y < top: + top = min_y + elif max_y > top + crop_h: + top = max_y - crop_h + else: + top = (min_y + max_y) / 2 - crop_h / 2 + + left = max(0, min(left, img_width - crop_w)) + top = max(0, min(top, img_height - crop_h)) return (int(left), int(top), int(left) + crop_w, int(top) + crop_h)