Files
mirabarukaso-ComfyUI_Mira/Mask.py
T
mirabarukaso c86c879c18 v 0.3.6.0
Add `Create Mask With Canvas`
2024-03-14 21:13:40 +09:00

736 lines
28 KiB
Python

from PIL import Image
import random
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageSequence
from PIL.PngImagePlugin import PngInfo
import numpy as np
import torch
import torch.nn.functional as F
import torchvision.transforms.v2 as T
import re
cat = "Mira/Mask"
def special_match(strg, search=re.compile(r'[^0-9.,;]').search):
return not bool(search(strg))
def RectWidth(Rectangles, Range, nowWidth, warpWidth, y, Width, Height, WarpTimesArray = None):
warpTimes = 1.0
for i in range(Range):
if None is not WarpTimesArray:
warpTimes = float(WarpTimesArray[i])
if Width - nowWidth - (warpWidth * warpTimes)<= 8:
Rectangles.append([int(nowWidth), y, Width, Height])
else:
Rectangles.append([int(nowWidth), y, int(nowWidth + (warpWidth * warpTimes)), Height])
nowWidth = nowWidth + (warpWidth*warpTimes)
return Rectangles
def RectHeight(Rectangles, Range, nowHeight, warpHeight, x, Width, Height, WarpTimesArray = None):
warpTimes = 1.0
for i in range(Range):
if None is not WarpTimesArray:
warpTimes = float(WarpTimesArray[i])
if Height - nowHeight - (warpHeight * warpTimes) <= 8:
Rectangles.append([x, int(nowHeight), Width, Height])
else:
Rectangles.append([x, int(nowHeight), Width, int(nowHeight + (warpHeight * warpTimes))])
nowHeight = nowHeight + (warpHeight * warpTimes)
return Rectangles
def CreatePNG(Width, Height, Rows, Colums, Colum_first, Layout, DebugMessage):
DebugMessage += 'Mira:\n'
Rectangles = []
BlocksCount = Rows * Colums
nowWidth = 0
nowHeight = 0
warpWidth = 0
warpHeight = 0
# , == seperate row
# ; == seperate colum, has higher prority to cut masks
# Colum_first == swap , and ;
if False == special_match(Layout):
#print('Mira: syntaxerror in layout -> [' + Layout + '] Will use Rows * Colums')
DebugMessage += 'syntaxerror in layout -> [' + Layout + '] Will use Rows * Colums\n'
new_layout = ''
for _ in range(Rows):
new_layout += '1,'
for _ in range(Colums):
new_layout += '1,'
new_layout = new_layout[:-1] + ';'
new_layout = new_layout[:-1]
DebugMessage += 'new_layout: ' + new_layout + '\n'
return CreatePNG(Width, Height, Rows, Colums, Colum_first, new_layout, DebugMessage)
else:
DebugMessage += 'use Layouts\n'
isSingleSeparator = False
BlocksCount = 0
WarpTimesArray = 0
if ',' in Layout and ';' not in Layout:
DebugMessage += 'Mira: only , \n'
BlocksCount = Layout.count(',') + 1
WarpTimesArray = Layout.split(',')
isSingleSeparator = True
elif ';' in Layout and ',' not in Layout:
DebugMessage += 'Mira: only ; \n'
BlocksCount = Layout.count(';') + 1
WarpTimesArray = Layout.split(';')
isSingleSeparator = True
else:
DebugMessage += 'Mira: both , ; \n'
if True == isSingleSeparator:
SingleBlock = 0
for WarpTimes in WarpTimesArray:
SingleBlock += float(WarpTimes)
if True == Colum_first:
warpWidth = int(Width / SingleBlock)
Rectangles = RectWidth(Rectangles, BlocksCount, nowWidth, warpWidth, 0, Width, Height, WarpTimesArray)
else:
warpHeight = int(Height / SingleBlock)
Rectangles = RectHeight(Rectangles, BlocksCount, nowHeight, warpHeight, 0, Width, Height, WarpTimesArray)
else:
GreatCuts = Layout.split(';')
GreatBlockArray = []
GreatBlock = 0
GreatBlockCounts = 0
for cut in GreatCuts:
GreatBlock += float(cut.split(',')[0])
GreatBlockCounts += 1
GreatBlockArray.append(cut.split(',')[0])
if True == Colum_first:
GreatWarpHeight = int(Height / GreatBlock)
#Rectangles = RectHeight(Rectangles, GreatBlockCounts, nowHeight, GreatWarpHeight, 0, Width, Height, GreatBlockArray)
#BlocksCount += GreatBlockCounts
now_cut = 0
for cut in GreatCuts:
SingleBlock = 0
FullWarpTimesArray = cut.split(',')
nowHeightEnd = int(nowHeight+GreatWarpHeight*float(GreatBlockArray[now_cut]))
if Height - nowHeightEnd <= 8:
nowHeightEnd = Height
if 1 >= len(FullWarpTimesArray):
#print('Mira: Bypass empty GreatCuts')
DebugMessage += 'Mira: Bypass empty GreatCuts\n'
else:
# remove first Great Cuts Value
FullWarpTimesArray.pop(0)
CurrentBlocksCount = len(FullWarpTimesArray)
for WarpTimes in FullWarpTimesArray:
SingleBlock += float(WarpTimes)
warpWidth = int(Width / SingleBlock)
Rectangles = RectWidth(Rectangles, CurrentBlocksCount, nowWidth, warpWidth, nowHeight, Width, nowHeightEnd, FullWarpTimesArray)
BlocksCount += CurrentBlocksCount
now_cut += 1
nowHeight = nowHeightEnd
else:
GreatWarpWidth = int(Width / GreatBlock)
#Rectangles = RectWidth(Rectangles, GreatBlockCounts, nowWidth, GreatWarpWidth, 0, Width, Height, GreatBlockArray)
#BlocksCount += GreatBlockCounts
now_cut = 0
for cut in GreatCuts:
SingleBlock = 0
FullWarpTimesArray = cut.split(',')
nowWidthEnd = int(nowWidth+GreatWarpWidth*float(GreatBlockArray[now_cut]))
if Width - nowWidthEnd <= 8:
nowWidthEnd = Width
if 1 >= len(FullWarpTimesArray):
#print('Mira: By pass empty GreatCuts')
DebugMessage += 'Mira: By pass empty GreatCuts\n'
else:
# remove first Great Cuts Value
FullWarpTimesArray.pop(0)
CurrentBlocksCount = len(FullWarpTimesArray)
for WarpTimes in FullWarpTimesArray:
SingleBlock += float(WarpTimes)
warpHeight = int(Height / SingleBlock)
Rectangles = RectHeight(Rectangles, CurrentBlocksCount, nowHeight, warpHeight, nowWidth, nowWidthEnd, Height, FullWarpTimesArray)
BlocksCount += CurrentBlocksCount
now_cut += 1
nowWidth = nowWidthEnd
PngImage = Image.new("RGBA", [Width, Height])
PngDraw = ImageDraw.Draw(PngImage)
PngColorMasks = []
for _ in range(BlocksCount):
R = random.randrange(0,255)
G = random.randrange(0,255)
B = random.randrange(0,255)
# Extremely low probability, but it happens....
while PngColorMasks.__contains__([R,G,B]):
R = random.randrange(0,255)
G = random.randrange(0,255)
B = random.randrange(0,255)
PngColorMasks.append([R,G,B])
DebugMessage += '[' + str(R) + ',' + str(G) + ','+ str(B) + '] '
DebugMessage += '\n'
#print('[' + str(R) + ',' + str(G) + ','+ str(B) + '] ')
for i in range(BlocksCount):
hex_rgb = ' #{:02X}{:02X}{:02X}'.format(PngColorMasks[i][0], PngColorMasks[i][1], PngColorMasks[i][2])
#print('Mira: [' + str(i) +']Draw ' + str(Rectangles[i]) + ' with ' + str(PngColorMasks[i]) + hex_rgb)
DebugMessage += '[' + str(i) +']Draw ' + str(Rectangles[i]) + ' with ' + str(PngColorMasks[i]) + hex_rgb +'\n'
PngDraw.rectangle(Rectangles[i], fill=(PngColorMasks[i][0], PngColorMasks[i][1], PngColorMasks[i][2], 255))
# Add Image Size to last
Rectangles.append([0,0,Width,Height])
DebugMessage += '\n'
return PngImage, Rectangles, PngColorMasks, DebugMessage
class CreateRegionalPNGMask:
'''
Create a PNG tiled image with Color Mask stack for regional conditioning mask.
Inputs:
Width - Image Width
Height - Image Height
Colum_first - A boolean trigger, when enabled, will treat default cut as a horizontal cut.
Rows - Low prority, only works when Layout is incorrect.
Colums - Low prority, only works when Layout is incorrect.
Layout - Refer to Readme.md @https://github.com/mirabarukaso/ComfyUI_Mira
Use_Catched_PNG - Save PNG to memory for performance
Outputs:
PngImage - Image
PngColorMasks - A List contains all PNG Blocks' color information.
PngRectangles - A List contains all PNG Blocks' rectangle informationm, last one is the whole Image's Width and Height
Debug - Debug output
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"Width": ("INT", {
"default": 576,
"min": 16,
"step": 8,
"display": "number"
}),
"Height": ("INT", {
"default": 1024,
"min": 16,
"step": 8,
"display": "number"
}),
"Colum_first": ("BOOLEAN", {
"default": False
}),
"Rows": ("INT", {
"default": 1,
"min": 1,
"step": 1,
"display": "number"
}),
"Colums": ("INT", {
"default": 1,
"min": 1,
"step": 1,
"display": "number"
}),
"Layout": ("STRING", {
"multiline": False,
"default": "1,1,1"
}),
},
}
RETURN_TYPES = ("IMAGE", "LIST", "LIST", "STRING",)
RETURN_NAMES = ("PngImage", "PngColorMasks", "PngRectangles", "Debug",)
FUNCTION = "CreateRegionalPNGMaskEx"
CATEGORY = cat
def CreateRegionalPNGMaskEx(self, Width, Height, Rows, Colums, Colum_first, Layout = '#'):
DebugMessage = ''
PngImage, PngRectangles, PngColorMasks, DebugMessage = CreatePNG(Width, Height, Rows, Colums, Colum_first, Layout, DebugMessage)
#refer: https://github.com/comfyanonymous/ComfyUI/blob/master/nodes.py#L1487
# LoadImage
output_images = []
output_masks = []
for i in ImageSequence.Iterator(PngImage):
i = ImageOps.exif_transpose(i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, PngColorMasks, PngRectangles, DebugMessage,)
class PngColorMasksToString:
'''
Convert specified Index of PngColorMasks to HEX value.
Inputs:
PngColorMasks - List from Create PNG Mask
Index - The Block id of PNG Mask
Outputs:
mask_color - String. e.g. RGB(255,0,255) to #FF00FF
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PngColorMasks": ("LIST", {
"display": "input"
}),
"Index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("mask_color",)
FUNCTION = "ColorMasksToStringEx"
CATEGORY = cat
def ColorMasksToStringEx(self, PngColorMasks, Index):
if len(PngColorMasks) <= Index:
print('Mira: ERROR Index is greater than Mask count! Will use 0')
Index = 0
ret = ('#{:02X}{:02X}{:02X}'.format(PngColorMasks[Index][0], PngColorMasks[Index][1], PngColorMasks[Index][2]))
return (ret,)
class PngColorMasksToRGB:
'''
Convert specified Index of PngColorMasks to RGB value.
Inputs:
PngColorMasks - List from Create PNG Mask
Index - The Block id of PNG Mask
Outputs:
R - Integer. Red
G - Integer. Green
B - Integer. Blue
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PngColorMasks": ("LIST", {
"display": "input"
}),
"Index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
},
}
RETURN_TYPES = ("INT","INT","INT",)
RETURN_NAMES = ("R", "G", "B", )
FUNCTION = "ColorMasksToRGBEx"
CATEGORY = cat
def ColorMasksToRGBEx(self, PngColorMasks, Index):
if len(PngColorMasks) <= Index:
print('Mira: ERROR Index is greater than Mask count! Will use 0')
Index = 0
R = PngColorMasks[Index][0]
G = PngColorMasks[Index][1]
B = PngColorMasks[Index][2]
return (R, G, B,)
class PngColorMasksToStringList:
'''
Convert ranged PngColorMasks to HEX value.
Inputs:
PngColorMasks - List from Create PNG Mask
Start_At_Index - The first Block id to start
Outputs:
mask_color[0-9] - String. e.g. RGB(255,0,255) to #FF00FF
'''
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"PngColorMasks": ("LIST", {
"display": "input"
}),
"Start_At_Index": ("INT", {
"default": 0,
"min": 0,
"max": 256,
"step": 1,
"display": "number"
}),
}
}
return inputs
# Not sure if there's a dynamic outputs solution....
r_t = ()
r_n = ()
for i in range(10):
r_t += ('STRING',)
r_n += (f'mask_color_{i}',)
RETURN_TYPES = r_t
RETURN_NAMES = r_n
FUNCTION = "ColorMasksToStringListEx"
CATEGORY = cat
def ColorMasksToStringListEx(self, PngColorMasks, Start_At_Index):
ret = []
for Index in range(Start_At_Index, Start_At_Index + 10, 1):
if len(PngColorMasks) <= Index:
ret.append('#000000')
else:
ret.append('#{:02X}{:02X}{:02X}'.format(PngColorMasks[Index][0], PngColorMasks[Index][1], PngColorMasks[Index][2]))
return (ret[0],ret[1],ret[2],ret[3],ret[4],ret[5],ret[6],ret[7],ret[8],ret[9],)
class PngColorMasksToMaskList:
'''
Convert ranged PngColorMasks to Mask with Mask Blur function.
This is a color based function, so it could NOT set Intenisity to Mask.
Inputs:
PngImage - Image from Create PNG Mask
PngColorMasks - List from Create PNG Mask
Blur - The amount of Blur, 0 for Soild.
Start_At_Index - The first Block id to start
Outputs:
mask_[0-9] - Mask for anyone who want a Mask
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PngImage": ("IMAGE", {
"display": "input"
}),
"PngColorMasks": ("LIST", {
"display": "input"
}),
"Blur": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"step": 0.5,
"display": "number"
}),
"Start_At_Index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
},
}
# Not sure if there's a dynamic outputs solution....
r_t = ()
r_n = ()
for i in range(10):
r_t += ('MASK',)
r_n += (f'mask_{i}',)
RETURN_TYPES = r_t
RETURN_NAMES = r_n
FUNCTION = "ColorMasksToMaskListEx"
CATEGORY = cat
# refer: https://github.com/cubiq/ComfyUI_essentials
# MaskBlur and MaskFromColor
def ColorMasksToMaskListEx(self, PngImage, PngColorMasks, Blur, Start_At_Index):
masks = []
for index in range(Start_At_Index, Start_At_Index + 10, 1):
if len(PngColorMasks) <= index:
color = torch.tensor([0,0,0])
else:
color = torch.tensor(PngColorMasks[index])
temp = (torch.clamp(PngImage, 0, 1.0) * 255.0).round().to(torch.int)
lower_bound = (color).clamp(min=0)
upper_bound = (color).clamp(max=255)
mask = (temp >= lower_bound) & (temp <= upper_bound)
mask = mask.all(dim=-1)
mask = mask.float()
if 0 < Blur:
size = int(6 * Blur +1)
if size % 2 == 0:
size+= 1
blurred = mask.unsqueeze(1)
blurred = T.GaussianBlur(size, Blur)(blurred)
blurred = blurred.squeeze(1)
mask = blurred
masks.append(mask)
return (masks[0], masks[1], masks[2], masks[3], masks[4], masks[5], masks[6], masks[7], masks[8], masks[9],)
# refer: https://github.com/comfyanonymous/ComfyUI
# SolidMask
# refer: https://github.com/cubiq/ComfyUI_essentials
# MaskBlur MaskBatch
def CreateMaskFromPngRectangles(PngRectangles, Intenisity, Blur, Start_At_Index, End_At_Step=1):
masks = []
sizePngRectangles = len(PngRectangles) - 1
Width = PngRectangles[sizePngRectangles][2]
Height = PngRectangles[sizePngRectangles][3]
destinationMask = torch.full((1,Height, Width), 0, dtype=torch.float32, device="cpu")
for index in range(Start_At_Index, End_At_Step, 1):
# In case of someone need a whole mask, changed <= to <
if sizePngRectangles < index:
mask = destinationMask
else:
W = PngRectangles[index][2] - PngRectangles[index][0]
H = PngRectangles[index][3] - PngRectangles[index][1]
output = destinationMask.reshape((-1, destinationMask.shape[-2], destinationMask.shape[-1])).clone()
sourceMask = torch.full((1,H, W), Intenisity, dtype=torch.float32, device="cpu")
source = sourceMask.reshape((-1, sourceMask.shape[-2], sourceMask.shape[-1]))
left, top = (PngRectangles[index][0],PngRectangles[index][1],)
right, bottom = (min(left + source.shape[-1], destinationMask.shape[-1]), min(top + source.shape[-2], destinationMask.shape[-2]))
visible_width, visible_height = (right - left, bottom - top,)
source_portion = source[:, :visible_height, :visible_width]
destination_portion = destinationMask[:, top:bottom, left:right]
# Add
output[:, top:bottom, left:right] = destination_portion + source_portion
mask = output
if 0 < Blur:
size = int(6 * Blur +1)
if size % 2 == 0:
size+= 1
blurred = mask.unsqueeze(1)
blurred = T.GaussianBlur(size, Blur)(blurred)
blurred = blurred.squeeze(1)
mask = blurred
masks.append(mask)
return masks
class PngRectanglesToMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PngRectangles": ("LIST", {
"display": "input"
}),
"Intenisity": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "number"
}),
"Blur": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"step": 0.5,
"display": "number"
}),
"Start_At_Index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
},
}
RETURN_TYPES = ('MASK',)
RETURN_NAMES = ('mask',)
FUNCTION = "PngRectanglesToMaskEx"
CATEGORY = cat
def PngRectanglesToMaskEx(self, PngRectangles, Intenisity, Blur, Start_At_Index):
masks = CreateMaskFromPngRectangles(PngRectangles, Intenisity, Blur, Start_At_Index, Start_At_Index + 1)
return (masks[0],)
class PngRectanglesToMaskList:
'''
Convert ranged PngRectangles to Mask with Mask Intenisity and Blur function.
This function creates Mask directly from Rectangles data.
Inputs:
PngRectangles - List from Create PNG Mask
Intenisity - The intenisity of Mask, 1 for Soild.
Blur - The amount of Blur, 0 for Soild.
Start_At_Index - The first Block id to start
Outputs:
mask_[0-9] - Mask for anyone who want a Mask
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"PngRectangles": ("LIST", {
"display": "input"
}),
"Intenisity": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.1,
"display": "number"
}),
"Blur": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"step": 0.5,
"display": "number"
}),
"Start_At_Index": ("INT", {
"default": 0,
"min": 0,
"step": 1,
"display": "number"
}),
},
}
# Not sure if there's a dynamic outputs solution....
r_t = ()
r_n = ()
for i in range(10):
r_t += ('MASK',)
r_n += (f'mask_{i}',)
RETURN_TYPES = r_t
RETURN_NAMES = r_n
FUNCTION = "PngRectanglesToMaskListEx"
CATEGORY = cat
def PngRectanglesToMaskListEx(self, PngRectangles, Intenisity, Blur, Start_At_Index):
masks = CreateMaskFromPngRectangles(PngRectangles, Intenisity, Blur, Start_At_Index, Start_At_Index + 10)
return (masks[0], masks[1], masks[2], masks[3], masks[4], masks[5], masks[6], masks[7], masks[8], masks[9],)
class CreateMaskWithCanvas:
'''
Create a new mask on defined cavans
Inputs:
C_Width - Width of cavans
C_Height - Height of cavans
X - The left point of new mask on cavans
Y - The top point of new mask on cavans
Width - Mask width
Height - Mask height
Intenisity - The intenisity of Mask, 1 for Soild.
Blur - The amount of Blur, 0 for Soild.
Outputs:
mask - New mask with defined cavans
'''
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"C_Width": ("INT", { "default": 512, "min": 8, "max": 4096, "step": 1, "display": "number" }),
"C_Height": ("INT", { "default": 512, "min": 8, "max": 4096, "step": 1, "display": "number" }),
"X": ("INT", { "default": 0, "min": 0, "max": 4096, "step": 1, "display": "number" }),
"Y": ("INT", { "default": 0, "min": 0, "max": 4096, "step": 1, "display": "number" }),
"Width": ("INT", { "default": 512, "min": 8, "max": 4096, "step": 1, "display": "number" }),
"Height": ("INT", { "default": 512, "min": 8, "max": 4096, "step": 1, "display": "number" }),
"Intenisity": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1, "display": "number" }),
"Blur": ("FLOAT", { "default": 0.0, "min": 0.0, "step": 0.5, "display": "number" }),
},
}
RETURN_TYPES = ('MASK',)
RETURN_NAMES = ('mask',)
FUNCTION = "CreateMaskWithCanvasEx"
CATEGORY = cat
def CreateMaskWithCanvasEx(self, C_Width, C_Height, X, Y, Width, Height, Intenisity, Blur):
destinationMask = torch.full((1,C_Height, C_Width), 0, dtype=torch.float32, device="cpu")
output = destinationMask.reshape((-1, destinationMask.shape[-2], destinationMask.shape[-1])).clone()
sourceMask = torch.full((1, Height, Width), Intenisity, dtype=torch.float32, device="cpu")
source = sourceMask.reshape((-1, sourceMask.shape[-2], sourceMask.shape[-1]))
left, top = (X, Y)
right, bottom = (min(left + source.shape[-1], destinationMask.shape[-1]), min(top + source.shape[-2], destinationMask.shape[-2]))
visible_width, visible_height = (right - left, bottom - top,)
source_portion = source[:, :visible_height, :visible_width]
destination_portion = destinationMask[:, top:bottom, left:right]
output[:, top:bottom, left:right] = destination_portion + source_portion
mask = output
if 0 < Blur:
size = int(6 * Blur +1)
if size % 2 == 0:
size+= 1
blurred = mask.unsqueeze(1)
blurred = T.GaussianBlur(size, Blur)(blurred)
blurred = blurred.squeeze(1)
mask = blurred
return (mask,)