Merge pull request #10 from ryanontheinside/feat/appendage-editor

Procedure poses editing is applicable with the appendage editor node.
This commit is contained in:
mhstar
2025-06-04 17:30:51 -05:00
committed by GitHub
3 changed files with 1221 additions and 0 deletions
+3
View File
@@ -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"]
+325
View File
@@ -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
+893
View File
@@ -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
}