Spaces:
Sleeping
Sleeping
Update app.py
Browse filesReplace PIL with CV2
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
|
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
|
67 |
-
width,
|
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 |
-
|
|
|
|
|
76 |
|
77 |
|
78 |
-
def resize_scale(image, scale
|
79 |
-
|
80 |
-
return
|
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 |
-
|
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 |
-
|
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,
|
154 |
-
height = get_px_or_fraction(height,
|
155 |
-
margin = get_px_or_fraction(margin, min(
|
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 |
-
|
164 |
|
165 |
if reverse_images:
|
166 |
-
|
167 |
|
168 |
# Blend images (take care of margins)
|
169 |
-
progress(0, desc=f"Blending {len(
|
170 |
for i in progress.tqdm(range(1, num_images + 1), desc="Blending images"):
|
171 |
-
inner_image =
|
172 |
-
outer_image =
|
173 |
-
inner_image = inner_image
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
image =
|
178 |
-
image
|
179 |
-
|
180 |
-
|
181 |
-
|
|
|
|
|
|
|
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
|
186 |
-
|
187 |
-
image.
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
images_resized[i] = image
|
195 |
|
196 |
-
|
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
|
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 |
-
|
230 |
else:
|
231 |
-
|
232 |
-
|
233 |
|
234 |
-
|
235 |
frame_path = os.path.join(tmp_dir_hash, f"{i:06d}.png")
|
236 |
-
|
237 |
|
238 |
progress(0, desc=f"Creating {num_frames} frames")
|
239 |
-
|
240 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|