File size: 2,841 Bytes
06c9f97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import argparse
import re
from pathlib import Path

import imageio.v2 as imageio
import numpy as np
from PIL import Image


def _natural_key(text):
    parts = re.split(r"(\d+)", text)
    return [int(p) if p.isdigit() else p.lower() for p in parts]


def _iter_images(folder, extensions, natural_sort):
    paths = [
        p
        for p in Path(folder).iterdir()
        if p.is_file() and p.suffix.lower() in extensions
    ]
    if natural_sort:
        return sorted(paths, key=lambda p: _natural_key(p.name))
    return sorted(paths, key=lambda p: p.name.lower())


def images_to_mp4(
    input_dir,
    output_path,
    fps,
    extensions,
    natural_sort,
    resize_to_first,
    codec,
):
    images = _iter_images(input_dir, extensions, natural_sort)
    if not images:
        raise ValueError(f"No images found in {input_dir}")

    first = Image.open(images[0]).convert("RGB")
    target_size = first.size

    with imageio.get_writer(output_path, fps=fps, codec=codec) as writer:
        writer.append_data(np.array(first))
        for path in images[1:]:
            img = Image.open(path).convert("RGB")
            if img.size != target_size:
                if resize_to_first:
                    img = img.resize(target_size, Image.Resampling.LANCZOS)
                else:
                    raise ValueError(
                        f"Size mismatch: {path.name} is {img.size}, expected {target_size}"
                    )
            writer.append_data(np.array(img))


def main():
    parser = argparse.ArgumentParser(
        description="Create an MP4 video from images in a folder."
    )
    parser.add_argument("input_dir", help="Folder containing images.")
    parser.add_argument("output", help="Output MP4 path, e.g. out.mp4.")
    parser.add_argument(
        "--fps", type=float, default=24, help="Frames per second (default: 24)."
    )
    parser.add_argument(
        "--ext",
        action="append",
        default=[".jpg", ".jpeg", ".png"],
        help="Allowed extensions (repeatable). Default: .jpg .jpeg .png",
    )
    parser.add_argument(
        "--natural",
        action="store_true",
        help="Use natural sort for filenames (e.g. img2 before img10).",
    )
    parser.add_argument(
        "--no-resize",
        action="store_true",
        help="Fail if image sizes differ instead of resizing to the first image.",
    )
    parser.add_argument(
        "--codec",
        default="libx264",
        help="Video codec for MP4 (default: libx264).",
    )
    args = parser.parse_args()

    images_to_mp4(
        args.input_dir,
        args.output,
        fps=args.fps,
        extensions={e.lower() for e in args.ext},
        natural_sort=args.natural,
        resize_to_first=not args.no_resize,
        codec=args.codec,
    )


if __name__ == "__main__":
    main()