"""Parse timestamps and pairing keys from calibration image filenames.""" from __future__ import annotations import re from pathlib import Path from typing import Optional, Tuple _TS_TOKEN = re.compile(r"ts(\d+)", re.IGNORECASE) _SCAN_TOKEN = re.compile(r"scan(\d{6})", re.IGNORECASE) _IR_SCAN = re.compile(r"^ir_scan_(\d+)", re.IGNORECASE) def _digits_after_prefix(name: str, prefixes: Tuple[str, ...]) -> Optional[str]: lower = name.lower() for prefix in sorted(prefixes, key=len, reverse=True): if lower.startswith(prefix): remainder = lower[len(prefix) :].lstrip("_-.") m = re.match(r"(\d+)", remainder) if m: return m.group(1) return None def parse_timestamp_sec(filename: str) -> Optional[float]: """ Normalize filename timestamps to seconds for time-window pairing. Supports: - lc_ts1634840093_ck.... -> ms since epoch - lc_1778599872850705.bmp -> µs since epoch (16+ digits) - lc_1778599872850.bmp -> ms (13 digits) """ name = Path(filename).name m = _TS_TOKEN.search(name) if m: digits = m.group(1) if len(digits) >= 16: return int(digits) / 1_000_000.0 if len(digits) >= 13: return int(digits) / 1_000.0 return int(digits) / 1_000.0 prefixes = ("lc-ir", "lcir", "lc_ir", "lc", "rc", "rg", "rgb", "ir") digits = _digits_after_prefix(name, prefixes) if digits is None: return None if len(digits) >= 16: return int(digits) / 1_000_000.0 if len(digits) >= 13: return int(digits) / 1_000.0 return float(digits) def parse_pair_key(filename: str) -> Optional[str]: """ Filename key for legacy exact matching (IR scan ids, shared numeric tails). """ name = Path(filename).name lower = name.lower() m = _IR_SCAN.match(lower) if m: return f"scan{int(m.group(1)):06d}" m = _SCAN_TOKEN.search(lower) if m: return m.group(0).lower() m = _TS_TOKEN.search(lower) if m: return f"ts{m.group(1)}" prefixes = ("lc-ir", "lcir", "lc_ir", "lc", "rc", "rg", "rgb", "ir") digits = _digits_after_prefix(lower, prefixes) if digits: return digits return Path(lower).stem