| import argparse |
| import os |
| import shutil |
| import tempfile |
| import cv2 |
| from typing import Optional |
|
|
| import numpy as np |
| import pandas as pd |
| import torch |
| import torch.nn.functional as F |
| from torch.utils import data |
|
|
| from configs import LstmConfig, TransformerConfig |
| from generate_keypoints import process_video |
| from models import LSTM, Transformer |
| from utils import load_json, load_label_map |
| from video_preprocess import PreprocessConfig |
|
|
|
|
| class KeypointsDataset(data.Dataset): |
| def __init__(self, keypoints_dir, max_frame_len=169, frame_length=1080, frame_width=1920): |
| self.files = sorted( |
| [f for f in os.listdir(keypoints_dir) if f.endswith(".json")] |
| ) |
| self.keypoints_dir = keypoints_dir |
| self.max_frame_len = max_frame_len |
| self.frame_length = frame_length |
| self.frame_width = frame_width |
|
|
| def interpolate(self, arr): |
| arr_x = arr[:, :, 0] |
| arr_x = pd.DataFrame(arr_x) |
| arr_x = arr_x.interpolate(method="linear", limit_direction="both").to_numpy() |
|
|
| arr_y = arr[:, :, 1] |
| arr_y = pd.DataFrame(arr_y) |
| arr_y = arr_y.interpolate(method="linear", limit_direction="both").to_numpy() |
|
|
| if np.count_nonzero(~np.isnan(arr_x)) == 0: |
| arr_x = np.zeros(arr_x.shape) |
| if np.count_nonzero(~np.isnan(arr_y)) == 0: |
| arr_y = np.zeros(arr_y.shape) |
|
|
| arr_x = arr_x * self.frame_width |
| arr_y = arr_y * self.frame_length |
|
|
| return np.stack([arr_x, arr_y], axis=-1) |
|
|
| def combine_xy(self, x, y): |
| x, y = np.array(x), np.array(y) |
| _, length = x.shape |
| x = x.reshape((-1, length, 1)) |
| y = y.reshape((-1, length, 1)) |
| return np.concatenate((x, y), -1).astype(np.float32) |
|
|
| def __getitem__(self, idx): |
| file_path = os.path.join(self.keypoints_dir, self.files[idx]) |
| row = pd.read_json(file_path, typ="series") |
|
|
| pose = self.combine_xy(row.pose_x, row.pose_y) |
| h1 = self.combine_xy(row.hand1_x, row.hand1_y) |
| h2 = self.combine_xy(row.hand2_x, row.hand2_y) |
|
|
| pose = self.interpolate(pose) |
| h1 = self.interpolate(h1) |
| h2 = self.interpolate(h2) |
|
|
| pose = pose.reshape(-1, 50).astype(np.float32) |
| h1 = h1.reshape(-1, 42).astype(np.float32) |
| h2 = h2.reshape(-1, 42).astype(np.float32) |
| final_data = np.concatenate((pose, h1, h2), -1) |
| final_data = np.pad( |
| final_data, |
| ((0, self.max_frame_len - final_data.shape[0]), (0, 0)), |
| "constant", |
| ) |
| return {"uid": row.uid, "data": torch.FloatTensor(final_data)} |
|
|
| def __len__(self): |
| return len(self.files) |
|
|
|
|
| def _pretrained_name(dataset: str, model_type: str, transformer_size: str) -> str: |
| name = dataset |
| name += "_no_cnn" |
| if model_type == "lstm": |
| name += "_lstm.pth" |
| elif model_type == "transformer": |
| name += "_transformer" |
| name += "_large.pth" if transformer_size == "large" else "_small.pth" |
| return name |
|
|
|
|
| def load_model( |
| dataset: str, |
| model_type: str, |
| transformer_size: str, |
| checkpoint_path: Optional[str], |
| label_map_path: Optional[str] = None, |
| ): |
| label_map = load_json(label_map_path) if label_map_path else load_label_map(dataset) |
| n_classes = len(label_map) |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| if model_type == "transformer": |
| config = TransformerConfig(size=transformer_size, max_position_embeddings=256) |
| model = Transformer(config=config, n_classes=n_classes) |
| elif model_type == "lstm": |
| config = LstmConfig() |
| model = LSTM(config=config, n_classes=n_classes) |
| else: |
| raise ValueError(f"Unsupported model_type: {model_type}") |
|
|
| model = model.to(device) |
|
|
| if checkpoint_path is None: |
| pretrained_links = load_json("pretrained_links.json") |
| model_name = _pretrained_name(dataset, model_type, transformer_size) |
| |
| |
| if os.path.isfile(model_name): |
| checkpoint_path = model_name |
| else: |
| |
| checkpoint_path = os.path.join("/tmp", model_name) |
| |
| |
| if not os.path.isfile(checkpoint_path): |
| link = pretrained_links.get(model_name) |
| if not link or link == "link": |
| raise FileNotFoundError(f"No pretrained link for {model_name}") |
| torch.hub.download_url_to_file(link, checkpoint_path, progress=True) |
| checkpoint_path = model_name |
|
|
| ckpt = torch.load(checkpoint_path, map_location=device, weights_only=False) |
| model.load_state_dict(ckpt["model"]) |
| model.eval() |
| return model, label_map |
|
|
|
|
| @torch.no_grad() |
| def predict_video( |
| video_path: str, |
| dataset: str, |
| model, |
| label_map: dict, |
| preprocess_config: PreprocessConfig, |
| *, |
| max_frame_len: int = 169, |
| use_holistic: bool = True, |
| face_mode: str = "full", |
| ): |
| temp_dir = tempfile.mkdtemp(prefix="keypoints_") |
| try: |
| process_video( |
| video_path, |
| temp_dir, |
| use_holistic=use_holistic, |
| face_mode=face_mode, |
| preprocess_config=preprocess_config, |
| ) |
| dataset_obj = KeypointsDataset(keypoints_dir=temp_dir, max_frame_len=max_frame_len) |
| dataloader = data.DataLoader(dataset_obj, batch_size=1, shuffle=False) |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| id_to_label = dict(zip(label_map.values(), label_map.keys())) |
| for batch in dataloader: |
| input_data = batch["data"].to(device) |
| logits = model(input_data) |
| probs = torch.softmax(logits, dim=-1).detach().cpu().numpy()[0] |
| idx = int(np.argmax(probs)) |
| return { |
| "uid": batch["uid"][0], |
| "label": id_to_label[idx], |
| "score": float(probs[idx]), |
| } |
| finally: |
| shutil.rmtree(temp_dir, ignore_errors=True) |
|
|
| raise RuntimeError("No frames processed from the video.") |
|
|
| |
| def smart_brighten(frame, darkness_threshold=80): |
| """ |
| Checks if a frame is too dark. If it is, applies CLAHE to enhance |
| the hand/pose visibility without blowing out the highlights. |
| """ |
| |
| gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
| average_brightness = np.mean(gray) |
| |
| |
| if average_brightness > darkness_threshold: |
| return frame |
| |
| |
| |
| lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB) |
| l_channel, a, b = cv2.split(lab) |
| |
| |
| clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)) |
| cl = clahe.apply(l_channel) |
| |
| |
| merged = cv2.merge((cl, a, b)) |
| brightened_frame = cv2.cvtColor(merged, cv2.COLOR_LAB2BGR) |
| |
| return brightened_frame |
|
|
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(description="Run inference on a single video") |
| parser.add_argument("--video", required=True, help="path to the video file") |
| parser.add_argument( |
| "--dataset", |
| default="isl_split_dataset", |
| help="dataset key used to resolve label map and pretrained model name", |
| ) |
| parser.add_argument("--model", default="transformer", choices=["transformer", "lstm"]) |
| parser.add_argument("--transformer_size", default="small", choices=["small", "large"]) |
| parser.add_argument("--checkpoint", default=None, help="path to model checkpoint") |
| parser.add_argument( |
| "--label_map_path", |
| default=None, |
| help="optional explicit label map JSON path", |
| ) |
| parser.add_argument("--apply_darken", action="store_true", help="apply darken before brighten") |
| parser.add_argument("--darken_min", default=0.3, type=float) |
| parser.add_argument("--darken_max", default=0.8, type=float) |
| parser.add_argument("--brighten_method", default="clahe", choices=["clahe", "gamma"]) |
| parser.add_argument("--brighten_gamma_min", default=1.2, type=float) |
| parser.add_argument("--brighten_gamma_max", default=1.8, type=float) |
| parser.add_argument( |
| "--max_frame_len", |
| default=169, |
| type=int, |
| help="sequence length used during training (must match inference)", |
| ) |
| args = parser.parse_args() |
|
|
| preprocess_config = PreprocessConfig( |
| apply_darken=args.apply_darken, |
| apply_brighten=True, |
| darken_min=args.darken_min, |
| darken_max=args.darken_max, |
| brighten_method=args.brighten_method, |
| brighten_gamma_min=args.brighten_gamma_min, |
| brighten_gamma_max=args.brighten_gamma_max, |
| ) |
|
|
| model, label_map = load_model( |
| dataset=args.dataset, |
| model_type=args.model, |
| transformer_size=args.transformer_size, |
| checkpoint_path=args.checkpoint, |
| label_map_path=args.label_map_path, |
| ) |
| result = predict_video( |
| args.video, |
| args.dataset, |
| model, |
| label_map, |
| preprocess_config=preprocess_config, |
| max_frame_len=args.max_frame_len, |
| ) |
| print(result) |
|
|