Spaces:
Running
Running
File size: 6,221 Bytes
d0eb71c | 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 | #!/usr/bin/env python3
"""kimodo motion -> OpenPose control image, for driving the weiner Klein LoRA.
The kimodo motion API (http://HOST:7862) returns, per clip, `posed_joints` [T,22,3]:
world-space 3D positions of the 22 SMPL-X body joints (y-up, metres). This module
projects a chosen frame to 2D and draws a canonical OpenPose (COCO-18) colored
skeleton — the abstract pose anchor that FLUX.2 Klein follows WITHOUT donating any
material (unlike a textured mannequin), so the weiner identity/LoRA supplies looks.
Pipeline: kimodo /animations/{id} -> openpose_png(frame) -> klein /generate
image=[skeleton, weiner_identity_shot], lora=weiner
CLI:
python kimodo_pose.py <anim_id> <out.png> [--frame N | --peak foot|hand] [--view front|side|auto]
"""
from __future__ import annotations
import argparse
import json
import urllib.request
import numpy as np
from PIL import Image, ImageDraw
KIMODO_URL = "http://127.0.0.1:7862"
# SMPL-X 22-joint order returned by the kimodo API (see /info bone_names).
SMPLX = {
"pelvis": 0, "left_hip": 1, "right_hip": 2, "spine1": 3, "left_knee": 4,
"right_knee": 5, "spine2": 6, "left_ankle": 7, "right_ankle": 8, "spine3": 9,
"left_foot": 10, "right_foot": 11, "neck": 12, "left_collar": 13,
"right_collar": 14, "head": 15, "left_shoulder": 16, "right_shoulder": 17,
"left_elbow": 18, "right_elbow": 19, "left_wrist": 20, "right_wrist": 21,
}
# OpenPose COCO-18 keypoint index -> source SMPL-X joint (eyes/ears synthesized below).
OP_FROM_SMPLX = {
0: "head", # nose ~ head
1: "neck", 2: "right_shoulder", 3: "right_elbow", 4: "right_wrist",
5: "left_shoulder", 6: "left_elbow", 7: "left_wrist",
8: "right_hip", 9: "right_knee", 10: "right_ankle",
11: "left_hip", 12: "left_knee", 13: "left_ankle",
}
# OpenPose limb pairs (COCO-18) + canonical colors (R,G,B).
OP_LIMBS = [
(1, 2, (255, 0, 0)), (1, 5, (255, 85, 0)), (2, 3, (255, 170, 0)),
(3, 4, (255, 255, 0)), (5, 6, (170, 255, 0)), (6, 7, (85, 255, 0)),
(1, 8, (0, 255, 0)), (8, 9, (0, 255, 85)), (9, 10, (0, 255, 170)),
(1, 11, (0, 255, 255)), (11, 12, (0, 170, 255)), (12, 13, (0, 85, 255)),
(1, 0, (0, 0, 255)), (0, 14, (85, 0, 255)), (14, 16, (170, 0, 255)),
(0, 15, (255, 0, 255)), (15, 17, (255, 0, 170)),
]
OP_POINT_COLOR = [
(255, 0, 0), (255, 85, 0), (255, 170, 0), (255, 255, 0), (170, 255, 0),
(85, 255, 0), (0, 255, 0), (0, 255, 85), (0, 255, 170), (0, 255, 255),
(0, 170, 255), (0, 85, 255), (0, 0, 255), (85, 0, 255), (170, 0, 255),
(255, 0, 255), (255, 0, 170), (255, 0, 85),
]
def fetch(anim_id: str, url: str = KIMODO_URL) -> dict:
return json.load(urllib.request.urlopen(f"{url}/animations/{anim_id}", timeout=30))
def peak_frame(P: np.ndarray, what: str = "foot") -> int:
"""Frame where the action reads clearest: foot/hand at max height (kick/punch peak)."""
idx = [SMPLX["left_foot"], SMPLX["right_foot"], SMPLX["left_ankle"], SMPLX["right_ankle"]] \
if what == "foot" else [SMPLX["left_wrist"], SMPLX["right_wrist"]]
return int(P[:, idx, 1].max(axis=1).argmax())
def _project(J: np.ndarray, view: str):
"""3D world joints [22,3] (y up) -> 2D [22,2] in image space. front: x-y; side: z-y."""
x, y, z = J[:, 0], J[:, 1], J[:, 2]
if view == "auto":
view = "side" if z.std() > x.std() * 1.15 else "front"
h = x if view == "front" else z
return np.stack([h, y], axis=1), view # (horizontal, vertical-up)
def openpose_image(P_frame: np.ndarray, size: int = 1024, view: str = "auto",
pad: float = 0.14) -> Image.Image:
"""Draw a COCO-18 OpenPose skeleton for one frame's [22,3] joints on black."""
pts2d, _ = _project(P_frame, view)
# build the 18 OpenPose keypoints
kp = np.full((18, 2), np.nan)
for op_i, name in OP_FROM_SMPLX.items():
kp[op_i] = pts2d[SMPLX[name]]
# synthesize eyes/ears around the head so the face limbs render
head, neck = pts2d[SMPLX["head"]], pts2d[SMPLX["neck"]]
up = (head - neck); span = np.linalg.norm(up) + 1e-6; up = up / span
side = np.array([-up[1], up[0]]) * span * 0.25
kp[0] = head + up * span * 0.15 # nose
kp[14], kp[15] = head + side * 0.6, head - side * 0.6 # R/L eye
kp[16], kp[17] = head + side, head - side # R/L ear
# normalize into the canvas (preserve aspect, feet at bottom, head at top)
valid = kp[~np.isnan(kp).any(axis=1)]
hmin, hmax = valid[:, 0].min(), valid[:, 0].max()
vmin, vmax = valid[:, 1].min(), valid[:, 1].max()
s = (1 - 2 * pad) * size / max(hmax - hmin, vmax - vmin, 1e-6)
cu = size / 2 - (hmin + hmax) / 2 * s
cv = size / 2 + (vmin + vmax) / 2 * s
def to_px(p):
return (cu + p[0] * s, cv - p[1] * s) # flip vertical (y-up -> image-down)
img = Image.new("RGB", (size, size), (0, 0, 0))
d = ImageDraw.Draw(img)
lw = max(4, size // 160)
for a, b, c in OP_LIMBS:
if np.isnan(kp[a]).any() or np.isnan(kp[b]).any():
continue
d.line([to_px(kp[a]), to_px(kp[b])], fill=c, width=lw)
r = max(4, size // 200)
for i, p in enumerate(kp):
if np.isnan(p).any():
continue
x, y = to_px(p)
d.ellipse([x - r, y - r, x + r, y + r], fill=OP_POINT_COLOR[i])
return img
def render_anim_frame(anim_id, out, frame=None, peak="foot", view="auto", url=KIMODO_URL):
rec = fetch(anim_id, url)
P = np.array(rec["posed_joints"])
f = peak_frame(P, peak) if frame is None else int(frame)
img = openpose_image(P[f], view=view)
img.save(out)
print(f"saved {out} anim={anim_id} frame={f}/{P.shape[0]} view={view} prompt={rec.get('prompt','')[:60]!r}")
return f
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("anim_id")
ap.add_argument("out")
ap.add_argument("--frame", type=int, default=None)
ap.add_argument("--peak", choices=["foot", "hand"], default="foot")
ap.add_argument("--view", choices=["front", "side", "auto"], default="auto")
ap.add_argument("--url", default=KIMODO_URL)
a = ap.parse_args()
render_anim_frame(a.anim_id, a.out, a.frame, a.peak, a.view, a.url)
|