added tests

This commit is contained in:
Andrea Pozzetti
2025-11-09 22:05:47 +00:00
parent a947f4961c
commit 331dd47869
31 changed files with 2164 additions and 495 deletions
+52
View File
@@ -0,0 +1,52 @@
name: Code Quality (Lint)
on:
pull_request:
branches: [main, master, develop]
push:
branches: [main, master, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: 'pip'
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install ruff black isort
- name: Run Ruff (linter)
run: |
ruff check . --output-format=github
continue-on-error: true
- name: Run Black (formatter check)
run: |
black --check --diff .
continue-on-error: true
- name: Run isort (import sorting check)
run: |
isort --check-only --diff .
continue-on-error: true
- name: Lint summary
if: always()
run: |
echo "## Code Quality Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Ruff linting completed" >> $GITHUB_STEP_SUMMARY
echo "✅ Black formatting check completed" >> $GITHUB_STEP_SUMMARY
echo "✅ isort import sorting check completed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Note**: Linting errors are not blocking CI (continue-on-error: true)" >> $GITHUB_STEP_SUMMARY
+29
View File
@@ -0,0 +1,29 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'PozzettiAndrea' }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
submodules: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+80
View File
@@ -0,0 +1,80 @@
name: Real Model Integration Tests
on:
# Run on schedule (weekly, every Monday at 3 AM UTC)
schedule:
- cron: '0 3 * * 1'
# Allow manual trigger
workflow_dispatch:
# Optionally run on specific branches (commented out to save resources)
# push:
# branches: [main]
jobs:
test-real-models:
runs-on: ubuntu-latest
timeout-minutes: 60 # Real model tests can take longer
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: 'pip'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgl1-mesa-glx libglib2.0-0
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-mock pytest-asyncio
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Create model cache directory
run: |
mkdir -p ~/.cache/huggingface
- name: Cache models
uses: actions/cache@v4
with:
path: ~/.cache/huggingface
key: ${{ runner.os }}-huggingface-models-${{ hashFiles('tests/integration_real/*.py') }}
restore-keys: |
${{ runner.os }}-huggingface-models-
- name: Run real model tests
run: |
pytest tests/integration_real/ -v -m real_model --tb=short
env:
PYTHONPATH: ${{ github.workspace }}
HF_HUB_DISABLE_TELEMETRY: 1
- name: Test summary
if: always()
run: |
echo "## Real Model Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ GroundingDINO SwinT tests completed" >> $GITHUB_STEP_SUMMARY
echo "✅ Florence-2 Base tests completed" >> $GITHUB_STEP_SUMMARY
echo "✅ OWLv2 Base tests completed" >> $GITHUB_STEP_SUMMARY
echo "✅ YOLO-World v2 S tests completed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Note**: SAM2 tests were skipped as requested" >> $GITHUB_STEP_SUMMARY
- name: Upload test artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-failures
path: |
pytest-results/
*.log
+66
View File
@@ -0,0 +1,66 @@
name: Unit and Integration Tests
on:
push:
branches: [main, master, develop]
pull_request:
branches: [main, master, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-mock pytest-asyncio
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run unit tests
run: |
pytest tests/unit/ -v --cov=nodes --cov-report=xml --cov-report=term-missing
env:
PYTHONPATH: ${{ github.workspace }}
- name: Run mocked integration tests
run: |
pytest tests/integration/ -v --cov=nodes --cov-append --cov-report=xml --cov-report=term-missing
env:
PYTHONPATH: ${{ github.workspace }}
- name: Run workflow validation tests
run: |
pytest tests/workflows/ -v
env:
PYTHONPATH: ${{ github.workspace }}
- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.10'
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
- name: Test summary
if: always()
run: |
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
echo "✅ Unit tests completed" >> $GITHUB_STEP_SUMMARY
echo "✅ Mocked integration tests completed" >> $GITHUB_STEP_SUMMARY
echo "✅ Workflow validation tests completed" >> $GITHUB_STEP_SUMMARY
+26 -14
View File
@@ -4,20 +4,32 @@ Supports GroundingDINO, MM-GroundingDINO, OWLv2, Florence-2, and YOLO-World
Now includes SAM2 segmentation nodes
"""
from .grounding_init import init
# Only run initialization and imports when loaded by ComfyUI, not during pytest
# This prevents relative import errors when pytest collects test modules
import sys
if 'pytest' not in sys.modules:
from .grounding_init import init
# Initialize web extensions for compatibility with older ComfyUI versions
init()
# Initialize web extensions for compatibility with older ComfyUI versions
init()
from .nodes import (
GroundingModelLoader,
GroundingDetector,
BboxVisualizer,
DownloadAndLoadSAM2Model,
Sam2Segmentation,
GroundingMaskModelLoader,
GroundingMaskDetector,
)
from .nodes import (
GroundingModelLoader,
GroundingDetector,
BboxVisualizer,
DownloadAndLoadSAM2Model,
Sam2Segmentation,
GroundingMaskModelLoader,
GroundingMaskDetector,
)
else:
# During testing, set dummy values
GroundingModelLoader = None
GroundingDetector = None
BboxVisualizer = None
DownloadAndLoadSAM2Model = None
Sam2Segmentation = None
GroundingMaskModelLoader = None
GroundingMaskDetector = None
NODE_CLASS_MAPPINGS = {
"GroundingModelLoader": GroundingModelLoader,
@@ -35,7 +47,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"BboxVisualizer": "Bounding Box Visualizer",
"DownloadAndLoadSAM2Model": "(Down)Load SAM2Model",
"Sam2Segmentation": "Sam2 Segmentation",
"GroundingMaskModelLoader": "Grounding Mask Loader",
"GroundingMaskModelLoader": "Grounding Mask Model Loader",
"GroundingMaskDetector": "Grounding Mask Detector",
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

+11 -11
View File
@@ -1,16 +1,16 @@
import yaml
from .sam2.modeling.sam2_base import SAM2Base
from .sam2.modeling.backbones.image_encoder import ImageEncoder
from .sam2.modeling.backbones.hieradet import Hiera
from .sam2.modeling.backbones.image_encoder import FpnNeck
from .sam2.modeling.position_encoding import PositionEmbeddingSine
from .sam2.modeling.memory_attention import MemoryAttention, MemoryAttentionLayer
from .sam2.modeling.sam.transformer import RoPEAttention
from .sam2.modeling.memory_encoder import MemoryEncoder, MaskDownSampler, Fuser, CXBlock
from .modeling.sam2.modeling.sam2_base import SAM2Base
from .modeling.sam2.modeling.backbones.image_encoder import ImageEncoder
from .modeling.sam2.modeling.backbones.hieradet import Hiera
from .modeling.sam2.modeling.backbones.image_encoder import FpnNeck
from .modeling.sam2.modeling.position_encoding import PositionEmbeddingSine
from .modeling.sam2.modeling.memory_attention import MemoryAttention, MemoryAttentionLayer
from .modeling.sam2.modeling.sam.transformer import RoPEAttention
from .modeling.sam2.modeling.memory_encoder import MemoryEncoder, MaskDownSampler, Fuser, CXBlock
from .sam2.sam2_image_predictor import SAM2ImagePredictor
from .sam2.sam2_video_predictor import SAM2VideoPredictor
from .sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
from .modeling.sam2.sam2_image_predictor import SAM2ImagePredictor
from .modeling.sam2.sam2_video_predictor import SAM2VideoPredictor
from .modeling.sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
from comfy.utils import load_torch_file
def load_model(model_path, model_cfg_path, segmentor, dtype, device):
+1 -1
View File
@@ -74,7 +74,7 @@ def load_sam2(model, segmentor, device, precision, script_directory):
version = "2.1" if "2.1" in model else "2.0"
model_cfg_path = next(
(os.path.join(script_directory, "nodes", "sam2", "configs", cfg)
(os.path.join(script_directory, "nodes", "sam2", "configs", "sam2_configs", cfg)
for key, cfg in model_mapping[version].items() if key in model),
None
)
+13 -2
View File
@@ -162,9 +162,20 @@ def segment_sam2(image, sam2_model, keep_model_loaded, coordinates_positive=None
else:
input_box = final_box[i] if i < len(final_box) else final_box[0]
# Handle point coordinates based on individual_objects mode
if individual_objects and coordinates_positive is not None:
point_coords = final_coords[i] if i < len(final_coords) else final_coords[0]
point_labels = final_labels[i] if i < len(final_labels) else final_labels[0]
elif coordinates_positive is not None:
point_coords = final_coords
point_labels = final_labels
else:
point_coords = None
point_labels = None
out_masks, scores, logits = model.predict(
point_coords=final_coords if coordinates_positive is not None else None,
point_labels=final_labels if coordinates_positive is not None else None,
point_coords=point_coords,
point_labels=point_labels,
box=input_box,
multimask_output=True if not individual_objects else False,
mask_input=input_mask[i].unsqueeze(0) if mask is not None else None,
+43
View File
@@ -0,0 +1,43 @@
[project]
name = "comfyui-grounding"
description = "Object grounding / detection and segmentation nodes for ComfyUI with GroundingDINO, OWLv2, Florence-2, YOLO-World, SAM2, and SA2VA support"
version = "1.0.0"
license = {file = "LICENSE"}
requires-python = ">=3.10"
# Core dependencies for ComfyUI-Grounding
dependencies = [
# For GroundingDINO, MM-GroundingDINO, and OWLv2 (HuggingFace transformers)
"transformers>=4.30.0",
"timm>=0.9.0",
# For YOLO-World
"ultralytics>=8.2.0",
# For model downloads and SAM2
"hf_transfer",
"huggingface_hub>=0.20.0",
# For fine-tuning and flash attention
"peft",
"flash_attn",
# Configuration and utilities
"pyyaml>=6.0",
"tqdm>=4.65.0",
# Core PyTorch (usually already installed with ComfyUI)
"torch>=2.0.0",
"torchvision",
]
[project.urls]
Repository = "https://github.com/PozzettiAndrea/ComfyUI-Grounding"
Documentation = "https://github.com/PozzettiAndrea/ComfyUI-Grounding/wiki"
"Bug Tracker" = "https://github.com/PozzettiAndrea/ComfyUI-Grounding/issues"
# Comfy Registry configuration
[tool.comfy]
PublisherId = "pozzettiandrea"
DisplayName = "ComfyUI-Grounding"
Icon = ""
+23
View File
@@ -0,0 +1,23 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Prevent pytest from recursing into these directories
norecursedirs = .git .github nodes workflows .eggs *.egg-info dist build web
markers =
unit: Unit tests (fast, no model loading)
real_model: Tests that download and use real models (slow)
addopts =
-v
--strict-markers
--tb=short
--import-mode=importlib
# Ignore warnings from dependencies
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
+12
View File
@@ -0,0 +1,12 @@
# Testing dependencies
pytest>=8.0.0
pytest-mock>=3.12.0
pytest-asyncio>=0.23.0
# Code quality tools
ruff>=0.2.0
black>=24.0.0
isort>=5.13.0
# Pre-commit hooks (optional)
pre-commit>=3.6.0
+54
View File
@@ -0,0 +1,54 @@
# ComfyUI-Grounding Tests
## Running Tests
**Unit tests (fast):**
```bash
pytest tests/unit/ -v
```
**Real model tests (slow, downloads models):**
```bash
pytest tests/integration_real/ -v -m real_model
```
**Real model tests with GPU:**
```bash
pytest tests/integration_real/ -v -m real_model --use-gpu
```
## Test List
### Unit Tests (6 tests)
- `test_node_registration.py` - Node class registration
- `test_model_registry.py` - Model registry validation
- `test_input_validation.py` - Input validation
- `test_cache.py` - Model caching
- `test_conversions.py` - Format conversions
- `test_drawing.py` - Bounding box drawing
### Real Model Tests (16 tests)
**Florence-2** (7 tests)
- Load with eager attention
- Load with SDPA attention
- Detection
- Model caching
- Attention implementations comparison
- Mask generation
- Different prompts
**GroundingDINO** (3 tests)
- Load SwinT model
- Detection
- Model caching
**OWLv2** (3 tests)
- Load Base Patch16 model
- Detection
- Model caching
**YOLO-World** (3 tests)
- Load v8s model
- Detection
- Model caching
+133
View File
@@ -0,0 +1,133 @@
"""
Pytest configuration and fixtures for ComfyUI-Grounding tests
"""
import sys
import os
from pathlib import Path
import pytest
import torch
from unittest.mock import MagicMock
def pytest_addoption(parser):
"""Add custom command line options"""
parser.addoption(
"--use-gpu",
action="store_true",
default=False,
help="Run tests on GPU instead of CPU (much faster for real model tests)"
)
# Add the custom node directory to Python path so we can import nodes package
custom_nodes_dir = Path(__file__).parent.parent
sys.path.insert(0, str(custom_nodes_dir))
# Mock ComfyUI modules at module level BEFORE pytest starts
# This prevents import errors when pytest tries to load __init__.py files
mock_folder_paths = type("folder_paths", (), {})()
mock_folder_paths.models_dir = "/tmp/test_models"
mock_folder_paths.get_folder_paths = lambda x: ["/tmp/test_models"]
sys.modules["folder_paths"] = mock_folder_paths
mock_comfy = type("comfy", (), {})()
mock_comfy_utils = type("utils", (), {})()
mock_comfy_utils.load_torch_file = lambda x: {}
mock_comfy_utils.ProgressBar = MagicMock()
mock_comfy_utils.common_upscale = MagicMock()
mock_comfy.utils = mock_comfy_utils
mock_comfy_mm = type("model_management", (), {})()
# Device selection: Check environment variable set by session fixture
def _get_test_device():
"""Get device for testing - GPU if --use-gpu flag is set, else CPU"""
use_gpu = os.environ.get("PYTEST_USE_GPU", "0") == "1"
if use_gpu and torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
mock_comfy_mm.get_torch_device = _get_test_device
mock_comfy_mm.soft_empty_cache = lambda: None
mock_comfy_mm.load_models_gpu = lambda x: None
mock_comfy.model_management = mock_comfy_mm
sys.modules["comfy"] = mock_comfy
sys.modules["comfy.utils"] = mock_comfy_utils
sys.modules["comfy.model_management"] = mock_comfy_mm
# Mock grounding_init module that __init__.py tries to import
mock_grounding_init = type("grounding_init", (), {})()
mock_grounding_init.init = lambda: None
sys.modules["grounding_init"] = mock_grounding_init
def pytest_ignore_collect(collection_path, path, config):
"""Ignore __init__.py files during collection"""
if collection_path.name == "__init__.py":
return True
return False
@pytest.fixture(scope="session", autouse=True)
def setup_test_device(request):
"""Configure test device based on --use-gpu flag"""
use_gpu = request.config.getoption("--use-gpu")
if use_gpu:
os.environ["PYTEST_USE_GPU"] = "1"
if torch.cuda.is_available():
print(f"\n🚀 Using GPU: {torch.cuda.get_device_name(0)}")
else:
print("\n⚠️ --use-gpu specified but CUDA not available, using CPU")
else:
os.environ["PYTEST_USE_GPU"] = "0"
print("\n💻 Using CPU (use --use-gpu for GPU acceleration)")
yield
# Cleanup
os.environ.pop("PYTEST_USE_GPU", None)
@pytest.fixture(scope="session", autouse=True)
def setup_mock_comfy():
"""Set up mock ComfyUI modules for testing - runs once per session"""
# Ensure mocks persist throughout test session
return True
@pytest.fixture
def mock_comfy_environment():
"""Provide access to mocked ComfyUI environment (already set up at module level)"""
return sys.modules["folder_paths"]
@pytest.fixture
def small_image():
"""Create a small test image tensor for faster tests"""
return torch.rand(1, 224, 224, 3)
@pytest.fixture(autouse=True)
def reset_model_cache():
"""Clear model cache between tests"""
from nodes.utils.cache import MODEL_CACHE
MODEL_CACHE.clear()
yield
MODEL_CACHE.clear()
def pytest_configure(config):
"""Configure pytest with custom markers"""
config.addinivalue_line(
"markers", "unit: Unit tests (fast, no model loading)"
)
config.addinivalue_line(
"markers", "integration: Integration tests with mocked models"
)
config.addinivalue_line(
"markers", "real_model: Tests that download and use real models (slow)"
)
config.addinivalue_line(
"markers", "workflow: Workflow validation tests"
)
View File
@@ -0,0 +1,211 @@
"""
Real model integration tests for Florence2
These tests download and use actual models - marked as slow
"""
import pytest
import torch
@pytest.mark.real_model
def test_florence2_base_load_eager(mock_comfy_environment):
"""Test loading real Florence-2 Base model with eager attention"""
from nodes import GroundingModelLoader
loader = GroundingModelLoader()
# Load Florence-2 Base with eager attention
model_dict = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
assert model_dict is not None
assert "type" in model_dict
assert model_dict["type"] == "florence2"
assert "model" in model_dict
assert "processor" in model_dict
@pytest.mark.real_model
def test_florence2_base_load_sdpa(mock_comfy_environment):
"""Test loading real Florence-2 Base model with SDPA attention"""
from nodes import GroundingModelLoader
loader = GroundingModelLoader()
# Load Florence-2 Base with SDPA attention
model_dict = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="sdpa"
)[0]
assert model_dict is not None
assert "type" in model_dict
assert model_dict["type"] == "florence2"
@pytest.mark.real_model
def test_florence2_base_detection(mock_comfy_environment, small_image):
"""Test real detection with Florence-2 Base model"""
from nodes import GroundingModelLoader, GroundingDetector
# Load model
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
# Run detection
detector = GroundingDetector()
bboxes, annotated_img, labels, masks = detector.detect(
model=model_dict,
image=small_image,
prompt="person",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Verify outputs structure
assert bboxes is not None
assert annotated_img is not None
assert annotated_img.shape == small_image.shape
assert isinstance(labels, str)
@pytest.mark.real_model
def test_florence2_model_caching(mock_comfy_environment, reset_model_cache):
"""Test that Florence-2 models are properly cached"""
from nodes import GroundingModelLoader
from nodes.utils.cache import MODEL_CACHE
loader = GroundingModelLoader()
# First load
model1 = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
cache_size_after_first = len(MODEL_CACHE)
# Second load (should use cache)
model2 = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
cache_size_after_second = len(MODEL_CACHE)
# Cache should have model
assert cache_size_after_first > 0
# Cache shouldn't grow on second load
assert cache_size_after_second == cache_size_after_first
@pytest.mark.real_model
def test_florence2_attention_implementations_equivalent(mock_comfy_environment, small_image):
"""Test that different attention implementations produce similar results"""
from nodes import GroundingModelLoader, GroundingDetector
loader = GroundingModelLoader()
detector = GroundingDetector()
# Load with eager
model_eager = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
bboxes_eager, _, _, _ = detector.detect(
model=model_eager,
image=small_image,
prompt="object",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Load with SDPA
model_sdpa = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="sdpa"
)[0]
bboxes_sdpa, _, _, _ = detector.detect(
model=model_sdpa,
image=small_image,
prompt="object",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Both should produce valid outputs (results may vary slightly)
assert bboxes_eager is not None
assert bboxes_sdpa is not None
@pytest.mark.real_model
def test_florence2_mask_generation(mock_comfy_environment, small_image):
"""Test Florence-2 mask generation capability (from bounding boxes)"""
from nodes import GroundingModelLoader, GroundingDetector
# Load regular bbox detection model
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
# Run detection with output_masks=True
detector = GroundingDetector()
bboxes, annotated_img, labels, masks = detector.detect(
model=model_dict,
image=small_image,
prompt="object",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=True
)
# Verify outputs structure
assert bboxes is not None
assert masks is not None
assert annotated_img is not None
assert isinstance(labels, str)
@pytest.mark.real_model
def test_florence2_different_prompts(mock_comfy_environment, small_image):
"""Test Florence-2 with different prompt formats"""
from nodes import GroundingModelLoader, GroundingDetector
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="Florence-2: Base (0.23B params)",
florence2_attn="eager"
)[0]
detector = GroundingDetector()
prompts = [
"person",
"person and dog",
"Locate person",
"Find the person in the image"
]
for prompt in prompts:
bboxes, _, _, _ = detector.detect(
model=model_dict,
image=small_image,
prompt=prompt,
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Each prompt should produce valid output
assert bboxes is not None
@@ -0,0 +1,86 @@
"""
Real model integration tests for GroundingDINO
These tests download and use actual models - marked as slow
"""
import pytest
import torch
@pytest.mark.real_model
def test_grounding_dino_swint_load(mock_comfy_environment):
"""Test loading real GroundingDINO SwinT model"""
from nodes import GroundingModelLoader
loader = GroundingModelLoader()
# Load smallest GroundingDINO model
model_dict = loader.load_model(
model="GroundingDINO: SwinT OGC (694MB)",
florence2_attn="eager"
)[0]
assert model_dict is not None
assert "type" in model_dict
assert model_dict["type"] == "grounding_dino"
assert "model" in model_dict
assert "processor" in model_dict
@pytest.mark.real_model
def test_grounding_dino_swint_detection(mock_comfy_environment, small_image):
"""Test real detection with GroundingDINO SwinT model"""
from nodes import GroundingModelLoader, GroundingDetector
# Load model
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="GroundingDINO: SwinT OGC (694MB)",
florence2_attn="eager"
)[0]
# Run detection
detector = GroundingDetector()
bboxes, annotated_img, labels, masks = detector.detect(
model=model_dict,
image=small_image,
prompt="person . dog .",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Verify outputs structure (may or may not find objects in random image)
assert bboxes is not None
assert annotated_img is not None
assert annotated_img.shape == small_image.shape
assert isinstance(labels, str)
@pytest.mark.real_model
def test_grounding_dino_model_caching(mock_comfy_environment, reset_model_cache):
"""Test that GroundingDINO models are properly cached"""
from nodes import GroundingModelLoader
from nodes.utils.cache import MODEL_CACHE
loader = GroundingModelLoader()
# First load
model1 = loader.load_model(
model="GroundingDINO: SwinT OGC (694MB)",
florence2_attn="eager"
)[0]
cache_size_after_first = len(MODEL_CACHE)
# Second load (should use cache)
model2 = loader.load_model(
model="GroundingDINO: SwinT OGC (694MB)",
florence2_attn="eager"
)[0]
cache_size_after_second = len(MODEL_CACHE)
# Cache should have model
assert cache_size_after_first > 0
# Cache shouldn't grow on second load
assert cache_size_after_second == cache_size_after_first
+86
View File
@@ -0,0 +1,86 @@
"""
Real model integration tests for OWLv2
These tests download and use actual models - marked as slow
"""
import pytest
import torch
@pytest.mark.real_model
def test_owlv2_base_load(mock_comfy_environment):
"""Test loading real OWLv2 Base Patch16 model"""
from nodes import GroundingModelLoader
loader = GroundingModelLoader()
# Load smallest OWLv2 model
model_dict = loader.load_model(
model="OWLv2: Base Patch16",
florence2_attn="eager"
)[0]
assert model_dict is not None
assert "type" in model_dict
assert model_dict["type"] == "owlv2"
assert "model" in model_dict
assert "processor" in model_dict
@pytest.mark.real_model
def test_owlv2_base_detection(mock_comfy_environment, small_image):
"""Test real detection with OWLv2 Base model"""
from nodes import GroundingModelLoader, GroundingDetector
# Load model
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="OWLv2: Base Patch16",
florence2_attn="eager"
)[0]
# Run detection
detector = GroundingDetector()
bboxes, annotated_img, labels, masks = detector.detect(
model=model_dict,
image=small_image,
prompt="person",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Verify outputs structure
assert bboxes is not None
assert annotated_img is not None
assert annotated_img.shape == small_image.shape
assert isinstance(labels, str)
@pytest.mark.real_model
def test_owlv2_model_caching(mock_comfy_environment, reset_model_cache):
"""Test that OWLv2 models are properly cached"""
from nodes import GroundingModelLoader
from nodes.utils.cache import MODEL_CACHE
loader = GroundingModelLoader()
# First load
model1 = loader.load_model(
model="OWLv2: Base Patch16",
florence2_attn="eager"
)[0]
cache_size_after_first = len(MODEL_CACHE)
# Second load (should use cache)
model2 = loader.load_model(
model="OWLv2: Base Patch16",
florence2_attn="eager"
)[0]
cache_size_after_second = len(MODEL_CACHE)
# Cache should have model
assert cache_size_after_first > 0
# Cache shouldn't grow on second load
assert cache_size_after_second == cache_size_after_first
@@ -0,0 +1,85 @@
"""
Real model integration tests for YOLO-World
These tests download and use actual models - marked as slow
"""
import pytest
import torch
@pytest.mark.real_model
def test_yolo_world_v2_s_load(mock_comfy_environment):
"""Test loading real YOLO-World v2 S (small) model"""
from nodes import GroundingModelLoader
loader = GroundingModelLoader()
# Load smallest YOLO-World model
model_dict = loader.load_model(
model="YOLO-World: v8s (Small)",
florence2_attn="eager"
)[0]
assert model_dict is not None
assert "type" in model_dict
assert model_dict["type"] == "yolo_world"
assert "model" in model_dict
@pytest.mark.real_model
def test_yolo_world_v2_s_detection(mock_comfy_environment, small_image):
"""Test real detection with YOLO-World v2 S model"""
from nodes import GroundingModelLoader, GroundingDetector
# Load model
loader = GroundingModelLoader()
model_dict = loader.load_model(
model="YOLO-World: v8s (Small)",
florence2_attn="eager"
)[0]
# Run detection
detector = GroundingDetector()
bboxes, annotated_img, labels, masks = detector.detect(
model=model_dict,
image=small_image,
prompt="person",
confidence_threshold=0.3,
bbox_output_format="dict_with_data",
output_masks=False
)
# Verify outputs structure
assert bboxes is not None
assert annotated_img is not None
assert annotated_img.shape == small_image.shape
assert isinstance(labels, str)
@pytest.mark.real_model
def test_yolo_world_model_caching(mock_comfy_environment, reset_model_cache):
"""Test that YOLO-World models are properly cached"""
from nodes import GroundingModelLoader
from nodes.utils.cache import MODEL_CACHE
loader = GroundingModelLoader()
# First load
model1 = loader.load_model(
model="YOLO-World: v8s (Small)",
florence2_attn="eager"
)[0]
cache_size_after_first = len(MODEL_CACHE)
# Second load (should use cache)
model2 = loader.load_model(
model="YOLO-World: v8s (Small)",
florence2_attn="eager"
)[0]
cache_size_after_second = len(MODEL_CACHE)
# Cache should have model
assert cache_size_after_first > 0
# Cache shouldn't grow on second load
assert cache_size_after_second == cache_size_after_first
+20
View File
@@ -0,0 +1,20 @@
"""
Unit tests for model caching in ComfyUI-Grounding
"""
import pytest
@pytest.mark.unit
def test_model_cache_can_store_and_retrieve(mock_comfy_environment, reset_model_cache):
"""Test that MODEL_CACHE can store and retrieve models"""
from nodes.utils.cache import MODEL_CACHE
# Test storing a value
test_key = "test_model_key"
test_value = {"model": "test", "type": "grounding_dino"}
MODEL_CACHE[test_key] = test_value
# Test retrieving the value
assert test_key in MODEL_CACHE
assert MODEL_CACHE[test_key] == test_value
+26
View File
@@ -0,0 +1,26 @@
"""
Unit tests for conversion utilities in ComfyUI-Grounding
"""
import pytest
import torch
@pytest.mark.unit
def test_boxes_to_masks_basic(mock_comfy_environment):
"""Test basic boxes_to_masks conversion"""
import numpy as np
from nodes.utils.conversions import boxes_to_masks
# Create test boxes as numpy arrays: [[x1, y1, x2, y2]]
boxes = [[np.array([10, 10, 50, 50]), np.array([60, 60, 100, 100])]]
image_shape = (1, 128, 128, 3)
masks = boxes_to_masks(boxes, image_shape)
# Check output shape: (num_boxes, height, width)
assert masks.shape[0] == 2, "Should have 2 masks"
assert masks.shape[1] == 128, "Mask height should match image height"
assert masks.shape[2] == 128, "Mask width should match image width"
# Check dtype
assert masks.dtype == torch.float32 or masks.dtype == torch.bool
+25
View File
@@ -0,0 +1,25 @@
"""
Unit tests for input validation in ComfyUI-Grounding nodes
"""
import pytest
@pytest.mark.unit
def test_grounding_detector_input_types(mock_comfy_environment):
"""Test GroundingDetector INPUT_TYPES"""
from nodes import GroundingDetector
input_types = GroundingDetector.INPUT_TYPES()
# Check required inputs
required = input_types["required"]
assert "model" in required
assert "image" in required
assert "prompt" in required
assert "confidence_threshold" in required
# Check optional inputs
optional = input_types["optional"]
assert "single_box_mode" in optional
assert "bbox_output_format" in optional
assert "output_masks" in optional
+12
View File
@@ -0,0 +1,12 @@
"""
Unit tests for model registry in ComfyUI-Grounding
"""
import pytest
@pytest.mark.unit
def test_model_registry_count(mock_comfy_environment):
"""Test that we have exactly 19 models registered"""
from nodes.config.bbox_models import MODEL_REGISTRY
assert len(MODEL_REGISTRY) == 19, f"Expected 19 models, found {len(MODEL_REGISTRY)}"
+26
View File
@@ -0,0 +1,26 @@
"""
Unit tests for node registration in ComfyUI-Grounding
"""
import pytest
@pytest.mark.unit
def test_node_class_mappings_exists(mock_comfy_environment):
"""Test that NODE_CLASS_MAPPINGS is defined and contains all expected nodes"""
from nodes import NODE_CLASS_MAPPINGS
expected_nodes = [
"GroundingModelLoader",
"GroundingDetector",
"BboxVisualizer",
"DownloadAndLoadSAM2Model",
"Sam2Segmentation",
"GroundingMaskModelLoader",
"GroundingMaskDetector"
]
assert NODE_CLASS_MAPPINGS is not None, "NODE_CLASS_MAPPINGS is not defined"
for node_name in expected_nodes:
assert node_name in NODE_CLASS_MAPPINGS, f"{node_name} not found in NODE_CLASS_MAPPINGS"
assert NODE_CLASS_MAPPINGS[node_name] is not None, f"{node_name} maps to None"
@@ -1,19 +1,19 @@
{
"id": "b29971c7-6074-4707-895e-9f2360e11e2d",
"id": "605c8e58-85ce-480e-ae26-54cf0f1d8ce7",
"revision": 0,
"last_node_id": 35,
"last_link_id": 48,
"last_node_id": 17,
"last_link_id": 19,
"nodes": [
{
"id": 18,
"id": 3,
"type": "GroundingModelLoader",
"pos": [
761.1567094157901,
37.11377863503034
-324.99609375,
-3.8984375
],
"size": [
272.064453125,
106
270,
82
],
"flags": {},
"order": 0,
@@ -24,43 +24,298 @@
"name": "model",
"type": "GROUNDING_MODEL",
"links": [
27
1
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "c9c72642cd7affb18d5aa54f21ee7d3acf7412eb",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingModelLoader"
},
"widgets_values": [
"Florence-2: Large (0.77B params)",
true,
"eager"
]
},
{
"id": 31,
"type": "LoadImagesFromFolderKJ",
"id": 12,
"type": "DownloadAndLoadSAM2Model",
"pos": [
681.3852212888811,
257.2916550578389
134.1244248881893,
-152.4815917189195
],
"size": [
290.5201171875,
262
270,
130
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"links": [
10
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "DownloadAndLoadSAM2Model"
},
"widgets_values": [
"sam2.1_hiera_large.safetensors",
"single_image",
"cuda",
"fp16"
]
},
{
"id": 14,
"type": "JoinImageWithAlpha",
"pos": [
806.4560194385198,
47.17325913006609
],
"size": [
176.86484375,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 13
},
{
"name": "alpha",
"type": "MASK",
"link": 17
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
19
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "JoinImageWithAlpha"
},
"widgets_values": []
},
{
"id": 11,
"type": "Sam2Segmentation",
"pos": [
507.4435292430558,
34.683721146105555
],
"size": [
272.087890625,
254
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"link": 10
},
{
"name": "image",
"type": "IMAGE",
"link": 11
},
{
"name": "coordinates_positive",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "coordinates_negative",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "bboxes",
"shape": 7,
"type": "BBOX",
"link": 12
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "mask",
"type": "MASK",
"links": [
16
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "Sam2Segmentation"
},
"widgets_values": [
false,
false,
0,
0,
0
]
},
{
"id": 1,
"type": "GroundingDetector",
"pos": [
68.70780892432435,
37.906621234841865
],
"size": [
400,
364
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"link": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 8
}
],
"outputs": [
{
"name": "bboxes",
"type": "BBOX",
"links": [
12
]
},
{
"name": "annotated_image",
"type": "IMAGE",
"links": []
},
{
"name": "labels",
"type": "STRING",
"links": null
},
{
"name": "masks",
"type": "MASK",
"links": null
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingDetector"
},
"widgets_values": [
"main object",
0.3,
false,
true,
"list_only",
false,
42,
42,
42,
"randomize"
]
},
{
"id": 15,
"type": "InvertMask",
"pos": [
634.4085072101292,
-31.37835895949953
],
"size": [
140,
26
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 16
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
17
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "InvertMask"
},
"widgets_values": []
},
{
"id": 9,
"type": "LoadImagesFromFolderKJ",
"pos": [
-319.8692780804565,
159.9365003928532
],
"size": [
290.5201171875,
262
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
41,
43
8,
11,
13
]
},
{
@@ -81,13 +336,13 @@
],
"properties": {
"cnr_id": "comfyui-kjnodes",
"ver": "1.1.8",
"ver": "1.1.9",
"Node name for S&R": "LoadImagesFromFolderKJ"
},
"widgets_values": [
"/workspace/depthy/ComfyUI/custom_nodes/ComfyUI-Grounding/docs/tobatch/",
1200,
1200,
"/workspace/groundingnode/ComfyUI/custom_nodes/ComfyUI-Grounding/assets/tobatch/",
1024,
1024,
"pad",
0,
0,
@@ -95,314 +350,117 @@
]
},
{
"id": 34,
"type": "MaskPreview",
"id": 17,
"type": "PreviewImage",
"pos": [
2238.8322620750596,
-304.2753331354774
1001.6651677560029,
55.417983277011885
],
"size": [
140,
246
330.90445054815405,
258
],
"flags": {},
"order": 6,
"order": 7,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 45
"name": "images",
"type": "IMAGE",
"link": 19
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "MaskPreview"
},
"widgets_values": []
},
{
"id": 32,
"type": "DownloadAndLoadSAM2Model",
"pos": [
1361.2896454187016,
-358.19085905073246
],
"size": [
270,
130
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"links": [
42
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "7bfc6241cba9a88ffc5ec2b837fd30d2af73d95c",
"Node name for S&R": "DownloadAndLoadSAM2Model"
},
"widgets_values": [
"sam2_hiera_large.safetensors",
"single_image",
"cuda",
"fp16"
]
},
{
"id": 30,
"type": "PreviewImage",
"pos": [
1717.3721807161592,
-103.37955289789586
],
"size": [
140,
246
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 39
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 19,
"type": "GroundingDetector",
"pos": [
1147.1057310247927,
-143.3627616665786
],
"size": [
400,
268
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"link": 27
},
{
"name": "image",
"type": "IMAGE",
"link": 41
}
],
"outputs": [
{
"name": "bboxes",
"type": "BBOX",
"links": [
48
]
},
{
"name": "annotated_image",
"type": "IMAGE",
"links": [
39
]
},
{
"name": "labels",
"type": "STRING",
"links": null
},
{
"name": "masks",
"type": "MASK",
"links": [
47
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "c9c72642cd7affb18d5aa54f21ee7d3acf7412eb",
"Node name for S&R": "GroundingDetector"
},
"widgets_values": [
"main object",
0.35,
0.35,
true,
"list_only",
true
]
},
{
"id": 33,
"type": "Sam2Segmentation",
"pos": [
1780.1313139653494,
-413.51370234675784
],
"size": [
272.087890625,
254
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"link": 42
},
{
"name": "image",
"type": "IMAGE",
"link": 43
},
{
"name": "coordinates_positive",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "coordinates_negative",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "bboxes",
"shape": 7,
"type": "BBOX",
"link": 48
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": 47
}
],
"outputs": [
{
"name": "mask",
"type": "MASK",
"links": [
45
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "7bfc6241cba9a88ffc5ec2b837fd30d2af73d95c",
"Node name for S&R": "Sam2Segmentation"
},
"widgets_values": [
true,
false,
2,
0,
0
]
}
],
"links": [
[
27,
18,
1,
3,
0,
19,
1,
0,
"GROUNDING_MODEL"
],
[
39,
19,
8,
9,
0,
1,
30,
0,
"IMAGE"
],
[
41,
31,
0,
19,
1,
"IMAGE"
],
[
42,
32,
10,
12,
0,
33,
11,
0,
"SAM2MODEL"
],
[
43,
31,
11,
9,
0,
33,
11,
1,
"IMAGE"
],
[
45,
33,
12,
1,
0,
34,
0,
"MASK"
],
[
47,
19,
3,
33,
5,
"MASK"
],
[
48,
19,
0,
33,
11,
4,
"BBOX"
],
[
13,
9,
0,
14,
0,
"IMAGE"
],
[
16,
11,
0,
15,
0,
"MASK"
],
[
17,
15,
0,
14,
1,
"MASK"
],
[
19,
14,
0,
17,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9483253215897468,
"scale": 0.9278994587027772,
"offset": [
-802.6149662434965,
550.1068245397717
508.01216790085954,
282.0476732486773
]
},
"frontendVersion": "1.28.8"
@@ -1,117 +1,84 @@
{
"id": "b29971c7-6074-4707-895e-9f2360e11e2d",
"id": "38945c5c-4e77-4d6c-b916-6a2f0a1b6a89",
"revision": 0,
"last_node_id": 30,
"last_link_id": 40,
"last_node_id": 32,
"last_link_id": 39,
"nodes": [
{
"id": 29,
"type": "LoadImage",
"id": 28,
"type": "InvertMask",
"pos": [
691.3480657963805,
244.71705551823334
98.71468314873522,
-76.55255233775446
],
"size": [
274.080078125,
314
140,
26
],
"flags": {},
"order": 0,
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
"inputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
38,
40
]
},
"name": "mask",
"type": "MASK",
"link": 33
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": null
"links": [
34
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png",
"image"
]
"ver": "0.3.67",
"Node name for S&R": "InvertMask"
}
},
{
"id": 28,
"type": "MaskPreview",
"id": 31,
"type": "PreviewAny",
"pos": [
1252.4602357504743,
329.44736978995775
110.50039305540561,
281.573065740487
],
"size": [
140,
246.00000000000003
76
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"name": "source",
"type": "*",
"link": 37
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "MaskPreview"
"ver": "0.3.67",
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 22,
"id": 29,
"type": "PreviewImage",
"pos": [
1937.3573937178255,
228.10370816997101
533.6126667407317,
103.92480629195937
],
"size": [
140,
246.00000000000003
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 32
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 30,
"type": "PreviewImage",
"pos": [
1664.5457193908678,
-114.32244507189236
],
"size": [
140,
246
279.1381383513526,
299.2129823737602
],
"flags": {},
"order": 4,
@@ -120,106 +87,152 @@
{
"name": "images",
"type": "IMAGE",
"link": 39
"link": 35
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.65",
"ver": "0.3.67",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 21,
"type": "BboxVisualizer",
"id": 30,
"type": "PreviewImage",
"pos": [
1608.8053963516986,
258.9104840919287
532.7368358026316,
-228.9107803208185
],
"size": [
210,
78
289.0763407319919,
290.26959173790755
],
"flags": {},
"order": 3,
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 36
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 32,
"type": "LoadImagesFromFolderKJ",
"pos": [
-309.5514298801789,
161.68998793772164
],
"size": [
290.5201171875,
262
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
38,
39
]
},
{
"name": "mask",
"type": "MASK",
"links": null
},
{
"name": "count",
"type": "INT",
"links": null
},
{
"name": "image_path",
"type": "STRING",
"links": null
}
],
"properties": {
"cnr_id": "comfyui-kjnodes",
"ver": "1.1.9",
"Node name for S&R": "LoadImagesFromFolderKJ"
},
"widgets_values": [
"/workspace/groundingnode/ComfyUI/custom_nodes/ComfyUI-Grounding/assets/tobatch",
1024,
1024,
"pad",
0,
0,
false
]
},
{
"id": 26,
"type": "JoinImageWithAlpha",
"pos": [
259.332162129211,
-105.19057150244885
],
"size": [
176.86484375,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 40
"link": 39
},
{
"name": "bboxes",
"type": "BBOX",
"link": 31
"name": "alpha",
"type": "MASK",
"link": 34
}
],
"outputs": [
{
"name": "annotated_image",
"name": "IMAGE",
"type": "IMAGE",
"links": [
32
36
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "c9c72642cd7affb18d5aa54f21ee7d3acf7412eb",
"Node name for S&R": "BboxVisualizer"
},
"widgets_values": [
3
]
},
{
"id": 18,
"type": "GroundingModelLoader",
"pos": [
761.1567094157901,
37.11377863503034
],
"size": [
272.064453125,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"links": [
27
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "c9c72642cd7affb18d5aa54f21ee7d3acf7412eb",
"Node name for S&R": "GroundingModelLoader"
},
"widgets_values": [
"GroundingDINO: SwinB (938MB)",
true,
"eager"
]
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "JoinImageWithAlpha"
}
},
{
"id": 19,
"type": "GroundingDetector",
"type": "GroundingMaskDetector",
"pos": [
1147.1057310247927,
-143.3627616665786
106.9620845129382,
-14.471635853967237
],
"size": [
400,
268
248
],
"flags": {},
"order": 2,
@@ -227,8 +240,8 @@
"inputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"link": 27
"type": "MASK_MODEL",
"link": 21
},
{
"name": "image",
@@ -237,28 +250,23 @@
}
],
"outputs": [
{
"name": "bboxes",
"type": "BBOX",
"links": [
31
]
},
{
"name": "annotated_image",
"type": "IMAGE",
"links": [
39
]
},
{
"name": "labels",
"type": "STRING",
"links": null
},
{
"name": "masks",
"type": "MASK",
"links": [
33
]
},
{
"name": "overlaid_mask",
"type": "IMAGE",
"links": [
35
]
},
{
"name": "text",
"type": "STRING",
"links": [
37
]
@@ -266,55 +274,105 @@
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "c9c72642cd7affb18d5aa54f21ee7d3acf7412eb",
"Node name for S&R": "GroundingDetector"
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingMaskDetector"
},
"widgets_values": [
"character",
0.1,
0.01,
true,
"dict_with_data",
true
"segment the main object in the image.",
0.3,
3,
2048,
1644266233,
"randomize"
]
},
{
"id": 18,
"type": "GroundingMaskModelLoader",
"pos": [
-299.81672209985305,
-40.87788547709978
],
"size": [
270,
82
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "model",
"type": "MASK_MODEL",
"links": [
21
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingMaskModelLoader"
},
"widgets_values": [
"SA2VA: 1B",
"bf16"
]
}
],
"links": [
[
27,
21,
18,
0,
19,
0,
"GROUNDING_MODEL"
"MASK_MODEL"
],
[
31,
33,
19,
0,
21,
1,
"BBOX"
28,
0,
"MASK"
],
[
32,
21,
34,
28,
0,
22,
26,
1,
"MASK"
],
[
35,
19,
1,
29,
0,
"IMAGE"
],
[
36,
26,
0,
30,
0,
"IMAGE"
],
[
37,
19,
3,
28,
2,
31,
0,
"MASK"
"*"
],
[
38,
29,
32,
0,
19,
1,
@@ -322,17 +380,9 @@
],
[
39,
19,
1,
30,
32,
0,
"IMAGE"
],
[
40,
29,
0,
21,
26,
0,
"IMAGE"
]
@@ -341,13 +391,13 @@
"config": {},
"extra": {
"ds": {
"scale": 0.8390550503631276,
"scale": 1.2894499156462411,
"offset": [
-372.4858306734141,
284.24946587737446
386.94731260454324,
226.3186553038392
]
},
"frontendVersion": "1.28.7"
"frontendVersion": "1.28.8"
},
"version": 0.4
}
+469
View File
@@ -0,0 +1,469 @@
{
"id": "23662dd8-0bfd-4ab0-833e-1775efda6729",
"revision": 0,
"last_node_id": 17,
"last_link_id": 19,
"nodes": [
{
"id": 3,
"type": "GroundingModelLoader",
"pos": [
-324.99609375,
-3.8984375
],
"size": [
270,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"links": [
1
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingModelLoader"
},
"widgets_values": [
"Florence-2: Large (0.77B params)",
"eager"
]
},
{
"id": 12,
"type": "DownloadAndLoadSAM2Model",
"pos": [
134.1244248881893,
-152.4815917189195
],
"size": [
270,
130
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"links": [
10
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "DownloadAndLoadSAM2Model"
},
"widgets_values": [
"sam2.1_hiera_large.safetensors",
"single_image",
"cuda",
"fp16"
]
},
{
"id": 14,
"type": "JoinImageWithAlpha",
"pos": [
806.4560194385198,
47.17325913006609
],
"size": [
176.86484375,
46
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 13
},
{
"name": "alpha",
"type": "MASK",
"link": 17
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
19
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "JoinImageWithAlpha"
},
"widgets_values": []
},
{
"id": 11,
"type": "Sam2Segmentation",
"pos": [
507.4435292430558,
34.683721146105555
],
"size": [
272.087890625,
254
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "sam2_model",
"type": "SAM2MODEL",
"link": 10
},
{
"name": "image",
"type": "IMAGE",
"link": 11
},
{
"name": "coordinates_positive",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "coordinates_negative",
"shape": 7,
"type": "STRING",
"link": null
},
{
"name": "bboxes",
"shape": 7,
"type": "BBOX",
"link": 12
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "mask",
"type": "MASK",
"links": [
16
]
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "Sam2Segmentation"
},
"widgets_values": [
false,
false,
0,
0,
0
]
},
{
"id": 1,
"type": "GroundingDetector",
"pos": [
68.70780892432435,
37.906621234841865
],
"size": [
400,
364
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "GROUNDING_MODEL",
"link": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 8
}
],
"outputs": [
{
"name": "bboxes",
"type": "BBOX",
"links": [
12
]
},
{
"name": "annotated_image",
"type": "IMAGE",
"links": []
},
{
"name": "labels",
"type": "STRING",
"links": null
},
{
"name": "masks",
"type": "MASK",
"links": null
}
],
"properties": {
"aux_id": "PozzettiAndrea/ComfyUI-Grounding",
"ver": "a947f4961c39cc150a98738fa83a0494e52f3de0",
"Node name for S&R": "GroundingDetector"
},
"widgets_values": [
"main object",
0.3,
false,
true,
"list_only",
false,
2793006679,
42,
42,
"randomize"
]
},
{
"id": 15,
"type": "InvertMask",
"pos": [
634.4085072101292,
-31.37835895949953
],
"size": [
140,
26
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 16
}
],
"outputs": [
{
"name": "MASK",
"type": "MASK",
"links": [
17
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "InvertMask"
},
"widgets_values": []
},
{
"id": 9,
"type": "LoadImagesFromFolderKJ",
"pos": [
-319.8692780804565,
159.9365003928532
],
"size": [
290.5201171875,
262
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
8,
11,
13
]
},
{
"name": "mask",
"type": "MASK",
"links": null
},
{
"name": "count",
"type": "INT",
"links": null
},
{
"name": "image_path",
"type": "STRING",
"links": null
}
],
"properties": {
"cnr_id": "comfyui-kjnodes",
"ver": "1.1.9",
"Node name for S&R": "LoadImagesFromFolderKJ"
},
"widgets_values": [
"/workspace/groundingnode/ComfyUI/custom_nodes/ComfyUI-Grounding/assets/tobatch/",
1024,
1024,
"pad",
0,
0,
false
]
},
{
"id": 17,
"type": "PreviewImage",
"pos": [
1001.6651677560029,
55.417983277011885
],
"size": [
330.90445054815405,
258
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 19
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.67",
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
1,
3,
0,
1,
0,
"GROUNDING_MODEL"
],
[
8,
9,
0,
1,
1,
"IMAGE"
],
[
10,
12,
0,
11,
0,
"SAM2MODEL"
],
[
11,
9,
0,
11,
1,
"IMAGE"
],
[
12,
1,
0,
11,
4,
"BBOX"
],
[
13,
9,
0,
14,
0,
"IMAGE"
],
[
16,
11,
0,
15,
0,
"MASK"
],
[
17,
15,
0,
14,
1,
"MASK"
],
[
19,
14,
0,
17,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9278994587027772,
"offset": [
508.01216790085954,
282.0476732486773
]
},
"frontendVersion": "1.28.8"
},
"version": 0.4
}