From 3f3fcf3a287fac0de516f74eb05d30c6da456bb3 Mon Sep 17 00:00:00 2001 From: grmchn Date: Sat, 13 Dec 2025 15:10:42 +0900 Subject: [PATCH] Add PoseData to pose_keypoint converter node --- README.md | 9 +- README_ja.md | 7 + ...on_changer_pose_data_to_pose_keypoint.json | 725 ++++++++++++++++++ nodes.py | 5 +- plan.md | 70 ++ proportion_changer/__init__.py | 10 +- proportion_changer/converter_nodes.py | 182 +++++ proportion_changer/params_node.py | 4 +- 8 files changed, 1007 insertions(+), 5 deletions(-) create mode 100644 example_workflows/proportion_changer_pose_data_to_pose_keypoint.json create mode 100644 plan.md create mode 100644 proportion_changer/converter_nodes.py diff --git a/README.md b/README.md index f018848..a2c2a6a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Additionally, the Openpose Editor node from [toyxyz/ComfyUI-ultimate-openpose-ed - **ProportionChanger DWPose Render**: Converts KeyPoints to images - **ProportionChanger Params**: Adjusts parameters for individual KeyPoint parts - **ProportionChanger Interpolator**: Interpolates KeyPoint videos with in-betweening +- **PoseData to pose_keypoint**: Converts WanAnimate `POSEDATA` into `POSE_KEYPOINT` - **pose_keypoint input**: Converts JSON text to KeyPoints - **pose_keypoint preview**: Converts KeyPoints to JSON @@ -49,6 +50,12 @@ Please refer to example_workflows. ![proportion_changer_basic2](docs/images/example_basic2.png) +### Converting WanAnimate POSEDATA +- Use WanAnimatePreprocess "Pose and Face Detection" to produce `POSEDATA` +- Feed `POSEDATA`, `width`, and `height` into **PoseData to pose_keypoint** +- Connect the resulting `pose_keypoint` to ProportionChanger Reference / Render nodes +- Example workflow: `example_workflows/proportion_changer_pose_data_to_pose_keypoint.json` + ## Troubleshooting ### Common Issues @@ -76,4 +83,4 @@ When combining code from Apache 2.0 and GPL 3.0 licensed projects, the resulting - ProportionChanger Params functionality: Copyright by toyxyz - Modifications and integration: This project's contributors -See the [LICENSE](LICENSE) file for the full GPL 3.0 license terms. \ No newline at end of file +See the [LICENSE](LICENSE) file for the full GPL 3.0 license terms. diff --git a/README_ja.md b/README_ja.md index 4eccf27..d1c593c 100644 --- a/README_ja.md +++ b/README_ja.md @@ -16,6 +16,7 @@ - **ProportionChanger DWPose Render**: KeyPointを画像に変換する - **ProportionChanger Params**: KeyPointの各パーツのパラメータを調整する - **ProportionChanger Interpolator**: KeyPoint動画を中割りして補間する +- **PoseData to pose_keypoint**: WanAnimateの`POSEDATA`を`POSE_KEYPOINT`に変換する - **pose_keypoint input**: JSONテキストからKeyPointに変換する - **pose_keypoint preview**: KeyPointからJSONに変換する @@ -49,6 +50,12 @@ example_workflowsを参照してください。 ![proportion_changer_basic2](docs/images/example_basic2.png) +### WanAnimate POSEDATAの変換 +- WanAnimatePreprocessの「Pose and Face Detection」で`POSEDATA`を取得 +- **PoseData to pose_keypoint**ノードに`POSEDATA`とキャンバスの`width`/`height`を入力 +- 出力された`pose_keypoint`をProportionChanger Reference / Renderノードに接続 +- サンプルワークフロー: `example_workflows/proportion_changer_pose_data_to_pose_keypoint.json` + ## トラブルシューティング diff --git a/example_workflows/proportion_changer_pose_data_to_pose_keypoint.json b/example_workflows/proportion_changer_pose_data_to_pose_keypoint.json new file mode 100644 index 0000000..3027e26 --- /dev/null +++ b/example_workflows/proportion_changer_pose_data_to_pose_keypoint.json @@ -0,0 +1,725 @@ +{ + "id": "deb90f3a-9e14-4806-8e90-98c0eea4d98e", + "revision": 0, + "last_node_id": 12, + "last_link_id": 18, + "nodes": [ + { + "id": 7, + "type": "DrawViTPose", + "pos": [ + 7440.435138869547, + 6129.696683823703 + ], + "size": [ + 270, + 178 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "pose_data", + "type": "POSEDATA", + "link": 9 + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 10 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 11 + } + ], + "outputs": [ + { + "name": "pose_images", + "type": "IMAGE", + "links": [ + 5 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanAnimatePreprocess", + "ver": "2fcbcae7eec637fdc712fdec18e6266feb8ba3a7", + "Node name for S&R": "DrawViTPose" + }, + "widgets_values": [ + 480, + 832, + 16, + -1, + -1, + true + ] + }, + { + "id": 9, + "type": "GetImageSizeAndCount", + "pos": [ + 6621.080613343585, + 6467.158135374485 + ], + "size": [ + 196.2994140625, + 86 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 16 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 13 + ] + }, + { + "label": "512 width", + "name": "width", + "type": "INT", + "links": [ + 7, + 10, + 14 + ] + }, + { + "label": "920 height", + "name": "height", + "type": "INT", + "links": [ + 8, + 11, + 15 + ] + }, + { + "label": "16 count", + "name": "count", + "type": "INT", + "links": [] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "f37df472dfb325f1f72c90e2df56e72f40f850f0", + "Node name for S&R": "GetImageSizeAndCount" + }, + "widgets_values": [] + }, + { + "id": 10, + "type": "VHS_LoadVideo", + "pos": [ + 6273.467729604059, + 6468.808175961504 + ], + "size": [ + 296.0120268545073, + 819.5215916898854 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 16 + ] + }, + { + "name": "frame_count", + "type": "INT", + "links": [] + }, + { + "name": "audio", + "type": "AUDIO", + "links": [] + }, + { + "name": "video_info", + "type": "VHS_VIDEOINFO", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "8550981384301e9bc5bfea83e5c2c75258102593", + "Node name for S&R": "VHS_LoadVideo" + }, + "widgets_values": { + "video": "yoshi1_sample_girl.mp4", + "force_rate": 16, + "custom_width": 512, + "custom_height": 920, + "frame_load_cap": 16, + "skip_first_frames": 1, + "select_every_nth": 1, + "format": "AnimateDiff", + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "yoshi1_sample_girl.mp4", + "type": "input", + "format": "video/mp4", + "force_rate": 16, + "custom_width": 512, + "custom_height": 920, + "frame_load_cap": 16, + "skip_first_frames": 1, + "select_every_nth": 1 + } + } + } + }, + { + "id": 11, + "type": "OnnxDetectionModelLoader", + "pos": [ + 6276.524633017252, + 6134.099122226672 + ], + "size": [ + 430.1680197224765, + 106 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "model", + "type": "POSEMODEL", + "links": [ + 12 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanAnimatePreprocess", + "ver": "2fcbcae7eec637fdc712fdec18e6266feb8ba3a7", + "Node name for S&R": "OnnxDetectionModelLoader" + }, + "widgets_values": [ + "onnx/vitpose_h_wholebody_model.onnx", + "onnx/yolov10m.onnx", + "CUDAExecutionProvider" + ] + }, + { + "id": 8, + "type": "PoseAndFaceDetection", + "pos": [ + 6902.806960621858, + 6134.325632658536 + ], + "size": [ + 313.125, + 162 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "POSEMODEL", + "link": 12 + }, + { + "name": "images", + "type": "IMAGE", + "link": 13 + }, + { + "name": "retarget_image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 14 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 15 + } + ], + "outputs": [ + { + "name": "pose_data", + "type": "POSEDATA", + "links": [ + 6, + 9 + ] + }, + { + "name": "face_images", + "type": "IMAGE", + "links": null + }, + { + "name": "key_frame_body_points", + "type": "STRING", + "links": null + }, + { + "name": "bboxes", + "type": "BBOX", + "links": null + }, + { + "name": "face_bboxes", + "type": "BBOX,", + "links": null + } + ], + "properties": { + "cnr_id": "ComfyUI-WanAnimatePreprocess", + "ver": "2fcbcae7eec637fdc712fdec18e6266feb8ba3a7", + "Node name for S&R": "PoseAndFaceDetection" + }, + "widgets_values": [ + 480, + 832 + ] + }, + { + "id": 6, + "type": "PoseDataToPoseKeypoint", + "pos": [ + 6998.0968322251765, + 6726.796517643764 + ], + "size": [ + 280.7720703125, + 82 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "pose_data", + "type": "POSEDATA", + "link": 6 + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 7 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 8 + } + ], + "outputs": [ + { + "name": "pose_keypoint", + "type": "POSE_KEYPOINT", + "links": [ + 3 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-ProportionChanger", + "ver": "16cda84ffa029ac60fc2da05f727fafcdf90fcd6", + "Node name for S&R": "PoseDataToPoseKeypoint" + }, + "widgets_values": [ + 512, + 768 + ] + }, + { + "id": 5, + "type": "VHS_VideoCombine", + "pos": [ + 7773.121842444329, + 6122.950426243646 + ], + "size": [ + 270.25917466462124, + 797.6844544754913 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 5 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "0a75c7958fe320efcb052f1d9f8451fd20c730a8", + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 16, + "loop_count": 0, + "filename_prefix": "ProportionChanger_Dev", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "ProportionChanger_Dev_00011.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "ProportionChanger_Dev_00011.png", + "fullpath": "/home/shins/works/ComfyUI/output/ProportionChanger_Dev_00011.mp4" + } + } + } + }, + { + "id": 1, + "type": "VHS_VideoCombine", + "pos": [ + 7776.91575807864, + 6987.710777633694 + ], + "size": [ + 270.25917466462124, + 797.6844544754913 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 18 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": null + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "0a75c7958fe320efcb052f1d9f8451fd20c730a8", + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 16, + "loop_count": 0, + "filename_prefix": "ProportionChanger", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "ProportionChanger_00001.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 16, + "workflow": "ProportionChanger_00001.png", + "fullpath": "/home/shins/works/ComfyUI/output/ProportionChanger_00001.mp4" + } + } + } + }, + { + "id": 3, + "type": "ProportionChangerDWPoseRender", + "pos": [ + 7349.095248798733, + 6869.300113529442 + ], + "size": [ + 337.8521484375, + 226 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "pose_keypoint", + "type": "POSE_KEYPOINT", + "link": 3 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 18 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-ProportionChanger", + "ver": "16cda84ffa029ac60fc2da05f727fafcdf90fcd6", + "Node name for S&R": "ProportionChangerDWPoseRender" + }, + "widgets_values": [ + true, + false, + true, + true, + -1, + 4, + 3, + 2 + ] + } + ], + "links": [ + [ + 3, + 6, + 0, + 3, + 0, + "POSE_KEYPOINT" + ], + [ + 5, + 7, + 0, + 5, + 0, + "IMAGE" + ], + [ + 6, + 8, + 0, + 6, + 0, + "POSEDATA" + ], + [ + 7, + 9, + 1, + 6, + 1, + "INT" + ], + [ + 8, + 9, + 2, + 6, + 2, + "INT" + ], + [ + 9, + 8, + 0, + 7, + 0, + "POSEDATA" + ], + [ + 10, + 9, + 1, + 7, + 1, + "INT" + ], + [ + 11, + 9, + 2, + 7, + 2, + "INT" + ], + [ + 12, + 11, + 0, + 8, + 0, + "POSEMODEL" + ], + [ + 13, + 9, + 0, + 8, + 1, + "IMAGE" + ], + [ + 14, + 9, + 1, + 8, + 3, + "INT" + ], + [ + 15, + 9, + 2, + 8, + 4, + "INT" + ], + [ + 16, + 10, + 0, + 9, + 0, + "IMAGE" + ], + [ + 18, + 3, + 0, + 1, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.5559917313492256, + "offset": [ + -6097.1435759818805, + -6089.885949666708 + ] + }, + "workflowRendererVersion": "LG", + "frontendVersion": "1.35.4", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/nodes.py b/nodes.py index ff87ca0..c8ff0ed 100644 --- a/nodes.py +++ b/nodes.py @@ -13,6 +13,7 @@ from .proportion_changer import ( ProportionChangerDWPoseRender, ProportionChangerParams, ProportionChangerInterpolator, + PoseDataToPoseKeypoint, # TODO: Re-enable after fixing Kalman filter issues # ProportionChangerKeypointDenoiser, # ProportionChangerKeypointDenoiserAdvanced @@ -202,6 +203,7 @@ NODE_CLASS_MAPPINGS = { # "ProportionChangerKeypointDenoiserAdvanced": ProportionChangerKeypointDenoiserAdvanced, # Utility nodes + "PoseDataToPoseKeypoint": PoseDataToPoseKeypoint, "PoseJSONToPoseKeypoint": PoseJSONToPoseKeypoint, "PoseKeypointPreview": PoseKeypointPreview, } @@ -219,6 +221,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { # "ProportionChangerKeypointDenoiserAdvanced": "ProportionChanger Denoiser (Advanced)", # Utility nodes + "PoseDataToPoseKeypoint": "PoseData to pose_keypoint", "PoseJSONToPoseKeypoint": "pose_keypoint input", "PoseKeypointPreview": "pose_keypoint preview", -} \ No newline at end of file +} diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..7aee631 --- /dev/null +++ b/plan.md @@ -0,0 +1,70 @@ +# Plan: PoseData to PoseKeypoint converter node + +## Goal +Add a ComfyUI node (e.g. `PoseDataToPoseKeypoint`) inside ProportionChanger that converts WanAnimatePreprocess `POSEDATA` into ProportionChanger/UniAnimate-style `POSE_KEYPOINT` so downstream nodes can reuse DWPose-format keypoints without needing WanAnimate code. + +## Expected IO +- Inputs (match Draw ViT Pose for familiarity): + - `pose_data` (POSEDATA) – produced by Pose and Face Detection. + - `width` (INT) – canvas width to scale normalized coords to pixels. + - `height` (INT) – canvas height. +- Output: + - `pose_keypoint` (POSE_KEYPOINT) – list of frame dicts following UniAnimate/OpenPose style used in `ProportionChangerDWPoseDetector`. + +## Source formats to bridge +- `pose_data["pose_metas"]`: list of `AAPoseMeta` or humanapi meta clones; body/hand/face keypoints are **normalized 0–1** and packed as numpy arrays with confidences. +- ProportionChanger expects per-frame dict: + ```json + { + "version": "1.0", + "people": [ + { + "pose_keypoints_2d": [x,y,conf]*25, + "face_keypoints_2d": [x,y,conf]*70, + "hand_left_keypoints_2d": [x,y,conf]*21, + "hand_right_keypoints_2d": [x,y,conf]*21 + } + ], + "canvas_width": W, + "canvas_height": H + } + ``` +- Body point count mismatch: AAPoseMeta body has 20 keypoints (19-22 are averaged joints, no toes). DWPose/UniAnimate uses 25 (includes toes idx 19–24). Plan to map available 20 into first 20 slots and pad toes (19–24) with zeros/conf=0. + +## Conversion rules +1) For each `meta` in `pose_data["pose_metas"]`: + - Scale all keypoints by `width`/`height` to pixels. + - Body: take `meta.kps_body` (shape 20x2) + `meta.kps_body_p` to build 25*3 list; pad missing 5 joints (toe L/R and ankle variants) with 0s and conf 0. +- Face: `meta.kps_face` (69 pts) -> first 69; pad to 70 with zeros; confidences from `kps_face_p` if available else 1. + - Hands: `meta.kps_lhand`, `kps_rhand` (21 pts each); pad if missing; use `kps_*_p` for conf else 1. + - Build `people` entry; if no valid body points (all conf 0), emit empty `people` list. +2) Append `canvas_width/height` from input args. +3) Return list ordered to match frames. + +## Node design +- New file: add class `PoseDataToPoseKeypoint` under `proportion_changer` package (e.g. `converter_nodes.py` or existing util file) and expose via `NODE_CLASS_MAPPINGS`/`NODE_DISPLAY_NAME_MAPPINGS` in `nodes.py`. +- Category: `ProportionChanger`. +- INPUT_TYPES same three fields; RETURN_TYPES (`POSE_KEYPOINT`,) RETURN_NAMES (`pose_keypoint`,). +- Keep tensor-free; operates on Python lists/numpy. +- Minimal deps: reuse numpy if already available; avoid torch. + +## Edge cases to handle +- Missing `kps_face` or hands: pad with zeros/conf=0. +- NaN/None in keypoints: coerce to 0, conf=0. +- Empty `pose_metas`: return `[ {version:"1.0", people:[], canvas_width:W, canvas_height:H} ]`. +- Width/height ≤0: fail fast with ValueError. + +## Tests / validation +- Add lightweight unit test under `test/` to feed fake `pose_data` with one meta and assert lengths (75/210/63/63) and canvas sizes. +- (Optional) round-trip check: feed output to `PoseKeypointPreview` to ensure JSON renders; manual workflow example in `example_workflows`. + +## Integration steps +1) Implement converter node code. +2) Wire into `NODE_CLASS_MAPPINGS`/`NODE_DISPLAY_NAME_MAPPINGS`. +3) Add doc blurb to `README.md` (usage + expected inputs). +4) Add test and run `pytest` (or targeted script) if available. + +## Reference file paths (relative to this plan) +- `../ComfyUI-WanAnimatePreprocess/nodes.py` — DrawViTPose input schema (`pose_data`, `width`, `height`), PoseAndFaceDetection return structure. +- `../ComfyUI-WanAnimatePreprocess/pose_utils/pose2d_utils.py` — `AAPoseMeta` structure, keypoint ordering, `load_from_meta` and related helpers. +- `../ComfyUI-WanAnimatePreprocess/pose_utils/human_visualization.py` (optional) — shows how body/hand indices map to drawing; useful to confirm joint ordering if needed. diff --git a/proportion_changer/__init__.py b/proportion_changer/__init__.py index b4000b8..70c4d93 100644 --- a/proportion_changer/__init__.py +++ b/proportion_changer/__init__.py @@ -28,6 +28,11 @@ from .interpolation_node import ( ProportionChangerInterpolator ) +# Converter nodes +from .converter_nodes import ( + PoseDataToPoseKeypoint +) + # Import keypoint denoiser node classes from .keypoint_denoiser_node import ( ProportionChangerKeypointDenoiser, @@ -49,8 +54,11 @@ __all__ = [ # Interpolation nodes "ProportionChangerInterpolator", + + # Converter nodes + "PoseDataToPoseKeypoint", # KeyPoint Denoiser nodes "ProportionChangerKeypointDenoiser", "ProportionChangerKeypointDenoiserAdvanced" -] \ No newline at end of file +] diff --git a/proportion_changer/converter_nodes.py b/proportion_changer/converter_nodes.py new file mode 100644 index 0000000..cf48634 --- /dev/null +++ b/proportion_changer/converter_nodes.py @@ -0,0 +1,182 @@ +""" +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,) diff --git a/proportion_changer/params_node.py b/proportion_changer/params_node.py index ba7acc7..6763621 100644 --- a/proportion_changer/params_node.py +++ b/proportion_changer/params_node.py @@ -308,7 +308,7 @@ class ProportionChangerParams: if not all([orig_r_hip, orig_l_hip, orig_neck, orig_nose]): return adjusted # Return unchanged if essential keypoints are missing - # 階層構造で変換を適用:hip_center → neck → shoulders → arms, legs + # 階層構造で変換を適用:hip_center -> neck -> shoulders -> arms, legs # 全て元の座標を基準に計算し、依存関係を考慮 # 1. 骨盤の調整(元の座標基準) @@ -801,4 +801,4 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { "ProportionChangerParams": "ProportionChanger Params" -} \ No newline at end of file +}