Add oversampled image output to all VFI Interpolate nodes

Second IMAGE output exposes the full power-of-2 oversampled frames
before target FPS selection. Identical to the first output when
target_fps=0. Document the new output in README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 17:39:58 +01:00
parent b2d7d3b634
commit 2f1cc17f5c
2 changed files with 41 additions and 24 deletions

View File

@@ -49,6 +49,11 @@ Interpolates frames from an image batch.
| **source_fps** | Input frame rate. Required when target_fps > 0 | | **source_fps** | Input frame rate. Required when target_fps > 0 |
| **target_fps** | Target output FPS. When > 0, overrides multiplier — auto-computes the optimal power-of-2 oversample then selects frames at exact target timestamps. 0 = use multiplier | | **target_fps** | Target output FPS. When > 0, overrides multiplier — auto-computes the optimal power-of-2 oversample then selects frames at exact target timestamps. 0 = use multiplier |
| Output | Description |
|--------|-------------|
| **images** | Interpolated frames at the target FPS (or at the multiplied rate when target_fps = 0) |
| **oversampled** | Full power-of-2 oversampled frames before target FPS selection. Same as `images` when target_fps = 0. Useful for inspecting the raw interpolation or feeding into another pipeline |
#### BIM-VFI Segment Interpolate #### BIM-VFI Segment Interpolate
Same as Interpolate but processes a single segment of the input. Chain multiple instances with Save nodes between them to bound peak RAM. The model pass-through output forces sequential execution. Same as Interpolate but processes a single segment of the input. Chain multiple instances with Save nodes between them to bound peak RAM. The model pass-through output forces sequential execution.

View File

