| """Keypoint layout, so a trimmed keypoint set can be trained without hardcoding 128. |
| |
| The full DWPose layout is 128 keypoints: body 0:18, face 18:86, left hand 86:107, |
| right hand 107:128. Dropping keypoints (e.g. knees/ankles, which these clips crop |
| out) changes the model's input dimension, so the group boundaries can no longer be |
| constants. |
| |
| Keypoints are always re-ordered into body / face / lhand / rhand blocks, so group |
| ranges stay contiguous slices -- only the boundaries move. A layout is persisted as |
| `layout.json` next to the packed memmaps. |
| """ |
| import json |
| import os |
|
|
| |
| FULL_GROUPS = {"body": (0, 18), "face": (18, 86), "lhand": (86, 107), "rhand": (107, 128)} |
| FULL_NKP = 128 |
|
|
| |
| |
| |
| |
| |
| LOWER_BODY = (9, 10, 12, 13) |
|
|
| PRESETS = { |
| "full": (), |
| "upper": LOWER_BODY, |
| "upper_nohip": LOWER_BODY + (8, 11), |
| } |
|
|
|
|
| class Layout: |
| """Which original DWPose keypoints are kept, and where the groups sit.""" |
|
|
| def __init__(self, keep, groups, name="full"): |
| self.keep = list(keep) |
| self.groups = {k: tuple(v) for k, v in groups.items()} |
| self.name = name |
| self.n_kpts = len(self.keep) |
| self.dim = self.n_kpts * 2 |
| |
| self.old2new = {o: n for n, o in enumerate(self.keep)} |
|
|
| |
| @classmethod |
| def full(cls): |
| return cls(list(range(FULL_NKP)), FULL_GROUPS, "full") |
|
|
| @classmethod |
| def from_drop(cls, drop, name="custom"): |
| """Build a layout that drops `drop` (original indices), keeping group order.""" |
| drop = set(int(d) for d in drop) |
| keep, groups, cur = [], {}, 0 |
| for g, (a, b) in FULL_GROUPS.items(): |
| idx = [i for i in range(a, b) if i not in drop] |
| keep.extend(idx) |
| groups[g] = (cur, cur + len(idx)) |
| cur += len(idx) |
| return cls(keep, groups, name) |
|
|
| @classmethod |
| def preset(cls, name): |
| if name not in PRESETS: |
| raise ValueError(f"unknown layout preset {name!r}; choices {list(PRESETS)}") |
| return cls.full() if name == "full" else cls.from_drop(PRESETS[name], name) |
|
|
| |
| def save(self, data_dir): |
| with open(os.path.join(data_dir, "layout.json"), "w") as f: |
| json.dump({"name": self.name, "keep": self.keep, |
| "groups": {k: list(v) for k, v in self.groups.items()}, |
| "n_kpts": self.n_kpts, "dim": self.dim}, f, indent=2) |
|
|
| @classmethod |
| def load(cls, data_dir): |
| p = os.path.join(data_dir, "layout.json") |
| if not os.path.exists(p): |
| return cls.full() |
| with open(p) as f: |
| d = json.load(f) |
| return cls(d["keep"], d["groups"], d.get("name", "custom")) |
|
|
| |
| def metric_groups(self): |
| """Group ranges for reporting, with 'hands' merging both hands.""" |
| g = dict(self.groups) |
| out = {"all": (0, self.n_kpts), "body": g["body"], "face": g["face"], |
| "hands": (g["lhand"][0], g["rhand"][1])} |
| return out |
|
|
| def __repr__(self): |
| return (f"Layout({self.name}, {self.n_kpts} kpts, dim {self.dim}, " |
| f"groups {self.groups})") |
|
|