tesalonikahtp commited on
Commit
ada6310
·
1 Parent(s): e82342b

fix: not enough zoom

Browse files
app/util/passport_photo_engine/passport_cropper.py CHANGED
@@ -2,56 +2,76 @@ import cv2
2
  import numpy as np
3
 
4
  class PassportCropper:
5
- def __init__(self, output_size=(600,800), bg_color=(255,255,255)):
 
6
  self.out_w, self.out_h = output_size
7
  self.bg_color = tuple(int(x) for x in bg_color)
8
  self.target_aspect = self.out_w / self.out_h
9
 
10
  def composite(self, img_rgb, mask):
 
11
  bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
 
 
 
12
  m = cv2.GaussianBlur(mask, (5,5), 0)
13
  alpha = (m.astype(float) / 255.0)[:,:,None]
14
  bg = np.full_like(bgr, np.array(self.bg_color, dtype=np.uint8))
 
 
15
  return (bgr.astype(float) * alpha + bg.astype(float) * (1.0 - alpha)).astype(np.uint8)
16
 
17
- def rotate_and_expand_face(self, img_bgr, angle_deg, raw_face_box):
 
 
 
 
 
 
18
  h0, w0 = img_bgr.shape[:2]
19
- x1, y1, x2, y2 = raw_face_box
20
- # Expansion Logic: 0.4 sides, 0.6 top, 1.2 bottom
21
- fw, fh = x2-x1, y2-y1
22
- hx1, hy1 = max(0, x1-int(fw*0.4)), max(0, y1-int(fh*0.6))
23
- hx2, hy2 = min(w0-1, x2+int(fw*0.4)), min(h0-1, y2+int(fh*1.2))
24
-
25
- # Rotation
26
- M = cv2.getRotationMatrix2D((w0/2, h0/2), -angle_deg, 1.0)
27
- cos, sin = np.abs(M[0,0]), np.abs(M[0,1])
28
- nW, nH = int((h0*sin)+(w0*cos)), int((h0*cos)+(w0*sin))
29
- M[0,2] += (nW/2) - w0/2; M[1,2] += (nH/2) - h0/2
30
-
31
- rot_img = cv2.warpAffine(img_bgr, M, (nW, nH), borderValue=self.bg_color)
32
-
33
- # Rotate Box Points
34
- pts = np.array([[hx1,hy1,1],[hx2,hy1,1],[hx2,hy2,1],[hx1,hy2,1]]).T
35
- rot_pts = M @ pts
36
- rx, ry = rot_pts[0,:], rot_pts[1,:]
37
- return rot_img, (int(rx.min()), int(ry.min()), int(rx.max()), int(ry.max()))
38
 
39
- def crop_to_ratio(self, img, box):
40
- bx1, by1, bx2, by2 = box
41
- bw, bh = bx2-bx1, by2-by1
42
- if bw/bh > self.target_aspect: # Too wide
43
- new_h = int(bw/self.target_aspect)
44
- by1 -= (new_h - bh)//2; by2 = by1 + new_h
45
- else: # Too tall
46
- new_w = int(bh*self.target_aspect)
47
- bx1 -= (new_w - bw)//2; bx2 = bx1 + new_w
48
-
49
- # Canvas Crop
50
- H, W = img.shape[:2]
51
- canvas = np.full((by2-by1, bx2-bx1, 3), self.bg_color, dtype=np.uint8)
52
- sx1, sy1 = max(0, bx1), max(0, by1)
53
- sx2, sy2 = min(W, bx2), min(H, by2)
54
- dx1, dy1 = max(0, sx1-bx1), max(0, sy1-by1)
55
- if sx2>sx1 and sy2>sy1: canvas[dy1:dy1+(sy2-sy1), dx1:dx1+(sx2-sx1)] = img[sy1:sy2, sx1:sx2]
56
- return cv2.resize(canvas, (self.out_w, self.out_h), interpolation=cv2.INTER_AREA)
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import numpy as np
3
 
4
  class PassportCropper:
5
+ def __init__(self, output_size=(413, 531), bg_color=(255, 255, 255)):
6
+ # Default size: 3.5cm x 4.5cm @ 300 DPI
7
  self.out_w, self.out_h = output_size
8
  self.bg_color = tuple(int(x) for x in bg_color)
9
  self.target_aspect = self.out_w / self.out_h
10
 
11
  def composite(self, img_rgb, mask):
12
+ """Standard background removal composite"""
13
  bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
