Spaces:
Running on Zero
Running on Zero
File size: 7,609 Bytes
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 | """RTMW-x whole-body 2D keypoints (ONNX Runtime, FP16 model, no flip test) -> COCO-63.
The crop (top_down_affine and helpers) and SimCC decoding (get_simcc_maximum) are adapted from
RTMLib (https://github.com/Tau-J/rtmlib), licensed under the Apache License 2.0, see
licenses/RTMLib_LICENSE.txt.
"""
import hashlib
from pathlib import Path
import cv2
import numpy as np
import onnxruntime as ort
import torch
from tqdm import tqdm
from exmr.preprocessing.keypoints.mappings import COCO133_TO_COCO63
INPUT_SIZE = (288, 384) # width, height
MEAN = (123.675, 116.28, 103.53)
STD = (58.395, 57.12, 57.375)
BOX_PADDING = 1.25
class RTMWKeypoints:
"""2D keypoints of one person per frame, given its bounding boxes.
Args:
checkpoint: RTMW-x 384x288 ONNX model with FP32 inputs/outputs
sha256: expected checksum of the checkpoint (None to skip the check)
"""
def __init__(self, checkpoint, sha256=None, batch_size=32, num_workers=8, device_id=0):
if sha256 is not None and _sha256(checkpoint) != sha256:
raise ValueError(f"Unexpected RTMW checkpoint (SHA256 mismatch): {checkpoint}")
self.batch_size = batch_size
self.num_workers = num_workers
self.session = ort.InferenceSession(
str(checkpoint),
providers=[("CUDAExecutionProvider", {"device_id": device_id}), "CPUExecutionProvider"],
)
if self.session.get_providers()[0] != "CUDAExecutionProvider":
raise RuntimeError("ONNX Runtime CUDA provider unavailable for RTMW")
def __call__(self, frame_dir, bbx_xyxy):
"""frame_dir with %06d.jpg frames, bbx_xyxy (L, 4) -> (L, 63, 3) [x, y, raw score]"""
boxes = bbx_xyxy.cpu().numpy()
loader = torch.utils.data.DataLoader(
_FrameDataset(frame_dir, boxes),
batch_size=self.batch_size,
num_workers=self.num_workers,
shuffle=False,
**({"prefetch_factor": 4} if self.num_workers else {}),
)
inp = self.session.get_inputs()[0]
out_names = [v.name for v in self.session.get_outputs()]
result = np.empty((len(boxes), 63, 3), dtype=np.float32)
cursor = 0
with tqdm(total=len(boxes), desc="RTMW-x keypoints", unit="frame") as pbar:
for batch in loader:
images = batch["input"].numpy()
n = len(images)
# pad the last batch to the full batch size: ONNX Runtime + cuDNN 9 set up the
# convolutions anew (~7 s) for every new input shape
images = np.pad(images, [(0, self.batch_size - n)] + [(0, 0)] * 3, mode="edge")
simcc_x, simcc_y = self.session.run(out_names, {inp.name: images})
simcc_x, simcc_y = simcc_x[:n], simcc_y[:n]
center, scale = batch["center"].numpy(), batch["scale"].numpy()
xy, scores = _decode(simcc_x, simcc_y, center, scale)
kp = np.concatenate((xy, scores[..., None]), axis=-1)
result[cursor : cursor + len(kp)] = kp[:, COCO133_TO_COCO63]
cursor += len(kp)
pbar.update(len(kp))
if cursor != len(boxes) or not np.isfinite(result).all():
raise RuntimeError("Incomplete or non-finite RTMW output")
return torch.from_numpy(result)
class _FrameDataset(torch.utils.data.Dataset):
def __init__(self, frame_dir, boxes):
self.paths = [str(Path(frame_dir) / f"{i:06d}.jpg") for i in range(len(boxes))]
self.boxes = boxes
self.mean = np.array(MEAN) # float64 normalization, as in the evaluated setup
self.std = np.array(STD)
def __len__(self):
return len(self.boxes)
def __getitem__(self, idx):
image = cv2.imread(self.paths[idx])
if image is None:
raise ValueError(f"Cannot decode {self.paths[idx]}")
center, scale = _bbox_xyxy2cs(self.boxes[idx], padding=BOX_PADDING)
crop, scale = _top_down_affine(INPUT_SIZE, scale, center, image)
crop = (crop - self.mean) / self.std
chw = np.ascontiguousarray(crop.transpose(2, 0, 1), dtype=np.float32)
return {
"input": torch.from_numpy(chw),
"center": torch.from_numpy(np.asarray(center, dtype=np.float32)),
"scale": torch.from_numpy(np.asarray(scale, dtype=np.float32)),
}
def _decode(simcc_x, simcc_y, centers, scales):
"""SimCC outputs -> keypoints in image coordinates and raw scores."""
locs, scores = _get_simcc_maximum(simcc_x, simcc_y)
keypoints = locs / 2.0 # SimCC split ratio
input_size = np.asarray(INPUT_SIZE, dtype=np.float32)
centers = np.asarray(centers, dtype=np.float32)
scales = np.asarray(scales, dtype=np.float32)
keypoints = keypoints / input_size * scales[:, None, :]
keypoints = keypoints + centers[:, None, :] - scales[:, None, :] / 2
return keypoints, scores.astype(np.float32)
def _sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
digest.update(chunk)
return digest.hexdigest()
# ----- adapted from RTMLib (rtmlib/tools/pose_estimation/{pre,post}_processings.py) ----- #
def _bbox_xyxy2cs(bbox, padding=1.0):
"""(4,) box [x1, y1, x2, y2] -> center (2,) and padded scale (2,)."""
x1, y1, x2, y2 = np.hsplit(bbox[None, :], [1, 2, 3])
center = np.hstack([x1 + x2, y1 + y2]) * 0.5
scale = np.hstack([x2 - x1, y2 - y1]) * padding
return center[0], scale[0]
def _get_3rd_point(a, b):
"""b + (a - b) rotated by 90 degrees anticlockwise."""
direction = a - b
return b + np.r_[-direction[1], direction[0]]
def _get_warp_matrix(center, scale, output_size):
"""2x3 affine matrix that maps the box (center, scale) onto an output_size image."""
src_w = scale[0]
dst_w, dst_h = output_size
src_dir = np.array([0.0, src_w * -0.5]) # no rotation
dst_dir = np.array([0.0, dst_w * -0.5])
shift = np.array((0.0, 0.0))
src = np.zeros((3, 2), dtype=np.float32)
src[0, :] = center + scale * shift
src[1, :] = center + src_dir + scale * shift
src[2, :] = _get_3rd_point(src[0, :], src[1, :])
dst = np.zeros((3, 2), dtype=np.float32)
dst[0, :] = [dst_w * 0.5, dst_h * 0.5]
dst[1, :] = np.array([dst_w * 0.5, dst_h * 0.5]) + dst_dir
dst[2, :] = _get_3rd_point(dst[0, :], dst[1, :])
return cv2.getAffineTransform(np.float32(src), np.float32(dst))
def _top_down_affine(input_size, bbox_scale, bbox_center, img):
"""Crop the box from img to input_size (box grown to the input aspect ratio)."""
w, h = input_size
aspect_ratio = w / h
b_w, b_h = np.hsplit(bbox_scale, [1])
bbox_scale = np.where(
b_w > b_h * aspect_ratio,
np.hstack([b_w, b_w / aspect_ratio]),
np.hstack([b_h * aspect_ratio, b_h]),
)
warp_mat = _get_warp_matrix(bbox_center, bbox_scale, output_size=(w, h))
img = cv2.warpAffine(img, warp_mat, (int(w), int(h)), flags=cv2.INTER_LINEAR)
return img, bbox_scale
def _get_simcc_maximum(simcc_x, simcc_y):
"""SimCC (N, K, W) x/y -> locations (N, K, 2) and scores (N, K) of the maxima."""
N, K, _ = simcc_x.shape
simcc_x = simcc_x.reshape(N * K, -1)
simcc_y = simcc_y.reshape(N * K, -1)
locs = np.stack((np.argmax(simcc_x, axis=1), np.argmax(simcc_y, axis=1)), axis=-1)
locs = locs.astype(np.float32)
vals = 0.5 * (np.amax(simcc_x, axis=1) + np.amax(simcc_y, axis=1))
locs[vals <= 0.0] = -1
return locs.reshape(N, K, 2), vals.reshape(N, K)
|