fer-inference / detect_face.py
kshitizgajurel's picture
Deploy FER inference app
eec43fb
Raw
History Blame Contribute Delete
4.18 kB
import warnings
from typing import Optional
import cv2
import numpy as np
from PIL import Image
def _load_mtcnn(device='cpu'):
"""Attempt to import and return MTCNN detector. Returns None on failure."""
try:
from facenet_pytorch import MTCNN
return MTCNN(keep_all=True, device=device, post_process=False)
except ImportError:
warnings.warn(
"facenet-pytorch not installed — falling back to Haar cascade. "
"Install with: pip install facenet-pytorch",
RuntimeWarning,
stacklevel=3
)
return None
def _pil_to_bgr(image: Image.Image) -> np.ndarray:
rgb = np.array(image.convert('RGB'))
return cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
def _bgr_to_pil(frame: np.ndarray) -> Image.Image:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
return Image.fromarray(rgb)
def _crop_with_margin(image: Image.Image, x1: int, y1: int, x2: int, y2: int, margin: int) -> tuple:
"""Apply margin, clamp to image bounds, return (cropped_pil, bbox_xyxy)."""
w, h = image.size
x1 = max(0, x1 - margin)
y1 = max(0, y1 - margin)
x2 = min(w, x2 + margin)
y2 = min(h, y2 + margin)
return image.crop((x1, y1, x2, y2)), (x1, y1, x2 - x1, y2 - y1) # bbox as (x,y,w,h)
def detect_and_crop_faces(
image,
method: str = 'mtcnn',
margin: int = 20,
device: str = 'cpu'
) -> list[tuple]:
"""
Detect faces and return crops.
Args:
image: PIL Image, numpy array (H,W,3 BGR or RGB), or file path str.
method: 'mtcnn' (with auto-fallback) or 'haar'.
margin: pixel margin to add around each detected bbox.
device: torch device string used by MTCNN.
Returns:
List of (face_crop_PIL, bbox_xywh) tuples.
bbox_xywh is (x, y, w, h) in original image coords, or None if no face detected.
If no face is detected the full image (resized to 224) is returned with bbox=None.
"""
# Normalise to PIL RGB
if isinstance(image, str):
pil_img = Image.open(image).convert('RGB')
elif isinstance(image, np.ndarray):
if image.ndim == 3 and image.shape[2] == 3:
# Assume BGR (OpenCV convention)
pil_img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
else:
pil_img = Image.fromarray(image).convert('RGB')
elif isinstance(image, Image.Image):
pil_img = image.convert('RGB')
else:
raise TypeError(f"Unsupported image type: {type(image)}")
faces = []
if method == 'mtcnn':
mtcnn = _load_mtcnn(device=device)
if mtcnn is not None:
faces = _detect_mtcnn(mtcnn, pil_img, margin)
else:
method = 'haar'
if method == 'haar' or (method == 'mtcnn' and not faces):
faces = _detect_haar(pil_img, margin)
if not faces:
warnings.warn(
"No face detected — running inference on full image.",
RuntimeWarning,
stacklevel=2
)
fallback = pil_img.resize((224, 224))
return [(fallback, None)]
return faces
def _detect_mtcnn(mtcnn, pil_img: Image.Image, margin: int) -> list[tuple]:
import torch
boxes, probs = mtcnn.detect(pil_img)
if boxes is None or len(boxes) == 0:
return []
results = []
for box in boxes:
x1, y1, x2, y2 = (int(v) for v in box)
crop, bbox = _crop_with_margin(pil_img, x1, y1, x2, y2, margin)
results.append((crop, bbox))
return results
def _detect_haar(pil_img: Image.Image, margin: int) -> list[tuple]:
bgr = _pil_to_bgr(pil_img)
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
cascade = cv2.CascadeClassifier(cascade_path)
detected = cascade.detectMultiScale(
gray,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30)
)
if len(detected) == 0:
return []
results = []
for (x, y, w, h) in detected:
crop, bbox = _crop_with_margin(pil_img, x, y, x + w, y + h, margin)
results.append((crop, bbox))
return results