Add Grow Blur Mask MXD node and bump version
Introduce a new Grow Blur Mask MXD node that expands/contracts a mask and blurs only the expanded ring while keeping the core solid. The node normalizes mask inputs (supports HxW or BxHxW), handles positive grow_blur by dilating and blurring the ring (Gaussian blur via PIL.ImageFilter) and negative values by eroding. Implementation uses torch.nn.functional for morphological ops and processes blur on CPU per-batch item, clamping outputs to [0,1]. Also update NODE_CLASS_MAPPINGS and NODE_DISPLAY_NAME_MAPPINGS to register the new node, and bump package metadata in pyproject.toml (name capitalized to "MaxedOut" and version -> 1.6.8). Files changed: maxedoutnodes.py, pyproject.toml.
This commit is contained in:
+89
-1
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
import torch, math, comfy, os, folder_paths, node_helpers, comfy.model_management, comfy.utils, json, hashlib, re
|
||||
import torch.nn.functional as F
|
||||
from comfy.comfy_types import IO, ComfyNodeABC, InputTypeDict
|
||||
import numpy as np
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
from PIL import Image, ImageOps, ImageSequence, ImageFilter
|
||||
from comfy_api.latest import io
|
||||
|
||||
########################################################################################################################
|
||||
@@ -774,6 +775,91 @@ class LatentHalfMasks:
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
# Grow Blur Mask MXD (single-image friendly)
|
||||
class GrowBlurMaskMXD:
|
||||
DESCRIPTION = """Expand or contract a mask and blur only the expanded ring (core stays solid)."""
|
||||
TITLE = "Grow Blur Mask MXD"
|
||||
CATEGORY = "MXD/Mask"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"grow_blur": ("INT", {"default": 0, "min": -64, "max": 64, "step": 1}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "run"
|
||||
|
||||
def _normalize_mask(self, mask: torch.Tensor) -> torch.Tensor:
|
||||
if not isinstance(mask, torch.Tensor):
|
||||
raise ValueError("GrowBlurMaskMXD: mask must be a torch.Tensor.")
|
||||
|
||||
if mask.dim() == 2:
|
||||
mask = mask.unsqueeze(0)
|
||||
elif mask.dim() == 4 and mask.shape[-1] == 1:
|
||||
mask = mask.squeeze(-1)
|
||||
|
||||
if mask.dim() != 3:
|
||||
raise ValueError("GrowBlurMaskMXD: mask must have shape (H,W) or (B,H,W).")
|
||||
|
||||
return mask.float().clamp(0.0, 1.0)
|
||||
|
||||
def _dilate(self, mask: torch.Tensor, radius: int) -> torch.Tensor:
|
||||
if radius <= 0:
|
||||
return mask
|
||||
x = mask.unsqueeze(1)
|
||||
k = 2 * radius + 1
|
||||
y = F.max_pool2d(x, kernel_size=k, stride=1, padding=radius)
|
||||
return y.squeeze(1)
|
||||
|
||||
def _erode(self, mask: torch.Tensor, radius: int) -> torch.Tensor:
|
||||
if radius <= 0:
|
||||
return mask
|
||||
x = mask.unsqueeze(1)
|
||||
k = 2 * radius + 1
|
||||
y = 1.0 - F.max_pool2d(1.0 - x, kernel_size=k, stride=1, padding=radius)
|
||||
return y.squeeze(1)
|
||||
|
||||
def _blur_ring(self, ring: torch.Tensor, radius: int, device: torch.device) -> torch.Tensor:
|
||||
if radius <= 0:
|
||||
return ring
|
||||
ring_cpu = ring.detach().cpu().numpy()
|
||||
blurred = []
|
||||
for i in range(ring_cpu.shape[0]):
|
||||
arr = (ring_cpu[i] * 255.0).astype(np.uint8)
|
||||
pil = Image.fromarray(arr, mode="L")
|
||||
pil = pil.filter(ImageFilter.GaussianBlur(radius))
|
||||
out = np.array(pil).astype(np.float32) / 255.0
|
||||
blurred.append(torch.from_numpy(out))
|
||||
blurred_t = torch.stack(blurred, dim=0).to(device)
|
||||
return blurred_t.clamp(0.0, 1.0)
|
||||
|
||||
def run(self, mask, grow_blur):
|
||||
mask = self._normalize_mask(mask)
|
||||
device = mask.device
|
||||
|
||||
if grow_blur == 0:
|
||||
return (mask,)
|
||||
|
||||
radius = abs(int(grow_blur))
|
||||
|
||||
if grow_blur < 0:
|
||||
eroded = self._erode(mask, radius)
|
||||
return (eroded.clamp(0.0, 1.0),)
|
||||
|
||||
core = mask
|
||||
expanded = self._dilate(mask, radius)
|
||||
ring = (expanded - core).clamp(0.0, 1.0)
|
||||
blurred_ring = self._blur_ring(ring, radius, device)
|
||||
blurred_ring = (blurred_ring * expanded).clamp(0.0, 1.0)
|
||||
out = (core + blurred_ring).clamp(0.0, 1.0)
|
||||
return (out,)
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
# Get Latent Size
|
||||
class GetLatentSizeMXD:
|
||||
DESCRIPTION = """Get image width/height from a latent."""
|
||||
@@ -1327,6 +1413,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"FluxResolutionMatcher": FluxResolutionMatcher,
|
||||
"SDXLResolutionMatcher": SDXLResolutionMatcher,
|
||||
"LatentHalfMasks": LatentHalfMasks,
|
||||
"Grow Blur Mask MXD": GrowBlurMaskMXD,
|
||||
"Get Latent Size": GetLatentSizeMXD,
|
||||
"Place Image By Mask": PlaceImageByMask,
|
||||
"Crop Image By Mask": CropImageByMask,
|
||||
@@ -1350,6 +1437,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FluxResolutionMatcher": "Flux Resolution Matcher MXD",
|
||||
"SDXLResolutionMatcher": "SDXL Resolution Matcher MXD",
|
||||
"LatentHalfMasks": "Latent to L/R Masks MXD",
|
||||
"Grow Blur Mask MXD": "Grow Blur Mask MXD",
|
||||
"Get Latent Size": "Get Latent Size MXD",
|
||||
"Place Image By Mask": "Place Image by Mask MXD",
|
||||
"Crop Image By Mask": "Crop Image by Mask MXD",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "maxedout"
|
||||
name = "MaxedOut"
|
||||
description = "Custom ComfyUI nodes used in Maxed Out workflows (SDXL, Flux, Wan 2.2, etc.)"
|
||||
version = "1.6.7"
|
||||
version = "1.6.8"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
Reference in New Issue
Block a user