From 06222d196f677aa1dd439aa749c64ba7944ffb82 Mon Sep 17 00:00:00 2001 From: Kaski <23-23enterprise@gmx.de> Date: Wed, 15 Oct 2025 14:15:57 +0200 Subject: [PATCH 1/2] streamlined a lot of the code, using torch.Tensors now for the whole pipeline without unneccessary conversions to pil, removed unneccessary checks that cost a lot of performance --- normal_crafter_nodes.py | 189 +++++++++++--------------- normalcrafter/normal_crafter_ppl.py | 204 ++++++++++++---------------- 2 files changed, 164 insertions(+), 229 deletions(-) diff --git a/normal_crafter_nodes.py b/normal_crafter_nodes.py index 35846a4..7c09819 100644 --- a/normal_crafter_nodes.py +++ b/normal_crafter_nodes.py @@ -2,6 +2,7 @@ import torch import torchvision.transforms as transforms +import torch.nn.functional as F import numpy as np from PIL import Image import os @@ -10,6 +11,7 @@ import logging import comfy.model_management import model_management import comfy.utils +from comfy.utils import common_upscale import folder_paths # ComfyUI's way to get model paths # Handle PIL version compatibility @@ -294,60 +296,34 @@ class NormalCrafterNode: print(f"ComfyUI-NormalCrafter: Pipeline loaded to {target_device_for_this_run} with dtype {final_torch_dtype_for_load}.") return self.pipe - def tensor_to_pil_list(self, images_tensor: torch.Tensor) -> list: - pil_images = [] - for i in range(images_tensor.shape[0]): - img_np = (images_tensor[i].cpu().numpy() * 255).astype(np.uint8) - pil_images.append(Image.fromarray(img_np)) - return pil_images - - def _validate_dimensions_for_attention(self, width: int, height: int) -> tuple: + + def make_safe_size(self, images: torch.Tensor, max_res_dim: int, device: torch.device) -> torch.Tensor: """ - Validates and adjusts dimensions to be compatible with flash attention requirements. - Returns (safe_width, safe_height, needs_adjustment) tuple. + Skaliert ein Batch von Bildern mit Torch, ohne PIL/NumPy, + und sorgt gleich für Attention-kompatible Dimensionen. """ - # Flash attention typically requires dimensions divisible by 8 or 16 - # and the total elements to be within certain bounds - - # After VAE encoding, dimensions will be divided by 8 - latent_width = width // 8 - latent_height = height // 8 - - # Common constraints for flash attention - min_dim = 2 # Minimum dimension after encoding - alignment = 8 # Dimension alignment requirement - - # Check if dimensions are safe - if latent_width < min_dim or latent_height < min_dim: - # Dimensions too small - safe_width = max(width, min_dim * 8) - safe_height = max(height, min_dim * 8) - return safe_width, safe_height, True - - # Check alignment - if latent_width % alignment != 0 or latent_height % alignment != 0: - # Need alignment adjustment - safe_width = ((latent_width + alignment - 1) // alignment) * alignment * 8 - safe_height = ((latent_height + alignment - 1) // alignment) * alignment * 8 - return safe_width, safe_height, True - - return width, height, False + imgs = images.to(device, non_blocking=True).permute(0, 3, 1, 2) # (B,C,H,W) + _, _, H, W = imgs.shape - def resize_pil_images(self, pil_images: list, max_res_dim: int) -> list: - resized_images = [] - if not pil_images: return [] - for img in pil_images: - original_width, original_height = img.size - if max(original_height, original_width) > max_res_dim: - scale = max_res_dim / max(original_height, original_width) - target_height = round(original_height * scale) - target_width = round(original_width * scale) - else: - target_height = original_height - target_width = original_width - resized_images.append(img.resize((target_width, target_height), LANCZOS)) - return resized_images + # 1. Erst mal auf max_res_dim runterskalieren (falls nötig) + if max(H, W) > max_res_dim: + scale = max_res_dim / max(H, W) + target_h, target_w = int(round(H * scale)), int(round(W * scale)) + else: + target_h, target_w = H, W + # 2. Attention-Safe-Dimensionen erzwingen (Vielfaches von 8, min 16) + min_dim = 64 + align = 64 + target_h = max(min_dim, ((target_h + align - 1) // align) * align) + target_w = max(min_dim, ((target_w + align - 1) // align) * align) + + # 3. Torch-Interpolation + imgs_resized = F.interpolate(imgs, size=(target_h, target_w), mode="bilinear", align_corners=False) + + return imgs_resized.permute(0, 2, 3, 1).contiguous() + + def process(self, images: torch.Tensor, seed: int, max_res_dimension: int, window_size: int, time_step_size: int, decode_chunk_size: int, offload_pipe_to_cpu_on_finish: bool, # <<< NEW PARAMETER @@ -361,6 +337,7 @@ class NormalCrafterNode: if pipe_override is not None: self.pipe = pipe_override print("ComfyUI-NormalCrafter: Using provided pipe_override.") + else: current_comfy_device = comfy.model_management.get_torch_device() device_str = "cuda" if current_comfy_device.type == 'cuda' else "cpu" @@ -370,7 +347,7 @@ class NormalCrafterNode: if self.pipe is None: raise RuntimeError("ComfyUI-NormalCrafter: Pipeline could not be loaded.") - + # Ensure the pipe instance self.pipe is on the correct device for processing *before* using it # This is important if self.pipe came from the global cache and might have been on CPU processing_device = comfy.model_management.get_torch_device() @@ -378,79 +355,54 @@ class NormalCrafterNode: print(f"ComfyUI-NormalCrafter: Moving self.pipe from {self.pipe.device} to {processing_device} for processing.") self.pipe.to(processing_device) - - pil_frames = self.tensor_to_pil_list(images) - if not pil_frames: return (torch.zeros_like(images),) - - resized_pil_frames = self.resize_pil_images(pil_frames, max_res_dimension) + #Resizing + orig_h, orig_w = images.shape[1:3] + resized_tensor = self.make_safe_size(images, max_res_dimension, processing_device) - num_actual_frames = len(resized_pil_frames) - effective_frames_for_pipeline = list(resized_pil_frames) + orig_h, orig_w = images.shape[1:3] + resized_tensor = self.make_safe_size(images, max_res_dimension, processing_device) + + # Anzahl Frames aus Tensor bestimmen + num_actual_frames = resized_tensor.shape[0] + + if num_actual_frames == 0: print("ComfyUI-NormalCrafter: Warning - No frames to process after resizing.") return (torch.zeros_like(images),) + if num_actual_frames < window_size: print(f"ComfyUI-NormalCrafter: Number of frames ({num_actual_frames}) is less than window_size ({window_size}). Padding...") padding_needed = window_size - num_actual_frames - last_frame_to_duplicate = resized_pil_frames[-1] - for _ in range(padding_needed): - effective_frames_for_pipeline.append(last_frame_to_duplicate) + last_frame = resized_tensor[-1:] # Shape (1,H,W,C) + pad_frames = last_frame.repeat(padding_needed, 1, 1, 1) + effective_frames_for_pipeline = torch.cat([resized_tensor, pad_frames], dim=0) + else: + effective_frames_for_pipeline = resized_tensor + + generator_device = self.pipe.device # Should be processing_device generator = torch.Generator(device=generator_device).manual_seed(seed) - print(f"ComfyUI-NormalCrafter: Processing {len(effective_frames_for_pipeline)} frames (effective) with seed {seed}. Original: {num_actual_frames}.") + effective_frame_count = effective_frames_for_pipeline.shape[0] + + print(f"ComfyUI-NormalCrafter: Processing {effective_frame_count} frames (effective) with seed {seed}. Original: {num_actual_frames}.") print(f"ComfyUI-NormalCrafter: Using (internal defaults) fps={default_fps_for_time_ids}, motion_id={default_motion_bucket_id}, noise_aug={default_noise_aug_strength}") - pbar = comfy.utils.ProgressBar(len(effective_frames_for_pipeline)) # This pbar seems not used by the pipe. + pbar = comfy.utils.ProgressBar(effective_frame_count) # optional, Pipe nutzt eh ihre eigene - # Check dimensions before processing - if resized_pil_frames: - first_frame_width, first_frame_height = resized_pil_frames[0].size - safe_width, safe_height, needs_adjustment = self._validate_dimensions_for_attention(first_frame_width, first_frame_height) - - if needs_adjustment and use_xformers != "force": - print(f"ComfyUI-NormalCrafter: Detected potentially problematic dimensions ({first_frame_width}x{first_frame_height}). " - f"Adjusting to safe dimensions ({safe_width}x{safe_height}) for flash attention compatibility.") - # Re-resize ALL frames in effective_frames_for_pipeline to safe dimensions - # This ensures padded frames also get resized - for i in range(len(effective_frames_for_pipeline)): - if isinstance(effective_frames_for_pipeline[i], Image.Image): - effective_frames_for_pipeline[i] = effective_frames_for_pipeline[i].resize((safe_width, safe_height), LANCZOS) - else: - # Handle case where frame might not be a PIL Image - print(f"ComfyUI-NormalCrafter: Warning - frame {i} is not a PIL Image, skipping resize") - # Final safety check: ensure all frames have the same dimensions - if effective_frames_for_pipeline: - reference_size = None - all_same_size = True - - for i, frame in enumerate(effective_frames_for_pipeline): - if isinstance(frame, Image.Image): - if reference_size is None: - reference_size = frame.size - elif frame.size != reference_size: - all_same_size = False - print(f"ComfyUI-NormalCrafter: Frame {i} has size {frame.size}, expected {reference_size}") - - if not all_same_size and reference_size: - print(f"ComfyUI-NormalCrafter: Ensuring all frames are {reference_size} for consistency") - for i in range(len(effective_frames_for_pipeline)): - if isinstance(effective_frames_for_pipeline[i], Image.Image): - if effective_frames_for_pipeline[i].size != reference_size: - effective_frames_for_pipeline[i] = effective_frames_for_pipeline[i].resize(reference_size, LANCZOS) - output_frames_np = None + output_frames_pt = None error_occurred = False # Try processing with current settings try: with torch.inference_mode(): - output_frames_np = self.pipe( # self.pipe should be on processing_device here - images=effective_frames_for_pipeline, + output_frames_pt = self.pipe( # self.pipe should be on processing_device here + images=resized_tensor, decode_chunk_size=decode_chunk_size, time_step_size=time_step_size, window_size=window_size, @@ -472,8 +424,8 @@ class NormalCrafterNode: try: with torch.inference_mode(): - output_frames_np = self.pipe( - images=effective_frames_for_pipeline, + output_frames_pt = self.pipe( + images=resized_tensor, decode_chunk_size=decode_chunk_size, time_step_size=time_step_size, window_size=window_size, @@ -498,14 +450,29 @@ class NormalCrafterNode: else: raise e - if output_frames_np is None: + if output_frames_pt is None: raise RuntimeError("ComfyUI-NormalCrafter: Failed to generate output frames.") - if len(effective_frames_for_pipeline) > num_actual_frames: - output_frames_np = output_frames_np[:num_actual_frames, :, :, :] - - output_normals_0_1 = (output_frames_np.clip(-1., 1.) * 0.5) + 0.5 - output_tensor = torch.from_numpy(output_normals_0_1).float() + if len(resized_tensor) > num_actual_frames: + output_frames_pt = output_frames_pt[:num_actual_frames, ...] + + + # Output von NormalCrafter: [B,C,H,W] + # 1. Normieren nach [0,1] + output_frames_pt = (output_frames_pt.clamp(-1., 1.) * 0.5) + 0.5 + + # 2. Resize zurück zur Eingangsauflösung + output_frames_pt = F.interpolate( + output_frames_pt, + size=(orig_h, orig_w), # Eingangsauflösung + mode="bilinear", + align_corners=False + ) + + # 3. Zurück nach [B,H,W,C] für ComfyUI + output_frames_pt = output_frames_pt.permute(0, 2, 3, 1).contiguous() + + # --- Explicit Offload After Processing --- # Only offload the globally managed pipe (NORMALCRAFTER_PIPE), not an overridden one. @@ -527,5 +494,5 @@ class NormalCrafterNode: except Exception as e: print(f"ComfyUI-NormalCrafter: Error offloading pipeline to CPU: {e}") - print(f"ComfyUI-NormalCrafter: Processing complete. Output tensor shape: {output_tensor.shape}") - return (output_tensor,) + print(f"ComfyUI-NormalCrafter: Processing complete. Output tensor shape: {output_frames_pt.shape}") + return (output_frames_pt,) diff --git a/normalcrafter/normal_crafter_ppl.py b/normalcrafter/normal_crafter_ppl.py index f8fbc17..73560bf 100644 --- a/normalcrafter/normal_crafter_ppl.py +++ b/normalcrafter/normal_crafter_ppl.py @@ -18,144 +18,99 @@ logger = logging.get_logger(__name__) # pylint: disable=invalid-name class NormalCrafterPipeline(StableVideoDiffusionPipeline): - def _encode_image(self, image, device, num_videos_per_prompt, do_classifier_free_guidance, scale=1, image_size=None): + def _encode_image(self, image: torch.Tensor, device, num_videos_per_prompt, do_classifier_free_guidance, scale=1, image_size=None): dtype = next(self.image_encoder.parameters()).dtype - if not isinstance(image, torch.Tensor): - image = self.video_processor.pil_to_numpy(image) # (0, 255) -> (0, 1) - image = self.video_processor.numpy_to_pt(image) # (n, h, w, c) -> (n, c, h, w) + if isinstance(image, torch.Tensor): + # Expected: (T,H,W,C) or (T,C,H,W) in [0,1] + if image.dim() != 4: + raise ValueError("Tensor image must be 4D (T,H,W,C) or (T,C,H,W).") + if image.shape[-1] == 3: # (T,H,W,C) -> (T,C,H,W) + image = image.permute(0,3,1,2) + elif image.shape[1] == 3: # (T,C,H,W) + pass + else: + raise ValueError("Channel dim must be 3.") - # We normalize the image before resizing to match with the original implementation. - # Then we unnormalize it after resizing. - pixel_values = image + pixel_values = image.to(device=device, dtype=torch.float32) # B=T B, C, H, W = pixel_values.shape + patches = [pixel_values] - # patches = [] for i in range(1, scale): - num_patches_HW_this_level = i + 1 - patch_H = H // num_patches_HW_this_level + 1 - patch_W = W // num_patches_HW_this_level + 1 - for j in range(num_patches_HW_this_level): - for k in range(num_patches_HW_this_level): - patches.append(pixel_values[:, :, j*patch_H:(j+1)*patch_H, k*patch_W:(k+1)*patch_W]) + num_patches = i + 1 + ph = H // num_patches + 1 + pw = W // num_patches + 1 + for j in range(num_patches): + for k in range(num_patches): + patches.append(pixel_values[:, :, j*ph:(j+1)*ph, k*pw:(k+1)*pw]) + + def encode_image(img_bchw): + img_bchw = img_bchw * 2.0 - 1.0 + img_bchw = _resize_with_antialiasing(img_bchw, image_size or (224, 224)) + img_bchw = (img_bchw + 1.0) / 2.0 # zurück in [0,1] + + # CLIP-Preproc + feat = self.feature_extractor( + images=img_bchw, do_normalize=True, do_center_crop=False, + do_resize=False, do_rescale=False, return_tensors="pt", + ).pixel_values.to(device=device, dtype=dtype) + + emb = self.image_encoder(feat).image_embeds + if emb.dim() < 3: + emb = emb.unsqueeze(1) + return emb + + image_embeddings = torch.cat([encode_image(p) for p in patches], dim=1) + else: + raise ValueError("_encode_image() didn't get handed a torch-tensor") - def encode_image(image): - image = image * 2.0 - 1.0 - if image_size is not None: - image = _resize_with_antialiasing(image, image_size) - else: - image = _resize_with_antialiasing(image, (224, 224)) - image = (image + 1.0) / 2.0 - - # Normalize the image with for CLIP input - image = self.feature_extractor( - images=image, - do_normalize=True, - do_center_crop=False, - do_resize=False, - do_rescale=False, - return_tensors="pt", - ).pixel_values - - image = image.to(device=device, dtype=dtype) - image_embeddings = self.image_encoder(image).image_embeds - if len(image_embeddings.shape) < 3: - image_embeddings = image_embeddings.unsqueeze(1) - return image_embeddings - - image_embeddings = [] - for patch in patches: - image_embeddings.append(encode_image(patch)) - image_embeddings = torch.cat(image_embeddings, dim=1) - - # duplicate image embeddings for each generation per prompt, using mps friendly method - # import pdb - # pdb.set_trace() + # Duplicate bs_embed, seq_len, _ = image_embeddings.shape image_embeddings = image_embeddings.repeat(1, num_videos_per_prompt, 1) image_embeddings = image_embeddings.view(bs_embed * num_videos_per_prompt, seq_len, -1) if do_classifier_free_guidance: - negative_image_embeddings = torch.zeros_like(image_embeddings) - - # For classifier free guidance, we need to do two forward passes. - # Here we concatenate the unconditional and text embeddings into a single batch - # to avoid doing two forward passes - image_embeddings = torch.cat([negative_image_embeddings, image_embeddings]) + negative = torch.zeros_like(image_embeddings) + image_embeddings = torch.cat([negative, image_embeddings]) return image_embeddings - def ecnode_video_vae(self, images, chunk_size: int = 14): - if isinstance(images, list): - width, height = images[0].size - else: - height, width = images[0].shape[:2] + + def encode_video_vae(self, images: torch.Tensor, chunk_size: int = 14): needs_upcasting = self.vae.dtype == torch.float16 and self.vae.config.force_upcast if needs_upcasting: self.vae.to(dtype=torch.float32) device = self._execution_device - images = self.video_processor.preprocess_video(images, height=height, width=width).to(device, self.vae.dtype) # torch type in range(-1, 1) with (1,3,h,w) - images = images.squeeze(0) # from (1, c, t, h, w) -> (c, t, h, w) - images = images.permute(1,0,2,3) # c, t, h, w -> (t, c, h, w) + + if isinstance(images, torch.Tensor): + # (T,H,W,C) oder (T,C,H,W) in [0,1] -> (1, C, T, H, W) in [-1,1] + if images.shape[-1] == 3: # (T,H,W,C) -> (T,C,H,W) + imgs = images.permute(0,3,1,2) + else: # (T,C,H,W) + imgs = images + imgs = imgs.to(device=device, dtype=self.vae.dtype) + imgs = imgs * 2.0 - 1.0 + imgs = imgs.permute(1,0,2,3).unsqueeze(0) # (C,T,H,W) -> (1,C,T,H,W) + else: + raise ValueError("encode_video_vae() didn't get handed a torch-tensor") + + # (1,C,T,H,W) -> (C,T,H,W) -> (T,C,H,W) + imgs = imgs.squeeze(0).permute(1,0,2,3) video_latents = [] - # chunk_size = 14 - for i in range(0, images.shape[0], chunk_size): - video_latents.append(self.vae.encode(images[i : i + chunk_size]).latent_dist.mode()) - image_latents = torch.cat(video_latents) + for i in range(0, imgs.shape[0], chunk_size): + video_latents.append(self.vae.encode(imgs[i:i+chunk_size]).latent_dist.mode()) + image_latents = torch.cat(video_latents, dim=0) - # cast back to fp16 if needed if needs_upcasting: self.vae.to(dtype=torch.float16) - return image_latents - - def pad_image(self, images, scale=64): - def get_pad(newW, W): - pad_W = (newW - W) // 2 - if W % 2 == 1: - pad_Ws = [pad_W, pad_W + 1] - else: - pad_Ws = [pad_W, pad_W] - return pad_Ws - - if type(images[0]) is np.ndarray: - H, W = images[0].shape[:2] - else: - W, H = images[0].size - - if W % scale == 0 and H % scale == 0: - return images, None - newW = int(np.ceil(W / scale) * scale) - newH = int(np.ceil(H / scale) * scale) - - pad_Ws = get_pad(newW, W) - pad_Hs = get_pad(newH, H) - - new_images = [] - for image in images: - if type(image) is np.ndarray: - image = cv2.copyMakeBorder(image, *pad_Hs, *pad_Ws, cv2.BORDER_CONSTANT, value=(1.,1.,1.)) - new_images.append(image) - else: - image = np.array(image) - image = cv2.copyMakeBorder(image, *pad_Hs, *pad_Ws, cv2.BORDER_CONSTANT, value=(255,255,255)) - new_images.append(Image.fromarray(image)) - return new_images, pad_Hs+pad_Ws - - def unpad_image(self, v, pad_HWs): - t, b, l, r = pad_HWs - if t > 0 or b > 0: - v = v[:, :, t:-b] - if l > 0 or r > 0: - v = v[:, :, :, l:-r] - return v @torch.no_grad() def __call__( self, - images: Union[PIL.Image.Image, List[PIL.Image.Image], torch.FloatTensor], + images: torch.FloatTensor, decode_chunk_size: Optional[int] = None, time_step_size: Optional[int] = 1, window_size: Optional[int] = 1, @@ -165,14 +120,28 @@ class NormalCrafterPipeline(StableVideoDiffusionPipeline): generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None, return_dict: bool = True ): - images, pad_HWs = self.pad_image(images) + #images, pad_HWs = self.pad_image(images) # 0. Default height and width to unet - width, height = images[0].size - num_frames = len(images) + if isinstance(images, torch.Tensor): + if images.dim() != 4: + raise ValueError("images tensor must be 4D (T,H,W,C) or (T,C,H,W)") + if images.shape[-1] == 3: # (T,H,W,C) + T, H, W, C = images.shape + images_for_enc = images.permute(0,3,1,2) # (T,C,H,W) + elif images.shape[1] == 3: # (T,C,H,W) + T, C, H, W = images.shape[0], images.shape[1], images.shape[2], images.shape[3] + images_for_enc = images + else: + raise ValueError("Channel dim must be 3.") + num_frames, height, width = T, H, W + else: + width, height = images[0].size + num_frames = len(images) + images_for_enc = images # bleibt Liste von PIL - # 1. Check inputs. Raise error if not correct - self.check_inputs(images, height, width) + # 1. Check inputs. Raise error if not correct -> Redundant, due to the rescaling done in normal_crafter_node.py + # self.check_inputs(images, height, width) # 2. Define call parameters batch_size = 1 @@ -186,7 +155,7 @@ class NormalCrafterPipeline(StableVideoDiffusionPipeline): num_inference_steps = 1 # For direct normal generation, this is typically 1 step if not iterative refinement # fps, motion_bucket_id, noise_aug_strength are now passed as arguments - output_type = "np" # Default output type from SVD pipeline is numpy array + output_type = "pt" # Default output type from SVD pipeline is numpy array, but we use torch because we are cooler # data_keys = ["normal"] # Not used directly in this simplified flow use_linear_merge = True determineTrain = True # This seems to indicate a direct generation rather than denoising from pure noise @@ -198,7 +167,7 @@ class NormalCrafterPipeline(StableVideoDiffusionPipeline): # 3. Encode input image using using clip. (num_image * num_videos_per_prompt, 1, 1024) image_embeddings = self._encode_image(images, device, num_videos_per_prompt, do_classifier_free_guidance=do_classifier_free_guidance, scale=encode_image_scale, image_size=encode_image_WH) # 4. Encode input image using VAE - image_latents = self.ecnode_video_vae(images, chunk_size=decode_chunk_size).to(image_embeddings.dtype) + image_latents = self.encode_video_vae(images, chunk_size=decode_chunk_size).to(image_embeddings.dtype) # image_latents [num_frames, channels, height, width] ->[1, num_frames, channels, height, width] image_latents = image_latents.unsqueeze(0) @@ -369,13 +338,12 @@ class NormalCrafterPipeline(StableVideoDiffusionPipeline): frames_decoded = self.decode_latents(latents_to_decode, num_frames_to_decode, decode_chunk_size_local) - frames_decoded = self.video_processor.postprocess_video(video=frames_decoded, output_type="np") + frames_decoded = self.video_processor.postprocess_video(video=frames_decoded, output_type=output_type) # "pt" frames_decoded = frames_decoded * 2 - 1 return frames_decoded frames = decode_latents(pred, num_frames, decode_chunk_size) - if pad_HWs is not None: - frames = self.unpad_image(frames, pad_HWs) + else: frames = pred From 0a3d986cb94a39d1b1d41f5877ba410d0137e1e4 Mon Sep 17 00:00:00 2001 From: Kaski <23-23enterprise@gmx.de> Date: Wed, 5 Nov 2025 16:08:06 +0100 Subject: [PATCH 2/2] Comments to english --- normal_crafter_nodes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/normal_crafter_nodes.py b/normal_crafter_nodes.py index 7c09819..42ddb9e 100644 --- a/normal_crafter_nodes.py +++ b/normal_crafter_nodes.py @@ -305,14 +305,14 @@ class NormalCrafterNode: imgs = images.to(device, non_blocking=True).permute(0, 3, 1, 2) # (B,C,H,W) _, _, H, W = imgs.shape - # 1. Erst mal auf max_res_dim runterskalieren (falls nötig) + # 1. Scale down to max_res_dim (if necessary if max(H, W) > max_res_dim: scale = max_res_dim / max(H, W) target_h, target_w = int(round(H * scale)), int(round(W * scale)) else: target_h, target_w = H, W - # 2. Attention-Safe-Dimensionen erzwingen (Vielfaches von 8, min 16) + # 2. Enforce Attention-Safe-Dimension (minimum 64) min_dim = 64 align = 64 target_h = max(min_dim, ((target_h + align - 1) // align) * align) @@ -362,7 +362,7 @@ class NormalCrafterNode: orig_h, orig_w = images.shape[1:3] resized_tensor = self.make_safe_size(images, max_res_dimension, processing_device) - # Anzahl Frames aus Tensor bestimmen + # Get number of frames in the tensor num_actual_frames = resized_tensor.shape[0] @@ -457,11 +457,11 @@ class NormalCrafterNode: output_frames_pt = output_frames_pt[:num_actual_frames, ...] - # Output von NormalCrafter: [B,C,H,W] - # 1. Normieren nach [0,1] + # Output from NormalCrafter has the pattern: [B,C,H,W] + # 1. Normalize to a range of [0,1] output_frames_pt = (output_frames_pt.clamp(-1., 1.) * 0.5) + 0.5 - # 2. Resize zurück zur Eingangsauflösung + # 2. Resize back to input-resolution output_frames_pt = F.interpolate( output_frames_pt, size=(orig_h, orig_w), # Eingangsauflösung @@ -469,7 +469,7 @@ class NormalCrafterNode: align_corners=False ) - # 3. Zurück nach [B,H,W,C] für ComfyUI + # 3. Permute Tensor to [B,H,W,C] for ComfyUI-Compatibility output_frames_pt = output_frames_pt.permute(0, 2, 3, 1).contiguous()