1
0
Files
h8-536-decoder/scripts/make_panel_sweep_contact_sheets.py
2026-05-27 21:37:50 +10:00

136 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Build labeled contact sheets from serial_scenario webcam snapshots."""
from __future__ import annotations
import argparse
import math
from pathlib import Path
import cv2
import numpy as np
CROP_PRESETS = {
"full": (0.00, 0.00, 1.00, 1.00),
"panel": (0.03, 0.10, 0.98, 0.94),
"right-stack": (0.70, 0.23, 0.92, 0.86),
"lcd": (0.00, 0.22, 0.38, 0.78),
}
def main() -> int:
args = build_arg_parser().parse_args()
images = sorted(args.snapshot_dir.glob(args.glob))
if args.only_candidates:
images = [path for path in images if "candidate_" in path.name]
if not images:
raise SystemExit(f"no images found in {args.snapshot_dir}")
output_dir = args.output_dir or args.snapshot_dir.with_name(args.snapshot_dir.name + "-sheets")
output_dir.mkdir(parents=True, exist_ok=True)
per_sheet = args.cols * args.rows
total_sheets = math.ceil(len(images) / per_sheet)
written: list[Path] = []
for sheet_index in range(total_sheets):
group = images[sheet_index * per_sheet : (sheet_index + 1) * per_sheet]
sheet = build_sheet(group, args)
out = output_dir / f"sheet-{sheet_index + 1:03d}.jpg"
cv2.imwrite(str(out), sheet)
written.append(out)
print(f"images={len(images)} sheets={len(written)} output_dir={output_dir}")
if written:
print(f"first={written[0]}")
print(f"last={written[-1]}")
return 0
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Create labeled contact sheets from webcam snapshots.")
parser.add_argument("snapshot_dir", type=Path, help="directory containing serial_scenario snapshots")
parser.add_argument("--output-dir", type=Path, help="directory for generated contact sheets")
parser.add_argument("--glob", default="*.jpg", help="image glob within snapshot_dir")
parser.add_argument("--cols", type=int, default=4, help="thumbnail columns per sheet")
parser.add_argument("--rows", type=int, default=5, help="thumbnail rows per sheet")
parser.add_argument("--thumb-width", type=int, default=360, help="thumbnail image width")
parser.add_argument("--label-height", type=int, default=48, help="label area height per thumbnail")
parser.add_argument(
"--crop",
choices=sorted(CROP_PRESETS),
default="panel",
help="crop preset before thumbnailing",
)
parser.add_argument(
"--only-candidates",
action="store_true",
help="include only filenames containing candidate_",
)
return parser
def build_sheet(paths: list[Path], args: argparse.Namespace) -> np.ndarray:
cells = [build_cell(path, args) for path in paths]
blank = np.full_like(cells[0], 245)
while len(cells) < args.cols * args.rows:
cells.append(blank.copy())
rows = []
for row_index in range(args.rows):
row_cells = cells[row_index * args.cols : (row_index + 1) * args.cols]
rows.append(np.hstack(row_cells))
return np.vstack(rows)
def build_cell(path: Path, args: argparse.Namespace) -> np.ndarray:
image = cv2.imread(str(path))
if image is None:
image = np.full((240, 320, 3), 220, dtype=np.uint8)
image = crop_image(image, CROP_PRESETS[args.crop])
thumb_height = max(1, int(image.shape[0] * args.thumb_width / max(1, image.shape[1])))
thumb = cv2.resize(image, (args.thumb_width, thumb_height), interpolation=cv2.INTER_AREA)
label = snapshot_label(path)
cell = np.full((args.label_height + thumb_height, args.thumb_width, 3), 245, dtype=np.uint8)
cell[args.label_height : args.label_height + thumb_height, 0 : args.thumb_width] = thumb
draw_label(cell, label, args.label_height)
return cell
def crop_image(image: np.ndarray, crop: tuple[float, float, float, float]) -> np.ndarray:
h, w = image.shape[:2]
x1, y1, x2, y2 = crop
left = min(w - 1, max(0, int(w * x1)))
top = min(h - 1, max(0, int(h * y1)))
right = min(w, max(left + 1, int(w * x2)))
bottom = min(h, max(top + 1, int(h * y2)))
return image[top:bottom, left:right]
def draw_label(cell: np.ndarray, text: str, label_height: int) -> None:
font = cv2.FONT_HERSHEY_SIMPLEX
scale = 0.48
thickness = 1
max_chars = 45
line1 = text[:max_chars]
line2 = text[max_chars : max_chars * 2]
cv2.putText(cell, line1, (6, 18), font, scale, (0, 0, 0), thickness, cv2.LINE_AA)
if line2:
cv2.putText(cell, line2, (6, min(label_height - 8, 38)), font, scale, (0, 0, 0), thickness, cv2.LINE_AA)
def snapshot_label(path: Path) -> str:
name = path.name
if "_tx_0500ms_" in name:
label = name.split("_tx_0500ms_", 1)[1]
elif "_tx_" in name:
label = name.split("_tx_", 1)[1]
else:
label = name
return label.rsplit("_", 1)[0]
if __name__ == "__main__":
raise SystemExit(main())