Script assisted by Claude
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
opencv-python>=4.8
|
||||
Pillow>=10.0
|
||||
+117
@@ -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")
|
||||
Reference in New Issue
Block a user