This commit is contained in:
PozzettiAndrea
2026-03-13 10:31:25 +00:00
parent d5a18a7aa4
commit e9413e5c71
8 changed files with 116 additions and 84 deletions
+14 -5
View File
@@ -73,18 +73,27 @@ def get_or_build_model(config):
)
del remapped_ckpt
if missing_keys:
log.info(f"Missing keys: {len(missing_keys)}")
log.debug("SAM3: %d missing keys during load", len(missing_keys))
if unexpected_keys:
log.info(f"Unexpected keys: {len(unexpected_keys)}")
log.debug("SAM3: %d unexpected keys during load", len(unexpected_keys))
# Fix leftover meta-device buffers
for name, buf in model.named_buffers():
# Fix leftover meta-device buffers.
# Non-persistent buffers (like causal attention masks) are NOT saved in the
# checkpoint, so they stay on meta device after load_state_dict(assign=True).
# We must rebuild them rather than blindly zeroing, since some (like
# attn_mask) need specific values (e.g. -inf upper triangle for causality).
for name, buf in list(model.named_buffers()):
if buf.device.type == "meta":
parts = name.split(".")
parent = model
for p in parts[:-1]:
parent = getattr(parent, p)
parent._buffers[parts[-1]] = torch.zeros_like(buf, device="cpu")
attr_name = parts[-1]
# Try to rebuild the buffer via its owner's builder method
if attr_name == "attn_mask" and hasattr(parent, "build_causal_mask"):
parent._buffers[attr_name] = parent.build_causal_mask()
else:
parent._buffers[attr_name] = torch.zeros_like(buf, device="cpu")
model.eval()
+24 -4
View File
@@ -105,6 +105,9 @@ def sam3_attention(q, k, v, num_heads):
# SplitMultiheadAttention — drop-in replacement for nn.MultiheadAttention
# ---------------------------------------------------------------------------
_split_mha_dbg_count = 0
class SplitMultiheadAttention(nn.Module):
"""Drop-in replacement for nn.MultiheadAttention using separate Linear projections.
@@ -177,19 +180,36 @@ class SplitMultiheadAttention(nn.Module):
# Prepare mask for attention
sdpa_mask = self._prepare_mask(attn_mask, key_padding_mask, B, L_q, L_k, q.dtype, q.device)
import sys
global _split_mha_dbg_count
_split_mha_dbg_count += 1
_do_dbg = _split_mha_dbg_count <= 5 or (_split_mha_dbg_count > 24 and _split_mha_dbg_count <= 30)
if _do_dbg:
_path = "masked" if sdpa_mask is not None else "unmasked"
print(f"[DBG] MHA#{_split_mha_dbg_count} batch_first={self.batch_first} "
f"q={list(q.shape)} k={list(k.shape)} dtype={q.dtype} "
f"path={_path} has_attn_mask={attn_mask is not None} "
f"has_kpm={key_padding_mask is not None}", file=sys.stderr)
if sdpa_mask is not None:
print(f"[DBG] MHA#{_split_mha_dbg_count} sdpa_mask: shape={list(sdpa_mask.shape)}, "
f"dtype={sdpa_mask.dtype}, "
f"min={sdpa_mask.min():.4f}, max={sdpa_mask.max():.4f}, "
f"has_neginf={(sdpa_mask == float('-inf')).sum().item()}, "
f"num_zero={(sdpa_mask == 0).sum().item()}", file=sys.stderr)
# When mask is needed, use SDPA (pytorch) which supports masks natively.
# Flash/sage/xformers don't support arbitrary attention masks.
if sdpa_mask is not None:
masked_fn = optimized_attention_for_device(q.device, mask=True)
out = masked_fn(q, k, v, heads=self.num_heads, mask=sdpa_mask, skip_reshape=True)
# pytorch_attn returns [B, L, H*D] (skip_output_reshape defaults to False)
log.debug("[SplitMHA] masked path -> out=%s", list(out.shape))
else:
out = sam3_attention(q, k, v, self.num_heads)
# sam3_attention returns [B, H, L, D] — transpose to [B, L, H, D]
log.debug("[SplitMHA] sam3_attn path -> out=%s (before transpose)", list(out.shape))
out = out.transpose(1, 2)
log.debug("[SplitMHA] after transpose -> out=%s", list(out.shape))
if _do_dbg:
print(f"[DBG] MHA#{_split_mha_dbg_count} out: shape={list(out.shape)}, "
f"norm={out.float().norm():.4f}", file=sys.stderr)
out = out.reshape(B, L_q, self.embed_dim)
log.debug("[SplitMHA] after reshape -> out=%s", list(out.shape))
+42 -7
View File
@@ -1439,6 +1439,18 @@ class TransformerDecoderLayer(nn.Module):
dac=False, dac_use_selfatt_ln=True, presence_token=None,
identity=0.0, **kwargs,
):
import sys
_li = getattr(self, 'layer_idx', '?')
_p = presence_token is not None
if _p and _li == 0:
print(f"[DBG] DecoderLayer-{_li}: tgt={list(tgt.shape)} dtype={tgt.dtype}, "
f"memory_text={list(memory_text.shape) if memory_text is not None else None} "
f"dtype={memory_text.dtype if memory_text is not None else None}, "
f"text_mask={list(text_attention_mask.shape) if text_attention_mask is not None else None} "
f"sum_True={text_attention_mask.sum().item() if text_attention_mask is not None else 'N/A'}, "
f"cross_attn_mask={list(cross_attn_mask.shape) if cross_attn_mask is not None else None}", file=sys.stderr)
print(f"[DBG] DecoderLayer-{_li}: use_text_cross_attention={self.use_text_cross_attention}", file=sys.stderr)
if self.self_attn is not None:
if dac:
assert tgt.shape[0] % 2 == 0
@@ -1721,10 +1733,15 @@ class TransformerDecoder(nn.Module):
else:
reference_boxes = None
intermediate_ref_boxes = None
import sys
output = tgt
presence_out = None
if self.presence_token is not None and is_instance_prompt is False:
presence_out = self.presence_token.weight[None].expand(1, bs, -1).to(device=tgt.device, dtype=tgt.dtype)
print(f"[DBG] Decoder: presence_token embed: shape={presence_out.shape}, dtype={presence_out.dtype}, "
f"norm={presence_out.float().norm():.6f}", file=sys.stderr)
print(f"[DBG] Decoder: tgt dtype={tgt.dtype}, memory dtype={memory.dtype}, "
f"memory_text dtype={memory_text.dtype if memory_text is not None else 'None'}", file=sys.stderr)
box_head = self.bbox_embed
if is_instance_prompt and self.instance_bbox_embed is not None:
box_head = self.instance_bbox_embed
@@ -1785,11 +1802,13 @@ class TransformerDecoder(nn.Module):
raise NotImplementedError("not implemented yet")
intermediate.append(out_norm(output))
if self.presence_token is not None and is_instance_prompt is False:
normed_presence = self.presence_token_out_norm(presence_out)
intermediate_layer_presence_logits = self.presence_token_head(
self.presence_token_out_norm(presence_out)
normed_presence
).squeeze(-1)
print(f"[DBG] Decoder layer {layer_idx}: presence_logit={intermediate_layer_presence_logits.item():.6f}", file=sys.stderr)
if self.clamp_presence_logits:
intermediate_layer_presence_logits.clamp(
intermediate_layer_presence_logits = intermediate_layer_presence_logits.clamp(
min=-self.clamp_presence_logit_max_val,
max=self.clamp_presence_logit_max_val,
)
@@ -3559,12 +3578,19 @@ class Sam3Image(torch.nn.Module):
device=geo_masks.device,
dtype=geo_masks.dtype,
)
import sys
print(f"[DBG] _encode_prompt:", file=sys.stderr)
print(f"[DBG] txt_feats: shape={txt_feats.shape}, dtype={txt_feats.dtype}, norm={txt_feats.float().norm():.4f}", file=sys.stderr)
print(f"[DBG] geo_feats: shape={geo_feats.shape}, dtype={geo_feats.dtype}, norm={geo_feats.float().norm():.4f}", file=sys.stderr)
print(f"[DBG] img_feats[-1]: shape={img_feats[-1].shape}, dtype={img_feats[-1].dtype}, norm={img_feats[-1].float().norm():.4f}", file=sys.stderr)
if encode_text:
prompt = torch.cat([txt_feats, geo_feats, visual_prompt_embed], dim=0)
prompt_mask = torch.cat([txt_masks, geo_masks, visual_prompt_mask], dim=1)
else:
prompt = torch.cat([geo_feats, visual_prompt_embed], dim=0)
prompt_mask = torch.cat([geo_masks, visual_prompt_mask], dim=1)
print(f"[DBG] prompt: shape={prompt.shape}, dtype={prompt.dtype}, norm={prompt.float().norm():.4f}", file=sys.stderr)
print(f"[DBG] prompt_mask: shape={prompt_mask.shape}, sum_True={prompt_mask.sum().item()}", file=sys.stderr)
return prompt, prompt_mask, backbone_out
def _run_encoder(
@@ -3696,6 +3722,15 @@ class Sam3Image(torch.nn.Module):
outputs_coord = (reference_boxes_inv_sig + anchor_box_offsets).sigmoid()
outputs_boxes_xyxy = box_cxcywh_to_xyxy(outputs_coord)
import sys
print(f"[DBG] _update_scores_and_boxes:", file=sys.stderr)
print(f"[DBG] outputs_class: shape={outputs_class.shape}, dtype={outputs_class.dtype}, "
f"min={outputs_class.float().min():.4f}, max={outputs_class.float().max():.4f}", file=sys.stderr)
if dec_presence_out is not None:
print(f"[DBG] dec_presence_out: shape={dec_presence_out.shape}, dtype={dec_presence_out.dtype}, "
f"min={dec_presence_out.float().min():.4f}, max={dec_presence_out.float().max():.4f}, "
f"last_layer_sigmoid={dec_presence_out[-1].float().sigmoid().squeeze().tolist()}", file=sys.stderr)
if dec_presence_out is not None:
_update_out(
out, "presence_logit_dec", dec_presence_out, update_aux=self.training
@@ -3706,12 +3741,12 @@ class Sam3Image(torch.nn.Module):
prob_dec_presence_out = dec_presence_out.clone().sigmoid()
if self.detach_presence_in_joint_score:
prob_dec_presence_out = prob_dec_presence_out.detach()
_dtype_debug("_update_scores: before joint_box_scores",
outputs_class_pre=outputs_class, dec_presence_out=dec_presence_out)
# Use float32 for the inverse_sigmoid round-trip to avoid bfloat16
# precision loss that collapses all logits to the same value.
orig_dtype = outputs_class.dtype
outputs_class = inverse_sigmoid(
outputs_class.sigmoid() * prob_dec_presence_out.unsqueeze(2)
).clamp(min=-10.0, max=10.0)
_dtype_debug("_update_scores: after joint_box_scores", outputs_class_post=outputs_class)
outputs_class.float().sigmoid() * prob_dec_presence_out.float().unsqueeze(2)
).clamp(min=-10.0, max=10.0).to(orig_dtype)
_update_out(
out, "pred_logits", outputs_class[:, :, :num_o2o], update_aux=self.training
+5 -2
View File
@@ -118,8 +118,11 @@ def generic_nms(
if ious.is_cuda:
if GENERIC_NMS_AVAILABLE:
return generic_nms_cuda(ious, scores, iou_threshold, use_iou_matrix=True)
else:
try:
return generic_nms_cuda(ious, scores, iou_threshold, use_iou_matrix=True)
except (ImportError, OSError, RuntimeError) as e:
log.warning("GPU NMS failed at runtime (%s), falling back to CPU", e)
if not GENERIC_NMS_AVAILABLE:
global _SHOWN_NMS_WARNING
if not _SHOWN_NMS_WARNING:
log.warning(
+30 -43
View File
@@ -1309,23 +1309,20 @@ class Sam3Processor:
raise ValueError("Image must be a PIL image or a tensor")
image = v2.functional.to_image(image).to(self.device)
image = self.transform(image).unsqueeze(0)
# Native ComfyUI pattern: cast input to target dtype at the pipeline
# boundary. manual_cast layers will cast their fp32 weights to match.
inference_dtype = getattr(self, '_inference_dtype', None)
if inference_dtype is not None and inference_dtype in (torch.float16, torch.bfloat16):
image = image.to(dtype=inference_dtype)
log.debug(f"[DEBUG] set_image: input shape={image.shape}, dtype={image.dtype}, "
f"min={image.min():.4f}, max={image.max():.4f}, device={image.device}")
# Cast image to match the backbone's native weight dtype so that
# manual_cast keeps weights in their stored precision (typically bf16).
# Without this, an fp32 image causes manual_cast to promote bf16
# weights to fp32, producing different features than the original model.
# Cast image to match the backbone's native weight dtype so that
# manual_cast keeps weights in their stored precision (typically bf16).
# Without this, an fp32 image causes manual_cast to promote bf16
# weights to fp32, producing different features than the original model.
backbone_dtype = next(self.model.backbone.parameters()).dtype
if backbone_dtype != image.dtype:
image = image.to(dtype=backbone_dtype)
state["original_height"] = height
state["original_width"] = width
state["backbone_out"] = self.model.backbone.forward_image(image)
# Debug: inspect backbone output
backbone_out = state["backbone_out"]
log.debug(f"[DEBUG] set_image: backbone_out keys: {list(backbone_out.keys())}")
if "backbone_fpn" in backbone_out:
for i, feat in enumerate(backbone_out["backbone_fpn"]):
log.debug(f"[DEBUG] backbone_fpn[{i}]: shape={feat.shape}, dtype={feat.dtype}, "
f"min={feat.min():.4f}, max={feat.max():.4f}")
inst_interactivity_en = self.model.inst_interactive_predictor is not None
if inst_interactivity_en and "sam2_backbone_out" in state["backbone_out"]:
sam2_backbone_out = state["backbone_out"]["sam2_backbone_out"]
@@ -1357,9 +1354,9 @@ class Sam3Processor:
for image in images
]
images = torch.stack(images, dim=0)
inference_dtype = getattr(self, '_inference_dtype', None)
if inference_dtype is not None and inference_dtype in (torch.float16, torch.bfloat16):
images = images.to(dtype=inference_dtype)
backbone_dtype = next(self.model.backbone.parameters()).dtype
if backbone_dtype != images.dtype:
images = images.to(dtype=backbone_dtype)
state["backbone_out"] = self.model.backbone.forward_image(images)
inst_interactivity_en = self.model.inst_interactive_predictor is not None
if inst_interactivity_en and "sam2_backbone_out" in state["backbone_out"]:
@@ -1488,33 +1485,23 @@ class Sam3Processor:
out_logits = outputs["pred_logits"]
out_masks = outputs["pred_masks"]
# Debug: print raw output shapes and stats
# Match the original SAM3 processor: multiply class logits by presence
# score explicitly. This is how the model was trained and evaluated.
out_probs = out_logits.float().sigmoid()
presence_score = outputs["presence_logit_dec"].float().sigmoid().unsqueeze(1)
out_probs = (out_probs * presence_score).squeeze(-1)
import sys
print(f"[SAM3-DBG] forward_grounding output keys: {list(outputs.keys())}", file=sys.stderr)
print(f"[SAM3-DBG] pred_logits shape: {out_logits.shape}, min: {out_logits.min():.4f}, max: {out_logits.max():.4f}", file=sys.stderr)
print(f"[SAM3-DBG] pred_boxes shape: {out_bbox.shape}", file=sys.stderr)
print(f"[SAM3-DBG] pred_masks shape: {out_masks.shape}", file=sys.stderr)
# pred_logits already have presence baked in via supervise_joint_box_scores:
# pred_logits = inverse_sigmoid(sigmoid(class_raw) * sigmoid(presence))
# So sigmoid(pred_logits) = sigmoid(class_raw) * sigmoid(presence).
# Do NOT multiply by presence again — that would square its effect.
out_probs = out_logits.sigmoid().squeeze(-1)
if "presence_logit_dec" in outputs:
presence_raw = outputs["presence_logit_dec"]
log.debug(f"[DEBUG] presence_logit_dec: {presence_raw.shape}, "
f"val={presence_raw.flatten().tolist()}, sigmoid={presence_raw.sigmoid().flatten().tolist()}")
log.debug(f"[DEBUG] out_probs (joint_box_scores, no double presence): "
f"min={out_probs.min():.4f}, max={out_probs.max():.4f}")
print(f"[SAM3-DBG] confidence_threshold: {self.confidence_threshold}", file=sys.stderr)
print(f"[SAM3-DBG] out_probs shape: {out_probs.shape}, min: {out_probs.min():.4f}, max: {out_probs.max():.4f}", file=sys.stderr)
print(f"[SAM3-DBG] detections above threshold: {(out_probs > self.confidence_threshold).sum().item()} / {out_probs.numel()}", file=sys.stderr)
# Top-10 probabilities for debugging
topk_vals, topk_idxs = out_probs.flatten().topk(min(10, out_probs.numel()))
print(f"[SAM3-DBG] top-10 probs: {[f'{v:.4f}' for v in topk_vals.tolist()]}", file=sys.stderr)
print(f"[DBG] _forward_grounding:", file=sys.stderr)
print(f"[DBG] out_logits: shape={out_logits.shape}, dtype={out_logits.dtype}, "
f"min={out_logits.float().min():.4f}, max={out_logits.float().max():.4f}", file=sys.stderr)
pld = outputs["presence_logit_dec"]
print(f"[DBG] presence_logit_dec: shape={pld.shape}, dtype={pld.dtype}, "
f"min={pld.float().min():.4f}, max={pld.float().max():.4f}, "
f"sigmoid_max={pld.float().sigmoid().max():.6f}", file=sys.stderr)
print(f"[DBG] out_probs: max={out_probs.max():.6f}, "
f"above_thresh={( out_probs > self.confidence_threshold).sum().item()}/{out_probs.numel()}, "
f"threshold={self.confidence_threshold}", file=sys.stderr)
keep = out_probs > self.confidence_threshold
out_probs = out_probs[keep]
-7
View File
@@ -28,12 +28,6 @@ class SAM3UnifiedModel(ModelPatcher):
self._load_device = load_device
self._offload_device = offload_device
self._model_dtype = dtype or torch.float32
# Set inference dtype on processor immediately so set_image() always
# casts image inputs to bf16/fp16 before backbone forward.
self._processor._inference_dtype = self._model_dtype
import logging, os
if os.environ.get("DEBUG_COMFYUI_SAM3", "").lower() in ("1", "true", "yes"):
logging.getLogger("sam3").warning("SAM3UnifiedModel: dtype=%s, processor._inference_dtype=%s", self._model_dtype, self._processor._inference_dtype)
# The full model (detector + tracker) is the nn.Module we manage
full_model = video_predictor.model
@@ -141,7 +135,6 @@ class SAM3UnifiedModel(ModelPatcher):
Follows the native ComfyUI pattern: the patcher owns device state and
pushes it to the processor as a torch.device (like ClipVisionModel.load_device).
"""
self._processor._inference_dtype = self._model_dtype
if hasattr(self._processor, 'device'):
self._processor.device = device
if hasattr(self._processor, 'find_stage') and self._processor.find_stage is not None:
-15
View File
@@ -84,18 +84,12 @@ class SAM3Grounding(io.ComfyNode):
from ._model_cache import get_or_build_model
import comfy.model_management
import sys
sam3_model = get_or_build_model(sam3_model_config)
comfy.model_management.load_models_gpu([sam3_model])
processor = sam3_model.processor
device = sam3_model.current_device
print(f"[SAM3-DBG] model device={device}, processor.device={processor.device}", file=sys.stderr)
print(f"[SAM3-DBG] processor._inference_dtype={getattr(processor, '_inference_dtype', 'NOT SET')}", file=sys.stderr)
print(f"[SAM3-DBG] backbone param dtype={next(processor.model.backbone.parameters()).dtype}, device={next(processor.model.backbone.parameters()).device}", file=sys.stderr)
print(f"[SAM3-DBG] find_stage img_ids device={processor.find_stage.img_ids.device}", file=sys.stderr)
if hasattr(processor, 'sync_device_with_model'):
processor.sync_device_with_model()
elif hasattr(processor, 'device') and str(processor.device) != str(device):
@@ -169,15 +163,6 @@ class SAM3Grounding(io.ComfyNode):
boxes = state.get("boxes", None)
scores = state.get("scores", None)
import sys
print(f"[SAM3-DBG] state keys: {list(state.keys())}", file=sys.stderr)
print(f"[SAM3-DBG] masks: {type(masks)}, shape={masks.shape if hasattr(masks, 'shape') else 'N/A'}, len={len(masks) if masks is not None else 'None'}", file=sys.stderr)
if scores is not None:
print(f"[SAM3-DBG] scores: {scores.shape}, values={scores.tolist() if scores.numel() < 20 else scores[:20].tolist()}", file=sys.stderr)
if "masks_logits" in state:
ml = state["masks_logits"]
print(f"[SAM3-DBG] masks_logits: shape={ml.shape}, min={ml.min():.4f}, max={ml.max():.4f}", file=sys.stderr)
# Check if we got any results AFTER threshold
if masks is None or len(masks) == 0:
log.info(f"No detections found for prompt: '{text_prompt}' at threshold {confidence_threshold}")
+1 -1
View File
@@ -1 +1 @@
{"id":"9e5b67d0-53dc-42aa-bdb7-541f1939e114","revision":0,"last_node_id":15,"last_link_id":29,"nodes":[{"id":1,"type":"LoadImage","pos":[626.0189034779078,251.07377362433684],"size":[274.080078125,314],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[26]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"LoadImage"},"widgets_values":["example_image.jpg","image"]},{"id":12,"type":"LoadSAM3Model","pos":[619.424717299163,120.03196674570798],"size":[270,82],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"precision","name":"precision","shape":7,"type":"COMBO","widget":{"name":"precision"},"link":null},{"localized_name":"compile","name":"compile","shape":7,"type":"BOOLEAN","widget":{"name":"compile"},"link":null}],"outputs":[{"localized_name":"sam3_model_config","name":"sam3_model_config","type":"SAM3_MODEL_CONFIG","links":[25]}],"properties":{"cnr_id":"comfyui-sam3","ver":"82c8e4f88a6c0a9242712b827e05f5e67c4a90a7","Node name for S&R":"LoadSAM3Model","aux_id":"PozzettiAndrea/ComfyUI-SAM3"},"widgets_values":["auto",false]},{"id":4,"type":"MaskPreview","pos":[1353.5680467079214,-163.05450100811575],"size":[210,258],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":29}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"MaskPreview"},"widgets_values":[]},{"id":3,"type":"PreviewImage","pos":[1287.2913250517656,206.2605361994992],"size":[312.1604553633704,258],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":28}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":15,"type":"SAM3Grounding","pos":[966.7400889373585,87.93762601383364],"size":[288.263671875,166],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"sam3_model_config","name":"sam3_model_config","type":"SAM3_MODEL_CONFIG","link":25},{"localized_name":"image","name":"image","type":"IMAGE","link":26},{"localized_name":"positive_boxes","name":"positive_boxes","shape":7,"type":"SAM3_BOXES_PROMPT","link":null},{"localized_name":"negative_boxes","name":"negative_boxes","shape":7,"type":"SAM3_BOXES_PROMPT","link":null},{"localized_name":"confidence_threshold","name":"confidence_threshold","type":"FLOAT","widget":{"name":"confidence_threshold"},"link":null},{"localized_name":"text_prompt","name":"text_prompt","shape":7,"type":"STRING","widget":{"name":"text_prompt"},"link":null},{"localized_name":"max_detections","name":"max_detections","shape":7,"type":"INT","widget":{"name":"max_detections"},"link":null}],"outputs":[{"localized_name":"masks","name":"masks","type":"MASK","links":[29]},{"localized_name":"visualization","name":"visualization","type":"IMAGE","links":[28]},{"localized_name":"boxes","name":"boxes","type":"STRING","links":null},{"localized_name":"scores","name":"scores","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-sam3","ver":"c7636fa4a0b8c0fb07aa01f0c7749ebd5f9c92f3","Node name for S&R":"SAM3Grounding"},"widgets_values":[0.2,"person",-1]}],"links":[[25,12,0,15,0,"SAM3_MODEL_CONFIG"],[26,1,0,15,1,"IMAGE"],[28,15,1,3,0,"IMAGE"],[29,15,0,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.4015052702425141,"offset":[-507.949628341597,201.90594531705412]},"frontendVersion":"1.32.9","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true,"workflowRendererVersion":"LG"},"version":0.4}
{"id":"9e5b67d0-53dc-42aa-bdb7-541f1939e114","revision":0,"last_node_id":15,"last_link_id":29,"nodes":[{"id":1,"type":"LoadImage","pos":[626.0189034779078,251.07377362433684],"size":[274.080078125,314],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[26]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"LoadImage"},"widgets_values":["example_image.jpg","image"]},{"id":12,"type":"LoadSAM3Model","pos":[619.424717299163,120.03196674570798],"size":[270,82],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"precision","name":"precision","shape":7,"type":"COMBO","widget":{"name":"precision"},"link":null},{"localized_name":"compile","name":"compile","shape":7,"type":"BOOLEAN","widget":{"name":"compile"},"link":null}],"outputs":[{"localized_name":"sam3_model_config","name":"sam3_model_config","type":"SAM3_MODEL_CONFIG","links":[25]}],"properties":{"cnr_id":"comfyui-sam3","ver":"82c8e4f88a6c0a9242712b827e05f5e67c4a90a7","Node name for S&R":"LoadSAM3Model","aux_id":"PozzettiAndrea/ComfyUI-SAM3"},"widgets_values":["auto",false]},{"id":4,"type":"MaskPreview","pos":[1353.5680467079214,-163.05450100811575],"size":[210,258],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":29}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"MaskPreview"},"widgets_values":[]},{"id":3,"type":"PreviewImage","pos":[1287.2913250517656,206.2605361994992],"size":[312.1604553633704,258],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":28}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.72","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":15,"type":"SAM3Grounding","pos":[966.7400889373585,87.93762601383364],"size":[288.263671875,166],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"sam3_model_config","name":"sam3_model_config","type":"SAM3_MODEL_CONFIG","link":25},{"localized_name":"image","name":"image","type":"IMAGE","link":26},{"localized_name":"positive_boxes","name":"positive_boxes","shape":7,"type":"SAM3_BOXES_PROMPT","link":null},{"localized_name":"negative_boxes","name":"negative_boxes","shape":7,"type":"SAM3_BOXES_PROMPT","link":null},{"localized_name":"confidence_threshold","name":"confidence_threshold","type":"FLOAT","widget":{"name":"confidence_threshold"},"link":null},{"localized_name":"text_prompt","name":"text_prompt","shape":7,"type":"STRING","widget":{"name":"text_prompt"},"link":null},{"localized_name":"max_detections","name":"max_detections","shape":7,"type":"INT","widget":{"name":"max_detections"},"link":null}],"outputs":[{"localized_name":"masks","name":"masks","type":"MASK","links":[29]},{"localized_name":"visualization","name":"visualization","type":"IMAGE","links":[28]},{"localized_name":"boxes","name":"boxes","type":"STRING","links":null},{"localized_name":"scores","name":"scores","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-sam3","ver":"c7636fa4a0b8c0fb07aa01f0c7749ebd5f9c92f3","Node name for S&R":"SAM3Grounding"},"widgets_values":[0.2,"child",-1]}],"links":[[25,12,0,15,0,"SAM3_MODEL_CONFIG"],[26,1,0,15,1,"IMAGE"],[28,15,1,3,0,"IMAGE"],[29,15,0,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.4015052702425141,"offset":[-507.949628341597,201.90594531705412]},"frontendVersion":"1.32.9","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true,"workflowRendererVersion":"LG"},"version":0.4}