feat(perception): qualify bounded temporal stability
This commit is contained in:
parent
5a29a536a2
commit
cfc7b062da
|
|
@ -125,12 +125,13 @@ viewer-ready form. Deleting a LAB catalog entry cannot delete the referenced
|
||||||
source session, and a source session with published LAB instances fails closed
|
source session, and a source session with published LAB instances fails closed
|
||||||
on deletion.
|
on deletion.
|
||||||
|
|
||||||
The visual result is resolved by the LAB session id, so `LAB E19` and `LAB E21`
|
The visual result is resolved by the LAB session id, so `LAB E19`, `LAB E21`
|
||||||
can coexist over `RAVNOVES00` without “latest accepted result” replacing an
|
and the derived temporal comparison `LAB E22` can coexist over `RAVNOVES00`
|
||||||
earlier experiment. The operator can open either row in **Сохранённые сессии**
|
without “latest accepted result” replacing an earlier experiment. The operator
|
||||||
and use the same **Объекты 2D / Сегментация / Кубы 3D** controls. First
|
can open any row in **Сохранённые сессии** and use the same
|
||||||
publication must also warm the result-specific Rerun overlay; later opens reuse
|
**Объекты 2D / Сегментация / Кубы 3D** controls. First publication must also
|
||||||
that verified cache and do not rerun AI inference.
|
warm the result-specific Rerun overlay; later opens reuse that verified cache
|
||||||
|
and do not rerun AI inference.
|
||||||
|
|
||||||
E21 is a bounded special case: its worker persisted 121 semantic mask hashes,
|
E21 is a bounded special case: its worker persisted 121 semantic mask hashes,
|
||||||
not duplicate mask pixels. Its visual publication materializes a mask only
|
not duplicate mask pixels. Its visual publication materializes a mask only
|
||||||
|
|
@ -143,6 +144,13 @@ the 60-second result to the complete 8:55 source replay is an invalid
|
||||||
presentation because latest-at image data would appear static after the E21
|
presentation because latest-at image data would appear static after the E21
|
||||||
window ends.
|
window ends.
|
||||||
|
|
||||||
|
E22 is also bounded to the exact E21 60-second range, but it resolves its
|
||||||
|
catalog provenance directly to the physical source session rather than chaining
|
||||||
|
one LAB entry to another. Its temporal result remains a separate immutable
|
||||||
|
integrated overlay. Raw point-cloud, camera, calibration and E21 inference
|
||||||
|
payloads are not replaced. Short held boxes are provenance-marked diagnostic
|
||||||
|
presentation/world-state values, not fresh sensor measurements.
|
||||||
|
|
||||||
The first preparation of a long capture can take time because every decodable
|
The first preparation of a long capture can take time because every decodable
|
||||||
point/pose message and every valid K1 `ModelingReport` is projected into RRD.
|
point/pose message and every valid K1 `ModelingReport` is projected into RRD.
|
||||||
Later openings reuse a verified, digest-bound cache. Derived RRD cache v9 is
|
Later openings reuse a verified, digest-bound cache. Derived RRD cache v9 is
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
# LAB E22 — Bounded temporal stability
|
||||||
|
|
||||||
|
Date: 2026-07-23
|
||||||
|
State: accepted diagnostic laboratory result
|
||||||
|
Authority: observation only; commands and navigation/safety acceptance disabled
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
LAB E22 tests whether the perception output already qualified by LAB E21 can
|
||||||
|
be made materially calmer without losing the real-time envelope required for
|
||||||
|
an unmanned vehicle.
|
||||||
|
|
||||||
|
This is not another model-quality run in isolation. The experiment adds the
|
||||||
|
missing temporal output stage between AI inference and the operator/autonomy
|
||||||
|
consumer:
|
||||||
|
|
||||||
|
- stabilize tracked 2D boxes without future-frame lookahead;
|
||||||
|
- preserve short detector gaps with explicit, bounded holds;
|
||||||
|
- stabilize map-frame 3D cuboids and their orientation;
|
||||||
|
- reduce unsupported semantic label islands;
|
||||||
|
- retain immutable raw camera, LiDAR, calibration and E21 inference evidence;
|
||||||
|
- measure the added latency and memory bound.
|
||||||
|
|
||||||
|
## 2. Exact source
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Source LAB | `lab-e21-window2-d0201712` |
|
||||||
|
| Source display name | `LAB E21.3 · Real-time 1× · synchronized 60s · d0201712` |
|
||||||
|
| Physical source session | `20260720T065719Z_viewer_live` (`RAVNOVES00`) |
|
||||||
|
| Source result | `e10-integrated-perception-5cbba0b9839ae843c05607cec8448a2b43464eb81edeb11cf7c01cfec8464c75` |
|
||||||
|
| Camera source | `sensor.camera.right`, XGRIDS camera slot `camera_1` |
|
||||||
|
| Resolution | 800 × 600 |
|
||||||
|
| Window | 601 camera frames, 59.962 s |
|
||||||
|
| Semantic masks | 121 |
|
||||||
|
| Source detector | YOLOX-S, COCO-80 |
|
||||||
|
| Source semantic model | EoMT Cityscapes large, FP16 |
|
||||||
|
| Source inference hardware | NVIDIA GeForce RTX 4090 |
|
||||||
|
| Source detector throughput | 9.865 FPS |
|
||||||
|
| Source semantic throughput | 2.010 FPS |
|
||||||
|
| Source world-state age | P95 174.40 ms; P99 289.03 ms |
|
||||||
|
|
||||||
|
The E22 postprocessor ran locally over the immutable E21.3 result. Its
|
||||||
|
implementation is streaming-equivalent: one pass, no future-frame lookahead,
|
||||||
|
bounded state, and no repeated neural inference.
|
||||||
|
|
||||||
|
## 3. Configuration
|
||||||
|
|
||||||
|
Profile:
|
||||||
|
`experiments/perception/e22_temporal_stability_profile.json`
|
||||||
|
|
||||||
|
Profile SHA-256:
|
||||||
|
`3d61b29562a851ec41e504bb175e392be874c4509e6944834a449d2e347022d2`
|
||||||
|
|
||||||
|
### 3.1 2D tracking
|
||||||
|
|
||||||
|
- adaptive center EMA: alpha 0.32–0.78;
|
||||||
|
- adaptive size EMA: alpha 0.22–0.65;
|
||||||
|
- center-velocity EMA: alpha 0.35;
|
||||||
|
- large normalized innovation: reset, not forced smoothing;
|
||||||
|
- missing observation hold: at most two camera frames;
|
||||||
|
- class-consistent tracklet stitch: at most eight frames, IoU/center gated.
|
||||||
|
|
||||||
|
### 3.2 3D cuboids
|
||||||
|
|
||||||
|
- map-frame center EMA: alpha 0.28;
|
||||||
|
- half-size EMA: alpha 0.18;
|
||||||
|
- pi-periodic yaw EMA: alpha 0.18;
|
||||||
|
- velocity EMA: alpha 0.25;
|
||||||
|
- center innovation reset: above 2.0 m;
|
||||||
|
- yaw innovation reset: above 55°;
|
||||||
|
- last-supported cuboid hold: at most 350 ms.
|
||||||
|
|
||||||
|
The complete point cloud is not filtered or reduced by this stage. The ground
|
||||||
|
and all other points remain available to mapping, terrain and traversability
|
||||||
|
consumers.
|
||||||
|
|
||||||
|
### 3.3 Semantic output
|
||||||
|
|
||||||
|
The filter uses one previous mask and accepts a changed pixel immediately when
|
||||||
|
the new label has spatial support. Unsupported isolated changes retain the
|
||||||
|
previous label.
|
||||||
|
|
||||||
|
This is deliberately not called optical-flow propagation. It removes label
|
||||||
|
islands but does not interpolate semantic geometry between the source model's
|
||||||
|
2 FPS updates.
|
||||||
|
|
||||||
|
### 3.4 Hard bounds
|
||||||
|
|
||||||
|
- maximum track states: 128;
|
||||||
|
- observed peak track states: 19;
|
||||||
|
- semantic history: one 800 × 600 `uint8` mask;
|
||||||
|
- estimated upper state bound: 1,004,288 bytes;
|
||||||
|
- future-frame lookahead: zero.
|
||||||
|
|
||||||
|
## 4. Baseline versus E22
|
||||||
|
|
||||||
|
| Metric | E21.3 baseline | E22 | Change |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| 2D normalized acceleration, P95 | 0.5232 | 0.3938 | −24.7% |
|
||||||
|
| 2D normalized size step, P95 | 0.2670 | 0.1520 | −43.1% |
|
||||||
|
| 2D track count | 165 | 160 | −5 stitched identities |
|
||||||
|
| Tracks lasting ≤2 frames | 34 | 0 | −34 |
|
||||||
|
| 3D center step, P95 | 0.8174 m | 0.5698 m | −30.3% |
|
||||||
|
| 3D half-size step, P95 | 0.0542 m | 0.0189 m | −65.1% |
|
||||||
|
| 3D yaw step, P95 | 15.51° | 5.26° | −66.1% |
|
||||||
|
| 3D discontinuity gaps | 174 | 87 | −50.0% |
|
||||||
|
| Unsupported semantic change, mean | 0.03284% | 0.00718% | −78.1% |
|
||||||
|
| Total semantic mask change, mean | 5.3658% | 5.3261% | −0.7% |
|
||||||
|
|
||||||
|
The distinction in the last two rows is important. E22 strongly reduces
|
||||||
|
unsupported semantic noise, but it does not yet solve the larger inter-frame
|
||||||
|
movement of semantic boundaries. That requires confidence-aware propagation
|
||||||
|
or optical flow in a later experiment.
|
||||||
|
|
||||||
|
## 5. Temporal events and provenance
|
||||||
|
|
||||||
|
| Event | Count |
|
||||||
|
| --- | ---: |
|
||||||
|
| Tracklets stitched | 7 |
|
||||||
|
| 2D held presentations | 639 |
|
||||||
|
| 3D held cuboids | 472 |
|
||||||
|
| 2D large-innovation resets | 60 |
|
||||||
|
| 3D large-innovation resets | 69 |
|
||||||
|
|
||||||
|
The rise from 881 source accepted cuboids to 1,353 E22 presented cuboids does
|
||||||
|
not mean 472 new measurements were invented. Those 472 are explicitly marked
|
||||||
|
short holds of the last supported cuboid. Every hold is diagnostic and must
|
||||||
|
not be interpreted as a fresh LiDAR association.
|
||||||
|
|
||||||
|
## 6. Added runtime cost
|
||||||
|
|
||||||
|
Measured on the local deterministic postprocessor:
|
||||||
|
|
||||||
|
| Stage | Mean | P95 | P99 | Max |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| Per camera frame, 2D/3D/world state | 0.468 ms | 0.821 ms | 0.959 ms | 1.244 ms |
|
||||||
|
| Per semantic frame | 3.463 ms | 3.650 ms | 3.815 ms | 4.054 ms |
|
||||||
|
|
||||||
|
At the source rates of approximately 10 camera frames/s and 2 semantic
|
||||||
|
frames/s, this temporal stage consumes far less than one frame interval. It
|
||||||
|
does not prove the complete live K1 path; it proves that this specific
|
||||||
|
stabilization stage is not the real-time bottleneck.
|
||||||
|
|
||||||
|
## 7. Acceptance
|
||||||
|
|
||||||
|
All gates passed:
|
||||||
|
|
||||||
|
- 2D acceleration P95 reduction ≥20%;
|
||||||
|
- 3D center-step P95 reduction ≥20%;
|
||||||
|
- 3D yaw-step P95 reduction ≥20%;
|
||||||
|
- unsupported semantic change reduction ≥10%;
|
||||||
|
- camera-frame postprocess P95 ≤5 ms;
|
||||||
|
- semantic-frame postprocess P95 ≤30 ms;
|
||||||
|
- track-state bound respected;
|
||||||
|
- source payloads immutable;
|
||||||
|
- diagnostic authority only.
|
||||||
|
|
||||||
|
## 8. Published operator result
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Session ID | `lab-e22-3d61b295` |
|
||||||
|
| Display name | `LAB E22 · Temporal stability · synchronized 60s · 3d61b295` |
|
||||||
|
| Result ID | `e10-integrated-perception-416e8b326ea36e7d534aa9e92b9136c916c7be8cd4e86a1df0feb97bd0d67c71` |
|
||||||
|
| Result size | approximately 7.0 MiB |
|
||||||
|
| Rerun perception overlay | 7,896,401 bytes |
|
||||||
|
| Catalog state | Ready |
|
||||||
|
|
||||||
|
UI acceptance on `http://127.0.0.1:8000/`:
|
||||||
|
|
||||||
|
- LAB E22 appears as a separate saved session;
|
||||||
|
- the replay duration is 59.962 s;
|
||||||
|
- AI overlay endpoint returns HTTP 200 and a valid `RRF2` payload;
|
||||||
|
- `Объекты 2D`, `Сегментация`, and `Кубы 3D` become enabled;
|
||||||
|
- all three layers can be enabled together;
|
||||||
|
- the original point cloud remains visible;
|
||||||
|
- the Mission Core server remained on PID 31145 throughout publication and
|
||||||
|
acceptance.
|
||||||
|
|
||||||
|
## 9. Artifact hashes
|
||||||
|
|
||||||
|
| Artifact | SHA-256 |
|
||||||
|
| --- | --- |
|
||||||
|
| `result.json` | `00ebc409fd09faccd79cfff3342f109cbfba907880cc93cc834026e2ce104764` |
|
||||||
|
| `run-report.json` | `cc1d775ae52d8faa7388ddc0a0c8aba0e53ff3e90ace46b2a3749f02dfeeb912` |
|
||||||
|
| `fusion-frames.jsonl` | `456a7d8b1682b85856cbd036053e61be95a6d3fe18453277bcadde879bc9cb9c` |
|
||||||
|
| `semantic-frames.jsonl` | `d52dfbb03877ec1e50da073a221f20f8b57e5c9669a0de4b16505caa97720e5e` |
|
||||||
|
| `world-state.jsonl` | `281f26e5fd645ed995bf253bd0ff45bbc07476ef84a2c3df9b198bbc02ef44ca` |
|
||||||
|
| `transient-perception.npz` | `84609a59abb437ffabc8ca3769327450da5d5250b16c4e1b5e5f7f2e9f73e90f` |
|
||||||
|
|
||||||
|
Runtime artifacts remain outside Git under `.runtime/`.
|
||||||
|
|
||||||
|
## 10. Honest limitations
|
||||||
|
|
||||||
|
- No ground-truth identities are available; tracklet stitching is evaluated
|
||||||
|
through continuity metrics, not IDF1/HOTA.
|
||||||
|
- Two-frame 2D and 350 ms 3D holds improve continuity but are not fresh sensor
|
||||||
|
evidence.
|
||||||
|
- E22 does not improve detector recall, class taxonomy, fisheye model quality
|
||||||
|
or LiDAR association correctness.
|
||||||
|
- Semantic boundaries are not optical-flow propagated.
|
||||||
|
- The temporal code is streaming-equivalent, but this run consumed a recorded
|
||||||
|
E21 result rather than the physical live K1 transport.
|
||||||
|
- Navigation and safety authority remain disabled.
|
||||||
|
|
||||||
|
## 11. Next gate
|
||||||
|
|
||||||
|
The next experiment should move this exact bounded temporal state machine into
|
||||||
|
the warm worker's live result path and rerun the same 60-second 1× envelope.
|
||||||
|
The acceptance question is no longer “does it look calmer?” but:
|
||||||
|
|
||||||
|
1. does inline E22 preserve detector/semantic throughput and E21 age budgets;
|
||||||
|
2. do real latest-wins replacements remain bounded;
|
||||||
|
3. do operator-visible holds carry freshness state end to end;
|
||||||
|
4. does memory remain flat during a longer replay;
|
||||||
|
5. only after that, does a physical K1 shadow run reproduce the recorded gate.
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"schema_version": "missioncore.e22-temporal-stability-profile/v1",
|
||||||
|
"profile_id": "lab-e22-bounded-temporal-stability-v1",
|
||||||
|
"mode": "recorded-streaming-qualification",
|
||||||
|
"source": {
|
||||||
|
"source_id": "sensor.camera.right",
|
||||||
|
"resolution": [
|
||||||
|
800,
|
||||||
|
600
|
||||||
|
],
|
||||||
|
"calibration_slot": "camera_1",
|
||||||
|
"calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
|
||||||
|
},
|
||||||
|
"tracking_2d": {
|
||||||
|
"center_alpha_low": 0.32,
|
||||||
|
"center_alpha_high": 0.78,
|
||||||
|
"size_alpha_low": 0.22,
|
||||||
|
"size_alpha_high": 0.65,
|
||||||
|
"velocity_alpha": 0.35,
|
||||||
|
"adaptive_innovation_low": 0.08,
|
||||||
|
"adaptive_innovation_high": 0.3,
|
||||||
|
"maximum_normalized_innovation": 1.2,
|
||||||
|
"hold_frames": 2,
|
||||||
|
"stitch_gap_frames": 8,
|
||||||
|
"stitch_minimum_iou": 0.42,
|
||||||
|
"stitch_maximum_normalized_center_distance": 0.22
|
||||||
|
},
|
||||||
|
"cuboids_3d": {
|
||||||
|
"center_alpha": 0.28,
|
||||||
|
"size_alpha": 0.18,
|
||||||
|
"yaw_alpha": 0.18,
|
||||||
|
"velocity_alpha": 0.25,
|
||||||
|
"maximum_center_innovation_m": 2.0,
|
||||||
|
"maximum_yaw_innovation_degrees": 55.0,
|
||||||
|
"hold_seconds": 0.35
|
||||||
|
},
|
||||||
|
"semantic": {
|
||||||
|
"mode": "spatially-supported-streaming-hysteresis-v1",
|
||||||
|
"minimum_same_label_neighbors": 3,
|
||||||
|
"class_count": 16
|
||||||
|
},
|
||||||
|
"bounds": {
|
||||||
|
"maximum_track_states": 128,
|
||||||
|
"maximum_track_idle_seconds": 1.25,
|
||||||
|
"semantic_history_masks": 1
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"minimum_2d_acceleration_p95_reduction_fraction": 0.2,
|
||||||
|
"minimum_3d_center_step_p95_reduction_fraction": 0.2,
|
||||||
|
"minimum_3d_yaw_step_p95_reduction_fraction": 0.2,
|
||||||
|
"minimum_semantic_unsupported_change_reduction_fraction": 0.1,
|
||||||
|
"maximum_camera_frame_processing_p95_ms": 5.0,
|
||||||
|
"maximum_semantic_frame_processing_p95_ms": 30.0,
|
||||||
|
"maximum_track_states_observed": 128
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"commands_enabled": false,
|
||||||
|
"navigation_or_safety_accepted": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,9 @@ from .jobs import (
|
||||||
)
|
)
|
||||||
from .lab_instances import (
|
from .lab_instances import (
|
||||||
PublishedIntegratedLabInstance,
|
PublishedIntegratedLabInstance,
|
||||||
|
PublishedTemporalLabInstance,
|
||||||
publish_e21_lab_instance,
|
publish_e21_lab_instance,
|
||||||
|
publish_e22_lab_instance,
|
||||||
publish_integrated_lab_instance,
|
publish_integrated_lab_instance,
|
||||||
)
|
)
|
||||||
from .live_perception import (
|
from .live_perception import (
|
||||||
|
|
@ -115,6 +117,7 @@ __all__ = [
|
||||||
"LivePerceptionIngress",
|
"LivePerceptionIngress",
|
||||||
"IntegratedPerceptionOverlayStore",
|
"IntegratedPerceptionOverlayStore",
|
||||||
"PublishedIntegratedLabInstance",
|
"PublishedIntegratedLabInstance",
|
||||||
|
"PublishedTemporalLabInstance",
|
||||||
"IntegratedPerceptionResult",
|
"IntegratedPerceptionResult",
|
||||||
"LiveReplayQualificationResult",
|
"LiveReplayQualificationResult",
|
||||||
"MultiratePerceptionArtifact",
|
"MultiratePerceptionArtifact",
|
||||||
|
|
@ -144,6 +147,7 @@ __all__ = [
|
||||||
"validate_live_replay_qualification_result",
|
"validate_live_replay_qualification_result",
|
||||||
"validate_integrated_perception_result",
|
"validate_integrated_perception_result",
|
||||||
"publish_e21_lab_instance",
|
"publish_e21_lab_instance",
|
||||||
|
"publish_e22_lab_instance",
|
||||||
"publish_integrated_lab_instance",
|
"publish_integrated_lab_instance",
|
||||||
"validate_multirate_perception_qualification_result",
|
"validate_multirate_perception_qualification_result",
|
||||||
"prepare_recorded_qualification_slice",
|
"prepare_recorded_qualification_slice",
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ from .integrated_perception import (
|
||||||
validate_integrated_perception_result,
|
validate_integrated_perception_result,
|
||||||
)
|
)
|
||||||
from .jobs import CameraComputeJob, validate_camera_compute_job
|
from .jobs import CameraComputeJob, validate_camera_compute_job
|
||||||
|
from .temporal_stability import (
|
||||||
|
TemporalStabilityBuild,
|
||||||
|
build_temporal_stability_result,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -36,6 +40,14 @@ class PublishedIntegratedLabInstance:
|
||||||
result: IntegratedPerceptionResult
|
result: IntegratedPerceptionResult
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class PublishedTemporalLabInstance:
|
||||||
|
binding: LabSessionBinding
|
||||||
|
job: CameraComputeJob
|
||||||
|
result: IntegratedPerceptionResult
|
||||||
|
build: TemporalStabilityBuild
|
||||||
|
|
||||||
|
|
||||||
def publish_integrated_lab_instance(
|
def publish_integrated_lab_instance(
|
||||||
*,
|
*,
|
||||||
repository_root: Path,
|
repository_root: Path,
|
||||||
|
|
@ -241,6 +253,114 @@ def publish_e21_lab_instance(
|
||||||
return PublishedIntegratedLabInstance(binding=binding, job=lab_job, result=validated)
|
return PublishedIntegratedLabInstance(binding=binding, job=lab_job, result=validated)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_e22_lab_instance(
|
||||||
|
*,
|
||||||
|
repository_root: Path,
|
||||||
|
source_result_root: Path,
|
||||||
|
profile_path: Path,
|
||||||
|
lab_session_id: str,
|
||||||
|
lab_id: str,
|
||||||
|
display_name: str,
|
||||||
|
) -> PublishedTemporalLabInstance:
|
||||||
|
"""Derive and publish one bounded temporal-stability LAB comparison."""
|
||||||
|
|
||||||
|
root = repository_root.expanduser().resolve(strict=True)
|
||||||
|
jobs_root = root / ".runtime" / "compute-jobs"
|
||||||
|
results_root = root / ".runtime" / "compute-experiments" / "e10" / "worker-results"
|
||||||
|
packs_root = root / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
|
||||||
|
source_path = source_result_root.expanduser().resolve(strict=True)
|
||||||
|
source_document = _read_object(source_path / "result.json", source_path)
|
||||||
|
identity = source_document.get("identity")
|
||||||
|
if not isinstance(identity, dict) or not isinstance(identity.get("job_id"), str):
|
||||||
|
raise SessionIntegrityError("E22 source has no job identity")
|
||||||
|
source = validate_integrated_perception_result(
|
||||||
|
jobs_root / identity["job_id"],
|
||||||
|
source_path,
|
||||||
|
packs_root,
|
||||||
|
)
|
||||||
|
if not source.accepted:
|
||||||
|
raise SessionIntegrityError("E22 source result is not accepted")
|
||||||
|
|
||||||
|
lab_job = _publish_lab_job(source.job, jobs_root, lab_session_id)
|
||||||
|
lab_pack = _publish_lab_pack(source, lab_job, packs_root, lab_session_id)
|
||||||
|
build = build_temporal_stability_result(
|
||||||
|
source=source,
|
||||||
|
lab_job=lab_job,
|
||||||
|
lab_pack=lab_pack,
|
||||||
|
results_root=results_root,
|
||||||
|
profile_path=profile_path,
|
||||||
|
)
|
||||||
|
validated = validate_integrated_perception_result(
|
||||||
|
lab_job.job_root,
|
||||||
|
build.result_root,
|
||||||
|
packs_root,
|
||||||
|
)
|
||||||
|
if not validated.accepted:
|
||||||
|
failed = [
|
||||||
|
name
|
||||||
|
for name, accepted in build.report["acceptance"]["checks"].items()
|
||||||
|
if not accepted
|
||||||
|
]
|
||||||
|
raise SessionIntegrityError(
|
||||||
|
f"E22 temporal acceptance failed: {', '.join(failed)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
store = SessionStore(root)
|
||||||
|
source_lab = store.get_lab_instance(source.job.session_id)
|
||||||
|
source_session_id = (
|
||||||
|
source.job.session_id
|
||||||
|
if source_lab is None
|
||||||
|
else source_lab.source_session_id
|
||||||
|
)
|
||||||
|
publish_lab_replay_cache(
|
||||||
|
store.data_dir,
|
||||||
|
source_session_id=source_session_id,
|
||||||
|
lab_session_id=lab_session_id,
|
||||||
|
timeline_start_ns=round(validated.timeline_start_seconds * 1_000_000_000),
|
||||||
|
timeline_end_ns=round(validated.timeline_end_seconds * 1_000_000_000),
|
||||||
|
)
|
||||||
|
metrics = build.report["metrics"]
|
||||||
|
binding = store.publish_lab_instance(
|
||||||
|
session_id=lab_session_id,
|
||||||
|
source_session_id=source_session_id,
|
||||||
|
display_name=display_name,
|
||||||
|
lab_id=lab_id,
|
||||||
|
result_kind="e22-temporal-stability",
|
||||||
|
result_id=validated.result_id,
|
||||||
|
source_result_id=source.result_id,
|
||||||
|
config_sha256=build.profile_sha256,
|
||||||
|
run_created_at_utc=validated.created_at_utc,
|
||||||
|
duration_seconds=(
|
||||||
|
validated.timeline_end_seconds - validated.timeline_start_seconds
|
||||||
|
),
|
||||||
|
include_recorded_media=False,
|
||||||
|
provenance={
|
||||||
|
"schema_version": "missioncore.e22-lab-publication/v1",
|
||||||
|
"storage_mode": "bounded-derived-replay-and-temporal-projection",
|
||||||
|
"source_result_id": source.result_id,
|
||||||
|
"source_lab_session_id": (
|
||||||
|
None if source_lab is None else source_lab.session_id
|
||||||
|
),
|
||||||
|
"source_payloads_mutated": False,
|
||||||
|
"lookahead_frames": 0,
|
||||||
|
"peak_track_states": metrics["runtime"]["peak_track_states"],
|
||||||
|
"camera_frame_processing_p95_ms": metrics["runtime"][
|
||||||
|
"camera_frame_processing_ms"
|
||||||
|
]["p95"],
|
||||||
|
"semantic_frame_processing_p95_ms": metrics["runtime"][
|
||||||
|
"semantic_frame_processing_ms"
|
||||||
|
]["p95"],
|
||||||
|
"quality_reductions": metrics["reductions"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return PublishedTemporalLabInstance(
|
||||||
|
binding=binding,
|
||||||
|
job=lab_job,
|
||||||
|
result=validated,
|
||||||
|
build=build,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_e21_inputs(
|
def _validate_e21_inputs(
|
||||||
e21_root: Path,
|
e21_root: Path,
|
||||||
worker_root: Path,
|
worker_root: Path,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -20,6 +20,7 @@ from k1link.artifacts import write_json_atomic
|
||||||
from k1link.compute import (
|
from k1link.compute import (
|
||||||
prepare_camera_compute_job,
|
prepare_camera_compute_job,
|
||||||
publish_e21_lab_instance,
|
publish_e21_lab_instance,
|
||||||
|
publish_e22_lab_instance,
|
||||||
publish_integrated_lab_instance,
|
publish_integrated_lab_instance,
|
||||||
)
|
)
|
||||||
from k1link.device_plugins.xgrids_k1.analyze import (
|
from k1link.device_plugins.xgrids_k1.analyze import (
|
||||||
|
|
@ -433,6 +434,69 @@ def publish_e21_lab(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@lab_app.command("publish-e22")
|
||||||
|
def publish_e22_lab(
|
||||||
|
result: Annotated[
|
||||||
|
Path,
|
||||||
|
typer.Option(
|
||||||
|
exists=True,
|
||||||
|
file_okay=False,
|
||||||
|
readable=True,
|
||||||
|
resolve_path=True,
|
||||||
|
help="Accepted integrated result used as the immutable E22 baseline.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
profile: Annotated[
|
||||||
|
Path,
|
||||||
|
typer.Option(
|
||||||
|
exists=True,
|
||||||
|
dir_okay=False,
|
||||||
|
readable=True,
|
||||||
|
resolve_path=True,
|
||||||
|
help="Bounded E22 temporal-stability profile.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
session_id: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option("--session-id", help="New immutable LAB session id."),
|
||||||
|
],
|
||||||
|
lab_id: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option("--lab-id", help="LAB marker, for example 'LAB E22'."),
|
||||||
|
],
|
||||||
|
display_name: Annotated[
|
||||||
|
str,
|
||||||
|
typer.Option("--display-name", help="Operator-facing saved-session title."),
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Build and publish a bounded 2D/3D/semantic temporal-stability run."""
|
||||||
|
|
||||||
|
repository_root = Path(__file__).resolve().parents[4]
|
||||||
|
try:
|
||||||
|
published = publish_e22_lab_instance(
|
||||||
|
repository_root=repository_root,
|
||||||
|
source_result_root=result,
|
||||||
|
profile_path=profile,
|
||||||
|
lab_session_id=session_id,
|
||||||
|
lab_id=lab_id,
|
||||||
|
display_name=display_name,
|
||||||
|
)
|
||||||
|
except (OSError, SessionIntegrityError, ValueError) as exc:
|
||||||
|
console.print(f"[red]E22 LAB publication failed:[/red] {exc}")
|
||||||
|
raise typer.Exit(code=2) from exc
|
||||||
|
reductions = published.build.report["metrics"]["reductions"]
|
||||||
|
console.print(
|
||||||
|
"[green]E22 LAB instance published.[/green] "
|
||||||
|
f"session={published.binding.session_id}; "
|
||||||
|
f"source={published.binding.source_session_id}; "
|
||||||
|
f"result={published.binding.result_id}; "
|
||||||
|
f"2d_reduction={reductions['tracking_2d_acceleration_p95_fraction']:.3f}; "
|
||||||
|
f"3d_center_reduction={reductions['cuboid_center_step_p95_fraction']:.3f}; "
|
||||||
|
f"3d_yaw_reduction={reductions['cuboid_yaw_step_p95_fraction']:.3f}; "
|
||||||
|
"source_payloads_mutated=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command("serve")
|
@app.command("serve")
|
||||||
def serve_console(
|
def serve_console(
|
||||||
port: Annotated[
|
port: Annotated[
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.compute.temporal_stability import (
|
||||||
|
TemporalStabilizer,
|
||||||
|
read_profile,
|
||||||
|
stabilize_semantic_masks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _profile() -> dict[str, object]:
|
||||||
|
root = Path(__file__).resolve().parents[1]
|
||||||
|
profile, digest = read_profile(
|
||||||
|
root / "experiments" / "perception" / "e22_temporal_stability_profile.json"
|
||||||
|
)
|
||||||
|
assert len(digest) == 64
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
|
def _object(track_id: int, box: list[float]) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"association_group": "vehicle",
|
||||||
|
"bbox_xyxy": box,
|
||||||
|
"candidate_projected_points": 20,
|
||||||
|
"clustered_points": 14,
|
||||||
|
"completion_fraction": 0.9,
|
||||||
|
"cuboid_center_map": [10.0, 2.0, 0.8],
|
||||||
|
"cuboid_half_size": [2.25, 0.925, 0.775],
|
||||||
|
"cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
"cuboid_status": "accepted-class-prior-amodal-v1",
|
||||||
|
"distance_median_m": 10.2,
|
||||||
|
"distance_p10_m": 9.8,
|
||||||
|
"distance_smoothed_m": 10.1,
|
||||||
|
"geometry": "class-prior-completed-from-visible-lidar-support",
|
||||||
|
"ground_rejected_points": 0,
|
||||||
|
"ground_z_map": 0.0,
|
||||||
|
"label": "car",
|
||||||
|
"observed_cuboid_center_map": [10.0, 2.0, 0.8],
|
||||||
|
"observed_cuboid_half_size": [2.25, 0.925, 0.775],
|
||||||
|
"observed_cuboid_quaternion_xyzw": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
"orientation_source": "support-pca",
|
||||||
|
"pre_ground_clustered_points": 14,
|
||||||
|
"score": 0.9,
|
||||||
|
"semantic_compatible_points": 18,
|
||||||
|
"support_coverage_fraction": 0.8,
|
||||||
|
"support_ground_z_map": None,
|
||||||
|
"temporal_status": "confirmed",
|
||||||
|
"track_id": track_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_e22_profile_is_bounded_and_has_no_control_authority() -> None:
|
||||||
|
profile = _profile()
|
||||||
|
|
||||||
|
assert profile["bounds"] == {
|
||||||
|
"maximum_track_states": 128,
|
||||||
|
"maximum_track_idle_seconds": 1.25,
|
||||||
|
"semantic_history_masks": 1,
|
||||||
|
}
|
||||||
|
assert profile["authority"] == {
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_e22_stitches_one_tracklet_and_marks_short_holds() -> None:
|
||||||
|
stabilizer = TemporalStabilizer(_profile())
|
||||||
|
first = stabilizer.update(
|
||||||
|
frame_index=0,
|
||||||
|
session_seconds=0.0,
|
||||||
|
objects=[_object(10, [100.0, 100.0, 200.0, 200.0])],
|
||||||
|
)
|
||||||
|
second = stabilizer.update(
|
||||||
|
frame_index=1,
|
||||||
|
session_seconds=0.1,
|
||||||
|
objects=[_object(11, [102.0, 100.0, 202.0, 200.0])],
|
||||||
|
)
|
||||||
|
held = stabilizer.update(
|
||||||
|
frame_index=2,
|
||||||
|
session_seconds=0.2,
|
||||||
|
objects=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert first[0]["track_id"] == 10
|
||||||
|
assert second[0]["track_id"] == 10
|
||||||
|
assert second[0]["temporal_source_track_id"] == 11
|
||||||
|
assert second[0]["temporal_2d_status"] == "stitched-observed"
|
||||||
|
assert held[0]["temporal_2d_status"] == "held-1-frame"
|
||||||
|
assert held[0]["cuboid_status"] == "accepted-temporal-hold-e22-v1"
|
||||||
|
assert stabilizer.stitched_tracks == 1
|
||||||
|
assert stabilizer.held_2d == 1
|
||||||
|
assert stabilizer.peak_states <= 128
|
||||||
|
|
||||||
|
|
||||||
|
def test_e22_semantic_hysteresis_removes_island_but_keeps_supported_region() -> None:
|
||||||
|
masks = np.zeros((2, 600, 800), dtype=np.uint8)
|
||||||
|
masks[1, 20, 20] = 4
|
||||||
|
masks[1, 100:110, 100:110] = 7
|
||||||
|
|
||||||
|
stabilized, metrics, durations = stabilize_semantic_masks(
|
||||||
|
masks,
|
||||||
|
minimum_same_label_neighbors=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert stabilized[1, 20, 20] == 0
|
||||||
|
assert np.all(stabilized[1, 102:108, 102:108] == 7)
|
||||||
|
assert (
|
||||||
|
metrics["stabilized_unsupported_change_fraction"]["mean"]
|
||||||
|
< metrics["baseline_unsupported_change_fraction"]["mean"]
|
||||||
|
)
|
||||||
|
assert len(durations) == 1
|
||||||
|
assert json.dumps(metrics, allow_nan=False)
|
||||||
Loading…
Reference in New Issue