miwaniza commited on
Commit
e092a31
·
1 Parent(s): a16de0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +80 -223
app.py CHANGED
@@ -24,21 +24,15 @@
24
  # SOFTWARE.
25
 
26
 
27
- import click
28
- from PIL import Image
29
  import os
30
  import shutil
31
  from hashlib import md5
32
- from multiprocessing import cpu_count
33
- from joblib import Parallel, delayed
34
- from tqdm import trange
35
- from math import log, ceil, pow, sin, cos, pi
36
- from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
37
- from moviepy.editor import VideoFileClip, AudioFileClip
38
- from concurrent.futures import ThreadPoolExecutor
39
- import concurrent
40
- from tqdm import tqdm
41
 
 
 
 
 
42
 
43
  EASING_FUNCTIONS = {
44
  "linear": lambda x: x,
@@ -108,203 +102,61 @@ def get_px_or_fraction(value, reference_value):
108
  return int(value)
109
 
110
 
111
- @click.command()
112
- @click.argument(
113
- "image_paths",
114
- nargs=-1,
115
- type=click.Path(exists=True),
116
- required=True,
117
- )
118
- @click.option(
119
- "-a",
120
- "--audio_path",
121
- type=click.Path(exists=True, dir_okay=False),
122
- default=None,
123
- help="Audio file path that will be added to the video.",
124
- )
125
- @click.option(
126
- "-z",
127
- "--zoom",
128
- type=float,
129
- default=2.0,
130
- help="Zoom factor/ratio between images.",
131
- show_default=True,
132
- )
133
- @click.option(
134
- "-d",
135
- "--duration",
136
- type=float,
137
- default=10.0,
138
- help="Duration of the video in seconds.",
139
- show_default=True,
140
- )
141
- @click.option(
142
- "-e",
143
- "--easing",
144
- type=click.Choice(list(EASING_FUNCTIONS.keys())),
145
- default=DEFAULT_EASING_KEY,
146
- help="Easing function.",
147
- show_default=True,
148
- )
149
- @click.option(
150
- "-r",
151
- "--direction",
152
- type=click.Choice(["in", "out", "inout", "outin"]),
153
- default="out",
154
- help="Zoom direction. Inout and outin combine both directions.",
155
- show_default=True,
156
- )
157
- @click.option(
158
- "-o",
159
- "--output",
160
- type=click.Path(),
161
- default="output.mp4",
162
- help="Output video file.",
163
- show_default=True,
164
- )
165
- @click.option(
166
- "-t",
167
- "--threads",
168
- type=int,
169
- default=-1,
170
- help="Number of threads to use to generate frames. Use values <= 0 for number of available threads on your machine minus the provided absolute value.",
171
- show_default=True,
172
- )
173
- @click.option(
174
- "--tmp-dir",
175
- type=click.Path(),
176
- default="tmp",
177
- help="Temporary directory to store frames.",
178
- show_default=True,
179
- )
180
- @click.option(
181
- "-f",
182
- "--fps",
183
- type=int,
184
- default=30,
185
- help="Frames per second of the output video.",
186
- show_default=True,
187
- )
188
- @click.option(
189
- "-w",
190
- "--width",
191
- type=float,
192
- default=1,
193
- help="Width of the output video. Values > 1 are interpreted as specific sizes in pixels. Values <= 1 are interpreted as a fraction of the width of the first image.",
194
- show_default=True,
195
- )
196
- @click.option(
197
- "-h",
198
- "--height",
199
- type=float,
200
- default=1,
201
- help="Height of the output video. Values > 1 are interpreted as specific sizes in pixels. Values <= 1 are interpreted as a fraction of the height of the first image.",
202
- show_default=True,
203
- )
204
- @click.option(
205
- "-s",
206
- "--resampling",
207
- type=click.Choice(list(RESAMPLING_FUNCTIONS.keys())),
208
- default=DEFAULT_RESAMPLING_KEY,
209
- help="Resampling techique to use when resizing images.",
210
- show_default=True,
211
- )
212
- @click.option(
213
- "-m",
214
- "--margin",
215
- type=float,
216
- default=0.05,
217
- help="Size of the margin to cut from the edges of each image for better blending with the next/previous image. Values > 1 are interpreted as specific sizes in pixels. Values <= 1 are interpreted as a fraction of the smaller size of the first image.",
218
- show_default=True,
219
- )
220
- @click.option(
221
- "--keep-frames",
222
- is_flag=True,
223
- default=False,
224
- help="Keep frames in the temporary directory. Otherwise, it will be deleted after the video is generated.",
225
- show_default=True,
226
- )
227
- @click.option(
228
- "--skip-video-generation",
229
- is_flag=True,
230
- default=False,
231
- help="Skip video generation. Useful if you only want to generate the frames. This option will keep the temporary directory similar to --keep-frames flag.",
232
- show_default=True,
233
- )
234
- @click.option(
235
- "--reverse-images",
236
- is_flag=True,
237
- default=False,
238
- help="Reverse the order of the images.",
239
- show_default=True,
240
- )
241
  def zoom_video_composer(
242
- image_paths,
243
- audio_path=None,
244
- zoom=2.0,
245
- duration=10.0,
246
- easing=DEFAULT_EASING_KEY,
247
- direction="out",
248
- output="output.mp4",
249
- threads=-1,
250
- tmp_dir="tmp",
251
- fps=30,
252
- width=1,
253
- height=1,
254
- resampling=DEFAULT_RESAMPLING_KEY,
255
- margin=0.05,
256
- keep_frames=False,
257
- skip_video_generation=False,
258
- reverse_images=False,
259
  ):
260
  """Compose a zoom video from multiple provided images."""
 
 
 
 
 
 
 
 
261
 
262
- # Read images
263
- _image_paths = []
264
- for image_path in image_paths:
265
- if os.path.isfile(image_path):
266
- _image_paths.append(image_path)
267
- elif os.path.isdir(image_path):
268
- for subimage_path in sorted(os.listdir(image_path)):
269
- _image_paths.append(os.path.join(image_path, subimage_path))
270
- image_paths = _image_paths
271
-
272
- images = []
273
- click.echo(f"Reading {len(image_paths)} image files ...")
274
- for image_path in image_paths:
275
- if not image_path.lower().endswith((".png", ".jpg", ".jpeg", ".webp")):
276
- click.echo(f"Unsupported file type: {image_path}, skipping")
277
- continue
278
-
279
- image = Image.open(image_path)
280
- images.append(image)
281
 
282
  if len(images) < 2:
283
- raise ValueError("At least two images are required to create a zoom video")
 
 
 
 
284
 
285
  # Setup some additional variables
286
  easing_func = EASING_FUNCTIONS.get(easing, None)
287
  if easing_func is None:
288
- raise ValueError(f"Unsupported easing function: {easing}")
 
289
 
290
  resampling_func = RESAMPLING_FUNCTIONS.get(resampling, None)
291
  if resampling_func is None:
292
- raise ValueError(f"Unsupported resampling function: {resampling}")
 
293
 
294
  num_images = len(images) - 1
295
  num_frames = int(duration * fps)
296
  num_frames_half = int(num_frames / 2)
297
- tmp_dir_hash = os.path.join(
298
- tmp_dir, md5("".join(image_paths).encode("utf-8")).hexdigest()
299
- )
300
-
301
  width = get_px_or_fraction(width, images[0].width)
302
  height = get_px_or_fraction(height, images[0].height)
303
  margin = get_px_or_fraction(margin, min(images[0].width, images[0].height))
304
 
305
  # Create tmp dir
306
  if not os.path.exists(tmp_dir_hash):
307
- click.echo(f"Creating temporary directory for frames: {tmp_dir} ...")
308
  os.makedirs(tmp_dir_hash, exist_ok=True)
309
 
310
  if direction in ["out", "outin"]:
@@ -314,38 +166,24 @@ def zoom_video_composer(
314
  images.reverse()
315
 
316
  # Blend images (take care of margins)
317
- click.echo(f"Blending {len(images)} images ...")
318
- for i in trange(1, num_images + 1):
319
  inner_image = images[i]
320
  outer_image = images[i - 1]
321
  inner_image = inner_image.crop(
322
  (margin, margin, inner_image.width - margin, inner_image.height - margin)
323
  )
324
 
325
- # Some coloring for debugging purposes
326
- # debug_colors = ['red', 'green', 'blue', 'yellow', 'cyan', 'magenta']
327
- # layer = Image.new('RGB', inner_image.size, debug_colors[i % 6])
328
- # inner_image = Image.blend(inner_image, layer, 0.25)
329
-
330
  image = zoom_crop(outer_image, zoom, resampling_func)
331
  image.paste(inner_image, (margin, margin))
332
  images[i] = image
333
 
334
- # Save image for debugging purposes
335
- # image_path = os.path.join(tmp_dir_hash, f"_blending_step_1_{i:06d}.png")
336
- # image.save(image_path)
337
-
338
  images_resized = [resize_scale(i, zoom, resampling_func) for i in images]
339
- for i in trange(num_images, 0, -1):
340
  inner_image = images_resized[i]
341
  image = images_resized[i - 1]
342
  inner_image = resize_scale(inner_image, 1.0 / zoom, resampling_func)
343
 
344
- # Some coloring for debugging purposes
345
- # debug_colors = ['red', 'green', 'blue', 'yellow', 'cyan', 'magenta']
346
- # layer = Image.new('RGB', inner_image.size, debug_colors[i % 6])
347
- # inner_image = Image.blend(inner_image, layer, 0.25)
348
-
349
  image.paste(
350
  inner_image,
351
  (
@@ -355,10 +193,6 @@ def zoom_video_composer(
355
  )
356
  images_resized[i] = image
357
 
358
- # Save image for debugging purposes
359
- # image_path = os.path.join(tmp_dir_hash, f"_blending_step_2_{i:06d}.png")
360
- # image.save(image_path)
361
-
362
  images = images_resized
363
 
364
  # Create frames
@@ -401,16 +235,12 @@ def zoom_video_composer(
401
  frame_path = os.path.join(tmp_dir_hash, f"{i:06d}.png")
402
  frame.save(frame_path)
403
 
404
- n_jobs = threads if threads > 0 else cpu_count() - threads
405
- click.echo(f"Creating frames in {n_jobs} threads ...")
406
-
407
- with ThreadPoolExecutor(max_workers=n_jobs) as executor:
408
- futures = [executor.submit(process_frame, i) for i in range(num_frames)]
409
- for _ in tqdm(concurrent.futures.as_completed(futures), total=num_frames):
410
- pass
411
 
412
  # Write video
413
- click.echo(f"Writing video to: {output} ...")
414
  image_files = [
415
  os.path.join(tmp_dir_hash, f"{i:06d}.png") for i in range(num_frames)
416
  ]
@@ -419,8 +249,9 @@ def zoom_video_composer(
419
 
420
  # Add audio
421
  if audio_path:
422
- click.echo(f"Adding audio from: {audio_path} ...")
423
- audio_clip = AudioFileClip(audio_path)
 
424
  audio_clip = audio_clip.subclip(0, video_clip.end)
425
  video_clip = video_clip.set_audio(audio_clip)
426
  video_write_kwargs["audio_codec"] = "aac"
@@ -431,11 +262,37 @@ def zoom_video_composer(
431
  if not keep_frames and not skip_video_generation:
432
  shutil.rmtree(tmp_dir_hash, ignore_errors=False, onerror=None)
433
  if not os.listdir(tmp_dir):
434
- click.echo(f"Removing empty temporary directory for frames: {tmp_dir} ...")
435
  os.rmdir(tmp_dir)
436
-
437
- click.echo("Done!")
438
-
439
-
440
- if __name__ == "__main__":
441
- zoom_video_composer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  # SOFTWARE.
25
 
26
 
 
 
27
  import os
28
  import shutil
29
  from hashlib import md5
30
+ from math import ceil, pow, sin, cos, pi
 
 
 
 
 
 
 
 
31
 
32
+ import gradio as gr
33
+ from PIL import Image
34
+ from moviepy.editor import AudioFileClip
35
+ from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
36
 
37
  EASING_FUNCTIONS = {
38
  "linear": lambda x: x,
 
102
  return int(value)
103
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  def zoom_video_composer(
106
+ image_paths,
107
+ audio_path,
108
+ zoom,
109
+ duration,
110
+ easing,
111
+ direction,
112
+ fps,
113
+ resampling,
114
+ reverse_images,
115
+ progress=gr.Progress()
 
 
 
 
 
 
 
116
  ):
117
  """Compose a zoom video from multiple provided images."""
118
+ output = "output.mp4"
119
+ threads = -1
120
+ tmp_dir = "tmp"
121
+ width = 1
122
+ height = 1
123
+ margin = 0.05
124
+ keep_frames = False
125
+ skip_video_generation = False
126
 
127
+ # Read images from image_paths
128
+
129
+ images = list(Image.open(image_path.name) for image_path in image_paths)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  if len(images) < 2:
132
+ raise gr.Error("At least two images are required to create a zoom video")
133
+ # raise ValueError("At least two images are required to create a zoom video")
134
+
135
+ # gr.Info("Images loaded")
136
+ progress(0, desc="Images loaded")
137
 
138
  # Setup some additional variables
139
  easing_func = EASING_FUNCTIONS.get(easing, None)
140
  if easing_func is None:
141
+ raise gr.Error(f"Unsupported easing function: {easing}")
142
+ # raise ValueError(f"Unsupported easing function: {easing}")
143
 
144
  resampling_func = RESAMPLING_FUNCTIONS.get(resampling, None)
145
  if resampling_func is None:
146
+ raise gr.Error(f"Unsupported resampling function: {resampling}")
147
+ # raise ValueError(f"Unsupported resampling function: {resampling}")
148
 
149
  num_images = len(images) - 1
150
  num_frames = int(duration * fps)
151
  num_frames_half = int(num_frames / 2)
152
+ tmp_dir_hash = os.path.join(tmp_dir, md5(output.encode("utf-8")).hexdigest())
 
 
 
153
  width = get_px_or_fraction(width, images[0].width)
154
  height = get_px_or_fraction(height, images[0].height)
155
  margin = get_px_or_fraction(margin, min(images[0].width, images[0].height))
156
 
157
  # Create tmp dir
158
  if not os.path.exists(tmp_dir_hash):
159
+ progress(0, desc="Creating temporary directory for frames")
160
  os.makedirs(tmp_dir_hash, exist_ok=True)
161
 
162
  if direction in ["out", "outin"]:
 
166
  images.reverse()
167
 
168
  # Blend images (take care of margins)
169
+ progress(0, desc=f"Blending {len(images)} images")
170
+ for i in progress.tqdm(range(1, num_images + 1), desc="Blending images"):
171
  inner_image = images[i]
172
  outer_image = images[i - 1]
173
  inner_image = inner_image.crop(
174
  (margin, margin, inner_image.width - margin, inner_image.height - margin)
175
  )
176
 
 
 
 
 
 
177
  image = zoom_crop(outer_image, zoom, resampling_func)
178
  image.paste(inner_image, (margin, margin))
179
  images[i] = image
180
 
 
 
 
 
181
  images_resized = [resize_scale(i, zoom, resampling_func) for i in images]
182
+ for i in progress.tqdm(range(num_images, 0, -1), desc="Resizing images"):
183
  inner_image = images_resized[i]
184
  image = images_resized[i - 1]
185
  inner_image = resize_scale(inner_image, 1.0 / zoom, resampling_func)
186
 
 
 
 
 
 
187
  image.paste(
188
  inner_image,
189
  (
 
193
  )
194
  images_resized[i] = image
195
 
 
 
 
 
196
  images = images_resized
197
 
198
  # Create frames
 
235
  frame_path = os.path.join(tmp_dir_hash, f"{i:06d}.png")
236
  frame.save(frame_path)
237
 
238
+ progress(0, desc=f"Creating {num_frames} frames")
239
+ for i in progress.tqdm(range(num_frames), desc="Creating frames"):
240
+ process_frame(i)
 
 
 
 
241
 
242
  # Write video
243
+ progress(0, desc=f"Writing video to: {output}")
244
  image_files = [
245
  os.path.join(tmp_dir_hash, f"{i:06d}.png") for i in range(num_frames)
246
  ]
 
249
 
250
  # Add audio
251
  if audio_path:
252
+ # audio file name
253
+ progress(0, desc=f"Adding audio from: {os.path.basename(audio_path.name)}")
254
+ audio_clip = AudioFileClip(audio_path.name)
255
  audio_clip = audio_clip.subclip(0, video_clip.end)
256
  video_clip = video_clip.set_audio(audio_clip)
257
  video_write_kwargs["audio_codec"] = "aac"
 
262
  if not keep_frames and not skip_video_generation:
263
  shutil.rmtree(tmp_dir_hash, ignore_errors=False, onerror=None)
264
  if not os.listdir(tmp_dir):
265
+ progress(0, desc=f"Removing empty temporary directory for frames: {tmp_dir} ...")
266
  os.rmdir(tmp_dir)
267
+ return output
268
+
269
+
270
+ grInputs = [
271
+ gr.File(file_count="multiple", label="Upload images as folder", file_types=["image"]),
272
+ gr.File(file_count="single", label="Upload audio", file_types=["audio"]),
273
+ gr.inputs.Slider(label="Zoom factor/ratio between images", minimum=1.0, maximum=5.0, step=0.1, default=2.0),
274
+ gr.inputs.Slider(label="Duration of the video in seconds", minimum=1.0, maximum=60.0, step=1.0, default=10.0),
275
+ gr.inputs.Dropdown(label="Easing function used for zooming",
276
+ choices=["linear", "easeInSine", "easeOutSine", "easeInOutSine", "easeInQuad", "easeOutQuad",
277
+ "easeInOutQuad", "easeInCubic", "easeOutCubic", "easeInOutCubic"],
278
+ default="easeInOutSine"),
279
+ gr.inputs.Dropdown(label="Zoom direction. Inout and outin combine both directions",
280
+ choices=["in", "out", "inout", "outin"], default="out"),
281
+ gr.inputs.Slider(label="Frames per second of the output video", minimum=1, maximum=60, step=1, default=30),
282
+ gr.inputs.Dropdown(label="Resampling technique used for resizing images",
283
+ choices=["nearest", "box", "bilinear", "hamming", "bicubic", "lanczos"], default="lanczos"),
284
+ gr.inputs.Checkbox(label="Reverse images", default=False)
285
+ ]
286
+
287
+ iface = gr.Interface(
288
+ fn=zoom_video_composer,
289
+ inputs=grInputs,
290
+ outputs=[gr.outputs.Video(label="Video")],
291
+ title="Zoom Video Composer",
292
+ description="Compose a zoom video from multiple provided images.",
293
+ allow_flagging=False,
294
+ allow_screenshot=True,
295
+ allow_embedding=True,
296
+ allow_download=True)
297
+
298
+ iface.queue(concurrency_count=10).launch()