Skip to content

Instantly share code, notes, and snippets.

@justdanpo
Last active February 26, 2025 12:04
Show Gist options
  • Save justdanpo/da4a046ea587728e2d164957efe7f6ca to your computer and use it in GitHub Desktop.
Save justdanpo/da4a046ea587728e2d164957efe7f6ca to your computer and use it in GitHub Desktop.
convert images to timelapse video
# convert images to timelapse video
# can work with file mask (glob); ffmpeg for windows itself cannot do that
# autodetect hardware encoder
# interpolate images for smoother video
import math
import subprocess
import glob
file_mask = "*.png"
frame_rate = 15 # frame rate of original image sequence
dest_frame_rate = 30 # interpolate to
fade_in = 0.6 # fade time in seconds
fade_out = 0.6 # fade time in seconds
pix_fmt = "yuv420p" # yuv420p/yuv444p
out_file_name = "out.mp4"
auto_overwrite = False
# resize_to_x = 2560
# resize_to_y = 1080 # 2160
Encoder_settings = {
"h264_nvenc": [
"-preset p7",
"-tune ull",
"-profile:v high",
"-b:v 6M",
"-maxrate 6M",
"-bufsize 12M",
"-rc ll_2pass_quality",
"-multipass fullres",
# strange artifacts on fade in without these:
"-bf 3", #-bf <int> E..V....... set maximum number of B-frames between non-B-frames (from -1 to INT_MAX) (default 0)
"-g 120", #-g <int> E..V....... set the group of picture (GOP) size (from INT_MIN to INT_MAX) (default 12)
],
"h264_qsv": ["-global_quality 13"],
"libx264": [
"-preset veryslow",
"-tune stillimage",
"-qp 16",
],
}
################################################################################
def detect_encoders():
default_encoder = "libx264"
def get_codec_name(codec_line: str):
return codec_line.split()[1]
def codec_filter(codec_line: str):
if not codec_line.startswith(" V"):
return False
if not "H.264" in codec_line:
return False
if default_encoder in codec_line:
return False
return True
def check_codec(codec_name: str):
try_encode_virtual_source = subprocess.run(
f"ffmpeg -f lavfi -i nullsrc=s=320x128:d=0 -c:v {codec_name} -f null -".split(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
return try_encode_virtual_source.returncode == 0
print("detecting supported encoders")
fetch_ffmpeg_encoders = subprocess.run(
["ffmpeg", "-hide_banner", "-encoders"], stdout=subprocess.PIPE
)
if fetch_ffmpeg_encoders.returncode != 0:
print("cannot fetch ffmpeg encoder list")
exit(1)
video_h264_encoders = [
get_codec_name(encoderLine)
for encoderLine in fetch_ffmpeg_encoders.stdout.decode("UTF-8").splitlines()
if codec_filter(encoderLine)
] + [default_encoder]
print("examining encoders: ", video_h264_encoders)
successfully_executed_codecs = [x for x in video_h264_encoders if check_codec(x)]
print("supported encoders list: ", successfully_executed_codecs)
return successfully_executed_codecs
input_files = [f for f in glob.glob(file_mask)]
if len(input_files) == 0:
print(f"cannot find files by mask {file_mask}")
exit(1)
options = []
video_filters = []
supported_encoders = detect_encoders()
assert len(supported_encoders) > 0
if auto_overwrite:
options.append("-y")
options = options + ["-r", f"{frame_rate}"]
options = options + ["-i", f'concat:{"|".join(input_files)}']
# add Encoder_settings for specified encoder
selected_codec = supported_encoders[0]
if not selected_codec in Encoder_settings:
print(f"cannot find settings for encoder {selected_codec}")
exit(1)
else:
print(f"using encoder: {selected_codec}")
selected_codec_settings = ["-c:v", selected_codec] + Encoder_settings[
selected_codec
]
for setting in selected_codec_settings:
options = options + setting.split()
fade_in_frames = fade_in * frame_rate
fade_out_frames = fade_out * frame_rate
fade_out_start_frame = (
len(input_files) - fade_out_frames - math.ceil(dest_frame_rate / frame_rate)
)
if fade_in_frames > 0:
video_filters.append(f"fade=in:s=0:n={fade_in_frames}")
if fade_out_frames > 0:
video_filters.append(f"fade=out:s={fade_out_start_frame}:n={fade_out_frames}")
if not "resize_to_x" in globals():
resize_to_x = -1
if not "resize_to_y" in globals():
resize_to_y = -1
video_filters.append(f"scale={resize_to_x}:{resize_to_y}")
# sometimes video with odd sizes cannot be played; crop images to make sizes even
video_filters.append("crop=trunc(iw/2)*2:trunc(ih/2)*2")
if frame_rate != dest_frame_rate:
video_filters.append(f"minterpolate=fps={dest_frame_rate}:mi_mode=blend")
if len(video_filters) > 0:
options = options + ["-vf", f'{",".join(video_filters)}']
options = options + ["-pix_fmt", pix_fmt]
options.append(out_file_name)
encode = subprocess.run(["ffmpeg"] + options)
exit(encode.returncode)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment