#!/usr/bin/env python3 """Shared helpers for standalone ScanNet self-localization data generation. This module intentionally does not import the existing VSI-590K construction code. Several helpers are copied and lightly adapted from the current QA construction pipeline so this directory can travel as an independent project. """ from __future__ import annotations import base64 import html import json import math import random from dataclasses import dataclass from pathlib import Path from typing import Any try: import numpy as np except ImportError as exc: # pragma: no cover - surfaced as a clear runtime error. raise RuntimeError("numpy is required for whereami data generation") from exc Point2D = tuple[float, float] Point3D = tuple[float, float, float] Matrix = list[list[float]] VisibilityKey = tuple[str, str | None] DISPLAY_FACING_CANDIDATES: tuple[tuple[int, int], ...] = ( (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), ) DEFAULT_MIN_MULTI_CAMERA_DISTANCE_M = 0.5 DEFAULT_MIN_MULTI_YAW_CHANGE_DEG = 20.0 DEFAULT_COHERENT_WINDOW_SIZE = 180 @dataclass(frozen=True) class SceneManifest: scene_id: str dataset: str video_path: Path pose_dir: Path frame_count: int @dataclass(frozen=True) class CoherentClip: keyframe_indices: list[int] dense_frame_indices: list[int] source_span: tuple[int, int] def load_json(path: Path) -> Any: with path.open() as handle: return json.load(handle) def write_json(path: Path, payload: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(json_safe(payload), indent=2, sort_keys=True) + "\n") def json_safe(value: Any) -> Any: if isinstance(value, dict): return {str(key): json_safe(item) for key, item in value.items()} if isinstance(value, (list, tuple, set)): return [json_safe(item) for item in value] if isinstance(value, Path): return str(value) if hasattr(value, "item"): return json_safe(value.item()) if isinstance(value, float): return value if math.isfinite(value) else None return value def load_cv2() -> Any: try: import cv2 # type: ignore except ImportError as exc: # pragma: no cover - depends on environment. raise RuntimeError("OpenCV is required for frame sharpness and extraction") from exc return cv2 def resolve_scene_path(candidate: Any, fallback: Path) -> Path: if candidate: path = Path(candidate) if path.exists(): return path return fallback def load_manifest( manifest_path: Path | None, *, data_root: Path, video_root: Path, scene_ids: list[str] | None = None, limit: int | None = None, frame_category_info: dict[str, Any] | None = None, ) -> list[SceneManifest]: selected_ids = list(dict.fromkeys(scene_ids or [])) rows: list[dict[str, Any]] = [] if manifest_path is not None and manifest_path.exists(): payload = load_json(manifest_path) scenes = payload.get("scenes", payload if isinstance(payload, list) else None) if not isinstance(scenes, list): raise ValueError(f"Expected manifest scenes list at {manifest_path}") for scene in scenes: if selected_ids and str(scene.get("scene_id")) not in selected_ids: continue rows.append(dict(scene)) elif selected_ids: rows = [{"scene_id": scene_id, "dataset": "scannet"} for scene_id in selected_ids] else: rows = [ {"scene_id": path.name, "dataset": "scannet"} for path in sorted(data_root.glob("scene*_*")) if path.is_dir() and (path / "pose").exists() ] manifests: list[SceneManifest] = [] for row in rows: scene_id = str(row["scene_id"]) video_path = resolve_scene_path(row.get("video_path"), video_root / f"{scene_id}.mp4") pose_dir = resolve_scene_path(row.get("pose_dir"), data_root / scene_id / "pose") frame_count = int( row.get("video_frame_count") or row.get("frame_count") or (len(frame_category_info[scene_id]) if frame_category_info and scene_id in frame_category_info else 0) ) if frame_count <= 0 and video_path.exists(): frame_count = probe_video_frame_count(video_path) manifests.append( SceneManifest( scene_id=scene_id, dataset=str(row.get("dataset", "scannet")), video_path=video_path, pose_dir=pose_dir, frame_count=frame_count, ) ) if limit is not None: manifests = manifests[:limit] return manifests def probe_video_frame_count(video_path: Path) -> int: cv2 = load_cv2() cap = cv2.VideoCapture(str(video_path)) try: if not cap.isOpened(): return 0 return int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) finally: cap.release() def load_pose(path: Path) -> Matrix | None: try: rows = [[float(value) for value in line.split()] for line in path.read_text().splitlines() if line.strip()] except OSError: return None if len(rows) != 4 or any(len(row) != 4 for row in rows): return None if any(not math.isfinite(value) for row in rows for value in row): return None return rows def camera_center(pose: Matrix) -> Point3D: return pose[0][3], pose[1][3], pose[2][3] def camera_forward_xy(pose: Matrix) -> Point2D | None: return normalize_2d((pose[0][2], pose[1][2])) def point_xy(point: Point3D) -> Point2D: return point[0], point[1] def distance_2d(a: Point2D, b: Point2D) -> float: return math.hypot(a[0] - b[0], a[1] - b[1]) def dot_2d(a: Point2D, b: Point2D) -> float: return a[0] * b[0] + a[1] * b[1] def normalize_2d(vector: Point2D) -> Point2D | None: norm = math.hypot(vector[0], vector[1]) if norm <= 1e-9: return None return vector[0] / norm, vector[1] / norm def angle_between_2d(a: Point2D, b: Point2D) -> float: dot = max(-1.0, min(1.0, dot_2d(a, b))) cross = a[0] * b[1] - a[1] * b[0] return abs(math.degrees(math.atan2(cross, dot))) def jaccard(a: set[Any], b: set[Any]) -> float: union = a | b if not union: return 0.0 return len(a & b) / len(union) def parse_pixel_value(value: Any) -> int: if hasattr(value, "item"): value = value.item() if isinstance(value, str): value = value.strip() return int(float(value)) def parse_num_pixels(record: dict[str, Any]) -> int: return parse_pixel_value(record.get("num_pixels", 0)) def scalar_list(value: Any) -> list[Any]: if value is None: return [] if hasattr(value, "tolist"): value = value.tolist() if isinstance(value, (list, tuple)): return list(value) return [value] def normalize_instance_id(value: Any) -> str: if hasattr(value, "item"): value = value.item() try: numeric = float(value) except (TypeError, ValueError): return str(value) if math.isfinite(numeric) and numeric.is_integer(): return str(int(numeric)) return str(value) def instance_pixel_entries(category: str, payload: dict[str, Any]) -> list[tuple[VisibilityKey, int]]: inst_ids = scalar_list(payload.get("inst_ids")) inst_pixels = scalar_list(payload.get("inst_num_pixels")) if inst_ids and inst_pixels and len(inst_ids) == len(inst_pixels): return [ ((category, normalize_instance_id(inst_id)), parse_pixel_value(pixel_count)) for inst_id, pixel_count in zip(inst_ids, inst_pixels) ] return [((category, None), parse_num_pixels(payload))] def build_adaptive_visibility_thresholds( frame_category_records: list[dict[str, Any]], *, min_visible_pixels: int, percentile: float, min_retained_frames: int, ) -> dict[VisibilityKey, int]: samples: dict[VisibilityKey, list[int]] = {} for frame_record in frame_category_records: for category, payload in frame_record.items(): if not isinstance(payload, dict): continue for key, pixel_count in instance_pixel_entries(category, payload): if pixel_count > 0: samples.setdefault(key, []).append(pixel_count) thresholds: dict[VisibilityKey, int] = {} for key, pixel_counts in samples.items(): percentile_threshold = int(math.ceil(float(np.percentile(pixel_counts, percentile)))) threshold = max(min_visible_pixels, percentile_threshold) if min_retained_frames > 0: retained_index = min(min_retained_frames, len(pixel_counts)) - 1 top_retained_floor = sorted(pixel_counts, reverse=True)[retained_index] threshold = min(threshold, max(min_visible_pixels, top_retained_floor)) thresholds[key] = threshold return thresholds def visible_instances( frame_record: dict[str, Any], min_visible_pixels: int, visibility_thresholds: dict[VisibilityKey, int] | None = None, ) -> set[VisibilityKey]: visible: set[VisibilityKey] = set() for category, payload in frame_record.items(): if not isinstance(payload, dict): continue for key, pixel_count in instance_pixel_entries(category, payload): threshold = min_visible_pixels if visibility_thresholds is not None: threshold = visibility_thresholds.get(key, min_visible_pixels) if pixel_count >= threshold: visible.add(key) return visible def categories_for_instances(instance_keys: set[VisibilityKey] | list[VisibilityKey]) -> set[str]: return {category for category, _instance_id in instance_keys} def visibility_key_sort_key(key: VisibilityKey) -> tuple[str, str]: category, instance_id = key return category, "" if instance_id is None else str(instance_id) def visibility_id_sort_key(value: str) -> tuple[int, float | str]: try: return (0, float(value)) except ValueError: return (1, value) def visibility_key_label(key: VisibilityKey) -> str: category, instance_id = key return category if instance_id is None else f"{category}#{instance_id}" def visibility_key_records(keys: set[VisibilityKey] | list[VisibilityKey]) -> list[dict[str, str | None]]: return [ {"category": category, "instance_id": instance_id} for category, instance_id in sorted(keys, key=visibility_key_sort_key) ] def frame_sharpness_score(image: Any, cv2_module: Any) -> float: gray = cv2_module.cvtColor(image, cv2_module.COLOR_BGR2GRAY) return float(cv2_module.Laplacian(gray, cv2_module.CV_64F).var()) def compute_selected_frame_sharpness_scores(video_path: Path, frame_indices: list[int]) -> dict[int, float]: if not frame_indices: return {} if not video_path.exists(): raise FileNotFoundError(f"Missing video file for blur-aware frame selection: {video_path}") cv2 = load_cv2() requested = set(frame_indices) max_frame = max(requested) cap = cv2.VideoCapture(str(video_path)) if not cap.isOpened(): raise RuntimeError(f"Could not open video for blur-aware frame selection: {video_path}") scores: dict[int, float] = {} try: frame_idx = 0 while frame_idx <= max_frame: ok, image = cap.read() if not ok or image is None: break if frame_idx in requested: scores[frame_idx] = frame_sharpness_score(image, cv2) frame_idx += 1 finally: cap.release() return scores def export_video_frames( video_path: Path, frame_indices: list[int], output_dir: Path, *, force: bool = False, ) -> list[str]: output_dir.mkdir(parents=True, exist_ok=True) paths = [output_dir / f"image_{index:02d}_frame_{frame_idx:06d}.jpg" for index, frame_idx in enumerate(frame_indices)] if not force and all(path.exists() for path in paths): return [str(path) for path in paths] if not frame_indices: return [] if not video_path.exists(): raise FileNotFoundError(f"Missing video file for frame export: {video_path}") cv2 = load_cv2() requested = {frame_idx: path for frame_idx, path in zip(frame_indices, paths)} cap = cv2.VideoCapture(str(video_path)) if not cap.isOpened(): raise RuntimeError(f"Could not open video for frame export: {video_path}") try: for frame_idx, output_path in requested.items(): if output_path.exists() and not force: continue cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) ok, image = cap.read() if not ok or image is None: raise RuntimeError(f"Could not read frame {frame_idx} from {video_path}") ok = cv2.imwrite(str(output_path), image) if not ok: raise RuntimeError(f"Could not write frame image: {output_path}") finally: cap.release() return [str(path) for path in paths] def export_video_frame_cache( video_path: Path, frame_indices: list[int], output_dir: Path, *, force: bool = False, ) -> list[str]: output_dir.mkdir(parents=True, exist_ok=True) unique_indices = sorted(dict.fromkeys(int(frame_idx) for frame_idx in frame_indices)) path_by_frame = {frame_idx: output_dir / f"frame_{frame_idx:06d}.jpg" for frame_idx in unique_indices} missing = [frame_idx for frame_idx, path in path_by_frame.items() if force or not path.exists()] if missing: if not video_path.exists(): raise FileNotFoundError(f"Missing video file for frame export: {video_path}") cv2 = load_cv2() cap = cv2.VideoCapture(str(video_path)) if not cap.isOpened(): raise RuntimeError(f"Could not open video for frame export: {video_path}") try: for frame_idx in missing: cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) ok, image = cap.read() if not ok or image is None: raise RuntimeError(f"Could not read frame {frame_idx} from {video_path}") ok = cv2.imwrite(str(path_by_frame[frame_idx]), image) if not ok: raise RuntimeError(f"Could not write frame image: {path_by_frame[frame_idx]}") finally: cap.release() return [str(path_by_frame[int(frame_idx)]) for frame_idx in frame_indices] def camera_distance_from_poses(pose_a: Matrix, pose_b: Matrix) -> float: return distance_2d(point_xy(camera_center(pose_a)), point_xy(camera_center(pose_b))) def yaw_change_from_poses(pose_a: Matrix, pose_b: Matrix) -> float | None: forward_a = camera_forward_xy(pose_a) forward_b = camera_forward_xy(pose_b) if forward_a is None or forward_b is None: return None return angle_between_2d(forward_a, forward_b) def frame_passes_previous_viewpoint_change( previous_frame_index: int, frame_index: int, pose_by_frame: dict[int, Matrix], *, min_camera_distance_m: float, min_yaw_change_deg: float, ) -> bool: pose_a = pose_by_frame.get(previous_frame_index) pose_b = pose_by_frame.get(frame_index) if pose_a is None or pose_b is None: return False camera_distance_m = camera_distance_from_poses(pose_a, pose_b) yaw_change_deg = yaw_change_from_poses(pose_a, pose_b) if yaw_change_deg is None: return False return camera_distance_m >= min_camera_distance_m or yaw_change_deg >= min_yaw_change_deg def sample_diverse_pose_path_frames( frame_indices: list[int], count: int, pose_by_frame: dict[int, Matrix], *, min_camera_distance_m: float, min_yaw_change_deg: float, ) -> list[int]: """Greedily keep the earliest eligible frame with enough change from the prior selection.""" candidates = sorted(dict.fromkeys(frame_indices)) if count <= 0 or not candidates: return [] selected = [candidates[0]] for frame_index in candidates[1:]: if frame_passes_previous_viewpoint_change( selected[-1], frame_index, pose_by_frame, min_camera_distance_m=min_camera_distance_m, min_yaw_change_deg=min_yaw_change_deg, ): selected.append(frame_index) if len(selected) >= count: break return selected def build_coherent_clips( *, frame_count: int, usable_frame_indices: list[int], pose_by_frame: dict[int, Matrix], max_multi_images: int, coherent_window_sizes: list[int], coherent_window_stride: int, min_multi_camera_distance_m: float = DEFAULT_MIN_MULTI_CAMERA_DISTANCE_M, min_multi_yaw_change_deg: float = DEFAULT_MIN_MULTI_YAW_CHANGE_DEG, ) -> list[CoherentClip]: usable_set = set(usable_frame_indices) keyframe_count = min(max_multi_images, 5) min_keyframes = 2 clips: list[CoherentClip] = [] for window_size in sorted(set(coherent_window_sizes)): for start in range(0, frame_count, coherent_window_stride): end = min(frame_count - 1, start + window_size) if end <= start: continue dense_indices = [idx for idx in range(start, end + 1) if idx in usable_set] if len(dense_indices) < min_keyframes: continue keyframes = sample_diverse_pose_path_frames( dense_indices, keyframe_count, pose_by_frame, min_camera_distance_m=min_multi_camera_distance_m, min_yaw_change_deg=min_multi_yaw_change_deg, ) if len(keyframes) < min_keyframes: continue clips.append( CoherentClip( keyframe_indices=keyframes, dense_frame_indices=dense_indices, source_span=(keyframes[0], keyframes[-1]), ) ) clips.sort(key=lambda clip: clip.source_span[0]) return clips def frame_spans_overlap(span_a: tuple[int, int], span_b: tuple[int, int]) -> bool: """Return whether two inclusive video-frame intervals share any frame.""" return max(span_a[0], span_b[0]) <= min(span_a[1], span_b[1]) def grid_cell_to_indices(cell: str) -> tuple[int, int]: token = cell.strip() col_token = "" row_token = "" for char in token: if char.isalpha(): col_token += char.upper() elif char.isdigit(): row_token += char if not col_token or not row_token: raise ValueError(f"Invalid grid cell label: {cell!r}") col_index = 0 for char in col_token: col_index = col_index * 26 + (ord(char) - ord("A") + 1) return col_index - 1, int(row_token) - 1 def object_grid_tokens_by_cell(object_grid: dict[str, Any]) -> dict[tuple[int, int], set[str]]: tokens: dict[tuple[int, int], set[str]] = {} for cell, labels in (object_grid.get("cell_labels") or {}).items(): col_index, row_index = grid_cell_to_indices(str(cell)) tokens.setdefault((col_index, row_index), set()).update(str(label) for label in labels) return tokens def object_grid_categories(object_grid: dict[str, Any]) -> set[str]: categories: set[str] = set() for obj in object_grid.get("objects") or []: if obj.get("centroid_cell") is not None: categories.add(str(obj.get("category"))) return categories def object_grid_instance_id(obj: dict[str, Any]) -> str | None: category = obj.get("category") instance_index = obj.get("instance_index") if category is None or instance_index is None: return None return f"{category}#{normalize_instance_id(instance_index)}" def object_grid_instance_ids(object_grid: dict[str, Any], *, require_centroid_cell: bool = False) -> set[str]: instance_ids: set[str] = set() for obj in object_grid.get("objects") or []: if require_centroid_cell and obj.get("centroid_cell") is None: continue instance_id = object_grid_instance_id(obj) if instance_id is not None: instance_ids.add(instance_id) return instance_ids def collect_visibility_ids_by_category(frame_category_records: list[dict[str, Any]]) -> dict[str, set[str]]: ids_by_category: dict[str, set[str]] = {} for frame_record in frame_category_records: if not isinstance(frame_record, dict): continue for category, payload in frame_record.items(): if not isinstance(payload, dict): continue for raw_id in scalar_list(payload.get("inst_ids")): ids_by_category.setdefault(str(category), set()).add(normalize_instance_id(raw_id)) return ids_by_category def build_visibility_instance_mapping( object_grid: dict[str, Any], frame_category_records: list[dict[str, Any]], ) -> tuple[dict[VisibilityKey, str], dict[str, str]]: objects_by_category: dict[str, list[dict[str, Any]]] = {} for obj in object_grid.get("objects") or []: category = obj.get("category") if category is None or obj.get("instance_index") is None: continue objects_by_category.setdefault(str(category), []).append(obj) for rows in objects_by_category.values(): rows.sort(key=lambda obj: visibility_id_sort_key(normalize_instance_id(obj.get("instance_index")))) visibility_ids = collect_visibility_ids_by_category(frame_category_records) mapping: dict[VisibilityKey, str] = {} status: dict[str, str] = {} for category, rows in sorted(objects_by_category.items()): ids = sorted(visibility_ids.get(category, set()), key=visibility_id_sort_key) if not ids: status[category] = "no_visibility_ids" continue if len(ids) != len(rows): status[category] = f"count_mismatch_grid_{len(rows)}_visibility_{len(ids)}" continue for visibility_id, obj in zip(ids, rows): instance_id = object_grid_instance_id(obj) if instance_id is not None: mapping[(category, visibility_id)] = instance_id status[category] = "rank_order_mapped" for category in sorted(set(visibility_ids) - set(objects_by_category)): status[category] = "no_grid_objects" return mapping, status def categories_for_grid_instance_ids(instance_ids: set[str] | list[str]) -> set[str]: return {str(instance_id).split("#", 1)[0] for instance_id in instance_ids} def object_cells_for_categories(object_grid: dict[str, Any], categories: set[str]) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for obj in object_grid.get("objects") or []: category = str(obj.get("category")) cell = obj.get("centroid_cell") if category not in categories or cell is None: continue col_index, row_index = grid_cell_to_indices(str(cell)) rows.append( { "category": category, "label": obj.get("label"), "cell": cell, "display_coord": [col_index + 1, row_index + 1], "object_id": obj.get("object_id"), "instance_index": obj.get("instance_index"), "raw_label": obj.get("raw_label"), } ) rows.sort(key=lambda row: (row["display_coord"][1], row["display_coord"][0], row["category"])) return rows def object_cells_for_instance_ids(object_grid: dict[str, Any], instance_ids: set[str]) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for obj in object_grid.get("objects") or []: instance_id = object_grid_instance_id(obj) cell = obj.get("centroid_cell") if instance_id not in instance_ids or cell is None: continue col_index, row_index = grid_cell_to_indices(str(cell)) category = str(obj.get("category")) rows.append( { "category": category, "label": obj.get("label"), "cell": cell, "display_coord": [col_index + 1, row_index + 1], "object_id": obj.get("object_id"), "instance_index": obj.get("instance_index"), "instance_id": instance_id, "raw_label": obj.get("raw_label"), } ) rows.sort(key=lambda row: (row["display_coord"][1], row["display_coord"][0], row["category"], row["instance_id"])) return rows def displayed_object_records(object_rows: list[dict[str, Any]]) -> list[dict[str, Any]]: records: list[dict[str, Any]] = [] for obj in object_rows: records.append( { "label": obj.get("label"), "category": obj.get("category"), "instance_id": obj.get("instance_id"), "instance_index": obj.get("instance_index"), "object_id": obj.get("object_id"), "cell": obj.get("cell"), "display_coord": obj.get("display_coord"), } ) return records def format_object_coordinate_block(displayed_objects: list[dict[str, Any]] | None) -> str: if not displayed_objects: return "Object Coordinates:\n- None" rows = [] for obj in displayed_objects: coord = obj.get("display_coord") if not isinstance(coord, list) or len(coord) != 2: continue label = str(obj.get("label") or obj.get("category") or "OBJECT") x, y = int(coord[0]), int(coord[1]) rows.append((y, x, label, f"- {label}: ({x}, {y})")) if not rows: return "Object Coordinates:\n- None" return "Object Coordinates:\n" + "\n".join(row[-1] for row in sorted(rows)) def format_object_grid_crop( object_grid: dict[str, Any], *, required_display_coords: list[list[int]], required_categories: set[str], required_instance_ids: set[str] | None = None, grid_size: int, margin: int, ) -> tuple[str, dict[str, Any]]: object_rows: list[dict[str, Any]] if required_instance_ids is None: token_by_cell = object_grid_tokens_by_cell(object_grid) object_rows = object_cells_for_categories(object_grid, required_categories) else: object_rows = object_cells_for_instance_ids(object_grid, required_instance_ids) token_by_cell: dict[tuple[int, int], set[str]] = {} for obj in object_rows: x, y = obj["display_coord"] label = str(obj.get("label") or obj["category"]) token_by_cell.setdefault((x - 1, y - 1), set()).add(label) required_cells: set[tuple[int, int]] = set() for coord in required_display_coords: if len(coord) != 2: continue x, y = int(coord[0]), int(coord[1]) required_cells.add((x - 1, y - 1)) for obj in object_rows: x, y = obj["display_coord"] required_cells.add((x - 1, y - 1)) if not required_cells: required_cells = set(token_by_cell) if not required_cells: required_cells = {(grid_size // 2, grid_size // 2)} min_col = max(0, min(col for col, _row in required_cells) - margin) max_col = min(grid_size - 1, max(col for col, _row in required_cells) + margin) min_row = max(0, min(row for _col, row in required_cells) - margin) max_row = min(grid_size - 1, max(row for _col, row in required_cells) + margin) def cell_token(col_index: int, row_index: int) -> str: object_tokens = sorted(token_by_cell.get((col_index, row_index), [])) return "+".join(object_tokens) if object_tokens else "." columns = [str(col_index + 1) for col_index in range(min_col, max_col + 1)] row_labels = [f"{row_index + 1:02d}" for row_index in range(min_row, max_row + 1)] tokens = [ cell_token(col_index, row_index) for row_index in range(min_row, max_row + 1) for col_index in range(min_col, max_col + 1) ] cell_width = max(len(token) for token in tokens + columns + ["."]) row_label_width = max(len(label) for label in row_labels) lines = [" " * (row_label_width + 2) + " ".join(column.rjust(cell_width) for column in columns)] for row_index, row_label in zip(range(min_row, max_row + 1), row_labels): row_tokens = [ cell_token(col_index, row_index).rjust(cell_width) for col_index in range(min_col, max_col + 1) ] lines.append(f"{row_label.rjust(row_label_width)} " + " ".join(row_tokens)) crop = { "min_col": min_col, "max_col": max_col, "min_row": min_row, "max_row": max_row, "display_columns": [min_col + 1, max_col + 1], "display_rows": [min_row + 1, max_row + 1], "required_categories": sorted(required_categories), "required_display_coords": required_display_coords, } if required_instance_ids is not None: crop["required_instance_ids"] = sorted(required_instance_ids) crop["displayed_instance_ids"] = sorted( str(obj["instance_id"]) for obj in object_rows if obj.get("instance_id") is not None ) crop["displayed_objects"] = displayed_object_records(object_rows) return "\n".join(lines), crop def valid_facing_vector(value: Any) -> bool: return isinstance(value, list) and len(value) == 2 and tuple(value) in DISPLAY_FACING_CANDIDATES def quantize_display_facing_vector(dx: float, dy: float) -> tuple[tuple[int, int], float, float] | None: norm = math.hypot(dx, dy) if norm <= 1e-9: return None unit_x = dx / norm unit_y = dy / norm best = max( DISPLAY_FACING_CANDIDATES, key=lambda item: (unit_x * item[0] + unit_y * item[1]) / math.hypot(item[0], item[1]), ) cos_angle = max(-1.0, min(1.0, (unit_x * best[0] + unit_y * best[1]) / math.hypot(best[0], best[1]))) angle_to_center = abs(math.degrees(math.acos(cos_angle))) margin = 22.5 - angle_to_center return best, angle_to_center, margin def facing_margin_from_camera_row(camera_row: dict[str, Any]) -> dict[str, Any] | None: forward = camera_row.get("forward_topdown") if not isinstance(forward, dict): return None try: forward_x = float(forward["x_m"]) forward_z = float(forward["z_m"]) except (KeyError, TypeError, ValueError): return None result = quantize_display_facing_vector(forward_x, -forward_z) if result is None: return None vector, angle_to_center, margin = result return { "computed_facing_vector": list(vector), "angle_to_bin_center_deg": angle_to_center, "facing_bin_margin_deg": margin, } def cell_margin_from_camera_row(camera_row: dict[str, Any], *, grid_size: int, cell_size_m: float) -> dict[str, Any] | None: coord = camera_row.get("display_coord") center = camera_row.get("center_topdown_m") if not (isinstance(coord, list) and len(coord) == 2 and isinstance(center, dict)): return None try: display_x = int(coord[0]) display_y = int(coord[1]) x_m = float(center["x_m"]) z_m = float(center["z_m"]) except (KeyError, TypeError, ValueError): return None col_index = display_x - 1 row_index = display_y - 1 origin_col = grid_size // 2 origin_row = grid_size // 2 cell_center_x = (col_index - origin_col) * cell_size_m cell_center_z = (origin_row - row_index) * cell_size_m dx = x_m - cell_center_x dz = z_m - cell_center_z max_axis_offset = max(abs(dx), abs(dz)) return { "cell_center_offset_m": math.hypot(dx, dz), "cell_center_axis_offset_m": max_axis_offset, "cell_boundary_margin_m": (0.5 * cell_size_m) - max_axis_offset, "cell_center_topdown_m": {"x_m": cell_center_x, "z_m": cell_center_z}, "camera_center_offset_from_cell_center_m": {"x_m": dx, "z_m": dz}, } def line_count(path: Path) -> int: with path.open() as handle: return sum(1 for _ in handle) def html_escape_text(text: Any) -> str: return html.escape("" if text is None else str(text)) def image_to_data_uri(path: Path, *, max_width: int = 360) -> str | None: if not path.exists(): return None try: from PIL import Image image = Image.open(path).convert("RGB") if image.width > max_width: ratio = max_width / float(image.width) image = image.resize((max_width, max(1, int(image.height * ratio)))) import io buffer = io.BytesIO() image.save(buffer, format="JPEG", quality=82) encoded = base64.b64encode(buffer.getvalue()).decode("ascii") except Exception: encoded = base64.b64encode(path.read_bytes()).decode("ascii") return f"data:image/jpeg;base64,{encoded}" def rng_sample_stable(items: list[Any], limit: int | None, rng: random.Random) -> list[Any]: if limit is None or limit <= 0 or len(items) <= limit: return list(items) indices = sorted(rng.sample(range(len(items)), limit)) return [items[index] for index in indices]