Files
lucky-cat/tools/generate_lcd_boundary_frames.py
T

146 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Generate deterministic 320x240 frames for LCD boundary testing."""
from __future__ import annotations
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
PROJECT_ROOT = Path(__file__).resolve().parent.parent
SOURCE_DIR = PROJECT_ROOT / "components" / "platform" / "assets" / "lcd_source"
PREVIEW_PATH = PROJECT_ROOT / "docs" / "lcd-boundary-test-preview.gif"
WIDTH = 320
HEIGHT = 240
FRAME_COUNT = 16
FRAME_PREFIX = "lcdtest"
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
CYAN = (0, 255, 255)
MAGENTA = (255, 0, 255)
YELLOW = (255, 255, 0)
GRAY = (132, 130, 132)
DARK_GRAY = (33, 32, 33)
def draw_corner_targets(draw: ImageDraw.ImageDraw) -> None:
targets = (
(0, 0, RED),
(WIDTH - 12, 0, GREEN),
(0, HEIGHT - 12, BLUE),
(WIDTH - 12, HEIGHT - 12, YELLOW),
)
for x, y, color in targets:
draw.rectangle((x, y, x + 11, y + 11), fill=color, outline=WHITE)
def draw_color_bars(draw: ImageDraw.ImageDraw) -> None:
colors = (WHITE, YELLOW, CYAN, GREEN, MAGENTA, RED, BLUE, BLACK)
for index, color in enumerate(colors):
x0 = index * (WIDTH // len(colors))
x1 = (index + 1) * (WIDTH // len(colors)) - 1
draw.rectangle((x0, 1, x1, 30), fill=color)
def draw_motion_field(draw: ImageDraw.ImageDraw, frame_index: int) -> None:
field_top = 32
field_bottom = 207
stripe_width = 16
phase = frame_index * 4
draw.rectangle((1, field_top, WIDTH - 2, field_bottom), fill=DARK_GRAY)
for x in range(-HEIGHT, WIDTH + HEIGHT, stripe_width * 2):
shifted = x + phase
draw.polygon(
(
(shifted, field_top),
(shifted + stripe_width, field_top),
(shifted - 160 + stripe_width, field_bottom),
(shifted - 160, field_bottom),
),
fill=GRAY,
)
vertical_x = frame_index * (WIDTH // FRAME_COUNT)
horizontal_y = field_top + frame_index * ((field_bottom - field_top + 1) // FRAME_COUNT)
draw.rectangle((vertical_x, field_top, vertical_x + 2, field_bottom), fill=WHITE)
draw.rectangle((1, horizontal_y, WIDTH - 2, horizontal_y + 2), fill=YELLOW)
center_x = WIDTH // 2
center_y = (field_top + field_bottom) // 2
angle = (2.0 * math.pi * frame_index / FRAME_COUNT) - (math.pi / 2.0)
pointer_x = round(center_x + math.cos(angle) * 62)
pointer_y = round(center_y + math.sin(angle) * 62)
draw.ellipse((center_x - 65, center_y - 65, center_x + 65, center_y + 65), outline=CYAN, width=2)
draw.line((center_x, center_y, pointer_x, pointer_y), fill=RED, width=4)
draw.ellipse((center_x - 6, center_y - 6, center_x + 6, center_y + 6), fill=WHITE, outline=BLACK)
moving_x = round(center_x + math.cos(angle) * 92)
moving_y = round(center_y + math.sin(angle) * 50)
draw.rectangle((moving_x - 7, moving_y - 7, moving_x + 7, moving_y + 7), fill=MAGENTA, outline=WHITE)
def draw_frame_footer(draw: ImageDraw.ImageDraw, frame_index: int, font: ImageFont.ImageFont) -> None:
footer_top = 209
draw.rectangle((1, footer_top, WIDTH - 2, HEIGHT - 2), fill=BLACK)
draw.text((16, 211), f"F{frame_index:02d}", font=font, fill=WHITE)
cell_width = 12
cell_gap = 2
strip_x = 102
strip_y = 214
for index in range(FRAME_COUNT):
x0 = strip_x + index * (cell_width + cell_gap)
color = WHITE if index == frame_index else DARK_GRAY
draw.rectangle((x0, strip_y, x0 + cell_width - 1, strip_y + 10), fill=color, outline=GRAY)
for bit in range(4):
x0 = strip_x + bit * 20
color = GREEN if frame_index & (1 << bit) else RED
draw.rectangle((x0, 229, x0 + 15, 237), fill=color)
def generate_frame(frame_index: int, font: ImageFont.ImageFont) -> Image.Image:
image = Image.new("RGB", (WIDTH, HEIGHT), BLACK)
draw = ImageDraw.Draw(image)
draw_color_bars(draw)
draw_motion_field(draw, frame_index)
draw_frame_footer(draw, frame_index, font)
draw.rectangle((0, 0, WIDTH - 1, HEIGHT - 1), outline=WHITE)
draw.rectangle((1, 1, WIDTH - 2, HEIGHT - 2), outline=RED)
draw_corner_targets(draw)
return image
def main() -> None:
SOURCE_DIR.mkdir(parents=True, exist_ok=True)
PREVIEW_PATH.parent.mkdir(parents=True, exist_ok=True)
font = ImageFont.load_default(size=22)
frames = [generate_frame(index, font) for index in range(FRAME_COUNT)]
for index, frame in enumerate(frames):
output_path = SOURCE_DIR / f"{FRAME_PREFIX}{index:02d}.png"
frame.save(output_path, format="PNG", optimize=True, compress_level=9)
frames[0].save(
PREVIEW_PATH,
format="GIF",
save_all=True,
append_images=frames[1:],
duration=80,
loop=0,
disposal=2,
)
print(f"Generated {FRAME_COUNT} LCD boundary frames and {PREVIEW_PATH}")
if __name__ == "__main__":
main()