from os.path import join, dirname
import numpy as np
from .text import put_text
from .. import const
from ..os import makedirs
from ..imprt import preset_import
from ..log import get_logger
logger = get_logger()
[docs]def make_video(
imgs, fps=24, outpath=None, method='matplotlib', dpi=96, bitrate=-1):
"""Writes a list of images into a grayscale or color video.
Args:
imgs (list(numpy.ndarray)): Each image should be of type ``uint8`` or
``uint16`` and of shape H-by-W (grayscale) or H-by-W-by-3 (RGB).
fps (int, optional): Frame rate.
outpath (str, optional): Where to write the video to (a .mp4 file).
``None`` means
``os.path.join(const.Dir.tmp, 'make_video.mp4')``.
method (str, optional): Method to use: ``'matplotlib'``, ``'opencv'``,
``'video_api'``.
dpi (int, optional): Dots per inch when using ``matplotlib``.
bitrate (int, optional): Bit rate in kilobits per second when using
``matplotlib``; reasonable values include 7200.
Writes
- A video of the images.
"""
if outpath is None:
outpath = join(const.Dir.tmp, 'make_video.mp4')
makedirs(dirname(outpath))
assert imgs, "Frame list is empty"
for frame in imgs:
assert np.issubdtype(frame.dtype, np.unsignedinteger), \
"Image type must be unsigned integer"
h, w = imgs[0].shape[:2]
for frame in imgs[1:]:
assert frame.shape[:2] == (h, w), \
"All frames must have the same shape"
if method == 'matplotlib':
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib import animation
w_in, h_in = w / dpi, h / dpi
fig = plt.figure(figsize=(w_in, h_in))
Writer = animation.writers['ffmpeg'] # may require you to specify path
writer = Writer(fps=fps, bitrate=bitrate)
def img_plt(arr):
img_plt_ = plt.imshow(arr)
ax = plt.gca()
ax.set_position([0, 0, 1, 1])
ax.set_axis_off()
return img_plt_
anim = animation.ArtistAnimation(fig, [(img_plt(x),) for x in imgs])
anim.save(outpath, writer=writer)
# If obscure error like "ValueError: Invalid file object: <_io.Buff..."
# occurs, consider upgrading matplotlib so that it prints out the real,
# underlying ffmpeg error
plt.close('all')
elif method == 'opencv':
cv2 = preset_import('cv2', assert_success=True)
# TODO: debug codecs (see http://www.fourcc.org/codecs.php)
if outpath.endswith('.mp4'):
# fourcc = cv2.VideoWriter_fourcc(*'MJPG')
# fourcc = cv2.VideoWriter_fourcc(*'X264')
fourcc = cv2.VideoWriter_fourcc(*'H264')
# fourcc = 0x00000021
elif outpath.endswith('.avi'):
fourcc = cv2.VideoWriter_fourcc(*'XVID')
else:
raise NotImplementedError("Video type of\n\t%s" % outpath)
vw = cv2.VideoWriter(outpath, fourcc, fps, (w, h))
for frame in imgs:
if frame.ndim == 3:
frame = frame[:, :, ::-1] # cv2 uses BGR
vw.write(frame)
vw.release()
elif method == 'video_api':
video_api = preset_import('video_api', assert_success=True)
assert outpath.endswith('.webm'), "`video_api` requires .webm"
with video_api.write(outpath, fps=fps) as h:
for frame in imgs:
if frame.ndim == 3 and frame.shape[2] == 4:
frame = frame[:, :, :3]
#frame = frame.astype(np.ubyte)
h.add_frame(frame)
else:
raise ValueError(method)
logger.debug("Images written as a video to:\n%s", outpath)
[docs]def make_comparison_video(
imgs1, imgs2, bar_width=4, bar_color=(1, 0, 0), sweep_vertically=False,
sweeps=1, label1='', label2='', font_size=None, font_ttf=None,
label1_top_left_xy=None, label2_top_left_xy=None, **make_video_kwargs):
"""Writes two lists of images into a comparison video that toggles between
two videos with a sweeping bar.
Args:
imgs? (list(numpy.ndarray)): Each image should be of type ``uint8`` or
``uint16`` and of shape H-by-W (grayscale) or H-by-W-by-3 (RGB).
bar_width (int, optional): Width of the sweeping bar.
bar_color (tuple(float), optional): Bar and label RGB, normalized to
:math:`[0,1]`. Defaults to red.
sweep_vertically (bool, optional): Whether to sweep vertically or
horizontally.
sweeps (int, optional): Number of sweeps.
label? (str, optional): Label for each video.
font_size (int, optional): Font size.
font_ttf (str, optional): Path to the .ttf font file. Defaults to Arial.
label?_top_left_xy (tuple(int), optional): The XY coordinate of the
label's top left corner.
make_video_kwargs (dict, optional): Keyword arguments for
:func:`make_video`.
Writes
- A comparison video.
"""
# Bar is perpendicular to sweep-along
sweep_along = 0 if sweep_vertically else 1
bar_along = 1 if sweep_vertically else 0
# Number of frames
n_frames = len(imgs1)
assert n_frames == len(imgs2), \
"Videos to be compared have different numbers of frames"
img_shape = imgs1[0].shape
# Bar color according to image dtype
img_dtype = imgs1[0].dtype
bar_color = np.array(bar_color, dtype=img_dtype)
if np.issubdtype(img_dtype, np.integer):
bar_color *= np.iinfo(img_dtype).max
# Map from frame index to bar location, considering possibly multiple trips
bar_locs = []
for i in range(sweeps):
ind = np.arange(0, img_shape[sweep_along])
if i % 2 == 1: # reverse every other trip
ind = ind[::-1]
bar_locs.append(ind)
bar_locs = np.hstack(bar_locs) # all possible locations
ind = np.linspace(0, len(bar_locs) - 1, num=n_frames, endpoint=True)
bar_locs = [bar_locs[int(x)] for x in ind] # uniformly sampled
# Label locations
if label1_top_left_xy is None:
# Label 1 at top left corner
label1_top_left_xy = (int(0.1 * img_shape[1]), int(0.05 * img_shape[0]))
if label2_top_left_xy is None:
if sweep_vertically:
# Label 2 at bottom left corner
label2_top_left_xy = (
int(0.1 * img_shape[1]), int(0.75 * img_shape[0]))
else:
# Label 2 at top right corner
label2_top_left_xy = (
int(0.7 * img_shape[1]), int(0.05 * img_shape[0]))
frames = []
for i, (img1, img2) in enumerate(zip(imgs1, imgs2)):
assert img1.shape == img_shape, f"`imgs1[{i}]` has a differnet shape"
assert img2.shape == img_shape, f"`imgs2[{i}]` has a differnet shape"
assert img1.dtype == img_dtype, f"`imgs1[{i}]` has a differnet dtype"
assert img2.dtype == img_dtype, f"`imgs2[{i}]` has a differnet dtype"
# Label the two images
img1 = put_text(
img1, label1, label_top_left_xy=label1_top_left_xy,
font_size=font_size, font_color=bar_color, font_ttf=font_ttf)
img2 = put_text(
img2, label2, label_top_left_xy=label2_top_left_xy,
font_size=font_size, font_color=bar_color, font_ttf=font_ttf)
# Bar start and end
bar_loc = bar_locs[i]
bar_width_half = bar_width // 2
bar_start = max(0, bar_loc - bar_width_half)
bar_end = min(bar_loc + bar_width_half, img_shape[sweep_along])
# Up to bar start, we show Image 1; bar end onwards, Image 2
img1 = np.take(img1, range(bar_start), axis=sweep_along)
img2 = np.take(
img2, range(bar_end, img_shape[sweep_along]), axis=sweep_along)
# Between the two images, we show the bar
actual_bar_width = img_shape[
sweep_along] - img1.shape[sweep_along] - img2.shape[sweep_along]
reps = [1, 1, 1]
reps[sweep_along] = actual_bar_width
reps[bar_along] = img_shape[bar_along]
bar_img = np.tile(bar_color, reps)
frame = np.concatenate((img1, bar_img, img2), axis=sweep_along)
frames.append(frame)
make_video(frames, **make_video_kwargs)