@@ -202,8 +202,8 @@ class BIMVFIInterpolate:
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("images",) RETURN_NAMES = ("images", "oversampled")
FUNCTION = "interpolate" FUNCTION = "interpolate"
CATEGORY = "video/BIM-VFI" CATEGORY = "video/BIM-VFI"
@@ -275,7 +275,7 @@ class BIMVFIInterpolate:
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
source_fps=0.0, target_fps=0.0): source_fps=0.0, target_fps=0.0):
if images.shape[0] < 2: if images.shape[0] < 2:
return (images,) return (images, images)
# Target FPS mode: auto-compute multiplier from fps ratio # Target FPS mode: auto-compute multiplier from fps ratio
use_target_fps = target_fps > 0 and source_fps > 0 use_target_fps = target_fps > 0 and source_fps > 0
@@ -285,7 +285,7 @@ class BIMVFIInterpolate:
# Downsampling or same fps — select from input directly # Downsampling or same fps — select from input directly
all_frames = images.permute(0, 3, 1, 2) all_frames = images.permute(0, 3, 1, 2)
result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0]) result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0])
return (result.cpu().permute(0, 2, 3, 1),) return (result.cpu().permute(0, 2, 3, 1), images)
else: else:
num_passes = {2: 1, 4: 2, 8: 3}[multiplier] num_passes = {2: 1, 4: 2, 8: 3}[multiplier]
mult = multiplier mult = multiplier
@@ -344,13 +344,16 @@ class BIMVFIInterpolate:
result = torch.cat(result_chunks, dim=0) result = torch.cat(result_chunks, dim=0)
# Convert oversampled to ComfyUI format for second output
oversampled = result.cpu().permute(0, 2, 3, 1)
# Target FPS: select frames from oversampled result # Target FPS: select frames from oversampled result
if use_target_fps: if use_target_fps:
result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input) result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input)
# Convert back to ComfyUI [B, H, W, C], on CPU # Convert back to ComfyUI [B, H, W, C], on CPU
result = result.cpu().permute(0, 2, 3, 1) result = result.cpu().permute(0, 2, 3, 1)
return (result,) return (result, oversampled)
class BIMVFISegmentInterpolate(BIMVFIInterpolate): class BIMVFISegmentInterpolate(BIMVFIInterpolate):
@@ -461,7 +464,7 @@ class BIMVFISegmentInterpolate(BIMVFIInterpolate):
# Standard multiplier mode # Standard multiplier mode
is_continuation = segment_index > 0 is_continuation = segment_index > 0
(result,) = super().interpolate( (result, _) = super().interpolate(
segment_images, model, multiplier, clear_cache_after_n_frames, segment_images, model, multiplier, clear_cache_after_n_frames,
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
) )
@@ -737,8 +740,8 @@ class EMAVFIInterpolate:
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("images",) RETURN_NAMES = ("images", "oversampled")
FUNCTION = "interpolate" FUNCTION = "interpolate"
CATEGORY = "video/EMA-VFI" CATEGORY = "video/EMA-VFI"
@@ -803,7 +806,7 @@ class EMAVFIInterpolate:
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
source_fps=0.0, target_fps=0.0): source_fps=0.0, target_fps=0.0):
if images.shape[0] < 2: if images.shape[0] < 2:
return (images,) return (images, images)
# Target FPS mode: auto-compute multiplier from fps ratio # Target FPS mode: auto-compute multiplier from fps ratio
use_target_fps = target_fps > 0 and source_fps > 0 use_target_fps = target_fps > 0 and source_fps > 0
@@ -812,7 +815,7 @@ class EMAVFIInterpolate:
if num_passes == 0: if num_passes == 0:
all_frames = images.permute(0, 3, 1, 2) all_frames = images.permute(0, 3, 1, 2)
result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0]) result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0])
return (result.cpu().permute(0, 2, 3, 1),) return (result.cpu().permute(0, 2, 3, 1), images)
else: else:
num_passes = {2: 1, 4: 2, 8: 3}[multiplier] num_passes = {2: 1, 4: 2, 8: 3}[multiplier]
mult = multiplier mult = multiplier
@@ -871,13 +874,16 @@ class EMAVFIInterpolate:
result = torch.cat(result_chunks, dim=0) result = torch.cat(result_chunks, dim=0)
# Convert oversampled to ComfyUI format for second output
oversampled = result.cpu().permute(0, 2, 3, 1)
# Target FPS: select frames from oversampled result # Target FPS: select frames from oversampled result
if use_target_fps: if use_target_fps:
result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input) result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input)
# Convert back to ComfyUI [B, H, W, C], on CPU # Convert back to ComfyUI [B, H, W, C], on CPU
result = result.cpu().permute(0, 2, 3, 1) result = result.cpu().permute(0, 2, 3, 1)
return (result,) return (result, oversampled)
class EMAVFISegmentInterpolate(EMAVFIInterpolate): class EMAVFISegmentInterpolate(EMAVFIInterpolate):
@@ -985,7 +991,7 @@ class EMAVFISegmentInterpolate(EMAVFIInterpolate):
# Standard multiplier mode # Standard multiplier mode
is_continuation = segment_index > 0 is_continuation = segment_index > 0
(result,) = super().interpolate( (result, _) = super().interpolate(
segment_images, model, multiplier, clear_cache_after_n_frames, segment_images, model, multiplier, clear_cache_after_n_frames,
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
) )
@@ -1128,8 +1134,8 @@ class SGMVFIInterpolate:
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("images",) RETURN_NAMES = ("images", "oversampled")
FUNCTION = "interpolate" FUNCTION = "interpolate"
CATEGORY = "video/SGM-VFI" CATEGORY = "video/SGM-VFI"
@@ -1194,7 +1200,7 @@ class SGMVFIInterpolate:
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
source_fps=0.0, target_fps=0.0): source_fps=0.0, target_fps=0.0):
if images.shape[0] < 2: if images.shape[0] < 2:
return (images,) return (images, images)
# Target FPS mode: auto-compute multiplier from fps ratio # Target FPS mode: auto-compute multiplier from fps ratio
use_target_fps = target_fps > 0 and source_fps > 0 use_target_fps = target_fps > 0 and source_fps > 0
@@ -1203,7 +1209,7 @@ class SGMVFIInterpolate:
if num_passes == 0: if num_passes == 0:
all_frames = images.permute(0, 3, 1, 2) all_frames = images.permute(0, 3, 1, 2)
result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0]) result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0])
return (result.cpu().permute(0, 2, 3, 1),) return (result.cpu().permute(0, 2, 3, 1), images)
else: else:
num_passes = {2: 1, 4: 2, 8: 3}[multiplier] num_passes = {2: 1, 4: 2, 8: 3}[multiplier]
mult = multiplier mult = multiplier
@@ -1262,13 +1268,16 @@ class SGMVFIInterpolate:
result = torch.cat(result_chunks, dim=0) result = torch.cat(result_chunks, dim=0)
# Convert oversampled to ComfyUI format for second output
oversampled = result.cpu().permute(0, 2, 3, 1)
# Target FPS: select frames from oversampled result # Target FPS: select frames from oversampled result
if use_target_fps: if use_target_fps:
result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input) result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input)
# Convert back to ComfyUI [B, H, W, C], on CPU # Convert back to ComfyUI [B, H, W, C], on CPU
result = result.cpu().permute(0, 2, 3, 1) result = result.cpu().permute(0, 2, 3, 1)
return (result,) return (result, oversampled)
class SGMVFISegmentInterpolate(SGMVFIInterpolate): class SGMVFISegmentInterpolate(SGMVFIInterpolate):
@@ -1376,7 +1385,7 @@ class SGMVFISegmentInterpolate(SGMVFIInterpolate):
# Standard multiplier mode # Standard multiplier mode
is_continuation = segment_index > 0 is_continuation = segment_index > 0
(result,) = super().interpolate( (result, _) = super().interpolate(
segment_images, model, multiplier, clear_cache_after_n_frames, segment_images, model, multiplier, clear_cache_after_n_frames,
keep_device, all_on_gpu, batch_size, chunk_size, keep_device, all_on_gpu, batch_size, chunk_size,
) )
@@ -1536,8 +1545,8 @@ class GIMMVFIInterpolate:
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("images",) RETURN_NAMES = ("images", "oversampled")
FUNCTION = "interpolate" FUNCTION = "interpolate"
CATEGORY = "video/GIMM-VFI" CATEGORY = "video/GIMM-VFI"
@@ -1647,7 +1656,7 @@ class GIMMVFIInterpolate:
batch_size, chunk_size, batch_size, chunk_size,
source_fps=0.0, target_fps=0.0): source_fps=0.0, target_fps=0.0):
if images.shape[0] < 2: if images.shape[0] < 2:
return (images,) return (images, images)
# Target FPS mode: auto-compute multiplier from fps ratio # Target FPS mode: auto-compute multiplier from fps ratio
use_target_fps = target_fps > 0 and source_fps > 0 use_target_fps = target_fps > 0 and source_fps > 0
@@ -1656,7 +1665,7 @@ class GIMMVFIInterpolate:
if num_passes == 0: if num_passes == 0:
all_frames = images.permute(0, 3, 1, 2) all_frames = images.permute(0, 3, 1, 2)
result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0]) result = _select_target_fps_frames(all_frames, source_fps, target_fps, mult, all_frames.shape[0])
return (result.cpu().permute(0, 2, 3, 1),) return (result.cpu().permute(0, 2, 3, 1), images)
# Override multiplier for single_pass mode # Override multiplier for single_pass mode
multiplier = mult multiplier = mult
else: else:
@@ -1732,13 +1741,16 @@ class GIMMVFIInterpolate:
result = torch.cat(result_chunks, dim=0) result = torch.cat(result_chunks, dim=0)
# Convert oversampled to ComfyUI format for second output
oversampled = result.cpu().permute(0, 2, 3, 1)
# Target FPS: select frames from oversampled result # Target FPS: select frames from oversampled result
if use_target_fps: if use_target_fps:
result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input) result = _select_target_fps_frames(result, source_fps, target_fps, mult, total_input)
# Convert back to ComfyUI [B, H, W, C], on CPU # Convert back to ComfyUI [B, H, W, C], on CPU
result = result.cpu().permute(0, 2, 3, 1) result = result.cpu().permute(0, 2, 3, 1)
return (result,) return (result, oversampled)
class GIMMVFISegmentInterpolate(GIMMVFIInterpolate): class GIMMVFISegmentInterpolate(GIMMVFIInterpolate):
@@ -1856,7 +1868,7 @@ class GIMMVFISegmentInterpolate(GIMMVFIInterpolate):
# Standard multiplier mode # Standard multiplier mode
is_continuation = segment_index > 0 is_continuation = segment_index > 0
(result,) = super().interpolate( (result, _) = super().interpolate(
segment_images, model, multiplier, single_pass, segment_images, model, multiplier, single_pass,
clear_cache_after_n_frames, keep_device, all_on_gpu, clear_cache_after_n_frames, keep_device, all_on_gpu,
batch_size, chunk_size, batch_size, chunk_size,