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()
|