updates
This commit is contained in:
226
scripts/build_panel_visual_sweep.py
Normal file
226
scripts/build_panel_visual_sweep.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a JSON serial_scenario for broad visible panel sweeps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
CHECKSUM_SEED = 0x5A
|
||||
CONNECT_OK_FRAME = "00 00 00 80 00 DA"
|
||||
SLAVE_POSITIVE_CONTROL = "00 00 13 80 00 C9"
|
||||
SLAVE_CLEAR = "00 00 13 00 00 49"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_arg_parser().parse_args()
|
||||
values = parse_int_list(args.values)
|
||||
skips = set(parse_int_list(args.skip)) if args.skip else set()
|
||||
selectors = [
|
||||
selector
|
||||
for selector in range(parse_int(args.start), parse_int(args.end) + 1)
|
||||
if selector not in skips and (args.include_selector_zero or selector != 0)
|
||||
]
|
||||
if not selectors:
|
||||
raise SystemExit("selector range is empty after skips")
|
||||
|
||||
scenario = build_scenario(args, selectors, values)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
candidate_images = len(selectors) * len(values)
|
||||
windows = window_count(len(selectors), args.power_cycle_every)
|
||||
print(f"wrote {args.output}")
|
||||
print(f"selectors={len(selectors)} values={len(values)} candidate_snapshots={candidate_images}")
|
||||
print(f"power_cycle_windows={windows} estimated_candidate_time={candidate_images * args.listen / 60:.1f}min")
|
||||
return 0
|
||||
|
||||
|
||||
def build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a serial_scenario JSON file for a broad panel-output webcam sweep."
|
||||
)
|
||||
parser.add_argument("output", type=Path, help="scenario JSON path to write")
|
||||
parser.add_argument("--start", default="0x0001", help="first logical selector, inclusive")
|
||||
parser.add_argument("--end", default="0x017F", help="last logical selector, inclusive")
|
||||
parser.add_argument(
|
||||
"--values",
|
||||
default="0x8000,0x4000,0x2000,0x1000,0x0800",
|
||||
help="comma-separated 16-bit values to try for each selector",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip",
|
||||
default="",
|
||||
help="comma-separated logical selectors to skip, e.g. 0x006C,0x006D",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-selector-zero",
|
||||
action="store_true",
|
||||
help="include selector 0 in the sweep; omitted by default because it controls CONNECT OK state",
|
||||
)
|
||||
parser.add_argument("--listen", type=float, default=0.65, help="seconds to listen after each candidate send")
|
||||
parser.add_argument("--clear-listen", type=float, default=0.15, help="seconds after selector clear writes")
|
||||
parser.add_argument("--ok-listen", type=float, default=0.30, help="seconds after CONNECT OK refresh writes")
|
||||
parser.add_argument(
|
||||
"--ok-every",
|
||||
type=int,
|
||||
default=8,
|
||||
help="refresh CONNECT OK every N selectors; use 0 to disable periodic refresh",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--power-cycle-every",
|
||||
type=int,
|
||||
default=32,
|
||||
help="power-cycle every N selectors to limit latch contamination; use 0 for one long session",
|
||||
)
|
||||
parser.add_argument("--off-seconds", type=float, default=1.5, help="relay power-off time per window")
|
||||
parser.add_argument("--ready-timeout", type=float, default=10.0, help="wait_ready timeout")
|
||||
parser.add_argument("--drain", type=float, default=0.5, help="seconds to drain after ready")
|
||||
parser.add_argument("--final-listen", type=float, default=1.0, help="seconds to listen at the end")
|
||||
return parser
|
||||
|
||||
|
||||
def build_scenario(args: argparse.Namespace, selectors: list[int], values: list[int]) -> dict[str, object]:
|
||||
start = selectors[0]
|
||||
end = selectors[-1]
|
||||
value_text = ",".join(f"0x{value:04X}" for value in values)
|
||||
scenario: dict[str, object] = {
|
||||
"name": f"panel-atlas-big-visual-sweep-{start:03X}-{end:03X}",
|
||||
"notes": [
|
||||
"Broad visible-output sweep generated by scripts/build_panel_visual_sweep.py.",
|
||||
"Candidate selector/value sends have webcam snapshots enabled; CONNECT OK refreshes and clears do not.",
|
||||
"Use image filenames candidate_XXXX_YYYY to refine any visible trigger into a smaller follow-up scenario.",
|
||||
f"Values: {value_text}",
|
||||
],
|
||||
"steps": [],
|
||||
}
|
||||
steps: list[dict[str, object]] = scenario["steps"] # type: ignore[assignment]
|
||||
|
||||
for window_index, window in enumerate(selector_windows(selectors, args.power_cycle_every), start=1):
|
||||
add_session_prelude(args, steps, window_index)
|
||||
if window_index == 1:
|
||||
steps.append(send_step("positive_control_0013_8000_slave_on", SLAVE_POSITIVE_CONTROL, args.listen))
|
||||
steps.append(send_step("clear_0013_after_slave", SLAVE_CLEAR, args.clear_listen, snapshot=False))
|
||||
|
||||
for selector_index, selector in enumerate(window, start=1):
|
||||
if args.ok_every > 0 and (selector_index - 1) % args.ok_every == 0:
|
||||
steps.append(
|
||||
send_step(
|
||||
f"ok_refresh_before_{selector:04X}",
|
||||
CONNECT_OK_FRAME,
|
||||
args.ok_listen,
|
||||
snapshot=False,
|
||||
)
|
||||
)
|
||||
for value in values:
|
||||
steps.append(
|
||||
send_step(
|
||||
f"candidate_{selector:04X}_{value:04X}",
|
||||
frame_hex(0x00, selector, value),
|
||||
args.listen,
|
||||
)
|
||||
)
|
||||
steps.append(
|
||||
send_step(
|
||||
f"clear_{selector:04X}",
|
||||
frame_hex(0x00, selector, 0),
|
||||
args.clear_listen,
|
||||
snapshot=False,
|
||||
)
|
||||
)
|
||||
|
||||
steps.append({"action": "listen", "seconds": args.final_listen})
|
||||
return scenario
|
||||
|
||||
|
||||
def add_session_prelude(args: argparse.Namespace, steps: list[dict[str, object]], window_index: int) -> None:
|
||||
steps.extend(
|
||||
[
|
||||
{"action": "power_cycle", "off_seconds": args.off_seconds},
|
||||
{
|
||||
"action": "wait_ready",
|
||||
"heartbeats": 2,
|
||||
"timeout": args.ready_timeout,
|
||||
"require": True,
|
||||
},
|
||||
{"action": "drain", "seconds": args.drain},
|
||||
send_step(f"window_{window_index:03d}_ok_seed_1", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
|
||||
send_step(f"window_{window_index:03d}_ok_seed_2", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def send_step(label: str, frame: str, listen: float, *, snapshot: bool = True) -> dict[str, object]:
|
||||
step: dict[str, object] = {
|
||||
"action": "send",
|
||||
"label": label,
|
||||
"frame": frame,
|
||||
"listen": listen,
|
||||
}
|
||||
if not snapshot:
|
||||
step["snapshot"] = False
|
||||
return step
|
||||
|
||||
|
||||
def selector_windows(selectors: list[int], size: int) -> Iterable[list[int]]:
|
||||
if size <= 0:
|
||||
yield selectors
|
||||
return
|
||||
for index in range(0, len(selectors), size):
|
||||
yield selectors[index : index + size]
|
||||
|
||||
|
||||
def window_count(selector_count: int, size: int) -> int:
|
||||
if size <= 0:
|
||||
return 1
|
||||
return (selector_count + size - 1) // size
|
||||
|
||||
|
||||
def frame_hex(command: int, selector: int, value: int) -> str:
|
||||
selector_hi, selector_lo = selector_bytes(selector)
|
||||
data = bytes(
|
||||
[
|
||||
command & 0xFF,
|
||||
selector_hi,
|
||||
selector_lo,
|
||||
(value >> 8) & 0xFF,
|
||||
value & 0xFF,
|
||||
]
|
||||
)
|
||||
return " ".join(f"{byte:02X}" for byte in data + bytes([frame_checksum(data)]))
|
||||
|
||||
|
||||
def selector_bytes(selector: int) -> tuple[int, int]:
|
||||
selector &= 0x01FF
|
||||
if selector <= 0x007F:
|
||||
return 0x00, selector
|
||||
if selector <= 0x017F:
|
||||
return 0x01, selector - 0x0080
|
||||
return 0x02, selector - 0x0180
|
||||
|
||||
|
||||
def frame_checksum(data: bytes) -> int:
|
||||
checksum = CHECKSUM_SEED
|
||||
for value in data[:5]:
|
||||
checksum ^= value
|
||||
return checksum & 0xFF
|
||||
|
||||
|
||||
def parse_int_list(text: str) -> list[int]:
|
||||
values = []
|
||||
for part in text.split(","):
|
||||
item = part.strip()
|
||||
if item:
|
||||
values.append(parse_int(item))
|
||||
return values
|
||||
|
||||
|
||||
def parse_int(text: str) -> int:
|
||||
return int(str(text).strip(), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
167
scripts/build_rom_button_output_sweep.py
Normal file
167
scripts/build_rom_button_output_sweep.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a fresh-boot webcam sweep for ROM-derived panel-output candidates."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CHECKSUM_SEED = 0x5A
|
||||
CONNECT_OK_FRAME = "00 00 00 80 00 DA"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Candidate:
|
||||
label: str
|
||||
selector: int
|
||||
value: int
|
||||
note: str
|
||||
|
||||
|
||||
CANDIDATES: tuple[Candidate, ...] = (
|
||||
Candidate("positive_0013_4000_iris_mblack_link", 0x0013, 0x4000, "known IRIS/M.BLACK LINK lamp positive control"),
|
||||
Candidate("positive_0013_8000_slave", 0x0013, 0x8000, "known SLAVE lamp positive control"),
|
||||
Candidate("positive_0015_8000_call", 0x0015, 0x8000, "known CALL lamp positive control"),
|
||||
Candidate("positive_0017_8000_bars", 0x0017, 0x8000, "known BARS lamp positive control"),
|
||||
Candidate("positive_0110_8000_knee_auto", 0x0110, 0x8000, "known KNEE AUTO positive control"),
|
||||
Candidate("rom_001a_0808_multi_button_default", 0x001A, 0x0808, "F6D3 group default/fallback value"),
|
||||
Candidate("rom_001a_2020_f6d3_bit3_family", 0x001A, 0x2020, "F6D3.3-style packed state candidate"),
|
||||
Candidate("rom_001a_4040_f6d3_bit4_family", 0x001A, 0x4040, "F6D3.4-style packed state candidate"),
|
||||
Candidate("rom_001a_8080_f6d3_bit5_family", 0x001A, 0x8080, "F6D3.5-style packed state candidate"),
|
||||
Candidate("rom_006b_8000_f6d4_bit6_candidate", 0x006B, 0x8000, "F6D4.6 handler report value"),
|
||||
Candidate("rom_0083_0004_f6d0_step_candidate", 0x0083, 0x0004, "F6D0.1 lower-step value candidate"),
|
||||
Candidate("rom_0083_4000_high_tag_candidate", 0x0083, 0x4000, "0x0083 high-bit/tag candidate"),
|
||||
Candidate("rom_0083_2000_high_tag_candidate", 0x0083, 0x2000, "0x0083 high-bit/tag candidate"),
|
||||
Candidate("rom_008f_8000_f6d0_bit7_local", 0x008F, 0x8000, "F6D0.7 local SHUTTER/OTHERS report bit"),
|
||||
Candidate("rom_008f_2000_f6d0_bit6_local", 0x008F, 0x2000, "F6D0.6 local SHUTTER/OTHERS report bit"),
|
||||
Candidate("known_008f_0800_evs_display", 0x008F, 0x0800, "known EVS/shutter display positive control"),
|
||||
Candidate("known_008f_1000_off_display", 0x008F, 0x1000, "known OFF/shutter display positive control"),
|
||||
Candidate("rom_0093_1020_f6dc_bit5_context", 0x0093, 0x1020, "F6DC.5 handler context candidate"),
|
||||
Candidate("rom_0093_4040_f6dc_bit4_context", 0x0093, 0x4040, "F6DC.4 handler context candidate"),
|
||||
Candidate("rom_0093_8040_f6dc_bit3_context", 0x0093, 0x8040, "F6DC.3 handler context candidate"),
|
||||
Candidate("rom_0093_0020_f6dc_bit1_context", 0x0093, 0x0020, "F6DC.1 handler low-field candidate"),
|
||||
Candidate("rom_0093_0040_f6dc_bit0_context", 0x0093, 0x0040, "F6DC.0 handler low-field candidate"),
|
||||
Candidate("rom_009a_0800_iris_auto_candidate", 0x009A, 0x0800, "F6DB.3 IRIS AUTO report candidate"),
|
||||
Candidate("rom_00b7_2000_f6d4_bit0_bundle", 0x00B7, 0x2000, "F6D4.0 bundle selector candidate"),
|
||||
Candidate("rom_00b9_4000_f6dc_bit7_candidate", 0x00B9, 0x4000, "F6DC.7 handler value candidate"),
|
||||
Candidate("rom_00c4_8000_f6d4_bit0_bundle", 0x00C4, 0x8000, "F6D4.0 bundle selector candidate"),
|
||||
Candidate("rom_00c6_8000_f6d4_bit0_bundle", 0x00C6, 0x8000, "F6D4.0 bundle selector candidate"),
|
||||
Candidate("rom_00f8_8000_f6d4_bit1_candidate", 0x00F8, 0x8000, "F6D4.1 handler candidate"),
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = build_arg_parser().parse_args()
|
||||
scenario = build_scenario(args)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(scenario, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"wrote {args.output}")
|
||||
print(f"candidates={len(CANDIDATES)} snapshots={len(CANDIDATES)}")
|
||||
print(f"estimated_hold_time={len(CANDIDATES) * args.listen / 60:.1f}min plus power-cycle/ready time")
|
||||
return 0
|
||||
|
||||
|
||||
def build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
type=Path,
|
||||
default=Path("scenarios/panel-atlas-rom-button-output-candidates-v1.json"),
|
||||
)
|
||||
parser.add_argument("--listen", type=float, default=0.75, help="seconds to listen after each candidate send")
|
||||
parser.add_argument("--ok-listen", type=float, default=0.25, help="seconds to listen after CONNECT OK seeds")
|
||||
parser.add_argument("--drain", type=float, default=0.25, help="seconds to drain after ready")
|
||||
parser.add_argument("--off-seconds", type=float, default=1.5, help="relay power-off time before each candidate")
|
||||
parser.add_argument("--ready-heartbeats", type=int, default=2, help="heartbeats before each candidate")
|
||||
parser.add_argument("--ready-timeout", type=float, default=10.0, help="ready wait timeout")
|
||||
return parser
|
||||
|
||||
|
||||
def build_scenario(args: argparse.Namespace) -> dict[str, object]:
|
||||
steps: list[dict[str, object]] = []
|
||||
for index, candidate in enumerate(CANDIDATES, start=1):
|
||||
steps.extend(candidate_steps(args, index, candidate))
|
||||
steps.append({"action": "listen", "seconds": 0.8})
|
||||
return {
|
||||
"name": "panel-atlas-rom-button-output-candidates-v1",
|
||||
"notes": [
|
||||
"Fresh-boot webcam sweep for ROM-derived button/report output candidates.",
|
||||
"This deliberately skips physical RCP button presses: each candidate sends command 0 directly.",
|
||||
"Each candidate gets its own power-cycle/CONNECT-OK baseline to reduce latch contamination.",
|
||||
"Candidate snapshots only are enabled; setup, CONNECT OK seeds, and clears should not produce webcam images.",
|
||||
"Run with --camera-index 4 --snapshot-delays 0.5 on the current bench.",
|
||||
],
|
||||
"steps": steps,
|
||||
}
|
||||
|
||||
|
||||
def candidate_steps(args: argparse.Namespace, index: int, candidate: Candidate) -> list[dict[str, object]]:
|
||||
label_base = f"case{index:03d}_{candidate.label}"
|
||||
return [
|
||||
{"action": "power_cycle", "off_seconds": args.off_seconds},
|
||||
{
|
||||
"action": "wait_ready",
|
||||
"heartbeats": args.ready_heartbeats,
|
||||
"timeout": args.ready_timeout,
|
||||
"require": True,
|
||||
},
|
||||
{"action": "drain", "seconds": args.drain},
|
||||
send_step(f"{label_base}_ok_seed_1", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
|
||||
send_step(f"{label_base}_ok_seed_2", CONNECT_OK_FRAME, args.ok_listen, snapshot=False),
|
||||
{
|
||||
"action": "note",
|
||||
"message": (
|
||||
f"{label_base}: selector 0x{candidate.selector:04X}=0x{candidate.value:04X}; "
|
||||
f"{candidate.note}"
|
||||
),
|
||||
},
|
||||
send_step(label_base, frame_hex(0x00, candidate.selector, candidate.value), args.listen, snapshot=True),
|
||||
send_step(
|
||||
f"{label_base}_clear",
|
||||
frame_hex(0x00, candidate.selector, 0x0000),
|
||||
0.12,
|
||||
snapshot=False,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def send_step(label: str, frame: str, listen: float, *, snapshot: bool) -> dict[str, object]:
|
||||
step: dict[str, object] = {
|
||||
"action": "send",
|
||||
"label": label,
|
||||
"frame": frame,
|
||||
"listen": listen,
|
||||
}
|
||||
if not snapshot:
|
||||
step["snapshot"] = False
|
||||
return step
|
||||
|
||||
|
||||
def frame_hex(command: int, selector: int, value: int) -> str:
|
||||
selector_hi, selector_lo = selector_bytes(selector)
|
||||
data = bytes([command & 0xFF, selector_hi, selector_lo, (value >> 8) & 0xFF, value & 0xFF])
|
||||
return " ".join(f"{byte:02X}" for byte in data + bytes([frame_checksum(data)]))
|
||||
|
||||
|
||||
def selector_bytes(selector: int) -> tuple[int, int]:
|
||||
selector &= 0x01FF
|
||||
if selector <= 0x007F:
|
||||
return 0x00, selector
|
||||
if selector <= 0x017F:
|
||||
return 0x01, selector - 0x0080
|
||||
return 0x02, selector - 0x0180
|
||||
|
||||
|
||||
def frame_checksum(data: bytes) -> int:
|
||||
value = CHECKSUM_SEED
|
||||
for byte in data[:5]:
|
||||
value ^= byte & 0xFF
|
||||
return value & 0xFF
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
135
scripts/make_panel_sweep_contact_sheets.py
Normal file
135
scripts/make_panel_sweep_contact_sheets.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user