ProGamerGov commited on
Commit
3e2b852
·
verified ·
1 Parent(s): e7affac

Add example video script

Browse files
Files changed (1) hide show
  1. create_360_sweep_frames.py +351 -0
create_360_sweep_frames.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Install dependencies:
3
+ pip install pytorch360convert
4
+
5
+ Example ffmpeg command to use on output frames:
6
+ ffmpeg -framerate 60 -i output_frames/sweep360_%06d.png -c:v libx264 -pix_fmt yuv420p my_360_video.mp4
7
+
8
+ # Example for calculating FOV to use for specific dimensions
9
+ import math
10
+ width, height = 1280, 896
11
+ ratio = width / height
12
+ vfov_deg = 70.0
13
+ vfov = math.radians(vfov_deg)
14
+ hfov = 2 * math.atan(ratio * math.tan(vfov / 2))
15
+ hfov_deg = math.degrees(hfov)
16
+ print(hfov_deg) # ~90.02°
17
+ """
18
+
19
+ import math
20
+ import os
21
+ from typing import Dict, List, Optional, Tuple, Union
22
+
23
+ import torch
24
+ from pytorch360convert import e2p
25
+ from PIL import Image
26
+ import numpy as np
27
+ from tqdm import tqdm
28
+
29
+
30
+ def load_image_to_tensor(path: str, device: Optional[torch.device] = None) -> torch.Tensor:
31
+ """
32
+ Load an image file to a float torch tensor in CHW format, range [0,1].
33
+ """
34
+ img = Image.open(path).convert("RGB")
35
+ arr = np.array(img).astype(np.float32) / 255.0 # HWC float32
36
+ t = torch.from_numpy(arr) # HWC
37
+ t = t.permute(2, 0, 1) # CHW
38
+ if device is not None:
39
+ t = t.to(device)
40
+ return t
41
+
42
+
43
+ def _linear_progress(n_frames: int) -> List[float]:
44
+ """
45
+ Generate a linear progression from 0.0 to 1.0 over n_frames.
46
+
47
+ Args:
48
+ n_frames (int): Number of frames.
49
+
50
+ Returns:
51
+ List[float]: List of normalized progress values.
52
+ """
53
+ return [i / max(1, (n_frames - 1)) for i in range(n_frames)]
54
+
55
+
56
+ def _ease_in_out_progress(n_frames: int) -> List[float]:
57
+ """
58
+ Generate an ease-in-out progression (cosine smoothing) from 0.0 to 1.0.
59
+
60
+ Args:
61
+ n_frames (int): Number of frames.
62
+
63
+ Returns:
64
+ List[float]: List of normalized progress values.
65
+ """
66
+ return [
67
+ 0.5 * (1 - math.cos(math.pi * (i / max(1, (n_frames - 1)))))
68
+ for i in range(n_frames)
69
+ ]
70
+
71
+
72
+ def _save_tensor_as_image(tensor: torch.Tensor, path: str) -> None:
73
+ """
74
+ Save a CHW float tensor (range [0, 1]) to directory
75
+ """
76
+ if tensor.dim() == 4: # [B,H,W,C] -> take first
77
+ tensor = tensor[0]
78
+ tensor = tensor.permute(1, 2, 0)
79
+ t = tensor.detach().cpu().clamp(0.0, 1.0) * 255.0
80
+ Image.fromarray(t.to(dtype=torch.uint8).numpy()).save(path)
81
+
82
+
83
+ def generate_frames_from_equirect(
84
+ equi_tensors: List[torch.Tensor],
85
+ out_dir: str,
86
+ resolution: Tuple[int, int] = (1080, 1920),
87
+ fps: int = 30,
88
+ duration_per_image: Optional[float] = 4.0,
89
+ total_duration: Optional[float] = None,
90
+ fov_deg: Union[float, Tuple[float, float]] = (70.0, 60.0),
91
+ interpolation_mode: str = "bilinear",
92
+ speed_profile: str = "constant",
93
+ vertical_movement: Optional[Dict] = None,
94
+ device: Optional[torch.device] = None,
95
+ start_frame_index: int = 0,
96
+ save_format: str = "png",
97
+ start_yaw_deg: float = 0.0,
98
+ end_yaw_deg: float = 360.0,
99
+ filename_prefix: str = "frame",
100
+ verbose: bool = True,
101
+ ) -> List[str]:
102
+ """
103
+ Generate video frames by sweeping through one or more equirectangular images.
104
+
105
+ Args:
106
+ equi_tensors (List[torch.Tensor]): List of equirectangular image tensors.
107
+ out_dir (str): Output directory where frames will be saved.
108
+ resolution (tuple of int): Output frame resolution as (height, width). Default: (1080, 1920)
109
+ fps (int): Frames per second for timing calculations. Default: 30
110
+ duration_per_image (float): Duration in seconds for each image sweep. Default: 4.0
111
+ total_duration (float): Total duration in seconds for all images combined. Default: None
112
+ fov_deg (float or tuple): Field of view in degrees. Default: (70.0, 60.0)
113
+ interpolation_mode (str): Resampling interpolation. Options: "nearest", "bilinear", "bicubic". Default: "bilinear"
114
+ speed_profile (str): Progression curve. Options: "constant", "ease_in_out". Default: "constant"
115
+ vertical_movement (dict): Parameters for adding pitch movement. Default: None
116
+ device (torch.device): Torch device to run on. Default: cpu
117
+ start_frame_index (int): Starting frame index for naming. Default: 0
118
+ save_format (str): Image format. Options: "png", "jpg", "jpeg", "bmp". Default: "png"
119
+ start_yaw_deg (float): Starting yaw angle in degrees. Default: 0.0
120
+ end_yaw_deg (float): Ending yaw angle in degrees. Default: 360.0
121
+ filename_prefix (str): Prefix for saved frame filenames. Default: "frame"
122
+ verbose (bool): Print progress information. Default: True
123
+
124
+ Returns:
125
+ List[str]: List of file paths for the saved frames.
126
+ """
127
+ os.makedirs(out_dir, exist_ok=True)
128
+ device = device if device is not None else torch.device("cpu")
129
+ saved_paths = []
130
+ n_images = len(equi_tensors)
131
+
132
+ if n_images == 0:
133
+ return saved_paths
134
+
135
+ # Decide frames per image
136
+ if total_duration is not None:
137
+ assert total_duration > 0
138
+ seconds_per_image = total_duration / n_images
139
+ else:
140
+ seconds_per_image = duration_per_image if duration_per_image is not None else 4.0
141
+
142
+ frames_per_image = max(1, int(round(seconds_per_image * fps)))
143
+
144
+ # Calculate degrees per frame for consistent speed
145
+ vm = vertical_movement or {"mode": "none"}
146
+ vm_mode = vm.get("mode", "none")
147
+ horizontal_distance = abs(end_yaw_deg - start_yaw_deg)
148
+ degrees_per_frame = horizontal_distance / frames_per_image
149
+
150
+ # Calculate total frames for progress tracking
151
+ total_frames = n_images * frames_per_image
152
+
153
+ # Add extra frames for separate pole sweep if enabled
154
+ if vm_mode == "separate" or vm_mode == "both":
155
+ # Pole sweep path: level (0°) -> down (-85°) -> up (+85°) -> level (0°) = 340° total
156
+ vertical_distance = 340.0
157
+ pole_frames = max(1, int(round(vertical_distance / degrees_per_frame)))
158
+ total_frames += n_images * pole_frames
159
+
160
+ # Choose progress function
161
+ if speed_profile == "constant":
162
+ progress_fn = _linear_progress
163
+ elif speed_profile == "ease_in_out":
164
+ progress_fn = _ease_in_out_progress
165
+ else:
166
+ raise ValueError("speed_profile must be 'constant' or 'ease_in_out'")
167
+
168
+ frame_idx = start_frame_index
169
+ current_frame = 0
170
+ e2p_jit = e2p
171
+
172
+ yaw_start, yaw_end = start_yaw_deg, end_yaw_deg
173
+
174
+ for img_idx, e_img in enumerate(equi_tensors):
175
+ if verbose:
176
+ print(f"Processing image {img_idx + 1}/{n_images}...")
177
+
178
+ n = frames_per_image
179
+ prog = progress_fn(n)
180
+ yaw_values = [yaw_start + p * (yaw_end - yaw_start) for p in prog]
181
+
182
+ # Vertical values
183
+ if vm_mode == "during" or vm_mode == "both":
184
+ amplitude = float(vm.get("amplitude_deg", 15.0))
185
+ vertical_pattern = vm.get("pattern", "sine")
186
+ if vertical_pattern == "sine":
187
+ v_values = [amplitude * math.sin(2 * math.pi * p) for p in prog]
188
+ else:
189
+ v_values = [amplitude * (2 * p - 1) for p in prog]
190
+ else:
191
+ v_values = [0.0] * n
192
+
193
+ # Rotation frames
194
+ for i_frame in tqdm(range(n), desc=f"Image {img_idx + 1} rotation", disable=not verbose):
195
+ h_deg = yaw_values[i_frame]
196
+ v_deg = v_values[i_frame]
197
+ pers = e2p_jit(
198
+ e_img,
199
+ fov_deg=fov_deg,
200
+ h_deg=h_deg,
201
+ v_deg=v_deg,
202
+ out_hw=resolution,
203
+ mode=interpolation_mode,
204
+ channels_first=True,
205
+ ).unsqueeze(0)
206
+ filename = f"{filename_prefix}_{frame_idx:06d}.{save_format}"
207
+ path = os.path.join(out_dir, filename)
208
+ _save_tensor_as_image(pers, path)
209
+ saved_paths.append(path)
210
+ frame_idx += 1
211
+ current_frame += 1
212
+
213
+ # Optional separate pole sweep - continues from end position
214
+ if vm_mode == "separate" or vm_mode == "both":
215
+ if verbose:
216
+ print(f" Generating pole sweep for image {img_idx + 1}...")
217
+
218
+ # Continue from the ending yaw position
219
+ final_yaw = yaw_values[-1]
220
+
221
+ # Calculate frames based on angular distance to maintain constant speed
222
+ horizontal_distance = abs(yaw_end - yaw_start)
223
+ degrees_per_frame = horizontal_distance / frames_per_image
224
+
225
+ # Vertical path: 0° -> -85° -> +85° -> 0° = 340° total
226
+ vertical_distance = 340.0
227
+ pole_frames = max(1, int(round(vertical_distance / degrees_per_frame)))
228
+
229
+ if verbose:
230
+ print(f" Horizontal: {horizontal_distance}° in {frames_per_image} frames ({degrees_per_frame:.2f}°/frame)")
231
+ print(f" Vertical: {vertical_distance}° in {pole_frames} frames ({degrees_per_frame:.2f}°/frame)")
232
+
233
+ # Use linear progress for consistent speed throughout
234
+ pole_progress = _linear_progress(pole_frames)
235
+ pole_v_values = []
236
+
237
+ # Phase distances: 85° down, 170° up, 85° down
238
+ total_distance = 340.0
239
+ phase1_distance = 85.0 # Level to bottom
240
+ phase2_distance = 170.0 # Bottom to top
241
+ phase3_distance = 85.0 # Top to level
242
+
243
+ for p in pole_progress:
244
+ current_distance = p * total_distance
245
+
246
+ if current_distance <= phase1_distance:
247
+ # Phase 1: Level (0°) -> Down (-85°)
248
+ phase_progress = current_distance / phase1_distance
249
+ v_deg = 0.0 - (85.0 * phase_progress)
250
+ elif current_distance <= phase1_distance + phase2_distance:
251
+ # Phase 2: Down (-85°) -> Up (+85°)
252
+ phase_progress = (current_distance - phase1_distance) / phase2_distance
253
+ v_deg = -85.0 + (170.0 * phase_progress)
254
+ else:
255
+ # Phase 3: Up (+85°) -> Level (0°)
256
+ phase_progress = (current_distance - phase1_distance - phase2_distance) / phase3_distance
257
+ v_deg = 85.0 - (85.0 * phase_progress)
258
+
259
+ pole_v_values.append(v_deg)
260
+
261
+ for pole_idx, v_deg in tqdm(enumerate(pole_v_values), total=len(pole_v_values), desc=f"Image {img_idx + 1} pole sweep", disable=not verbose):
262
+ pers = e2p(
263
+ e_img,
264
+ fov_deg=fov_deg,
265
+ h_deg=final_yaw,
266
+ v_deg=v_deg,
267
+ out_hw=resolution,
268
+ mode=interpolation_mode,
269
+ channels_first=True,
270
+ )
271
+ filename = f"{filename_prefix}_{frame_idx:06d}.{save_format}"
272
+ path = os.path.join(out_dir, filename)
273
+ _save_tensor_as_image(pers, path)
274
+ saved_paths.append(path)
275
+ frame_idx += 1
276
+ current_frame += 1
277
+
278
+ if verbose:
279
+ print(f"\nCompleted! Generated {len(saved_paths)} frames in {out_dir}")
280
+
281
+ return saved_paths
282
+
283
+
284
+ def main():
285
+ """
286
+ Main function - configure your parameters here
287
+ """
288
+ # Configuration
289
+ IMAGE_PATHS = ["path/to/equi_image.jpg"]
290
+ OUTPUT_DIR = "path/to/output_frames"
291
+ start_idx = 0
292
+
293
+ # Frame generation settings
294
+ WIDTH = 1280
295
+ HEIGHT = 896
296
+ FPS = 60
297
+ DURATION_PER_IMAGE = 10.0
298
+ FOV_HORIZONTAL = 90.0169847156118
299
+ FOV_VERTICAL = 70
300
+
301
+ # Movement settings
302
+ SPEED_PROFILE = "constant" # "constant" or "ease_in_out"
303
+ START_YAW = 0.0
304
+ END_YAW = 360.0
305
+
306
+ # Vertical movement (set mode to "none" to disable)
307
+ VERTICAL_MOVEMENT = {
308
+ "mode": "separate", # "none", "during", "separate", or "both"
309
+ "amplitude_deg": 90.0,
310
+ "pattern": "sine", # "sine" or "linear"
311
+ }
312
+
313
+ # Other settings
314
+ INTERPOLATION_MODE = "bilinear" # "bilinear", "bicubic", or "nearest"
315
+ SAVE_FORMAT = "png" # "png", "jpg", "jpeg", or "bmp"
316
+ FILENAME_PREFIX = "sweep360"
317
+ DEVICE = "cuda:0"
318
+
319
+ # Load images as tensors
320
+ equi_tensors = []
321
+ for img_path in IMAGE_PATHS:
322
+ equi_tensors.append(load_image_to_tensor(img_path, DEVICE))
323
+
324
+ if not equi_tensors:
325
+ print("No images loaded. Please add your equirectangular images.")
326
+ return
327
+
328
+ # Generate frames
329
+ saved_paths = generate_frames_from_equirect(
330
+ equi_tensors=equi_tensors,
331
+ out_dir=OUTPUT_DIR,
332
+ resolution=(HEIGHT, WIDTH),
333
+ fps=FPS,
334
+ duration_per_image=DURATION_PER_IMAGE,
335
+ fov_deg=(FOV_HORIZONTAL, FOV_VERTICAL),
336
+ interpolation_mode=INTERPOLATION_MODE,
337
+ speed_profile=SPEED_PROFILE,
338
+ vertical_movement=VERTICAL_MOVEMENT,
339
+ start_yaw_deg=START_YAW,
340
+ end_yaw_deg=END_YAW,
341
+ save_format=SAVE_FORMAT,
342
+ filename_prefix=FILENAME_PREFIX,
343
+ verbose=True,
344
+ start_frame_index=start_idx,
345
+ )
346
+
347
+ print(f"Successfully generated {len(saved_paths)} frames")
348
+
349
+
350
+ if __name__ == "__main__":
351
+ main()