2121"""Fractional padding added around each bounding box side before cropping."""
2222
2323
24- def generate_crop (image_path : Path , x : float , y : float , width : float , height : float ) -> str :
25- """Generate a base64-encoded 150 x 150 JPEG face crop.
24+ def _calculate_square_crop_coords (
25+ x : float , y : float , width : float , height : float , img_w : int , img_h : int , padding_factor : float
26+ ) -> tuple [int , int , int , int ]:
27+ """Calculate square crop coordinates centered on the face bounding box.
28+
29+ Attempts to create a square crop centered on the face. If the square would
30+ extend beyond image boundaries, it shifts the crop to fit. If the square is
31+ larger than the image dimensions, the crop will be the maximum square that
32+ fits within the image.
2633
2734 Args:
28- image_path: Absolute path to the source image.
2935 x: Normalised left edge of the bounding box (0.0-1.0).
3036 y: Normalised top edge of the bounding box (0.0-1.0).
3137 width: Normalised bounding-box width (0.0-1.0).
3238 height: Normalised bounding-box height (0.0-1.0).
39+ img_w: Image width in pixels.
40+ img_h: Image height in pixels.
41+ padding_factor: Fractional padding to add around the bounding box.
3342
3443 Returns:
35- Base64-encoded JPEG bytes (ASCII string) .
44+ Tuple of (x1, y1, x2, y2) defining a square crop region in absolute pixels .
3645 """
37- img = Image .open (image_path ).convert ("RGB" )
38- img_w , img_h = img .size
39-
40- # Absolute pixel coordinates
46+ # Convert to absolute pixels
4147 abs_x = x * img_w
4248 abs_y = y * img_h
4349 abs_w = width * img_w
4450 abs_h = height * img_h
4551
4652 # Add padding
47- pad_x = abs_w * _PADDING_FACTOR
48- pad_y = abs_h * _PADDING_FACTOR
53+ pad_x = abs_w * padding_factor
54+ pad_y = abs_h * padding_factor
55+
56+ padded_x = abs_x - pad_x
57+ padded_y = abs_y - pad_y
58+ padded_w = abs_w + 2 * pad_x
59+ padded_h = abs_h + 2 * pad_y
60+
61+ # Determine square size (use the larger dimension)
62+ square_size = max (padded_w , padded_h )
63+
64+ # Cap square size to image dimensions (can't crop larger than the image)
65+ max_possible_size = min (img_w , img_h )
66+ square_size = min (square_size , max_possible_size )
67+
68+ # Calculate center point of the padded bounding box
69+ center_x = padded_x + padded_w / 2
70+ center_y = padded_y + padded_h / 2
71+
72+ # Calculate square crop coordinates centered on the face
73+ x1 = center_x - square_size / 2
74+ y1 = center_y - square_size / 2
75+ x2 = center_x + square_size / 2
76+ y2 = center_y + square_size / 2
77+
78+ # Adjust to keep square within image boundaries
79+ # If the square extends beyond the left edge, shift it right
80+ if x1 < 0 :
81+ shift = - x1
82+ x1 = 0
83+ x2 = min (float (img_w ), x2 + shift )
84+ # If the square extends beyond the right edge, shift it left
85+ if x2 > img_w :
86+ shift = x2 - img_w
87+ x2 = img_w
88+ x1 = max (0.0 , x1 - shift )
89+
90+ # If the square extends beyond the top edge, shift it down
91+ if y1 < 0 :
92+ shift = - y1
93+ y1 = 0
94+ y2 = min (float (img_h ), y2 + shift )
95+ # If the square extends beyond the bottom edge, shift it up
96+ if y2 > img_h :
97+ shift = y2 - img_h
98+ y2 = img_h
99+ y1 = max (0.0 , y1 - shift )
100+
101+ return int (x1 ), int (y1 ), int (x2 ), int (y2 )
102+
103+
104+ def _pad_to_square (img : Image .Image ) -> Image .Image :
105+ """Pad a non-square image to square with black borders.
49106
50- x1 = max (0.0 , abs_x - pad_x )
51- y1 = max (0.0 , abs_y - pad_y )
52- x2 = min (float (img_w ), abs_x + abs_w + pad_x )
53- y2 = min (float (img_h ), abs_y + abs_h + pad_y )
107+ Args:
108+ img: Input PIL Image.
54109
55- crop = img .crop ((int (x1 ), int (y1 ), int (x2 ), int (y2 )))
110+ Returns:
111+ Square PIL Image with black padding if needed.
112+ """
113+ width , height = img .size
114+ if width == height :
115+ return img
116+
117+ size = max (width , height )
118+ square_img = Image .new ("RGB" , (size , size ), (0 , 0 , 0 ))
119+ paste_x = (size - width ) // 2
120+ paste_y = (size - height ) // 2
121+ square_img .paste (img , (paste_x , paste_y ))
122+ return square_img
123+
124+
125+ def generate_crop (image_path : Path , x : float , y : float , width : float , height : float ) -> str :
126+ """Generate a base64-encoded 150 x 150 JPEG face crop.
127+
128+ Args:
129+ image_path: Absolute path to the source image.
130+ x: Normalised left edge of the bounding box (0.0-1.0).
131+ y: Normalised top edge of the bounding box (0.0-1.0).
132+ width: Normalised bounding-box width (0.0-1.0).
133+ height: Normalised bounding-box height (0.0-1.0).
134+
135+ Returns:
136+ Base64-encoded JPEG bytes (ASCII string).
137+ """
138+ img = Image .open (image_path ).convert ("RGB" )
139+ img_w , img_h = img .size
140+
141+ # Calculate square crop coordinates
142+ x1 , y1 , x2 , y2 = _calculate_square_crop_coords (x , y , width , height , img_w , img_h , _PADDING_FACTOR )
143+
144+ # Crop and ensure it's square (pad if needed due to edge constraints)
145+ crop = img .crop ((x1 , y1 , x2 , y2 ))
146+ crop = _pad_to_square (crop )
56147 crop = crop .resize ((CROP_SIZE , CROP_SIZE ), Image .Resampling .LANCZOS )
57148
58149 buf = io .BytesIO ()
@@ -76,20 +167,12 @@ def generate_crop_from_bytes(image_bytes: bytes, x: float, y: float, width: floa
76167 img = Image .open (io .BytesIO (image_bytes )).convert ("RGB" )
77168 img_w , img_h = img .size
78169
79- abs_x = x * img_w
80- abs_y = y * img_h
81- abs_w = width * img_w
82- abs_h = height * img_h
83-
84- pad_x = abs_w * _PADDING_FACTOR
85- pad_y = abs_h * _PADDING_FACTOR
86-
87- x1 = max (0.0 , abs_x - pad_x )
88- y1 = max (0.0 , abs_y - pad_y )
89- x2 = min (float (img_w ), abs_x + abs_w + pad_x )
90- y2 = min (float (img_h ), abs_y + abs_h + pad_y )
170+ # Calculate square crop coordinates
171+ x1 , y1 , x2 , y2 = _calculate_square_crop_coords (x , y , width , height , img_w , img_h , _PADDING_FACTOR )
91172
92- crop = img .crop ((int (x1 ), int (y1 ), int (x2 ), int (y2 )))
173+ # Crop and ensure it's square (pad if needed due to edge constraints)
174+ crop = img .crop ((x1 , y1 , x2 , y2 ))
175+ crop = _pad_to_square (crop )
93176 crop = crop .resize ((CROP_SIZE , CROP_SIZE ), Image .Resampling .LANCZOS )
94177
95178 buf = io .BytesIO ()
0 commit comments