import argparse
import subprocess
import os
import glob
from tqdm import tqdm
import matplotlib.pyplot as plt
[docs]
def create_movie_from_images(
input_path,
output_name='output_movie',
image_format='png',
frame_rate=10,
resolution=None,
quality=15,
start_frame=0,
crop_region=None,
timestamps=None,
image_dpi=200,
preserve_temp=False,
use_existing_temp=False,
allow_rotation=True,
force_overwrite=False
):
"""
Create a movie from a series of images.
:param str input_path: Path to image files or list of image paths
:param str output_name: Name of the output movie file (without extension)
:param str image_format: Format of the input image files
:param int frame_rate: Frames per second in the output movie
:param str resolution: Output movie resolution (format: 'widthxheight')
:param int quality: Video quality (0-51, lower is better)
:param int start_frame: Starting number for image sequence
:param list crop_region: Crop images: [left, right, top, bottom]
:param list timestamps: List of timestamps for each frame
:param int image_dpi: DPI for image processing
:param bool preserve_temp: Keep temporary files after processing
:param bool use_existing_temp: Use existing images in the temp folder
:param bool allow_rotation: Allow automatic image rotation
:param bool force_overwrite: Overwrite existing output file
:return: None
"""
if isinstance(input_path, list):
image_files = input_path
base_dir = os.path.dirname(input_path[0])
else:
image_files = sorted(glob.glob(f"{input_path}*.{image_format}"))
base_dir = os.path.dirname(input_path)
if not image_files:
print('No images found!')
return
temp_dir = os.path.join(base_dir, 'temp_images') + os.path.sep
os.makedirs(temp_dir, exist_ok=True)
if not use_existing_temp:
process_images(image_files, temp_dir, image_format, crop_region, timestamps, resolution, image_dpi)
ffmpeg_options = {
'r': frame_rate,
's': resolution,
'start_number': start_frame,
'crf': quality
}
if resolution is None:
ffmpeg_options.pop('s')
overwrite_flag = '-y' if force_overwrite else '-n'
rotation_flag = '' if allow_rotation else '-noautorotate'
ffmpeg_args = ' '.join([f'-{k} {v}' for k, v in ffmpeg_options.items()])
output_path = f"{os.path.join(base_dir, output_name)}"
if not output_path.lower().endswith('.mp4'):
output_path += '.mp4'
if not force_overwrite and os.path.exists(output_path):
print(f"Error: Output file '{output_path}' already exists. Use -y or --force-overwrite to overwrite.")
return
overwrite_flag = '-y' if force_overwrite else '-n'
rotation_flag = '' if allow_rotation else '-noautorotate'
ffmpeg_command = (
f'ffmpeg {rotation_flag} {overwrite_flag} -r {frame_rate} -f image2 '
f'-i {temp_dir}%04d.{image_format} '
f'-vcodec libx264 -pix_fmt yuv420p {ffmpeg_args} '
f'-vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" '
f'{output_path}'
)
try:
subprocess.check_output(['bash', '-c', ffmpeg_command], stderr=subprocess.STDOUT)
print(f"Movie created: {output_path}")
except subprocess.CalledProcessError as e:
print(f"Error creating movie: {e.output.decode()}")
print(f"FFmpeg command: {ffmpeg_command}")
if not preserve_temp:
os.system(f'rm -rf {temp_dir}')
[docs]
def process_images(image_files, temp_dir, image_format, crop_region, timestamps, resolution, image_dpi):
if crop_region or timestamps:
plt.ioff()
fig_size = [float(dim) / image_dpi for dim in resolution.split('x')] if resolution else None
fig, ax = plt.subplots(figsize=fig_size)
for idx, img_path in enumerate(tqdm(image_files, desc="Processing images")):
data = plt.imread(img_path)
if crop_region:
left, right, top, bottom = crop_region
data = data[top:bottom + 1, left:right + 1, :]
if idx == 0:
im = ax.imshow(data)
ax.set_axis_off()
else:
im.set_array(data)
if timestamps:
ax.set_title(timestamps[idx])
if idx == 0:
fig.tight_layout()
fig.savefig(f'{temp_dir}{idx:04d}.{image_format}', dpi=image_dpi, format=image_format)
plt.close(fig)
else:
for idx, img_path in enumerate(tqdm(image_files, desc="Copying images")):
os.system(f'cp {img_path} {temp_dir}{idx:04d}.{image_format}')
if __name__ == "__main__":
[docs]
parser = argparse.ArgumentParser(description="Create a movie from a series of images.")
parser.add_argument("input_path", help="Path to image files or list of image paths")
parser.add_argument("-o", "--output-name", default="output_movie",
help="Name of the output movie file (without extension)")
parser.add_argument("-f", "--image-format", default="png", help="Format of the input image files")
parser.add_argument("-r", "--frame-rate", type=int, default=10, help="Frames per second in the output movie")
parser.add_argument("-s", "--resolution", help="Output movie resolution (format: 'widthxheight')")
parser.add_argument("-q", "--quality", type=int, default=15, help="Video quality (0-51, lower is better)")
parser.add_argument("-n", "--start-frame", type=int, default=0, help="Starting number for image sequence")
parser.add_argument("-c", "--crop-region", nargs=4, type=int, help="Crop images: left right top bottom")
parser.add_argument("-t", "--timestamps", nargs="+", help="List of timestamps for each frame")
parser.add_argument("-d", "--image-dpi", type=int, default=200, help="DPI for image processing")
parser.add_argument("-p", "--preserve-temp", action="store_true", help="Keep temporary files after processing")
parser.add_argument("-u", "--use-existing-temp", action="store_true", help="Use existing images in the temp folder")
parser.add_argument("--no-rotation", dest="allow_rotation", action="store_false",
help="Disable automatic image rotation")
parser.add_argument("-y", "--force-overwrite", action="store_true", help="Overwrite existing output file")
args = parser.parse_args()
create_movie_from_images(
input_path=args.input_path,
output_name=args.output_name,
image_format=args.image_format,
frame_rate=args.frame_rate,
resolution=args.resolution,
quality=args.quality,
start_frame=args.start_frame,
crop_region=args.crop_region,
timestamps=args.timestamps,
image_dpi=args.image_dpi,
preserve_temp=args.preserve_temp,
use_existing_temp=args.use_existing_temp,
allow_rotation=args.allow_rotation,
force_overwrite=args.force_overwrite
)