feat(perception): restore camera-first semantic candidate

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 21:53:34 +03:00
parent fdcaf57ae7
commit e4e5bf6ea1
6 changed files with 1436 additions and 0 deletions
@@ -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 E37E40, 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())