| import cv2 |
| import os |
| from PIL import Image |
| import sys |
| import cv2.data |
| import numpy as np |
|
|
| |
| cascade_path = os.path.join( |
| cv2.data.haarcascades, "haarcascade_frontalface_default.xml" |
| ) |
| face_cascade = cv2.CascadeClassifier(cascade_path) |
|
|
|
|
| def _load_image_exif_safe(image_path): |
| """Loads an image using PIL, handles EXIF orientation, and converts to OpenCV BGR.""" |
| try: |
| from PIL import ImageOps |
| pil_img = Image.open(image_path) |
| pil_img = ImageOps.exif_transpose(pil_img) |
| |
| return cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR) |
| except Exception as e: |
| print(f"Error loading image safe: {e}") |
| return None |
|
|
|
|
| def get_auto_crop_rect(image_path): |
| """ |
| Detects a face and calculates the 5:7 crop rectangle. |
| Returns (x1, y1, x2, y2) in original image coordinates or None. |
| """ |
| image = _load_image_exif_safe(image_path) |
| if image is None: |
| return None |
| h, w, _ = image.shape |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
| faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) |
|
|
| if len(faces) == 0: |
| |
| aspect_ratio = 5 / 7 |
| crop_h = int(h * 0.8) |
| crop_w = int(crop_h * aspect_ratio) |
| x1 = (w - crop_w) // 2 |
| y1 = (h - crop_h) // 2 |
| return (x1, y1, x1 + crop_w, y1 + crop_h) |
|
|
| faces = sorted(faces, key=lambda x: x[2] * x[3], reverse=True) |
| (x, y, fw, fh) = faces[0] |
| cx, cy = x + fw // 2, y + fh // 2 |
| aspect_ratio = 5 / 7 |
|
|
| crop_height = int(min(h, w / aspect_ratio) * 0.7) |
| crop_width = int(crop_height * aspect_ratio) |
|
|
| head_top = y - int(fh * 0.35) |
| HEAD_SPACE_RATIO = 0.10 |
| y1 = max(0, head_top - int(crop_height * HEAD_SPACE_RATIO)) |
| x1 = max(0, cx - crop_width // 2) |
|
|
| x2 = min(w, x1 + crop_width) |
| y2 = min(h, y1 + crop_height) |
|
|
| |
| if x2 - x1 < crop_width: |
| x1 = max(0, x2 - crop_width) |
| if y2 - y1 < crop_height: |
| y1 = max(0, y2 - crop_height) |
|
|
| return (int(x1), int(y1), int(x1 + crop_width), int(y1 + crop_height)) |
|
|
|
|
| def apply_custom_crop(image_path, output_path, rect): |
| """ |
| Applies a specific (x1, y1, x2, y2) crop and resizes to 10x14cm @ 300DPI. |
| """ |
| x1, y1, x2, y2 = rect |
| try: |
| image = _load_image_exif_safe(image_path) |
| if image is None: return False |
|
|
| cropped = image[y1:y2, x1:x2] |
| final = cv2.resize(cropped, (1181, 1654)) |
| final_rgb = cv2.cvtColor(final, cv2.COLOR_BGR2RGB) |
| pil_img = Image.fromarray(final_rgb) |
| pil_img.save(output_path, dpi=(300, 300), quality=95) |
| return True |
| except Exception as e: |
| print(f"Error applying custom crop: {e}") |
| return False |
|
|
|
|
| def crop_to_4x6_opencv(image_path, output_path): |
| """Standard AI auto-crop.""" |
| rect = get_auto_crop_rect(image_path) |
| if rect: |
| return apply_custom_crop(image_path, output_path, rect) |
| return False |
|
|
|
|
| def batch_process(input_folder, output_folder): |
| if not os.path.exists(output_folder): |
| os.makedirs(output_folder) |
| files = [ |
| f |
| for f in os.listdir(input_folder) |
| if f.lower().endswith((".jpg", ".jpeg", ".png")) |
| ] |
| for filename in files: |
| crop_to_4x6_opencv( |
| os.path.join(input_folder, filename), os.path.join(output_folder, filename) |
| ) |
|
|