Files
KAVVATARE-ComfyUI_RightEyeD…/VideoRightEyeNode.py
T
2025-05-24 02:16:57 +09:00

239 lines
11 KiB
Python

import torch
import numpy as np
import os
import sys
from typing import Union, List
from PIL import Image
import cv2
file_dir = os.path.dirname(__file__)
sys.path.append(file_dir)
import stereoimage_generation as sig
from comfy.utils import ProgressBar
def tensor2np(tensor: torch.Tensor) -> np.ndarray:
if tensor.dim() == 4: # Batch of images
tensor = tensor[0] # Assuming we take the first image in the batch
np_array = tensor.cpu().numpy()
np_array = np.clip(255.0 * np_array, 0, 255).astype(np.uint8)
if np_array.shape[0] == 3: # Convert from (3, H, W) to (H, W, 3)
np_array = np_array.transpose(1, 2, 0)
return np_array
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
if isinstance(img_np, list):
return torch.cat([np2tensor(img) for img in img_np], dim=0)
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
class VideoRightEyeNode:
"""
動画用の右目視差生成ノード
入力された動画フレームを左目として、右目用の視差動画のみを生成
メモリ効率を重視したシンプルな実装
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",), # 左目動画フレーム(バッチ)
"depth_maps": ("IMAGE",), # 各フレームの深度マップ
"fill_technique": ([
'No fill', 'No fill - Reverse projection', 'Imperfect fill - Hybrid Edge', 'Fill - Naive',
'Fill - Naive interpolating', 'Fill - Polylines Soft', 'Fill - Polylines Sharp',
'Fill - Post-fill', 'Fill - Reverse projection with Post-fill', 'Fill - Hybrid Edge with fill'
], {"default": "Fill - Polylines Soft"}),
},
"optional": {
"divergence": ("FLOAT", {"default": 3.5, "min": 0.05, "max": 15, "step": 0.01}),
"separation": ("FLOAT", {"default": 0, "min": -5, "max": 5, "step": 0.01}),
"stereo_balance": ("FLOAT", {"default": 0, "min": -0.95, "max": 0.95, "step": 0.05}),
"stereo_offset_exponent": ("FLOAT", {"default": 2, "min": 1, "max": 2, "step": 1}),
"depth_blur_sigma": ("FLOAT", {"default": 0, "min": 0, "max": 10, "step": 0.1}),
"depth_blur_edge_threshold": ("FLOAT", {"default": 40, "min": 0.1, "max": 100, "step": 0.1}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",) # 右目動画のみを出力
FUNCTION = "generate_right_eye"
CATEGORY = "image/3d"
def generate_right_eye(self, images, depth_maps, divergence=3.5, separation=0,
stereo_balance=0, stereo_offset_exponent=2, fill_technique="Fill - Polylines Soft",
depth_blur_sigma=0, depth_blur_edge_threshold=40):
fill_technique_mapping = {
'No fill': 'none',
'No fill - Reverse projection': 'inverse',
'Imperfect fill - Hybrid Edge': 'hybrid_edge',
'Fill - Naive': 'naive',
'Fill - Naive interpolating': 'naive_interpolating',
'Fill - Polylines Soft': 'polylines_soft',
'Fill - Polylines Sharp': 'polylines_sharp',
'Fill - Post-fill': 'none_post',
'Fill - Reverse projection with Post-fill': 'inverse_post',
'Fill - Hybrid Edge with fill': 'hybrid_edge_plus'
}
fill_technique = fill_technique_mapping.get(fill_technique, 'none')
right_eye_frames = []
total_frames = len(images)
pbar = ProgressBar(total_frames)
for i in range(total_frames):
# 現在のフレームを処理
left_frame = tensor2np(images[i:i+1])
depth_map = tensor2np(depth_maps[i:i+1])
# 深度マップをグレースケールに変換
if len(depth_map.shape) == 3 and depth_map.shape[2] == 3:
depth_map = np.dot(depth_map[..., :3], [0.2989, 0.5870, 0.1140]).astype(np.uint8)
# サイズを合わせる
if left_frame.shape[:2] != depth_map.shape:
depth_map = np.array(Image.fromarray(depth_map).resize((left_frame.shape[1], left_frame.shape[0])))
# 右目画像を生成(divergenceを負にして右目用に)
output = sig.create_stereoimages(
left_frame, depth_map,
-1.0 * divergence, # 右目用に反転
separation,
['left-only'], # 単一画像として出力
stereo_balance,
stereo_offset_exponent,
fill_technique,
depth_blur_sigma,
depth_blur_edge_threshold,
direction_aware_depth_blur=False, # メモリ節約のため無効
return_modified_depth=False # 深度マップは返さない
)
# 結果を取得(単一画像のリスト)
right_frame = np.array(output[0])
# テンソルに変換して追加
right_eye_frames.append(np2tensor(right_frame))
pbar.update(1)
# すべてのフレームを結合して返す
return (torch.cat(right_eye_frames),)
# 既存のRightEyeImageNodeもそのまま残す(単一画像用)
class RightEyeImageNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"depth_map": ("IMAGE",),
"fill_technique": ([
'No fill', 'No fill - Reverse projection', 'Imperfect fill - Hybrid Edge', 'Fill - Naive',
'Fill - Naive interpolating', 'Fill - Polylines Soft', 'Fill - Polylines Sharp',
'Fill - Post-fill', 'Fill - Reverse projection with Post-fill', 'Fill - Hybrid Edge with fill'
], {"default": "Fill - Polylines Soft"}),
},
"optional": {
"divergence": ("FLOAT", {"default": 3.5, "min": 0.05, "max": 15, "step": 0.01}),
"separation": ("FLOAT", {"default": 0, "min": -5, "max": 5, "step": 0.01}),
"stereo_balance": ("FLOAT", {"default": 0, "min": -0.95, "max": 0.95, "step": 0.05}),
"stereo_offset_exponent": ("FLOAT", {"default": 2, "min": 1, "max": 2, "step": 1}),
"depth_blur_sigma": ("FLOAT", {"default": 0, "min": 0, "max": 10, "step": 0.1}),
"depth_blur_edge_threshold": ("FLOAT", {"default": 40, "min": 0.1, "max": 100, "step": 0.1})
}
}
RETURN_TYPES = ("IMAGE", "IMAGE", "MASK")
RETURN_NAMES = ("right_eye_image", "modified_depthmap_right", "right_no_fill_mask")
FUNCTION = "generate_right_eye"
def generate_right_eye(self, image, depth_map, divergence=3.5, separation=0,
stereo_balance=0, stereo_offset_exponent=2, fill_technique="Fill - Polylines Soft",
depth_blur_sigma=0, depth_blur_edge_threshold=40):
fill_technique_mapping = {
'No fill': 'none',
'No fill - Reverse projection': 'inverse',
'Imperfect fill - Hybrid Edge': 'hybrid_edge',
'Fill - Naive': 'naive',
'Fill - Naive interpolating': 'naive_interpolating',
'Fill - Polylines Soft': 'polylines_soft',
'Fill - Polylines Sharp': 'polylines_sharp',
'Fill - Post-fill': 'none_post',
'Fill - Reverse projection with Post-fill': 'inverse_post',
'Fill - Hybrid Edge with fill': 'hybrid_edge_plus'
}
fill_technique = fill_technique_mapping.get(fill_technique, 'none')
right_images_final = []
modified_depthmap_final = []
mask_final = []
total_steps = len(image)
pbar = ProgressBar(total_steps)
for i in range(len(image)):
img = tensor2np(image[i:i+1])
dm = tensor2np(depth_map[i:i+1])
if len(dm.shape) == 3 and dm.shape[2] == 3:
dm = np.dot(dm[..., :3], [0.2989, 0.5870, 0.1140]).astype(np.uint8)
if img.shape[:2] != dm.shape:
dm = np.array(Image.fromarray(dm).resize((img.shape[1], img.shape[0])))
output = sig.create_stereoimages(img, dm, -1.0 * divergence, separation,
['left-only'], stereo_balance, stereo_offset_exponent,
fill_technique, depth_blur_sigma, depth_blur_edge_threshold,
direction_aware_depth_blur=True, return_modified_depth=True)
if len(output) == 3:
results, _, right_modified_depthmap = output
modified_depthmap = right_modified_depthmap
else:
results, modified_depthmap = output
right_img = results[0]
right_img_tensor = np2tensor(np.array(right_img))
right_images_final.append(right_img_tensor)
if isinstance(modified_depthmap, Image.Image):
modified_depthmap_np = np.array(modified_depthmap)
else:
modified_depthmap_np = modified_depthmap
modified_depthmap_tensor = np2tensor(modified_depthmap_np)
modified_depthmap_final.append(modified_depthmap_tensor)
mask = self.generate_mask(right_img)
mask_final.append(mask)
pbar.update(1)
return (torch.cat(right_images_final), torch.cat(modified_depthmap_final), torch.cat(mask_final))
def generate_mask(self, image):
if isinstance(image, Image.Image):
np_img = np.array(image)
else:
np_img = image
if len(np_img.shape) == 3 and np_img.shape[2] >= 3:
mask = (np_img.sum(axis=-1) == 0).astype(np.uint8) * 255
else:
mask = (np_img == 0).astype(np.uint8) * 255
return np2tensor(mask)
NODE_CLASS_MAPPINGS = {
"VideoRightEyeNode": VideoRightEyeNode,
"RightEyeImageNode": RightEyeImageNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"VideoRightEyeNode": "Video Right Eye Disparity",
"RightEyeImageNode": "Right Eye Image Node"
}