diff --git a/README.md b/README.md index b2116d05d..f972cda40 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Next, see our ["Hello World" example](docs/HelloWorld.md) to generate an image & - [Installation Guide](docs/Installation.md) - ["Hello World": Generate your first Infinigen scene](docs/HelloWorld.md) - [Configuring Infinigen](docs/ConfiguringInfinigen.md) +- [Downloading pre-generated data](docs/PreGeneratedData.md) - [Extended ground-truth](docs/GroundTruthAnnotations.md) - [Generating individual assets](docs/GeneratingIndividualAssets.md) - [Implementing new materials & assets](docs/ImplementingAssets.md) diff --git a/docs/PreGeneratedData.md b/docs/PreGeneratedData.md new file mode 100644 index 000000000..2857b5cfa --- /dev/null +++ b/docs/PreGeneratedData.md @@ -0,0 +1,40 @@ +# Downloading and using Pre-Generated Data + +## Downloading pre-generated data + +All pre-generated data released by the Princeton Vision and Learning lab is hosted here: +[https://infinigen-data.cs.princeton.edu/](https://infinigen-data.cs.princeton.edu/) + +You can download these yourself, or use our rudimentary download/untar script as shown in the examples below. To minimize traffic, please use the --cameras, --seeds and --data_types arguments to download only the data you are interested in. + +``` +# See all available options (recommended): +python -m tools.download_pregenerated_data --help + +# Download a few images with geometry ground truth visualization pngs to inspect locally: +python -m tools.download_pregenerated_data --output_folder outputs/my_download --repo_url https://infinigen-data.cs.princeton.edu/ --release_name 2023_10_13_preview --seeds 4bbdd3e0 2d2c1104 --cameras camera_0 --data_types Image_png Depth_png Flow3D_png SurfaceNormal_png OcclusionBoundaries_png + +# Download only the data needed monocular depth (modify as needed for Flow3D, ObjectSegmentation etc): +python -m tools.download_pregenerated_data --output_folder outputs/my_download --repo_url https://infinigen-data.cs.princeton.edu/ --release_name 2023_10_13_preview --cameras camera_0 --data_types Image_png Depth_npy + +# Download everything available in a particular datarelease +python -m tools.download_pregenerated_data --output_folder outputs/my_download --repo_url https://infinigen-data.cs.princeton.edu/ --release_name 2023_10_13_preview +``` + +## Using Infinigen data with a Pytorch-style dataset class + +We provide an example pytorch-style dataset class ([dataset_loader.py](../worldgen/tools)) to help load data in our format. + +Assuming you ran the "Download only the data needed monocular depth" example command above, you should be able to use the following example by running `python` from the `worldgen/` folder: + +```python +from tools.dataset_loader import get_infinigen_dataset +dataset = get_infinigen_dataset("outputs/my_download", data_types=["Image_png", "Depth_npy"]) +print(len(dataset)) +print(dataset[0].keys()) +``` + +Note: dataset_loader.py is designed to be separable from the main infinigen codebase; you can copy/move this file into your own codebase, but you must also copy it's dependency `suffixes.py`, or copy `suffixes.py`'s contents into `dataset_loader.py`. + +## Ground Truth +Please see [GroundTruthAnnotations.md](./GroundTruthAnnotations.md) for documentation on the various available ground truth, and examples of how they can be used once loaded. \ No newline at end of file diff --git a/worldgen/tools/datarelease_toolkit.py b/worldgen/tools/datarelease_toolkit.py index f906a117b..824be2a3a 100644 --- a/worldgen/tools/datarelease_toolkit.py +++ b/worldgen/tools/datarelease_toolkit.py @@ -1,3 +1,8 @@ +# Copyright (c) Princeton University. +# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. + +# Authors: Alexander Raistrick + import os from pathlib import Path import argparse @@ -22,7 +27,7 @@ from . import dataset_loader from . import compress_masks -TOOLKIT_VERSION = '0.1.1' +TOOLKIT_VERSION = '0.2.0' IMAGE_RESIZE_ACTIONS = { diff --git a/worldgen/tools/dataset_loader.py b/worldgen/tools/dataset_loader.py index cc2fbb758..984ca43c1 100644 --- a/worldgen/tools/dataset_loader.py +++ b/worldgen/tools/dataset_loader.py @@ -14,46 +14,46 @@ # Read docs/GroundTruthAnnotations.md for more explanations - ('Image', '.png'), + 'Image_png', #('Image', '.exr'), # NOT IMPLEMENTED - ('camview', '.npz'), # contains intrinsic extrinsic etc + 'camview_npz', # intrinisic, extrinsic, etc # names available via EITHER blender_gt.gin and opengl_gt.gin - ('Depth', '.npy'), - ('Depth', '.png'), - ('InstanceSegmentation', '.npz'), - ('InstanceSegmentation', '.png'), - ('ObjectSegmentation', '.npz'), - ('ObjectSegmentation', '.png'), - ('SurfaceNormal', '.npy'), - ('SurfaceNormal', '.png'), - ('Objects', '.json'), + 'Depth_npy', + 'Depth_png', + 'InstanceSegmentation_npz', + 'InstanceSegmentation_png', + 'ObjectSegmentation_npz', + 'ObjectSegmentation_png', + 'SurfaceNormal_npy', + 'SurfaceNormal_png', + 'Objects_json', # blender_gt.gin only provides 2D flow. opengl_gt.gin produces Flow3D instead - ('Flow3D', '.npy'), - ('Flow3D', '.png'), + 'Flow3D_npy', + 'Flow3D_png', # names available ONLY from opengl_gt.gin - ('OcclusionBoundaries', '.png'), - ('TagSegmentation', '.npz'), - ('TagSegmentation', '.png'), - ('Flow3DMask', '.png'), + 'OcclusionBoundaries_png', + 'TagSegmentation_npz', + 'TagSegmentation_png', + 'Flow3DMask_png', # info from blender image rendering passes, usually enabled regardless of GT method - ('AO', '.png'), - ('DiffCol', '.png'), - ('DiffDir', '.png'), - ('DiffInd', '.png'), - ('Emit', '.png'), - ('Env', '.png'), - ('GlossCol', '.png'), - ('GlossDir', '.png'), - ('GlossInd', '.png'), - ('TransCol', '.png'), - ('TransDir', '.png'), - ('TransInd', '.png'), - ('VolumeDir', '.png'), + 'AO_png', + 'DiffCol_png', + 'DiffDir_png', + 'DiffInd_png', + 'Emit_png', + 'Env_png', + 'GlossCol_png', + 'GlossDir_png', + 'GlossInd_png', + 'TransCol_png', + 'TransDir_png', + 'TransInd_png', + 'VolumeDir_png', } def get_blocksize(scene_folder): @@ -68,49 +68,46 @@ def get_framebounds_inclusive(scene_folder): parse_suffix(last)['frame'] ) -def get_subcams_available(scene_folder): - rgb = scene_folder/'frames'/'Image' - return [int(p.name.split('_')[-1]) for p in rgb.iterdir()] +def get_cameras_available(scene_folder): + return [int(p.name.split('_')[-1]) for p in (scene_folder/'frames'/'Image').iterdir()] def get_imagetypes_available(scene_folder): dtypes = [] for dtype_folder in (scene_folder/'frames').iterdir(): frames = dtype_folder/'camera_0' uniq = set(p.suffix for p in frames.iterdir()) - dtypes += [(dtype_folder.name, u) for u in uniq] + dtypes += [f'{dtype_folder.name}_{u.strip(".")}' for u in uniq] return dtypes -def get_frame_path(scene_folder, cam_idx, frame_idx, data_type_name, data_type_ext) -> Path: - imgname = f'{data_type_name}_0_0_{frame_idx:04d}_{cam_idx}{data_type_ext}' - return Path(scene_folder)/'frames'/data_type_name/f'camera_{cam_idx}'/imgname +def get_frame_path(scene_folder, cam: int, frame_idx, data_type) -> Path: + data_type_name, data_type_ext = data_type.split('_') + imgname = f'{data_type_name}_0_0_{frame_idx:04d}_{cam}.{data_type_ext}' + return Path(scene_folder)/'frames'/data_type_name/f'camera_{cam}'/imgname class InfinigenSceneDataset: def __init__( self, scene_folder: Path, - image_types: list[str] = None, # see ALLOWED_IMAGE_KEYS above. Use 'None' to retrieve all available PNG datatypes - - # [0] for monocular, [0, 1] for stereo, 'None' to use whatever is present in the dataset - subcam_keys=None, + data_types: list[str] = None, # see ALLOWED_IMAGE_KEYS above. Use 'None' to retrieve all available PNG datatypes + cameras=None, gt_for_first_camera_only=True, ): self.scene_folder = Path(scene_folder) self.gt_for_first_camera_only = gt_for_first_camera_only - if image_types is None: - image_types = get_imagetypes_available(self.scene_folder) - image_types = [v for v in image_types if v[1] != '.exr'] # loading not implemented yet - logging.info(f'{self.__class__.__name__} recieved image_types=None, using whats available in {scene_folder}: {image_types}') - for t in image_types: + if data_types is None: + data_types = get_imagetypes_available(self.scene_folder) + logging.info(f'{self.__class__.__name__} recieved data_types=None, using whats available in {scene_folder}: {data_types}') + for t in data_types: if t not in ALLOWED_IMAGE_TYPES: - raise ValueError(f'Recieved image_types containing {t} which is not in ALLOWED_IMAGE_TYPES') - self.image_types = image_types + raise ValueError(f'Recieved data_types containing {t} which is not in ALLOWED_IMAGE_TYPES') + self.data_types = data_types - if subcam_keys is None: - subcam_keys = get_subcams_available(self.scene_folder) - self.subcam_keys = subcam_keys + if cameras is None: + cameras = get_cameras_available(self.scene_folder) + self.cameras = cameras self.framebounds_inclusive = get_framebounds_inclusive(self.scene_folder) @@ -136,46 +133,46 @@ def load_any_filetype(path): case _: raise ValueError(f'Unhandled {path.suffix=} for {path=}') - def _imagetypes_to_load(self, subcam): - for image_type in self.image_types: - dtypename = image_type[0] + def _imagetypes_to_load(self, cam: int): + for data_type in self.data_types: + dtypename = data_type[0] if ( self.gt_for_first_camera_only and - subcam != 0 and + camera != 0 and dtypename != 'Image' and dtypename != 'camview' ): continue - yield image_type + yield data_type def validate(self): for i in range(len(self)): - for j in self.subcam_keys: - for k in self._imagetypes_to_load(j): + for cam in self.cameras: + for dtype in self._imagetypes_to_load(cam): frame = self.framebounds_inclusive[0] - p = self.frame_path(frame + i, j, k) + p = self.frame_path(frame + i, cam, dtype) if not p.exists(): raise ValueError(f'validate() failed for {self.scene_folder}, could not find {p}') - def frame_path(self, i, subcam, dtype): + def frame_path(self, i: int, cam: int, dtype: str): frame_num = self.framebounds_inclusive[0] + i - return get_frame_path(self.scene_folder, subcam, frame_num, dtype[0], dtype[1]) + return get_frame_path(self.scene_folder, cam, frame_num, dtype) def __getitem__(self, i): - def get_camera_images(subcam): + def get_camera_images(cam: int): imgs = {} - for dtype in self._imagetypes_to_load(subcam): - path = self.frame_path(subcam, i, subcam, dtype) - imgs[dtype[0]] = self.load_any_filetype(path) + for dtype in self._imagetypes_to_load(cam): + path = self.frame_path(i, cam, dtype) + imgs[dtype] = self.load_any_filetype(path) return imgs - per_camera_data = [get_camera_images(i) for i in self.subcam_keys] - - if len(per_camera_data) == 1: - per_camera_data = per_camera_data[0] + per_camera_data = [get_camera_images(i) for i in self.cameras] - return per_camera_data + if len(self.cameras) == 1: + return per_camera_data[0] + else: + return per_camera_data def get_infinigen_dataset(data_folder: Path, mode='concat', validate=False, **kwargs): diff --git a/worldgen/tools/download_pregenerated_data.py b/worldgen/tools/download_pregenerated_data.py new file mode 100644 index 000000000..763e0e29e --- /dev/null +++ b/worldgen/tools/download_pregenerated_data.py @@ -0,0 +1,143 @@ +# Copyright (c) Princeton University. +# This source code is licensed under the BSD 3-Clause license found in the LICENSE file in the root directory of this source tree. + +# Authors: Alexander Raistrick + +import argparse +from pathlib import Path +import urllib.request +import re +import subprocess +import json +from multiprocessing import Pool +from functools import partial + +SUPPORTED_DATARELEASE_FORMATS = {"0.2.0"} + +def wget_path(args, path): + url = args.repo_url + str(path) + cmd = f'wget -q -N --show-progress {url} -P {str(args.output_folder)}' + subprocess.check_call(cmd.split()) + +def untar_path(args, tarfile): + assert tarfile.exists() + cmd = f'tar -xzf {tarfile} -C {args.output_folder}' + print(cmd) + subprocess.check_call(cmd.split()) + tarfile.unlink() + +def url_to_text(url): + with urllib.request.urlopen(url) as f: + return f.read().decode('utf-8') + +def check_and_preprocess_args(args, metadata): + + datarelease_format_version = metadata['datarelease_format_version'] + if datarelease_format_version not in SUPPORTED_DATARELEASE_FORMATS: + raise ValueError( + f'{args.release_name} uses {datarelease_format_version=} which is not ' + ' supported by this download script. Please download a newer version of the code.' + ) + + if args.seeds is None: + args.seeds = metadata['seeds'] + print(f'User did not specify --seeds, using all available ({len(args.seeds)} total):\n\t{args.seeds}') + else: + missing = set(args.seeds) - set(metadata['seeds']) + if len(missing): + raise ValueError(f"In user-provided --seeds, {missing} could not be found in {args.release_name} metadata.json") + + if args.cameras is None: + args.cameras = metadata['cameras'] + print(f'User did not specify --cameras, using all available ({len(args.cameras)} total):\n\t{args.cameras}') + else: + missing = set(args.cameras) - set(metadata['cameras']) + if len(missing): + raise ValueError(f"In user-provided --cameras, {missing} are not supported acording {args.release_name} metadata.json") + + if args.data_types is None: + args.data_types = metadata['data_types'] + print(f'User did not specify --data_types, using all available ({len(args.data_types)} total): \n\t{args.data_types}') + else: + missing = set(args.data_types) - set(metadata['data_types']) + if len(missing): + raise ValueError(f"In user-provided --seeds, {missing} could not be found in {args.release_name} metadata.json") + +def process_path(args, path): + wget_path(args, path) + untar_path(args, tarfile=args.output_folder/path.name) + +def main(args): + + metadata_url = f'{args.repo_url}/{args.release_name}/metadata.json' + metadata = json.loads(url_to_text(metadata_url)) + + print("=" * 10) + print(f"Description for release {repr(args.release_name)}:") + print("-" * 10) + print(metadata['description']) + print("=" * 10) + + check_and_preprocess_args(args, metadata) + + toplevel = Path(args.release_name)/'renders' + + paths = [] + for seed in args.seeds: + for camera in args.cameras: + for imgtype in args.data_types: + name = f'{seed}_{imgtype}_{camera}.tar.gz' + paths.append(toplevel/seed/name) + + print(f'User requested {len(args.seeds)} seeds x {len(args.cameras)} cameras x {len(args.data_types)} data types') + print(f'This script will download and untar {len(paths)} tarballs from {args.repo_url}') + if input('Do you wish to proceed? [y/n]: ') != 'y': + exit() + + with Pool(args.n_workers) as pool: + pool.map(partial(process_path, args), paths) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--output_folder', type=Path) + parser.add_argument( + "--repo_url", + type=str, + help="Fileserver URL to download from", + ) + parser.add_argument( + "--release_name", + type=str, + help="What named datarelease do you want to download? (pick any toplevel folder name from the URL)", + ) + parser.add_argument( + "--seeds", + type=str, + nargs="+", + default=None, + help="What scenes should we download? Omit to download all available in this release", + ) + parser.add_argument( + "--cameras", + type=str, + nargs='+', + default=None, + help="What cameras should we download data for? Omit to download all available in this release", + ) + parser.add_argument( + "--data_types", + type=str, + nargs='+', + default=None, + help="What data types (e.g Image, Depth, etc) should we download data for? Omit to download all available in this release", + ) + parser.add_argument( + '--n_workers', + type=int, + default=1 + ) + + args = parser.parse_args() + main(args) + diff --git a/worldgen/tools/ground_truth/bounding_boxes_3d.py b/worldgen/tools/ground_truth/bounding_boxes_3d.py index a23bf737f..84ce3388b 100644 --- a/worldgen/tools/ground_truth/bounding_boxes_3d.py +++ b/worldgen/tools/ground_truth/bounding_boxes_3d.py @@ -72,11 +72,11 @@ def arr2color(e): parser.add_argument('--output', type=Path, default=Path("testbed")) args = parser.parse_args() - object_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'ObjectSegmentation', '.npz'))) - instance_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'InstanceSegmentation', '.npz'))) - image = imread(get_frame_path(args.folder, 0, args.frame, "Image", ".png")) - object_json = json.loads(get_frame_path(args.folder, 0, args.frame, 'Objects', '.json').read_text()) - camview = np.load(get_frame_path(args.folder, 0, args.frame, 'camview', '.npz')) + object_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'ObjectSegmentation_npz'))) + instance_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'InstanceSegmentation_npz'))) + image = imread(get_frame_path(args.folder, 0, args.frame, "Image_png")) + object_json = json.loads(get_frame_path(args.folder, 0, args.frame, 'Objects_json').read_text()) + camview = np.load(get_frame_path(args.folder, 0, args.frame, 'camview_npz')) # Identify objects visible in the image unique_object_idxs = set(np.unique(object_segmentation_mask)) diff --git a/worldgen/tools/ground_truth/depth_to_normals.py b/worldgen/tools/ground_truth/depth_to_normals.py index 03de8b50b..f0089a2c5 100644 --- a/worldgen/tools/ground_truth/depth_to_normals.py +++ b/worldgen/tools/ground_truth/depth_to_normals.py @@ -42,10 +42,10 @@ def normalize(v): parser.add_argument('--output', type=Path, default=Path("testbed")) args = parser.parse_args() - depth_path = get_frame_path(args.folder, 0, args.frame, 'Depth', '.npy') - normal_path = get_frame_path(args.folder, 0, args.frame, 'SurfaceNormal', '.png') - image_path = get_frame_path(args.folder, 0, args.frame, 'Image', '.png') - camview_path = get_frame_path(args.folder, 0, args.frame, 'camview', '.npz') + depth_path = get_frame_path(args.folder, 0, args.frame, 'Depth_npy') + normal_path = get_frame_path(args.folder, 0, args.frame, 'SurfaceNormal_png') + image_path = get_frame_path(args.folder, 0, args.frame, 'Image_png') + camview_path = get_frame_path(args.folder, 0, args.frame, 'camview_npz') assert depth_path.exists() assert image_path.exists() assert camview_path.exists() diff --git a/worldgen/tools/ground_truth/optical_flow_warp.py b/worldgen/tools/ground_truth/optical_flow_warp.py index 5ddf48ac0..534ec3fb3 100644 --- a/worldgen/tools/ground_truth/optical_flow_warp.py +++ b/worldgen/tools/ground_truth/optical_flow_warp.py @@ -28,10 +28,10 @@ parser.add_argument('frame', type=int) parser.add_argument('--output', type=Path, default=Path("testbed")) args = parser.parse_args() - flow3d_path = get_frame_path(args.folder, 0, args.frame, "Flow3D", ".npy") - image1_path = get_frame_path(args.folder, 0, args.frame, "Image", ".png") - image2_path = get_frame_path(args.folder, 0, args.frame+1, "Image", ".png") - camview_path = get_frame_path(args.folder, 0, args.frame, 'camview', '.npz') + flow3d_path = get_frame_path(args.folder, 0, args.frame, "Flow3D_npy") + image1_path = get_frame_path(args.folder, 0, args.frame, "Image_png") + image2_path = get_frame_path(args.folder, 0, args.frame+1, "Image_png") + camview_path = get_frame_path(args.folder, 0, args.frame, 'camview_npz') assert flow3d_path.exists() assert image1_path.exists() assert image2_path.exists() diff --git a/worldgen/tools/ground_truth/rigid_warp.py b/worldgen/tools/ground_truth/rigid_warp.py index f17b1e89d..2fcbdd30b 100644 --- a/worldgen/tools/ground_truth/rigid_warp.py +++ b/worldgen/tools/ground_truth/rigid_warp.py @@ -49,11 +49,11 @@ def reproject(depth1, pose1, pose2, K1, K2): parser.add_argument('--output', type=Path, default=Path("testbed")) args = parser.parse_args() - depth_path = get_frame_path(args.folder, 0, args.frame_1, 'Depth', '.npy') - image1_path = get_frame_path(args.folder, 0, args.frame_1, 'Image', '.png') - image2_path = get_frame_path(args.folder, 0, args.frame_2, 'Image', '.png') - camview1_path = get_frame_path(args.folder, 0, args.frame_1, 'camview', '.npz') - camview2_path = get_frame_path(args.folder, 0, args.frame_2, 'camview', '.npz') + depth_path = get_frame_path(args.folder, 0, args.frame_1, 'Depth_npy') + image1_path = get_frame_path(args.folder, 0, args.frame_1, 'Image_png') + image2_path = get_frame_path(args.folder, 0, args.frame_2, 'Image_png') + camview1_path = get_frame_path(args.folder, 0, args.frame_1, 'camview_npz') + camview2_path = get_frame_path(args.folder, 0, args.frame_2, 'camview_npz') image2 = imread(image2_path) image1 = imread(image1_path) diff --git a/worldgen/tools/ground_truth/segmentation_lookup.py b/worldgen/tools/ground_truth/segmentation_lookup.py index c59477429..089463b04 100644 --- a/worldgen/tools/ground_truth/segmentation_lookup.py +++ b/worldgen/tools/ground_truth/segmentation_lookup.py @@ -74,10 +74,10 @@ def arr2color(e): args = parser.parse_args() # Load images & masks - object_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'ObjectSegmentation', '.npz'))) - instance_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'InstanceSegmentation', '.npz'))) - image = imread(get_frame_path(args.folder, 0, args.frame, "Image", ".png")) - object_json = json.loads(get_frame_path(args.folder, 0, args.frame, 'Objects', '.json').read_text()) + object_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'ObjectSegmentation_npz'))) + instance_segmentation_mask = recover(np.load(get_frame_path(args.folder, 0, args.frame, 'InstanceSegmentation_npz'))) + image = imread(get_frame_path(args.folder, 0, args.frame, "Image_png")) + object_json = json.loads(get_frame_path(args.folder, 0, args.frame, 'Objects_json').read_text()) H, W = object_segmentation_mask.shape image = cv2.resize(image, dsize=(W, H), interpolation=cv2.INTER_LINEAR) diff --git a/worldgen/tools/results/parse_videos.py b/worldgen/tools/results/parse_videos.py index 867aab7c7..b0850060b 100644 --- a/worldgen/tools/results/parse_videos.py +++ b/worldgen/tools/results/parse_videos.py @@ -12,9 +12,11 @@ parser.add_argument('input_folder', type=Path, nargs='+') parser.add_argument('--output_folder', type=Path, default=None) parser.add_argument('--image_type', default='Image') +parser.add_argument('--camera', type=int, default=0) parser.add_argument('--overlay', type=int, default=1) parser.add_argument('--join', type=int, default=1) parser.add_argument('--fps', type=int, default=24) +parser.add_argument('--resize', type=int, nargs='+', default=[720, 1280]) args = parser.parse_args() for input_folder in args.input_folder: @@ -34,12 +36,21 @@ if len(list(seed_folder.glob('frames*'))) == 0: print(f'{seed_folder=} has no frames*') continue - filters = " " + filters = [] + if args.resize is not None: + filters += ["-s", f"{args.resize[0]}x{args.resize[1]}"] if args.overlay: - filters += f"-vf drawtext='text={seed_folder.absolute()}' " - cmd = f'ffmpeg -y -r {args.fps} -pattern_type glob -i {seed_folder.absolute()}/frames*_0/{args.image_type}*.png {filters} -pix_fmt yuv420p {output_folder}/{seed_folder.name}_{args.image_type}.mp4' - print(cmd.split()) - subprocess.run(cmd.split()) + text = f'{seed_folder.name} {args.image_type} camera_{args.camera}' + filters += ["-vf", f"drawtext='text={text}'"] + cmd = ( + f'ffmpeg -y -r {args.fps} -pattern_type glob '.split() + + f'-i {seed_folder.absolute()}/frames/{args.image_type}/camera_{args.camera}/*.png'.split() + + filters + + '-pix_fmt yuv420p '.split() + + f'{output_folder}/{seed_folder.name}_{args.image_type}_{args.camera}.mp4'.split() + ) + print(cmd) + subprocess.run(cmd) if args.join: cat_output = output_folder/f'{output_folder.name}_{args.image_type}.mp4'