commit Film node
This commit is contained in:
@@ -17,6 +17,8 @@ Nodes are divided into 5 groups according to their functions: LayerStyle, LayerC
|
||||
|
||||
## Update
|
||||
**If the dependency package error after updating, please reinstall the relevant dependency packages. for details, please refer to [here](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5).
|
||||
|
||||
* Commit [Film](#Film) node, this filter Simulate the grain, dark edge, and blurred edge of the film, support input depth map to simulate defocus. it is reorganize and encapsulate of [digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost).
|
||||
* Commit [ImageAutoCrop](#ImageAutoCrop) node, which is designed to generate image materials for training models.
|
||||
* Commit [ImageScaleByAspectRatio](#ImageScaleByAspectRatio) node, it can be scaled image or mask according to frame ratio.
|
||||
* Fix the bug of color gradation in [LUT Apply](#LUT) node rendering, and this node now support for log color space. *Please load the dedicated log lut file for the log color space image.
|
||||
@@ -884,6 +886,27 @@ Node options:
|
||||
* angle: Angle of channel separation.
|
||||
* mode: Channel shift arrangement order.
|
||||
|
||||
### <a id="table1">Film</a>
|
||||
Simulate the grain, dark edge, and blurred edge of the film, support input depth map to simulate defocus.
|
||||
This node is reorganize and encapsulate of [digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost).
|
||||

|
||||
|
||||
Node options:
|
||||

|
||||
* image: The input image.
|
||||
* depth_map: Input depth map to simulate defocus effect. it is an optional input. if there is no input, will simulates radial blur at the edges of the image.
|
||||
* center_x: The horizontal axis of the center point position of the dark edge and radial blur, where 0 represents the leftmost side, 1 represents the rightmost side, and 0.5 represents at the center.
|
||||
* center_y: The vertical axis of the center point position of the dark edge and radial blur, where 0 represents the leftmost side, 1 represents the rightmost side, and 0.5 represents at the center.
|
||||
* saturation: Color saturation, 1 is the original value.
|
||||
* grain_power: Grain intensity. larger value means more pronounced the noise.
|
||||
* grain_scale: Grain size.
|
||||
* grain_sat: The color saturation of grain. 0 represents mono noise, and the larger the value, the more prominent the color.
|
||||
* grain_shadows: Grain intensity of dark part.
|
||||
* grain_highs: Grain intensity of light part.
|
||||
* blur_strength: The strength of blur. larger value means more blurry it becomes.
|
||||
* blur_focus_spread: Focus diffusion range. larger value means larger clear range.
|
||||
* focal_depth: Simulate the focal distance of defucus. 0 indicates that focus is farthest, and 1 indicates that is closest. this setting only valid when input the depth_map.
|
||||
|
||||
### <a id="table1">ColorMap</a>
|
||||
Pseudo color heat map effect.
|
||||

|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
## 更新说明
|
||||
**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。详情见[这里](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5)。
|
||||
* 添加 [Film](#Film) 节点, 这个滤镜模拟胶片的颗粒、暗边和边缘模糊,支持输入深度图模拟虚焦,是[digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost)的重新封装。
|
||||
* 添加 [ImageAutoCrop](#ImageAutoCrop) 节点, 这个节点是为生成训练模型的图片素材而设计的。
|
||||
* 添加 [ImageScaleByAspectRatio](#ImageScaleByAspectRatio) 节点, 可按画幅比例缩放图像。
|
||||
* 改正 [LUT Apply](#LUT) 节点渲染出现色阶的bug, 并增加log色彩空间支持。*log色彩空间图片请加载专门的log lut。
|
||||
@@ -873,6 +874,28 @@ mask
|
||||
* angle: 通道分离的角度。
|
||||
* mode: 通道错位排列顺序。
|
||||
|
||||
### <a id="table1">Film</a>
|
||||
模拟胶片的颗粒、暗边和边缘模糊,支持输入深度图模拟虚焦。
|
||||
这个节点是[digitaljohn/comfyui-propost](https://github.com/digitaljohn/comfyui-propost)的重新封装。
|
||||

|
||||
|
||||
节点选项说明:
|
||||

|
||||
* image: 输入的图片。
|
||||
* depth_map: 深度图输入,由此模拟虚焦效果。此项是可选输入,如果没有输入则模拟为图片边缘的径向模糊。
|
||||
* center_x: 暗边和径向模糊的中心点位置横坐标,0表示最左侧,1表示最右侧,0.5表示在中心。
|
||||
* center_y: 暗边和径向模糊的中心点位置纵坐标,0表示最上方,1表示最下方,0.5表示在中心。
|
||||
* saturation: 颜色饱和度,1为原始值。
|
||||
* grain_power: 噪点强度。数值越大,噪点越明显。
|
||||
* grain_scale: 噪点颗粒大小。数值越大,颗粒越大。
|
||||
* grain_sat: 噪点的色彩饱和度。0表示黑白噪点,数值越大,彩色越明显。
|
||||
* grain_shadows: 暗部噪点强度。
|
||||
* grain_highs: 亮部噪点强度。
|
||||
* blur_strength: 模糊强度。数值越大越模糊。
|
||||
* blur_focus_spread: 焦点扩散范围。数值越大,清晰的范围越大。
|
||||
* focal_depth: 模拟虚焦的焦点距离。0表示焦点在最远,1表示焦点在最近。此项设置只在depth_map有输入时才生效。
|
||||
|
||||
|
||||
### <a id="table1">ColorMap</a>
|
||||
伪彩色热力图效果。
|
||||

|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,89 @@
|
||||
from .imagefunc import *
|
||||
|
||||
|
||||
NODE_NAME = 'Film'
|
||||
|
||||
class Film:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", ), #
|
||||
"center_x": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
|
||||
"center_y": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
|
||||
"saturation": ("FLOAT", {"default": 1, "min": 0.01, "max": 3, "step": 0.01}),
|
||||
"vignette_intensity": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
|
||||
"grain_power": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01}),
|
||||
"grain_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10, "step": 0.1}),
|
||||
"grain_sat": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
|
||||
"grain_shadows": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}),
|
||||
"grain_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01}),
|
||||
"blur_strength": ("INT", {"default": 90, "min": 0, "max": 256, "step": 1}),
|
||||
"blur_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1}),
|
||||
"focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"depth_map": ("IMAGE",), #
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = 'film'
|
||||
CATEGORY = '😺dzNodes/LayerFilter'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def film(self, image, center_x, center_y, saturation, vignette_intensity,
|
||||
grain_power, grain_scale, grain_sat, grain_shadows, grain_highs,
|
||||
blur_strength, blur_focus_spread, focal_depth,
|
||||
depth_map=None
|
||||
):
|
||||
|
||||
ret_images = []
|
||||
|
||||
for i in image:
|
||||
i = torch.unsqueeze(i, 0)
|
||||
_canvas = tensor2pil(i).convert('RGB')
|
||||
|
||||
if saturation != 1:
|
||||
color_image = ImageEnhance.Color(_canvas)
|
||||
_canvas = color_image.enhance(factor= saturation)
|
||||
|
||||
if blur_strength:
|
||||
if depth_map is not None:
|
||||
depth_map = tensor2pil(depth_map).convert('L').convert('RGB')
|
||||
if depth_map.size != _canvas.size:
|
||||
depth_map.resize((_canvas.size), Image.BILINEAR)
|
||||
_canvas = depthblur_image(_canvas, depth_map, blur_strength, focal_depth, blur_focus_spread)
|
||||
else:
|
||||
_canvas = radialblur_image(_canvas, blur_strength, center_x, center_y, blur_focus_spread * 2)
|
||||
|
||||
if vignette_intensity:
|
||||
# adjust image gamma and saturation
|
||||
_canvas = gamma_trans(_canvas, 1 - vignette_intensity / 3)
|
||||
color_image = ImageEnhance.Color(_canvas)
|
||||
_canvas = color_image.enhance(factor= 1+ vignette_intensity / 3)
|
||||
# add vignette
|
||||
_canvas = vignette_image(_canvas, vignette_intensity, center_x, center_y)
|
||||
|
||||
if grain_power:
|
||||
_canvas = filmgrain_image(_canvas, grain_scale, grain_power, grain_shadows, grain_highs, grain_sat)
|
||||
|
||||
ret_image = _canvas
|
||||
ret_images.append(pil2tensor(ret_image))
|
||||
|
||||
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerFilter: Film": Film
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerFilter: Film": "LayerFilter: Film"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = "1.0.2"
|
||||
@@ -0,0 +1,116 @@
|
||||
# Filmgrainer - by Lars Ole Pontoppidan - MIT License
|
||||
|
||||
from PIL import Image, ImageFilter
|
||||
import os
|
||||
import tempfile
|
||||
import numpy as np
|
||||
|
||||
import filmgrainer.graingamma as graingamma
|
||||
import filmgrainer.graingen as graingen
|
||||
|
||||
|
||||
def _grainTypes(typ):
|
||||
# After rescaling to make different grain sizes, the standard deviation
|
||||
# of the pixel values change. The following values of grain size and power
|
||||
# have been imperically chosen to end up with approx the same standard
|
||||
# deviation in the result:
|
||||
if typ == 1:
|
||||
return (0.8, 63) # more interesting fine grain
|
||||
elif typ == 2:
|
||||
return (1, 45) # basic fine grain
|
||||
elif typ == 3:
|
||||
return (1.5, 50) # coarse grain
|
||||
elif typ == 4:
|
||||
return (1.6666, 50) # coarser grain
|
||||
else:
|
||||
raise ValueError("Unknown grain type: " + str(typ))
|
||||
|
||||
# Grain mask cache
|
||||
MASK_CACHE_PATH = os.path.join(tempfile.gettempdir(), "mask-cache")
|
||||
|
||||
def _getGrainMask(img_width:int, img_height:int, saturation:float, grayscale:bool, grain_size:float, grain_gauss:float, seed):
|
||||
if grayscale:
|
||||
str_sat = "BW"
|
||||
sat = -1.0 # Graingen makes a grayscale image if sat is negative
|
||||
else:
|
||||
str_sat = str(saturation)
|
||||
sat = saturation
|
||||
|
||||
filename = MASK_CACHE_PATH + "grain-%d-%d-%s-%s-%s-%d.png" % (
|
||||
img_width, img_height, str_sat, str(grain_size), str(grain_gauss), seed)
|
||||
if os.path.isfile(filename):
|
||||
# print("Reusing: %s" % filename)
|
||||
mask = Image.open(filename)
|
||||
else:
|
||||
mask = graingen.grainGen(img_width, img_height, grain_size, grain_gauss, sat, seed)
|
||||
# print("Saving: %s" % filename)
|
||||
if not os.path.isdir(MASK_CACHE_PATH):
|
||||
os.mkdir(MASK_CACHE_PATH)
|
||||
mask.save(filename, format="png", compress_level=1)
|
||||
return mask
|
||||
|
||||
|
||||
def process(image:Image, scale:float, src_gamma:float, grain_power:float, shadows:float,
|
||||
highs:float, grain_type:int, grain_sat:float, gray_scale:bool, sharpen:int, seed:int):
|
||||
|
||||
# image = np.clip(image, 0, 1) # Ensure the values are within [0, 1]
|
||||
# image = (image * 255).astype(np.uint8)
|
||||
# img = Image.fromarray(image).convert("RGB")
|
||||
img = image
|
||||
org_width = img.size[0]
|
||||
org_height = img.size[1]
|
||||
|
||||
if scale != 1.0:
|
||||
# print("Scaling source image ...")
|
||||
img = img.resize((int(org_width / scale), int(org_height / scale)),
|
||||
resample = Image.LANCZOS)
|
||||
|
||||
img_width = img.size[0]
|
||||
img_height = img.size[1]
|
||||
# print("Size: %d x %d" % (img_width, img_height))
|
||||
|
||||
# print("Calculating map ...")
|
||||
map = graingamma.Map.calculate(src_gamma, grain_power, shadows, highs)
|
||||
# map.saveToFile("map.png")
|
||||
|
||||
# print("Calculating grain stock ...")
|
||||
(grain_size, grain_gauss) = _grainTypes(grain_type)
|
||||
mask = _getGrainMask(img_width, img_height, grain_sat, gray_scale, grain_size, grain_gauss, seed)
|
||||
|
||||
mask_pixels = mask.load()
|
||||
img_pixels = img.load()
|
||||
|
||||
# Instead of calling map.lookup(a, b) for each pixel, use the map directly:
|
||||
lookup = map.map
|
||||
|
||||
if gray_scale:
|
||||
# print("Film graining image ... (grayscale)")
|
||||
for y in range(0, img_height):
|
||||
for x in range(0, img_width):
|
||||
m = mask_pixels[x, y]
|
||||
(r, g, b) = img_pixels[x, y]
|
||||
gray = int(0.21*r + 0.72*g + 0.07*b)
|
||||
#gray_lookup = map.lookup(gray, m)
|
||||
gray_lookup = lookup[gray, m]
|
||||
img_pixels[x, y] = (gray_lookup, gray_lookup, gray_lookup)
|
||||
else:
|
||||
# print("Film graining image ...")
|
||||
for y in range(0, img_height):
|
||||
for x in range(0, img_width):
|
||||
(mr, mg, mb) = mask_pixels[x, y]
|
||||
(r, g, b) = img_pixels[x, y]
|
||||
r = lookup[r, mr]
|
||||
g = lookup[g, mg]
|
||||
b = lookup[b, mb]
|
||||
img_pixels[x, y] = (r, g, b)
|
||||
|
||||
if scale != 1.0:
|
||||
# print("Scaling image back to original size ...")
|
||||
img = img.resize((org_width, org_height), resample = Image.LANCZOS)
|
||||
|
||||
if sharpen > 0:
|
||||
# print("Sharpening image: %d pass ..." % sharpen)
|
||||
for x in range(sharpen):
|
||||
img = img.filter(ImageFilter.SHARPEN)
|
||||
|
||||
return np.array(img).astype('float32') / 255.0
|
||||
@@ -0,0 +1,113 @@
|
||||
import numpy as np
|
||||
|
||||
_ShadowEnd = 160
|
||||
_HighlightStart = 200
|
||||
|
||||
|
||||
def _gammaCurve(gamma, x):
|
||||
""" Returns from 0.0 to 1.0"""
|
||||
return pow((x / 255.0), (1.0 / gamma))
|
||||
|
||||
|
||||
def _calcDevelopment(shadow_level, high_level, x):
|
||||
"""
|
||||
This function returns a development like this:
|
||||
|
||||
(return)
|
||||
^
|
||||
|
|
||||
0.5 | o - o <-- mids level, always 0.5
|
||||
| - -
|
||||
| - -
|
||||
| - o <-- high_level eg. 0.25
|
||||
| -
|
||||
| o <-- shadow_level eg. 0.15
|
||||
|
|
||||
0 -+-----------------|-------|------------|-----> x (input)
|
||||
0 160 200 255
|
||||
"""
|
||||
if x < _ShadowEnd:
|
||||
power = 0.5 - (_ShadowEnd - x) * (0.5 - shadow_level) / _ShadowEnd
|
||||
elif x < _HighlightStart:
|
||||
power = 0.5
|
||||
else:
|
||||
power = 0.5 - (x - _HighlightStart) * (0.5 - high_level) / (255 - _HighlightStart)
|
||||
|
||||
return power
|
||||
|
||||
class Map:
|
||||
def __init__(self, map):
|
||||
self.map = map
|
||||
|
||||
@staticmethod
|
||||
def calculate(src_gamma, noise_power, shadow_level, high_level) -> 'Map':
|
||||
map = np.zeros([256, 256], dtype=np.uint8)
|
||||
|
||||
# We need to level off top end and low end to leave room for the noise to breathe
|
||||
crop_top = noise_power * high_level / 12
|
||||
crop_low = noise_power * shadow_level / 20
|
||||
|
||||
pic_scale = 1 - (crop_top + crop_low)
|
||||
pic_offs = 255 * crop_low
|
||||
|
||||
for src_value in range(0, 256):
|
||||
# Gamma compensate picture source value itself
|
||||
pic_value = _gammaCurve(src_gamma, src_value) * 255.0
|
||||
|
||||
# In the shadows we want noise gamma to be 0.5, in the highs, 2.0:
|
||||
gamma = pic_value * (1.5 / 256) + 0.5
|
||||
gamma_offset = _gammaCurve(gamma, 128)
|
||||
|
||||
# Power is determined by the development
|
||||
power = _calcDevelopment(shadow_level, high_level, pic_value)
|
||||
|
||||
for noise_value in range(0, 256):
|
||||
gamma_compensated = _gammaCurve(gamma, noise_value) - gamma_offset
|
||||
value = pic_value * pic_scale + pic_offs + 255.0 * power * noise_power * gamma_compensated
|
||||
if value < 0:
|
||||
value = 0
|
||||
elif value < 255.0:
|
||||
value = int(value)
|
||||
else:
|
||||
value = 255
|
||||
map[src_value, noise_value] = value
|
||||
|
||||
return Map(map)
|
||||
|
||||
def lookup(self, pic_value, noise_value):
|
||||
return self.map[pic_value, noise_value]
|
||||
|
||||
def saveToFile(self, filename):
|
||||
from PIL import Image
|
||||
img = Image.fromarray(self.map)
|
||||
img.save(filename)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
def plotfunc(x_min, x_max, step, func):
|
||||
x_all = np.arange(x_min, x_max, step)
|
||||
y = []
|
||||
for x in x_all:
|
||||
y.append(func(x))
|
||||
|
||||
plt.figure()
|
||||
plt.plot(x_all, y)
|
||||
plt.grid()
|
||||
|
||||
def development1(x):
|
||||
return _calcDevelopment(0.2, 0.3, x)
|
||||
|
||||
def gamma05(x):
|
||||
return _gammaCurve(0.5, x)
|
||||
def gamma1(x):
|
||||
return _gammaCurve(1, x)
|
||||
def gamma2(x):
|
||||
return _gammaCurve(2, x)
|
||||
|
||||
plotfunc(0.0, 255.0, 1.0, development1)
|
||||
plotfunc(0.0, 255.0, 1.0, gamma05)
|
||||
plotfunc(0.0, 255.0, 1.0, gamma1)
|
||||
plotfunc(0.0, 255.0, 1.0, gamma2)
|
||||
plt.show()
|
||||
@@ -0,0 +1,61 @@
|
||||
from PIL import Image
|
||||
import random
|
||||
import numpy as np
|
||||
|
||||
def _makeGrayNoise(width, height, power):
|
||||
buffer = np.zeros([height, width], dtype=int)
|
||||
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
buffer[y, x] = random.gauss(128, power)
|
||||
buffer = buffer.clip(0, 255)
|
||||
return Image.fromarray(buffer.astype(dtype=np.uint8))
|
||||
|
||||
def _makeRgbNoise(width, height, power, saturation):
|
||||
buffer = np.zeros([height, width, 3], dtype=int)
|
||||
intens_power = power * (1.0 - saturation)
|
||||
for y in range(0, height):
|
||||
for x in range(0, width):
|
||||
intens = random.gauss(128, intens_power)
|
||||
buffer[y, x, 0] = random.gauss(0, power) * saturation + intens
|
||||
buffer[y, x, 1] = random.gauss(0, power) * saturation + intens
|
||||
buffer[y, x, 2] = random.gauss(0, power) * saturation + intens
|
||||
|
||||
buffer = buffer.clip(0, 255)
|
||||
return Image.fromarray(buffer.astype(dtype=np.uint8))
|
||||
|
||||
|
||||
def grainGen(width, height, grain_size, power, saturation, seed = 1):
|
||||
# A grain_size of 1 means the noise buffer will be made 1:1
|
||||
# A grain_size of 2 means the noise buffer will be resampled 1:2
|
||||
noise_width = int(width / grain_size)
|
||||
noise_height = int(height / grain_size)
|
||||
random.seed(seed)
|
||||
|
||||
if saturation < 0.0:
|
||||
print("Making B/W grain, width: %d, height: %d, grain-size: %s, power: %s, seed: %d" % (
|
||||
noise_width, noise_height, str(grain_size), str(power), seed))
|
||||
img = _makeGrayNoise(noise_width, noise_height, power)
|
||||
else:
|
||||
print("Making RGB grain, width: %d, height: %d, saturation: %s, grain-size: %s, power: %s, seed: %d" % (
|
||||
noise_width, noise_height, str(saturation), str(grain_size), str(power), seed))
|
||||
img = _makeRgbNoise(noise_width, noise_height, power, saturation)
|
||||
|
||||
# Resample
|
||||
if grain_size != 1.0:
|
||||
img = img.resize((width, height), resample = Image.LANCZOS)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) == 8:
|
||||
width = int(sys.argv[2])
|
||||
height = int(sys.argv[3])
|
||||
grain_size = float(sys.argv[4])
|
||||
power = float(sys.argv[5])
|
||||
sat = float(sys.argv[6])
|
||||
seed = int(sys.argv[7])
|
||||
out = grainGen(width, height, grain_size, power, sat, seed)
|
||||
out.save(sys.argv[1])
|
||||
@@ -0,0 +1,32 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
def generate_blurred_images(image, blur_strength, steps, focus_spread=1):
|
||||
blurred_images = []
|
||||
for step in range(1, steps + 1):
|
||||
# Adjust the curve based on the curve_weight
|
||||
blur_factor = (step / steps) ** focus_spread * blur_strength
|
||||
blur_size = max(1, int(blur_factor))
|
||||
blur_size = blur_size if blur_size % 2 == 1 else blur_size + 1 # Ensure blur_size is odd
|
||||
|
||||
# Apply Gaussian Blur
|
||||
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
|
||||
blurred_images.append(blurred_image)
|
||||
return blurred_images
|
||||
|
||||
def apply_blurred_images(image, blurred_images, mask):
|
||||
steps = len(blurred_images) # Calculate the number of steps based on the blurred images provided
|
||||
final_image = np.zeros_like(image)
|
||||
step_size = 1.0 / steps
|
||||
for i, blurred_image in enumerate(blurred_images):
|
||||
# Calculate the mask for the current step
|
||||
current_mask = np.clip((mask - i * step_size) * steps, 0, 1)
|
||||
next_mask = np.clip((mask - (i + 1) * step_size) * steps, 0, 1)
|
||||
blend_mask = current_mask - next_mask
|
||||
|
||||
# Apply the blend mask
|
||||
final_image += blend_mask[:, :, np.newaxis] * blurred_image
|
||||
|
||||
# Ensure no division by zero; add the original image for areas without blurring
|
||||
final_image += (1 - np.clip(mask * steps, 0, 1))[:, :, np.newaxis] * image
|
||||
return final_image
|
||||
+126
-2
@@ -1,8 +1,11 @@
|
||||
'''Image process functions for ComfyUI nodes
|
||||
by chflame https://github.com/chflame163
|
||||
'''
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import copy
|
||||
import re
|
||||
import json
|
||||
import math
|
||||
@@ -24,7 +27,8 @@ from colour.io.luts.iridas_cube import read_LUT_IridasCube, LUT3D, LUT3x1D
|
||||
from typing import Union
|
||||
import folder_paths as COMFY_FOLDER_PATH
|
||||
from .briarmbg import BriaRMBG
|
||||
|
||||
from .filmgrainer import processing as processing_utils
|
||||
from .filmgrainer import filmgrainer as filmgrainer
|
||||
|
||||
def log(message:str, message_type:str='info'):
|
||||
name = 'LayerStyle'
|
||||
@@ -342,6 +346,126 @@ def motion_blur(image:Image, angle:int, blur:int) -> Image:
|
||||
ret_image = cv22pil(blurred)
|
||||
return ret_image
|
||||
|
||||
def __apply_vignette(image, vignette):
|
||||
# If image needs to be normalized (0-1 range)
|
||||
needs_normalization = image.max() > 1
|
||||
if needs_normalization:
|
||||
image = image.astype(np.float32) / 255
|
||||
final_image = np.clip(image * vignette[..., np.newaxis], 0, 1)
|
||||
if needs_normalization:
|
||||
final_image = (final_image * 255).astype(np.uint8)
|
||||
return final_image
|
||||
def vignette_image(image:Image, intensity: float, center_x: float, center_y: float) -> Image:
|
||||
image = pil2tensor(image)
|
||||
_, height, width, _ = image.shape
|
||||
# Generate the vignette for each image in the batch
|
||||
# Create linear space but centered around the provided center point ratios
|
||||
x = np.linspace(-1, 1, width)
|
||||
y = np.linspace(-1, 1, height)
|
||||
X, Y = np.meshgrid(x - (2 * center_x - 1), y - (2 * center_y - 1))
|
||||
# Calculate distances to the furthest corner
|
||||
distances_to_corners = [
|
||||
np.sqrt((0 - center_x) ** 2 + (0 - center_y) ** 2),
|
||||
np.sqrt((1 - center_x) ** 2 + (0 - center_y) ** 2),
|
||||
np.sqrt((0 - center_x) ** 2 + (1 - center_y) ** 2),
|
||||
np.sqrt((1 - center_x) ** 2 + (1 - center_y) ** 2)
|
||||
]
|
||||
max_distance_to_corner = np.max(distances_to_corners)
|
||||
radius = np.sqrt(X ** 2 + Y ** 2)
|
||||
radius = radius / (max_distance_to_corner * np.sqrt(2)) # Normalize radius
|
||||
opacity = np.clip(intensity, 0, 1)
|
||||
vignette = 1 - radius * opacity
|
||||
tensor_image = image.numpy()
|
||||
# Apply vignette
|
||||
vignette_image = __apply_vignette(tensor_image, vignette)
|
||||
return tensor2pil(torch.from_numpy(vignette_image).unsqueeze(0))
|
||||
|
||||
def filmgrain_image(image:Image, scale:float, grain_power:float,
|
||||
shadows:float, highs:float, grain_sat:float,
|
||||
sharpen:int=1, grain_type:int=4, src_gamma:float=1.0,
|
||||
gray_scale:bool=False, seed:int=0) -> Image:
|
||||
# image = pil2tensor(image)
|
||||
# grain_type, 1=fine, 2=fine simple, 3=coarse, 4=coarser
|
||||
grain_type_index = 3
|
||||
|
||||
# Apply grain
|
||||
grain_image = filmgrainer.process(image, scale=scale, src_gamma=src_gamma, grain_power=grain_power,
|
||||
shadows=shadows, highs=highs, grain_type=grain_type_index,
|
||||
grain_sat=grain_sat, gray_scale=gray_scale, sharpen=sharpen, seed=seed)
|
||||
return tensor2pil(torch.from_numpy(grain_image).unsqueeze(0))
|
||||
|
||||
def __apply_radialblur(image, blur_strength, radial_mask, focus_spread, steps):
|
||||
needs_normalization = image.max() > 1
|
||||
if needs_normalization:
|
||||
image = image.astype(np.float32) / 255
|
||||
blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread)
|
||||
final_image = processing_utils.apply_blurred_images(image, blurred_images, radial_mask)
|
||||
if needs_normalization:
|
||||
final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8)
|
||||
return final_image
|
||||
|
||||
def radialblur_image(image:Image, blur_strength:float, center_x:float, center_y:float, focus_spread:float, steps:int=5) -> Image:
|
||||
width, height = image.size
|
||||
image = pil2tensor(image)
|
||||
if image.dim() == 4:
|
||||
image = image[0]
|
||||
|
||||
# _, height, width, = image.shape
|
||||
# Generate the vignette for each image in the batch
|
||||
c_x, c_y = int(width * center_x), int(height * center_y)
|
||||
# Calculate distances to all corners from the center
|
||||
distances_to_corners = [
|
||||
np.sqrt((c_x - 0)**2 + (c_y - 0)**2),
|
||||
np.sqrt((c_x - width)**2 + (c_y - 0)**2),
|
||||
np.sqrt((c_x - 0)**2 + (c_y - height)**2),
|
||||
np.sqrt((c_x - width)**2 + (c_y - height)**2)
|
||||
]
|
||||
max_distance_to_corner = max(distances_to_corners)
|
||||
# Create and adjust radial mask
|
||||
X, Y = np.meshgrid(np.arange(width) - c_x, np.arange(height) - c_y)
|
||||
radial_mask = np.sqrt(X**2 + Y**2) / max_distance_to_corner
|
||||
tensor_image = image.numpy()
|
||||
# Apply blur
|
||||
blur_image = __apply_radialblur(tensor_image, blur_strength, radial_mask, focus_spread, steps)
|
||||
return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0))
|
||||
|
||||
def __apply_depthblur(image, depth_map, blur_strength, focal_depth, focus_spread, steps):
|
||||
# Normalize the input image if needed
|
||||
needs_normalization = image.max() > 1
|
||||
if needs_normalization:
|
||||
image = image.astype(np.float32) / 255
|
||||
# Normalize the depth map if needed
|
||||
depth_map = depth_map.astype(np.float32) / 255 if depth_map.max() > 1 else depth_map
|
||||
# Resize depth map to match the image dimensions
|
||||
depth_map_resized = cv2.resize(depth_map, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_LINEAR)
|
||||
if len(depth_map_resized.shape) > 2:
|
||||
depth_map_resized = cv2.cvtColor(depth_map_resized, cv2.COLOR_BGR2GRAY)
|
||||
# Adjust the depth map based on the focal plane
|
||||
depth_mask = np.abs(depth_map_resized - focal_depth)
|
||||
depth_mask = np.clip(depth_mask / np.max(depth_mask), 0, 1)
|
||||
# Generate blurred versions of the image
|
||||
blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread)
|
||||
# Use the adjusted depth map as a mask for applying blurred images
|
||||
final_image = processing_utils.apply_blurred_images(image, blurred_images, depth_mask)
|
||||
# Convert back to original range if the image was normalized
|
||||
if needs_normalization:
|
||||
final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8)
|
||||
return final_image
|
||||
|
||||
def depthblur_image(image:Image, depth_map:Image, blur_strength:float, focal_depth:float, focus_spread:float, steps:int=5) -> Image:
|
||||
width, height = image.size
|
||||
image = pil2tensor(image)
|
||||
depth_map = pil2tensor(depth_map)
|
||||
if image.dim() == 4:
|
||||
image = image[0]
|
||||
if depth_map.dim() == 4:
|
||||
depth_map = depth_map[0]
|
||||
tensor_image = image.numpy()
|
||||
tensor_image_depth = depth_map.numpy()
|
||||
# Apply blur
|
||||
blur_image = __apply_depthblur(tensor_image, tensor_image_depth, blur_strength, focal_depth, focus_spread, steps)
|
||||
return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0))
|
||||
|
||||
def fit_resize_image(image:Image, target_width:int, target_height:int, fit:str, resize_sampler:str, background_color:str = '#000000') -> Image:
|
||||
image = image.convert('RGB')
|
||||
orig_width, orig_height = image.size
|
||||
|
||||
Reference in New Issue
Block a user