import logging import math import cv2 import numpy as np logger = logging.getLogger(__name__) def _expand_box(x, y, w, h, img_w, img_h, scale=1.2): pad_w = int(w * (scale - 1) / 2) pad_h = int(h * (scale - 1) / 2) x1 = max(0, x - pad_w) y1 = max(0, y - pad_h) x2 = min(img_w, x + w + pad_w) y2 = min(img_h, y + h + pad_h) return x1, y1, x2, y2 def _crop_to_face(img): cv_img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) face_cascade_path = cv2.data.haarcascades + "haarcascade_frontalface_alt2.xml" face_cascade = cv2.CascadeClassifier(face_cascade_path) if face_cascade.empty(): face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") faces = face_cascade.detectMultiScale(gray, 1.3, 5) if len(faces) == 0: logger.info("No face detected; using full image for classification.") return img x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) img_h, img_w = gray.shape[:2] x1, y1, x2, y2 = _expand_box(x, y, w, h, img_w, img_h, scale=1.25) return img.crop((x1, y1, x2, y2)) def _gaussian_score(value, target, sigma): return math.exp(-((value - target) ** 2) / (2 * sigma ** 2)) def _shape_probabilities_from_geometry(landmarks): from geometry import extract_features feats = extract_features(landmarks) lw_ratio = feats.get("lw_ratio", 0) jaw_ratio = feats.get("jaw_ratio", 0) if lw_ratio <= 0 or jaw_ratio <= 0: return None targets = { "Round": {"lw": 1.05, "jaw": 0.88}, "Square": {"lw": 1.12, "jaw": 0.95}, "Oval": {"lw": 1.25, "jaw": 0.86}, "Oblong": {"lw": 1.35, "jaw": 0.86}, "Heart": {"lw": 1.22, "jaw": 0.75}, "Diamond": {"lw": 1.20, "jaw": 0.70}, } scores = {} for shape, target in targets.items(): lw_score = _gaussian_score(lw_ratio, target["lw"], 0.1) jaw_score = _gaussian_score(jaw_ratio, target["jaw"], 0.07) scores[shape] = lw_score * jaw_score total = sum(scores.values()) if total <= 0: return None probabilities = {k: round(v / total, 4) for k, v in scores.items()} return dict(sorted(probabilities.items(), key=lambda item: item[1], reverse=True)) def _blend_probabilities(primary, secondary, alpha=0.55): labels = set(primary.keys()) | set(secondary.keys()) blended = {} for label in labels: blended[label] = alpha * primary.get(label, 0) + (1 - alpha) * secondary.get(label, 0) total = sum(blended.values()) if total <= 0: return primary blended = {k: round(v / total, 4) for k, v in blended.items()} return dict(sorted(blended.items(), key=lambda item: item[1], reverse=True)) def detect_face_shape(image_path): """ Detects face shape using PIL and trained SVM model. """ from PIL import Image from landmarks import get_landmarks from classifier import classify_face_shape try: # Load image with PIL if isinstance(image_path, str): img = Image.open(image_path) else: # Assume it's already a PIL image or numpy array img = image_path if img is None: raise ValueError("Could not load image") # Crop to detected face region to improve accuracy if isinstance(image_path, str): img = img.convert("RGB") img = _crop_to_face(img) geom_probs = None if isinstance(image_path, str): try: landmarks = get_landmarks(image_path) geom_probs = _shape_probabilities_from_geometry(landmarks) except Exception as e: logger.info(f"Landmark-based detection failed, falling back: {e}") # Classify directly (classifier handles resizing/grayscale) shape_probs = classify_face_shape(img) if geom_probs: geom_values = list(geom_probs.values()) top = geom_values[0] if geom_values else 0 runner_up = geom_values[1] if len(geom_values) > 1 else 0 if top >= 0.45 and (top - runner_up) >= 0.08: return geom_probs return _blend_probabilities(geom_probs, shape_probs, alpha=0.55) # Get the top shape best_shape = list(shape_probs.keys())[0] logger.info(f"Detected face shape: {best_shape} for {image_path}") return shape_probs except Exception as e: logger.error(f"Detection failed: {e}") return {"Error": 1.0}