feat(lab): complete E30 evidence review gate
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Issue an immutable Codex engineering generation for LAB E30.
|
||||
|
||||
The decisions in this file are the compact, reproducible encoding of a visual
|
||||
audit of all 42 camera-backed engineering sheets (486 immutable E30 items).
|
||||
They are explicitly AI-assisted engineering judgments, not human annotation
|
||||
and not navigation/safety acceptance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute.e30_engineering_generation import (
|
||||
E30_ENGINEERING_DECISION_SCHEMA,
|
||||
build_e30_engineering_generation,
|
||||
)
|
||||
|
||||
SheetKey = tuple[str, int]
|
||||
|
||||
# Boxes placed on road/construction barriers, façades, signs, the recording
|
||||
# rig, or other non-object image structure.
|
||||
FALSE_POSITIVES: Final[frozenset[SheetKey]] = frozenset(
|
||||
{
|
||||
("agree-02.jpg", 2),
|
||||
("agree-03.jpg", 5),
|
||||
("agree-05.jpg", 5),
|
||||
("agree-05.jpg", 6),
|
||||
("agree-05.jpg", 7),
|
||||
("agree-05.jpg", 8),
|
||||
("agree-07.jpg", 4),
|
||||
("agree-07.jpg", 6),
|
||||
("agree-07.jpg", 12),
|
||||
("camera-only-01.jpg", 9),
|
||||
("camera-only-01.jpg", 10),
|
||||
# Frame 162: the bus box is on the façade/background; the real truck is
|
||||
# outside the box and therefore does not rescue this observation.
|
||||
("camera-only-01.jpg", 4),
|
||||
("camera-only-03.jpg", 6),
|
||||
("camera-only-03.jpg", 7),
|
||||
("camera-only-03.jpg", 8),
|
||||
("camera-only-04.jpg", 1),
|
||||
("camera-only-04.jpg", 2),
|
||||
("camera-only-04.jpg", 3),
|
||||
("camera-only-05.jpg", 11),
|
||||
("camera-only-05.jpg", 12),
|
||||
("camera-only-06.jpg", 1),
|
||||
("camera-only-06.jpg", 2),
|
||||
("camera-only-06.jpg", 3),
|
||||
("camera-only-06.jpg", 4),
|
||||
("camera-only-08.jpg", 11),
|
||||
("camera-only-09.jpg", 3),
|
||||
("camera-only-09.jpg", 4),
|
||||
("camera-only-10.jpg", 3),
|
||||
("camera-only-10.jpg", 4),
|
||||
("unknown-02.jpg", 1),
|
||||
("unknown-02.jpg", 2),
|
||||
("unknown-02.jpg", 3),
|
||||
("unknown-04.jpg", 9),
|
||||
}
|
||||
)
|
||||
|
||||
SELF_DETECTIONS: Final[frozenset[SheetKey]] = frozenset(
|
||||
{
|
||||
("camera-only-01.jpg", 8),
|
||||
("camera-only-02.jpg", 1),
|
||||
("camera-only-06.jpg", 10),
|
||||
("camera-only-07.jpg", 4),
|
||||
("camera-only-08.jpg", 4),
|
||||
("camera-only-09.jpg", 7),
|
||||
("camera-only-09.jpg", 9),
|
||||
("camera-only-10.jpg", 6),
|
||||
("geometry-only-01.jpg", 1),
|
||||
("geometry-only-01.jpg", 2),
|
||||
}
|
||||
)
|
||||
|
||||
# The broad semantic group remains usable, but the fine detector label does
|
||||
# not match the visible object (typically car/truck/bus or stroller/motorcycle).
|
||||
CLASS_MISMATCHES: Final[frozenset[SheetKey]] = frozenset(
|
||||
{
|
||||
("agree-01.jpg", 4),
|
||||
("agree-01.jpg", 5),
|
||||
("agree-03.jpg", 7),
|
||||
("agree-03.jpg", 9),
|
||||
("agree-03.jpg", 11),
|
||||
("agree-04.jpg", 5),
|
||||
("agree-05.jpg", 2),
|
||||
("agree-05.jpg", 4),
|
||||
("agree-05.jpg", 11),
|
||||
("agree-06.jpg", 1),
|
||||
("agree-06.jpg", 2),
|
||||
("agree-06.jpg", 3),
|
||||
("agree-06.jpg", 12),
|
||||
("agree-08.jpg", 3),
|
||||
("agree-08.jpg", 4),
|
||||
("agree-08.jpg", 6),
|
||||
("camera-only-04.jpg", 8),
|
||||
("camera-only-08.jpg", 12),
|
||||
}
|
||||
)
|
||||
|
||||
# Class-bearing objects visible in the camera projection but represented by
|
||||
# E29 only as geometry-only clusters.
|
||||
MISSED_OBJECTS: Final[frozenset[SheetKey]] = frozenset(
|
||||
{
|
||||
("geometry-only-01.jpg", 6),
|
||||
("geometry-only-01.jpg", 7),
|
||||
("geometry-only-01.jpg", 8),
|
||||
("geometry-only-01.jpg", 9),
|
||||
("geometry-only-01.jpg", 12),
|
||||
("geometry-only-02.jpg", 11),
|
||||
("geometry-only-02.jpg", 12),
|
||||
("geometry-only-03.jpg", 1),
|
||||
("geometry-only-03.jpg", 9),
|
||||
("geometry-only-04.jpg", 5),
|
||||
("geometry-only-05.jpg", 8),
|
||||
("geometry-only-07.jpg", 4),
|
||||
("geometry-only-08.jpg", 8),
|
||||
("geometry-only-08.jpg", 10),
|
||||
("geometry-only-08.jpg", 11),
|
||||
("geometry-only-08.jpg", 12),
|
||||
("geometry-only-10.jpg", 1),
|
||||
("geometry-only-10.jpg", 7),
|
||||
("geometry-only-10.jpg", 9),
|
||||
("geometry-only-10.jpg", 10),
|
||||
("geometry-only-11.jpg", 7),
|
||||
}
|
||||
)
|
||||
|
||||
# These two cases remain genuinely ambiguous after the camera-backed audit.
|
||||
# The three semantic cases from generation v1 are now handled automatically:
|
||||
# one detector false positive, one camera-only object, and one stale track.
|
||||
AMBIGUITIES: Final[frozenset[SheetKey]] = frozenset(
|
||||
{
|
||||
("geometry-only-07.jpg", 6),
|
||||
("geometry-only-10.jpg", 11),
|
||||
}
|
||||
)
|
||||
|
||||
EXCEPTION_REVIEW: Final[dict[SheetKey, dict[str, object]]] = {
|
||||
("geometry-only-07.jpg", 6): {
|
||||
"question": "Белый кластер — самостоятельное физическое препятствие?",
|
||||
"focus": (
|
||||
"Кадр 2622: проверьте четыре выбранные белые точки. Нужно отличить "
|
||||
"занятую геометрию реального объекта от поверхности сцены или шума."
|
||||
),
|
||||
"effects": {
|
||||
"object-present": (
|
||||
"Сохранить кластер как подтверждённую занятую геометрию."
|
||||
),
|
||||
"background-or-noise": (
|
||||
"Исключить кластер из положительной LiDAR-поддержки и учесть "
|
||||
"как leakage или шум."
|
||||
),
|
||||
"insufficient-evidence": (
|
||||
"Оставить кейс неизвестным и не использовать его для настройки "
|
||||
"порогов."
|
||||
),
|
||||
},
|
||||
},
|
||||
("geometry-only-10.jpg", 11): {
|
||||
"question": "Вертикальный белый кластер занимает реальное препятствие?",
|
||||
"focus": (
|
||||
"Кадр 4147: проверьте 31 выбранную белую точку на растительности. "
|
||||
"Нужно подтвердить физически занятую область либо фон/шум."
|
||||
),
|
||||
"effects": {
|
||||
"object-present": (
|
||||
"Сохранить кластер как подтверждённую занятую геометрию."
|
||||
),
|
||||
"background-or-noise": (
|
||||
"Исключить кластер из положительной LiDAR-поддержки и учесть "
|
||||
"как leakage или шум."
|
||||
),
|
||||
"insufficient-evidence": (
|
||||
"Оставить кейс неизвестным и не использовать его для настройки "
|
||||
"порогов."
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class IssueGenerationError(RuntimeError):
|
||||
"""The frozen review inputs do not match the audited sheet set."""
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(value, dict):
|
||||
raise IssueGenerationError(f"JSON object required: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
values: list[dict[str, Any]] = []
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line in stream:
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise IssueGenerationError(f"JSONL object required: {path}")
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
|
||||
def _sheet_bindings(
|
||||
root: Path,
|
||||
) -> tuple[dict[str, SheetKey], dict[str, dict[str, object]]]:
|
||||
manifest = _read_json(root / "manifest.json")
|
||||
keys_by_item: dict[str, SheetKey] = {}
|
||||
sheets_by_item: dict[str, dict[str, object]] = {}
|
||||
for sheet in manifest["sheets"]:
|
||||
for entry in sheet["entries"]:
|
||||
item_id = str(entry["item_id"])
|
||||
keys_by_item[item_id] = (str(sheet["path"]), int(entry["ordinal"]))
|
||||
sheets_by_item[item_id] = {
|
||||
"path": sheet["path"],
|
||||
"sha256": sheet["sha256"],
|
||||
"ordinal": entry["ordinal"],
|
||||
}
|
||||
return keys_by_item, sheets_by_item
|
||||
|
||||
|
||||
def _projected_selected_count(materialization_root: Path, item: dict[str, Any]) -> int:
|
||||
with np.load(materialization_root / item["artifact"]["path"]) as arrays:
|
||||
return int(np.count_nonzero(arrays["projected_selected_mask"]))
|
||||
|
||||
|
||||
def _baseline(
|
||||
*,
|
||||
item: dict[str, Any],
|
||||
materialization_root: Path,
|
||||
) -> dict[str, object]:
|
||||
stratum = str(item["stratum"])
|
||||
if stratum == "conflict":
|
||||
return {
|
||||
"verdict": "corrected",
|
||||
"effective_stratum": None,
|
||||
"detector_assessment": "false-positive",
|
||||
"projection_assessment": "aligned",
|
||||
"point_ownership": "surface-or-background",
|
||||
"cause_code": "detector_error",
|
||||
"confidence": 0.97,
|
||||
"note": (
|
||||
"В рамке находится полосатое дорожное/строительное ограждение, "
|
||||
"а не транспорт. Проекция согласована с изображением; LiDAR "
|
||||
"принадлежит поверхности ограждения."
|
||||
),
|
||||
}
|
||||
if stratum == "agree":
|
||||
return {
|
||||
"verdict": "confirmed",
|
||||
"effective_stratum": stratum,
|
||||
"detector_assessment": "valid",
|
||||
"projection_assessment": "aligned",
|
||||
"point_ownership": "object",
|
||||
"cause_code": None,
|
||||
"confidence": 0.94,
|
||||
"note": (
|
||||
"Камерный объект и занятые LiDAR-точки пространственно "
|
||||
"согласованы; поддержка принадлежит наблюдаемому объекту."
|
||||
),
|
||||
}
|
||||
if stratum == "camera-only":
|
||||
return {
|
||||
"verdict": "confirmed",
|
||||
"effective_stratum": stratum,
|
||||
"detector_assessment": "valid",
|
||||
"projection_assessment": "aligned",
|
||||
"point_ownership": "insufficient-support",
|
||||
"cause_code": "sparse_support",
|
||||
"confidence": 0.82,
|
||||
"note": (
|
||||
"Объект читается в камере, но в его рамке недостаточно "
|
||||
"квалифицированной LiDAR-поддержки; глобальная регистрация "
|
||||
"проекции визуально согласована."
|
||||
),
|
||||
}
|
||||
if stratum == "unknown":
|
||||
return {
|
||||
"verdict": "confirmed",
|
||||
"effective_stratum": stratum,
|
||||
"detector_assessment": "valid",
|
||||
"projection_assessment": "not-assessable",
|
||||
"point_ownership": "insufficient-evidence",
|
||||
"cause_code": "time_mismatch",
|
||||
"confidence": 0.9,
|
||||
"note": (
|
||||
"E29 использует удержанный world-track, а не актуальное "
|
||||
"семантическое наблюдение этого кадра. Неизвестность "
|
||||
"обусловлена временной свежестью, не геометрическим порогом."
|
||||
),
|
||||
}
|
||||
if stratum == "geometry-only":
|
||||
projected = _projected_selected_count(materialization_root, item)
|
||||
return {
|
||||
"verdict": "confirmed",
|
||||
"effective_stratum": stratum,
|
||||
"detector_assessment": "not-applicable",
|
||||
"projection_assessment": "aligned" if projected else "not-assessable",
|
||||
"point_ownership": "static-environment",
|
||||
"cause_code": None,
|
||||
"confidence": 0.8,
|
||||
"note": (
|
||||
"Кластер соответствует статической сцене: фасаду, растительности "
|
||||
"или иной геометрии без обязательного семантического объекта."
|
||||
),
|
||||
}
|
||||
raise IssueGenerationError(f"unexpected stratum: {stratum}")
|
||||
|
||||
|
||||
def _decision(
|
||||
*,
|
||||
item: dict[str, Any],
|
||||
key: SheetKey,
|
||||
sheet: dict[str, object],
|
||||
materialization_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
result = _baseline(item=item, materialization_root=materialization_root)
|
||||
source = str(item["stratum"])
|
||||
if key in AMBIGUITIES:
|
||||
result.update(
|
||||
verdict="insufficient-evidence",
|
||||
effective_stratum=None,
|
||||
detector_assessment="insufficient-evidence",
|
||||
projection_assessment="not-assessable",
|
||||
point_ownership="insufficient-evidence",
|
||||
cause_code="unknown",
|
||||
confidence=0.48,
|
||||
note=(
|
||||
"Камерный crop и спроецированная принадлежность не дают "
|
||||
"устойчиво отделить объект от конструкции/края сцены. "
|
||||
"Оставлено единственным типом ручного исключения."
|
||||
),
|
||||
)
|
||||
elif key in SELF_DETECTIONS:
|
||||
result.update(
|
||||
verdict="corrected",
|
||||
effective_stratum=None,
|
||||
detector_assessment=(
|
||||
"false-positive"
|
||||
if source != "geometry-only"
|
||||
else "not-applicable"
|
||||
),
|
||||
projection_assessment="aligned",
|
||||
point_ownership="self",
|
||||
cause_code="self_points",
|
||||
confidence=0.96,
|
||||
note=(
|
||||
"Выделение принадлежит элементам собственной платформы/оператора "
|
||||
"в нижней или краевой части кадра, а не внешнему объекту сцены."
|
||||
),
|
||||
)
|
||||
elif source == "conflict" or key in FALSE_POSITIVES:
|
||||
result.update(
|
||||
verdict="corrected",
|
||||
effective_stratum="geometry-only" if source == "agree" else None,
|
||||
detector_assessment="false-positive",
|
||||
projection_assessment="aligned",
|
||||
point_ownership="surface-or-background",
|
||||
cause_code="detector_error",
|
||||
confidence=0.95,
|
||||
note=(
|
||||
"Детектор поставил объектную рамку на ограждение, знак, фасад "
|
||||
"или иной фон. Проекция камеры и LiDAR согласована; ошибка "
|
||||
"локализована в семантической детекции."
|
||||
),
|
||||
)
|
||||
elif key in MISSED_OBJECTS:
|
||||
result.update(
|
||||
verdict="confirmed",
|
||||
effective_stratum=source,
|
||||
detector_assessment="missed-object",
|
||||
projection_assessment="aligned",
|
||||
point_ownership="object",
|
||||
cause_code="detector_error",
|
||||
confidence=0.88,
|
||||
note=(
|
||||
"Geometry-only кластер пространственно совпадает с различимым "
|
||||
"в камере человеком или транспортом. Геометрия корректна, "
|
||||
"но семантический объект пропущен."
|
||||
),
|
||||
)
|
||||
elif key in CLASS_MISMATCHES:
|
||||
result.update(
|
||||
detector_assessment="class-mismatch",
|
||||
cause_code="detector_error",
|
||||
confidence=0.86,
|
||||
note=(
|
||||
"Объектная поддержка корректна на уровне широкой группы, но "
|
||||
"видимый подтип не совпадает с fine-label детектора."
|
||||
),
|
||||
)
|
||||
|
||||
snapshot = item.get("e29_snapshot")
|
||||
label = (
|
||||
snapshot.get("label") or snapshot.get("semantic_class")
|
||||
if isinstance(snapshot, dict)
|
||||
else None
|
||||
)
|
||||
frame = item["evidence_binding"]["source_frame_index"]
|
||||
exception = key in AMBIGUITIES
|
||||
return {
|
||||
"schema_version": E30_ENGINEERING_DECISION_SCHEMA,
|
||||
"sequence": item["sequence"],
|
||||
"item_id": item["item_id"],
|
||||
"review_key": item["review_key"],
|
||||
"source_stratum": source,
|
||||
"verdict": result["verdict"],
|
||||
"effective_stratum": result["effective_stratum"],
|
||||
"detector_assessment": result["detector_assessment"],
|
||||
"projection_assessment": result["projection_assessment"],
|
||||
"point_ownership": result["point_ownership"],
|
||||
"cause_code": result["cause_code"],
|
||||
"confidence": result["confidence"],
|
||||
"human_exception_required": exception,
|
||||
"exception_reason": "ambiguity" if exception else None,
|
||||
"review_prompt": EXCEPTION_REVIEW.get(key),
|
||||
"evidence_note": (
|
||||
f"Кадр {frame}; label={label or 'geometry-cluster'}. {result['note']}"
|
||||
),
|
||||
"review_sheet": sheet,
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--materialization-root", type=Path, required=True)
|
||||
parser.add_argument("--review-pack-root", type=Path, required=True)
|
||||
parser.add_argument("--review-sheets-root", type=Path, required=True)
|
||||
parser.add_argument("--decisions-path", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
materialization_root = args.materialization_root.resolve()
|
||||
items = _read_jsonl(materialization_root / "materialized-items.jsonl")
|
||||
keys_by_item, sheets_by_item = _sheet_bindings(
|
||||
args.review_sheets_root.resolve()
|
||||
)
|
||||
all_audited_keys = set(keys_by_item.values())
|
||||
declared_sets = (
|
||||
FALSE_POSITIVES,
|
||||
SELF_DETECTIONS,
|
||||
CLASS_MISMATCHES,
|
||||
MISSED_OBJECTS,
|
||||
AMBIGUITIES,
|
||||
)
|
||||
for index, values in enumerate(declared_sets):
|
||||
if not values <= all_audited_keys:
|
||||
raise IssueGenerationError(
|
||||
f"annotation set {index} references a missing sheet tile"
|
||||
)
|
||||
for other in declared_sets[index + 1 :]:
|
||||
overlap = values & other
|
||||
if overlap:
|
||||
raise IssueGenerationError(
|
||||
f"annotation categories overlap: {sorted(overlap)}"
|
||||
)
|
||||
decisions = [
|
||||
_decision(
|
||||
item=item,
|
||||
key=keys_by_item[str(item["item_id"])],
|
||||
sheet=sheets_by_item[str(item["item_id"])],
|
||||
materialization_root=materialization_root,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
args.decisions_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.decisions_path.open("w", encoding="utf-8") as stream:
|
||||
for decision in decisions:
|
||||
stream.write(
|
||||
json.dumps(
|
||||
decision,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
manifest = build_e30_engineering_generation(
|
||||
materialization_root=materialization_root,
|
||||
review_pack_root=args.review_pack_root.resolve(),
|
||||
review_sheets_root=args.review_sheets_root.resolve(),
|
||||
decisions_path=args.decisions_path.resolve(),
|
||||
output_root=args.output_root.resolve(),
|
||||
producer_id="codex:a3-engineering-review",
|
||||
method_id="camera-lidar-42-sheet-audit/v2",
|
||||
)
|
||||
print(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render reproducible camera-backed A3 engineering review sheets.
|
||||
|
||||
The sheets are presentation derivatives only. Every tile is bound to one
|
||||
immutable E30 materialization item and contains:
|
||||
|
||||
* the exact camera frame;
|
||||
* the complete bounded LiDAR projection;
|
||||
* candidate/selected point ownership;
|
||||
* the semantic bbox or a geometry-derived region of interest;
|
||||
* the source metrics required for engineering adjudication.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import colorsys
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
MATERIALIZATION_SCHEMA: Final = "missioncore.e30-evidence-materialization/v2"
|
||||
ITEM_SCHEMA: Final = "missioncore.e30-evidence-materialization-item/v2"
|
||||
SHEET_MANIFEST_SCHEMA: Final = "missioncore.e30-engineering-review-sheets/v1"
|
||||
STRATA: Final = (
|
||||
"conflict",
|
||||
"agree",
|
||||
"camera-only",
|
||||
"unknown",
|
||||
"geometry-only",
|
||||
)
|
||||
|
||||
TILE_WIDTH: Final = 480
|
||||
TILE_HEIGHT: Final = 300
|
||||
FULL_WIDTH: Final = 320
|
||||
FULL_HEIGHT: Final = 240
|
||||
ROI_WIDTH: Final = 160
|
||||
ROI_HEIGHT: Final = 160
|
||||
SHEET_COLUMNS: Final = 4
|
||||
SHEET_ROWS: Final = 3
|
||||
ITEMS_PER_SHEET: Final = SHEET_COLUMNS * SHEET_ROWS
|
||||
|
||||
BACKGROUND: Final = (10, 10, 11)
|
||||
PANEL: Final = (22, 22, 24)
|
||||
TEXT: Final = (242, 242, 239)
|
||||
MUTED: Final = (155, 155, 150)
|
||||
WARNING: Final = (255, 209, 102)
|
||||
CONFLICT: Final = (255, 98, 92)
|
||||
SELECTED: Final = (255, 255, 255)
|
||||
|
||||
|
||||
class ReviewSheetError(RuntimeError):
|
||||
"""The source evidence cannot produce trustworthy review sheets."""
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReviewSheetError(f"missing regular JSON: {path}")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise ReviewSheetError(f"invalid JSON: {path}") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReviewSheetError(f"JSON must contain an object: {path}")
|
||||
return value
|
||||
|
||||
|
||||
def _read_items(path: Path) -> list[dict[str, Any]]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ReviewSheetError("materialized item index is unavailable")
|
||||
values: list[dict[str, Any]] = []
|
||||
with path.open("r", encoding="utf-8") as stream:
|
||||
for line_number, line in enumerate(stream, start=1):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ReviewSheetError(
|
||||
f"invalid materialized item at line {line_number}"
|
||||
) from exc
|
||||
if not isinstance(value, dict):
|
||||
raise ReviewSheetError(
|
||||
f"materialized item {line_number} is not an object"
|
||||
)
|
||||
values.append(value)
|
||||
return values
|
||||
|
||||
|
||||
def _font(size: int, *, bold: bool = False) -> ImageFont.ImageFont:
|
||||
candidates = (
|
||||
"/System/Library/Fonts/SFNSMono.ttf",
|
||||
"/System/Library/Fonts/SFNS.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial Bold.ttf"
|
||||
if bold
|
||||
else "/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
return ImageFont.truetype(candidate, size=size)
|
||||
except OSError:
|
||||
continue
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
FONT_SMALL: Final = _font(11)
|
||||
FONT_META: Final = _font(12)
|
||||
FONT_BOLD: Final = _font(13, bold=True)
|
||||
|
||||
|
||||
def _depth_color(depth: float, minimum: float, maximum: float) -> tuple[int, int, int]:
|
||||
span = max(maximum - minimum, 0.001)
|
||||
position = min(1.0, max(0.0, (depth - minimum) / span))
|
||||
hue = (220.0 - position * 205.0) / 360.0
|
||||
red, green, blue = colorsys.hls_to_rgb(hue, 0.62, 0.88)
|
||||
return round(red * 255), round(green * 255), round(blue * 255)
|
||||
|
||||
|
||||
def _verify_item(
|
||||
*,
|
||||
root: Path,
|
||||
item: dict[str, Any],
|
||||
) -> tuple[Path, Path]:
|
||||
if item.get("schema_version") != ITEM_SCHEMA:
|
||||
raise ReviewSheetError("materialized item schema differs")
|
||||
artifact = item.get("artifact")
|
||||
camera = item.get("camera_frame")
|
||||
if not isinstance(artifact, dict) or not isinstance(camera, dict):
|
||||
raise ReviewSheetError("item artifacts are incomplete")
|
||||
item_path = root / str(artifact.get("path"))
|
||||
frame_path = root / str(camera.get("path"))
|
||||
for path, metadata in ((item_path, artifact), (frame_path, camera)):
|
||||
if (
|
||||
path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != metadata.get("byte_length")
|
||||
or _sha256(path) != metadata.get("sha256")
|
||||
):
|
||||
raise ReviewSheetError(f"artifact changed: {path}")
|
||||
return item_path, frame_path
|
||||
|
||||
|
||||
def _draw_projection(
|
||||
*,
|
||||
frame: Image.Image,
|
||||
arrays: Any,
|
||||
item: dict[str, Any],
|
||||
) -> Image.Image:
|
||||
image = frame.convert("RGB")
|
||||
draw = ImageDraw.Draw(image)
|
||||
pixels = np.asarray(arrays["projected_pixels_xy"], dtype=np.float64)
|
||||
depths = np.asarray(arrays["projected_depth_m"], dtype=np.float64)
|
||||
candidates = np.asarray(arrays["projected_candidate_mask"], dtype=np.uint8)
|
||||
selected = np.asarray(arrays["projected_selected_mask"], dtype=np.uint8)
|
||||
finite_depths = depths[np.isfinite(depths)]
|
||||
minimum = float(finite_depths.min()) if finite_depths.size else 0.0
|
||||
maximum = float(finite_depths.max()) if finite_depths.size else 1.0
|
||||
for index, (x, y) in enumerate(pixels):
|
||||
if not math.isfinite(float(x)) or not math.isfinite(float(y)):
|
||||
continue
|
||||
if x < 0 or y < 0 or x >= image.width or y >= image.height:
|
||||
continue
|
||||
if selected[index] == 1:
|
||||
color, radius = SELECTED, 4
|
||||
elif candidates[index] == 1:
|
||||
color, radius = WARNING, 3
|
||||
else:
|
||||
color, radius = _depth_color(float(depths[index]), minimum, maximum), 1
|
||||
draw.ellipse(
|
||||
(
|
||||
round(x) - radius,
|
||||
round(y) - radius,
|
||||
round(x) + radius,
|
||||
round(y) + radius,
|
||||
),
|
||||
fill=color,
|
||||
)
|
||||
snapshot = item.get("e29_snapshot")
|
||||
bbox = snapshot.get("bbox_xyxy") if isinstance(snapshot, dict) else None
|
||||
if (
|
||||
isinstance(bbox, list)
|
||||
and len(bbox) == 4
|
||||
and all(isinstance(value, (int, float)) for value in bbox)
|
||||
):
|
||||
draw.rectangle(
|
||||
tuple(round(float(value)) for value in bbox),
|
||||
outline=CONFLICT if item.get("stratum") == "conflict" else SELECTED,
|
||||
width=3,
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
def _expanded_roi(
|
||||
*,
|
||||
item: dict[str, Any],
|
||||
arrays: Any,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> tuple[int, int, int, int]:
|
||||
snapshot = item.get("e29_snapshot")
|
||||
bbox = snapshot.get("bbox_xyxy") if isinstance(snapshot, dict) else None
|
||||
if (
|
||||
isinstance(bbox, list)
|
||||
and len(bbox) == 4
|
||||
and all(isinstance(value, (int, float)) for value in bbox)
|
||||
):
|
||||
x1, y1, x2, y2 = (float(value) for value in bbox)
|
||||
else:
|
||||
pixels = np.asarray(arrays["projected_pixels_xy"], dtype=np.float64)
|
||||
selected = np.asarray(arrays["projected_selected_mask"], dtype=np.uint8) == 1
|
||||
candidate = np.asarray(arrays["projected_candidate_mask"], dtype=np.uint8) == 1
|
||||
owned = pixels[selected | candidate]
|
||||
if owned.size == 0:
|
||||
return 0, 0, width, height
|
||||
x1, y1 = owned.min(axis=0)
|
||||
x2, y2 = owned.max(axis=0)
|
||||
roi_width = max(x2 - x1, 64.0)
|
||||
roi_height = max(y2 - y1, 64.0)
|
||||
padding = max(18.0, 0.35 * max(roi_width, roi_height))
|
||||
center_x = (x1 + x2) / 2.0
|
||||
center_y = (y1 + y2) / 2.0
|
||||
side = min(max(roi_width, roi_height) + 2.0 * padding, float(max(width, height)))
|
||||
left = max(0.0, min(float(width) - side, center_x - side / 2.0))
|
||||
top = max(0.0, min(float(height) - side, center_y - side / 2.0))
|
||||
right = min(float(width), left + side)
|
||||
bottom = min(float(height), top + side)
|
||||
return round(left), round(top), round(right), round(bottom)
|
||||
|
||||
|
||||
def _fit_cover(image: Image.Image, size: tuple[int, int]) -> Image.Image:
|
||||
target_width, target_height = size
|
||||
ratio = max(target_width / image.width, target_height / image.height)
|
||||
resized = image.resize(
|
||||
(round(image.width * ratio), round(image.height * ratio)),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
left = max(0, (resized.width - target_width) // 2)
|
||||
top = max(0, (resized.height - target_height) // 2)
|
||||
return resized.crop((left, top, left + target_width, top + target_height))
|
||||
|
||||
|
||||
def _tile(
|
||||
*,
|
||||
ordinal: int,
|
||||
item: dict[str, Any],
|
||||
item_path: Path,
|
||||
frame_path: Path,
|
||||
) -> Image.Image:
|
||||
with np.load(item_path, allow_pickle=False) as arrays:
|
||||
with Image.open(frame_path) as source_frame:
|
||||
camera = item["camera_frame"]
|
||||
if source_frame.size != (camera.get("width"), camera.get("height")):
|
||||
raise ReviewSheetError("camera dimensions changed")
|
||||
annotated = _draw_projection(
|
||||
frame=source_frame,
|
||||
arrays=arrays,
|
||||
item=item,
|
||||
)
|
||||
full = annotated.resize(
|
||||
(FULL_WIDTH, FULL_HEIGHT),
|
||||
Image.Resampling.LANCZOS,
|
||||
)
|
||||
roi_box = _expanded_roi(
|
||||
item=item,
|
||||
arrays=arrays,
|
||||
width=annotated.width,
|
||||
height=annotated.height,
|
||||
)
|
||||
roi = _fit_cover(annotated.crop(roi_box), (ROI_WIDTH, ROI_HEIGHT))
|
||||
|
||||
tile = Image.new("RGB", (TILE_WIDTH, TILE_HEIGHT), PANEL)
|
||||
tile.paste(full, (0, 0))
|
||||
tile.paste(roi, (FULL_WIDTH, 0))
|
||||
draw = ImageDraw.Draw(tile)
|
||||
draw.line((FULL_WIDTH, 0, FULL_WIDTH, ROI_HEIGHT), fill=(65, 65, 68), width=1)
|
||||
draw.rectangle((0, 240, TILE_WIDTH - 1, TILE_HEIGHT - 1), fill=BACKGROUND)
|
||||
|
||||
snapshot = item.get("e29_snapshot")
|
||||
label = (
|
||||
str(snapshot.get("label"))
|
||||
if isinstance(snapshot, dict) and snapshot.get("label")
|
||||
else "geometry"
|
||||
)
|
||||
materialization = item["materialization"]
|
||||
frame_index = item["evidence_binding"]["source_frame_index"]
|
||||
score = materialization.get("detector_score")
|
||||
score_text = "n/a" if score is None else f"{float(score):.2f}"
|
||||
draw.text(
|
||||
(8, 246),
|
||||
f"{ordinal:02d} · {item['stratum']} · {label} · f{frame_index}",
|
||||
font=FONT_BOLD,
|
||||
fill=TEXT,
|
||||
)
|
||||
draw.text(
|
||||
(8, 266),
|
||||
(
|
||||
f"score {score_text} · proj {materialization['projected_point_count']} "
|
||||
f"· cand {materialization['candidate_point_count']} "
|
||||
f"· sel {materialization['selected_point_count']}"
|
||||
),
|
||||
font=FONT_META,
|
||||
fill=MUTED,
|
||||
)
|
||||
draw.text(
|
||||
(FULL_WIDTH + 7, ROI_HEIGHT + 7),
|
||||
item["review_key"][:24],
|
||||
font=FONT_SMALL,
|
||||
fill=MUTED,
|
||||
)
|
||||
draw.text(
|
||||
(FULL_WIDTH + 7, ROI_HEIGHT + 25),
|
||||
f"seq {item['sequence']} · roi {roi_box[0]},{roi_box[1]}",
|
||||
font=FONT_SMALL,
|
||||
fill=MUTED,
|
||||
)
|
||||
return tile
|
||||
|
||||
|
||||
def render_review_sheets(
|
||||
*,
|
||||
materialization_root: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
root = materialization_root.resolve()
|
||||
manifest = _read_json(root / "manifest.json")
|
||||
index_path = root / "materialized-items.jsonl"
|
||||
if (
|
||||
manifest.get("schema_version") != MATERIALIZATION_SCHEMA
|
||||
or manifest.get("result_id") != root.name
|
||||
or manifest.get("item_count") != 486
|
||||
or manifest.get("camera_evidence_available") is not True
|
||||
):
|
||||
raise ReviewSheetError("unsupported E30 materialization")
|
||||
items = _read_items(index_path)
|
||||
if (
|
||||
len(items) != manifest.get("item_count")
|
||||
or len({item.get("item_id") for item in items}) != len(items)
|
||||
):
|
||||
raise ReviewSheetError("materialization index count differs")
|
||||
|
||||
by_stratum: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for item in items:
|
||||
stratum = item.get("stratum")
|
||||
if stratum not in STRATA:
|
||||
raise ReviewSheetError("unknown E30 stratum")
|
||||
by_stratum[stratum].append(item)
|
||||
for values in by_stratum.values():
|
||||
values.sort(
|
||||
key=lambda item: (
|
||||
item["evidence_binding"]["source_frame_index"],
|
||||
item["sequence"],
|
||||
)
|
||||
)
|
||||
|
||||
output_root.mkdir(parents=True, exist_ok=True)
|
||||
destination = output_root / root.name
|
||||
if destination.exists():
|
||||
if destination.is_symlink() or not destination.is_dir():
|
||||
raise ReviewSheetError("review sheet destination is invalid")
|
||||
shutil.rmtree(destination)
|
||||
staging = Path(tempfile.mkdtemp(prefix=f".{root.name}.", dir=output_root))
|
||||
sheet_documents: list[dict[str, Any]] = []
|
||||
try:
|
||||
for stratum in STRATA:
|
||||
values = by_stratum[stratum]
|
||||
for page_index, offset in enumerate(
|
||||
range(0, len(values), ITEMS_PER_SHEET),
|
||||
start=1,
|
||||
):
|
||||
page = values[offset : offset + ITEMS_PER_SHEET]
|
||||
sheet = Image.new(
|
||||
"RGB",
|
||||
(
|
||||
SHEET_COLUMNS * TILE_WIDTH,
|
||||
SHEET_ROWS * TILE_HEIGHT,
|
||||
),
|
||||
BACKGROUND,
|
||||
)
|
||||
entries: list[dict[str, Any]] = []
|
||||
for ordinal, item in enumerate(page, start=1):
|
||||
item_path, frame_path = _verify_item(root=root, item=item)
|
||||
tile = _tile(
|
||||
ordinal=ordinal,
|
||||
item=item,
|
||||
item_path=item_path,
|
||||
frame_path=frame_path,
|
||||
)
|
||||
column = (ordinal - 1) % SHEET_COLUMNS
|
||||
row = (ordinal - 1) // SHEET_COLUMNS
|
||||
sheet.paste(tile, (column * TILE_WIDTH, row * TILE_HEIGHT))
|
||||
entries.append(
|
||||
{
|
||||
"ordinal": ordinal,
|
||||
"item_id": item["item_id"],
|
||||
"sequence": item["sequence"],
|
||||
"review_key": item["review_key"],
|
||||
"stratum": item["stratum"],
|
||||
"source_frame_index": item["evidence_binding"][
|
||||
"source_frame_index"
|
||||
],
|
||||
"label": item["e29_snapshot"].get("label"),
|
||||
}
|
||||
)
|
||||
file_name = f"{stratum}-{page_index:02d}.jpg"
|
||||
sheet_path = staging / file_name
|
||||
sheet.save(sheet_path, format="JPEG", quality=94, optimize=True)
|
||||
sheet_documents.append(
|
||||
{
|
||||
"path": file_name,
|
||||
"sha256": _sha256(sheet_path),
|
||||
"byte_length": sheet_path.stat().st_size,
|
||||
"stratum": stratum,
|
||||
"page": page_index,
|
||||
"entries": entries,
|
||||
}
|
||||
)
|
||||
identity = {
|
||||
"schema_version": SHEET_MANIFEST_SCHEMA,
|
||||
"materialization_id": root.name,
|
||||
"materialization_manifest_sha256": _sha256(root / "manifest.json"),
|
||||
"materialization_index_sha256": _sha256(index_path),
|
||||
"layout": {
|
||||
"columns": SHEET_COLUMNS,
|
||||
"rows": SHEET_ROWS,
|
||||
"tile_width": TILE_WIDTH,
|
||||
"tile_height": TILE_HEIGHT,
|
||||
},
|
||||
"sheets": sheet_documents,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
document = {
|
||||
**identity,
|
||||
"identity_sha256": identity_sha256,
|
||||
"sheet_count": len(sheet_documents),
|
||||
"item_count": len(items),
|
||||
"authority": {
|
||||
"presentation_derivative_only": True,
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
(staging / "manifest.json").write_bytes(_canonical_json(document) + b"\n")
|
||||
staging.rename(destination)
|
||||
return document
|
||||
except Exception:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--materialization-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
document = render_review_sheets(
|
||||
materialization_root=args.materialization_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"materialization_id": document["materialization_id"],
|
||||
"sheet_count": document["sheet_count"],
|
||||
"item_count": document["item_count"],
|
||||
"identity_sha256": document["identity_sha256"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build camera-backed LAB E30 evidence from immutable A2 sources."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e30_materialization import build_e30_materialization
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--review-pack", type=Path, required=True)
|
||||
parser.add_argument("--e29-root", type=Path, required=True)
|
||||
parser.add_argument("--source-result-root", type=Path, required=True)
|
||||
parser.add_argument("--source-pack-root", type=Path, required=True)
|
||||
parser.add_argument("--local-surface-root", type=Path, required=True)
|
||||
parser.add_argument("--camera-job", type=Path, required=True)
|
||||
parser.add_argument("--ffmpeg", type=Path, required=True)
|
||||
parser.add_argument(
|
||||
"--output-root",
|
||||
type=Path,
|
||||
default=Path(".runtime/compute-experiments/e30/materializations"),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = build_e30_materialization(
|
||||
review_pack_root=args.review_pack,
|
||||
e29_root=args.e29_root,
|
||||
source_result_root=args.source_result_root,
|
||||
source_pack_root=args.source_pack_root,
|
||||
local_surface_root=args.local_surface_root,
|
||||
camera_job_root=args.camera_job,
|
||||
ffmpeg_path=args.ffmpeg,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"item_count": result.manifest["item_count"],
|
||||
"camera_evidence_available": result.manifest[
|
||||
"camera_evidence_available"
|
||||
],
|
||||
"human_review_complete": result.manifest[
|
||||
"human_review_complete"
|
||||
],
|
||||
"lab_published": result.manifest["lab_published"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user