CoolFace
Apppublic

brunvelop/ComfyUI

sourceHugging Faceupdated 3y agoView on Hugging Face
2likes
nodes_mask.py363 linesDownload Raw Back to comfy_extras
1import numpy as np2import scipy.ndimage3import torch4import comfy.utils5 6from nodes import MAX_RESOLUTION7 8def composite(destination, source, x, y, mask = None, multiplier = 8, resize_source = False):9    if resize_source:10        source = torch.nn.functional.interpolate(source, size=(destination.shape[2], destination.shape[3]), mode="bilinear")11 12    source = comfy.utils.repeat_to_batch_size(source, destination.shape[0])13 14    x = max(-source.shape[3] * multiplier, min(x, destination.shape[3] * multiplier))15    y = max(-source.shape[2] * multiplier, min(y, destination.shape[2] * multiplier))16 17    left, top = (x // multiplier, y // multiplier)18    right, bottom = (left + source.shape[3], top + source.shape[2],)19 20    if mask is None:21        mask = torch.ones_like(source)22    else:23        mask = mask.clone()24        mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(source.shape[2], source.shape[3]), mode="bilinear")25        mask = comfy.utils.repeat_to_batch_size(mask, source.shape[0])26 27    # calculate the bounds of the source that will be overlapping the destination28    # this prevents the source trying to overwrite latent pixels that are out of bounds29    # of the destination30    visible_width, visible_height = (destination.shape[3] - left + min(0, x), destination.shape[2] - top + min(0, y),)31 32    mask = mask[:, :, :visible_height, :visible_width]33    inverse_mask = torch.ones_like(mask) - mask34 35    source_portion = mask * source[:, :, :visible_height, :visible_width]36    destination_portion = inverse_mask  * destination[:, :, top:bottom, left:right]37 38    destination[:, :, top:bottom, left:right] = source_portion + destination_portion39    return destination40 41class LatentCompositeMasked:42    @classmethod43    def INPUT_TYPES(s):44        return {45            "required": {46                "destination": ("LATENT",),47                "source": ("LATENT",),48                "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),49                "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}),50                "resize_source": ("BOOLEAN", {"default": False}),51            },52            "optional": {53                "mask": ("MASK",),54            }55        }56    RETURN_TYPES = ("LATENT",)57    FUNCTION = "composite"58 59    CATEGORY = "latent"60 61    def composite(self, destination, source, x, y, resize_source, mask = None):62        output = destination.copy()63        destination = destination["samples"].clone()64        source = source["samples"]65        output["samples"] = composite(destination, source, x, y, mask, 8, resize_source)66        return (output,)67 68class ImageCompositeMasked:69    @classmethod70    def INPUT_TYPES(s):71        return {72            "required": {73                "destination": ("IMAGE",),74                "source": ("IMAGE",),75                "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),76                "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),77                "resize_source": ("BOOLEAN", {"default": False}),78            },79            "optional": {80                "mask": ("MASK",),81            }82        }83    RETURN_TYPES = ("IMAGE",)84    FUNCTION = "composite"85 86    CATEGORY = "image"87 88    def composite(self, destination, source, x, y, resize_source, mask = None):89        destination = destination.clone().movedim(-1, 1)90        output = composite(destination, source.movedim(-1, 1), x, y, mask, 1, resize_source).movedim(1, -1)91        return (output,)92 93class MaskToImage:94    @classmethod95    def INPUT_TYPES(s):96        return {97                "required": {98                    "mask": ("MASK",),99                }100        }101 102    CATEGORY = "mask"103 104    RETURN_TYPES = ("IMAGE",)105    FUNCTION = "mask_to_image"106 107    def mask_to_image(self, mask):108        result = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)109        return (result,)110 111class ImageToMask:112    @classmethod113    def INPUT_TYPES(s):114        return {115                "required": {116                    "image": ("IMAGE",),117                    "channel": (["red", "green", "blue", "alpha"],),118                }119        }120 121    CATEGORY = "mask"122 123    RETURN_TYPES = ("MASK",)124    FUNCTION = "image_to_mask"125 126    def image_to_mask(self, image, channel):127        channels = ["red", "green", "blue", "alpha"]128        mask = image[:, :, :, channels.index(channel)]129        return (mask,)130 131class ImageColorToMask:132    @classmethod133    def INPUT_TYPES(s):134        return {135                "required": {136                    "image": ("IMAGE",),137                    "color": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFF, "step": 1, "display": "color"}),138                }139        }140 141    CATEGORY = "mask"142 143    RETURN_TYPES = ("MASK",)144    FUNCTION = "image_to_mask"145 146    def image_to_mask(self, image, color):147        temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int)148        temp = torch.bitwise_left_shift(temp[:,:,:,0], 16) + torch.bitwise_left_shift(temp[:,:,:,1], 8) + temp[:,:,:,2]149        mask = torch.where(temp == color, 255, 0).float()150        return (mask,)151 152class SolidMask:153    @classmethod154    def INPUT_TYPES(cls):155        return {156            "required": {157                "value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),158                "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),159                "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),160            }161        }162 163    CATEGORY = "mask"164 165    RETURN_TYPES = ("MASK",)166 167    FUNCTION = "solid"168 169    def solid(self, value, width, height):170        out = torch.full((1, height, width), value, dtype=torch.float32, device="cpu")171        return (out,)172 173class InvertMask:174    @classmethod175    def INPUT_TYPES(cls):176        return {177            "required": {178                "mask": ("MASK",),179            }180        }181 182    CATEGORY = "mask"183 184    RETURN_TYPES = ("MASK",)185 186    FUNCTION = "invert"187 188    def invert(self, mask):189        out = 1.0 - mask190        return (out,)191 192class CropMask:193    @classmethod194    def INPUT_TYPES(cls):195        return {196            "required": {197                "mask": ("MASK",),198                "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),199                "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),200                "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),201                "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}),202            }203        }204 205    CATEGORY = "mask"206 207    RETURN_TYPES = ("MASK",)208 209    FUNCTION = "crop"210 211    def crop(self, mask, x, y, width, height):212        mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1]))213        out = mask[:, y:y + height, x:x + width]214        return (out,)215 216class MaskComposite:217    @classmethod218    def INPUT_TYPES(cls):219        return {220            "required": {221                "destination": ("MASK",),222                "source": ("MASK",),223                "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),224                "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),225                "operation": (["multiply", "add", "subtract", "and", "or", "xor"],),226            }227        }228 229    CATEGORY = "mask"230 231    RETURN_TYPES = ("MASK",)232 233    FUNCTION = "combine"234 235    def combine(self, destination, source, x, y, operation):236        output = destination.reshape((-1, destination.shape[-2], destination.shape[-1])).clone()237        source = source.reshape((-1, source.shape[-2], source.shape[-1]))238 239        left, top = (x, y,)240        right, bottom = (min(left + source.shape[-1], destination.shape[-1]), min(top + source.shape[-2], destination.shape[-2]))241        visible_width, visible_height = (right - left, bottom - top,)242 243        source_portion = source[:, :visible_height, :visible_width]244        destination_portion = destination[:, top:bottom, left:right]245 246        if operation == "multiply":247            output[:, top:bottom, left:right] = destination_portion * source_portion248        elif operation == "add":249            output[:, top:bottom, left:right] = destination_portion + source_portion250        elif operation == "subtract":251            output[:, top:bottom, left:right] = destination_portion - source_portion252        elif operation == "and":253            output[:, top:bottom, left:right] = torch.bitwise_and(destination_portion.round().bool(), source_portion.round().bool()).float()254        elif operation == "or":255            output[:, top:bottom, left:right] = torch.bitwise_or(destination_portion.round().bool(), source_portion.round().bool()).float()256        elif operation == "xor":257            output[:, top:bottom, left:right] = torch.bitwise_xor(destination_portion.round().bool(), source_portion.round().bool()).float()258 259        output = torch.clamp(output, 0.0, 1.0)260 261        return (output,)262 263class FeatherMask:264    @classmethod265    def INPUT_TYPES(cls):266        return {267            "required": {268                "mask": ("MASK",),269                "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),270                "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),271                "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),272                "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),273            }274        }275 276    CATEGORY = "mask"277 278    RETURN_TYPES = ("MASK",)279 280    FUNCTION = "feather"281 282    def feather(self, mask, left, top, right, bottom):283        output = mask.reshape((-1, mask.shape[-2], mask.shape[-1])).clone()284 285        left = min(left, output.shape[-1])286        right = min(right, output.shape[-1])287        top = min(top, output.shape[-2])288        bottom = min(bottom, output.shape[-2])289 290        for x in range(left):291            feather_rate = (x + 1.0) / left292            output[:, :, x] *= feather_rate293 294        for x in range(right):295            feather_rate = (x + 1) / right296            output[:, :, -x] *= feather_rate297 298        for y in range(top):299            feather_rate = (y + 1) / top300            output[:, y, :] *= feather_rate301 302        for y in range(bottom):303            feather_rate = (y + 1) / bottom304            output[:, -y, :] *= feather_rate305 306        return (output,)307    308class GrowMask:309    @classmethod310    def INPUT_TYPES(cls):311        return {312            "required": {313                "mask": ("MASK",),314                "expand": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1}),315                "tapered_corners": ("BOOLEAN", {"default": True}),316            },317        }318    319    CATEGORY = "mask"320 321    RETURN_TYPES = ("MASK",)322 323    FUNCTION = "expand_mask"324 325    def expand_mask(self, mask, expand, tapered_corners):326        c = 0 if tapered_corners else 1327        kernel = np.array([[c, 1, c],328                           [1, 1, 1],329                           [c, 1, c]])330        mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1]))331        out = []332        for m in mask:333            output = m.numpy()334            for _ in range(abs(expand)):335                if expand < 0:336                    output = scipy.ndimage.grey_erosion(output, footprint=kernel)337                else:338                    output = scipy.ndimage.grey_dilation(output, footprint=kernel)339            output = torch.from_numpy(output)340            out.append(output)341        return (torch.stack(out, dim=0),)342 343 344 345NODE_CLASS_MAPPINGS = {346    "LatentCompositeMasked": LatentCompositeMasked,347    "ImageCompositeMasked": ImageCompositeMasked,348    "MaskToImage": MaskToImage,349    "ImageToMask": ImageToMask,350    "ImageColorToMask": ImageColorToMask,351    "SolidMask": SolidMask,352    "InvertMask": InvertMask,353    "CropMask": CropMask,354    "MaskComposite": MaskComposite,355    "FeatherMask": FeatherMask,356    "GrowMask": GrowMask,357}358 359NODE_DISPLAY_NAME_MAPPINGS = {360    "ImageToMask": "Convert Image to Mask",361    "MaskToImage": "Convert Mask to Image",362}363