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,
|
||||
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 (
|
||||
ANNOTATION_CONTRACT_SCHEMA,
|
||||
EVALUATION_PACK_SCHEMA,
|
||||
@@ -419,6 +430,15 @@ __all__ = [
|
||||
"build_e51_motion_semantic_qualification",
|
||||
"derive_motion_semantic_signal",
|
||||
"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_benchmark",
|
||||
"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