esmaill1
feat: implement image processing core, FastAPI backend, and full-stack integration tests
f19ba0f | 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) | |
| ) | |