CoolFace
Apppublic

algoryn/validation

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
gesture_validator.py502 linesDownload Raw Back to validate
1"""2Gesture validation service for identity verification.3 4This module provides gesture validation functionality by leveraging the existing5gesture detection system in src/gesturedetection/. It processes user videos to6detect specific gestures and validates them against a list of required gestures.7"""8 9import os10import logging11import tempfile12from typing import List, Dict, Any, Optional, Tuple13from datetime import datetime, timezone14 15from .models import ValidationResult, ValidationStatus, GestureRequirement16 17logger = logging.getLogger(__name__)18 19 20class GestureValidator:21    """22    Gesture validation service for identity verification.23 24    This class processes user videos to detect and validate specific gestures25    against a list of required gestures. It uses the existing gesture detection26    pipeline from src/gesturedetection/ and provides configurable validation27    parameters including error margins and minimum requirements.28    """29 30    def __init__(31        self,32        detector_path: str = "models/hand_detector.onnx",33        classifier_path: str = "models/crops_classifier.onnx",34        frame_skip: int = 1,35        min_gesture_duration: int = 5,36        confidence_threshold: float = 0.737    ):38        """39        Initialize the gesture validator.40 41        Parameters42        ----------43        detector_path : str, optional44            Path to the hand detection ONNX model, by default "models/hand_detector.onnx"45        classifier_path : str, optional46            Path to the gesture classification ONNX model, by default "models/crops_classifier.onnx"47        frame_skip : int, optional48            Number of frames to skip between processing, by default 149        min_gesture_duration : int, optional50            Minimum duration for gesture detection, by default 551        confidence_threshold : float, optional52            Minimum confidence threshold for gesture detection, by default 0.753        """54        self.detector_path = detector_path55        self.classifier_path = classifier_path56        self.frame_skip = frame_skip57        self.min_gesture_duration = min_gesture_duration58        self.confidence_threshold = confidence_threshold59 60        # Import here to avoid circular imports and handle missing dependencies gracefully61        try:62            from ..gesturedetection.main_controller import MainController63            from ..gesturedetection.models import PRODUCTION_GESTURE_MAPPING64            self._main_controller_class = MainController65            self._gesture_mapping = PRODUCTION_GESTURE_MAPPING66            self._initialized = True67            logger.info("GestureValidator initialized successfully with PRODUCTION_GESTURE_MAPPING")68        except ImportError as e:69            logger.warning(f"Could not import gesture detection components: {e}")70            self._initialized = False71 72    def validate_gestures(73        self,74        video_path: str,75        required_gestures: List[str],76        error_margin: float = 0.33,77        require_all: bool = True78    ) -> ValidationResult:79        """80        Validate that required gestures are present in the video.81 82        Parameters83        ----------84        video_path : str85            Path to the video file to analyze86        required_gestures : List[str]87            List of gesture names that must be detected88        error_margin : float, optional89            Fraction of gestures that can be missed (0.0-1.0), by default 0.3390        require_all : bool, optional91            Whether all gestures must be present, by default True92 93        Returns94        -------95        ValidationResult96            Validation result with success status and detailed metrics97        """98        if not self._initialized:99            error_msg = "GestureValidator not properly initialized - missing gesture detection components"100            logger.error(error_msg)101            return ValidationResult(102                status=ValidationStatus.FAILED,103                success=False,104                confidence=0.0,105                error_message=error_msg106            )107 108        logger.info(f"Starting gesture validation for video: {video_path}")109        logger.info(f"Required gestures: {required_gestures}, error_margin: {error_margin}")110 111        # Validate input file112        if not os.path.exists(video_path):113            error_msg = f"Video file not found: {video_path}"114            logger.error(error_msg)115            return ValidationResult(116                status=ValidationStatus.FAILED,117                success=False,118                confidence=0.0,119                error_message=error_msg120            )121 122        # Validate required gestures123        if not required_gestures:124            error_msg = "No gestures specified for validation"125            logger.error(error_msg)126            return ValidationResult(127                status=ValidationStatus.FAILED,128                success=False,129                confidence=0.0,130                error_message=error_msg131            )132 133        try:134            # Process video using existing gesture detection pipeline135            detected_gestures = self._process_video_for_gestures(video_path)136 137            # Analyze detected gestures against requirements138            validation_metrics = self._analyze_gesture_requirements(139                detected_gestures, required_gestures, error_margin, require_all140            )141 142            # Determine overall success143            if require_all:144                success = validation_metrics["required_gestures_met"] >= len(required_gestures)145            else:146                # Allow for error margin147                min_required = max(1, int(len(required_gestures) * (1.0 - error_margin)))148                success = validation_metrics["required_gestures_met"] >= min_required149 150            # Calculate confidence based on detection quality151            confidence = self._calculate_confidence(detected_gestures, validation_metrics)152 153            status = ValidationStatus.SUCCESS if success else ValidationStatus.PARTIAL154 155            result = ValidationResult(156                status=status,157                success=success,158                confidence=confidence,159                details={160                    "detected_gestures": [161                        {162                            "gesture": g["gesture"],163                            "duration": g["duration"],164                            "confidence": g["confidence"]165                        }166                        for g in detected_gestures167                    ],168                    "validation_metrics": validation_metrics,169                    "required_gestures": required_gestures,170                    "error_margin": error_margin,171                    "require_all": require_all,172                    "processing_timestamp": datetime.now(timezone.utc).isoformat()173                }174            )175 176            logger.info(f"Gesture validation completed: success={success}, confidence={confidence}")177            return result178 179        except Exception as e:180            error_msg = f"Error during gesture validation: {str(e)}"181            logger.error(error_msg, exc_info=True)182            return ValidationResult(183                status=ValidationStatus.FAILED,184                success=False,185                confidence=0.0,186                error_message=error_msg187            )188 189    def _process_video_for_gestures(self, video_path: str) -> List[Dict[str, Any]]:190        """191        Process video file to detect gestures using existing pipeline.192 193        Parameters194        ----------195        video_path : str196            Path to the video file197 198        Returns199        -------200        List[Dict[str, Any]]201            List of detected gestures with metadata202        """203        logger.debug(f"Processing video for gestures: {video_path}")204 205        # Initialize the main controller206        controller = self._main_controller_class(self.detector_path, self.classifier_path)207 208        # Import video processing function from existing API209        try:210            from ..gesturedetection.api import process_video_for_gestures211            gestures = process_video_for_gestures(212                video_path,213                detector_path=self.detector_path,214                classifier_path=self.classifier_path,215                frame_skip=self.frame_skip216            )217        except ImportError:218            # Fallback: use controller directly if import fails219            logger.warning("Using fallback gesture processing method")220            gestures = self._process_video_with_controller(controller, video_path)221 222        # Convert to our internal format223        detected_gestures = []224        for gesture in gestures:225            # Map gesture names to standardized format226            gesture_name = self._normalize_gesture_name(gesture.gesture)227 228            detected_gestures.append({229                "gesture": gesture_name,230                "duration": gesture.duration,231                "confidence": gesture.confidence,232                "raw_gesture": gesture.gesture233            })234 235        logger.debug(f"Detected {len(detected_gestures)} gestures")236        return detected_gestures237 238    def _process_video_with_controller(self, controller, video_path: str) -> List[Dict[str, Any]]:239        """240        Fallback method to process video using controller directly.241 242        This is used if the import from api.py fails for any reason.243        """244        import cv2245        from collections import defaultdict246 247        logger.debug("Processing video with controller fallback method")248 249        # Open video file250        cap = cv2.VideoCapture(video_path)251        if not cap.isOpened():252            raise ValueError(f"Could not open video file: {video_path}")253 254        gesture_tracks = defaultdict(list)255        frame_count = 0256 257        try:258            while True:259                ret, frame = cap.read()260                if not ret:261                    break262 263                # Skip frames based on frame_skip parameter264                if frame_count % self.frame_skip == 0:265                    # Process frame through the controller266                    bboxes, ids, labels = controller(frame)267 268                    if bboxes is not None and ids is not None and labels is not None:269                        # Track gestures for each detected hand270                        for i in range(len(bboxes)):271                            hand_id = int(ids[i])272                            gesture_id = labels[i]273 274                            if gesture_id is not None:275                                confidence = 0.8  # Default confidence276                                gesture_tracks[hand_id].append((gesture_id, confidence))277 278                frame_count += 1279 280        finally:281            cap.release()282 283        # Process gesture tracks to find continuous gestures284        detected_gestures = []285 286        for hand_id, gesture_sequence in gesture_tracks.items():287            if not gesture_sequence:288                continue289 290            # Group consecutive identical gestures291            current_gesture = None292            current_duration = 0293            current_confidence = 0.0294 295            for gesture_id, confidence in gesture_sequence:296                if current_gesture is None or current_gesture != gesture_id:297                    # Save previous gesture if it was significant298                    if current_gesture is not None and current_duration >= self.min_gesture_duration:299                        gesture_name = self._gesture_mapping.get(current_gesture, f"unknown_{current_gesture}")300                        avg_confidence = current_confidence / current_duration if current_duration > 0 else 0.0301                        scaled_duration = current_duration * self.frame_skip302 303                        detected_gestures.append({304                            "gesture": gesture_name,305                            "duration": scaled_duration,306                            "confidence": avg_confidence307                        })308 309                    # Start new gesture310                    current_gesture = gesture_id311                    current_duration = 1312                    current_confidence = confidence313                else:314                    # Continue current gesture315                    current_duration += 1316                    current_confidence += confidence317 318            # Don't forget the last gesture319            if current_gesture is not None and current_duration >= self.min_gesture_duration:320                gesture_name = self._gesture_mapping.get(current_gesture, f"unknown_{current_gesture}")321                avg_confidence = current_confidence / current_duration if current_duration > 0 else 0.0322                scaled_duration = current_duration * self.frame_skip323 324                detected_gestures.append({325                    "gesture": gesture_name,326                    "duration": scaled_duration,327                    "confidence": avg_confidence328                })329 330        return detected_gestures331 332    def _analyze_gesture_requirements(333        self,334        detected_gestures: List[Dict[str, Any]],335        required_gestures: List[str],336        error_margin: float,337        require_all: bool338    ) -> Dict[str, Any]:339        """340        Analyze detected gestures against requirements.341 342        Parameters343        ----------344        detected_gestures : List[Dict[str, Any]]345            List of detected gestures346        required_gestures : List[str]347            List of required gesture names348        error_margin : float349            Error margin for validation350        require_all : bool351            Whether all gestures are required352 353        Returns354        -------355        Dict[str, Any]356            Validation metrics and analysis357        """358        logger.debug("Analyzing gesture requirements")359 360        # Create lookup for detected gestures361        detected_gesture_counts = {}362        for gesture in detected_gestures:363            gesture_name = gesture["gesture"]364            if gesture_name not in detected_gesture_counts:365                detected_gesture_counts[gesture_name] = []366            detected_gesture_counts[gesture_name].append(gesture)367 368        # Analyze each required gesture369        required_gestures_met = 0370        gesture_analysis = {}371 372        for required_gesture in required_gestures:373            detected_instances = detected_gesture_counts.get(required_gesture, [])374 375            # Filter by minimum duration and confidence if specified376            valid_instances = [377                g for g in detected_instances378                if g["duration"] >= self.min_gesture_duration and379                g["confidence"] >= self.confidence_threshold380            ]381 382            met_requirement = len(valid_instances) > 0383 384            gesture_analysis[required_gesture] = {385                "required": True,386                "detected": len(detected_instances),387                "valid_instances": len(valid_instances),388                "met_requirement": met_requirement,389                "best_confidence": max([g["confidence"] for g in detected_instances], default=0.0),390                "best_duration": max([g["duration"] for g in detected_instances], default=0)391            }392 393            if met_requirement:394                required_gestures_met += 1395 396        # Calculate success rate397        total_required = len(required_gestures)398        success_rate = required_gestures_met / total_required if total_required > 0 else 0.0399 400        # Determine if validation passes based on error margin401        if require_all:402            passes_validation = required_gestures_met >= total_required403        else:404            min_required = max(1, int(total_required * (1.0 - error_margin)))405            passes_validation = required_gestures_met >= min_required406 407        metrics = {408            "total_required_gestures": total_required,409            "required_gestures_met": required_gestures_met,410            "success_rate": success_rate,411            "passes_validation": passes_validation,412            "error_margin": error_margin,413            "require_all": require_all,414            "gesture_analysis": gesture_analysis415        }416 417        logger.debug(f"Gesture analysis completed: {required_gestures_met}/{total_required} gestures met requirement")418        return metrics419 420    def _calculate_confidence(421        self,422        detected_gestures: List[Dict[str, Any]],423        validation_metrics: Dict[str, Any]424    ) -> float:425        """426        Calculate overall confidence score for gesture validation.427 428        Parameters429        ----------430        detected_gestures : List[Dict[str, Any]]431            List of detected gestures432        validation_metrics : Dict[str, Any]433            Validation metrics from analysis434 435        Returns436        -------437        float438            Overall confidence score (0.0-1.0)439        """440        if not detected_gestures:441            return 0.0442 443        # Base confidence on success rate444        success_rate = validation_metrics.get("success_rate", 0.0)445 446        # Boost confidence based on average gesture quality447        if detected_gestures:448            avg_confidence = sum(g["confidence"] for g in detected_gestures) / len(detected_gestures)449            avg_duration = sum(g["duration"] for g in detected_gestures) / len(detected_gestures)450 451            # Normalize duration to confidence boost (longer, more confident gestures = higher score)452            duration_boost = min(0.2, avg_duration / 100.0)  # Cap at 0.2 boost453            confidence_boost = min(0.1, avg_confidence * 0.1)  # Cap at 0.1 boost454 455            success_rate = min(1.0, success_rate + duration_boost + confidence_boost)456 457        return success_rate458 459    def _normalize_gesture_name(self, gesture_name: str) -> str:460        """461        Normalize gesture names to production-standard format.462        463        Handles legacy naming and variations to ensure consistent gesture names464        across different parts of the system. Maps old names like "like" to 465        "thumbs_up", and handles hand-agnostic counting variations.466 467        Parameters468        ----------469        gesture_name : str470            Raw gesture name from detection471 472        Returns473        -------474        str475            Normalized gesture name matching PRODUCTION_GESTURE_MAPPING476        """477        # Convert to lowercase and remove common variations478        normalized = gesture_name.lower().strip()479 480        # Handle common variations and legacy names481        variations = {482            "thumbs_up": ["thumbsup", "thumb_up", "like"],  # "like" is legacy name483            "one": ["one_finger", "one_left", "one_right", "one_down"],  # Hand-agnostic484            "two": ["peace_sign", "victory", "two_fingers", "two_up", "two_left", "two_right", "two_down"],  # Hand-agnostic485            "three": ["three_fingers", "three2", "three3"],  # Hand-agnostic486            "four": ["four_fingers"],487            "five": ["palm", "open_palm", "five_fingers"],  # "palm" is alias for "five"488            "peace_inverted": ["peace_inverted_sign"],489            "ok": ["okay", "ok_sign"],490            "call": ["call_me", "phone"],491            "fist": ["closed_fist"],492            "point": ["pointing"],493            "stop": ["stop_sign"],494            "middle_finger": ["middle"],495        }496 497        for standard_name, variant_list in variations.items():498            if normalized in variant_list or normalized == standard_name:499                return standard_name500 501        return normalized502