212 lines
9.2 KiB
Python
212 lines
9.2 KiB
Python
# ComfyUI-RMBG
|
|
# VRMBG Video Background Removal Node
|
|
#
|
|
# Model License Notice:
|
|
# - VRMBG-3.0: Provided by BRIA AI (https://huggingface.co/briaai/VRMBG-3.0)
|
|
# Please respect the original BRIA AI model license and usage terms.
|
|
#
|
|
# This integration script follows GPL-3.0 License.
|
|
# Source: https://github.com/1038lab/ComfyUI-RMBG
|
|
|
|
import os
|
|
from typing import Tuple, Optional
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import numpy as np
|
|
from PIL import Image, ImageFilter
|
|
from torchvision import transforms
|
|
import folder_paths
|
|
import comfy.model_management as mm
|
|
|
|
from AILab_utils import get_device, clean_vram, unload_model as release_model, tensor2pil, pil2tensor
|
|
|
|
device = get_device()
|
|
folder_paths.add_model_folder_path("rmbg", os.path.join(folder_paths.models_dir, "RMBG"))
|
|
|
|
MODEL_SUBDIR = os.path.join(folder_paths.models_dir, "RMBG", "VRMBG-3.0")
|
|
REPO_URL = "https://huggingface.co/briaai/VRMBG-3.0"
|
|
|
|
INFER_SIZE = 1024
|
|
REQUIRED_FILES = ["config.json", "model.safetensors", "model.py", "vrmbg3_config.py"]
|
|
|
|
|
|
class AILab_VRMBG:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", {"tooltip": "Batch of consecutive video frames [B, H, W, C] for temporal background removal."}),
|
|
},
|
|
"optional": {
|
|
"reset_state": ("BOOLEAN", {"default": True, "tooltip": "Reset temporal prior state at the beginning of this batch. Keep True for standalone clips, or False when feeding consecutive chunks."}),
|
|
"mask_blur": ("INT", {"default": 0, "min": 0, "max": 64, "step": 1, "tooltip": "Gaussian blur radius for softening mask edges."}),
|
|
"mask_offset": ("INT", {"default": 0, "min": -64, "max": 64, "step": 1, "tooltip": "Expand (positive) or shrink (negative) the mask boundary in pixels."}),
|
|
"invert_output": ("BOOLEAN", {"default": False, "tooltip": "Invert the output mask."}),
|
|
"unload_model": ("BOOLEAN", {"default": False, "tooltip": "Unload model weights from VRAM immediately after execution."}),
|
|
"background": (["Alpha", "Color"], {"default": "Alpha", "tooltip": "Output format: Alpha for transparent cutout, or Color to composite against solid background."}),
|
|
"background_color": ("COLORCODE", {"default": "#222222", "tooltip": "Hex background color used when background mode is set to Color."}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "IMAGE")
|
|
RETURN_NAMES = ("IMAGE", "MASK", "MASK_IMAGE")
|
|
FUNCTION = "process"
|
|
CATEGORY = "🧪AILab/🧽RMBG"
|
|
|
|
def __init__(self):
|
|
self.model = None
|
|
self.prev_rgb_t = None
|
|
self.prev_alpha = None
|
|
self.target_device = device
|
|
self.normalize = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
|
|
|
|
def _find_model_dir(self) -> str:
|
|
for base in folder_paths.get_folder_paths("rmbg"):
|
|
cand = os.path.join(base, "VRMBG-3.0")
|
|
if os.path.isdir(cand) and all(os.path.exists(os.path.join(cand, f)) for f in REQUIRED_FILES):
|
|
return cand
|
|
return MODEL_SUBDIR
|
|
|
|
def _ensure_model(self):
|
|
if self.model is not None:
|
|
return self.model
|
|
|
|
model_dir = self._find_model_dir()
|
|
missing_files = [f for f in REQUIRED_FILES if not os.path.exists(os.path.join(model_dir, f))]
|
|
|
|
if missing_files:
|
|
raise FileNotFoundError(
|
|
f"[VRMBG-3.0] Model files missing: {missing_files}\n"
|
|
f"Automatic download has been disabled. Please download the model manually and place all required files in:\n"
|
|
f" {model_dir}\n\n"
|
|
f"Required files:\n"
|
|
f" - " + "\n - ".join(REQUIRED_FILES) + "\n\n"
|
|
f"Download link:\n"
|
|
f" - {REPO_URL}"
|
|
)
|
|
|
|
from transformers import AutoModelForImageSegmentation, PreTrainedModel
|
|
if not hasattr(PreTrainedModel, "all_tied_weights_keys"):
|
|
PreTrainedModel.all_tied_weights_keys = {}
|
|
|
|
try:
|
|
self.model = AutoModelForImageSegmentation.from_pretrained(
|
|
model_dir,
|
|
trust_remote_code=True,
|
|
local_files_only=True
|
|
).eval()
|
|
except Exception as e:
|
|
raise RuntimeError(f"[VRMBG-3.0] Failed to load local model from '{model_dir}': {e}")
|
|
|
|
if not hasattr(self.model, "all_tied_weights_keys"):
|
|
self.model.all_tied_weights_keys = {}
|
|
|
|
if torch.cuda.is_available() and str(self.target_device).startswith("cuda"):
|
|
self.model = self.model.half().to(self.target_device)
|
|
else:
|
|
self.model = self.model.to(self.target_device)
|
|
|
|
return self.model
|
|
|
|
def process(self, images, reset_state=True, mask_blur=0, mask_offset=0,
|
|
invert_output=False, unload_model=False,
|
|
background="Alpha", background_color="#222222", **kwargs):
|
|
do_unload = kwargs.get("unload_model", unload_model)
|
|
model = self._ensure_model()
|
|
|
|
model_dtype = next(model.parameters()).dtype
|
|
|
|
if reset_state or self.prev_rgb_t is None or self.prev_alpha is None:
|
|
self.prev_rgb_t = torch.zeros(3, INFER_SIZE, INFER_SIZE, device=self.target_device, dtype=model_dtype)
|
|
self.prev_alpha = torch.zeros(1, INFER_SIZE, INFER_SIZE, device=self.target_device, dtype=model_dtype)
|
|
|
|
processed_images = []
|
|
processed_masks = []
|
|
|
|
try:
|
|
for single_img in images:
|
|
orig_h, orig_w = single_img.shape[0], single_img.shape[1]
|
|
pil_img = tensor2pil(single_img).convert("RGB")
|
|
|
|
# Permute [H, W, C] -> [C, H, W]
|
|
img_chw = single_img.permute(2, 0, 1).to(self.target_device, dtype=torch.float32)
|
|
# Resize to 1024x1024
|
|
img_resized = F.interpolate(img_chw.unsqueeze(0), size=(INFER_SIZE, INFER_SIZE), mode="bilinear", align_corners=False).squeeze(0)
|
|
current_t = self.normalize(img_resized).to(dtype=model_dtype)
|
|
|
|
# Pair with temporal prior: [current_RGB, prev_RGB * prev_alpha]
|
|
temporal_prior = (self.prev_rgb_t * self.prev_alpha).to(dtype=model_dtype)
|
|
paired = torch.cat([current_t, temporal_prior], dim=0).unsqueeze(0)
|
|
paired = paired.contiguous(memory_format=torch.channels_last)
|
|
|
|
with torch.no_grad():
|
|
raw_out = model(paired)
|
|
if isinstance(raw_out, (list, tuple)):
|
|
out_tensor = raw_out[-1]
|
|
else:
|
|
out_tensor = raw_out
|
|
pred_alpha = out_tensor.sigmoid().squeeze(0) # [1, 1024, 1024]
|
|
|
|
# Update autoregressive temporal states for next frame
|
|
self.prev_rgb_t = current_t
|
|
self.prev_alpha = pred_alpha
|
|
|
|
# Resize alpha back to native resolution
|
|
alpha_resized = F.interpolate(pred_alpha.unsqueeze(0).float(), size=(orig_h, orig_w), mode="bilinear", align_corners=False).squeeze(0).squeeze(0)
|
|
alpha_np = alpha_resized.detach().cpu().numpy()
|
|
|
|
mask_img = Image.fromarray((np.clip(alpha_np, 0.0, 1.0) * 255.0).astype(np.uint8), mode="L")
|
|
|
|
if mask_offset != 0:
|
|
offset_k = abs(mask_offset) * 2 + 1
|
|
filter_type = ImageFilter.MaxFilter if mask_offset > 0 else ImageFilter.MinFilter
|
|
mask_img = mask_img.filter(filter_type(offset_k))
|
|
|
|
if mask_blur > 0:
|
|
mask_img = mask_img.filter(ImageFilter.GaussianBlur(radius=mask_blur))
|
|
|
|
if invert_output:
|
|
mask_np_inv = 255.0 - np.array(mask_img, dtype=np.float32)
|
|
mask_img = Image.fromarray(np.clip(mask_np_inv, 0, 255).astype(np.uint8), mode="L")
|
|
|
|
mask_tensor = pil2tensor(mask_img)
|
|
processed_masks.append(mask_tensor)
|
|
|
|
if background == "Alpha":
|
|
cutout = Image.new("RGBA", (orig_w, orig_h), (0, 0, 0, 0))
|
|
cutout.paste(pil_img, (0, 0), mask_img)
|
|
processed_images.append(pil2tensor(cutout))
|
|
else:
|
|
bg_hex = background_color.lstrip("#")
|
|
if len(bg_hex) == 6:
|
|
bg_rgb = tuple(int(bg_hex[i:i+2], 16) for i in (0, 2, 4))
|
|
else:
|
|
bg_rgb = (34, 34, 34)
|
|
|
|
bg_canvas = Image.new("RGB", (orig_w, orig_h), bg_rgb)
|
|
bg_canvas.paste(pil_img, (0, 0), mask_img)
|
|
processed_images.append(pil2tensor(bg_canvas))
|
|
|
|
out_image = torch.cat(processed_images, dim=0)
|
|
out_mask = torch.cat(processed_masks, dim=0)
|
|
out_mask_image = out_mask.unsqueeze(-1).expand(-1, -1, -1, 3)
|
|
|
|
return (out_image, out_mask, out_mask_image)
|
|
|
|
finally:
|
|
if do_unload:
|
|
release_model(self, "model")
|
|
self.prev_rgb_t = None
|
|
self.prev_alpha = None
|
|
else:
|
|
clean_vram()
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"AILab_VRMBG": AILab_VRMBG
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"AILab_VRMBG": "VRMBG (Video Background Removal)"
|
|
}
|