prism-api-backend / ai_models /prism /feature_engine.py
TanishkOrion's picture
Deploy isolated API Backend
e1404c1
Raw
History Blame Contribute Delete
6.37 kB
import cv2
import numpy as np
import mediapipe as mp
from scipy.stats import entropy
import warnings
warnings.filterwarnings("ignore")
# Initialize MediaPipe Face Detection (Faster than full Face Mesh for simple bounding boxes)
mp_face_detection = mp.solutions.face_detection
class FaceKalman:
"""Smoothes the bounding box to prevent jitter from corrupting optical flow/motion calculations."""
def __init__(self):
self.kf = cv2.KalmanFilter(4, 4)
self.kf.measurementMatrix = np.array(
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], np.float32
)
self.kf.transitionMatrix = np.array(
[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], np.float32
)
self.kf.processNoiseCov = np.eye(4, dtype=np.float32) * 0.01
def update(self, x, y, w, h):
measured = np.array(
[[np.float32(x)], [np.float32(y)], [np.float32(w)], [np.float32(h)]]
)
self.kf.correct(measured)
return self.kf.predict().flatten().astype(int)
class PrismFeatureExtractor:
def __init__(self):
# The exact 10 features expected by your trained MLPRegressor
self.detector = mp_face_detection.FaceDetection(
model_selection=1, min_detection_confidence=0.5
)
def _process_single_frame(self, frame, prev_gray_full, kf):
"""Extracts the 11 raw optical primitives from a single frame."""
h_f, w_f, _ = frame.shape
gray_full = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Fast Face Detection
results = self.detector.process(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
if not results.detections:
raw_x, raw_y, raw_w, raw_h = w_f // 4, h_f // 4, w_f // 2, h_f // 2
else:
bbox = results.detections[0].location_data.relative_bounding_box
raw_x, raw_y = int(bbox.xmin * w_f), int(bbox.ymin * h_f)
raw_w, raw_h = int(bbox.width * w_f), int(bbox.height * h_f)
x, y, w, h = kf.update(raw_x, raw_y, raw_w, raw_h)
x, y = max(0, x), max(0, y)
w, h = max(1, min(w_f - x, w)), max(1, min(h_f - y, h))
face_roi = frame[y : y + h, x : x + w]
if face_roi.size == 0:
return np.zeros(10), gray_full
face_g = face_roi[:, :, 1]
face_gray = gray_full[y : y + h, x : x + w]
# Physics Metrics
phi = np.mean(face_g) / 255.0
ycrcb = cv2.cvtColor(face_roi, cv2.COLOR_BGR2YCrCb)
mask = cv2.inRange(ycrcb, np.array([0, 133, 77]), np.array([255, 173, 127]))
mu = np.mean(ycrcb[:, :, 0][mask > 0]) / 255.0 if np.sum(mask) > 0 else 0.5
sigma = (
np.std(face_g[mask > 0]) / 50.0
if np.sum(mask) > 0
else np.std(face_g) / 50.0
)
chi = min(cv2.Laplacian(face_gray, cv2.CV_64F).var(), 1000.0) / 1000.0
hist, _ = np.histogram(face_g.flatten(), bins=20, density=True)
H = entropy(hist + 1e-10) / 5.0
clip = (np.sum(face_g > 250) + np.sum(face_g < 5)) / face_g.size
# Motion Calculation (Optical Flow)
m_val = 0.0
if prev_gray_full is not None:
try:
prev_face = prev_gray_full[y : y + h, x : x + w]
if prev_face.shape == face_gray.shape:
flow = cv2.calcOpticalFlowFarneback(
prev_face, face_gray, None, 0.5, 3, 15, 3, 5, 1.2, 0
)
m_val = np.mean(np.linalg.norm(flow, axis=2))
except:
pass
res = (w * h) / (w_f * h_f)
f_fft = np.fft.fft2(gray_full)
mag = 20 * np.log(np.abs(np.fft.fftshift(f_fft)) + 1e-10)
cy, cx = h_f // 2, w_f // 2
ghost = np.mean(mag[cy - 10 : cy + 10, cx - 10 : cx + 10]) / (
np.mean(mag) + 1e-5
)
return [
phi,
sigma,
mu,
chi,
H,
clip,
m_val,
res,
np.mean(face_g),
ghost,
], gray_full
def extract_from_video(self, video_path: str, max_samples=300) -> np.ndarray:
"""Reads video, uniformly samples frames, and returns the final 10 engineered features."""
kf = FaceKalman()
accumulators = {i: [] for i in range(10)}
prev_gray = None
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"Could not open video file: {video_path}")
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
step = max(1, total // max_samples)
count = processed = 0
while processed < max_samples:
ret, frame = cap.read()
if not ret:
break
if count % step == 0:
stats, prev_gray = self._process_single_frame(frame, prev_gray, kf)
if np.sum(stats) > 0:
for idx, val in enumerate(stats):
accumulators[idx].append(val)
processed += 1
count += 1
cap.release()
if processed < 10:
raise ValueError("Video too short or face not reliably detected.")
# 1. Aggregate into Base Primitives
phi = np.mean(accumulators[0])
sigma = np.mean(accumulators[1])
mu = np.mean(accumulators[2])
chi = np.mean(accumulators[3])
motion = min(np.percentile(accumulators[6], 95), 5.0) / 5.0
ghost = min(np.mean(accumulators[9]), 5.0) / 5.0
# 2. Compute the 10 Engineered Features (Matching Training Config)
sigma_safe = sigma + 1e-6
snr = phi / sigma_safe
stability = mu / (sigma_safe + 1e-3)
motion_delta = 0.0 # For a single inference video, cross-video delta is 0
purity_score = (phi * chi) / (sigma_safe * ghost + 1e-6)
# Output shape: (1, 10)
engineered_features = np.array(
[
[
snr,
stability,
motion,
motion_delta,
purity_score,
phi,
sigma,
chi,
ghost,
np.log1p(np.abs(phi)),
]
]
)
return engineered_features