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:
Maxed-Out-99
2026-02-06 03:46:42 -08:00
parent 7e46538129
commit dc3130bd21
2 changed files with 91 additions and 3 deletions
+89 -1
View File
@@ -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
View File
@@ -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)