Script assisted by Claude

This commit is contained in:
2026-08-27 12:32:12 +01:00
commit efd904ff9f
2 changed files with 119 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
"""
VIDEO MOSAIC MAKER
==================
Grabs 9 equally spaced frames from a video and returns a 1080p mosaic image,
with the video timestamp of each frame stamped in its upper-left corner.
Requirements:
pip install opencv-python Pillow
"""
import cv2
from PIL import Image, ImageDraw, ImageFont
def _format_timestamp(seconds):
"""Formats a number of seconds as HH:MM:SS (or MM:SS if under an hour)."""
total_seconds = int(seconds)
hours, remainder = divmod(total_seconds, 3600)
minutes, secs = divmod(remainder, 60)
if hours:
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
return f"{minutes:02d}:{secs:02d}"
def _load_font(size):
"""Tries to load a truetype font, falling back to PIL's default bitmap font."""
for font_name in ("DejaVuSans-Bold.ttf", "Arial.ttf", "arial.ttf"):
try:
return ImageFont.truetype(font_name, size)
except IOError:
continue
return ImageFont.load_default()
def _draw_timestamp(image, text, margin=10):
"""Draws `text` in the upper-left corner of `image` with a dark backing
box behind it so it stays readable over any frame content."""
draw = ImageDraw.Draw(image, "RGBA")
font_size = max(16, image.height // 20)
font = _load_font(font_size)
bbox = draw.textbbox((0, 0), text, font=font)
text_w = bbox[2] - bbox[0]
text_h = bbox[3] - bbox[1]
box_x0, box_y0 = margin, margin
box_x1 = box_x0 + text_w + 2 * margin
box_y1 = box_y0 + text_h + 2 * margin
draw.rectangle([box_x0, box_y0, box_x1, box_y1], fill=(0, 0, 0, 160))
draw.text(
(box_x0 + margin - bbox[0], box_y0 + margin - bbox[1]),
text,
font=font,
fill=(255, 255, 255, 255),
)
return image
def video_mosaic(video_path):
"""
Takes a video file path and returns a 1920x1080 PIL image
with 9 frames arranged in a 3x3 grid, each stamped with its
timestamp in the video.
"""
# --- Open the video ---
video = cv2.VideoCapture(video_path)
total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
fps = video.get(cv2.CAP_PROP_FPS) or 30.0 # fallback if fps is unreadable
# --- Calculate which 9 frames to grab (evenly spaced) ---
frame_positions = [
int(total_frames * (i + 1) / 10) # 10%, 20%, ... 90%
for i in range(9)
]
# --- Extract each frame, paired with its timestamp ---
frames = []
for pos in frame_positions:
video.set(cv2.CAP_PROP_POS_FRAMES, pos)
success, frame = video.read()
if success:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # BGR → RGB
timestamp = _format_timestamp(pos / fps)
frames.append((Image.fromarray(frame_rgb), timestamp))
video.release()
# --- Build the 1920x1080 mosaic ---
COLS, ROWS = 3, 3
PADDING = 10
CANVAS_W = 1920
CANVAS_H = 1080
cell_w = (CANVAS_W - PADDING * (COLS + 1)) // COLS
cell_h = (CANVAS_H - PADDING * (ROWS + 1)) // ROWS
mosaic = Image.new("RGB", (CANVAS_W, CANVAS_H), color=(0, 0, 0))
for i, (frame, timestamp) in enumerate(frames):
row, col = divmod(i, COLS)
x = PADDING + col * (cell_w + PADDING)
y = PADDING + row * (cell_h + PADDING)
resized = frame.resize((cell_w, cell_h), Image.LANCZOS)
_draw_timestamp(resized, timestamp)
mosaic.paste(resized, (x, y))
return mosaic
# --- Example usage ---
if __name__ == "__main__":
image = video_mosaic("video.mp4")
image.save("mosaic.jpg")