Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions py/AILab_BiRefNet.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import sys
import importlib.util
from safetensors.torch import load_file
import cv2

device = "cuda" if torch.cuda.is_available() else "cpu"

Expand Down Expand Up @@ -198,19 +197,20 @@ def handle_model_error(message):
raise RuntimeError(message)

def refine_foreground(image_bchw, masks_b1hw):
import cv2 # Lazy import: cv2 is heavy (~0.4s) and only needed for foreground refinement
b, c, h, w = image_bchw.shape
if b != masks_b1hw.shape[0]:
raise ValueError("images and masks must have the same batch size")

image_np = image_bchw.cpu().numpy()
mask_np = masks_b1hw.cpu().numpy()

refined_fg = []
for i in range(b):
mask = mask_np[i, 0]
mask = mask_np[i, 0]
thresh = 0.45
mask_binary = (mask > thresh).astype(np.float32)

edge_blur = cv2.GaussianBlur(mask_binary, (3, 3), 0)
transition_mask = np.logical_and(mask > 0.05, mask < 0.95)

Expand Down
2 changes: 1 addition & 1 deletion py/AILab_BodySegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import numpy as np
from typing import Tuple, Union
from PIL import Image, ImageFilter
import onnxruntime
import folder_paths
from huggingface_hub import hf_hub_download
import shutil
Expand Down Expand Up @@ -135,6 +134,7 @@ def segment_body(self, images, mask_blur=0, mask_offset=0, background="Alpha", b

# Load model if needed
if self.model is None:
import onnxruntime # Lazy: onnxruntime is heavy and only needed when node executes
self.model = onnxruntime.InferenceSession(
os.path.join(self.cache_dir, self.model_file)
)
Expand Down
2 changes: 1 addition & 1 deletion py/AILab_ClothSegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import numpy as np
from typing import Tuple, Union
from PIL import Image, ImageFilter
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
import folder_paths
from huggingface_hub import hf_hub_download
import shutil
Expand Down Expand Up @@ -155,6 +154,7 @@ def segment_clothes(self, images, process_res=1024, mask_blur=0, mask_offset=0,

# Load model if needed
if self.processor is None:
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation # Lazy: transformers is ~3.7s to import
self.processor = SegformerImageProcessor.from_pretrained(self.cache_dir)
self.model = AutoModelForSemanticSegmentation.from_pretrained(self.cache_dir)
self.model.eval()
Expand Down
2 changes: 1 addition & 1 deletion py/AILab_FaceSegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import numpy as np
from typing import Tuple, Union
from PIL import Image, ImageFilter
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
import folder_paths
from huggingface_hub import hf_hub_download
import shutil
Expand Down Expand Up @@ -155,6 +154,7 @@ def segment_face(self, images, process_res=512, mask_blur=0, mask_offset=0, back

# Load model if needed
if self.processor is None:
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation # Lazy: transformers is ~3.7s to import
self.processor = SegformerImageProcessor.from_pretrained(self.cache_dir)
self.model = AutoModelForSemanticSegmentation.from_pretrained(self.cache_dir)
self.model.eval()
Expand Down
2 changes: 1 addition & 1 deletion py/AILab_FashionSegment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import numpy as np
from typing import Tuple, Union
from PIL import Image, ImageFilter
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
import folder_paths
from huggingface_hub import hf_hub_download
import shutil
Expand Down Expand Up @@ -251,6 +250,7 @@ def segment_fashion(self, images, accessories_options=None, process_res=512, mas

# Load model if needed
if self.processor is None:
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation # Lazy: transformers is ~3.7s to import
self.processor = SegformerImageProcessor.from_pretrained(self.cache_dir)
self.model = AutoModelForSemanticSegmentation.from_pretrained(self.cache_dir)
self.model.eval()
Expand Down
17 changes: 9 additions & 8 deletions py/AILab_Florence2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
from typing import Any, Dict, List, Tuple
from unittest.mock import patch

import matplotlib

matplotlib.use("Agg")
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
import torch
import torchvision.transforms.functional as TF
Expand All @@ -17,9 +12,6 @@
import comfy.model_management as mm
from comfy.utils import ProgressBar
import folder_paths
import transformers
from transformers import AutoModelForCausalLM, AutoProcessor
from transformers.dynamic_module_utils import get_imports

MODEL_DIR = os.path.join(folder_paths.models_dir, "LLM")
os.makedirs(MODEL_DIR, exist_ok=True)
Expand Down Expand Up @@ -64,6 +56,7 @@


def _fixed_get_imports(filename):
from transformers.dynamic_module_utils import get_imports # Lazy: avoid top-level transformers import (~3.7s)
try:
if not str(filename).endswith("modeling_florence2.py"):
return get_imports(filename)
Expand Down Expand Up @@ -148,6 +141,9 @@ def _ensure_weights(self, model_name: str) -> str:
return target

def _get_model(self, model_name: str, precision: str, attention: str) -> Dict[str, Any]:
import transformers # Lazy: transformers is ~3.7s to import
from transformers import AutoModelForCausalLM, AutoProcessor

key = (model_name, precision, attention)
if key in self.MODEL_CACHE:
return self.MODEL_CACHE[key]
Expand Down Expand Up @@ -194,6 +190,11 @@ def _draw_regions(
fill_mask: bool,
select_filter: List[str],
) -> Tuple[torch.Tensor, torch.Tensor]:
import matplotlib # Lazy: matplotlib is ~0.4s to import
matplotlib.use("Agg")
import matplotlib.patches as patches
import matplotlib.pyplot as plt

width, height = image_pil.size
fig, ax = plt.subplots(figsize=(width / 100, height / 100), dpi=100)
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
Expand Down
15 changes: 6 additions & 9 deletions py/AILab_ImageMaskTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
import numpy as np
import hashlib
import torch
import cv2
import re
from nodes import MAX_RESOLUTION
from comfy.utils import common_upscale
Expand All @@ -60,7 +59,6 @@
import torch.nn.functional as F
from comfy import model_management
from comfy_extras.nodes_mask import ImageCompositeMasked
from scipy import ndimage
from AILab_utils import (
tensor2pil,
pil2tensor,
Expand Down Expand Up @@ -409,6 +407,7 @@ def INPUT_TYPES(cls):

def fill_mask_region(self, mask_pil):
"""Fill holes in the mask"""
import cv2 # Lazy import: cv2 is heavy (~0.4s) and only needed for hole filling
mask_np = np.array(mask_pil)
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
filled_mask = np.zeros_like(mask_np)
Expand All @@ -425,6 +424,7 @@ def process_mask(self, mask, sensitivity=1.0, mask_blur=0, mask_offset=0, smooth
m = torch.clamp(m, 0, 1)

if smooth > 0:
from scipy import ndimage # Lazy import: scipy is heavy and only needed for smoothing
mask_np = m.cpu().numpy()
binary_mask = (mask_np > 0.5).astype(np.float32)
blurred_mask = ndimage.gaussian_filter(binary_mask, sigma=smooth)
Expand Down Expand Up @@ -547,7 +547,7 @@ def get_folder_list(cls):
input_dir = folder_paths.get_input_directory()
os.makedirs(input_dir, exist_ok=True)
return [f for f in os.listdir(input_dir) if os.path.isdir(os.path.join(input_dir, f))]

def download_image(self, url):
try:
import requests
Expand All @@ -557,17 +557,14 @@ def download_image(self, url):
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

response = requests.get(url, timeout=10, headers=headers, allow_redirects=True)
response = requests.get(url, stream=True, timeout=10, headers=headers)
if response.status_code != 200:
raise ValueError(f"Failed to download image from URL: {url}, status code: {response.status_code}")

img = Image.open(BytesIO(response.content))
img.load()
return img
return Image.open(BytesIO(response.content))

except Exception as e:
print(f"Error downloading image from URL: {str(e)}")
raise
raise e

def get_image(self, image_path_or_URL="", image=""):
if not image_path_or_URL and (not image or image == ""):
Expand Down
12 changes: 6 additions & 6 deletions py/AILab_RMBG.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
import shutil
import sys
import importlib.util
from transformers import AutoModelForImageSegmentation
import cv2
import types

device = "cuda" if torch.cuda.is_available() else "cpu"
Expand Down Expand Up @@ -220,6 +218,7 @@ def load_model(self, model_name):
except Exception as modern_e:
print(f"[RMBG INFO] Using standard transformers loading (fallback mode)...")
try:
from transformers import AutoModelForImageSegmentation # Lazy: transformers is ~3.7s to import
self.model = AutoModelForImageSegmentation.from_pretrained(
cache_dir,
trust_remote_code=True,
Expand Down Expand Up @@ -474,19 +473,20 @@ def process_image(self, images, model_name, params):
handle_model_error(f"Error in BEN2 processing: {str(e)}")

def refine_foreground(image_bchw, masks_b1hw):
import cv2 # Lazy import: cv2 is heavy (~0.4s) and only needed for foreground refinement
b, c, h, w = image_bchw.shape
if b != masks_b1hw.shape[0]:
raise ValueError("images and masks must have the same batch size")

image_np = image_bchw.cpu().numpy()
mask_np = masks_b1hw.cpu().numpy()

refined_fg = []
for i in range(b):
mask = mask_np[i, 0]
mask = mask_np[i, 0]
thresh = 0.45
mask_binary = (mask > thresh).astype(np.float32)

edge_blur = cv2.GaussianBlur(mask_binary, (3, 3), 0)
transition_mask = np.logical_and(mask > 0.05, mask < 0.95)

Expand Down
32 changes: 21 additions & 11 deletions py/AILab_SAM2Segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@
from hydra import initialize_config_dir
from hydra.core.global_hydra import GlobalHydra

try:
from groundingdino.util.slconfig import SLConfig
from groundingdino.models import build_model
from groundingdino.util.utils import clean_state_dict
from groundingdino.util import box_ops
from groundingdino.datasets.transforms import Compose, RandomResize, ToTensor, Normalize
GROUNDINGDINO_AVAILABLE = True
except ImportError:
GROUNDINGDINO_AVAILABLE = False
print("Warning: GroundingDINO not available. Text prompts will use fallback method.")
# GroundingDINO imports are deferred to first use (segment_v2) to avoid ~12.5s import at startup.
# The GROUNDINGDINO_AVAILABLE flag is checked lazily on first invocation.
GROUNDINGDINO_AVAILABLE = None # Tri-state: None = not yet checked, True/False = checked

current_dir = Path(__file__).resolve().parent
repo_root = current_dir.parent
Expand Down Expand Up @@ -249,8 +242,25 @@ def load_sam2(self, model_name, device="Auto"):
return self.sam2_model_cache[cache_key]

def segment_v2(self, image, prompt, sam2_model, dino_model, device, threshold=0.35,
mask_blur=0, mask_offset=0, background="Alpha",
mask_blur=0, mask_offset=0, background="Alpha",
background_color="#222222", invert_output=False):
# Lazy import groundingdino (~12.5s) -- only when the node is actually executed
global GROUNDINGDINO_AVAILABLE
if GROUNDINGDINO_AVAILABLE is None:
try:
from groundingdino.util.slconfig import SLConfig # noqa: F401
GROUNDINGDINO_AVAILABLE = True
except ImportError:
GROUNDINGDINO_AVAILABLE = False
print("Warning: GroundingDINO not available. Text prompts will use fallback method.")
if not GROUNDINGDINO_AVAILABLE:
raise RuntimeError("GroundingDINO is required for SAM2Segment but is not installed.")
from groundingdino.util.slconfig import SLConfig
from groundingdino.models import build_model
from groundingdino.util.utils import clean_state_dict
from groundingdino.util import box_ops
from groundingdino.datasets.transforms import Compose, RandomResize, ToTensor, Normalize

device_obj = comfy.model_management.get_torch_device()

# Process batch images
Expand Down
17 changes: 10 additions & 7 deletions py/AILab_SDMatte.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,8 @@ class MockComfy:
SAFETENSORS_AVAILABLE = False
print("Warning: safetensors not available. Will use torch.load for model loading.")

try:
import diffusers
import transformers
DIFFUSERS_AVAILABLE = True
except ImportError:
DIFFUSERS_AVAILABLE = False
print("Warning: diffusers/transformers not available. SDMatte functionality will be limited.")
# Lazy: diffusers/transformers availability is checked on first use to avoid ~3.7s import at startup
DIFFUSERS_AVAILABLE = None # Tri-state: None = not yet checked

current_dir = Path(__file__).resolve().parent
repo_root = current_dir.parent
Expand Down Expand Up @@ -251,6 +246,14 @@ def load_sdmatte_model(self, model_name, device="Auto"):
torch.cuda.empty_cache()

if cache_key not in self.model_cache:
global DIFFUSERS_AVAILABLE
if DIFFUSERS_AVAILABLE is None:
try:
import diffusers # noqa: F401
import transformers # noqa: F401
DIFFUSERS_AVAILABLE = True
except ImportError:
DIFFUSERS_AVAILABLE = False
if not DIFFUSERS_AVAILABLE:
raise ImportError("diffusers and transformers are required for SDMatte functionality")

Expand Down
14 changes: 8 additions & 6 deletions py/AILab_SegmentV2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@

import folder_paths
from segment_anything import sam_model_registry, SamPredictor
from groundingdino.util.slconfig import SLConfig
from groundingdino.models import build_model
from groundingdino.util.utils import clean_state_dict
from groundingdino.util import box_ops
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection

from AILab_ImageMaskTools import pil2tensor, tensor2pil

Expand Down Expand Up @@ -107,6 +102,7 @@ def hex_to_rgba(hex_color):
return rgba_image

def get_groundingdino_model(device):
from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection # Lazy: transformers is ~3.7s to import
processor = AutoProcessor.from_pretrained("IDEA-Research/grounding-dino-tiny")
model = AutoModelForZeroShotObjectDetection.from_pretrained("IDEA-Research/grounding-dino-tiny").to(device)
return processor, model
Expand Down Expand Up @@ -163,8 +159,14 @@ def __init__(self):
self.sam_model_cache = {}

def segment_v2(self, image, prompt, sam_model, dino_model, threshold=0.30,
mask_blur=0, mask_offset=0, background="Alpha",
mask_blur=0, mask_offset=0, background="Alpha",
background_color="#222222", invert_output=False):
# Lazy import groundingdino (~12.5s) -- only when the node is actually executed
from groundingdino.util.slconfig import SLConfig
from groundingdino.models import build_model
from groundingdino.util.utils import clean_state_dict
from groundingdino.util import box_ops

device = "cuda" if torch.cuda.is_available() else "cpu"

batch_size = image.shape[0] if len(image.shape) == 4 else 1
Expand Down