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)