Spaces:
Running on Zero
Running on Zero
Merge pull request #58 from some1galaxy/tensor-only-interpolation-upscale-pipeline
Browse files- app.py +24 -3
- postprocess/interpolation.py +16 -3
- postprocess/upscale/upscale.py +17 -7
app.py
CHANGED
|
@@ -109,7 +109,7 @@ def _gpu_duration(
|
|
| 109 |
return duration
|
| 110 |
|
| 111 |
|
| 112 |
-
def _apply_interpolation(raw_frames, frame_factor, progress):
|
| 113 |
if frame_factor <= 1:
|
| 114 |
return list(raw_frames), FIXED_FPS
|
| 115 |
|
|
@@ -119,8 +119,13 @@ def _apply_interpolation(raw_frames, frame_factor, progress):
|
|
| 119 |
print_stage_start("interpolation")
|
| 120 |
t0 = _time.perf_counter()
|
| 121 |
try:
|
|
|
|
|
|
|
| 122 |
interpolated = interpolate_frames(
|
| 123 |
-
raw_frames,
|
|
|
|
|
|
|
|
|
|
| 124 |
)
|
| 125 |
print_stage_done("interpolation", _time.perf_counter() - t0)
|
| 126 |
return interpolated, FIXED_FPS * frame_factor
|
|
@@ -131,6 +136,16 @@ def _apply_interpolation(raw_frames, frame_factor, progress):
|
|
| 131 |
return list(raw_frames), FIXED_FPS
|
| 132 |
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
def _apply_upscale(frames, progress):
|
| 135 |
from postprocess.upscale import upscale_frames
|
| 136 |
|
|
@@ -196,11 +211,17 @@ def run_inference(
|
|
| 196 |
raw_frames = result.frames[0] # (T, H, W, C) float32 in [0, 1]
|
| 197 |
|
| 198 |
frame_factor = frame_multiplier // FIXED_FPS
|
| 199 |
-
final_frames, final_fps = _apply_interpolation(raw_frames, frame_factor, progress)
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
upscaled_frames = _apply_upscale(final_frames, progress) if upscale_output else None
|
| 202 |
|
| 203 |
if upscaled_frames is None:
|
|
|
|
|
|
|
|
|
|
| 204 |
print_infer_done(_time.perf_counter() - t_start, final_fps, len(final_frames))
|
| 205 |
return final_frames, final_fps
|
| 206 |
|
|
|
|
| 109 |
return duration
|
| 110 |
|
| 111 |
|
| 112 |
+
def _apply_interpolation(raw_frames, frame_factor, upscale_output, progress):
|
| 113 |
if frame_factor <= 1:
|
| 114 |
return list(raw_frames), FIXED_FPS
|
| 115 |
|
|
|
|
| 119 |
print_stage_start("interpolation")
|
| 120 |
t0 = _time.perf_counter()
|
| 121 |
try:
|
| 122 |
+
# When upscale will run right after, hand it GPU tensors directly instead of round-
|
| 123 |
+
# tripping through CPU numpy in between (both stages run in the same @spaces.GPU call).
|
| 124 |
interpolated = interpolate_frames(
|
| 125 |
+
raw_frames,
|
| 126 |
+
multiplier=int(frame_factor),
|
| 127 |
+
progress_callback=_report_interpolation_progress,
|
| 128 |
+
as_tensor=upscale_output,
|
| 129 |
)
|
| 130 |
print_stage_done("interpolation", _time.perf_counter() - t0)
|
| 131 |
return interpolated, FIXED_FPS * frame_factor
|
|
|
|
| 136 |
return list(raw_frames), FIXED_FPS
|
| 137 |
|
| 138 |
|
| 139 |
+
def _frames_to_numpy(frames):
|
| 140 |
+
# Fallback-path conversion only: interpolation may have handed back GPU tensors (C,H,W)
|
| 141 |
+
# expecting upscale to consume them next (see _apply_interpolation's as_tensor). If upscale
|
| 142 |
+
# then fails or wasn't requested, callers downstream (export_to_video, logging) need plain
|
| 143 |
+
# (H,W,C) numpy — this is a no-op when frames are already numpy.
|
| 144 |
+
if frames and isinstance(frames[0], torch.Tensor):
|
| 145 |
+
return [f.permute(1, 2, 0).float().cpu().numpy() for f in frames]
|
| 146 |
+
return frames
|
| 147 |
+
|
| 148 |
+
|
| 149 |
def _apply_upscale(frames, progress):
|
| 150 |
from postprocess.upscale import upscale_frames
|
| 151 |
|
|
|
|
| 211 |
raw_frames = result.frames[0] # (T, H, W, C) float32 in [0, 1]
|
| 212 |
|
| 213 |
frame_factor = frame_multiplier // FIXED_FPS
|
| 214 |
+
final_frames, final_fps = _apply_interpolation(raw_frames, frame_factor, upscale_output, progress)
|
| 215 |
+
# Confirms which handoff mode actually ran: frame_type=Tensor (GPU, no CPU round trip) when
|
| 216 |
+
# interpolation->upscale both ran, vs. ndarray otherwise. See issue #53.
|
| 217 |
+
print_frames_info("interpolation_output", final_frames, final_fps)
|
| 218 |
|
| 219 |
upscaled_frames = _apply_upscale(final_frames, progress) if upscale_output else None
|
| 220 |
|
| 221 |
if upscaled_frames is None:
|
| 222 |
+
# Covers both upscale_output=False (already numpy, no-op) and upscale_output=True but
|
| 223 |
+
# upscale failing (final_frames may be GPU tensors from the as_tensor interpolation path).
|
| 224 |
+
final_frames = _frames_to_numpy(final_frames)
|
| 225 |
print_infer_done(_time.perf_counter() - t_start, final_fps, len(final_frames))
|
| 226 |
return final_frames, final_fps
|
| 227 |
|
postprocess/interpolation.py
CHANGED
|
@@ -60,15 +60,21 @@ def interpolate_frames(
|
|
| 60 |
multiplier: int,
|
| 61 |
scale: float = 1.0,
|
| 62 |
progress_callback: ProgressCallback | None = None,
|
| 63 |
-
|
|
|
|
| 64 |
"""Interpolate frames with RIFE to `multiplier`x the input frame rate.
|
| 65 |
|
| 66 |
Args:
|
| 67 |
frames_np: (T, H, W, C) array or list of (H, W, C) arrays, float32 in [0, 1].
|
| 68 |
multiplier: 2, 4, or 8. Values < 2 return the frames unchanged (as a list).
|
| 69 |
progress_callback: progress_callback(done, total) fired after each input-frame gap.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
Returns:
|
| 71 |
-
List of (H, W, C) float32 numpy arrays in [0, 1]
|
|
|
|
| 72 |
"""
|
| 73 |
if multiplier < 2:
|
| 74 |
return list(frames_np) if isinstance(frames_np, np.ndarray) else frames_np
|
|
@@ -88,11 +94,18 @@ def interpolate_frames(
|
|
| 88 |
t = t.permute(2, 0, 1).unsqueeze(0)
|
| 89 |
return F.pad(t, padding).half()
|
| 90 |
|
| 91 |
-
def
|
| 92 |
t = tensor[0, :, :height, :width]
|
| 93 |
t = t.permute(1, 2, 0)
|
| 94 |
return t.float().cpu().numpy()
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
def make_inference(i0: torch.Tensor, i1: torch.Tensor, n: int) -> list[torch.Tensor]:
|
| 97 |
if model.version >= 3.9:
|
| 98 |
return [model.inference(i0, i1, (i + 1) / (n + 1), scale) for i in range(n)]
|
|
|
|
| 60 |
multiplier: int,
|
| 61 |
scale: float = 1.0,
|
| 62 |
progress_callback: ProgressCallback | None = None,
|
| 63 |
+
as_tensor: bool = False,
|
| 64 |
+
) -> list[np.ndarray] | list[torch.Tensor]:
|
| 65 |
"""Interpolate frames with RIFE to `multiplier`x the input frame rate.
|
| 66 |
|
| 67 |
Args:
|
| 68 |
frames_np: (T, H, W, C) array or list of (H, W, C) arrays, float32 in [0, 1].
|
| 69 |
multiplier: 2, 4, or 8. Values < 2 return the frames unchanged (as a list).
|
| 70 |
progress_callback: progress_callback(done, total) fired after each input-frame gap.
|
| 71 |
+
as_tensor: if True, skip the GPU->CPU conversion and return (C, H, W) fp16 GPU
|
| 72 |
+
tensors instead of (H, W, C) float32 numpy arrays — for callers (e.g. upscale)
|
| 73 |
+
that consume the result on GPU right after, so frames never round-trip to CPU
|
| 74 |
+
in between.
|
| 75 |
Returns:
|
| 76 |
+
List of (H, W, C) float32 numpy arrays in [0, 1], or (if as_tensor) list of
|
| 77 |
+
(C, H, W) fp16 GPU tensors in [0, 1].
|
| 78 |
"""
|
| 79 |
if multiplier < 2:
|
| 80 |
return list(frames_np) if isinstance(frames_np, np.ndarray) else frames_np
|
|
|
|
| 94 |
t = t.permute(2, 0, 1).unsqueeze(0)
|
| 95 |
return F.pad(t, padding).half()
|
| 96 |
|
| 97 |
+
def from_tensor_np(tensor: torch.Tensor) -> np.ndarray:
|
| 98 |
t = tensor[0, :, :height, :width]
|
| 99 |
t = t.permute(1, 2, 0)
|
| 100 |
return t.float().cpu().numpy()
|
| 101 |
|
| 102 |
+
def from_tensor_gpu(tensor: torch.Tensor) -> torch.Tensor:
|
| 103 |
+
# .contiguous() detaches from the padded working tensor's storage (a plain crop is
|
| 104 |
+
# just a view into it) so the padded buffer isn't kept alive for the batch's lifetime.
|
| 105 |
+
return tensor[0, :, :height, :width].contiguous()
|
| 106 |
+
|
| 107 |
+
from_tensor = from_tensor_gpu if as_tensor else from_tensor_np
|
| 108 |
+
|
| 109 |
def make_inference(i0: torch.Tensor, i1: torch.Tensor, n: int) -> list[torch.Tensor]:
|
| 110 |
if model.version >= 3.9:
|
| 111 |
return [model.inference(i0, i1, (i + 1) / (n + 1), scale) for i in range(n)]
|
postprocess/upscale/upscale.py
CHANGED
|
@@ -146,19 +146,29 @@ def _tile_process(model: torch.nn.Module, img: torch.Tensor) -> torch.Tensor:
|
|
| 146 |
return output
|
| 147 |
|
| 148 |
|
| 149 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
array = np.stack(frames)
|
| 151 |
return torch.from_numpy(array).permute(0, 3, 1, 2).to(device=device, dtype=dtype)
|
| 152 |
|
| 153 |
|
| 154 |
def upscale_frames(
|
| 155 |
-
frames: list[np.ndarray], progress_callback: ProgressCallback | None = None
|
| 156 |
) -> list[Image.Image]:
|
| 157 |
"""Upscale every frame 4x with `4xLSDIRCompact`, tiled to bound peak memory.
|
| 158 |
|
| 159 |
-
frames are numpy float32 arrays in `[0,1]`, shape `(H,W,C)`
|
| 160 |
-
`_apply_interpolation`
|
| 161 |
-
`PIL.Image` just to cross this
|
|
|
|
| 162 |
|
| 163 |
Frames are processed in batches (of up to FRAME_BATCH_SIZE, split wherever frame size
|
| 164 |
changes) rather than one at a time, so the model sees a real batch dimension instead of
|
|
@@ -172,10 +182,10 @@ def upscale_frames(
|
|
| 172 |
|
| 173 |
i = 0
|
| 174 |
while i < len(frames):
|
| 175 |
-
h, w = frames[i]
|
| 176 |
batch = [frames[i]]
|
| 177 |
i += 1
|
| 178 |
-
while i < len(frames) and len(batch) < FRAME_BATCH_SIZE and frames[i]
|
| 179 |
batch.append(frames[i])
|
| 180 |
i += 1
|
| 181 |
|
|
|
|
| 146 |
return output
|
| 147 |
|
| 148 |
|
| 149 |
+
def _frame_hw(frame: np.ndarray | torch.Tensor) -> tuple[int, int]:
|
| 150 |
+
# numpy frames are (H, W, C); GPU tensor frames (from RIFE's as_tensor=True path) are (C, H, W).
|
| 151 |
+
return tuple(frame.shape[-2:]) if isinstance(frame, torch.Tensor) else tuple(frame.shape[:2])
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _frames_to_tensor(frames: list[np.ndarray] | list[torch.Tensor]) -> torch.Tensor:
|
| 155 |
+
if isinstance(frames[0], torch.Tensor):
|
| 156 |
+
# Already (C, H, W) fp16 GPU tensors (RIFE's as_tensor=True output) — just batch them,
|
| 157 |
+
# no host round trip.
|
| 158 |
+
return torch.stack(frames, dim=0).to(device=device, dtype=dtype)
|
| 159 |
array = np.stack(frames)
|
| 160 |
return torch.from_numpy(array).permute(0, 3, 1, 2).to(device=device, dtype=dtype)
|
| 161 |
|
| 162 |
|
| 163 |
def upscale_frames(
|
| 164 |
+
frames: list[np.ndarray] | list[torch.Tensor], progress_callback: ProgressCallback | None = None
|
| 165 |
) -> list[Image.Image]:
|
| 166 |
"""Upscale every frame 4x with `4xLSDIRCompact`, tiled to bound peak memory.
|
| 167 |
|
| 168 |
+
frames are either numpy float32 arrays in `[0,1]`, shape `(H,W,C)`, or (C,H,W) fp16 GPU
|
| 169 |
+
tensors — the same representations `_apply_interpolation` can produce, so callers don't
|
| 170 |
+
need to round-trip through `PIL.Image`, or through the CPU at all, just to cross this
|
| 171 |
+
boundary.
|
| 172 |
|
| 173 |
Frames are processed in batches (of up to FRAME_BATCH_SIZE, split wherever frame size
|
| 174 |
changes) rather than one at a time, so the model sees a real batch dimension instead of
|
|
|
|
| 182 |
|
| 183 |
i = 0
|
| 184 |
while i < len(frames):
|
| 185 |
+
h, w = _frame_hw(frames[i])
|
| 186 |
batch = [frames[i]]
|
| 187 |
i += 1
|
| 188 |
+
while i < len(frames) and len(batch) < FRAME_BATCH_SIZE and _frame_hw(frames[i]) == (h, w):
|
| 189 |
batch.append(frames[i])
|
| 190 |
i += 1
|
| 191 |
|