136 lines
4.8 KiB
Python
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())
|