import os import cv2 import numpy as np import torch from PIL import Image from torch.autograd import Variable from torchvision import transforms def _mask_path(filename: str) -> str: assets_path = os.path.join("assets", filename) if os.path.exists(assets_path): return assets_path return filename mask_file = torch.from_numpy( np.array(Image.open(_mask_path("mask1024.jpg")).convert("L")) ).float() / 255.0 small_mask_file = torch.from_numpy( np.array(Image.open(_mask_path("mask512.jpg")).convert("L")) ).float() / 255.0 def _detect_face_box(image_rgb: np.ndarray): """Detect face using OpenCV Haar Cascade. Returns (top, right, bottom, left).""" gray = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2GRAY) cascade_path = os.path.join( cv2.data.haarcascades, "haarcascade_frontalface_default.xml", ) face_cascade = cv2.CascadeClassifier(cascade_path) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(60, 60), ) if len(faces) == 0: return None # Choose the largest detected face. x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) return (y, x + w, y + h, x) def _fallback_center_face_box(image_rgb: np.ndarray): """Fallback crop when no face is detected, so API still returns an image.""" h, w = image_rgb.shape[:2] box_size = int(min(w, h) * 0.72) cx, cy = w // 2, h // 2 left = max(cx - box_size // 2, 0) right = min(cx + box_size // 2, w) top = max(cy - box_size // 2, 0) bottom = min(cy + box_size // 2, h) return (top, right, bottom, left) def sliding_window_tensor( input_tensor, window_size, stride, your_model, mask=mask_file, small_mask=small_mask_file, ): input_tensor = input_tensor.to(next(your_model.parameters()).device) mask = mask.to(next(your_model.parameters()).device) small_mask = small_mask.to(next(your_model.parameters()).device) n, c, h, w = input_tensor.size() output_tensor = torch.zeros((n, 3, h, w), dtype=input_tensor.dtype, device=input_tensor.device) count_tensor = torch.zeros((n, 3, h, w), dtype=torch.float32, device=input_tensor.device) add = 2 if window_size % stride != 0 else 1 for y in range(0, h - window_size + add, stride): for x in range(0, w - window_size + add, stride): window = input_tensor[:, :, y : y + window_size, x : x + window_size] input_variable = Variable(window, requires_grad=False) with torch.no_grad(): output = your_model(input_variable) output_tensor[:, :, y : y + window_size, x : x + window_size] += output * small_mask count_tensor[:, :, y : y + window_size, x : x + window_size] += small_mask count_tensor = torch.clamp(count_tensor, min=1.0) output_tensor /= count_tensor output_tensor *= mask return output_tensor.cpu() def full_image_tensor( input_tensor, your_model, mask=mask_file, ): """Run the model once on the full 1024x1024 tensor. This replaces the old sliding-window path that ran the model 9 times and blended overlapping patches. It is much faster, but output can be slightly different from the previous blended result. """ device = next(your_model.parameters()).device input_tensor = input_tensor.to(device) mask = mask.to(device) with torch.no_grad(): output_tensor = your_model(input_tensor) output_tensor = output_tensor * mask return output_tensor.cpu() def process_image(your_model, image, source_age, target_age=0, window_size=512, stride=256): input_size = (1024, 1024) image_np = np.array(image.convert("RGB")) fl = _detect_face_box(image_np) if fl is None: fl = _fallback_center_face_box(image_np) margin_y_t = int((fl[2] - fl[0]) * 0.63 * 0.85) margin_y_b = int((fl[2] - fl[0]) * 0.37 * 0.85) margin_x = int((fl[1] - fl[3]) // (2 / 0.85)) margin_y_t += 2 * margin_x - margin_y_t - margin_y_b l_y = max(fl[0] - margin_y_t, 0) r_y = min(fl[2] + margin_y_b, image_np.shape[0]) l_x = max(fl[3] - margin_x, 0) r_x = min(fl[1] + margin_x, image_np.shape[1]) cropped_image = image_np[l_y:r_y, l_x:r_x, :] orig_size = cropped_image.shape[:2] cropped_image = transforms.ToTensor()(cropped_image) cropped_image_resized = transforms.Resize( input_size, interpolation=Image.BILINEAR, antialias=True, )(cropped_image) source_age_channel = torch.full_like(cropped_image_resized[:1, :, :], source_age / 100) target_age_channel = torch.full_like(cropped_image_resized[:1, :, :], target_age / 100) input_tensor = torch.cat( [cropped_image_resized, source_age_channel, target_age_channel], dim=0, ).unsqueeze(0) original_image_tensor = transforms.ToTensor()(image_np) # Fast path: run the model once on the full 1024x1024 crop. # Old rollback path: # aged_cropped_image = sliding_window_tensor(input_tensor, window_size, stride, your_model) aged_cropped_image = full_image_tensor( input_tensor, your_model, ) aged_cropped_image_resized = transforms.Resize( orig_size, interpolation=Image.BILINEAR, antialias=True, )(aged_cropped_image) original_image_tensor[:, l_y:r_y, l_x:r_x] += aged_cropped_image_resized.squeeze(0) original_image_tensor = torch.clamp(original_image_tensor, 0, 1) return transforms.functional.to_pil_image(original_image_tensor)