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

Update app.py

Browse files

Replace PIL with CV2

Files changed (1) hide show
  1. app.py +78 -89
app.py CHANGED
@@ -2,8 +2,9 @@
2
 
3
  # zoom_video_composer.py v0.2.1
4
  # https://github.com/mwydmuch/ZoomVideoComposer
 
5
 
6
- # Copyright (c) 2023 Marek Wydmuch
7
 
8
  # Permission is hereby granted, free of charge, to any person obtaining a copy
9
  # of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +27,13 @@
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,
@@ -51,33 +52,25 @@ EASING_FUNCTIONS = {
51
  DEFAULT_EASING_KEY = "easeInOutSine"
52
  DEFAULT_EASING_FUNCTION = EASING_FUNCTIONS[DEFAULT_EASING_KEY]
53
 
54
- RESAMPLING_FUNCTIONS = {
55
- "nearest": Image.Resampling.NEAREST,
56
- "box": Image.Resampling.BOX,
57
- "bilinear": Image.Resampling.BILINEAR,
58
- "hamming": Image.Resampling.HAMMING,
59
- "bicubic": Image.Resampling.BICUBIC,
60
- "lanczos": Image.Resampling.LANCZOS,
61
- }
62
- DEFAULT_RESAMPLING_KEY = "lanczos"
63
- DEFAULT_RESAMPLING_FUNCTION = RESAMPLING_FUNCTIONS[DEFAULT_RESAMPLING_KEY]
64
-
65
 
66
- def zoom_crop(image, zoom, resampling_func=Image.Resampling.LANCZOS):
67
- width, height = image.size
68
  zoom_size = (int(width * zoom), int(height * zoom))
 
69
  crop_box = (
70
- (zoom_size[0] - width) / 2,
71
- (zoom_size[1] - height) / 2,
72
- (zoom_size[0] + width) / 2,
73
- (zoom_size[1] + height) / 2,
74
  )
75
- return image.resize(zoom_size, resampling_func).crop(crop_box)
 
 
76
 
77
 
78
- def resize_scale(image, scale, resampling_func=Image.Resampling.LANCZOS):
79
- width, height = image.size
80
- return image.resize((int(width * scale), int(height * scale)), resampling_func)
81
 
82
 
83
  def zoom_in_log(easing_func, i, num_frames, num_images):
@@ -110,13 +103,11 @@ def zoom_video_composer(
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
@@ -125,34 +116,25 @@ def zoom_video_composer(
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):
@@ -160,40 +142,43 @@ def zoom_video_composer(
160
  os.makedirs(tmp_dir_hash, exist_ok=True)
161
 
162
  if direction in ["out", "outin"]:
163
- images.reverse()
164
 
165
  if reverse_images:
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
- (
190
- int((image.width - inner_image.width) / 2),
191
- int((image.height - inner_image.height) / 2),
192
- ),
193
- )
194
  images_resized[i] = image
195
 
196
- images = images_resized
197
 
198
  # Create frames
199
  def process_frame(i): # to improve
@@ -220,43 +205,49 @@ def zoom_video_composer(
220
  easing_func, i - num_frames_half, num_frames_half, num_images
221
  )
222
  else:
223
- raise ValueError(f"Unsupported direction: {direction}")
224
 
225
  current_image_idx = ceil(current_zoom_log)
226
  local_zoom = zoom ** (current_zoom_log - current_image_idx + 1)
227
 
228
  if current_zoom_log == 0.0:
229
- frame = images[0]
230
  else:
231
- frame = images[current_image_idx]
232
- frame = zoom_crop(frame, local_zoom, resampling_func)
233
 
234
- frame = frame.resize((width, height), resampling_func)
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
  ]
247
- video_clip = ImageSequenceClip(image_files, fps=fps)
248
- video_write_kwargs = {"codec": "libx264"}
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"
258
 
259
- video_clip.write_videofile(output, **video_write_kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
 
261
  # Remove tmp dir
262
  if not keep_frames and not skip_video_generation:
@@ -279,8 +270,6 @@ grInputs = [
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
 
@@ -295,4 +284,4 @@ iface = gr.Interface(
295
  allow_embedding=True,
296
  allow_download=True)
297
 
298
- iface.queue(concurrency_count=10).launch()
 
2
 
3
  # zoom_video_composer.py v0.2.1
4
  # https://github.com/mwydmuch/ZoomVideoComposer
5
+ # https://github.com/miwaniza/ZoomVideoComposer
6
 
7
+ # Copyright (c) 2023 Marek Wydmuch, Dmytro Yemelianov
8
 
9
  # Permission is hereby granted, free of charge, to any person obtaining a copy
10
  # of this software and associated documentation files (the "Software"), to deal
 
27
 
28
  import os
29
  import shutil
30
+ from concurrent.futures import ThreadPoolExecutor
31
  from hashlib import md5
32
  from math import ceil, pow, sin, cos, pi
33
 
34
+ import cv2
35
  import gradio as gr
36
+ from moviepy.editor import AudioFileClip, VideoFileClip
 
 
37
 
38
  EASING_FUNCTIONS = {
39
  "linear": lambda x: x,
 
52
  DEFAULT_EASING_KEY = "easeInOutSine"
53
  DEFAULT_EASING_FUNCTION = EASING_FUNCTIONS[DEFAULT_EASING_KEY]
54
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
+ def zoom_crop_cv2(image, zoom):
57
+ height, width, channels = image.shape
58
  zoom_size = (int(width * zoom), int(height * zoom))
59
+ # crop box as integers
60
  crop_box = (
61
+ int((zoom_size[0] - width) / 2),
62
+ int((zoom_size[1] - height) / 2),
63
+ int((zoom_size[0] + width) / 2),
64
+ int((zoom_size[1] + height) / 2),
65
  )
66
+ im = cv2.resize(image, zoom_size, interpolation=cv2.INTER_LANCZOS4)
67
+ im = im[crop_box[1]:crop_box[3], crop_box[0]:crop_box[2]]
68
+ return im
69
 
70
 
71
+ def resize_scale(image, scale):
72
+ height, width = image.shape[:2]
73
+ return cv2.resize(image, (int(width * scale), int(height * scale)), interpolation=cv2.INTER_LANCZOS4)
74
 
75
 
76
  def zoom_in_log(easing_func, i, num_frames, num_images):
 
103
  easing,
104
  direction,
105
  fps,
 
106
  reverse_images,
107
  progress=gr.Progress()
108
  ):
109
  """Compose a zoom video from multiple provided images."""
110
  output = "output.mp4"
 
111
  tmp_dir = "tmp"
112
  width = 1
113
  height = 1
 
116
  skip_video_generation = False
117
 
118
  # Read images from image_paths
119
+ images_cv2 = list(cv2.imread(image_path.name) for image_path in image_paths)
120
 
121
+ if len(images_cv2) < 2:
 
 
122
  raise gr.Error("At least two images are required to create a zoom video")
 
123
 
 
124
  progress(0, desc="Images loaded")
125
 
126
  # Setup some additional variables
127
  easing_func = EASING_FUNCTIONS.get(easing, None)
128
  if easing_func is None:
129
  raise gr.Error(f"Unsupported easing function: {easing}")
 
130
 
131
+ num_images = len(images_cv2) - 1
 
 
 
 
 
132
  num_frames = int(duration * fps)
133
  num_frames_half = int(num_frames / 2)
134
  tmp_dir_hash = os.path.join(tmp_dir, md5(output.encode("utf-8")).hexdigest())
135
+ width = get_px_or_fraction(width, images_cv2[0].shape[1])
136
+ height = get_px_or_fraction(height, images_cv2[0].shape[0])
137
+ margin = get_px_or_fraction(margin, min(images_cv2[0].shape[1], images_cv2[0].shape[0]))
138
 
139
  # Create tmp dir
140
  if not os.path.exists(tmp_dir_hash):
 
142
  os.makedirs(tmp_dir_hash, exist_ok=True)
143
 
144
  if direction in ["out", "outin"]:
145
+ images_cv2.reverse()
146
 
147
  if reverse_images:
148
+ images_cv2.reverse()
149
 
150
  # Blend images (take care of margins)
151
+ progress(0, desc=f"Blending {len(images_cv2)} images")
152
  for i in progress.tqdm(range(1, num_images + 1), desc="Blending images"):
153
+ inner_image = images_cv2[i]
154
+ outer_image = images_cv2[i - 1]
155
+ inner_image = inner_image[
156
+ margin:inner_image.shape[0] - margin,
157
+ margin:inner_image.shape[1] - margin
158
+ ]
159
+ image = zoom_crop_cv2(outer_image, zoom)
160
+ image[
161
+ margin:margin + inner_image.shape[0],
162
+ margin:margin + inner_image.shape[1]
163
+ ] = inner_image
164
+ images_cv2[i] = image
165
+
166
+ images_resized = [resize_scale(i, zoom) for i in images_cv2]
167
  for i in progress.tqdm(range(num_images, 0, -1), desc="Resizing images"):
168
  inner_image = images_resized[i]
169
  image = images_resized[i - 1]
170
+ inner_image = resize_scale(inner_image, 1.0 / zoom)
171
+
172
+ h, w = image.shape[:2]
173
+ ih, iw = inner_image.shape[:2]
174
+ x = int((w - iw) / 2)
175
+ y = int((h - ih) / 2)
176
+
177
+ image[y:y + ih, x:x + iw] = inner_image
178
+
179
  images_resized[i] = image
180
 
181
+ images_cv2 = images_resized
182
 
183
  # Create frames
184
  def process_frame(i): # to improve
 
205
  easing_func, i - num_frames_half, num_frames_half, num_images
206
  )
207
  else:
208
+ raise gr.Error(f"Unsupported direction: {direction}")
209
 
210
  current_image_idx = ceil(current_zoom_log)
211
  local_zoom = zoom ** (current_zoom_log - current_image_idx + 1)
212
 
213
  if current_zoom_log == 0.0:
214
+ frame_image = images_cv2[0]
215
  else:
216
+ frame_image = images_cv2[current_image_idx]
217
+ frame_image = zoom_crop_cv2(frame_image, local_zoom)
218
 
219
+ frame_image = cv2.resize(frame_image, (width, height), interpolation=cv2.INTER_LANCZOS4)
220
  frame_path = os.path.join(tmp_dir_hash, f"{i:06d}.png")
221
+ cv2.imwrite(frame_path, frame_image)
222
 
223
  progress(0, desc=f"Creating {num_frames} frames")
224
+
225
+ with ThreadPoolExecutor(8) as executor:
226
+ list(progress.tqdm(executor.map(process_frame, range(num_frames)), total=num_frames, desc="Creating frames"))
227
 
228
  # Write video
229
  progress(0, desc=f"Writing video to: {output}")
230
  image_files = [
231
  os.path.join(tmp_dir_hash, f"{i:06d}.png") for i in range(num_frames)
232
  ]
 
 
 
 
 
 
 
 
 
 
 
233
 
234
+ # Create video clip using images in tmp dir and audio if provided
235
+ frame_size = (width, height)
236
+ out = cv2.VideoWriter(output, cv2.VideoWriter_fourcc(*'mp4v'), fps, frame_size)
237
+ for i in progress.tqdm(range(num_frames), desc="Writing video"):
238
+ frame = cv2.imread(image_files[i])
239
+ out.write(frame)
240
+ out.release()
241
+
242
+ if audio_path is not None:
243
+ audio = AudioFileClip(audio_path.name)
244
+ video = VideoFileClip(output)
245
+ audio = audio.subclip(0, video.end)
246
+ video = video.set_audio(audio)
247
+ video_write_kwargs = {"audio_codec": "aac"}
248
+ output_audio = os.path.splitext(output)[0] + "_audio.mp4"
249
+ video.write_videofile(output_audio, **video_write_kwargs)
250
+ output = output_audio
251
 
252
  # Remove tmp dir
253
  if not keep_frames and not skip_video_generation:
 
270
  gr.inputs.Dropdown(label="Zoom direction. Inout and outin combine both directions",
271
  choices=["in", "out", "inout", "outin"], default="out"),
272
  gr.inputs.Slider(label="Frames per second of the output video", minimum=1, maximum=60, step=1, default=30),
 
 
273
  gr.inputs.Checkbox(label="Reverse images", default=False)
274
  ]
275
 
 
284
  allow_embedding=True,
285
  allow_download=True)
286
 
287
+ iface.queue(concurrency_count=10).launch()