14
+ if mask is None: return bgr
15
+
16
+ # Blur mask slightly for better blending
17
  m = cv2.GaussianBlur(mask, (5,5), 0)
18
  alpha = (m.astype(float) / 255.0)[:,:,None]
19
  bg = np.full_like(bgr, np.array(self.bg_color, dtype=np.uint8))
20
+
21
+ # Blend
22
  return (bgr.astype(float) * alpha + bg.astype(float) * (1.0 - alpha)).astype(np.uint8)
23
 
24
+ def crop_with_dynamic_zoom(self, img_bgr, angle_deg, raw_face_box, head_percent=0.75):
25
+ """
26
+ REPLACES 'rotate_and_expand_face' AND 'crop_to_ratio'.
27
+
28
+ This function guarantees the face height is exactly 'head_percent' (75%)
29
+ of the final image height, performing rotation and padding in one step.
30
+ """
31
  h0, w0 = img_bgr.shape[:2]
32
+ fx, fy, fw, fh = raw_face_box
33
+
34
+ # 1. Estimate Real Head Height (Chin to Top of Hair)
35
+ # Haar usually detects eyebrows to chin. We multiply by 1.35 to get full head.
36
+ estimated_head_h = fh * 1.35
37
+
38
+ # 2. Calculate the Required Canvas Height based on the 75% rule
39
+ # If head needs to be 75%, then Canvas = Head / 0.75
40
+ required_crop_h = int(estimated_head_h / head_percent)
41
+
42
+ # 3. Calculate Width based on the Aspect Ratio of the output (e.g. 3.5cm / 4.5cm)
43
+ required_crop_w = int(required_crop_h * self.target_aspect)
 
 
 
 
 
 
 
44
 
45
+ # 4. Find the center of the face
46
+ face_center_x = fx + fw // 2
47
+ face_center_y = fy + fh // 2
48
+
49
+ # 5. Determine the center of the Crop
50
+ # Eyes are usually at 45-50% from the top, not dead center.
51
+ # We shift the crop box UP slightly so the face sits nicely.
52
+ shift_y = int(required_crop_h * 0.05)
53
+ center_x = face_center_x
54
+ center_y = face_center_y - shift_y
 
 
 
 
 
 
 
 
55
 
56
+ # 6. Create the Rotation Matrix
57
+ # We rotate around the calculated center
58
+ M = cv2.getRotationMatrix2D((center_x, center_y), -angle_deg, 1.0)
59
+
60
+ # 7. Modify the Matrix for Translation
61
+ # This is the math magic: We tell OpenCV to move the (center_x, center_y)
62
+ # to the center of our new (required_crop_w, required_crop_h) image.
63
+ M[0, 2] += (required_crop_w / 2) - center_x
64
+ M[1, 2] += (required_crop_h / 2) - center_y
65
+
66
+ # 8. Generate the Final Image
67
+ # cv2.warpAffine does the rotation, cropping, and background padding all at once.
68
+ result = cv2.warpAffine(
69
+ img_bgr,
70
+ M,
71
+ (required_crop_w, required_crop_h),
72
+ borderValue=self.bg_color,
73
+ flags=cv2.INTER_LINEAR
74
+ )
75
+
76
+ # 9. Resize to the final output pixels (e.g. 413x531)
77
+ return cv2.resize(result, (self.out_w, self.out_h), interpolation=cv2.INTER_AREA)
server.py CHANGED
@@ -268,9 +268,12 @@ def create_app() -> Flask:
268
  cropper = PassportCropper(output_size=output_size, bg_color=selected_bg)
269
 
270
  img_clean = cropper.composite(img_rgb, mask)
271
- img_rot, rot_box = cropper.rotate_and_expand_face(img_clean, angle, (x,y,x+w,y+h))
272
- final_passport = cropper.crop_to_ratio(img_rot, rot_box)
273
-
 
 
 
274
  # Return result
275
  is_success, buffer = cv2.imencode(".jpg", final_passport)
276
  return send_file(io.BytesIO(buffer), mimetype='image/jpeg')
 
268
  cropper = PassportCropper(output_size=output_size, bg_color=selected_bg)
269
 
270
  img_clean = cropper.composite(img_rgb, mask)
271
+ final_passport = cropper.crop_with_dynamic_zoom(
272
+ img_clean,
273
+ angle,
274
+ (x, y, w, h),
275
+ head_percent=0.75
276
+ )
277
  # Return result
278
  is_success, buffer = cv2.imencode(".jpg", final_passport)
279
  return send_file(io.BytesIO(buffer), mimetype='image/jpeg')