-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstitch-frames
More file actions
executable file
·166 lines (134 loc) · 4.9 KB
/
stitch-frames
File metadata and controls
executable file
·166 lines (134 loc) · 4.9 KB
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "pillow",
# "pillow-heif",
# "pillow-jxl-plugin",
# ]
# ///
"""
Stitches image frames into an HD 1080p video.
Handles both portrait and landscape images by fitting them into 1920x1080 frames.
"""
import argparse
import subprocess
import sys
from pathlib import Path
from PIL import Image
import tempfile
import shutil
# Register HEIF and JXL plugins
try:
from pillow_heif import register_heif_opener
register_heif_opener()
except ImportError:
pass
try:
import pillow_jxl
except ImportError:
pass
def analyze_images(image_dir: Path):
"""Analyze images to determine orientation mix."""
image_files = sorted([
f for f in image_dir.iterdir()
if f.suffix.lower() in {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp', '.heic', '.heif', '.jxl', '.avif'}
])
if not image_files:
print(f"No image files found in {image_dir}")
sys.exit(1)
orientations = {'portrait': 0, 'landscape': 0, 'square': 0}
for img_path in image_files:
try:
with Image.open(img_path) as img:
width, height = img.size
if width > height:
orientations['landscape'] += 1
elif height > width:
orientations['portrait'] += 1
else:
orientations['square'] += 1
except Exception as e:
print(f"Warning: Could not read {img_path}: {e}")
return image_files, orientations
def process_images(image_files: list[Path], output_dir: Path, target_width: int = 1920, target_height: int = 1080):
"""Process images to fit into target resolution with letterboxing."""
output_dir.mkdir(parents=True, exist_ok=True)
for idx, img_path in enumerate(image_files):
try:
with Image.open(img_path) as img:
# Convert to RGB if necessary
if img.mode != 'RGB':
img = img.convert('RGB')
# Calculate scaling to fit within target dimensions
img_width, img_height = img.size
scale = min(target_width / img_width, target_height / img_height)
new_width = int(img_width * scale)
new_height = int(img_height * scale)
# Resize image
img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Create black background at target resolution
background = Image.new('RGB', (target_width, target_height), (0, 0, 0))
# Calculate position to center the image
x_offset = (target_width - new_width) // 2
y_offset = (target_height - new_height) // 2
# Paste resized image onto background
background.paste(img_resized, (x_offset, y_offset))
# Save with sequential numbering for ffmpeg
output_path = output_dir / f"frame_{idx:06d}.jpg"
background.save(output_path, 'JPEG', quality=95)
except Exception as e:
print(f"Error processing {img_path}: {e}")
sys.exit(1)
return len(image_files)
def create_video(frames_dir: Path, output_path: Path, fps: int = 30):
"""Use ffmpeg to create video from processed frames."""
cmd = [
'ffmpeg',
'-framerate', str(fps),
'-i', str(frames_dir / 'frame_%06d.jpg'),
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-crf', '18',
'-y',
str(output_path)
]
try:
subprocess.run(cmd, check=True)
print(f"\nVideo created: {output_path}")
except subprocess.CalledProcessError as e:
print(f"Error running ffmpeg: {e}")
sys.exit(1)
except FileNotFoundError:
print("Error: ffmpeg not found. Please install ffmpeg.")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description='Stitch image frames into HD 1080p video')
parser.add_argument('directory', type=Path, help='Directory containing image frames')
parser.add_argument('-o', '--output', type=Path, help='Output video path (default: output.mp4)')
parser.add_argument('-f', '--fps', type=int, default=30, help='Frames per second (default: 30)')
parser.add_argument('--width', type=int, default=1920, help='Output width (default: 1920)')
parser.add_argument('--height', type=int, default=1080, help='Output height (default: 1080)')
args = parser.parse_args()
if not args.directory.is_dir():
print(f"Error: {args.directory} is not a directory")
sys.exit(1)
output_path = args.output or Path('output.mp4')
print(f"Analyzing images in {args.directory}...")
image_files, orientations = analyze_images(args.directory)
print(f"\nFound {len(image_files)} images:")
print(f" Landscape: {orientations['landscape']}")
print(f" Portrait: {orientations['portrait']}")
print(f" Square: {orientations['square']}")
print(f"\nTarget resolution: {args.width}x{args.height}")
print(f"Frame rate: {args.fps} fps\n")
# Create temporary directory for processed frames
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
print("Processing images...")
frame_count = process_images(image_files, temp_path, args.width, args.height)
print(f"Processed {frame_count} frames")
print("\nCreating video with ffmpeg...")
create_video(temp_path, output_path, args.fps)
if __name__ == '__main__':
main()