Spaces:
Running on Zero
Running on Zero
multimodalart HF Staff
Upload lib/exmr/preprocessing/hand_features.py with huggingface_hub
bd46c49 verified Download lib/exmr/preprocessing/hand_features.py from hugging-apps/exmr-motion-refinement: direct link, hf CLI and curl.
- Browser
- Download file 8.38 kB
-
https://huggingface.co/spaces/hugging-apps/exmr-motion-refinement/resolve/main/lib/exmr/preprocessing/hand_features.py
- Command line
-
hf download hf://spaces/hugging-apps/exmr-motion-refinement/lib/exmr/preprocessing/hand_features.py
-
curl -L -o hand_features.py https://huggingface.co/spaces/hugging-apps/exmr-motion-refinement/resolve/main/lib/exmr/preprocessing/hand_features.py
8.38 kB
| """HaMeR image features: the MANO decoder token of each hand, one pair per frame. | |
| The hand crop (_crop_hand) is adapted from HaMeR's hamer/datasets/utils.py | |
| (https://github.com/geopavlakos/hamer, MIT License, Copyright (c) 2023 UC Regents, Georgios | |
| Pavlakos), see THIRD_PARTY_NOTICES.md. | |
| """ | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| from hamer.configs import get_config | |
| from hamer.models import HAMER | |
| from torch.utils.data import DataLoader | |
| from tqdm import tqdm | |
| def hand_boxes_from_keypoints(kp2d, threshold, min_kpts=4): | |
| """Per-frame boxes around the confident keypoints of one hand. | |
| Args: | |
| kp2d: (L, J, 3) hand keypoints [x, y, score] | |
| threshold: keypoints with score > threshold are used | |
| min_kpts: frames with fewer confident keypoints get an all-zero box (= no hand) | |
| Returns: | |
| (L, 4) boxes [x1, y1, x2, y2] | |
| """ | |
| x, y = kp2d[..., 0], kp2d[..., 1] | |
| mask = kp2d[..., 2] > threshold | |
| inf = x.new_tensor(float("inf")) | |
| boxes = torch.stack( | |
| [ | |
| torch.where(mask, x, inf).amin(dim=1), | |
| torch.where(mask, y, inf).amin(dim=1), | |
| torch.where(mask, x, -inf).amax(dim=1), | |
| torch.where(mask, y, -inf).amax(dim=1), | |
| ], | |
| dim=1, | |
| ) | |
| valid = mask.sum(dim=1) >= min_kpts | |
| return torch.where(valid.unsqueeze(1), boxes, torch.zeros_like(boxes)) | |
| class HamerHandFeatures: | |
| """Per-frame HaMeR tokens (1024-d) of both hands, given their bounding boxes. | |
| The token (the MANO decoder output consumed by HaMeR's regression heads) is read with a | |
| forward hook. Hands are cropped like in HaMeR's demo; the left hand is mirrored, as HaMeR | |
| only models right hands. Frames without a hand box get an all-black crop. | |
| Args: | |
| data_dir: HaMeR's data directory (from its hamer_demo_data download): contains | |
| hamer_ckpts/checkpoints/hamer.ckpt, hamer_ckpts/model_config.yaml, | |
| data/mano_mean_params.npz, and the MANO model data/mano/MANO_RIGHT.pkl | |
| fp16: run the network under float16 autocast (~1.9x faster, tokens nearly identical) | |
| """ | |
| TOKEN_DIM = 1024 | |
| def __init__(self, data_dir, fp16=True, batch_size=16, num_workers=8, device="cuda"): | |
| data_dir = Path(data_dir) | |
| cfg = get_config(str(data_dir / "hamer_ckpts" / "model_config.yaml")) | |
| cfg.defrost() | |
| cfg.MANO.MODEL_PATH = str(data_dir / "data" / "mano") | |
| cfg.MANO.MEAN_PARAMS = str(data_dir / "data" / "mano_mean_params.npz") | |
| if cfg.MODEL.BACKBONE.TYPE == "vit" and "BBOX_SHAPE" not in cfg.MODEL: | |
| cfg.MODEL.BBOX_SHAPE = [192, 256] | |
| cfg.MODEL.BACKBONE.pop("PRETRAINED_WEIGHTS", None) | |
| cfg.freeze() | |
| self.cfg = cfg | |
| # like hamer.models.load_hamer, without the renderer; built on the GPU: initializing the | |
| # weights on the CPU takes ~2 s, and the checkpoint replaces them anyway | |
| with torch.device(device): | |
| self.model = HAMER.load_from_checkpoint( | |
| str(data_dir / "hamer_ckpts" / "checkpoints" / "hamer.ckpt"), | |
| strict=False, | |
| cfg=cfg, | |
| init_renderer=False, | |
| map_location="cpu", # ZeroGPU: deserialize on CPU; .to(device) handles the move | |
| ) | |
| self.model.eval().to(device) | |
| self.fp16, self.batch_size, self.num_workers, self.device = ( | |
| fp16, batch_size, num_workers, device | |
| ) # fmt: skip | |
| self._tokens = None | |
| self.model.mano_head.transformer.register_forward_hook(self._store_tokens) | |
| def _store_tokens(self, _module, _inputs, output): | |
| self._tokens = output.squeeze(1) | |
| def __call__(self, frame_dir, hand_bbx_xyxy): | |
| """frame_dir with %06d.jpg frames, hand_bbx_xyxy (L, 2, 4) [left, right] boxes | |
| -> (L, 2, 1024) tokens""" | |
| dataset = _FrameDataset(Path(frame_dir), hand_bbx_xyxy, self.cfg) | |
| loader = DataLoader( | |
| dataset, | |
| batch_size=self.batch_size, | |
| shuffle=False, | |
| num_workers=self.num_workers, | |
| pin_memory=True, | |
| ) | |
| tokens = torch.zeros((len(dataset), 2, self.TOKEN_DIM), dtype=torch.float32) | |
| i = 0 | |
| with tqdm(total=len(dataset), desc="HaMeR hand features", unit="frame") as pbar: | |
| for crops in loader: | |
| bs = crops.shape[0] | |
| tokens[i : i + bs] = ( | |
| self._forward(crops.reshape(-1, 3, 256, 256)) | |
| .reshape(bs, 2, self.TOKEN_DIM) | |
| .cpu() | |
| ) | |
| i += bs | |
| pbar.update(bs) | |
| return tokens | |
| def _forward(self, crops): | |
| """(B, 3, 256, 256) normalized crops -> (B, 1024) tokens""" | |
| with torch.autocast(self.device, dtype=torch.float16, enabled=self.fp16): | |
| # HaMeR's ViT backbone sees the central 256x192 of the square crop | |
| feats = self.model.backbone(crops[:, :, :, 32:-32].to(self.device)) | |
| self.model.mano_head(feats) # the hook stores the tokens | |
| return self._tokens.float() | |
| class _FrameDataset(torch.utils.data.Dataset): | |
| """Normalized (2, 3, S, S) [left, right] hand crops per frame.""" | |
| def __init__(self, frame_dir, hand_bbx_xyxy, cfg): | |
| self.frame_dir, self.hand_bbx_xyxy = frame_dir, hand_bbx_xyxy | |
| self.img_size = cfg.MODEL.IMAGE_SIZE | |
| self.mean = 255.0 * np.array(cfg.MODEL.IMAGE_MEAN) | |
| self.std = 255.0 * np.array(cfg.MODEL.IMAGE_STD) | |
| self.bbox_shape = cfg.MODEL.BBOX_SHAPE | |
| def __len__(self): | |
| return len(self.hand_bbx_xyxy) | |
| def __getitem__(self, idx): | |
| path = self.frame_dir / f"{idx:06d}.jpg" | |
| img = cv2.imread(str(path)) | |
| if img is None: | |
| raise FileNotFoundError(f"Could not read frame {path}") | |
| boxes = self.hand_bbx_xyxy[idx].numpy() | |
| center = (boxes[:, 2:4] + boxes[:, 0:2]) / 2.0 | |
| scale = 2.0 * (boxes[:, 2:4] - boxes[:, 0:2]) / 200.0 # box enlarged 2x, in 200 px | |
| crops = [] | |
| for i in range(2): # left, right | |
| if scale[i, 0] == 0 or scale[i, 1] == 0: # no hand in this frame | |
| crops.append(np.zeros((1, 3, self.img_size, self.img_size), dtype=np.float32)) | |
| continue | |
| bbox_size = _expand_to_aspect_ratio(scale[i] * 200, self.bbox_shape).max() | |
| patch = _crop_hand( | |
| img.copy(), center[i, 0], center[i, 1], bbox_size, self.img_size, flip=i == 0 | |
| ) | |
| patch = patch[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) # BGR HWC -> RGB CHW | |
| for c in range(min(img.shape[2], 3)): | |
| patch[c] = (patch[c] - self.mean[c]) / self.std[c] | |
| crops.append(patch[None]) | |
| return np.concatenate(crops, axis=0) | |
| def _expand_to_aspect_ratio(size_wh, target_aspect_ratio): | |
| """Grow a (w, h) box size to the target (w, h) aspect ratio.""" | |
| w, h = size_wh | |
| w_t, h_t = target_aspect_ratio | |
| if h / w < h_t / w_t: | |
| return np.array([w, max(w * h_t / w_t, h)]) | |
| return np.array([max(h * w_t / h_t, w), h]) | |
| def _crop_hand(img, c_x, c_y, bbox_size, patch_size, flip): | |
| """Square crop of size bbox_size around (c_x, c_y), resized to patch_size (optionally | |
| mirrored), with black borders.""" | |
| if flip: | |
| img = img[:, ::-1, :] | |
| c_x = img.shape[1] - c_x - 1 | |
| src_center = np.array([c_x, c_y], dtype=np.float64) | |
| src_down = np.array([0, bbox_size * 0.5], dtype=np.float32) | |
| src_right = np.array([bbox_size * 0.5, 0], dtype=np.float32) | |
| dst_center = np.array([patch_size * 0.5, patch_size * 0.5], dtype=np.float32) | |
| dst_down = np.array([0, patch_size * 0.5], dtype=np.float32) | |
| dst_right = np.array([patch_size * 0.5, 0], dtype=np.float32) | |
| src = np.zeros((3, 2), dtype=np.float32) | |
| src[0, :] = src_center | |
| src[1, :] = src_center + src_down | |
| src[2, :] = src_center + src_right | |
| dst = np.zeros((3, 2), dtype=np.float32) | |
| dst[0, :] = dst_center | |
| dst[1, :] = dst_center + dst_down | |
| dst[2, :] = dst_center + dst_right | |
| trans = cv2.getAffineTransform(np.float32(src), np.float32(dst)) | |
| return cv2.warpAffine( | |
| img, | |
| trans, | |
| (int(patch_size), int(patch_size)), | |
| flags=cv2.INTER_LINEAR, | |
| borderMode=cv2.BORDER_CONSTANT, | |
| borderValue=0, | |
| ) | |