Added node "Extract Frames from Video"

This commit is contained in:
Bruno Fargnoli
2026-04-05 12:20:36 +02:00
parent f791215b2a
commit fdfa2de24d
4 changed files with 1156 additions and 19 deletions
+1
View File
@@ -14,6 +14,7 @@
| Date | Description |
| --- | --- |
| **2026-04-05** | Added node "Extract Images from Video"<br>Can be used with "Sparse Generator with ReconViaGen" |
| **2026-04-04** | Added node "Sparse Generator with ReconViaGen" |
| **2026-04-01** | Added node "Voxel to Mesh"<br>It replaces Remeshing to make watertight mesh |
| **2026-03-21** | Added node "Projection HighPoly to LowPoly"<br>Added node "Render MultiView" |
File diff suppressed because it is too large Load Diff
+101 -18
View File
@@ -2383,20 +2383,58 @@ class Trellis2PreProcessImage:
CATEGORY = "Trellis2Wrapper"
def process(self, image, padding, remove_background, max_size):
image = tensor2pil(image)
if remove_background:
from rembg import remove
image = remove(image)
image = self.preprocess_image(image, max_size)
if padding>0:
border = (int(padding), int(padding), int(padding), int(padding))
fill_color = self.parse_fill_for_image("0,0,0,255", image)
image = ImageOps.expand(image,border=border,fill=fill_color)
image = pil2tensor(image)
if image.ndim == 3:
image = tensor2pil(image)
if remove_background:
from rembg import remove
image = remove(image)
image = self.preprocess_image(image, max_size)
if padding>0:
border = (int(padding), int(padding), int(padding), int(padding))
fill_color = self.parse_fill_for_image("0,0,0,255", image)
image = ImageOps.expand(image,border=border,fill=fill_color)
image = pil2tensor(image)
elif image.ndim == 4:
images = convert_tensor_images_to_pil(image)
tensor_list = []
for img in images:
if remove_background:
from rembg import remove
img = remove(img)
img = self.preprocess_image(img, max_size)
if padding>0:
border = (int(padding), int(padding), int(padding), int(padding))
fill_color = self.parse_fill_for_image("0,0,0,255", img)
img = ImageOps.expand(img,border=border,fill=fill_color)
tensor_list.append(pil2tensor(img))
max_h = max(t.shape[-3] for t in tensor_list)
max_w = max(t.shape[-2] for t in tensor_list)
resized_tensors = []
for t in tensor_list:
# Ensure tensor is [C, H, W] for PyTorch's interpolate function
# Current shape is likely [H, W, C] or [1, H, W, C]
temp_t = t.squeeze() # Get to [H, W, C]
temp_t = temp_t.permute(2, 0, 1).unsqueeze(0) # Becomes [1, C, H, W]
# 2. Resize to the max dimensions
# Using 'bicubic' or 'bilinear' for better quality than 'nearest'
temp_t = F.interpolate(temp_t, size=(max_h, max_w), mode='bicubic', align_corners=False)
# 3. Convert back to ComfyUI format [H, W, C]
temp_t = temp_t.squeeze(0).permute(1, 2, 0)
resized_tensors.append(temp_t)
image = torch.stack(resized_tensors)
return (image,)
@@ -3527,7 +3565,7 @@ class Trellis2ImageCondGenerator:
"required": {
"pipeline": ("TRELLIS2PIPELINE",),
"image": ("IMAGE",),
"max_views": ("INT", {"default": 4, "min": 1, "max": 16}),
"max_views": ("INT", {"default": 1, "min": 1, "max": 999}),
},
}
@@ -3779,6 +3817,9 @@ class Trellis2ShapeCascadeGenerator:
print(f"Num Tokens: {num_tokens}")
hr_resolution = 512
break
if pipeline.low_vram:
cond = pipeline._cond_to(cond, pipeline.device)
coords_dev = coords.to(pipeline.device)
# Sample structured latent
@@ -4804,7 +4845,7 @@ class Trellis2SparseGeneratorWithReconViaGen:
pipeline.unload_sparse_structure_vggt_cond()
self.unload_vggt_model(pipeline)
return (coords, sparse_structure_resolution, pipeline,)
return (coords, sparse_structure_resolution, pipeline)
def load_vggt_model(self, pipeline):
if pipeline.VGGT_model is None:
@@ -4900,7 +4941,7 @@ class Trellis2SparseGeneratorWithReconViaGen:
ss_cond = pipeline._cond_cpu(ss_cond)
torch.cuda.empty_cache()
return coords
return coords
@torch.no_grad()
def _run_ss_stage(
@@ -4986,6 +5027,7 @@ class Trellis2SparseGeneratorWithReconViaGen:
Returns:
dict: The conditioning information
"""
pipeline.models['sparse_structure_vggt_cond'].to(pipeline.device)
cond = pipeline.models['sparse_structure_vggt_cond'](aggregated_tokens_list, image_cond)
neg_cond = torch.zeros_like(cond)
return {
@@ -5059,7 +5101,46 @@ class Trellis2SparseGeneratorWithReconViaGen:
transform = transforms.Compose([
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
pipeline.image_cond_model_transform = transform
pipeline.image_cond_model_transform = transform
class Trellis2ExtractImagesFromVideo:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"video_file": ("STRING",),
"frames_per_second": ("INT",{"default":1,"min":1,"max":50,"step":1}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "process"
CATEGORY = "Trellis2Wrapper"
OUTPUT_NODE = True
def process(self, video_file, frames_per_second):
import imageio
vid = imageio.get_reader(video_file, 'ffmpeg')
fps = vid.get_meta_data()['fps']
frames = []
for i, frame in enumerate(vid):
if i % max(int(fps/frames_per_second), 1) == 0:
img = Image.fromarray(frame)
W, H = img.size
img = img.resize((int(W / H * 1024), 1024))
frames.append(img)
vid.close()
tensor_list = [torch.from_numpy(np.array(img).astype(np.float32) / 255.0) for img in frames]
print(f"{len(frames)} frames extracted")
tensor_frames = torch.stack(tensor_list)
#tensor_frames = tensor_frames.permute(0, 2, 3, 1)
return (tensor_frames,)
NODE_CLASS_MAPPINGS = {
"Trellis2LoadModel": Trellis2LoadModel,
@@ -5118,6 +5199,7 @@ NODE_CLASS_MAPPINGS = {
"Trellis2VoxelToMesh": Trellis2VoxelToMesh,
"Trellis2UnloadAllModels": Trellis2UnloadAllModels,
"Trellis2SparseGeneratorWithReconViaGen": Trellis2SparseGeneratorWithReconViaGen,
"Trellis2ExtractImagesFromVideo": Trellis2ExtractImagesFromVideo,
}
@@ -5178,4 +5260,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Trellis2VoxelToMesh": "Trellis2 - Voxel to Mesh",
"Trellis2UnloadAllModels": "Trellis2 - Unload All ComfyUI Models",
"Trellis2SparseGeneratorWithReconViaGen": "Trellis2 - Sparse Generator with ReconViaGen",
"Trellis2ExtractImagesFromVideo": "Trellis 2 - Extract Images from Video",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "trellis2"
description = "ComfyUI Wrapper for Microsoft Trellis.2 - Native and Compact Structured Latents for 3D Generation"
version = "1.0.20"
version = "1.0.21"
license = {file = "LICENSE"}
# classifiers = [
# # For OS-independent nodes (works on all operating systems)