diff --git a/docs/updating.md b/docs/updating.md index a59aa1ac7..38d619df7 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -1625,7 +1625,7 @@ pages for more details on the input and output variable types. * pre v4.0: NA * post v4.0: frame_size = **pcv.visualize.time_lapse_video**(*img_list, out_filename='./time_lapse_video.mp4', fps=29.97, display=True*) -* post v5.0: deprecated. +* post v5.0: frame_size = **pcv.visualize.time_lapse_video**(*source, out_filename='./time_lapse_video.mp4', fps=29.97*) #### plantcv.watershed_segmentation diff --git a/docs/visualize_time_lapse_video.md b/docs/visualize_time_lapse_video.md new file mode 100644 index 000000000..b4857440e --- /dev/null +++ b/docs/visualize_time_lapse_video.md @@ -0,0 +1,36 @@ +## Automatically Generate a Time-Lapse Video given A Directory of Images + +This function generates and saves the time-lapse video based on a list of paths to the images. + +**plantcv.visualize.time_lapse_video**(*source, out_filename='./time_lapse_video.mp4', fps=29.97*) + +**returns** frame_size + +- **Parameters:** + - source - List of paths to the images to create the video or file path to a directory of images to use. + - out_filename - Name of file to save the generated video to. + - fps - Frame rate (frames per second) By default fps=29.97. Commonly used values: 23.98, 24, 25, 29.97, 30, 50, 59.94, 60 + +- **Context:** + - Used to generate time-lapse video given a list of images. + +- **Example Use:** + - Below + + +```python +from plantcv import plantcv as pcv +# Note you will have to change this part on your own path +img_directory = './path_to_images_directory/' + +frame_size = pcv.visualize.time_lapse_video(source=img_directory, + out_filename='./eg_time_lapse.mp4') +``` + +**Video generated** + +The generated video should look similar to the one below: + + + +**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/master/plantcv/plantcv/visualize/time_lapse_video.py) diff --git a/environment.yml b/environment.yml index 263bc40b6..816c90f4f 100644 --- a/environment.yml +++ b/environment.yml @@ -29,6 +29,8 @@ dependencies: - flyr - nbconvert - nbformat + - imageio >= 2.28 + - imageio-ffmpeg channels: - conda-forge diff --git a/mkdocs.yml b/mkdocs.yml index 0bd65f4cc..2155f6122 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -225,6 +225,7 @@ nav: - 'Pixel Scatter Plot': 'visualize_pixel_scatter_vis.md' - 'Pseudocolor': visualize_pseudocolor.md - 'Tile': visualize_tile.md + - 'Time Lapse Video': visualize_time_lapse_video.md - 'Watershed Segmentation': watershed.md - 'White balance': white_balance.md - 'Within Frame': within_frame.md diff --git a/plantcv/plantcv/visualize/__init__.py b/plantcv/plantcv/visualize/__init__.py index bb5b3fa31..3ee60b7eb 100644 --- a/plantcv/plantcv/visualize/__init__.py +++ b/plantcv/plantcv/visualize/__init__.py @@ -11,7 +11,8 @@ from plantcv.plantcv.visualize.pixel_scatter_vis import pixel_scatter_plot from plantcv.plantcv.visualize.chlorophyll_fluorescence import chlorophyll_fluorescence from plantcv.plantcv.visualize.tile import tile +from plantcv.plantcv.visualize.time_lapse_video import time_lapse_video __all__ = ["pseudocolor", "colorize_masks", "histogram", "colorspaces", "auto_threshold_methods", "overlay_two_imgs", "colorize_label_img", "obj_size_ecdf", "obj_sizes", "hyper_histogram", - "pixel_scatter_plot", "chlorophyll_fluorescence", "tile"] + "pixel_scatter_plot", "chlorophyll_fluorescence", "tile", "time_lapse_video"] diff --git a/plantcv/plantcv/visualize/time_lapse_video.py b/plantcv/plantcv/visualize/time_lapse_video.py new file mode 100644 index 000000000..5a390cb91 --- /dev/null +++ b/plantcv/plantcv/visualize/time_lapse_video.py @@ -0,0 +1,58 @@ +# Create time-lapse videos with input directory of images + +import os +import imageio.v3 as iio +import numpy as np +from plantcv.plantcv._globals import params +from plantcv.plantcv.transform.resize import resize +from plantcv.plantcv.io.read_dataset import read_dataset +from plantcv.plantcv.warn import warn + + +def time_lapse_video(source, out_filename='./time_lapse_video.mp4', fps=29.97): + """Generate time-lapse video given a list of paths to the images + + Parameters: + ----------- + source = string or list, + Path to a folder of images to use or list of paths to the images to create the video + out_filename = string, + name of file to save the generated video to + fps = float, + frame rate (frames per second) + + Returns: + -------- + frame_size = tuple, + the frame size of the generated video + """ + params.debug = None + img_list = source + if isinstance(source, str): + img_list = read_dataset(source, sort=True) + + imgs = [] + list_r = [] + list_c = [] + for file in img_list: + img = iio.imread(file) + list_r.append(img.shape[0]) + list_c.append(img.shape[1]) + imgs.append(img) + max_c, max_r = np.max(list_c), np.max(list_r) + + # use the largest size of the images as the frame size + # frame_size = frame_size or (max_c, max_r) + frame_size = (max_c, max_r) + + if not (len(np.unique(list_r)) == 1 and len(np.unique(list_c)) == 1): + warn("The sizes of images are not the same, an image resizing (cropping or zero-padding) will be done " + f"to make all images the same size ({frame_size[0]}x{frame_size[1]}) before creating the video! ") + + out_path, _ = os.path.splitext(out_filename) + out_filename = out_path + '.mp4' + + frames = [resize(img, frame_size, interpolation=None) for img in imgs] + iio.imwrite(out_filename, frames, plugin="FFMPEG", fps=fps, codec="libx264") + + return frame_size diff --git a/pyproject.toml b/pyproject.toml index 53f20b2dc..03ba275fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,9 @@ dependencies = [ "nd2", "flyr", "nbconvert", - "nbformat" + "nbformat", + "imageio >= 2.28", + "imageio-ffmpeg" ] requires-python = ">=3.8" authors = [ diff --git a/tests/plantcv/visualize/test_time_lapse_video.py b/tests/plantcv/visualize/test_time_lapse_video.py new file mode 100644 index 000000000..3d39357e7 --- /dev/null +++ b/tests/plantcv/visualize/test_time_lapse_video.py @@ -0,0 +1,56 @@ +import os +import cv2 +import numpy as np +from plantcv.plantcv.visualize.time_lapse_video import time_lapse_video + + +def test_plantcv_visualize_time_lapse_video_list_input(tmpdir): + """Test for PlantCV.""" + # Generate 3 test images and saved in tmpdir + list_im = [] + for i in range(3): + temp_img = np.random.rand(3, 3) + min_, max_ = np.nanmin(temp_img), np.nanmax(temp_img) + temp_img = np.interp(temp_img, (min_, max_), (0, 255)).astype('uint8') + img_i_path = os.path.join(tmpdir, f"img{i}.png") + cv2.imwrite(img_i_path, temp_img) + list_im.append(img_i_path) + + vid_name = os.path.join(tmpdir, 'test_time_lapse_video.mp4') + _ = time_lapse_video(source=list_im, out_filename=vid_name, fps=29.97) + assert os.path.exists(vid_name) and os.path.getsize(vid_name) > 100 + + +def test_plantcv_visualize_time_lapse_video_str_input(tmpdir): + """Test for PlantCV.""" + # Generate 3 test images and saved in tmpdir + for i in range(3): + temp_img = np.random.rand(3, 3) + min_, max_ = np.nanmin(temp_img), np.nanmax(temp_img) + temp_img = np.interp(temp_img, (min_, max_), (0, 255)).astype('uint8') + img_i_path = os.path.join(tmpdir, f"img{i}.png") + cv2.imwrite(img_i_path, temp_img) + + vid_name = os.path.join(tmpdir, 'test_time_lapse_video.mp4') + _ = time_lapse_video(source=str(tmpdir), out_filename=vid_name, fps=29.97) + assert os.path.exists(vid_name) and os.path.getsize(vid_name) > 100 + + +# not all images have the same size (essential to generate a video) +def test_plantcv_visualize_time_lapse_video_different_img_sizes_warns(tmpdir, capsys): + """Test for PlantCV.""" + # Generate 3 test images of different size and save in tmpdir + list_im = [] + for i in range(2): + temp_img = np.random.rand(i+2, 3) + min_, max_ = np.nanmin(temp_img), np.nanmax(temp_img) + temp_img = np.interp(temp_img, (min_, max_), (0, 255)).astype('uint8') + img_i_path = os.path.join(tmpdir, f"img{i}.png") + cv2.imwrite(img_i_path, temp_img) + list_im.append(img_i_path) + + vid_name = os.path.join(tmpdir, 'test_time_lapse_video.mp4') + _ = time_lapse_video(source=list_im, out_filename=vid_name, fps=29.97) + _, err = capsys.readouterr() + + assert "Warning" in err and os.path.exists(vid_name)