From a3629f2aa4f4dc587d712bc65c85bac156c87fdf Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Mon, 9 Feb 2026 21:18:41 +0100 Subject: [PATCH] implemented polyline fill --- .gitignore | 1 + __pycache__/nodes.cpython-311.pyc | Bin 8826 -> 17258 bytes nodes.py | 255 ++++++++++++++++++++++++------ 3 files changed, 205 insertions(+), 51 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7288a50 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/nodes.cpython-311.pyc diff --git a/__pycache__/nodes.cpython-311.pyc b/__pycache__/nodes.cpython-311.pyc index ee2d6f9eb1131f71f22b9c64c8afed083c07bdc0..88d40672d3b54b3e5a0e43b7427b1914c246684a 100644 GIT binary patch literal 17258 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 X6s{u#%nsWOML!gqpx97&6t?~!YlF;A diff --git a/nodes.py b/nodes.py index 943f48c..9ed9198 100644 --- a/nodes.py +++ b/nodes.py @@ -1,5 +1,6 @@ -# my_node.py -import torch, numpy as np +# duke_stereo_nodes.py - DukeStereoSBS with Polylines Fill +import torch +import numpy as np from PIL import Image from transformers import AutoImageProcessor, AutoModelForDepthEstimation from time import time @@ -7,18 +8,129 @@ from queue import Queue from threading import Thread import cv2 +try: + from numba import njit, prange +except Exception as e: + print(f"WARNING! Numba failed to import! Stereo generation will be much slower! ({str(e)})") + from builtins import range as prange + def njit(parallel=False): + def Inner(func): return lambda *args, **kwargs: func(*args, **kwargs) + return Inner + + def writer_thread(out_path, fps, width, height, queue): fourcc = cv2.VideoWriter_fourcc(*'mp4v') - out = cv2.VideoWriter(out_path, fourcc, fps, (width*2, height)) + out = cv2.VideoWriter(out_path, fourcc, fps, (width * 2, height)) while True: item = queue.get() - if item is None: break + if item is None: + break out.write(item) out.release() + +@njit(parallel=True) +def Das mit den Friends haben wir extra gemacht, dass er sie direkt wegschreibt, weil ich sonst mit meinem Ramm nicht hinkomme.apply_stereo_divergence_polylines(original_image, normalized_depth, divergence_px: float, separation_px: float, stereo_offset_exponent: float, fill_technique: str): + """ + Polylines-basierter Stereo-Warp mit intelligentem Lücken-Füllen. + Aus stereoimage_generation.py übernommen. + """ + EPSILON = 1e-7 + PIXEL_HALF_WIDTH = 0.45 if fill_technique == 'polylines_sharp' else 0.0 + h, w, c = original_image.shape + derived_image = np.zeros_like(original_image) + + for row in prange(h): + pt = np.zeros((5 + 2 * w, 3), dtype=np.float32) + pt_end: int = 0 + pt[pt_end] = [-1.0 * w, 0.0, 0.0] + pt_end += 1 + + for col in range(0, w): + coord_d = (normalized_depth[row][col] ** stereo_offset_exponent) * divergence_px + coord_x = col + 0.5 + coord_d + separation_px + if PIXEL_HALF_WIDTH < EPSILON: + pt[pt_end] = [coord_x, abs(coord_d), col] + pt_end += 1 + else: + pt[pt_end] = [coord_x - PIXEL_HALF_WIDTH, abs(coord_d), col] + pt[pt_end + 1] = [coord_x + PIXEL_HALF_WIDTH, abs(coord_d), col] + pt_end += 2 + + pt[pt_end] = [2.0 * w, 0.0, w - 1] + pt_end += 1 + sg_end: int = pt_end - 1 + sg = np.zeros((sg_end, 6), dtype=np.float32) + + for i in range(sg_end): + sg[i] += np.concatenate((pt[i], pt[i + 1])) + + for i in range(1, sg_end): + u = i - 1 + while pt[u][0] > pt[u + 1][0] and 0 <= u: + pt[u], pt[u + 1] = np.copy(pt[u + 1]), np.copy(pt[u]) + sg[u], sg[u + 1] = np.copy(sg[u + 1]), np.copy(sg[u]) + u -= 1 + + csg = np.zeros((5 * int(abs(divergence_px)) + 25, 6), dtype=np.float32) + csg_end: int = 0 + sg_pointer: int = 0 + pt_i: int = 0 + + for col in range(w): + color = np.full(c, 0.5, dtype=np.float32) + while pt[pt_i][0] < col: + pt_i += 1 + pt_i -= 1 + + while pt[pt_i][0] < col + 1: + coord_from = max(col, pt[pt_i][0]) + EPSILON + coord_to = min(col + 1, pt[pt_i + 1][0]) - EPSILON + significance = coord_to - coord_from + coord_center = coord_from + 0.5 * significance + + while sg_pointer < sg_end and sg[sg_pointer][0] < coord_center: + csg[csg_end] = sg[sg_pointer] + sg_pointer += 1 + csg_end += 1 + + csg_i = 0 + while csg_i < csg_end: + if csg[csg_i][3] < coord_center: + csg[csg_i] = csg[csg_end - 1] + csg_end -= 1 + else: + csg_i += 1 + + best_csg_i: int = 0 + if csg_end != 1: + best_csg_closeness: float = -EPSILON + for csg_i in range(csg_end): + ip_k = (coord_center - csg[csg_i][0]) / (csg[csg_i][3] - csg[csg_i][0]) + closeness = (1.0 - ip_k) * csg[csg_i][1] + ip_k * csg[csg_i][4] + if best_csg_closeness < closeness and 0.0 < ip_k < 1.0: + best_csg_closeness = closeness + best_csg_i = csg_i + + col_l: int = int(csg[best_csg_i][2] + EPSILON) + col_r: int = int(csg[best_csg_i][5] + EPSILON) + if col_l == col_r: + color += original_image[row][col_l] * significance + else: + ip_k = (coord_center - csg[best_csg_i][0]) / (csg[best_csg_i][3] - csg[best_csg_i][0]) + color += (original_image[row][col_l] * (1.0 - ip_k) + + original_image[row][col_r] * ip_k + ) * significance + pt_i += 1 + + derived_image[row][col] = np.asarray(color, dtype=np.uint8) + + return derived_image + + class DukeStereoSBS: """ - Ein Node, der aus Bilder hauptsächlich auf der GPU SBS Stero bilder der selben auflösung (breite mal 2) macht. + Node für GPU-beschleunigte SBS Stereo-Bilder mit Polylines Fill-Technik. """ @classmethod @@ -26,16 +138,19 @@ class DukeStereoSBS: return { "required": { "output_path": ("STRING", {"default": "/ComfyUI/output/stereo_output.mp4"}), - "images": ("IMAGE",), # Bild-Input von anderem Node + "images": ("IMAGE",), }, "optional": { - "model": (["Large", "Base"],), # COMBO/Dropdown + "model": (["Large", "Base"],), "depth_size": ("INT", {"default": 518, "min": 128, "max": 1024}), "depth_batch_size": ("INT", {"default": 64, "min": 4, "max": 256}), "warp_batch_size": ("INT", {"default": 64, "min": 4, "max": 256}), "depth_blur": ("INT", {"default": 6, "min": 0, "max": 10}), "divergence": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.1}), - "fps": ("INT", {"default": 24, "min": 1, "max": 120, "step": 1}) + "fps": ("INT", {"default": 24, "min": 1, "max": 120, "step": 1}), + "fill_technique": (["polylines_soft", "polylines_sharp", "none"],), + "stereo_offset_exponent": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 4.0, "step": 0.1}), + "convergence_point": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}), } } @@ -45,75 +160,113 @@ class DukeStereoSBS: CATEGORY = "Stereo" @torch.no_grad() - def depth_batch(self,imgs, depth_batch_size,depth_size): + def depth_batch(self, imgs, depth_batch_size, depth_size): depths = [] for i in range(0, len(imgs), depth_batch_size): - batch = imgs[i:i+depth_batch_size] - inputs = self.proc(images=batch, return_tensors="pt",size=depth_size).to("cuda") + batch = imgs[i:i + depth_batch_size] + inputs = self.proc(images=batch, return_tensors="pt", size=depth_size).to("cuda") out = self.model(**inputs).predicted_depth - depths.extend([d.cpu() for d in out]) + depths.extend([d.cpu().numpy() for d in out]) return depths - @torch.no_grad() - def stereo_warp_streaming(self,imgs, depths, div, warp_batch_size,depth_blur): + def normalize_depth(self, depth, h, w, convergence_point): + """Depth auf Bildgröße interpolieren und normalisieren mit Convergence Point.""" + depth_t = torch.from_numpy(depth).unsqueeze(0).unsqueeze(0).float() + depth_t = torch.nn.functional.interpolate(depth_t, size=(h, w), mode="bilinear", align_corners=True) + depth_np = depth_t.squeeze().numpy() + + # Normalisieren auf 0-1 + d_min, d_max = depth_np.min(), depth_np.max() + normalized = (depth_np - d_min) / (d_max - d_min + 1e-7) + + # Convergence point anwenden (verschiebt wo Null-Parallax liegt) + normalized = normalized - convergence_point + + return normalized + + def stereo_warp_polylines(self, imgs, depths, divergence, depth_blur, warp_batch_size, + fill_technique, stereo_offset_exponent, convergence_point): + """Stereo-Warp mit Polylines Fill-Technik.""" + + divergence_px = divergence * (self.w / 100) + for i in range(0, len(imgs), warp_batch_size): - batch_imgs = imgs[i:i+warp_batch_size] - batch_depths = depths[i:i+warp_batch_size] - img_t = torch.stack([torch.from_numpy(np.array(im)).permute(2,0,1) for im in batch_imgs]).float().cuda() / 255 - depth_t = torch.stack([d.cuda() for d in batch_depths]).unsqueeze(1).float() - depth_t = torch.nn.functional.interpolate(depth_t, size=(self.h, self.w), mode="bilinear", align_corners=True) - if depth_blur > 0: - kernel = depth_blur if depth_blur % 2 == 1 else depth_blur + 1 # ungerade machen - pad = kernel // 2 - depth_t = torch.nn.functional.avg_pool2d( - torch.nn.functional.pad(depth_t, (pad, pad, pad, pad), mode='replicate'), - kernel, stride=1, padding=0 - ) - depth_t = depth_t / depth_t.amax(dim=(2,3), keepdim=True) - disp = (depth_t - 0.5) * div * (self.w / 100) - grid = self.base_grid.expand(len(batch_imgs), -1, -1, -1).clone() - gl, gr = grid.clone(), grid.clone() - gl[..., 0] += disp.squeeze(1) * 2 / self.w - gr[..., 0] -= disp.squeeze(1) * 2 / self.w - left = torch.nn.functional.grid_sample(img_t, gl, padding_mode='border', align_corners=True) - right = torch.nn.functional.grid_sample(img_t, gr, padding_mode='border', align_corners=True) - # Optimiert: Alles auf GPU - sbs = torch.cat([left, right], dim=3) # SBS auf GPU - sbs = sbs.flip(dims=[1]) - sbs = (sbs.permute(0,2,3,1) * 255).to(torch.uint8) # NCHW→NHWC, uint8 - sbs_np = sbs.cpu().numpy() # Ein Transfer pro Batch - for frame in sbs_np: - self.write_queue.put(frame) + batch_imgs = imgs[i:i + warp_batch_size] + batch_depths = depths[i:i + warp_batch_size] + + for img, depth in zip(batch_imgs, batch_depths): + img_np = np.array(img, dtype=np.uint8) + depth_norm = self.normalize_depth(depth, self.h, self.w, convergence_point) + + # Optional: Depth Blur + if depth_blur > 0: + kernel = depth_blur if depth_blur % 2 == 1 else depth_blur + 1 + depth_norm = cv2.blur(depth_norm, (kernel, kernel)) + + if fill_technique == "none": + # Fallback: einfacher grid_sample (alter Code) + left = self._simple_warp(img_np, depth_norm, divergence_px) + right = self._simple_warp(img_np, depth_norm, -divergence_px) + else: + # Polylines-basierter Warp + left = apply_stereo_divergence_polylines( + img_np, depth_norm, divergence_px, 0.0, stereo_offset_exponent, fill_technique + ) + right = apply_stereo_divergence_polylines( + img_np, depth_norm, -divergence_px, 0.0, stereo_offset_exponent, fill_technique + ) + + # SBS zusammenfügen (BGR für OpenCV) + sbs = np.concatenate([left, right], axis=1) + sbs_bgr = cv2.cvtColor(sbs, cv2.COLOR_RGB2BGR) + self.write_queue.put(sbs_bgr) + + def _simple_warp(self, img_np, depth_norm, divergence_px): + """Einfacher GPU-Warp als Fallback (alte Methode).""" + img_t = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).float().cuda() / 255 + depth_t = torch.from_numpy(depth_norm).unsqueeze(0).unsqueeze(0).float().cuda() + + disp = depth_t * divergence_px * 2 / self.w + grid = self.base_grid.clone() + grid[..., 0] += disp.squeeze() + + warped = torch.nn.functional.grid_sample(img_t, grid, padding_mode='border', align_corners=True) + warped_np = (warped.squeeze().permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8) + return warped_np def load_model(self, model): model_id = f"depth-anything/Depth-Anything-V2-{model}-hf" self.proc = AutoImageProcessor.from_pretrained(model_id) self.model = AutoModelForDepthEstimation.from_pretrained(model_id).cuda().half() + def execute(self, images, output_path, model="Large", fps=24, depth_size=518, + divergence=2.5, depth_blur=6, warp_batch_size=64, depth_batch_size=64, + fill_technique="polylines_soft", stereo_offset_exponent=1.0, convergence_point=0.5): - def execute(self, images,output_path, model="Large",fps=24, depth_size=518, divergence=2.5, depth_blur=6,warp_batch_size=64,depth_batch_size=64): self.load_model(model) - - # h, w aus erstem Bild + self.h, self.w = images.shape[1], images.shape[2] - # base_grid für Warping - gy, gx = torch.meshgrid(torch.linspace(-1,1,self.h), torch.linspace(-1,1,self.w), indexing='ij') - + + # base_grid für Fallback-Warp + gy, gx = torch.meshgrid(torch.linspace(-1, 1, self.h), torch.linspace(-1, 1, self.w), indexing='ij') self.base_grid = torch.stack([gx, gy], dim=-1).unsqueeze(0).cuda() - + self.write_queue = Queue(maxsize=16) writer = Thread(target=writer_thread, args=(output_path, fps, self.w, self.h, self.write_queue)) writer.start() + imgs = [Image.fromarray((img.cpu().numpy() * 255).astype(np.uint8)) for img in images] depths = self.depth_batch(imgs, depth_batch_size, depth_size) - self.stereo_warp_streaming(imgs, depths, divergence, warp_batch_size, depth_blur) + + self.stereo_warp_polylines(imgs, depths, divergence, depth_blur, warp_batch_size, + fill_technique, stereo_offset_exponent, convergence_point) + self.write_queue.put(None) writer.join() - + return (output_path,) -# Mappings für die Registrierung der Node NODE_CLASS_MAPPINGS = { "DukeStereoSBS": DukeStereoSBS }