Spaces:
Sleeping
Sleeping
File size: 7,380 Bytes
b0c6daf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """
Performance Optimizations β frame deduplication + FP16 inference.
This module provides:
1. Frame Deduplication: Detects when a face hasn't significantly changed
between frames (e.g., static webcam feed) and returns the cached
prediction instead of running full inference again. Saves 50-70%
of unnecessary GPU operations.
2. FP16 Inference Wrapper: When CUDA is available, runs the model in
float16 mode for ~1.5x speedup and 40% less VRAM.
3. Lightweight perceptual hash for fast frame comparison.
Usage (from main.py):
from performance import FrameDeduplicator, maybe_enable_fp16
dedup = FrameDeduplicator(threshold=0.98)
maybe_enable_fp16(model, device)
# In /predict:
cached = dedup.check(session_id, face_pil)
if cached:
return cached # skip inference entirely
# ... run model ...
dedup.store(session_id, face_pil, result)
"""
import time
import hashlib
from collections import defaultdict
from typing import Optional
import numpy as np
from PIL import Image
import torch
# ββββββββββββββββββββββββββββββββββββββ
# Perceptual Hash (pHash)
# ββββββββββββββββββββββββββββββββββββββ
def _compute_phash(img: Image.Image, hash_size: int = 8) -> str:
"""
Compute a perceptual hash of an image.
Resizes to (hash_size+1) Γ hash_size, computes DCT-like differences,
and returns a hex string.
"""
# Resize to small grayscale
small = img.convert('L').resize((hash_size + 1, hash_size), Image.LANCZOS)
pixels = np.array(small, dtype=np.float64)
# Compute differences (approximates DCT behavior)
diff = pixels[:, 1:] > pixels[:, :-1]
# Convert boolean array to hash
return hashlib.md5(diff.tobytes()).hexdigest()
def _compute_similarity(img1: Image.Image, img2: Image.Image) -> float:
"""
Compute structural similarity between two face crops.
Uses downscaled pixel-level MSE as a fast proxy.
Returns 0.0 (totally different) to 1.0 (identical).
"""
# Resize both to small thumbnails
size = (32, 32)
a = np.array(img1.convert('L').resize(size, Image.LANCZOS), dtype=np.float64)
b = np.array(img2.convert('L').resize(size, Image.LANCZOS), dtype=np.float64)
# Normalized MSE β similarity
mse = np.mean((a - b) ** 2)
max_mse = 255.0 ** 2
similarity = 1.0 - (mse / max_mse)
return round(similarity, 4)
# ββββββββββββββββββββββββββββββββββββββ
# Frame Deduplicator
# ββββββββββββββββββββββββββββββββββββββ
class FrameDeduplicator:
"""
Caches recent predictions per session and skips inference when
the incoming face is nearly identical to the last analyzed face.
Args:
threshold: Similarity threshold (0.0β1.0). Above this, the frame
is considered a duplicate and the cached result is returned.
Default 0.97 is conservative β catches static feeds.
ttl_seconds: How long to keep cached results before forcing re-analysis.
Prevents stale results if the scene changes slowly.
max_sessions: Maximum number of active sessions to track.
"""
def __init__(
self,
threshold: float = 0.97,
ttl_seconds: float = 10.0,
max_sessions: int = 200,
):
self.threshold = threshold
self.ttl_seconds = ttl_seconds
self.max_sessions = max_sessions
self._cache: dict = {}
def check(self, session_id: str, face_pil: Image.Image) -> Optional[dict]:
"""
Check if this frame is similar enough to the last one to skip inference.
Returns:
dict with cached result if duplicate, None if new frame needs analysis.
Cached result includes an extra field: "dedup_cache_hit": True
"""
if session_id not in self._cache:
return None
entry = self._cache[session_id]
# Check TTL
age = time.time() - entry["timestamp"]
if age > self.ttl_seconds:
del self._cache[session_id]
return None
# Compute similarity
similarity = _compute_similarity(face_pil, entry["face_pil"])
if similarity >= self.threshold:
# Cache hit β return stored result with marker
cached_result = entry["result"].copy()
cached_result["dedup_cache_hit"] = True
cached_result["dedup_similarity"] = similarity
return cached_result
return None
def store(
self,
session_id: str,
face_pil: Image.Image,
result: dict,
):
"""
Store the latest prediction for a session.
Automatically evicts oldest sessions if at capacity.
"""
# Evict oldest if at capacity
if len(self._cache) >= self.max_sessions and session_id not in self._cache:
oldest_key = min(self._cache, key=lambda k: self._cache[k]["timestamp"])
del self._cache[oldest_key]
self._cache[session_id] = {
"face_pil": face_pil.copy().resize((64, 64), Image.LANCZOS), # store small
"result": result,
"timestamp": time.time(),
}
def clear_session(self, session_id: str):
"""Remove a session from the cache."""
self._cache.pop(session_id, None)
@property
def active_sessions(self) -> int:
return len(self._cache)
# ββββββββββββββββββββββββββββββββββββββ
# FP16 Inference Optimization
# ββββββββββββββββββββββββββββββββββββββ
def maybe_enable_fp16(model: torch.nn.Module, device: torch.device) -> bool:
"""
Attempts to enable FP16 (half-precision) inference on CUDA.
Returns True if FP16 was enabled, False otherwise.
FP16 provides:
- ~1.5x inference speedup
- ~40% less VRAM usage
- Negligible accuracy loss for inference (not training)
Note: Only applies to CUDA devices. CPU stays FP32.
"""
if device.type != 'cuda':
print("[PERF] FP16 skipped β CPU mode, staying FP32")
return False
try:
model.half()
print("[PERF] FP16 inference enabled β faster, less VRAM")
return True
except Exception as e:
print(f"[PERF] FP16 failed, staying FP32: {e}")
# Revert to FP32
model.float()
return False
def prepare_input_fp16(tensor: torch.Tensor, device: torch.device) -> torch.Tensor:
"""
Converts input tensor to FP16 if the device is CUDA.
Call this before feeding into a model that has been half()'d.
"""
if device.type == 'cuda':
return tensor.to(device).half()
return tensor.to(device)
|