someone-in-the-world commited on
Commit
97e9c44
·
unverified ·
2 Parent(s): 663dfc8fed6bc3

Merge pull request #58 from some1galaxy/tensor-only-interpolation-upscale-pipeline

Browse files
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, multiplier=int(frame_factor), progress_callback=_report_interpolation_progress
 
 
 
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
- ) -> list[np.ndarray]:
 
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 from_tensor(tensor: torch.Tensor) -> np.ndarray:
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 _frames_to_tensor(frames: list[np.ndarray]) -> torch.Tensor:
 
 
 
 
 
 
 
 
 
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)` — the same representation
160
- `_apply_interpolation` already produces, so callers don't need to round-trip through
161
- `PIL.Image` just to cross this boundary.
 
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].shape[:2]
176
  batch = [frames[i]]
177
  i += 1
178
- while i < len(frames) and len(batch) < FRAME_BATCH_SIZE and frames[i].shape[:2] == (h, w):
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