|
import tempfile |
|
from pathlib import Path |
|
|
|
import numpy as np |
|
import onnxruntime as ort |
|
import torch |
|
from PIL import Image |
|
|
|
from ..errors import ModelNotFound |
|
from ..log import mklog |
|
from ..utils import ( |
|
get_model_path, |
|
tensor2pil, |
|
tiles_infer, |
|
tiles_merge, |
|
tiles_split, |
|
) |
|
|
|
|
|
ort.disable_telemetry_events() |
|
log = mklog(__name__) |
|
|
|
|
|
|
|
def color_to_normals( |
|
color_img, overlap, progress_callback, *, save_temp=False |
|
): |
|
"""Compute a normal map from the given color map. |
|
|
|
'color_img' must be a numpy array in C,H,W format (with C as RGB). |
|
'overlap' must be one of 'SMALL', 'MEDIUM', 'LARGE'. |
|
""" |
|
temp_dir = Path(tempfile.mkdtemp()) if save_temp else None |
|
|
|
|
|
img = np.mean(color_img[:3], axis=0, keepdims=True) |
|
|
|
if temp_dir: |
|
Image.fromarray((img[0] * 255).astype(np.uint8)).save( |
|
temp_dir / "grayscale_img.png" |
|
) |
|
|
|
log.debug( |
|
"Converting color image to grayscale by taking " |
|
f"the mean over color channels: {img.shape}" |
|
) |
|
|
|
|
|
log.debug("DeepBump Color → Normals : tilling") |
|
tile_size = 256 |
|
overlaps = { |
|
"SMALL": tile_size // 6, |
|
"MEDIUM": tile_size // 4, |
|
"LARGE": tile_size // 2, |
|
} |
|
stride_size = tile_size - overlaps[overlap] |
|
tiles, paddings = tiles_split( |
|
img, (tile_size, tile_size), (stride_size, stride_size) |
|
) |
|
if temp_dir: |
|
for i, tile in enumerate(tiles): |
|
Image.fromarray((tile[0] * 255).astype(np.uint8)).save( |
|
temp_dir / f"tile_{i}.png" |
|
) |
|
|
|
|
|
log.debug("DeepBump Color → Normals : loading model") |
|
model = get_model_path("deepbump", "deepbump256.onnx") |
|
if not model or not model.exists(): |
|
raise ModelNotFound(f"deepbump ({model})") |
|
|
|
providers = [ |
|
"TensorrtExecutionProvider", |
|
"CUDAExecutionProvider", |
|
"CoreMLProvider", |
|
"CPUExecutionProvider", |
|
] |
|
available_providers = [ |
|
provider |
|
for provider in providers |
|
if provider in ort.get_available_providers() |
|
] |
|
|
|
if not available_providers: |
|
raise RuntimeError( |
|
"No valid ONNX Runtime providers available on this machine." |
|
) |
|
log.debug(f"Using ONNX providers: {available_providers}") |
|
ort_session = ort.InferenceSession( |
|
model.as_posix(), providers=available_providers |
|
) |
|
|
|
|
|
log.debug("DeepBump Color → Normals : generating") |
|
pred_tiles = tiles_infer( |
|
tiles, ort_session, progress_callback=progress_callback |
|
) |
|
|
|
if temp_dir: |
|
for i, pred_tile in enumerate(pred_tiles): |
|
Image.fromarray( |
|
(pred_tile.transpose(1, 2, 0) * 255).astype(np.uint8) |
|
).save(temp_dir / f"pred_tile_{i}.png") |
|
|
|
|
|
log.debug("DeepBump Color → Normals : merging") |
|
pred_img = tiles_merge( |
|
pred_tiles, |
|
(stride_size, stride_size), |
|
(3, img.shape[1], img.shape[2]), |
|
paddings, |
|
) |
|
|
|
if temp_dir: |
|
Image.fromarray( |
|
(pred_img.transpose(1, 2, 0) * 255).astype(np.uint8) |
|
).save(temp_dir / "merged_img.png") |
|
|
|
|
|
pred_img = normalize(pred_img) |
|
|
|
if temp_dir: |
|
Image.fromarray( |
|
(pred_img.transpose(1, 2, 0) * 255).astype(np.uint8) |
|
).save(temp_dir / "final_img.png") |
|
|
|
log.debug(f"Debug images saved in {temp_dir}") |
|
|
|
return pred_img |
|
|
|
|
|
|
|
def conv_1d(array, kernel_1d): |
|
"""Perform row by row 1D convolutions. |
|
|
|
of the given 2D image with the given 1D kernel. |
|
""" |
|
|
|
k_l = len(kernel_1d) |
|
|
|
assert k_l % 2 != 0 |
|
|
|
extended = np.pad(array, k_l // 2, mode="wrap") |
|
|
|
output = np.empty(array.shape) |
|
for i in range(array.shape[0]): |
|
output[i] = np.convolve( |
|
extended[i + (k_l // 2)], kernel_1d, mode="valid" |
|
) |
|
|
|
return output * -1 |
|
|
|
|
|
def gaussian_kernel(length, sigma): |
|
"""Return a 1D gaussian kernel of size 'length'.""" |
|
space = np.linspace(-(length - 1) / 2, (length - 1) / 2, length) |
|
kernel = np.exp(-0.5 * np.square(space) / np.square(sigma)) |
|
return kernel / np.sum(kernel) |
|
|
|
|
|
def normalize(np_array): |
|
"""Normalize all elements of the given numpy array to [0,1].""" |
|
return (np_array - np.min(np_array)) / ( |
|
np.max(np_array) - np.min(np_array) |
|
) |
|
|
|
|
|
def normals_to_curvature(normals_img, blur_radius, progress_callback): |
|
"""Compute a curvature map from the given normal map. |
|
|
|
'normals_img' must be a numpy array in C,H,W format (with C as RGB). |
|
'blur_radius' must be one of: |
|
'SMALLEST', 'SMALLER', 'SMALL', 'MEDIUM', 'LARGE', 'LARGER', 'LARGEST'. |
|
""" |
|
|
|
if progress_callback is not None: |
|
progress_callback(0, 4) |
|
diff_kernel = np.array([-1, 0, 1]) |
|
h_conv = conv_1d(normals_img[0, :, :], diff_kernel) |
|
if progress_callback is not None: |
|
progress_callback(1, 4) |
|
v_conv = conv_1d(-1 * normals_img[1, :, :].T, diff_kernel).T |
|
if progress_callback is not None: |
|
progress_callback(2, 4) |
|
|
|
|
|
edges_conv = h_conv + v_conv |
|
|
|
|
|
blur_factors = { |
|
"SMALLEST": 1 / 256, |
|
"SMALLER": 1 / 128, |
|
"SMALL": 1 / 64, |
|
"MEDIUM": 1 / 32, |
|
"LARGE": 1 / 16, |
|
"LARGER": 1 / 8, |
|
"LARGEST": 1 / 4, |
|
} |
|
if blur_radius not in blur_factors: |
|
raise ValueError(f"{blur_radius} not found in {blur_factors}") |
|
|
|
blur_radius_px = int( |
|
np.mean(normals_img.shape[1:3]) * blur_factors[blur_radius] |
|
) |
|
|
|
|
|
if blur_radius_px < 2: |
|
edges_conv = normalize(edges_conv) |
|
return np.stack([edges_conv, edges_conv, edges_conv]) |
|
|
|
|
|
if blur_radius_px % 2 == 0: |
|
blur_radius_px += 1 |
|
|
|
|
|
sigma = blur_radius_px // 8 |
|
if sigma == 0: |
|
sigma = 1 |
|
g_kernel = gaussian_kernel(blur_radius_px, sigma) |
|
h_blur = conv_1d(edges_conv, g_kernel) |
|
if progress_callback is not None: |
|
progress_callback(3, 4) |
|
v_blur = conv_1d(h_blur.T, g_kernel).T |
|
if progress_callback is not None: |
|
progress_callback(4, 4) |
|
|
|
|
|
curvature = normalize(v_blur) |
|
|
|
|
|
return np.stack([curvature, curvature, curvature]) |
|
|
|
|
|
|
|
def normals_to_grad(normals_img): |
|
return (normals_img[0] - 0.5) * 2, (normals_img[1] - 0.5) * 2 |
|
|
|
|
|
def copy_flip(grad_x, grad_y): |
|
"""Concat 4 flipped copies of input gradients (makes them wrap). |
|
|
|
Output is twice bigger in both dimensions. |
|
""" |
|
grad_x_top = np.hstack([grad_x, -np.flip(grad_x, axis=1)]) |
|
grad_x_bottom = np.hstack([np.flip(grad_x, axis=0), -np.flip(grad_x)]) |
|
new_grad_x = np.vstack([grad_x_top, grad_x_bottom]) |
|
|
|
grad_y_top = np.hstack([grad_y, np.flip(grad_y, axis=1)]) |
|
grad_y_bottom = np.hstack([-np.flip(grad_y, axis=0), -np.flip(grad_y)]) |
|
new_grad_y = np.vstack([grad_y_top, grad_y_bottom]) |
|
|
|
return new_grad_x, new_grad_y |
|
|
|
|
|
def frankot_chellappa(grad_x, grad_y, progress_callback=None): |
|
"""Frankot-Chellappa depth-from-gradient algorithm.""" |
|
if progress_callback is not None: |
|
progress_callback(0, 3) |
|
|
|
rows, cols = grad_x.shape |
|
|
|
rows_scale = (np.arange(rows) - (rows // 2 + 1)) / (rows - rows % 2) |
|
cols_scale = (np.arange(cols) - (cols // 2 + 1)) / (cols - cols % 2) |
|
|
|
u_grid, v_grid = np.meshgrid(cols_scale, rows_scale) |
|
|
|
u_grid = np.fft.ifftshift(u_grid) |
|
v_grid = np.fft.ifftshift(v_grid) |
|
|
|
if progress_callback is not None: |
|
progress_callback(1, 3) |
|
|
|
grad_x_F = np.fft.fft2(grad_x) |
|
grad_y_F = np.fft.fft2(grad_y) |
|
|
|
if progress_callback is not None: |
|
progress_callback(2, 3) |
|
|
|
nominator = (-1j * u_grid * grad_x_F) + (-1j * v_grid * grad_y_F) |
|
denominator = (u_grid**2) + (v_grid**2) + 1e-16 |
|
|
|
Z_F = nominator / denominator |
|
Z_F[0, 0] = 0.0 |
|
|
|
Z = np.real(np.fft.ifft2(Z_F)) |
|
|
|
if progress_callback is not None: |
|
progress_callback(3, 3) |
|
|
|
return (Z - np.min(Z)) / (np.max(Z) - np.min(Z)) |
|
|
|
|
|
def normals_to_height(normals_img, seamless, progress_callback): |
|
"""Computes a height map from the given normal map. 'normals_img' must be a numpy array |
|
in C,H,W format (with C as RGB). 'seamless' is a bool that should indicates if 'normals_img' |
|
is seamless. |
|
""" |
|
|
|
flip_img = np.flip(normals_img, axis=1) |
|
|
|
|
|
grad_x, grad_y = normals_to_grad(flip_img) |
|
grad_x = np.flip(grad_x, axis=0) |
|
grad_y = np.flip(grad_y, axis=0) |
|
|
|
|
|
if not seamless: |
|
grad_x, grad_y = copy_flip(grad_x, grad_y) |
|
|
|
|
|
pred_img = frankot_chellappa( |
|
-grad_x, grad_y, progress_callback=progress_callback |
|
) |
|
|
|
|
|
if not seamless: |
|
height, width = normals_img.shape[1], normals_img.shape[2] |
|
pred_img = pred_img[:height, :width] |
|
|
|
|
|
return np.stack([pred_img, pred_img, pred_img]) |
|
|
|
|
|
|
|
class MTB_DeepBump: |
|
"""Normal & height maps generation from single pictures""" |
|
|
|
@classmethod |
|
def INPUT_TYPES(cls): |
|
return { |
|
"required": { |
|
"image": ("IMAGE",), |
|
"mode": ( |
|
[ |
|
"Color to Normals", |
|
"Normals to Curvature", |
|
"Normals to Height", |
|
], |
|
), |
|
"color_to_normals_overlap": (["SMALL", "MEDIUM", "LARGE"],), |
|
"normals_to_curvature_blur_radius": ( |
|
[ |
|
"SMALLEST", |
|
"SMALLER", |
|
"SMALL", |
|
"MEDIUM", |
|
"LARGE", |
|
"LARGER", |
|
"LARGEST", |
|
], |
|
), |
|
"normals_to_height_seamless": ("BOOLEAN", {"default": True}), |
|
}, |
|
} |
|
|
|
RETURN_TYPES = ("IMAGE",) |
|
FUNCTION = "apply" |
|
|
|
CATEGORY = "mtb/textures" |
|
|
|
def apply( |
|
self, |
|
*, |
|
image, |
|
mode="Color to Normals", |
|
color_to_normals_overlap="SMALL", |
|
normals_to_curvature_blur_radius="SMALL", |
|
normals_to_height_seamless=True, |
|
): |
|
images = tensor2pil(image) |
|
out_images = [] |
|
|
|
for image in images: |
|
log.debug(f"Input image shape: {image}") |
|
|
|
in_img = np.transpose(image, (2, 0, 1)) / 255 |
|
log.debug(f"transposed for deep image shape: {in_img.shape}") |
|
out_img = None |
|
|
|
|
|
if mode == "Color to Normals": |
|
out_img = color_to_normals( |
|
in_img, color_to_normals_overlap, None |
|
) |
|
if mode == "Normals to Curvature": |
|
out_img = normals_to_curvature( |
|
in_img, normals_to_curvature_blur_radius, None |
|
) |
|
if mode == "Normals to Height": |
|
out_img = normals_to_height( |
|
in_img, normals_to_height_seamless, None |
|
) |
|
|
|
if out_img is not None: |
|
log.debug(f"Output image shape: {out_img.shape}") |
|
out_images.append( |
|
torch.from_numpy( |
|
np.transpose(out_img, (1, 2, 0)).astype(np.float32) |
|
).unsqueeze(0) |
|
) |
|
else: |
|
log.error("No out img... This should not happen") |
|
for outi in out_images: |
|
log.debug(f"Shape fed to utils: {outi.shape}") |
|
return (torch.cat(out_images, dim=0),) |
|
|
|
|
|
__nodes__ = [MTB_DeepBump] |
|
|