183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
"""
|
|
Converter nodes for bridging WanAnimate POSEDATA into POSE_KEYPOINT format.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
try: # Standard package import (ComfyUI runtime)
|
|
from ..utils import log
|
|
except ImportError: # Fallback for standalone/unit test environments
|
|
from utils import log
|
|
|
|
|
|
def _to_array(value):
|
|
"""Convert nested list/tuple/ndarray to numpy array or return None."""
|
|
if value is None:
|
|
return None
|
|
try:
|
|
arr = np.array(value, dtype=float)
|
|
if arr.size == 0:
|
|
return None
|
|
return arr
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _looks_normalized(coords: np.ndarray) -> bool:
|
|
"""
|
|
Heuristic: treat coordinates as normalized (0..1-ish) if their absolute max is small.
|
|
WanAnimate/UniAnimate often uses normalized coordinates; some POSEDATA variants already store pixels.
|
|
"""
|
|
if coords is None or coords.size == 0:
|
|
return True
|
|
finite = coords[np.isfinite(coords)]
|
|
if finite.size == 0:
|
|
return True
|
|
return float(np.max(np.abs(finite))) <= 2.0
|
|
|
|
|
|
def _extract_keypoints(meta, coord_name, conf_name, target_points, width, height, default_conf=1.0):
|
|
"""
|
|
Extract, scale, and pad keypoints to the desired target length.
|
|
Missing coordinates or invalid values are zeroed with confidence 0.
|
|
"""
|
|
coords_raw = getattr(meta, coord_name, None)
|
|
conf_raw = getattr(meta, conf_name, None)
|
|
|
|
# Fall back to dict-style access if present
|
|
if coords_raw is None and isinstance(meta, dict):
|
|
coords_raw = meta.get(coord_name)
|
|
if conf_raw is None and isinstance(meta, dict):
|
|
conf_raw = meta.get(conf_name)
|
|
|
|
coords = _to_array(coords_raw)
|
|
confs = _to_array(conf_raw)
|
|
if confs is not None:
|
|
confs = confs.reshape(-1)
|
|
|
|
# Normalize coordinate shape to (N, 2)
|
|
if coords is not None:
|
|
if coords.ndim == 1:
|
|
# Flattened x,y pairs
|
|
if coords.size % 2 == 0:
|
|
coords = coords.reshape(-1, 2)
|
|
else:
|
|
coords = None
|
|
elif coords.shape[-1] >= 2:
|
|
coords = coords.reshape(-1, coords.shape[-1])[:, :2]
|
|
else:
|
|
coords = None
|
|
|
|
scale_from_normalized = _looks_normalized(coords) if coords is not None else True
|
|
|
|
points = []
|
|
max_points = coords.shape[0] if coords is not None else 0
|
|
|
|
for idx in range(target_points):
|
|
if coords is not None and idx < max_points:
|
|
x, y = coords[idx]
|
|
conf = confs[idx] if confs is not None and idx < confs.shape[0] else default_conf
|
|
|
|
# Validate numerical values
|
|
if not np.isfinite(x) or not np.isfinite(y) or not np.isfinite(conf):
|
|
x = y = 0.0
|
|
conf = 0.0
|
|
else:
|
|
x = float(x)
|
|
y = float(y)
|
|
if scale_from_normalized:
|
|
x *= width
|
|
y *= height
|
|
conf = float(conf)
|
|
else:
|
|
x = y = 0.0
|
|
conf = 0.0
|
|
|
|
points.extend([x, y, conf])
|
|
|
|
return points
|
|
|
|
|
|
class PoseDataToPoseKeypoint:
|
|
"""
|
|
Convert WanAnimatePreprocess POSEDATA into POSE_KEYPOINT format used by ProportionChanger.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"pose_data": ("POSEDATA", {"tooltip": "POSEDATA output from WanAnimate Pose & Face Detection."}),
|
|
"width": ("INT", {"default": 512, "min": 1, "max": 4096, "step": 1}),
|
|
"height": ("INT", {"default": 768, "min": 1, "max": 4096, "step": 1}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("POSE_KEYPOINT",)
|
|
RETURN_NAMES = ("pose_keypoint",)
|
|
FUNCTION = "convert"
|
|
CATEGORY = "ProportionChanger"
|
|
|
|
def convert(self, pose_data, width: int, height: int):
|
|
if width <= 0 or height <= 0:
|
|
raise ValueError("width and height must be positive integers")
|
|
|
|
pose_metas = None
|
|
if isinstance(pose_data, dict):
|
|
pose_metas = pose_data.get("pose_metas") or pose_data.get("pose_meta")
|
|
if pose_metas is None:
|
|
pose_metas = getattr(pose_data, "pose_metas", None)
|
|
|
|
if not pose_metas:
|
|
empty_frame = {
|
|
"version": "1.0",
|
|
"people": [],
|
|
"canvas_width": width,
|
|
"canvas_height": height,
|
|
}
|
|
return ([empty_frame],)
|
|
|
|
frames = []
|
|
for meta in pose_metas:
|
|
try:
|
|
body_points = _extract_keypoints(meta, "kps_body", "kps_body_p", 25, width, height, default_conf=1.0)
|
|
# AAPoseMeta typically supplies 20 points; padder already set remaining to zeros
|
|
face_points = _extract_keypoints(meta, "kps_face", "kps_face_p", 70, width, height, default_conf=1.0)
|
|
lhand_points = _extract_keypoints(meta, "kps_lhand", "kps_lhand_p", 21, width, height, default_conf=1.0)
|
|
rhand_points = _extract_keypoints(meta, "kps_rhand", "kps_rhand_p", 21, width, height, default_conf=1.0)
|
|
|
|
# Determine if body has any confidence > 0
|
|
has_body = any(body_points[i] > 0 for i in range(2, len(body_points), 3))
|
|
|
|
frame = {
|
|
"version": "1.0",
|
|
"people": [],
|
|
"canvas_width": width,
|
|
"canvas_height": height,
|
|
}
|
|
|
|
if has_body:
|
|
person = {
|
|
"pose_keypoints_2d": body_points,
|
|
"face_keypoints_2d": face_points,
|
|
"hand_left_keypoints_2d": lhand_points,
|
|
"hand_right_keypoints_2d": rhand_points,
|
|
}
|
|
frame["people"].append(person)
|
|
|
|
frames.append(frame)
|
|
except Exception as exc:
|
|
log.error(f"PoseDataToPoseKeypoint conversion failed for frame: {exc}")
|
|
frames.append(
|
|
{
|
|
"version": "1.0",
|
|
"people": [],
|
|
"canvas_width": width,
|
|
"canvas_height": height,
|
|
}
|
|
)
|
|
|
|
return (frames,)
|