v3.3.0
This commit is contained in:
@@ -21,6 +21,9 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
|
||||
## 📜 更新日志
|
||||
|
||||
**v3.3.0**
|
||||
- feat(conversion): 添加 `Mask to SAM3 Box`、`Text to Any` 节点
|
||||
|
||||
**v3.2.6**
|
||||
- refactor(condition): 优化 `Text Encode QwenImageEdit` 节点
|
||||
|
||||
@@ -442,9 +445,11 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
| Image List to Batch | 将图像列表转换为批量图像,自动进行尺寸标准化 |
|
||||
| Mask Batch to List | 将批量遮罩转换为遮罩列表,用于单独处理 |
|
||||
| Mask List to Batch | 将遮罩列表转换为批量遮罩,自动进行尺寸标准化 |
|
||||
| Mask to SAM3 Box | 将遮罩转换为 SAM3 框提示(归一化 cx,cy,bw,bh),支持正/负与合并/分离模式 |
|
||||
| String Coordinate to BBoxes | 将字符串格式坐标转换为BBOXES格式,增强格式支持并改进SAM2兼容性 |
|
||||
| String Coordinate to BBox Mask | 将字符串格式坐标转换为BBoxMask格式,支持图像尺寸获取和灵活的输出模式 |
|
||||
| Text List to String | 文本列表合并,逐项应用前后缀并按分隔符拼接,支持转义与复合分隔符 |
|
||||
| Text to Any | 将文本输出为通配类型,便于连接到更多输入端 |
|
||||
|
||||
### 🧠 逻辑节点
|
||||
| 节点名称 | 功能描述 |
|
||||
|
||||
@@ -21,6 +21,9 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
|
||||
## 📜 Changelog
|
||||
|
||||
**v3.3.0**
|
||||
- feat(conversion): Add `Mask to SAM3 Box` and `Text to Any` nodes
|
||||
|
||||
**v3.2.6**
|
||||
- refactor(condition): Optimize `Text Encode QwenImageEdit` node
|
||||
|
||||
@@ -442,9 +445,11 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes
|
||||
| Image List to Batch | Convert image lists to batch images with automatic size normalization |
|
||||
| Mask Batch to List | Convert batch masks to mask lists for individual processing |
|
||||
| Mask List to Batch | Convert mask lists to batch masks with automatic size normalization |
|
||||
| Mask to SAM3 Box | Convert masks to SAM3 box prompts (normalized cx, cy, bw, bh), supporting positive/negative and merge/separate modes |
|
||||
| String Coordinate to BBoxes | Convert string format coordinates to BBOXES format with enhanced format support and improved SAM2 compatibility |
|
||||
| String Coordinate to BBox Mask | Convert string format coordinates to BBoxMask format with image dimension support and flexible output modes |
|
||||
| Text List to String | Merge a text list by applying per-item prefix/suffix and joining with a separator; supports escape sequences and composite separators |
|
||||
| Text to Any | Output text as a wildcard payload for flexible connections (e.g., COMBO inputs) |
|
||||
|
||||
### 🧠 Logic Nodes
|
||||
| Node Name | Description |
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:968e531a2abe373d5c52c6c3a10beba1d3fc4fabce2c27e515aa6b97dd7be71e
|
||||
size 117824
|
||||
oid sha256:12effe04a03c1c0b4a2a1c0a9faedbb40189fedd8dec8b2996d6d75b3786633d
|
||||
size 133970
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
from comfy_api.latest import io
|
||||
from skimage.measure import label, regionprops
|
||||
|
||||
|
||||
class MaskToSam3Box(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
return io.Schema(
|
||||
node_id="1hew_MaskToSam3Box",
|
||||
display_name="Mask to SAM3 Box",
|
||||
category="1hewNodes/conversion",
|
||||
inputs=[
|
||||
io.Mask.Input("mask"),
|
||||
io.Combo.Input(
|
||||
"condition",
|
||||
options=["positive", "negative"],
|
||||
default="positive",
|
||||
),
|
||||
io.Combo.Input(
|
||||
"output_mode",
|
||||
options=["merge", "separate"],
|
||||
default="merge",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("SAM3_BOXES_PROMPT").Output(
|
||||
display_name="sam3_box"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(
|
||||
cls,
|
||||
mask: torch.Tensor,
|
||||
condition: str,
|
||||
output_mode: str,
|
||||
) -> io.NodeOutput:
|
||||
if mask.dim() == 2:
|
||||
mask = mask.unsqueeze(0)
|
||||
|
||||
batch, height, width = mask.shape[:3]
|
||||
prompts: list[dict[str, list]] = []
|
||||
is_positive = condition == "positive"
|
||||
for b in range(batch):
|
||||
boxes_xyxy = cls._boxes_from_mask(
|
||||
mask[b],
|
||||
width=width,
|
||||
height=height,
|
||||
output_mode=output_mode,
|
||||
)
|
||||
formatted = cls._to_collector_boxes(
|
||||
boxes_xyxy,
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
prompts.append(
|
||||
{
|
||||
"boxes": formatted,
|
||||
"labels": [is_positive] * len(formatted),
|
||||
}
|
||||
)
|
||||
|
||||
if batch == 1:
|
||||
return io.NodeOutput(prompts[0])
|
||||
return io.NodeOutput(prompts)
|
||||
|
||||
@classmethod
|
||||
def _boxes_from_mask(
|
||||
cls,
|
||||
mask_2d: torch.Tensor,
|
||||
width: int,
|
||||
height: int,
|
||||
output_mode: str,
|
||||
) -> list[list[int]]:
|
||||
if mask_2d.numel() == 0:
|
||||
return []
|
||||
|
||||
mask_np = (mask_2d.detach().cpu().numpy() > 0.5).astype(np.uint8)
|
||||
if not np.any(mask_np):
|
||||
return []
|
||||
|
||||
if output_mode == "merge":
|
||||
ys, xs = np.where(mask_np > 0)
|
||||
x_min = int(xs.min())
|
||||
x_max = int(xs.max()) + 1
|
||||
y_min = int(ys.min())
|
||||
y_max = int(ys.max()) + 1
|
||||
box = cls._clamp_box(
|
||||
[x_min, y_min, x_max, y_max],
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
return [box] if box else []
|
||||
|
||||
labeled = label(mask_np > 0)
|
||||
regions = regionprops(labeled)
|
||||
boxes: list[list[int]] = []
|
||||
for region in regions:
|
||||
min_row, min_col, max_row, max_col = region.bbox
|
||||
x_min = int(min_col)
|
||||
x_max = int(max_col)
|
||||
y_min = int(min_row)
|
||||
y_max = int(max_row)
|
||||
box = cls._clamp_box(
|
||||
[x_min, y_min, x_max, y_max],
|
||||
width=width,
|
||||
height=height,
|
||||
)
|
||||
if box:
|
||||
boxes.append(box)
|
||||
return boxes
|
||||
|
||||
@staticmethod
|
||||
def _clamp_box(
|
||||
box_xyxy: list[int],
|
||||
width: int,
|
||||
height: int,
|
||||
) -> list[int] | None:
|
||||
x_min, y_min, x_max, y_max = box_xyxy
|
||||
x_min = max(0, x_min)
|
||||
y_min = max(0, y_min)
|
||||
x_max = min(width, x_max)
|
||||
y_max = min(height, y_max)
|
||||
if x_max <= x_min or y_max <= y_min:
|
||||
return None
|
||||
return [int(x_min), int(y_min), int(x_max), int(y_max)]
|
||||
|
||||
@staticmethod
|
||||
def _to_collector_boxes(
|
||||
boxes_xyxy: list[list[int]],
|
||||
width: int,
|
||||
height: int,
|
||||
) -> list[list[float]]:
|
||||
w = float(width) if width else 1.0
|
||||
h = float(height) if height else 1.0
|
||||
formatted: list[list[float]] = []
|
||||
for x1, y1, x2, y2 in boxes_xyxy:
|
||||
cx = (float(x1) + float(x2)) / 2.0 / w
|
||||
cy = (float(y1) + float(y2)) / 2.0 / h
|
||||
bw = (float(x2) - float(x1)) / w
|
||||
bh = (float(y2) - float(y1)) / h
|
||||
formatted.append([cx, cy, bw, bh])
|
||||
return formatted
|
||||
@@ -0,0 +1,28 @@
|
||||
from comfy_api.latest import io
|
||||
|
||||
|
||||
class TextToAny(io.ComfyNode):
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
return io.Schema(
|
||||
node_id="1hew_TextToAny",
|
||||
display_name="Text to Any",
|
||||
category="1hewNodes/conversion",
|
||||
inputs=[
|
||||
io.String.Input("text", default="", multiline=True),
|
||||
],
|
||||
outputs=[
|
||||
io.Custom("*").Output(display_name="any"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_inputs(cls, **kwargs):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
async def execute(cls, text: str) -> io.NodeOutput:
|
||||
if isinstance(text, (list, tuple)):
|
||||
text = text[0] if len(text) > 0 else ""
|
||||
text = "" if text is None else str(text)
|
||||
return io.NodeOutput(text)
|
||||
@@ -94,31 +94,266 @@ class TextCustomExtract(io.ComfyNode):
|
||||
|
||||
@classmethod
|
||||
def _parse_json_data(cls, json_data):
|
||||
text = cls._clean_input_text(json_data) if isinstance(json_data, str) else str(json_data)
|
||||
text = (
|
||||
cls._clean_input_text(json_data)
|
||||
if isinstance(json_data, str)
|
||||
else str(json_data)
|
||||
)
|
||||
if not text:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
parsed = cls._try_literal_eval(text)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
parsed = cls._try_json_loads(text.replace("'", '"'))
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
parsed = cls._try_loose_parse(text)
|
||||
if parsed is not None:
|
||||
return parsed
|
||||
|
||||
pattern = r'"([^"]+)"\s*:\s*"([^"]+)"'
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
return {key: value for key, value in matches}
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def _try_json_loads(cls, text: str):
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _try_literal_eval(cls, text: str):
|
||||
try:
|
||||
return ast.literal_eval(text)
|
||||
except (ValueError, SyntaxError):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _try_loose_parse(cls, text: str):
|
||||
stripped = text.strip()
|
||||
if not stripped:
|
||||
return None
|
||||
|
||||
start_idx = None
|
||||
start_char = None
|
||||
for idx, ch in enumerate(stripped):
|
||||
if ch in "[{":
|
||||
start_idx = idx
|
||||
start_char = ch
|
||||
break
|
||||
if start_idx is None or start_char is None:
|
||||
return None
|
||||
|
||||
end_char = "}" if start_char == "{" else "]"
|
||||
end_idx = stripped.rfind(end_char)
|
||||
if end_idx == -1 or end_idx <= start_idx:
|
||||
return None
|
||||
|
||||
container = stripped[start_idx : end_idx + 1]
|
||||
try:
|
||||
if start_char == "{":
|
||||
parsed, _ = cls._loose_parse_dict(container, 0)
|
||||
return parsed
|
||||
parsed, _ = cls._loose_parse_list(container, 0)
|
||||
return parsed
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _loose_parse_dict(cls, text: str, start: int):
|
||||
if start >= len(text) or text[start] != "{":
|
||||
raise ValueError("Expected '{'")
|
||||
|
||||
idx = start + 1
|
||||
result = {}
|
||||
while idx < len(text):
|
||||
idx = cls._skip_ws(text, idx)
|
||||
if idx >= len(text):
|
||||
break
|
||||
if text[idx] == "}":
|
||||
return result, idx + 1
|
||||
if text[idx] == ",":
|
||||
idx += 1
|
||||
continue
|
||||
|
||||
key, idx = cls._read_key(text, idx)
|
||||
idx = cls._skip_ws(text, idx)
|
||||
if idx < len(text) and text[idx] == ":":
|
||||
idx += 1
|
||||
idx = cls._skip_ws(text, idx)
|
||||
|
||||
value, idx = cls._loose_parse_value(text, idx)
|
||||
if key:
|
||||
result[key] = value
|
||||
|
||||
idx = cls._skip_ws(text, idx)
|
||||
if idx < len(text) and text[idx] == ",":
|
||||
idx += 1
|
||||
return result, idx
|
||||
|
||||
@classmethod
|
||||
def _loose_parse_list(cls, text: str, start: int):
|
||||
if start >= len(text) or text[start] != "[":
|
||||
raise ValueError("Expected '['")
|
||||
|
||||
idx = start + 1
|
||||
result = []
|
||||
while idx < len(text):
|
||||
idx = cls._skip_ws(text, idx)
|
||||
if idx >= len(text):
|
||||
break
|
||||
if text[idx] == "]":
|
||||
return result, idx + 1
|
||||
if text[idx] == ",":
|
||||
idx += 1
|
||||
continue
|
||||
|
||||
value, idx = cls._loose_parse_value(text, idx)
|
||||
result.append(value)
|
||||
|
||||
idx = cls._skip_ws(text, idx)
|
||||
if idx < len(text) and text[idx] == ",":
|
||||
idx += 1
|
||||
return result, idx
|
||||
|
||||
@classmethod
|
||||
def _loose_parse_value(cls, text: str, start: int):
|
||||
idx = cls._skip_ws(text, start)
|
||||
if idx >= len(text):
|
||||
return None, idx
|
||||
|
||||
ch = text[idx]
|
||||
if ch in ('"', "'"):
|
||||
value, idx = cls._read_loose_quoted_string(text, idx)
|
||||
return value, idx
|
||||
|
||||
if ch == "{":
|
||||
value, idx = cls._loose_parse_dict(text, idx)
|
||||
return value, idx
|
||||
|
||||
if ch == "[":
|
||||
value, idx = cls._loose_parse_list(text, idx)
|
||||
return value, idx
|
||||
|
||||
token, idx = cls._read_token(text, idx)
|
||||
lowered = token.lower()
|
||||
if lowered in {"null", "none"}:
|
||||
return None, idx
|
||||
if lowered == "true":
|
||||
return True, idx
|
||||
if lowered == "false":
|
||||
return False, idx
|
||||
|
||||
if re.fullmatch(r"-?\d+", token):
|
||||
try:
|
||||
return ast.literal_eval(text)
|
||||
except (ValueError, SyntaxError):
|
||||
try:
|
||||
fixed_text = text.replace("'", '"')
|
||||
return json.loads(fixed_text)
|
||||
except json.JSONDecodeError:
|
||||
pattern = r'"([^"]+)"\s*:\s*"([^"]+)"'
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
return {key: value for key, value in matches}
|
||||
return {}
|
||||
return int(token), idx
|
||||
except ValueError:
|
||||
return token, idx
|
||||
if re.fullmatch(r"-?\d+\.\d+", token):
|
||||
try:
|
||||
return float(token), idx
|
||||
except ValueError:
|
||||
return token, idx
|
||||
return token, idx
|
||||
|
||||
@classmethod
|
||||
def _skip_ws(cls, text: str, idx: int) -> int:
|
||||
while idx < len(text) and text[idx].isspace():
|
||||
idx += 1
|
||||
return idx
|
||||
|
||||
@classmethod
|
||||
def _read_key(cls, text: str, start: int):
|
||||
idx = start
|
||||
ch = text[idx]
|
||||
if ch in ('"', "'"):
|
||||
quote = ch
|
||||
idx += 1
|
||||
buf = []
|
||||
while idx < len(text):
|
||||
cur = text[idx]
|
||||
if cur == "\\" and idx + 1 < len(text):
|
||||
buf.append(text[idx + 1])
|
||||
idx += 2
|
||||
continue
|
||||
if cur == quote:
|
||||
idx += 1
|
||||
break
|
||||
buf.append(cur)
|
||||
idx += 1
|
||||
return "".join(buf).strip(), idx
|
||||
|
||||
buf = []
|
||||
while idx < len(text):
|
||||
cur = text[idx]
|
||||
if cur == ":" or cur.isspace() or cur in ",}":
|
||||
break
|
||||
buf.append(cur)
|
||||
idx += 1
|
||||
return "".join(buf).strip(), idx
|
||||
|
||||
@classmethod
|
||||
def _read_loose_quoted_string(cls, text: str, start: int):
|
||||
quote = text[start]
|
||||
idx = start + 1
|
||||
buf = []
|
||||
while idx < len(text):
|
||||
cur = text[idx]
|
||||
if cur == "\\" and idx + 1 < len(text):
|
||||
buf.append(cur)
|
||||
buf.append(text[idx + 1])
|
||||
idx += 2
|
||||
continue
|
||||
if cur == quote:
|
||||
look = cls._skip_ws(text, idx + 1)
|
||||
if look >= len(text) or text[look] in ",}]":
|
||||
return cls._decode_string("".join(buf), quote), idx + 1
|
||||
buf.append(cur)
|
||||
idx += 1
|
||||
continue
|
||||
buf.append(cur)
|
||||
idx += 1
|
||||
return cls._decode_string("".join(buf), quote), idx
|
||||
|
||||
@classmethod
|
||||
def _decode_string(cls, raw: str, quote: str) -> str:
|
||||
candidate = f"{quote}{raw}{quote}"
|
||||
try:
|
||||
return json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
return raw
|
||||
|
||||
@classmethod
|
||||
def _read_token(cls, text: str, start: int):
|
||||
idx = start
|
||||
buf = []
|
||||
while idx < len(text):
|
||||
cur = text[idx]
|
||||
if cur in ",}]":
|
||||
break
|
||||
buf.append(cur)
|
||||
idx += 1
|
||||
return "".join(buf).strip(), idx
|
||||
|
||||
@classmethod
|
||||
def _get_enhanced_keys(cls, key_name: str) -> list[str]:
|
||||
if not key_name.strip():
|
||||
return []
|
||||
key_lower = key_name.lower().strip()
|
||||
enhanced = [key_name, key_name.upper(), key_name.lower(), key_name.capitalize()]
|
||||
enhanced = [
|
||||
key_name,
|
||||
key_name.upper(),
|
||||
key_name.lower(),
|
||||
key_name.capitalize(),
|
||||
]
|
||||
mappings = {
|
||||
"bbox": [
|
||||
"bbox",
|
||||
@@ -130,7 +365,15 @@ class TextCustomExtract(io.ComfyNode):
|
||||
"coord",
|
||||
"bbox_2d",
|
||||
],
|
||||
"label": ["label", "LABEL", "Label", "name", "title", "text", "class"],
|
||||
"label": [
|
||||
"label",
|
||||
"LABEL",
|
||||
"Label",
|
||||
"name",
|
||||
"title",
|
||||
"text",
|
||||
"class",
|
||||
],
|
||||
"confidence": [
|
||||
"confidence",
|
||||
"CONFIDENCE",
|
||||
@@ -143,8 +386,23 @@ class TextCustomExtract(io.ComfyNode):
|
||||
"y": ["y", "Y", "pos_y", "position_y"],
|
||||
"width": ["width", "w", "W", "WIDTH"],
|
||||
"height": ["height", "h", "H", "HEIGHT"],
|
||||
"zh": ["zh", "ZH", "chinese", "Chinese", "CHINESE", "中文"],
|
||||
"en": ["en", "EN", "english", "English", "ENGLISH", "英文", "英语"],
|
||||
"zh": [
|
||||
"zh",
|
||||
"ZH",
|
||||
"chinese",
|
||||
"Chinese",
|
||||
"CHINESE",
|
||||
"中文",
|
||||
],
|
||||
"en": [
|
||||
"en",
|
||||
"EN",
|
||||
"english",
|
||||
"English",
|
||||
"ENGLISH",
|
||||
"英文",
|
||||
"英语",
|
||||
],
|
||||
}
|
||||
for base, vars in mappings.items():
|
||||
if key_lower == base or key_name in vars:
|
||||
@@ -201,5 +459,3 @@ class TextCustomExtract(io.ComfyNode):
|
||||
return False
|
||||
label_str = str(label_value).lower()
|
||||
return any(f.lower() in label_str for f in filters)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI-1hewNodes"
|
||||
description = "This is a custom node collection for ComfyUI that provides some utility nodes."
|
||||
version = "3.2.10"
|
||||
version = "3.3.0"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Mask to SAM3 Box - Mask to SAM3 box prompt
|
||||
|
||||
**Node Purpose:** `Mask to SAM3 Box` converts an input mask into bounding-box prompts and outputs a `SAM3_BOXES_PROMPT` structure for SAM3 workflows that require box prompts. It supports positive/negative prompts, merged output, and connected-component split output.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `mask` | - | MASK | - | - | Input mask (batch supported). Pixels with value `> 0.5` are treated as foreground |
|
||||
| `condition` | - | COMBO | `positive` | `positive` / `negative` | Prompt label; `positive` means foreground boxes, `negative` means exclusion boxes |
|
||||
| `output_mode` | - | COMBO | `merge` | `merge` / `separate` | `merge` outputs a single merged box; `separate` outputs multiple boxes by connected components |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `sam3_box` | SAM3_BOXES_PROMPT | SAM3 box prompt; a dict for single mask, and a list of dicts for batched masks |
|
||||
|
||||
## Features
|
||||
|
||||
- Box extraction:
|
||||
- `merge`: computes one minimal enclosing rectangle over all foreground pixels.
|
||||
- `separate`: computes an enclosing rectangle per foreground connected component.
|
||||
- Normalized format: each box is output as `[cx, cy, bw, bh]` normalized to 0-1 relative to mask width/height.
|
||||
- Label output: `labels` is a boolean list where `positive` maps to `True` and `negative` maps to `False`, aligned with `boxes`.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Connect a `MASK` from segmentation/detection to this node, then connect `sam3_box` to nodes that accept SAM3 box prompts.
|
||||
- Use `condition=negative` to exclude areas; use `output_mode=merge` to reduce the number of boxes.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- Mask noise can produce many small boxes in `separate` mode. Consider upstream cleanup or use `merge`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Mask to SAM3 Box - 遮罩转 SAM3 Box 提示
|
||||
|
||||
**节点功能:** `Mask to SAM3 Box` 将输入遮罩解析为边界框提示,并输出 `SAM3_BOXES_PROMPT` 结构,适用于需要框提示的 SAM3 工作流。支持正/负提示、合并输出与按连通域拆分输出。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `mask` | - | MASK | - | - | 输入遮罩(支持批量)。遮罩值按 `> 0.5` 视为前景 |
|
||||
| `condition` | - | COMBO | `positive` | `positive` / `negative` | 提示标签;`positive` 表示前景框,`negative` 表示排除框 |
|
||||
| `output_mode` | - | COMBO | `merge` | `merge` / `separate` | `merge` 输出单个合并框;`separate` 按连通域输出多个框 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| `sam3_box` | SAM3_BOXES_PROMPT | SAM3 框提示;单张遮罩输出字典,批量遮罩输出字典列表 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 框提取:
|
||||
- `merge`:对全部前景像素取一个整体最小外接矩形。
|
||||
- `separate`:对前景连通域逐个计算外接矩形。
|
||||
- 归一化格式:每个框输出为 `[cx, cy, bw, bh]`,均为 0-1 归一化值(相对于遮罩宽高)。
|
||||
- 标签输出:`labels` 为布尔列表,`positive` 对应 `True`,`negative` 对应 `False`,长度与 `boxes` 一致。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 将分割/检测得到的 `MASK` 连接到本节点,再将 `sam3_box` 输出连接到需要 SAM3 框提示的节点。
|
||||
- 需要排除区域时设置 `condition=negative`;希望减少框数量时使用 `output_mode=merge`。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 遮罩噪声会导致 `separate` 生成大量小框,建议在上游做适度的形态学清理或改用 `merge`。
|
||||
@@ -0,0 +1,28 @@
|
||||
# Text to Any - Text to wildcard
|
||||
|
||||
**Node Purpose:** `Text to Any` outputs a user-typed text as a wildcard (`*`) payload, enabling more flexible connections (for example, some COMBO inputs can accept a text payload when wired).
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Port | Type | Default | Range | Description |
|
||||
| ---- | ---- | ---- | ------- | ----- | ----------- |
|
||||
| `text` | - | STRING | `` | multiline | Input text |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `any` | * | Wildcard payload containing the input text |
|
||||
|
||||
## Features
|
||||
|
||||
- Wildcard output: uses `*` to maximize connection compatibility.
|
||||
- List input support: when `text` is provided as a list, the node uses the first item.
|
||||
|
||||
## Typical Usage
|
||||
|
||||
- Provide file names, identifiers, or prompt text to downstream nodes via a flexible wildcard connection.
|
||||
|
||||
## Notes & Tips
|
||||
|
||||
- This node only forwards the text; it does not validate formatting. Keep the value consistent with downstream expectations.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Text to Any - 文本转通配类型
|
||||
|
||||
**节点功能:** `Text to Any` 将手写文本作为通配类型(`*`)输出,便于连接到更多输入端类型(例如部分节点的 COMBO 参数在连接时也可接收文本载荷)。
|
||||
|
||||
## 输入
|
||||
|
||||
| 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 |
|
||||
| -------- | -------- | -------- | ------ | -------- | ---- |
|
||||
| `text` | - | STRING | `` | 多行 | 输入文本 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出名称 | 数据类型 | 描述 |
|
||||
|---------|----------|------|
|
||||
| `any` | * | 输出通配类型载荷,内容为输入文本 |
|
||||
|
||||
## 功能说明
|
||||
|
||||
- 通配输出:输出端使用通配类型 `*`,用于提升连接兼容性。
|
||||
- 列表输入兼容:当 `text` 以列表形式输入时,节点取第一个元素作为输出。
|
||||
|
||||
## 典型用法
|
||||
|
||||
- 需要为下游节点提供文件名/标识符/提示词文本时,将 `any` 输出直接连接到对应输入端。
|
||||
|
||||
## 注意与建议
|
||||
|
||||
- 本节点仅负责传递文本内容,不对内容做格式校验;建议保持与下游节点期望的命名/路径/格式一致。
|
||||
Reference in New Issue
Block a user