Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
077af5ce5c | ||
|
|
a00a4f1186 | ||
|
|
4888e4886e | ||
|
|
6b5c1bb1b1 | ||
|
|
2685495c65 |
+1
-2
@@ -3,5 +3,4 @@ from comfy_env import register_nodes
|
||||
NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = register_nodes()
|
||||
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[node_reqs]
|
||||
[node_packs]
|
||||
ComfyUI-VideoHelperSuite = "Kosinkadink/ComfyUI-VideoHelperSuite"
|
||||
ComfyUI-Multiband = "PozzettiAndrea/ComfyUI-Multiband"
|
||||
+9
-18
@@ -1,18 +1,9 @@
|
||||
[test]
|
||||
levels = ["syntax", "install", "registration", "instantiation", "execution"]
|
||||
|
||||
[test.workflows]
|
||||
cpu = ["!video_point_prompt"]
|
||||
gpu = "all"
|
||||
|
||||
[test.platforms]
|
||||
linux = true
|
||||
macos = true
|
||||
windows = true
|
||||
windows_portable = true
|
||||
linux_gpu = true
|
||||
windows_gpu = true
|
||||
windows_portable_gpu = true
|
||||
macos_desktop = true
|
||||
windows_desktop = true
|
||||
windows_desktop_gpu = true
|
||||
[test]
|
||||
levels = ["syntax", "javascript", "coverage", "install", "registration", "instantiation", "execution"]
|
||||
|
||||
[test.workflows]
|
||||
cpu = ["!video_point_prompt"]
|
||||
cuda = "all"
|
||||
|
||||
[test.platforms]
|
||||
platforms = ["linux", "macos", "windows", "windows_portable", "macos_desktop", "windows_desktop", "linux_cuda", "windows_cuda", "windows_portable_cuda", "windows_desktop_cuda"]
|
||||
|
||||
@@ -41,7 +41,7 @@ function hideWidgetForGood(node, widget, suffix = '') {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SAM3.SimpleBBoxCollector",
|
||||
name: "sam3.simplebboxcollector",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SAM3BBoxCollector") {
|
||||
@@ -32,38 +32,31 @@ function hideWidgetForGood(node, widget, suffix = '') {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSpinnerCSS() {
|
||||
if (document.getElementById("sam3-spinner-css")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "sam3-spinner-css";
|
||||
style.textContent = `
|
||||
@keyframes sam3spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.sam3-spinner {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
border: 2px solid rgba(136, 204, 255, 0.2);
|
||||
border-top-color: #8cf;
|
||||
border-radius: 50%;
|
||||
animation: sam3spin 0.65s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
// Spinner styling is inlined on the element and animated via the Web
|
||||
// Animations API rather than an injected <style>. A stylesheet in
|
||||
// document.head is shared-realm state every other extension inherits and
|
||||
// nobody can clean up; this keeps the whole spinner inside our own DOM.
|
||||
function makeSpinner() {
|
||||
const spinner = document.createElement("span");
|
||||
spinner.style.cssText =
|
||||
"display: inline-block; width: 10px; height: 10px;" +
|
||||
"border: 2px solid rgba(136, 204, 255, 0.2); border-top-color: #8cf;" +
|
||||
"border-radius: 50%; vertical-align: middle; margin-left: 4px;" +
|
||||
"flex-shrink: 0;";
|
||||
spinner.animate(
|
||||
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
||||
{ duration: 650, iterations: Infinity, easing: "linear" },
|
||||
);
|
||||
return spinner;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SAM3.InteractiveCollector",
|
||||
name: "sam3.interactivecollector",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "SAM3InteractiveCollector") return;
|
||||
|
||||
console.log("[SAM3] Registering SAM3InteractiveCollector node");
|
||||
ensureSpinnerCSS();
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
@@ -518,9 +511,7 @@ app.registerExtension({
|
||||
|
||||
// Running: full CSS circle spinner
|
||||
if (this.canvasWidget.isProcessing && prompt.isRunning) {
|
||||
const spinner = document.createElement("span");
|
||||
spinner.className = "sam3-spinner";
|
||||
tab.appendChild(spinner);
|
||||
tab.appendChild(makeSpinner());
|
||||
// Pending: waiting its turn in the queue
|
||||
} else if (this.canvasWidget.isProcessing && prompt.isPending) {
|
||||
const dots = document.createElement("span");
|
||||
@@ -37,7 +37,7 @@ function hideWidgetForGood(node, widget, suffix = '') {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SAM3.MultiRegionCollector",
|
||||
name: "sam3.multiregioncollector",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "SAM3MultiRegionCollector") return;
|
||||
@@ -41,7 +41,7 @@ function hideWidgetForGood(node, widget, suffix = '') {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SAM3.SimplePointCollector",
|
||||
name: "sam3.simplepointcollector",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "SAM3PointCollector") {
|
||||
@@ -193,7 +193,7 @@ function refreshNode(node) {
|
||||
|
||||
// Main extension registration
|
||||
app.registerExtension({
|
||||
name: "comfyui.sam3.video_dynamic",
|
||||
name: "sam3.video_dynamic",
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "SAM3VideoSegmentation") {
|
||||
+20
-41
@@ -276,49 +276,28 @@ class SAM3BBoxCollector(io.ComfyNode):
|
||||
img_height, img_width = image.shape[1], image.shape[2]
|
||||
log.info(f"Image dimensions: {img_width}x{img_height}")
|
||||
|
||||
def _to_center_box(bbox: dict) -> list:
|
||||
"""Normalize a raw {x1,y1,x2,y2} canvas rect (pixel space) to
|
||||
[center_x, center_y, width, height] in 0-1 range."""
|
||||
x1, y1, x2, y2 = bbox['x1'], bbox['y1'], bbox['x2'], bbox['y2']
|
||||
if x2 < x1:
|
||||
x1, x2 = x2, x1
|
||||
if y2 < y1:
|
||||
y1, y2 = y2, y1
|
||||
x1_norm, y1_norm = x1 / img_width, y1 / img_height
|
||||
x2_norm, y2_norm = x2 / img_width, y2 / img_height
|
||||
return [(x1_norm + x2_norm) / 2, (y1_norm + y2_norm) / 2, x2_norm - x1_norm, y2_norm - y1_norm]
|
||||
|
||||
# Convert to SAM3_BOXES_PROMPT format with boxes and labels
|
||||
positive_boxes = []
|
||||
positive_labels = []
|
||||
negative_boxes = []
|
||||
negative_labels = []
|
||||
positive_boxes = [_to_center_box(b) for b in pos_bbox_list]
|
||||
positive_labels = [True] * len(positive_boxes)
|
||||
negative_boxes = [_to_center_box(b) for b in neg_bbox_list]
|
||||
negative_labels = [False] * len(negative_boxes)
|
||||
|
||||
# Add positive bboxes (label = True)
|
||||
for bbox in pos_bbox_list:
|
||||
# Normalize bbox coordinates to 0-1 range
|
||||
x1_norm = bbox['x1'] / img_width
|
||||
y1_norm = bbox['y1'] / img_height
|
||||
x2_norm = bbox['x2'] / img_width
|
||||
y2_norm = bbox['y2'] / img_height
|
||||
|
||||
# Convert from [x1, y1, x2, y2] to [center_x, center_y, width, height]
|
||||
# SAM3 expects boxes in center format
|
||||
center_x = (x1_norm + x2_norm) / 2
|
||||
center_y = (y1_norm + y2_norm) / 2
|
||||
width = x2_norm - x1_norm
|
||||
height = y2_norm - y1_norm
|
||||
|
||||
positive_boxes.append([center_x, center_y, width, height])
|
||||
positive_labels.append(True) # Positive boxes
|
||||
log.info(f" Positive BBox: ({bbox['x1']:.1f}, {bbox['y1']:.1f}, {bbox['x2']:.1f}, {bbox['y2']:.1f}) -> center=({center_x:.3f}, {center_y:.3f}) size=({width:.3f}, {height:.3f})")
|
||||
|
||||
# Add negative bboxes (label = False)
|
||||
for bbox in neg_bbox_list:
|
||||
# Normalize bbox coordinates to 0-1 range
|
||||
x1_norm = bbox['x1'] / img_width
|
||||
y1_norm = bbox['y1'] / img_height
|
||||
x2_norm = bbox['x2'] / img_width
|
||||
y2_norm = bbox['y2'] / img_height
|
||||
|
||||
# Convert from [x1, y1, x2, y2] to [center_x, center_y, width, height]
|
||||
# SAM3 expects boxes in center format
|
||||
center_x = (x1_norm + x2_norm) / 2
|
||||
center_y = (y1_norm + y2_norm) / 2
|
||||
width = x2_norm - x1_norm
|
||||
height = y2_norm - y1_norm
|
||||
|
||||
negative_boxes.append([center_x, center_y, width, height])
|
||||
negative_labels.append(False) # Negative boxes
|
||||
log.info(f" Negative BBox: ({bbox['x1']:.1f}, {bbox['y1']:.1f}, {bbox['x2']:.1f}, {bbox['y2']:.1f}) -> center=({center_x:.3f}, {center_y:.3f}) size=({width:.3f}, {height:.3f})")
|
||||
for b in positive_boxes:
|
||||
log.info(f" Positive BBox: center=({b[0]:.3f}, {b[1]:.3f}) size=({b[2]:.3f}, {b[3]:.3f})")
|
||||
for b in negative_boxes:
|
||||
log.info(f" Negative BBox: center=({b[0]:.3f}, {b[1]:.3f}) size=({b[2]:.3f}, {b[3]:.3f})")
|
||||
|
||||
log.info(f"Output: {len(positive_boxes)} positive, {len(negative_boxes)} negative bboxes")
|
||||
|
||||
|
||||
+9
-59
@@ -59,9 +59,9 @@ class SAM3Grounding(io.ComfyNode):
|
||||
io.String.Input("text_prompt", default="", multiline=False, optional=True,
|
||||
tooltip="Describe what to segment using natural language (e.g., 'person', 'cat', 'red car', 'shoes')"),
|
||||
io.Custom("SAM3_BOXES_PROMPT").Input("positive_boxes", optional=True,
|
||||
tooltip="Optional box prompts to focus detection on specific regions. Connect from SAM3CombineBoxes node."),
|
||||
tooltip="Optional box prompts to focus detection on specific regions. Connect from SAM3CreateBox."),
|
||||
io.Custom("SAM3_BOXES_PROMPT").Input("negative_boxes", optional=True,
|
||||
tooltip="Optional box prompts to exclude specific regions from detection. Connect from SAM3CombineBoxes node."),
|
||||
tooltip="Optional box prompts to exclude specific regions from detection. Connect from SAM3CreateBox."),
|
||||
io.Int.Input("max_detections", default=-1, min=-1, max=100, step=1, optional=True,
|
||||
tooltip="Maximum number of detections to return (-1 for all)"),
|
||||
],
|
||||
@@ -229,7 +229,9 @@ class SAM3CreateBox(io.ComfyNode):
|
||||
"""
|
||||
Helper node to create a box prompt visually
|
||||
|
||||
Use sliders to define a bounding box for refinement.
|
||||
Use sliders to define a bounding box for refinement. Outputs a ready-to-use
|
||||
SAM3_BOXES_PROMPT (a single-box list) so it can be wired directly into
|
||||
SAM3Grounding/SAM3Segmentation without a separate combine step.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -251,7 +253,7 @@ class SAM3CreateBox(io.ComfyNode):
|
||||
tooltip="True for positive (include), False for negative (exclude)"),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("SAM3_BOX_PROMPT").Output(display_name="box_prompt"),
|
||||
io.Custom("SAM3_BOXES_PROMPT").Output(display_name="box_prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -259,8 +261,8 @@ class SAM3CreateBox(io.ComfyNode):
|
||||
def execute(cls, center_x, center_y, width, height, is_positive):
|
||||
"""Create a box prompt"""
|
||||
box_prompt = {
|
||||
"box": [center_x, center_y, width, height],
|
||||
"label": is_positive
|
||||
"boxes": [[center_x, center_y, width, height]],
|
||||
"labels": [is_positive],
|
||||
}
|
||||
return io.NodeOutput(box_prompt)
|
||||
|
||||
@@ -301,56 +303,6 @@ class SAM3CreatePoint(io.ComfyNode):
|
||||
return io.NodeOutput(point_prompt)
|
||||
|
||||
|
||||
class SAM3CombineBoxes(io.ComfyNode):
|
||||
"""
|
||||
Combine multiple box prompts into a single input
|
||||
|
||||
Connect multiple SAM3CreateBox nodes to combine them.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls):
|
||||
return io.Schema(
|
||||
node_id="SAM3CombineBoxes",
|
||||
display_name="SAM3 Combine Boxes",
|
||||
category="SAM3/prompts",
|
||||
inputs=[
|
||||
io.Custom("SAM3_BOX_PROMPT").Input("box_1", optional=True,
|
||||
tooltip="Connect box prompts from SAM3CreateBox nodes. Combines multiple boxes into a single prompt for SAM3Segmentation."),
|
||||
io.Custom("SAM3_BOX_PROMPT").Input("box_2", optional=True,
|
||||
tooltip="Connect box prompts from SAM3CreateBox nodes. Combines multiple boxes into a single prompt for SAM3Segmentation."),
|
||||
io.Custom("SAM3_BOX_PROMPT").Input("box_3", optional=True,
|
||||
tooltip="Connect box prompts from SAM3CreateBox nodes. Combines multiple boxes into a single prompt for SAM3Segmentation."),
|
||||
io.Custom("SAM3_BOX_PROMPT").Input("box_4", optional=True,
|
||||
tooltip="Connect box prompts from SAM3CreateBox nodes. Combines multiple boxes into a single prompt for SAM3Segmentation."),
|
||||
io.Custom("SAM3_BOX_PROMPT").Input("box_5", optional=True,
|
||||
tooltip="Connect box prompts from SAM3CreateBox nodes. Combines multiple boxes into a single prompt for SAM3Segmentation."),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("SAM3_BOXES_PROMPT").Output(display_name="boxes_prompt"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, **kwargs):
|
||||
"""Combine multiple box prompts"""
|
||||
boxes = []
|
||||
labels = []
|
||||
|
||||
for i in range(1, 6):
|
||||
box_key = f"box_{i}"
|
||||
if box_key in kwargs and kwargs[box_key] is not None:
|
||||
box_data = kwargs[box_key]
|
||||
boxes.append(box_data["box"])
|
||||
labels.append(box_data["label"])
|
||||
|
||||
combined = {
|
||||
"boxes": boxes,
|
||||
"labels": labels
|
||||
}
|
||||
return io.NodeOutput(combined)
|
||||
|
||||
|
||||
class SAM3CombinePoints(io.ComfyNode):
|
||||
"""
|
||||
Combine multiple point prompts into a single input
|
||||
@@ -439,7 +391,7 @@ class SAM3Segmentation(io.ComfyNode):
|
||||
io.Custom("SAM3_POINTS_PROMPT").Input("negative_points", optional=True,
|
||||
tooltip="Background points - exclude these areas from segmentation. Connect from SAM3CombinePoints or SAM3PointCollector."),
|
||||
io.Custom("SAM3_BOXES_PROMPT").Input("box", optional=True,
|
||||
tooltip="Box prompt to constrain segmentation region. Only first box is used. Connect from SAM3CombineBoxes."),
|
||||
tooltip="Box prompt to constrain segmentation region. Only first box is used. Connect from SAM3CreateBox."),
|
||||
io.Int.Input("refinement_iterations", default=0, min=0, max=10, optional=True,
|
||||
tooltip="Number of refinement passes. Each pass feeds the mask back for cleaner edges."),
|
||||
io.Boolean.Input("use_multimask", default=True, optional=True,
|
||||
@@ -866,7 +818,6 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SAM3MultipromptSegmentation": SAM3MultipromptSegmentation,
|
||||
"SAM3CreateBox": SAM3CreateBox,
|
||||
"SAM3CreatePoint": SAM3CreatePoint,
|
||||
"SAM3CombineBoxes": SAM3CombineBoxes,
|
||||
"SAM3CombinePoints": SAM3CombinePoints,
|
||||
}
|
||||
|
||||
@@ -876,6 +827,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SAM3MultipromptSegmentation": "SAM3 Multiprompt Segmentation",
|
||||
"SAM3CreateBox": "SAM3 Create Box",
|
||||
"SAM3CreatePoint": "SAM3 Create Point",
|
||||
"SAM3CombineBoxes": "SAM3 Combine Boxes",
|
||||
"SAM3CombinePoints": "SAM3 Combine Points",
|
||||
}
|
||||
|
||||
+38
-3
@@ -1,12 +1,47 @@
|
||||
"""ComfyUI-SAM3 Prestartup Script."""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from comfy_env import setup_env, copy_files
|
||||
import folder_paths
|
||||
from comfy_env import setup_env
|
||||
|
||||
log = logging.getLogger("sam3")
|
||||
|
||||
setup_env()
|
||||
|
||||
# The CONFIGURED input directory, never the code-tree one. ComfyUI Desktop
|
||||
# (--base-directory) and --input-directory both relocate it, and the load
|
||||
# nodes only ever scan folder_paths.get_input_directory(). main.py runs
|
||||
# apply_custom_paths() before prestartup scripts, so this is already resolved.
|
||||
INPUT = Path(folder_paths.get_input_directory())
|
||||
|
||||
|
||||
def copy_files(src: Path, dst: Path, pattern: str = "*") -> int:
|
||||
"""Copy bundled assets into a directory. Returns files written.
|
||||
|
||||
Seeds rather than syncs: an existing file is left alone, so a user's
|
||||
edited demo asset survives every relaunch. Raises if `src` is missing --
|
||||
a typo'd asset directory is a packaging bug, and silence is how it stays
|
||||
one.
|
||||
"""
|
||||
src, dst = Path(src), Path(dst)
|
||||
if not src.is_dir():
|
||||
raise FileNotFoundError(f"asset directory not found: {src}")
|
||||
written = 0
|
||||
for f in src.glob(pattern):
|
||||
if not f.is_file():
|
||||
continue
|
||||
target = dst / f.relative_to(src)
|
||||
if target.exists():
|
||||
continue
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(f, target)
|
||||
written += 1
|
||||
return written
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
COMFYUI_DIR = SCRIPT_DIR.parent.parent
|
||||
|
||||
# Copy assets
|
||||
copy_files(SCRIPT_DIR / "assets", COMFYUI_DIR / "input")
|
||||
copy_files(SCRIPT_DIR / "assets", INPUT)
|
||||
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
[project]
|
||||
name = "comfyui-sam3"
|
||||
description = "Warning, uses experimental package `comfy-env` to attempt a one click isolated install. Will download and use pixi package manager. SAM 3 (Segment Anything Model 3) integration for ComfyUI. Image and video segmentation with text prompts, boxes, and points"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "pznodes"
|
||||
DisplayName = "SAM3"
|
||||
web = "javascript"
|
||||
Icon = "https://raw.githubusercontent.com/PozzettiAndrea/ComfyUI-SAM3/main/assets/icon.png"
|
||||
|
||||
[project.urls]
|
||||
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
comfy-env==0.3.89
|
||||
comfy-3d-viewers==0.2.44
|
||||
comfy-env==0.4.3
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user