Sign2Voice / signspeak /live_debug.py
lilblueyes's picture
Speed up live ASL debug diagnostics
6dc8a0d
Raw
History Blame Contribute Delete
7.27 kB
from __future__ import annotations
import os
from typing import Any
import numpy as np
from .asl.asl_detector import ASLDetector
from .asl.mediapipe_utils import draw_holistic_landmarks, extract_keypoints_from_holistic
class LiveASLSession:
def __init__(self) -> None:
self.asl = ASLDetector()
self.interpreter: Any | None = None
self.frame_keypoints: list[np.ndarray] = []
self.latest_prediction = ""
self.latest_top_candidate = ""
self.latest_top_predictions: list[dict[str, Any]] = []
self.latest_emotion = "unknown"
self.min_prediction_frames = max(1, int(os.getenv("LIVE_ASL_MIN_FRAMES", "4")))
self.max_prediction_frames = max(self.min_prediction_frames, int(os.getenv("LIVE_ASL_MAX_FRAMES", "12")))
self.predict_every = max(1, int(os.getenv("LIVE_ASL_PREDICT_EVERY", "1")))
self.emotion_every = max(1, int(os.getenv("LIVE_EMOTION_EVERY", "45")))
self.latest_status = f"Waiting for live frames: 0/{self.min_prediction_frames}."
self.frames_seen = 0
self.mp: Any | None = None
self.holistic: Any | None = None
self._load_mediapipe()
def process_frame(self, frame_rgb: np.ndarray) -> tuple[np.ndarray, str]:
output = frame_rgb.copy()
self.frames_seen += 1
if self.mp is None or self.holistic is None:
self.latest_status = "MediaPipe unavailable."
return self._draw(output), self.latest_status
try:
results = self.holistic.process(frame_rgb)
draw_holistic_landmarks(self.mp, output, results)
keypoints = extract_keypoints_from_holistic(results, missing_value=np.nan)
self.frame_keypoints.append(keypoints)
self.frame_keypoints = self.frame_keypoints[-self.max_prediction_frames :]
if self.frames_seen % self.emotion_every == 0:
self._update_emotion(frame_rgb)
if len(self.frame_keypoints) < self.min_prediction_frames:
self.latest_status = f"Waiting for live frames: {len(self.frame_keypoints)}/{self.min_prediction_frames}."
elif self.frames_seen % self.predict_every == 0:
self._predict_latest()
except Exception as exc:
self.latest_status = f"Live ASL error: {type(exc).__name__}: {exc}"
return self._draw(output), self._status_text()
def _load_mediapipe(self) -> None:
try:
import mediapipe as mp
self.mp = mp
self.holistic = mp.solutions.holistic.Holistic(
model_complexity=0,
min_detection_confidence=0.5,
min_tracking_confidence=0.5,
)
except Exception as exc:
self.latest_status = f"MediaPipe unavailable: {type(exc).__name__}: {exc}"
def _predict_latest(self) -> None:
if not self.asl.model_path.exists():
self.latest_prediction = ""
self.latest_status = "ASL model missing."
return
if self.interpreter is None:
self.interpreter = self.asl._load_interpreter()
keypoints = np.asarray(self.frame_keypoints, dtype=np.float32)
output = self.asl._predict(self.interpreter, keypoints)
probs = self.asl._softmax_if_needed(np.asarray(output).reshape(-1))
top_idx = int(np.argmax(probs))
label = self.asl._label_for_index(top_idx)
confidence = float(probs[top_idx])
top_predictions = self.asl._top_predictions(probs, limit=3)
self.latest_top_predictions = top_predictions
self.latest_top_candidate = f"{label} ({confidence:.0%})"
if confidence >= self.asl.confidence_threshold:
self.latest_prediction = f"{label} ({confidence:.0%})"
self.latest_status = (
f"Accepted: {label} at {confidence:.2f} "
f"with {len(self.frame_keypoints)} live frames."
)
else:
self.latest_prediction = ""
self.latest_status = (
f"Top candidate: {label} at {confidence:.2f}; "
f"waiting for {self.asl.confidence_threshold:.2f}."
)
def _update_emotion(self, frame_rgb: np.ndarray) -> None:
try:
os.environ.setdefault("TF_USE_LEGACY_KERAS", "1")
import cv2
from deepface import DeepFace
frame_bgr = cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR)
analysis = DeepFace.analyze(
img_path=frame_bgr,
actions=["emotion"],
enforce_detection=False,
silent=True,
)
if isinstance(analysis, list):
analysis = analysis[0] if analysis else {}
dominant = analysis.get("dominant_emotion") if isinstance(analysis, dict) else None
if dominant:
self.latest_emotion = str(dominant)
except Exception:
self.latest_emotion = "unavailable"
def _draw(self, frame_rgb: np.ndarray) -> np.ndarray:
import cv2
height, width = frame_rgb.shape[:2]
cv2.rectangle(frame_rgb, (0, 0), (width, 146), (8, 11, 16), -1)
cv2.putText(
frame_rgb,
f"Sign: {self.latest_prediction or '-'}",
(14, 36),
cv2.FONT_HERSHEY_SIMPLEX,
0.86,
(45, 212, 191),
2,
cv2.LINE_AA,
)
cv2.putText(
frame_rgb,
f"Top: {self.latest_top_candidate or '-'}",
(14, 68),
cv2.FONT_HERSHEY_SIMPLEX,
0.58,
(129, 140, 248),
2,
cv2.LINE_AA,
)
cv2.putText(
frame_rgb,
f"Emotion: {self.latest_emotion}",
(14, 98),
cv2.FONT_HERSHEY_SIMPLEX,
0.58,
(245, 158, 11),
2,
cv2.LINE_AA,
)
cv2.putText(
frame_rgb,
self.latest_status[:96],
(14, 128),
cv2.FONT_HERSHEY_SIMPLEX,
0.48,
(248, 250, 252),
1,
cv2.LINE_AA,
)
return frame_rgb
def _status_text(self) -> str:
top_lines = [
f"- {item.get('label')}: {float(item.get('confidence', 0.0) or 0.0):.2f}"
for item in self.latest_top_predictions
]
top_block = "\n".join(top_lines) if top_lines else "- None yet"
return (
f"{self.latest_status}\n"
f"Accepted sign: {self.latest_prediction or 'None'}\n"
f"Top candidates:\n{top_block}\n"
f"Frames in rolling buffer: {len(self.frame_keypoints)}/{self.max_prediction_frames}\n"
f"Acceptance threshold: {self.asl.confidence_threshold:.2f}\n"
f"Emotion: {self.latest_emotion}"
)
_live_session: LiveASLSession | None = None
def process_live_debug_frame(frame: np.ndarray | None) -> tuple[np.ndarray | None, str]:
global _live_session
if frame is None:
return None, "Waiting for camera frame."
if _live_session is None:
_live_session = LiveASLSession()
return _live_session.process_frame(frame)