Spaces:
Running on Zero
Running on Zero
File size: 8,381 Bytes
762d5e5 bd46c49 762d5e5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | """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)
@torch.no_grad()
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,
)
|