feat(perception): restore camera-first semantic candidate
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
# LAB E52 · восстановление camera-first семантики
|
||||||
|
|
||||||
|
Дата: 2026-07-30
|
||||||
|
|
||||||
|
Статус: принят инженерный кандидат; runtime-модель не изменена и не
|
||||||
|
авторизована
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
|
||||||
|
Операторская проверка E40 показала, что интерфейс выдавал фиксированную
|
||||||
|
`stratum-policy` за уверенность модели. В частности,
|
||||||
|
`geometry-only → occupied-environment` отображалось как решение модели с
|
||||||
|
уверенностью 100%. Из-за этого человек, тележка или часть фасада могли
|
||||||
|
выглядеть как ошибочно распознанный «фон/шум».
|
||||||
|
|
||||||
|
Это не было результатом рабочего camera-first контура. E29 по-прежнему
|
||||||
|
реализует исходное разделение ответственности:
|
||||||
|
|
||||||
|
- камера владеет классом и идентичностью объекта;
|
||||||
|
- LiDAR проверяет геометрическую поддержку и вычисляет дальность;
|
||||||
|
- LiDAR-кластер без camera-детекта не получает семантический класс.
|
||||||
|
|
||||||
|
E40 являлся историческим видимым мета-оценщиком и повторно решал presence по
|
||||||
|
фиксированным правилам поверх E29. E52 не переносит эти правила в runtime.
|
||||||
|
|
||||||
|
## Неизменяемые входы
|
||||||
|
|
||||||
|
- E29/E30:
|
||||||
|
`e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a`;
|
||||||
|
- E40:
|
||||||
|
`e40-perception-product-gate-e96eec9fd68c3ffaaee898d46285dd329191267200011680f084c75095b92e9a`;
|
||||||
|
- полная операторская ревизия E40: 23 из 23 случаев;
|
||||||
|
- camera candidate:
|
||||||
|
`result-f4cebdea8a82698a5b8a65d2c3fbdb0428b88b9dc49fe45f8cb37d740ed83d02`;
|
||||||
|
- camera source:
|
||||||
|
`recorded-camera-602ac89026ed12978619801d`, 4 489 кадров;
|
||||||
|
- Mask R-CNN:
|
||||||
|
`torchvision/maskrcnn_resnet50_fpn_v2`, COCO, threshold 0.5,
|
||||||
|
checkpoint SHA-256
|
||||||
|
`73cbd0190fcbe3ba339921fbce2c3a0b6bb9126c9a133c85e43a2a8e060a109e`.
|
||||||
|
|
||||||
|
Все входные SHA-256 проверены до и после E52. Исходные артефакты не
|
||||||
|
перезаписывались.
|
||||||
|
|
||||||
|
## Метод
|
||||||
|
|
||||||
|
E52 использует уже рассчитанные camera-детекты и выполняет только лёгкое
|
||||||
|
source-bound сопоставление:
|
||||||
|
|
||||||
|
1. берёт 20 случаев, где оператор подтвердил ошибку представления E40;
|
||||||
|
2. отбрасывает camera-боксы больше 25% изображения как патологические;
|
||||||
|
3. для `geometry-only` требует не меньше четырёх выбранных LiDAR-точек;
|
||||||
|
4. camera-бокс должен покрывать не меньше 70% точек выбранного кластера;
|
||||||
|
5. класс берётся только из camera-детекта;
|
||||||
|
6. дальность вычисляется как медиана глубин связанных LiDAR-точек;
|
||||||
|
7. нерешённый кластер остаётся геометрией без класса.
|
||||||
|
|
||||||
|
Для исходных camera-observation используется camera-box IoU не ниже 0.25.
|
||||||
|
Группы `car/bus/truck` сравниваются как транспорт, но исходная camera-метка
|
||||||
|
сохраняется в evidence.
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
|
||||||
|
```text
|
||||||
|
e52-camera-first-restoration-37ea5269a7f49fc84cbe2c394682f30ccc0d06b781d7d734c2f5420ba764b09f
|
||||||
|
```
|
||||||
|
|
||||||
|
| Измерение | Результат |
|
||||||
|
| --- | ---: |
|
||||||
|
| Подтверждённые оператором случаи | 20 |
|
||||||
|
| Подтверждённые `geometry-only` | 8 |
|
||||||
|
| Восстановлена camera-семантика + LiDAR-дальность | 3 |
|
||||||
|
| Намеренно оставлены нерешёнными | 5 |
|
||||||
|
| Pathological full-frame box принят | 0 |
|
||||||
|
|
||||||
|
Восстановленные случаи:
|
||||||
|
|
||||||
|
| Кадр | Camera-класс | Camera score | Покрытие LiDAR | LiDAR range |
|
||||||
|
| ---: | --- | ---: | ---: | ---: |
|
||||||
|
| 167 | truck | 0.979998 | 15/15 | 8.038404 m |
|
||||||
|
| 259 | person | 0.999834 | 49/50 | 0.512825 m |
|
||||||
|
| 3181 | car | 0.878104 | 4/4 | 0.301412 m |
|
||||||
|
|
||||||
|
Кадр 259 подтверждает исходную проблему: YOLOX не дал семантического
|
||||||
|
наблюдения, однако уже рассчитанный camera candidate уверенно выделил человека,
|
||||||
|
а LiDAR обеспечил независимую метрическую дальность.
|
||||||
|
|
||||||
|
Пять случаев не были автоматически «исправлены»:
|
||||||
|
|
||||||
|
- кадры 1270, 1878 и 2535 имеют только 1–2 выбранные LiDAR-точки;
|
||||||
|
- на кадрах 267 и 1080 пересечение давал патологический бокс почти на весь
|
||||||
|
кадр;
|
||||||
|
- E52 не повышает такие случаи до объектов и не выдаёт отсутствие точек за
|
||||||
|
свободное пространство.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Camera-first принцип восстановлен в отдельном инженерном кандидате:
|
||||||
|
|
||||||
|
```text
|
||||||
|
camera detection
|
||||||
|
→ проверка LiDAR-точек внутри camera bbox
|
||||||
|
→ camera semantic + LiDAR range
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime E29, sealed E37–E40, K1-команды и persistent map не изменены. Общий
|
||||||
|
Mask R-CNN пока не принят как production detector.
|
||||||
|
|
||||||
|
Следующий gate — прогнать уже рассчитанные детекты через неизменённую
|
||||||
|
ассоциацию E29 в полном source-bound shadow-контуре и измерить:
|
||||||
|
|
||||||
|
- восстановление camera-семантики;
|
||||||
|
- отбрасывание больших ложных боксов;
|
||||||
|
- временную стабильность;
|
||||||
|
- задержку и ресурсную стоимость;
|
||||||
|
- отсутствие регрессии текущих корректных camera-first объектов.
|
||||||
|
|
||||||
|
Только после отдельного явного принятия этого gate кандидат может заменить или
|
||||||
|
дополнить YOLOX в runtime.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "missioncore.e52-camera-first-restoration-profile/v1",
|
||||||
|
"profile_id": "e52-ravnoves00-camera-first-restoration/v1",
|
||||||
|
"expected_e30_materialization_id": "e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a",
|
||||||
|
"expected_e40_result_id": "e40-perception-product-gate-e96eec9fd68c3ffaaee898d46285dd329191267200011680f084c75095b92e9a",
|
||||||
|
"expected_detector_result_id": "result-f4cebdea8a82698a5b8a65d2c3fbdb0428b88b9dc49fe45f8cb37d740ed83d02",
|
||||||
|
"expected_camera_job_id": "recorded-camera-602ac89026ed12978619801d",
|
||||||
|
"target_labels": [
|
||||||
|
"person",
|
||||||
|
"bicycle",
|
||||||
|
"car",
|
||||||
|
"motorcycle",
|
||||||
|
"bus",
|
||||||
|
"truck"
|
||||||
|
],
|
||||||
|
"label_groups": {
|
||||||
|
"person": [
|
||||||
|
"person"
|
||||||
|
],
|
||||||
|
"two-wheel": [
|
||||||
|
"bicycle",
|
||||||
|
"motorcycle"
|
||||||
|
],
|
||||||
|
"vehicle": [
|
||||||
|
"car",
|
||||||
|
"bus",
|
||||||
|
"truck"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"minimum_detector_score": 0.5,
|
||||||
|
"maximum_bbox_image_fraction": 0.25,
|
||||||
|
"minimum_selected_lidar_points": 4,
|
||||||
|
"minimum_selected_lidar_coverage": 0.7,
|
||||||
|
"minimum_original_bbox_iou": 0.25
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Build the immutable E52 camera-first restoration candidate."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from k1link.compute.e52_camera_first_restoration import (
|
||||||
|
build_e52_camera_first_restoration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--e30-materialization", type=Path, required=True)
|
||||||
|
parser.add_argument("--e40-result", type=Path, required=True)
|
||||||
|
parser.add_argument("--operator-review", type=Path, required=True)
|
||||||
|
parser.add_argument("--detector-result", type=Path, required=True)
|
||||||
|
parser.add_argument("--profile", type=Path, required=True)
|
||||||
|
parser.add_argument("--output-root", type=Path, required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
result = build_e52_camera_first_restoration(
|
||||||
|
e30_materialization_root=args.e30_materialization,
|
||||||
|
e40_result_root=args.e40_result,
|
||||||
|
operator_review_path=args.operator_review,
|
||||||
|
detector_result_root=args.detector_result,
|
||||||
|
profile_path=args.profile,
|
||||||
|
output_root=args.output_root,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_id": result.result_id,
|
||||||
|
"result_root": str(result.result_root),
|
||||||
|
"metrics": result.report["metrics"],
|
||||||
|
"decision": result.report["decision"],
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -52,6 +52,17 @@ from .e51_motion_semantic_qualification import (
|
|||||||
derive_motion_semantic_signal,
|
derive_motion_semantic_signal,
|
||||||
read_e51_motion_semantic_qualification,
|
read_e51_motion_semantic_qualification,
|
||||||
)
|
)
|
||||||
|
from .e52_camera_first_restoration import (
|
||||||
|
E52_CASE_SCHEMA,
|
||||||
|
E52_PROFILE_SCHEMA,
|
||||||
|
E52_REPORT_SCHEMA,
|
||||||
|
E52_RESULT_SCHEMA,
|
||||||
|
E52CameraFirstRestoration,
|
||||||
|
E52CameraFirstRestorationError,
|
||||||
|
build_e52_camera_first_restoration,
|
||||||
|
evaluate_camera_first_case,
|
||||||
|
read_e52_camera_first_restoration,
|
||||||
|
)
|
||||||
from .evaluation_pack import (
|
from .evaluation_pack import (
|
||||||
ANNOTATION_CONTRACT_SCHEMA,
|
ANNOTATION_CONTRACT_SCHEMA,
|
||||||
EVALUATION_PACK_SCHEMA,
|
EVALUATION_PACK_SCHEMA,
|
||||||
@@ -419,6 +430,15 @@ __all__ = [
|
|||||||
"build_e51_motion_semantic_qualification",
|
"build_e51_motion_semantic_qualification",
|
||||||
"derive_motion_semantic_signal",
|
"derive_motion_semantic_signal",
|
||||||
"read_e51_motion_semantic_qualification",
|
"read_e51_motion_semantic_qualification",
|
||||||
|
"E52_CASE_SCHEMA",
|
||||||
|
"E52_PROFILE_SCHEMA",
|
||||||
|
"E52_REPORT_SCHEMA",
|
||||||
|
"E52_RESULT_SCHEMA",
|
||||||
|
"E52CameraFirstRestoration",
|
||||||
|
"E52CameraFirstRestorationError",
|
||||||
|
"build_e52_camera_first_restoration",
|
||||||
|
"evaluate_camera_first_case",
|
||||||
|
"read_e52_camera_first_restoration",
|
||||||
"build_lidar_ground_annotation_template",
|
"build_lidar_ground_annotation_template",
|
||||||
"build_lidar_ground_benchmark",
|
"build_lidar_ground_benchmark",
|
||||||
"build_k1_local_surface",
|
"build_k1_local_surface",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.compute.e52_camera_first_restoration import (
|
||||||
|
E52_CASE_SCHEMA,
|
||||||
|
evaluate_camera_first_case,
|
||||||
|
)
|
||||||
|
|
||||||
|
PROFILE: dict[str, Any] = {
|
||||||
|
"target_labels": ["person", "car", "bus", "truck"],
|
||||||
|
"label_groups": {
|
||||||
|
"person": ["person"],
|
||||||
|
"vehicle": ["car", "bus", "truck"],
|
||||||
|
},
|
||||||
|
"minimum_detector_score": 0.5,
|
||||||
|
"maximum_bbox_image_fraction": 0.25,
|
||||||
|
"minimum_selected_lidar_points": 4,
|
||||||
|
"minimum_selected_lidar_coverage": 0.7,
|
||||||
|
"minimum_original_bbox_iou": 0.25,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _item(*, stratum: str, snapshot: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"item_id": "e30-review-item-" + "a" * 64,
|
||||||
|
"stratum": stratum,
|
||||||
|
"evidence_binding": {"source_frame_index": 259},
|
||||||
|
"materialization": {
|
||||||
|
"projection_width": 800,
|
||||||
|
"projection_height": 600,
|
||||||
|
},
|
||||||
|
"e29_snapshot": snapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _case(*, stratum: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"sequence": 7,
|
||||||
|
"source_stratum": stratum,
|
||||||
|
"prediction": {
|
||||||
|
"presence": "occupied-environment",
|
||||||
|
"geometry_association": "independent-occupied",
|
||||||
|
"freshness": "current",
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"presence": "object-present",
|
||||||
|
"geometry_association": "object-associated",
|
||||||
|
"freshness": "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _frame(instances: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"schema_version": "missioncore.panoptic-frame/v1",
|
||||||
|
"frame_index": 259,
|
||||||
|
"instances": instances,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_e52_restores_camera_semantic_then_derives_lidar_range() -> None:
|
||||||
|
pixels = np.asarray([[150.0, 225.0], [152.0, 230.0], [155.0, 235.0], [160.0, 240.0]])
|
||||||
|
depths = np.asarray([4.0, 4.2, 4.4, 4.6])
|
||||||
|
result = evaluate_camera_first_case(
|
||||||
|
item=_item(
|
||||||
|
stratum="geometry-only",
|
||||||
|
snapshot={
|
||||||
|
"geometry_status": "single-source-geometry",
|
||||||
|
"nearest_range_m": 3.9,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
e40_case=_case(stratum="geometry-only"),
|
||||||
|
detector_frame=_frame(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"instance_id": 1,
|
||||||
|
"label": "person",
|
||||||
|
"score": 0.99,
|
||||||
|
"box_xyxy": [140.0, 210.0, 220.0, 450.0],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
projected_pixels_xy=pixels,
|
||||||
|
projected_depth_m=depths,
|
||||||
|
projected_selected_mask=np.ones(4, dtype=np.uint8),
|
||||||
|
profile=PROFILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["schema_version"] == E52_CASE_SCHEMA
|
||||||
|
assert result["restoration"] == {
|
||||||
|
"state": "camera-semantic-restored-with-lidar-range",
|
||||||
|
"semantic_owner": "camera",
|
||||||
|
"semantic_label": "person",
|
||||||
|
"metric_geometry_owner": "lidar",
|
||||||
|
"range_m": 4.3,
|
||||||
|
"runtime_publishable": False,
|
||||||
|
"requires_shadow_acceptance": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_e52_rejects_pathological_full_frame_detector_box() -> None:
|
||||||
|
pixels = np.asarray([[150.0, 225.0], [152.0, 230.0], [155.0, 235.0], [160.0, 240.0]])
|
||||||
|
result = evaluate_camera_first_case(
|
||||||
|
item=_item(
|
||||||
|
stratum="geometry-only",
|
||||||
|
snapshot={"geometry_status": "single-source-geometry"},
|
||||||
|
),
|
||||||
|
e40_case=_case(stratum="geometry-only"),
|
||||||
|
detector_frame=_frame(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"instance_id": 1,
|
||||||
|
"label": "car",
|
||||||
|
"score": 0.9,
|
||||||
|
"box_xyxy": [0.0, 0.0, 800.0, 600.0],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
projected_pixels_xy=pixels,
|
||||||
|
projected_depth_m=np.asarray([4.0, 4.2, 4.4, 4.6]),
|
||||||
|
projected_selected_mask=np.ones(4, dtype=np.uint8),
|
||||||
|
profile=PROFILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["candidate_camera"]["selected"] is None
|
||||||
|
assert result["candidate_camera"]["rejected_pathological_large_box_count"] == 1
|
||||||
|
assert result["restoration"]["state"] == "unresolved-no-camera-semantic"
|
||||||
|
|
||||||
|
|
||||||
|
def test_e52_corroborates_existing_camera_semantic_without_inventing_range() -> None:
|
||||||
|
result = evaluate_camera_first_case(
|
||||||
|
item=_item(
|
||||||
|
stratum="camera-only",
|
||||||
|
snapshot={
|
||||||
|
"label": "bus",
|
||||||
|
"score": 0.4,
|
||||||
|
"bbox_xyxy": [100.0, 100.0, 200.0, 200.0],
|
||||||
|
"geometry_status": "camera-only",
|
||||||
|
"range_m": None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
e40_case=_case(stratum="camera-only"),
|
||||||
|
detector_frame=_frame(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"instance_id": 4,
|
||||||
|
"label": "truck",
|
||||||
|
"score": 0.95,
|
||||||
|
"box_xyxy": [105.0, 105.0, 205.0, 205.0],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
projected_pixels_xy=np.empty((0, 2), dtype=np.float64),
|
||||||
|
projected_depth_m=np.empty(0, dtype=np.float64),
|
||||||
|
projected_selected_mask=np.empty(0, dtype=np.uint8),
|
||||||
|
profile=PROFILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["restoration"]["state"] == "camera-semantic-corroborated-no-lidar-range"
|
||||||
|
assert result["restoration"]["semantic_label"] == "truck"
|
||||||
|
assert result["restoration"]["range_m"] is None
|
||||||
Reference in New Issue
Block a user