* feat: transform depth nodes into a complete v3 depth toolkit * fix(ci): validate metadata on Python 3.10
155 lines
4.8 KiB
Python
155 lines
4.8 KiB
Python
"""Depth-map preview node for ComfyUI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
import folder_paths
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
try:
|
|
from .depth_nodes import (
|
|
NODE_CLASS_MAPPINGS as TOOL_NODE_CLASS_MAPPINGS,
|
|
NODE_DISPLAY_NAME_MAPPINGS as TOOL_NODE_DISPLAY_NAME_MAPPINGS,
|
|
)
|
|
except (ImportError, ModuleNotFoundError): # Standalone import used by pytest.
|
|
from depth_nodes import ( # type: ignore[no-redef]
|
|
NODE_CLASS_MAPPINGS as TOOL_NODE_CLASS_MAPPINGS,
|
|
NODE_DISPLAY_NAME_MAPPINGS as TOOL_NODE_DISPLAY_NAME_MAPPINGS,
|
|
)
|
|
|
|
|
|
def _as_pil(
|
|
image: Any,
|
|
*,
|
|
grayscale: bool = False,
|
|
bit_depth: int = 8,
|
|
) -> Image.Image:
|
|
"""Convert one ComfyUI IMAGE tensor to a web-safe PIL image."""
|
|
array = image.detach().cpu().float().numpy()
|
|
array = np.nan_to_num(array, nan=0.0, posinf=1.0, neginf=0.0)
|
|
array = np.clip(array, 0.0, 1.0)
|
|
|
|
if array.ndim == 2:
|
|
mode = "L"
|
|
elif array.ndim == 3 and array.shape[-1] == 1:
|
|
array = array[..., 0]
|
|
mode = "L"
|
|
elif array.ndim == 3 and array.shape[-1] >= 3:
|
|
array = array[..., :3]
|
|
mode = "RGB"
|
|
else:
|
|
raise ValueError(f"Expected an HxW, HxWx1, or HxWx3+ image, got {array.shape}.")
|
|
|
|
if grayscale and bit_depth == 16:
|
|
if mode == "RGB":
|
|
array = array[..., 0] * 0.2126 + array[..., 1] * 0.7152 + array[..., 2] * 0.0722
|
|
return Image.fromarray((array * 65535.0).round().astype(np.uint16))
|
|
converted = Image.fromarray((array * 255.0).round().astype(np.uint8), mode=mode)
|
|
return converted.convert("L" if grayscale else "RGB")
|
|
|
|
|
|
def _batch_item(batch: Any, index: int, target_count: int, name: str) -> Any:
|
|
count = len(batch)
|
|
if count == target_count:
|
|
return batch[index]
|
|
if count == 1:
|
|
return batch[0]
|
|
raise ValueError(
|
|
f"{name} has {count} images but the other input has {target_count}. "
|
|
"Batch sizes must match, or one input must contain a single image."
|
|
)
|
|
|
|
|
|
def _save_image(
|
|
image: Image.Image,
|
|
*,
|
|
prefix: str,
|
|
suffix: str,
|
|
batch_number: int,
|
|
) -> dict[str, str]:
|
|
output_dir = folder_paths.get_temp_directory()
|
|
full_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
|
|
prefix,
|
|
output_dir,
|
|
image.width,
|
|
image.height,
|
|
)
|
|
filename = filename.replace("%batch_num%", str(batch_number))
|
|
image_name = f"{filename}_{counter:05}_{suffix}.png"
|
|
image.save(os.path.join(full_folder, image_name), compress_level=1)
|
|
return {"filename": image_name, "subfolder": subfolder, "type": "temp"}
|
|
|
|
|
|
class DepthViewer:
|
|
"""Preview an image displaced by a matching depth map in an interactive 3D view."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"reference_image": ("IMAGE",),
|
|
"depth_map": ("IMAGE",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "IMAGE")
|
|
RETURN_NAMES = ("reference_passthrough", "depth_passthrough")
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "process_images"
|
|
CATEGORY = "visualization/3D"
|
|
DESCRIPTION = (
|
|
"Interactively previews an image as a depth-displaced mesh. "
|
|
"Supports image batches and browser-side PNG, OBJ, GLTF, and GLB export."
|
|
)
|
|
|
|
def process_images(self, reference_image, depth_map):
|
|
reference_count = len(reference_image)
|
|
depth_count = len(depth_map)
|
|
batch_count = max(reference_count, depth_count)
|
|
|
|
references: list[dict[str, str]] = []
|
|
depths: list[dict[str, str]] = []
|
|
for index in range(batch_count):
|
|
reference = _as_pil(
|
|
_batch_item(reference_image, index, batch_count, "reference_image")
|
|
)
|
|
depth = _as_pil(
|
|
_batch_item(depth_map, index, batch_count, "depth_map"),
|
|
grayscale=True,
|
|
bit_depth=16,
|
|
)
|
|
references.append(
|
|
_save_image(
|
|
reference,
|
|
prefix="depth_viewer",
|
|
suffix="reference",
|
|
batch_number=index,
|
|
)
|
|
)
|
|
depths.append(
|
|
_save_image(
|
|
depth,
|
|
prefix="depth_viewer",
|
|
suffix="depth",
|
|
batch_number=index,
|
|
)
|
|
)
|
|
|
|
return {
|
|
"ui": {"reference_image": references, "depth_map": depths},
|
|
"result": (reference_image, depth_map),
|
|
}
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {"DepthViewer": DepthViewer, **TOOL_NODE_CLASS_MAPPINGS}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"DepthViewer": "Depth Viewer Pro",
|
|
**TOOL_NODE_DISPLAY_NAME_MAPPINGS,
|
|
}
|
|
WEB_DIRECTORY = "./web"
|
|
|
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|