commit Film node

This commit is contained in:
chflame
2024-02-26 17:00:50 +08:00
parent fd48e41bcb
commit 06ab54d701
11 changed files with 584 additions and 2 deletions
+23
View File
@@ -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).
![image](image/film_example.png)
Node options:
![image](image/film_node.png)
* 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.
![image](image/colormap_result.png)
+23
View File
@@ -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](image/film_example.png)
节点选项说明:
![image](image/film_node.png)
* 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>
伪彩色热力图效果。
![image](image/colormap_result.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+89
View File
@@ -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"
}
+1
View File
@@ -0,0 +1 @@
__version__ = "1.0.2"
+116
View File
@@ -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
+113
View File
@@ -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()
+61
View File
@@ -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])
+32
View File
@@ -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
View File
@@ -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