diff --git a/nodes/_model_cache.py b/nodes/_model_cache.py index 51fdfd1..bb9cba7 100644 --- a/nodes/_model_cache.py +++ b/nodes/_model_cache.py @@ -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() diff --git a/nodes/sam3/attention.py b/nodes/sam3/attention.py index e248d76..cb6b407 100644 --- a/nodes/sam3/attention.py +++ b/nodes/sam3/attention.py @@ -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)) diff --git a/nodes/sam3/model.py b/nodes/sam3/model.py index ed11e6f..e897656 100644 --- a/nodes/sam3/model.py +++ b/nodes/sam3/model.py @@ -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 diff --git a/nodes/sam3/perflib.py b/nodes/sam3/perflib.py index acecd48..6c98d12 100644 --- a/nodes/sam3/perflib.py +++ b/nodes/sam3/perflib.py @@ -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( diff --git a/nodes/sam3/utils.py b/nodes/sam3/utils.py index b57c9c5..1a8c1f9 100644 --- a/nodes/sam3/utils.py +++ b/nodes/sam3/utils.py @@ -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] diff --git a/nodes/sam3_model_patcher.py b/nodes/sam3_model_patcher.py index 28b841e..e02fe50 100644 --- a/nodes/sam3_model_patcher.py +++ b/nodes/sam3_model_patcher.py @@ -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: diff --git a/nodes/segmentation.py b/nodes/segmentation.py index 3b8a17c..0d7d397 100644 --- a/nodes/segmentation.py +++ b/nodes/segmentation.py @@ -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}") diff --git a/workflows/image_seg_text.json b/workflows/image_seg_text.json index 3643c8a..c8f5403 100644 --- a/workflows/image_seg_text.json +++ b/workflows/image_seg_text.json @@ -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} \ No newline at end of file +{"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} \ No newline at end of file