From 82a6e8a4b3ebf462842e53ef9125ae83ad0556bd Mon Sep 17 00:00:00 2001 From: grmchn Date: Sat, 13 Dec 2025 15:49:35 +0900 Subject: [PATCH] Add pose_keypoint resize node --- README.md | 3 +- README_ja.md | 3 +- nodes.py | 3 + proportion_changer/__init__.py | 4 +- proportion_changer/converter_nodes.py | 184 ++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2c2a6a..ee790b9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Additionally, the Openpose Editor node from [toyxyz/ComfyUI-ultimate-openpose-ed - **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 resize**: Resizes `POSE_KEYPOINT` to a target size (pads then scales to avoid stretching when aspect differs) - **pose_keypoint input**: Converts JSON text to KeyPoints - **pose_keypoint preview**: Converts KeyPoints to JSON @@ -60,7 +61,7 @@ Please refer to example_workflows. ### Common Issues 1. **Model Loading Errors**: Models should be automatically downloaded from HuggingFace. Please ensure DWPose models are in the correct directory. -2. **Incorrect body proportions after transformation with reference image**: Make sure the reference image size matches the pose image size. Fine-tune individual body parts using the "ProportionChanger Params" node. +2. **Incorrect body proportions after transformation with reference image**: The `pose_keypoint` and `reference_pose_keypoint` aspect ratios (canvas width/height) may not match. Make the reference image size match the pose image size, or use **pose_keypoint resize** to align `pose_keypoint` to the same `width`/`height`. Fine-tune individual body parts using the "ProportionChanger Params" node. 3. **Nothing displays after transformation with reference image**: The reference image's pose estimation by DWPose has failed. Use OpenposeEditor or similar tools to input parameter values manually. ## Attribution and Credits diff --git a/README_ja.md b/README_ja.md index d1c593c..4eed2fd 100644 --- a/README_ja.md +++ b/README_ja.md @@ -17,6 +17,7 @@ - **ProportionChanger Params**: KeyPointの各パーツのパラメータを調整する - **ProportionChanger Interpolator**: KeyPoint動画を中割りして補間する - **PoseData to pose_keypoint**: WanAnimateの`POSEDATA`を`POSE_KEYPOINT`に変換する +- **pose_keypoint resize**: `POSE_KEYPOINT`を指定サイズにリサイズ(アスペクト比が違う場合はパディング→スケールで歪みを防ぐ) - **pose_keypoint input**: JSONテキストからKeyPointに変換する - **pose_keypoint preview**: KeyPointからJSONに変換する @@ -61,7 +62,7 @@ example_workflowsを参照してください。 ### よくある問題 1. **モデル読み込みエラー**: HuggingFaceから自動ダウンロードされるはずです。DWPoseモデルが正しいディレクトリにあることを確認してください。 -2. **リファレンス画像ありでプロポーション変換した後の体型がおかしい**:リファレンス画像のサイズをポーズ画像側と同じにしてください。体の各パーツの微調整は、「ProportionChanger Params」ノードで行ってください。 +2. **リファレンス画像ありでプロポーション変換した後の体型がおかしい**: `pose_keypoint`と`reference_pose_keypoint`の縦横比(canvas幅/高さ)が一致していない可能性があります。リファレンス画像のサイズをポーズ画像側と同じにするか、**pose_keypoint resize**ノードで`pose_keypoint`側をリファレンスと同じ`width`/`height`に揃えてください。体の各パーツの微調整は「ProportionChanger Params」ノードで行ってください。 3. **リファレンス画像ありでプロポーション変換した後に何も表示されない**:リファレンス画像のDWPoseによる姿勢推定が失敗しています。OpenposeEditorなどを利用してパラメータの数値を入力してください。 ## 帰属とクレジット diff --git a/nodes.py b/nodes.py index c8ff0ed..92dede9 100644 --- a/nodes.py +++ b/nodes.py @@ -14,6 +14,7 @@ from .proportion_changer import ( ProportionChangerParams, ProportionChangerInterpolator, PoseDataToPoseKeypoint, + PoseKeypointResize, # TODO: Re-enable after fixing Kalman filter issues # ProportionChangerKeypointDenoiser, # ProportionChangerKeypointDenoiserAdvanced @@ -204,6 +205,7 @@ NODE_CLASS_MAPPINGS = { # Utility nodes "PoseDataToPoseKeypoint": PoseDataToPoseKeypoint, + "PoseKeypointResize": PoseKeypointResize, "PoseJSONToPoseKeypoint": PoseJSONToPoseKeypoint, "PoseKeypointPreview": PoseKeypointPreview, } @@ -222,6 +224,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { # Utility nodes "PoseDataToPoseKeypoint": "PoseData to pose_keypoint", + "PoseKeypointResize": "pose_keypoint resize", "PoseJSONToPoseKeypoint": "pose_keypoint input", "PoseKeypointPreview": "pose_keypoint preview", } diff --git a/proportion_changer/__init__.py b/proportion_changer/__init__.py index 70c4d93..ce09f1f 100644 --- a/proportion_changer/__init__.py +++ b/proportion_changer/__init__.py @@ -30,7 +30,8 @@ from .interpolation_node import ( # Converter nodes from .converter_nodes import ( - PoseDataToPoseKeypoint + PoseDataToPoseKeypoint, + PoseKeypointResize, ) # Import keypoint denoiser node classes @@ -57,6 +58,7 @@ __all__ = [ # Converter nodes "PoseDataToPoseKeypoint", + "PoseKeypointResize", # KeyPoint Denoiser nodes "ProportionChangerKeypointDenoiser", diff --git a/proportion_changer/converter_nodes.py b/proportion_changer/converter_nodes.py index cf48634..4f4d744 100644 --- a/proportion_changer/converter_nodes.py +++ b/proportion_changer/converter_nodes.py @@ -4,6 +4,7 @@ Converter nodes for bridging WanAnimate POSEDATA into POSE_KEYPOINT format. from __future__ import annotations +import copy import numpy as np try: # Standard package import (ComfyUI runtime) @@ -100,6 +101,81 @@ def _extract_keypoints(meta, coord_name, conf_name, target_points, width, height return points +def _looks_normalized_xy_triplets(triplets: list[float]) -> bool: + """ + Heuristic: treat POSE_KEYPOINT x/y as normalized if values are in 0..1-ish scale. + """ + if not triplets: + return True + + max_abs = 0.0 + # sample all points with confidence > 0 + for i in range(0, len(triplets) - 2, 3): + conf = triplets[i + 2] + if conf is None or conf <= 0: + continue + x = triplets[i] + y = triplets[i + 1] + if not np.isfinite(x) or not np.isfinite(y): + continue + max_abs = max(max_abs, float(abs(x)), float(abs(y))) + + return max_abs <= 2.0 + + +def _resize_pose_triplets_with_pad( + triplets: list[float], + *, + src_w: float, + src_h: float, + dst_w: float, + dst_h: float, + pad_x: float, + pad_y: float, + scale: float, +) -> list[float]: + if not triplets: + return triplets + + normalized = _looks_normalized_xy_triplets(triplets) + out = list(triplets) + + for i in range(0, len(out) - 2, 3): + x = out[i] + y = out[i + 1] + conf = out[i + 2] + + if conf is None or conf <= 0: + out[i] = 0.0 + out[i + 1] = 0.0 + out[i + 2] = 0.0 if conf is None else conf + continue + + if not np.isfinite(x) or not np.isfinite(y) or not np.isfinite(conf): + out[i] = 0.0 + out[i + 1] = 0.0 + out[i + 2] = 0.0 + continue + + x = float(x) + y = float(y) + conf = float(conf) + + if normalized: + x *= float(src_w) + y *= float(src_h) + + # pad to match target aspect, then scale to destination + x = (x + pad_x) * scale + y = (y + pad_y) * scale + + out[i] = x + out[i + 1] = y + out[i + 2] = conf + + return out + + class PoseDataToPoseKeypoint: """ Convert WanAnimatePreprocess POSEDATA into POSE_KEYPOINT format used by ProportionChanger. @@ -180,3 +256,111 @@ class PoseDataToPoseKeypoint: ) return (frames,) + + +class PoseKeypointResize: + """ + Resize POSE_KEYPOINT to desired width/height without stretching: + - If aspect ratio matches: scale directly. + - If aspect ratio differs: pad the shorter side (centered) then scale. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "pose_keypoint": ("POSE_KEYPOINT", {"tooltip": "Input POSE_KEYPOINT to resize."}), + "width": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}), + "height": ("INT", {"default": 768, "min": 1, "max": 8192, "step": 1}), + } + } + + RETURN_TYPES = ("POSE_KEYPOINT",) + RETURN_NAMES = ("pose_keypoint",) + FUNCTION = "resize" + CATEGORY = "ProportionChanger" + + def resize(self, pose_keypoint, width: int, height: int): + if width <= 0 or height <= 0: + raise ValueError("width and height must be positive integers") + + if not pose_keypoint: + return ( + [ + { + "version": "1.0", + "people": [], + "canvas_width": width, + "canvas_height": height, + } + ], + ) + + dst_w = float(width) + dst_h = float(height) + dst_ratio = dst_w / dst_h + + frames_out = [] + for frame in pose_keypoint: + frame_out = copy.deepcopy(frame) if isinstance(frame, dict) else {} + + src_w = float(frame_out.get("canvas_width", width) or width) + src_h = float(frame_out.get("canvas_height", height) or height) + if src_w <= 0 or src_h <= 0: + src_w = float(width) + src_h = float(height) + + src_ratio = src_w / src_h + + if abs(src_ratio - dst_ratio) < 1e-8: + pad_x = 0.0 + pad_y = 0.0 + padded_w = src_w + padded_h = src_h + elif src_ratio < dst_ratio: + # pad width (left/right) + padded_h = src_h + padded_w = src_h * dst_ratio + pad_x = (padded_w - src_w) / 2.0 + pad_y = 0.0 + else: + # pad height (top/bottom) + padded_w = src_w + padded_h = src_w / dst_ratio + pad_x = 0.0 + pad_y = (padded_h - src_h) / 2.0 + + scale = dst_w / padded_w if padded_w > 0 else 1.0 + + people = frame_out.get("people", []) + if not isinstance(people, list): + people = [] + + for person in people: + if not isinstance(person, dict): + continue + for key in ( + "pose_keypoints_2d", + "face_keypoints_2d", + "hand_left_keypoints_2d", + "hand_right_keypoints_2d", + ): + triplets = person.get(key) + if not isinstance(triplets, list): + continue + person[key] = _resize_pose_triplets_with_pad( + triplets, + src_w=src_w, + src_h=src_h, + dst_w=dst_w, + dst_h=dst_h, + pad_x=pad_x, + pad_y=pad_y, + scale=scale, + ) + + frame_out["canvas_width"] = width + frame_out["canvas_height"] = height + frames_out.append(frame_out) + + return (frames_out,)