mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-14 02:17:13 +08:00
Merge remote-tracking branch 'upstream/master' into sam3d_body
This commit is contained in:
commit
7a479beede
@ -325,21 +325,25 @@ class VideoFromFile(VideoInput):
|
|||||||
checked_alpha = True
|
checked_alpha = True
|
||||||
|
|
||||||
# Fix non-deterministic video decode when the video width is not a multiple of 32
|
# Fix non-deterministic video decode when the video width is not a multiple of 32
|
||||||
# For non-yuvj pixel formats (all H.264/H.265 video)
|
# For non-yuvj pixel formats: most H.264/H.265 video and static images (e.g. lossy WebP via LoadImage)
|
||||||
|
# Pad both axes to a multiple of 32 and smear the border so the alignment padding never bleeds into the cropped edges
|
||||||
if image_format in ('gbrpf32le', 'gbrapf32le') and frame.width % 32 != 0:
|
if image_format in ('gbrpf32le', 'gbrapf32le') and frame.width % 32 != 0:
|
||||||
if align_graph is None:
|
if align_graph is None:
|
||||||
pad_w = ((frame.width + 31) // 32) * 32
|
pad_w = ((frame.width + 31) // 32) * 32
|
||||||
|
pad_h = ((frame.height + 31) // 32) * 32
|
||||||
g = av.filter.Graph()
|
g = av.filter.Graph()
|
||||||
g_src = g.add_buffer(width=frame.width, height=frame.height,
|
g_src = g.add_buffer(width=frame.width, height=frame.height,
|
||||||
format=frame.format.name, time_base=video_stream.time_base)
|
format=frame.format.name, time_base=video_stream.time_base)
|
||||||
g_pad = g.add('pad', f'{pad_w}:{frame.height}:0:0')
|
g_pad = g.add('pad', f'{pad_w}:{pad_h}:0:0')
|
||||||
|
g_fill = g.add('fillborders', f'left=0:right={pad_w - frame.width}:top=0:bottom={pad_h - frame.height}:mode=smear')
|
||||||
g_sink = g.add('buffersink')
|
g_sink = g.add('buffersink')
|
||||||
g_src.link_to(g_pad)
|
g_src.link_to(g_pad)
|
||||||
g_pad.link_to(g_sink)
|
g_pad.link_to(g_fill)
|
||||||
|
g_fill.link_to(g_sink)
|
||||||
g.configure()
|
g.configure()
|
||||||
align_graph = (g, g_src, g_sink)
|
align_graph = (g, g_src, g_sink)
|
||||||
align_graph[1].push(frame)
|
align_graph[1].push(frame)
|
||||||
img = np.ascontiguousarray(align_graph[2].pull().to_ndarray(format=image_format)[:, :frame.width])
|
img = np.ascontiguousarray(align_graph[2].pull().to_ndarray(format=image_format)[:frame.height, :frame.width])
|
||||||
else:
|
else:
|
||||||
img = frame.to_ndarray(format=image_format)
|
img = frame.to_ndarray(format=image_format)
|
||||||
if frame.rotation != 0:
|
if frame.rotation != 0:
|
||||||
|
|||||||
@ -100,8 +100,7 @@ class SoniloTextToMusic(IO.ComfyNode):
|
|||||||
node_id="SoniloTextToMusic",
|
node_id="SoniloTextToMusic",
|
||||||
display_name="Sonilo Text to Music",
|
display_name="Sonilo Text to Music",
|
||||||
category="partner/audio/Sonilo",
|
category="partner/audio/Sonilo",
|
||||||
description="Generate music from a text prompt using Sonilo's AI model. "
|
description="Generate music from a text prompt using Sonilo's AI model.",
|
||||||
"Leave duration at 0 to let the model infer it from the prompt.",
|
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.String.Input(
|
IO.String.Input(
|
||||||
"prompt",
|
"prompt",
|
||||||
@ -135,13 +134,7 @@ class SoniloTextToMusic(IO.ComfyNode):
|
|||||||
is_api_node=True,
|
is_api_node=True,
|
||||||
price_badge=IO.PriceBadge(
|
price_badge=IO.PriceBadge(
|
||||||
depends_on=IO.PriceBadgeDepends(widgets=["duration"]),
|
depends_on=IO.PriceBadgeDepends(widgets=["duration"]),
|
||||||
expr="""
|
expr='{"type":"usd","usd": 0.0025 * widgets.duration}',
|
||||||
(
|
|
||||||
widgets.duration > 0
|
|
||||||
? {"type":"usd","usd": 0.005 * widgets.duration}
|
|
||||||
: {"type":"usd","usd": 0.005, "format":{"suffix":"/second"}}
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class RTDETR_detect(io.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="RTDETR_detect",
|
node_id="RTDETR_detect",
|
||||||
display_name="RT-DETR Detect",
|
display_name="Run Real-Time Detection (RT-DETR)",
|
||||||
category="image/detection",
|
category="image/detection",
|
||||||
search_aliases=["bbox", "bounding box", "object detection", "coco"],
|
search_aliases=["bbox", "bounding box", "object detection", "coco"],
|
||||||
inputs=[
|
inputs=[
|
||||||
|
|||||||
@ -264,7 +264,7 @@ class SAM3_VideoTrack(io.ComfyNode):
|
|||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
return io.Schema(
|
return io.Schema(
|
||||||
node_id="SAM3_VideoTrack",
|
node_id="SAM3_VideoTrack",
|
||||||
display_name="SAM3 Video Track",
|
display_name="Run SAM3 Video Track",
|
||||||
category="image/detection",
|
category="image/detection",
|
||||||
search_aliases=["sam3", "video", "track", "propagate"],
|
search_aliases=["sam3", "video", "track", "propagate"],
|
||||||
inputs=[
|
inputs=[
|
||||||
|
|||||||
@ -2,10 +2,11 @@ import torch
|
|||||||
import comfy.utils
|
import comfy.utils
|
||||||
import comfy.model_management
|
import comfy.model_management
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import math
|
||||||
|
import colorsys
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from comfy_api.latest import ComfyExtension, io
|
from comfy_api.latest import ComfyExtension, io
|
||||||
from comfy_extras.pose.keypoint_draw import KeypointDraw
|
|
||||||
from comfy_extras.nodes_lotus import LotusConditioning
|
from comfy_extras.nodes_lotus import LotusConditioning
|
||||||
|
|
||||||
|
|
||||||
@ -72,6 +73,299 @@ def _to_openpose_frames(all_keypoints, all_scores, height, width):
|
|||||||
return frames
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
class KeypointDraw:
|
||||||
|
"""
|
||||||
|
Pose keypoint drawing class that supports both numpy and cv2 backends.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
try:
|
||||||
|
import cv2
|
||||||
|
self.draw = cv2
|
||||||
|
except ImportError:
|
||||||
|
self.draw = self
|
||||||
|
|
||||||
|
# Hand connections (same for both hands)
|
||||||
|
self.hand_edges = [
|
||||||
|
[0, 1], [1, 2], [2, 3], [3, 4], # thumb
|
||||||
|
[0, 5], [5, 6], [6, 7], [7, 8], # index
|
||||||
|
[0, 9], [9, 10], [10, 11], [11, 12], # middle
|
||||||
|
[0, 13], [13, 14], [14, 15], [15, 16], # ring
|
||||||
|
[0, 17], [17, 18], [18, 19], [19, 20], # pinky
|
||||||
|
]
|
||||||
|
|
||||||
|
# Body connections - matching DWPose limbSeq (1-indexed, converted to 0-indexed)
|
||||||
|
self.body_limbSeq = [
|
||||||
|
[2, 3], [2, 6], [3, 4], [4, 5], [6, 7], [7, 8], [2, 9], [9, 10],
|
||||||
|
[10, 11], [2, 12], [12, 13], [13, 14]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Head connections (1-indexed, converted to 0-indexed)
|
||||||
|
self.head_edges = [
|
||||||
|
[2, 1], [1, 15], [15, 17], [1, 16], [16, 18]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Colors matching DWPose
|
||||||
|
self.colors = [
|
||||||
|
[255, 0, 0], [255, 85, 0], [255, 170, 0], [255, 255, 0], [170, 255, 0],
|
||||||
|
[85, 255, 0], [0, 255, 0], [0, 255, 85], [0, 255, 170], [0, 255, 255],
|
||||||
|
[0, 170, 255], [0, 85, 255], [0, 0, 255], [85, 0, 255],
|
||||||
|
[170, 0, 255], [255, 0, 255], [255, 0, 170], [255, 0, 85]
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def circle(canvas_np, center, radius, color, **kwargs):
|
||||||
|
"""Draw a filled circle using NumPy vectorized operations."""
|
||||||
|
cx, cy = center
|
||||||
|
h, w = canvas_np.shape[:2]
|
||||||
|
|
||||||
|
radius_int = int(np.ceil(radius))
|
||||||
|
|
||||||
|
y_min, y_max = max(0, cy - radius_int), min(h, cy + radius_int + 1)
|
||||||
|
x_min, x_max = max(0, cx - radius_int), min(w, cx + radius_int + 1)
|
||||||
|
|
||||||
|
if y_max <= y_min or x_max <= x_min:
|
||||||
|
return
|
||||||
|
|
||||||
|
y, x = np.ogrid[y_min:y_max, x_min:x_max]
|
||||||
|
mask = (x - cx)**2 + (y - cy)**2 <= radius**2
|
||||||
|
canvas_np[y_min:y_max, x_min:x_max][mask] = color
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def line(canvas_np, pt1, pt2, color, thickness=1, **kwargs):
|
||||||
|
"""Draw line using Bresenham's algorithm with NumPy operations."""
|
||||||
|
x0, y0, x1, y1 = *pt1, *pt2
|
||||||
|
h, w = canvas_np.shape[:2]
|
||||||
|
dx, dy = abs(x1 - x0), abs(y1 - y0)
|
||||||
|
sx, sy = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1)
|
||||||
|
err, x, y, line_points = dx - dy, x0, y0, []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line_points.append((x, y))
|
||||||
|
if x == x1 and y == y1:
|
||||||
|
break
|
||||||
|
e2 = 2 * err
|
||||||
|
if e2 > -dy:
|
||||||
|
err, x = err - dy, x + sx
|
||||||
|
if e2 < dx:
|
||||||
|
err, y = err + dx, y + sy
|
||||||
|
|
||||||
|
if thickness > 1:
|
||||||
|
radius, radius_int = (thickness / 2.0) + 0.5, int(np.ceil((thickness / 2.0) + 0.5))
|
||||||
|
for px, py in line_points:
|
||||||
|
y_min, y_max, x_min, x_max = max(0, py - radius_int), min(h, py + radius_int + 1), max(0, px - radius_int), min(w, px + radius_int + 1)
|
||||||
|
if y_max > y_min and x_max > x_min:
|
||||||
|
yy, xx = np.ogrid[y_min:y_max, x_min:x_max]
|
||||||
|
canvas_np[y_min:y_max, x_min:x_max][(xx - px)**2 + (yy - py)**2 <= radius**2] = color
|
||||||
|
else:
|
||||||
|
line_points = np.array(line_points)
|
||||||
|
valid = (line_points[:, 1] >= 0) & (line_points[:, 1] < h) & (line_points[:, 0] >= 0) & (line_points[:, 0] < w)
|
||||||
|
if (valid_points := line_points[valid]).size:
|
||||||
|
canvas_np[valid_points[:, 1], valid_points[:, 0]] = color
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fillConvexPoly(canvas_np, pts, color, **kwargs):
|
||||||
|
"""Fill polygon using vectorized scanline algorithm."""
|
||||||
|
if len(pts) < 3:
|
||||||
|
return
|
||||||
|
pts = np.array(pts, dtype=np.int32)
|
||||||
|
h, w = canvas_np.shape[:2]
|
||||||
|
y_min, y_max, x_min, x_max = max(0, pts[:, 1].min()), min(h, pts[:, 1].max() + 1), max(0, pts[:, 0].min()), min(w, pts[:, 0].max() + 1)
|
||||||
|
if y_max <= y_min or x_max <= x_min:
|
||||||
|
return
|
||||||
|
yy, xx = np.mgrid[y_min:y_max, x_min:x_max]
|
||||||
|
mask = np.zeros((y_max - y_min, x_max - x_min), dtype=bool)
|
||||||
|
|
||||||
|
for i in range(len(pts)):
|
||||||
|
p1, p2 = pts[i], pts[(i + 1) % len(pts)]
|
||||||
|
y1, y2 = p1[1], p2[1]
|
||||||
|
if y1 == y2:
|
||||||
|
continue
|
||||||
|
if y1 > y2:
|
||||||
|
p1, p2, y1, y2 = p2, p1, p2[1], p1[1]
|
||||||
|
if not (edge_mask := (yy >= y1) & (yy < y2)).any():
|
||||||
|
continue
|
||||||
|
mask ^= edge_mask & (xx >= p1[0] + (yy - y1) * (p2[0] - p1[0]) / (y2 - y1))
|
||||||
|
|
||||||
|
canvas_np[y_min:y_max, x_min:x_max][mask] = color
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ellipse2Poly(center, axes, angle, arc_start, arc_end, delta=1, **kwargs):
|
||||||
|
"""Python implementation of cv2.ellipse2Poly."""
|
||||||
|
axes = (axes[0] + 0.5, axes[1] + 0.5) # to better match cv2 output
|
||||||
|
angle = angle % 360
|
||||||
|
if arc_start > arc_end:
|
||||||
|
arc_start, arc_end = arc_end, arc_start
|
||||||
|
while arc_start < 0:
|
||||||
|
arc_start, arc_end = arc_start + 360, arc_end + 360
|
||||||
|
while arc_end > 360:
|
||||||
|
arc_end, arc_start = arc_end - 360, arc_start - 360
|
||||||
|
if arc_end - arc_start > 360:
|
||||||
|
arc_start, arc_end = 0, 360
|
||||||
|
|
||||||
|
angle_rad = math.radians(angle)
|
||||||
|
alpha, beta = math.cos(angle_rad), math.sin(angle_rad)
|
||||||
|
pts = []
|
||||||
|
for i in range(arc_start, arc_end + delta, delta):
|
||||||
|
theta_rad = math.radians(min(i, arc_end))
|
||||||
|
x, y = axes[0] * math.cos(theta_rad), axes[1] * math.sin(theta_rad)
|
||||||
|
pts.append([int(round(center[0] + x * alpha - y * beta)), int(round(center[1] + x * beta + y * alpha))])
|
||||||
|
|
||||||
|
unique_pts, prev_pt = [], (float('inf'), float('inf'))
|
||||||
|
for pt in pts:
|
||||||
|
if (pt_tuple := tuple(pt)) != prev_pt:
|
||||||
|
unique_pts.append(pt)
|
||||||
|
prev_pt = pt_tuple
|
||||||
|
|
||||||
|
return unique_pts if len(unique_pts) > 1 else [[center[0], center[1]], [center[0], center[1]]]
|
||||||
|
|
||||||
|
def draw_wholebody_keypoints(self, canvas, keypoints, scores=None, threshold=0.3,
|
||||||
|
draw_body=True, draw_head=True, draw_feet=True, draw_face=True, draw_hands=True, stick_width=4, face_point_size=3):
|
||||||
|
"""
|
||||||
|
Draw wholebody keypoints (134 keypoints after processing) in DWPose style.
|
||||||
|
|
||||||
|
Expected keypoint format (after neck insertion and remapping):
|
||||||
|
- Body: 0-17 (18 keypoints in OpenPose format, neck at index 1)
|
||||||
|
- Foot: 18-23 (6 keypoints)
|
||||||
|
- Face: 24-91 (68 landmarks)
|
||||||
|
- Right hand: 92-112 (21 keypoints)
|
||||||
|
- Left hand: 113-133 (21 keypoints)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
canvas: The canvas to draw on (numpy array)
|
||||||
|
keypoints: Array of keypoint coordinates
|
||||||
|
scores: Optional confidence scores for each keypoint
|
||||||
|
threshold: Minimum confidence threshold for drawing keypoints
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
canvas: The canvas with keypoints drawn
|
||||||
|
"""
|
||||||
|
H, W, C = canvas.shape
|
||||||
|
|
||||||
|
# Draw body limbs & head connections
|
||||||
|
if (draw_body or draw_head) and len(keypoints) >= 18:
|
||||||
|
colorIndexOffset = 0
|
||||||
|
edges = []
|
||||||
|
if draw_body:
|
||||||
|
edges += self.body_limbSeq
|
||||||
|
else:
|
||||||
|
colorIndexOffset += len(self.body_limbSeq)
|
||||||
|
if draw_head:
|
||||||
|
edges += self.head_edges
|
||||||
|
for i, limb in enumerate(edges):
|
||||||
|
# Convert from 1-indexed to 0-indexed
|
||||||
|
idx1, idx2 = limb[0] - 1, limb[1] - 1
|
||||||
|
|
||||||
|
if idx1 >= 18 or idx2 >= 18:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if scores is not None:
|
||||||
|
if scores[idx1] < threshold or scores[idx2] < threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
Y = [keypoints[idx1][0], keypoints[idx2][0]]
|
||||||
|
X = [keypoints[idx1][1], keypoints[idx2][1]]
|
||||||
|
mX, mY = (X[0] + X[1]) / 2, (Y[0] + Y[1]) / 2
|
||||||
|
length = math.sqrt((X[0] - X[1]) ** 2 + (Y[0] - Y[1]) ** 2)
|
||||||
|
|
||||||
|
if length < 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
angle = math.degrees(math.atan2(X[0] - X[1], Y[0] - Y[1]))
|
||||||
|
|
||||||
|
polygon = self.draw.ellipse2Poly((int(mY), int(mX)), (int(length / 2), stick_width), int(angle), 0, 360, 1)
|
||||||
|
|
||||||
|
self.draw.fillConvexPoly(canvas, polygon, self.colors[(i + colorIndexOffset) % len(self.colors)])
|
||||||
|
|
||||||
|
# Draw body & head keypoints
|
||||||
|
if (draw_body or draw_head) and len(keypoints) >= 18:
|
||||||
|
head_keypoints = {0, 14, 15, 16, 17} # nose, eyes, ears
|
||||||
|
neck_point = 1
|
||||||
|
for i in range(18):
|
||||||
|
if not draw_head and i in head_keypoints:
|
||||||
|
continue
|
||||||
|
if not draw_body and i not in head_keypoints and i != neck_point:
|
||||||
|
continue
|
||||||
|
if scores is not None and scores[i] < threshold:
|
||||||
|
continue
|
||||||
|
x, y = int(keypoints[i][0]), int(keypoints[i][1])
|
||||||
|
if 0 <= x < W and 0 <= y < H:
|
||||||
|
self.draw.circle(canvas, (x, y), 4, self.colors[i % len(self.colors)], thickness=-1)
|
||||||
|
|
||||||
|
# Draw foot keypoints (18-23, 6 keypoints)
|
||||||
|
if draw_feet and len(keypoints) >= 24:
|
||||||
|
for i in range(18, 24):
|
||||||
|
if scores is not None and scores[i] < threshold:
|
||||||
|
continue
|
||||||
|
x, y = int(keypoints[i][0]), int(keypoints[i][1])
|
||||||
|
if 0 <= x < W and 0 <= y < H:
|
||||||
|
self.draw.circle(canvas, (x, y), 4, self.colors[i % len(self.colors)], thickness=-1)
|
||||||
|
|
||||||
|
# Draw right hand (92-112)
|
||||||
|
if draw_hands and len(keypoints) >= 113:
|
||||||
|
eps = 0.01
|
||||||
|
for ie, edge in enumerate(self.hand_edges):
|
||||||
|
idx1, idx2 = 92 + edge[0], 92 + edge[1]
|
||||||
|
if scores is not None:
|
||||||
|
if scores[idx1] < threshold or scores[idx2] < threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
x1, y1 = int(keypoints[idx1][0]), int(keypoints[idx1][1])
|
||||||
|
x2, y2 = int(keypoints[idx2][0]), int(keypoints[idx2][1])
|
||||||
|
|
||||||
|
if x1 > eps and y1 > eps and x2 > eps and y2 > eps:
|
||||||
|
if 0 <= x1 < W and 0 <= y1 < H and 0 <= x2 < W and 0 <= y2 < H:
|
||||||
|
# HSV to RGB conversion for rainbow colors
|
||||||
|
r, g, b = colorsys.hsv_to_rgb(ie / float(len(self.hand_edges)), 1.0, 1.0)
|
||||||
|
color = (int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
self.draw.line(canvas, (x1, y1), (x2, y2), color, thickness=2)
|
||||||
|
|
||||||
|
# Draw right hand keypoints
|
||||||
|
for i in range(92, 113):
|
||||||
|
if scores is not None and scores[i] < threshold:
|
||||||
|
continue
|
||||||
|
x, y = int(keypoints[i][0]), int(keypoints[i][1])
|
||||||
|
if x > eps and y > eps and 0 <= x < W and 0 <= y < H:
|
||||||
|
self.draw.circle(canvas, (x, y), 4, (0, 0, 255), thickness=-1)
|
||||||
|
|
||||||
|
# Draw left hand (113-133)
|
||||||
|
if draw_hands and len(keypoints) >= 134:
|
||||||
|
eps = 0.01
|
||||||
|
for ie, edge in enumerate(self.hand_edges):
|
||||||
|
idx1, idx2 = 113 + edge[0], 113 + edge[1]
|
||||||
|
if scores is not None:
|
||||||
|
if scores[idx1] < threshold or scores[idx2] < threshold:
|
||||||
|
continue
|
||||||
|
|
||||||
|
x1, y1 = int(keypoints[idx1][0]), int(keypoints[idx1][1])
|
||||||
|
x2, y2 = int(keypoints[idx2][0]), int(keypoints[idx2][1])
|
||||||
|
|
||||||
|
if x1 > eps and y1 > eps and x2 > eps and y2 > eps:
|
||||||
|
if 0 <= x1 < W and 0 <= y1 < H and 0 <= x2 < W and 0 <= y2 < H:
|
||||||
|
# HSV to RGB conversion for rainbow colors
|
||||||
|
r, g, b = colorsys.hsv_to_rgb(ie / float(len(self.hand_edges)), 1.0, 1.0)
|
||||||
|
color = (int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
self.draw.line(canvas, (x1, y1), (x2, y2), color, thickness=2)
|
||||||
|
|
||||||
|
# Draw left hand keypoints
|
||||||
|
for i in range(113, 134):
|
||||||
|
if scores is not None and i < len(scores) and scores[i] < threshold:
|
||||||
|
continue
|
||||||
|
x, y = int(keypoints[i][0]), int(keypoints[i][1])
|
||||||
|
if x > eps and y > eps and 0 <= x < W and 0 <= y < H:
|
||||||
|
self.draw.circle(canvas, (x, y), 4, (0, 0, 255), thickness=-1)
|
||||||
|
|
||||||
|
# Draw face keypoints (24-91) - white dots only, no lines
|
||||||
|
if draw_face and len(keypoints) >= 92:
|
||||||
|
eps = 0.01
|
||||||
|
for i in range(24, 92):
|
||||||
|
if scores is not None and scores[i] < threshold:
|
||||||
|
continue
|
||||||
|
x, y = int(keypoints[i][0]), int(keypoints[i][1])
|
||||||
|
if x > eps and y > eps and 0 <= x < W and 0 <= y < H:
|
||||||
|
self.draw.circle(canvas, (x, y), face_point_size, (255, 255, 255), thickness=-1)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
|
||||||
class SDPoseDrawKeypoints(io.ComfyNode):
|
class SDPoseDrawKeypoints(io.ComfyNode):
|
||||||
@classmethod
|
@classmethod
|
||||||
def define_schema(cls):
|
def define_schema(cls):
|
||||||
@ -89,6 +383,7 @@ class SDPoseDrawKeypoints(io.ComfyNode):
|
|||||||
io.Int.Input("stick_width", default=4, min=1, max=10, step=1),
|
io.Int.Input("stick_width", default=4, min=1, max=10, step=1),
|
||||||
io.Int.Input("face_point_size", default=3, min=1, max=10, step=1),
|
io.Int.Input("face_point_size", default=3, min=1, max=10, step=1),
|
||||||
io.Float.Input("score_threshold", default=0.3, min=0.0, max=1.0, step=0.01),
|
io.Float.Input("score_threshold", default=0.3, min=0.0, max=1.0, step=0.01),
|
||||||
|
io.Boolean.Input("draw_head", default=True),
|
||||||
],
|
],
|
||||||
outputs=[
|
outputs=[
|
||||||
io.Image.Output(),
|
io.Image.Output(),
|
||||||
@ -96,7 +391,7 @@ class SDPoseDrawKeypoints(io.ComfyNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, keypoints, draw_body, draw_hands, draw_face, draw_feet, stick_width, face_point_size, score_threshold) -> io.NodeOutput:
|
def execute(cls, keypoints, draw_body, draw_hands, draw_face, draw_feet, stick_width, face_point_size, score_threshold, draw_head) -> io.NodeOutput:
|
||||||
if not keypoints:
|
if not keypoints:
|
||||||
return io.NodeOutput(torch.zeros((1, 64, 64, 3), dtype=torch.float32))
|
return io.NodeOutput(torch.zeros((1, 64, 64, 3), dtype=torch.float32))
|
||||||
height = keypoints[0]["canvas_height"]
|
height = keypoints[0]["canvas_height"]
|
||||||
@ -129,7 +424,7 @@ class SDPoseDrawKeypoints(io.ComfyNode):
|
|||||||
canvas = drawer.draw_wholebody_keypoints(
|
canvas = drawer.draw_wholebody_keypoints(
|
||||||
canvas, kp, sc,
|
canvas, kp, sc,
|
||||||
threshold=score_threshold,
|
threshold=score_threshold,
|
||||||
draw_body=draw_body, draw_feet=draw_feet,
|
draw_body=draw_body, draw_head=draw_head, draw_feet=draw_feet,
|
||||||
draw_face=draw_face, draw_hands=draw_hands,
|
draw_face=draw_face, draw_hands=draw_hands,
|
||||||
stick_width=stick_width, face_point_size=face_point_size,
|
stick_width=stick_width, face_point_size=face_point_size,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
# This file is automatically generated by the build process when version is
|
# This file is automatically generated by the build process when version is
|
||||||
# updated in pyproject.toml.
|
# updated in pyproject.toml.
|
||||||
__version__ = "0.24.0"
|
__version__ = "0.25.0"
|
||||||
|
|||||||
49
main.py
49
main.py
@ -55,7 +55,11 @@ if __name__ == "__main__" and args.debug_hang:
|
|||||||
import comfy_aimdo.control
|
import comfy_aimdo.control
|
||||||
|
|
||||||
if enables_dynamic_vram():
|
if enables_dynamic_vram():
|
||||||
comfy_aimdo.control.init(simple_vram_headroom=None if args.reserve_vram is None else int(args.reserve_vram * 1024 ** 3))
|
try:
|
||||||
|
comfy_aimdo.control.init(simple_vram_headroom=None if args.reserve_vram is None else int(args.reserve_vram * 1024 ** 3))
|
||||||
|
except TypeError:
|
||||||
|
# comfy-aimdo 0.4.9 protocol.
|
||||||
|
comfy_aimdo.control.init()
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
os.environ['MIMALLOC_PURGE_DELAY'] = '0'
|
os.environ['MIMALLOC_PURGE_DELAY'] = '0'
|
||||||
@ -123,6 +127,10 @@ def apply_custom_paths():
|
|||||||
for config_path in itertools.chain(*args.extra_model_paths_config):
|
for config_path in itertools.chain(*args.extra_model_paths_config):
|
||||||
utils.extra_config.load_extra_path_config(config_path)
|
utils.extra_config.load_extra_path_config(config_path)
|
||||||
|
|
||||||
|
# --base-directory
|
||||||
|
if args.base_directory:
|
||||||
|
logging.info(f"Setting base directory to: {folder_paths.base_path}")
|
||||||
|
|
||||||
# --output-directory, --input-directory, --user-directory
|
# --output-directory, --input-directory, --user-directory
|
||||||
if args.output_directory:
|
if args.output_directory:
|
||||||
output_dir = os.path.abspath(args.output_directory)
|
output_dir = os.path.abspath(args.output_directory)
|
||||||
@ -231,23 +239,30 @@ import comfy.model_patcher
|
|||||||
if args.enable_dynamic_vram or (enables_dynamic_vram() and comfy.model_management.is_nvidia() and not comfy.model_management.is_wsl()):
|
if args.enable_dynamic_vram or (enables_dynamic_vram() and comfy.model_management.is_nvidia() and not comfy.model_management.is_wsl()):
|
||||||
if (not args.enable_dynamic_vram) and (comfy.model_management.torch_version_numeric < (2, 8)):
|
if (not args.enable_dynamic_vram) and (comfy.model_management.torch_version_numeric < (2, 8)):
|
||||||
logging.warning("Unsupported Pytorch detected. DynamicVRAM support requires Pytorch version 2.8 or later. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
logging.warning("Unsupported Pytorch detected. DynamicVRAM support requires Pytorch version 2.8 or later. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
||||||
elif comfy_aimdo.control.init_devices((d.index, int(args.vram_headroom * 1024 ** 3)) for d in comfy.model_management.get_all_torch_devices()):
|
|
||||||
if args.verbose == 'DEBUG':
|
|
||||||
comfy_aimdo.control.set_log_debug()
|
|
||||||
elif args.verbose == 'CRITICAL':
|
|
||||||
comfy_aimdo.control.set_log_critical()
|
|
||||||
elif args.verbose == 'ERROR':
|
|
||||||
comfy_aimdo.control.set_log_error()
|
|
||||||
elif args.verbose == 'WARNING':
|
|
||||||
comfy_aimdo.control.set_log_warning()
|
|
||||||
else: #INFO
|
|
||||||
comfy_aimdo.control.set_log_info()
|
|
||||||
|
|
||||||
comfy.model_patcher.CoreModelPatcher = comfy.model_patcher.ModelPatcherDynamic
|
|
||||||
comfy.memory_management.aimdo_enabled = True
|
|
||||||
logging.info("DynamicVRAM support detected and enabled")
|
|
||||||
else:
|
else:
|
||||||
logging.warning("No working comfy-aimdo install detected. DynamicVRAM support disabled. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
try:
|
||||||
|
aimdo_initialized = comfy_aimdo.control.init_devices((d.index, int(args.vram_headroom * 1024 ** 3)) for d in comfy.model_management.get_all_torch_devices())
|
||||||
|
except TypeError:
|
||||||
|
# comfy-aimdo 0.4.9 protocol.
|
||||||
|
aimdo_initialized = comfy_aimdo.control.init_devices(d.index for d in comfy.model_management.get_all_torch_devices())
|
||||||
|
|
||||||
|
if aimdo_initialized:
|
||||||
|
if args.verbose == 'DEBUG':
|
||||||
|
comfy_aimdo.control.set_log_debug()
|
||||||
|
elif args.verbose == 'CRITICAL':
|
||||||
|
comfy_aimdo.control.set_log_critical()
|
||||||
|
elif args.verbose == 'ERROR':
|
||||||
|
comfy_aimdo.control.set_log_error()
|
||||||
|
elif args.verbose == 'WARNING':
|
||||||
|
comfy_aimdo.control.set_log_warning()
|
||||||
|
else: #INFO
|
||||||
|
comfy_aimdo.control.set_log_info()
|
||||||
|
|
||||||
|
comfy.model_patcher.CoreModelPatcher = comfy.model_patcher.ModelPatcherDynamic
|
||||||
|
comfy.memory_management.aimdo_enabled = True
|
||||||
|
logging.info("DynamicVRAM support detected and enabled")
|
||||||
|
else:
|
||||||
|
logging.warning("No working comfy-aimdo install detected. DynamicVRAM support disabled. Falling back to legacy ModelPatcher. VRAM estimates may be unreliable especially on Windows")
|
||||||
|
|
||||||
|
|
||||||
def cuda_malloc_warning():
|
def cuda_malloc_warning():
|
||||||
|
|||||||
20
openapi.yaml
20
openapi.yaml
@ -896,11 +896,6 @@ components:
|
|||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
description: The workflow graph to execute
|
description: The workflow graph to execute
|
||||||
type: object
|
type: object
|
||||||
prompt_id:
|
|
||||||
description: Optional client-supplied job id. Must be a UUID in canonical lowercase hyphenated form; it is echoed back in the response. Omitted or null means the server generates one.
|
|
||||||
format: uuid
|
|
||||||
nullable: true
|
|
||||||
type: string
|
|
||||||
workflow_id:
|
workflow_id:
|
||||||
description: UUID identifying the cloud workflow entity to associate with this job
|
description: UUID identifying the cloud workflow entity to associate with this job
|
||||||
type: string
|
type: string
|
||||||
@ -1800,7 +1795,9 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ErrorResponse'
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
description: Invalid request (no fields provided)
|
description: |
|
||||||
|
Invalid request — no fields provided, or `preview_id` is the zero UUID
|
||||||
|
(`INVALID_PREVIEW_ID`).
|
||||||
"401":
|
"401":
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@ -1812,7 +1809,10 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ErrorResponse'
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
description: Asset not found
|
description: |
|
||||||
|
Asset not found — returned both when the asset being updated does
|
||||||
|
not exist and when `preview_id` does not reference an asset
|
||||||
|
accessible to the caller.
|
||||||
"500":
|
"500":
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
@ -3050,6 +3050,12 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PromptErrorResponse'
|
$ref: '#/components/schemas/PromptErrorResponse'
|
||||||
description: Payment required - Insufficient credits
|
description: Payment required - Insufficient credits
|
||||||
|
"413":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PromptErrorResponse'
|
||||||
|
description: Workflow JSON too large
|
||||||
"429":
|
"429":
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ComfyUI"
|
name = "ComfyUI"
|
||||||
version = "0.24.0"
|
version = "0.25.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user