diff --git a/__init__.py b/__init__.py index bda963a..f5690f5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,14 +1,17 @@ from .openpose_editor_nodes import OpenposeEditorNode +from .appendage_editor_nodes import AppendageEditorNode WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = { "OpenposeEditorNode": OpenposeEditorNode, + "AppendageEditorNode": AppendageEditorNode, } NODE_DISPLAY_NAME_MAPPINGS = { "OpenposeEditorNode": "Openpose Editor Node", + "AppendageEditorNode": "Appendage Editor", } __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/appendage_editor_nodes.py b/appendage_editor_nodes.py new file mode 100644 index 0000000..14d6df2 --- /dev/null +++ b/appendage_editor_nodes.py @@ -0,0 +1,325 @@ +import json +import copy +import math +import torch +import numpy as np +from .util import scale, draw_pose_json +from .openpose_editor_nodes import OpenposeEditorNode + +class AppendageEditorNode: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "POSE_KEYPOINT": ("POSE_KEYPOINT",), + "appendage_type": ([ + "left_upper_arm", "left_forearm", "left_full_arm", + "right_upper_arm", "right_forearm", "right_full_arm", + "left_upper_leg", "left_lower_leg", "left_full_leg", + "right_upper_leg", "right_lower_leg", "right_full_leg", + "left_hand", "right_hand", "left_foot", "right_foot", + "torso", "shoulders" + ], { + "default": "left_upper_arm" + }), + }, + "optional": { + "scale": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 10.0, + "step": 0.05 + }), + "x_offset": ("FLOAT", { + "default": 0.0, + "min": -2.0, + "max": 2.0, + "step": 0.01 + }), + "y_offset": ("FLOAT", { + "default": 0.0, + "min": -2.0, + "max": 2.0, + "step": 0.01 + }), + "rotation": ("FLOAT", { + "default": 0.0, + "min": -180.0, + "max": 180.0, + "step": 1.0 + }), + "bidirectional_scale": ("BOOLEAN", { + "default": False, + "tooltip": "If true, scales in both directions from pivot. If false, only scales away from body to prevent cannibalizing adjacent parts." + }), + "person_index": ("INT", { + "default": -1, + "min": -1, + "max": 100, + "tooltip": "Person to edit (-1 for all people)" + }), + "list_mismatch_behavior": (["truncate", "loop", "repeat"], {"default": "loop", "tooltip": "Truncate: Truncate the list to the shortest length. Loop: Loop the list to the longest length. Repeat: Repeat the list to the longest length."}), + }, + } + + RETURN_NAMES = ("POSE_KEYPOINT",) + RETURN_TYPES = ("POSE_KEYPOINT",) + FUNCTION = "edit_appendage" + CATEGORY = "ultimate-openpose" + + def edit_appendage(self, POSE_KEYPOINT, appendage_type, scale=1.0, x_offset=0.0, y_offset=0.0, rotation=0.0, bidirectional_scale=False, person_index=-1, list_mismatch_behavior="loop"): + if POSE_KEYPOINT is None: + return (None,) + + # Deep copy to avoid modifying the original + pose_data = copy.deepcopy(POSE_KEYPOINT) + if not isinstance(pose_data, list): + pose_data = [pose_data] + + pose_count = len(pose_data) + + # Normalize scale parameters to handle lists vs single floats using the original node's methods + scale_params = [scale, x_offset, y_offset, rotation] + output_length = OpenposeEditorNode.determine_output_length(scale_params, pose_count, list_mismatch_behavior) + + scale_list = OpenposeEditorNode.normalize_scale_parameter(scale, output_length, list_mismatch_behavior) + x_offset_list = OpenposeEditorNode.normalize_scale_parameter(x_offset, output_length, list_mismatch_behavior) + y_offset_list = OpenposeEditorNode.normalize_scale_parameter(y_offset, output_length, list_mismatch_behavior) + rotation_list = OpenposeEditorNode.normalize_scale_parameter(rotation, output_length, list_mismatch_behavior) + + # Process each frame with its corresponding parameter values + output_pose_data = [] + + for i in range(output_length): + # Get the pose data for this index + pose_idx = i if i < pose_count else pose_count - 1 + if list_mismatch_behavior == "loop" and pose_count > 0: + pose_idx = i % pose_count + + # Get current frame and parameter values + current_frame = copy.deepcopy(pose_data[pose_idx]) + current_scale = scale_list[i] + current_x_offset = x_offset_list[i] + current_y_offset = y_offset_list[i] + current_rotation = rotation_list[i] + + # Apply transformations to this frame + if 'people' in current_frame: + people_to_edit = range(len(current_frame['people'])) if person_index == -1 else [person_index] + + for person_idx in people_to_edit: + if person_idx >= len(current_frame['people']): + continue + + person = current_frame['people'][person_idx] + + if appendage_type in ["left_hand", "right_hand"]: + self._edit_hand_appendage(person, appendage_type, current_scale, current_x_offset, current_y_offset, current_rotation, bidirectional_scale) + else: + self._edit_body_appendage(person, appendage_type, current_scale, current_x_offset, current_y_offset, current_rotation, bidirectional_scale) + + output_pose_data.append(current_frame) + + return (output_pose_data if isinstance(POSE_KEYPOINT, list) else output_pose_data[0],) + + def _edit_hand_appendage(self, person, appendage_type, scale_factor, x_offset, y_offset, rotation, bidirectional_scale): + """Edit hand appendages using hand keypoints.""" + keypoint_field = "hand_left_keypoints_2d" if appendage_type == "left_hand" else "hand_right_keypoints_2d" + + if keypoint_field not in person or not person[keypoint_field]: + return + + keypoints = person[keypoint_field] + + # Use wrist (first point) as pivot for hands + if len(keypoints) >= 3 and keypoints[2] > 0: + pivot = [keypoints[0], keypoints[1]] + else: + # Calculate center of mass if wrist not available + pivot = self._calculate_center_of_mass(keypoints) + if pivot is None: + return + + # Apply transformations + new_keypoints = self._apply_transformations(keypoints, scale_factor, x_offset, y_offset, rotation, pivot, bidirectional_scale) + person[keypoint_field] = new_keypoints + + def _edit_body_appendage(self, person, appendage_type, scale_factor, x_offset, y_offset, rotation, bidirectional_scale): + """Edit body appendages (arms, legs, feet) using body pose keypoints.""" + if 'pose_keypoints_2d' not in person or not person['pose_keypoints_2d']: + return + + keypoints = person['pose_keypoints_2d'] + + # Get keypoint indices for the specific appendage + appendage_indices, pivot_index = self._get_appendage_indices(appendage_type) + if not appendage_indices: + return + + # Calculate pivot point for the appendage + pivot = self._calculate_appendage_pivot(keypoints, appendage_indices, pivot_index) + if pivot is None: + return + + # Apply transformations only to the appendage keypoints + new_keypoints = keypoints[:] + + for i in range(0, len(keypoints), 3): + keypoint_idx = i // 3 + if keypoint_idx in appendage_indices and len(keypoints) > i+2: + x, y, conf = keypoints[i], keypoints[i+1], keypoints[i+2] + + if conf > 0: + # Apply rotation + if rotation != 0.0: + rad = math.radians(rotation) + cos_r, sin_r = math.cos(rad), math.sin(rad) + rel_x, rel_y = x - pivot[0], y - pivot[1] + x = rel_x * cos_r - rel_y * sin_r + pivot[0] + y = rel_x * sin_r + rel_y * cos_r + pivot[1] + + # Apply scaling with directional control + if scale_factor != 1.0: + if bidirectional_scale: + scaled_point = scale([x, y], scale_factor, pivot) + x, y = scaled_point[0], scaled_point[1] + else: + # Unidirectional scaling - only scale away from body + x, y = self._apply_unidirectional_scale([x, y], scale_factor, pivot, keypoint_idx, pivot_index) + + # Apply offset + x += x_offset + y += y_offset + + new_keypoints[i] = x + new_keypoints[i+1] = y + + person['pose_keypoints_2d'] = new_keypoints + + def _get_appendage_indices(self, appendage_type): + """Get OpenPose keypoint indices for specific appendages and their pivot points.""" + # COCO 18-keypoint format (0-based) used by ComfyUI ControlNet Aux OpenPose Pose node: + # 0: Nose, 1: Neck, 2: RShoulder, 3: RElbow, 4: RWrist, 5: LShoulder, 6: LElbow, 7: LWrist, + # 8: RHip, 9: RKnee, 10: RAnkle, 11: LHip, 12: LKnee, 13: LAnkle, 14: REye, 15: LEye, 16: REar, 17: LEar + + appendage_map = { + # Arms - COCO format + "left_upper_arm": ([5, 6], 5), # LShoulder, LElbow (pivot: shoulder) + "left_forearm": ([6, 7], 6), # LElbow, LWrist (pivot: elbow) + "left_full_arm": ([5, 6, 7], 5), # LShoulder, LElbow, LWrist (pivot: shoulder) + "right_upper_arm": ([2, 3], 2), # RShoulder, RElbow (pivot: shoulder) + "right_forearm": ([3, 4], 3), # RElbow, RWrist (pivot: elbow) + "right_full_arm": ([2, 3, 4], 2), # RShoulder, RElbow, RWrist (pivot: shoulder) + + # Legs - COCO format (FIXED!) + "left_upper_leg": ([11, 12], 11), # LHip, LKnee (pivot: hip) + "left_lower_leg": ([12, 13], 12), # LKnee, LAnkle (pivot: knee) - FIXED: was [13,14] which was LAnkle,REye! + "left_full_leg": ([11, 12, 13], 11), # LHip, LKnee, LAnkle (pivot: hip) + "right_upper_leg": ([8, 9], 8), # RHip, RKnee (pivot: hip) + "right_lower_leg": ([9, 10], 9), # RKnee, RAnkle (pivot: knee) + "right_full_leg": ([8, 9, 10], 8), # RHip, RKnee, RAnkle (pivot: hip) + + # Feet - COCO format (no foot keypoints in COCO, use ankle only) + "left_foot": ([13], 13), # LAnkle only (pivot: ankle) + "right_foot": ([10], 10), # RAnkle only (pivot: ankle) + + # Torso and Shoulders - COCO format + "torso": ([1, 2, 5, 8, 11], 1), # Neck, RShoulder, LShoulder, RHip, LHip (pivot: neck) + "shoulders": ([2, 5], 1), # RShoulder, LShoulder (pivot: neck) + } + + result = appendage_map.get(appendage_type, ([], None)) + return result[0], result[1] + + def _calculate_appendage_pivot(self, keypoints, appendage_indices, pivot_index): + """Calculate pivot point for body appendage using specified pivot index.""" + if pivot_index is not None: + # Use specific pivot point (e.g., shoulder for upper arm, elbow for forearm) + i = pivot_index * 3 + if len(keypoints) > i+2 and keypoints[i+2] > 0: + return [keypoints[i], keypoints[i+1]] + + # Fallback to center of mass if pivot point not available + valid_points = [] + for idx in appendage_indices: + i = idx * 3 + if len(keypoints) > i+2 and keypoints[i+2] > 0: + valid_points.append([keypoints[i], keypoints[i+1]]) + + if not valid_points: + return None + + pivot_x = sum(p[0] for p in valid_points) / len(valid_points) + pivot_y = sum(p[1] for p in valid_points) / len(valid_points) + return [pivot_x, pivot_y] + + def _apply_unidirectional_scale(self, point, scale_factor, pivot, keypoint_idx, pivot_index): + """Apply scaling only in the direction away from the body/pivot.""" + x, y = point + + if keypoint_idx == pivot_index: + # Don't scale the pivot point itself + return x, y + + # Calculate direction vector from pivot to point + dx = x - pivot[0] + dy = y - pivot[1] + + # Scale only the distance, keeping direction + distance = math.sqrt(dx*dx + dy*dy) + if distance > 0: + new_distance = distance * scale_factor + scale_ratio = new_distance / distance + + new_x = pivot[0] + dx * scale_ratio + new_y = pivot[1] + dy * scale_ratio + return new_x, new_y + + return x, y + + def _calculate_center_of_mass(self, keypoints): + """Calculate center of mass from valid keypoints.""" + valid_points = [] + for i in range(0, len(keypoints), 3): + if len(keypoints) > i+2 and keypoints[i+2] > 0: + valid_points.append([keypoints[i], keypoints[i+1]]) + + if not valid_points: + return None + + pivot_x = sum(p[0] for p in valid_points) / len(valid_points) + pivot_y = sum(p[1] for p in valid_points) / len(valid_points) + return [pivot_x, pivot_y] + + def _apply_transformations(self, keypoints, scale_factor, x_offset, y_offset, rotation, pivot, bidirectional_scale): + """Apply transformations to all keypoints.""" + new_keypoints = [] + for i in range(0, len(keypoints), 3): + if len(keypoints) > i+2: + x, y, conf = keypoints[i], keypoints[i+1], keypoints[i+2] + + if conf > 0: + # Apply rotation + if rotation != 0.0: + rad = math.radians(rotation) + cos_r, sin_r = math.cos(rad), math.sin(rad) + rel_x, rel_y = x - pivot[0], y - pivot[1] + x = rel_x * cos_r - rel_y * sin_r + pivot[0] + y = rel_x * sin_r + rel_y * cos_r + pivot[1] + + # Apply scaling + if scale_factor != 1.0: + if bidirectional_scale: + scaled_point = scale([x, y], scale_factor, pivot) + x, y = scaled_point[0], scaled_point[1] + else: + # For hands, use unidirectional scaling from wrist + x, y = self._apply_unidirectional_scale([x, y], scale_factor, pivot, i//3, 0) + + # Apply offset + x += x_offset + y += y_offset + + new_keypoints.extend([x, y, conf]) + + return new_keypoints diff --git a/examples/appendage_editor_example.json b/examples/appendage_editor_example.json new file mode 100644 index 0000000..f4cd811 --- /dev/null +++ b/examples/appendage_editor_example.json @@ -0,0 +1,893 @@ +{ + "id": "b7f4ff12-a2ac-431b-8ea5-f6f2e7e1b982", + "revision": 0, + "last_node_id": 567, + "last_link_id": 1093, + "nodes": [ + { + "id": 556, + "type": "AppendageEditorNode", + "pos": [ + 53.78276062011719, + 1469.3089599609375 + ], + "size": [ + 456, + 226 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1078 + } + ], + "outputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1080 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "ea50244cd95102872209f60087ed49c80e750ff8", + "widget_ue_connectable": {}, + "Node name for S&R": "AppendageEditorNode" + }, + "widgets_values": [ + "shoulders", + 0.6300000000000001, + 0, + 0, + 0, + false, + -1, + "loop" + ] + }, + { + "id": 557, + "type": "AppendageEditorNode", + "pos": [ + 636.4037475585938, + 1472.977783203125 + ], + "size": [ + 456, + 226 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1080 + } + ], + "outputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1082 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "ea50244cd95102872209f60087ed49c80e750ff8", + "widget_ue_connectable": {}, + "Node name for S&R": "AppendageEditorNode" + }, + "widgets_values": [ + "torso", + 0.6600000000000001, + 0, + 0, + 0, + false, + -1, + "loop" + ] + }, + { + "id": 558, + "type": "AppendageEditorNode", + "pos": [ + 1213.678466796875, + 1469.1339111328125 + ], + "size": [ + 456, + 226 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1082 + } + ], + "outputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1084 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "ea50244cd95102872209f60087ed49c80e750ff8", + "widget_ue_connectable": {}, + "Node name for S&R": "AppendageEditorNode" + }, + "widgets_values": [ + "left_lower_leg", + 1, + 0.10000000000000002, + 0, + 11.818115234375, + false, + -1, + "loop" + ] + }, + { + "id": 521, + "type": "PoseRendererNode", + "pos": [ + 1762.187744140625, + 1466.8565673828125 + ], + "size": [ + 315, + 202 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1084 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1019 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "4cfb549344eb2c6f893b364da88d690c5414d3a2", + "Node name for S&R": "PoseRendererNode", + "widget_ue_connectable": {} + }, + "widgets_values": [ + true, + true, + true, + -1, + 4, + 3, + 2 + ] + }, + { + "id": 524, + "type": "PreviewImage", + "pos": [ + 2159.125, + 1455.14111328125 + ], + "size": [ + 210, + 246 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 1019 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.30", + "Node name for S&R": "PreviewImage", + "widget_ue_connectable": {} + }, + "widgets_values": [] + }, + { + "id": 518, + "type": "LoadImage", + "pos": [ + -1169.2633056640625, + 1442.111328125 + ], + "size": [ + 342.5999755859375, + 314 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1014 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.30", + "Node name for S&R": "LoadImage", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "tpose.png", + "image" + ] + }, + { + "id": 516, + "type": "OpenposePreprocessor", + "pos": [ + -686.077392578125, + 1477.248779296875 + ], + "size": [ + 481.1999816894531, + 174 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 1014 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": null + }, + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1078, + 1086 + ] + } + ], + "properties": { + "cnr_id": "comfyui_controlnet_aux", + "ver": "1.0.7", + "Node name for S&R": "OpenposePreprocessor", + "widget_ue_connectable": {} + }, + "widgets_values": [ + "enable", + "enable", + "enable", + 512, + "disable" + ] + }, + { + "id": 564, + "type": "FeatureToFlexFloatParam", + "pos": [ + 20, + 2690 + ], + "size": [ + 343.35400390625, + 106 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "feature", + "type": "FEATURE", + "link": 1088 + } + ], + "outputs": [ + { + "name": "PARAMETER", + "type": "FLOAT", + "links": [ + 1090 + ] + } + ], + "properties": { + "cnr_id": "comfyui_ryanontheinside", + "ver": "5907a3591a2feb5531d533194b6c26ead84ce835", + "widget_ue_connectable": {}, + "Node name for S&R": "FeatureToFlexFloatParam" + }, + "widgets_values": [ + true, + 0.5000000000000001, + 1 + ] + }, + { + "id": 565, + "type": "PoseRendererNode", + "pos": [ + 1610, + 2450 + ], + "size": [ + 315, + 202 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1093 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1092 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "4cfb549344eb2c6f893b364da88d690c5414d3a2", + "widget_ue_connectable": {}, + "Node name for S&R": "PoseRendererNode" + }, + "widgets_values": [ + true, + true, + true, + -1, + 4, + 3, + 2 + ] + }, + { + "id": 561, + "type": "AppendageEditorNode", + "pos": [ + 1090, + 2430 + ], + "size": [ + 456, + 226 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1085 + }, + { + "name": "scale", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "scale" + }, + "link": 1090 + } + ], + "outputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1093 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "ea50244cd95102872209f60087ed49c80e750ff8", + "widget_ue_connectable": {}, + "Node name for S&R": "AppendageEditorNode" + }, + "widgets_values": [ + "torso", + 0.6600000000000001, + 0, + 0, + 0, + false, + -1, + "loop" + ] + }, + { + "id": 559, + "type": "Note", + "pos": [ + -1086.4820556640625, + 1180.427734375 + ], + "size": [ + 362.96533203125, + 155.5653076171875 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Chain any number of the appendage editors together, then render with the pose renderer node." + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 560, + "type": "AppendageEditorNode", + "pos": [ + 570, + 2430 + ], + "size": [ + 456, + 226 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "link": 1086 + }, + { + "name": "scale", + "shape": 7, + "type": "FLOAT", + "widget": { + "name": "scale" + }, + "link": 1089 + } + ], + "outputs": [ + { + "name": "POSE_KEYPOINT", + "type": "POSE_KEYPOINT", + "links": [ + 1085 + ] + } + ], + "properties": { + "cnr_id": "westNeighbor_ComfyUI-ultimate-openpose-editor", + "ver": "ea50244cd95102872209f60087ed49c80e750ff8", + "widget_ue_connectable": {}, + "Node name for S&R": "AppendageEditorNode" + }, + "widgets_values": [ + "shoulders", + 0.6300000000000001, + 0, + 0, + 0, + false, + -1, + "loop" + ] + }, + { + "id": 562, + "type": "DrawableFeatureNode", + "pos": [ + -1188.8538818359375, + 2666.18310546875 + ], + "size": [ + 1050, + 800 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "FEATURE", + "type": "FEATURE", + "links": [ + 1087, + 1088 + ] + } + ], + "properties": { + "cnr_id": "comfyui_ryanontheinside", + "ver": "5907a3591a2feb5531d533194b6c26ead84ce835", + "widget_ue_connectable": {}, + "Node name for S&R": "DrawableFeatureNode" + }, + "widgets_values": [ + "drawn", + 30, + 30, + 512, + 512, + 0, + 1, + "linear", + 0, + "[[0,0.00197697778680872],[12,0.6067085132724491],[17,0.549718180796861],[21,0.8061749426494923],[26,0.796676465332732],[29,0.9993090183870311]]", + "selected_template", + 1, + "load", + "clear" + ], + "points": [ + [ + 0, + 0.00197697778680872 + ], + [ + 12, + 0.6067085132724491 + ], + [ + 17, + 0.549718180796861 + ], + [ + 21, + 0.8061749426494923 + ], + [ + 26, + 0.796676465332732 + ], + [ + 29, + 0.9993090183870311 + ] + ] + }, + { + "id": 567, + "type": "Note", + "pos": [ + -1087.937255859375, + 2311.41943359375 + ], + "size": [ + 406.4625549316406, + 253.2763671875 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "You can also schedule the parameters using features. We still have a single image as input, but 30 frames in the feature, so the number of images is increased (according to list_mismatch_behavior). Instead of a single image, a video of poses can be used if so desired. \n\nThis is a drawable feature below, but there are many feature sources including but not limited to \n\nAudio\nMIDI\nDepth\nColor\nProximity\nTime\nText \nArea\nand so on" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 563, + "type": "FeatureToFlexFloatParam", + "pos": [ + 20, + 2490 + ], + "size": [ + 330.15570068359375, + 109.04576110839844 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "feature", + "type": "FEATURE", + "link": 1087 + } + ], + "outputs": [ + { + "name": "PARAMETER", + "type": "FLOAT", + "links": [ + 1089 + ] + } + ], + "properties": { + "cnr_id": "comfyui_ryanontheinside", + "ver": "5907a3591a2feb5531d533194b6c26ead84ce835", + "widget_ue_connectable": {}, + "Node name for S&R": "FeatureToFlexFloatParam" + }, + "widgets_values": [ + false, + 0.6000000000000001, + 3.3600000000000008 + ] + }, + { + "id": 566, + "type": "PreviewImage", + "pos": [ + 1983.6669921875, + 2444.78759765625 + ], + "size": [ + 210, + 246 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 1092 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.30", + "widget_ue_connectable": {}, + "Node name for S&R": "PreviewImage" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 1014, + 518, + 0, + 516, + 0, + "IMAGE" + ], + [ + 1019, + 521, + 0, + 524, + 0, + "IMAGE" + ], + [ + 1078, + 516, + 1, + 556, + 0, + "POSE_KEYPOINT" + ], + [ + 1080, + 556, + 0, + 557, + 0, + "POSE_KEYPOINT" + ], + [ + 1082, + 557, + 0, + 558, + 0, + "POSE_KEYPOINT" + ], + [ + 1084, + 558, + 0, + 521, + 0, + "POSE_KEYPOINT" + ], + [ + 1085, + 560, + 0, + 561, + 0, + "POSE_KEYPOINT" + ], + [ + 1086, + 516, + 1, + 560, + 0, + "POSE_KEYPOINT" + ], + [ + 1087, + 562, + 0, + 563, + 0, + "FEATURE" + ], + [ + 1088, + 562, + 0, + 564, + 0, + "FEATURE" + ], + [ + 1089, + 563, + 0, + 560, + 1, + "FLOAT" + ], + [ + 1090, + 564, + 0, + 561, + 1, + "FLOAT" + ], + [ + 1092, + 565, + 0, + 566, + 0, + "IMAGE" + ], + [ + 1093, + 561, + 0, + 565, + 0, + "POSE_KEYPOINT" + ] + ], + "groups": [ + { + "id": 1, + "title": "Single image", + "bounding": [ + -1244.7318115234375, + 998.99267578125, + 3810.3720703125, + 943.616943359375 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 2, + "title": "Multi image", + "bounding": [ + -1244.4027099609375, + 2165.394775390625, + 3705.696533203125, + 1379.8316650390625 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 0.25937424601001946, + "offset": [ + 2334.3305516827477, + -871.2916448549256 + ] + }, + "node_versions": { + "comfy-core": "0.3.15", + "ComfyUI-VideoHelperSuite": "8629188458dc6cb832f871ece3bd273507e8a766", + "ComfyUI-Custom-Scripts": "a53ef9b617ed1331640d7a2cd97644995908dc00", + "ComfyUI_Comfyroll_CustomNodes": "d78b780ae43fcf8c6b7c6505e6ffb4584281ceca", + "ComfyUI-KJNodes": "86b5453a5ca9ecb883eedc9d0a96bf942b9ca73e", + "comfyui_ttp_toolset": "6dd3f3566ce0925b71e9cdb54243119685ccbc10", + "was-node-suite-comfyui": "3ed45af34a14551dc28cb3127235cc7197d4633f", + "ComfyUI-Frame-Interpolation": "c336f7184cb1ac1243381e725fea1ad2c0a10c09", + "comfyui-easy-use": "123917da9adec0d2b0b5f817deefb9ac3ed464f1", + "ComfyLiterals": "bdddb08ca82d90d75d97b1d437a652e0284a32ac", + "ComfyUI-Image-Filters": "0ff33fe29f7be072ad5d2cd89efa18fed82957fe", + "ComfyUI_essentials": "33ff89fd354d8ec3ab6affb605a79a931b445d99", + "ComfyUI-Impact-Pack": "1ae7cae2df8cca06027edfa3a24512671239d6c4", + "comfyui-detail-daemon": "90e703d3d3f979438471c646a5d030840a2caac3", + "comfyui-reactor-node": "a43075813dbd17de9c51c9fc7ea768eaf1681d89", + "ComfyUI-HunyuanVideoMultiLora": "7e3e3444d4e34557a24b3e0c502c94fe556237e4", + "ComfyUI-AutomaticCFG": "2e395317b65c05a97a0ef566c4a8c7969305dafa", + "ComfyUI_UltimateSDUpscale": "ff3fdfeee03de46d4462211cffd165d27155e858", + "ComfyUI-DepthAnythingV2": "003d7b44bafd3a8a4c3693a9ca3ddcd72f4883ab", + "comfyui-various": "36454f91606bbff4fc36d90234981ca4a47e2695", + "ComfyUI_SLK_joy_caption_two": "667751cab945bd8e9fb0be4d557d47e36821350a", + "comfyui-ollama": "cfa314d36efbb2344ba22a76256200016ad9570a", + "ComfyUI-Florence2": "90b012e922f8bb0482bcd2ae24cdc191ec12a11f", + "save-image-extended-comfyui": "5f104fddfc8281d9fd85d03e63c5f70454db6701" + }, + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true, + "ue_links": [], + "links_added_by_ue": [], + "frontendVersion": "1.17.11" + }, + "version": 0.4 +} \ No newline at end of file