feat(lcd): add boundary performance test
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user