feat(perception): qualify bounded temporal stability

This commit is contained in:
DCCONSTRUCTIONS 2026-07-23 23:30:50 +03:00
parent 5a29a536a2
commit cfc7b062da
8 changed files with 1674 additions and 6 deletions

View File

@ -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

View File

@ -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.320.78;
- adaptive size EMA: alpha 0.220.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.

View File

@ -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
}
}

View File

@ -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",

View File

@ -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

View File

@ -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[

View File

@ -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)