CoolFace
Apppublic

Skylorjustine/Video-Action-Recognition

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
predict_fixed.py360 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3Fixed video action prediction with proper TimeSformer tensor format.4This version resolves the tensor compatibility issues definitively.5"""6 7import argparse8import json9import logging10from pathlib import Path11from typing import List, Tuple, Optional12import warnings13 14# Suppress warnings for cleaner output15warnings.filterwarnings("ignore", category=UserWarning)16warnings.filterwarnings("ignore", category=DeprecationWarning)17 18import torch19from PIL import Image20 21# Configure logging22logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')23 24# Video reading libraries25try:26    import cv227    HAS_CV2 = True28except ImportError:29    HAS_CV2 = False30    cv2 = None31 32try:33    import decord34    HAS_DECORD = True35except ImportError:36    HAS_DECORD = False37    decord = None38 39MODEL_ID = "facebook/timesformer-base-finetuned-k400"40 41def read_video_frames_cv2(video_path: Path, num_frames: int = 8) -> List[Image.Image]:42    """Read frames using OpenCV with robust error handling."""43    if not HAS_CV2:44        raise RuntimeError("OpenCV not available")45 46    cap = cv2.VideoCapture(str(video_path))47    if not cap.isOpened():48        raise RuntimeError(f"Cannot open video: {video_path}")49 50    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))51    if total_frames == 0:52        cap.release()53        raise RuntimeError("Video has no frames")54 55    # Sample frames uniformly across the video56    if total_frames <= num_frames:57        frame_indices = list(range(total_frames))58    else:59        step = max(1, total_frames // num_frames)60        frame_indices = [i * step for i in range(num_frames)]61        # Ensure we don't exceed total frames62        frame_indices = [min(idx, total_frames - 1) for idx in frame_indices]63 64    frames = []65    for idx in frame_indices:66        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)67        ret, frame = cap.read()68        if ret:69            # Convert BGR to RGB70            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)71            pil_image = Image.fromarray(frame_rgb)72            frames.append(pil_image)73 74    cap.release()75 76    # Pad with last frame if needed77    while len(frames) < num_frames:78        if frames:79            frames.append(frames[-1].copy())80        else:81            # Create black frame as fallback82            black_frame = Image.new('RGB', (224, 224), (0, 0, 0))83            frames.append(black_frame)84 85    return frames[:num_frames]86 87def read_video_frames_decord(video_path: Path, num_frames: int = 8) -> List[Image.Image]:88    """Read frames using decord."""89    if not HAS_DECORD:90        raise RuntimeError("Decord not available")91 92    vr = decord.VideoReader(str(video_path))93    total_frames = len(vr)94 95    if total_frames == 0:96        raise RuntimeError("Video has no frames")97 98    # Sample frames99    if total_frames <= num_frames:100        indices = list(range(total_frames))101    else:102        step = max(1, total_frames // num_frames)103        indices = [i * step for i in range(num_frames)]104        indices = [min(idx, total_frames - 1) for idx in indices]105 106    try:107        frame_arrays = vr.get_batch(indices).asnumpy()108        frames = [Image.fromarray(frame) for frame in frame_arrays]109    except Exception:110        # Fallback to individual frame reading111        frames = []112        for idx in indices:113            try:114                frame = vr[idx].asnumpy()115                frames.append(Image.fromarray(frame))116            except Exception:117                continue118 119    # Pad if necessary120    while len(frames) < num_frames:121        if frames:122            frames.append(frames[-1].copy())123        else:124            black_frame = Image.new('RGB', (224, 224), (0, 0, 0))125            frames.append(black_frame)126 127    return frames[:num_frames]128 129def read_video_frames(video_path: Path, num_frames: int = 8) -> List[Image.Image]:130    """Read video frames with fallback methods."""131    last_error = None132 133    # Try decord first (usually faster and more reliable)134    if HAS_DECORD:135        try:136            frames = read_video_frames_decord(video_path, num_frames)137            if frames and len(frames) > 0:138                logging.debug(f"Successfully read {len(frames)} frames using decord")139                return frames140        except Exception as e:141            last_error = e142            logging.debug(f"Decord failed: {e}")143 144    # Fallback to OpenCV145    if HAS_CV2:146        try:147            frames = read_video_frames_cv2(video_path, num_frames)148            if frames and len(frames) > 0:149                logging.debug(f"Successfully read {len(frames)} frames using OpenCV")150                return frames151        except Exception as e:152            last_error = e153            logging.debug(f"OpenCV failed: {e}")154 155    if last_error:156        raise RuntimeError(f"Failed to read video frames: {last_error}")157    else:158        raise RuntimeError("No video reading library available")159 160def normalize_frames(frames: List[Image.Image], target_size: Tuple[int, int] = (224, 224)) -> List[Image.Image]:161    """Normalize frames to consistent format."""162    if not frames:163        raise RuntimeError("No frames to normalize")164 165    normalized = []166    for i, frame in enumerate(frames):167        try:168            # Convert to RGB if needed169            if frame.mode != 'RGB':170                frame = frame.convert('RGB')171 172            # Resize to target size173            if frame.size != target_size:174                frame = frame.resize(target_size, Image.Resampling.LANCZOS)175 176            normalized.append(frame)177        except Exception as e:178            logging.warning(f"Error normalizing frame {i}: {e}")179            # Create a black frame as fallback180            black_frame = Image.new('RGB', target_size, (0, 0, 0))181            normalized.append(black_frame)182 183    return normalized184 185def create_timesformer_tensor(frames: List[Image.Image]) -> torch.Tensor:186    """187    Create properly formatted tensor for TimeSformer model.188 189    TimeSformer expects 5D input tensor:190    Input format: [batch_size, num_frames, channels, height, width]191    For 8 frames of 224x224: [1, 8, 3, 224, 224]192    """193    if len(frames) != 8:194        raise ValueError(f"Expected 8 frames, got {len(frames)}")195 196    # Convert frames to tensors without using numpy197    frame_tensors = []198 199    for frame in frames:200        # Ensure correct format201        if frame.mode != 'RGB':202            frame = frame.convert('RGB')203        if frame.size != (224, 224):204            frame = frame.resize((224, 224), Image.Resampling.LANCZOS)205 206        # Convert PIL image to tensor manually to avoid numpy issues207        pixels = list(frame.getdata())  # List of (R, G, B) tuples208 209        # Separate into RGB channels and normalize210        r_channel = []211        g_channel = []212        b_channel = []213 214        for r, g, b in pixels:215            r_channel.append(r / 255.0)216            g_channel.append(g / 255.0)217            b_channel.append(b / 255.0)218 219        # Reshape to 2D (224, 224) for each channel220        r_tensor = torch.tensor(r_channel, dtype=torch.float32).view(224, 224)221        g_tensor = torch.tensor(g_channel, dtype=torch.float32).view(224, 224)222        b_tensor = torch.tensor(b_channel, dtype=torch.float32).view(224, 224)223 224        # Stack channels: (3, 224, 224)225        frame_tensor = torch.stack([r_tensor, g_tensor, b_tensor], dim=0)226        frame_tensors.append(frame_tensor)227 228    # Stack frames: (8, 3, 224, 224)229    video_tensor = torch.stack(frame_tensors, dim=0)230 231    # Rearrange to TimeSformer format: (batch, frames, channels, height, width)232    # From (8, 3, 224, 224) to (1, 8, 3, 224, 224)233    video_tensor = video_tensor.unsqueeze(0)  # Add batch dimension: (1, 8, 3, 224, 224)234 235    logging.debug(f"Created tensor with shape: {video_tensor.shape}")236    logging.debug(f"Tensor dtype: {video_tensor.dtype}")237    logging.debug(f"Tensor range: [{video_tensor.min():.3f}, {video_tensor.max():.3f}]")238 239    return video_tensor240 241def load_model(device: Optional[str] = None):242    """Load TimeSformer model and processor."""243    try:244        from transformers import AutoImageProcessor, TimesformerForVideoClassification245 246        device = device or ("cuda" if torch.cuda.is_available() else "cpu")247        logging.info(f"Loading model on device: {device}")248 249        processor = AutoImageProcessor.from_pretrained(MODEL_ID)250        model = TimesformerForVideoClassification.from_pretrained(MODEL_ID)251        model.to(device)252        model.eval()253 254        logging.info("Model loaded successfully")255        return processor, model, device256 257    except Exception as e:258        logging.error(f"Failed to load model: {e}")259        raise RuntimeError(f"Model loading failed: {e}")260 261def predict_actions(video_path: str, top_k: int = 5) -> List[Tuple[str, float]]:262    """263    Predict actions in video using TimeSformer model.264 265    Args:266        video_path: Path to video file267        top_k: Number of top predictions to return268 269    Returns:270        List of (action_label, confidence_score) tuples271    """272    video_path = Path(video_path)273 274    if not video_path.exists():275        raise FileNotFoundError(f"Video file not found: {video_path}")276 277    try:278        # Load model279        processor, model, device = load_model()280 281        # Extract and normalize frames282        logging.info(f"Processing video: {video_path.name}")283        frames = read_video_frames(video_path, num_frames=8)284        frames = normalize_frames(frames, target_size=(224, 224))285 286        logging.info(f"Extracted and normalized {len(frames)} frames")287 288        # Create tensor in correct format289        pixel_values = create_timesformer_tensor(frames)290        pixel_values = pixel_values.to(device)291 292        # Run inference293        logging.info("Running model inference...")294        with torch.no_grad():295            outputs = model(pixel_values=pixel_values)296            logits = outputs.logits297 298        # Get top-k predictions299        probabilities = torch.softmax(logits, dim=-1)[0]  # Remove batch dimension300        top_probs, top_indices = torch.topk(probabilities, k=top_k)301 302        # Convert to results303        results = []304        for prob, idx in zip(top_probs, top_indices):305            label = model.config.id2label[idx.item()]306            confidence = float(prob.item())307            results.append((label, confidence))308 309        logging.info(f"Generated {len(results)} predictions successfully")310 311        # Log top prediction for debugging312        if results:313            top_label, top_conf = results[0]314            logging.info(f"Top prediction: {top_label} ({top_conf:.3f})")315 316        return results317 318    except Exception as e:319        logging.error(f"Prediction failed: {e}")320        raise RuntimeError(f"Video processing error: {e}")321 322def main():323    """Command line interface."""324    parser = argparse.ArgumentParser(description="Predict actions in video using TimeSformer")325    parser.add_argument("video", type=str, help="Path to video file")326    parser.add_argument("--top-k", type=int, default=5, help="Number of top predictions")327    parser.add_argument("--json", action="store_true", help="Output as JSON")328    parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose logging")329 330    args = parser.parse_args()331 332    if args.verbose:333        logging.getLogger().setLevel(logging.DEBUG)334 335    try:336        # Run prediction337        predictions = predict_actions(args.video, top_k=args.top_k)338 339        if args.json:340            output = [{"label": label, "confidence": confidence}341                     for label, confidence in predictions]342            print(json.dumps(output, indent=2))343        else:344            print(f"\nTop {len(predictions)} predictions for: {args.video}")345            print("-" * 60)346            for i, (label, confidence) in enumerate(predictions, 1):347                print(f"{i:2d}. {label:<35} {confidence:.4f}")348 349        return 0350 351    except Exception as e:352        print(f"Error: {e}")353        if args.verbose:354            import traceback355            traceback.print_exc()356        return 1357 358if __name__ == "__main__":359    exit(main())360