import cv2 import os from PIL import Image import sys import cv2.data import numpy as np # Locate the standard frontal face XML classifier provided by OpenCV 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) # Convert to BGR for OpenCV 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: # Fallback: Center crop if no face found 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) # Adjust to maintain size 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] # Use Lanczos resampling for better quality final = cv2.resize(cropped, (1181, 1654), interpolation=cv2.INTER_LANCZOS4) final_rgb = cv2.cvtColor(final, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(final_rgb) ext = os.path.splitext(output_path)[1].lower() if ext == ".png": pil_img.save(output_path, dpi=(300, 300), compress_level=1) # Low compression for speed, lossless else: pil_img.save(output_path, dpi=(300, 300), quality=100, subsampling=0) 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) )