codernotme's picture
update
7f3db4a verified
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}