feat(lab): project system obstacles into fisheye
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
useM4ThreatTimelineMetadata,
|
||||
} from "./useM4ThreatTimeline";
|
||||
import { useE47SemanticTimelineFrame } from "./useE47SemanticTimeline";
|
||||
import { buildM4StaticObstacleBoxes } from "./m4StaticObstacleBoxes";
|
||||
|
||||
type M4ThreatMediaMode = "video" | "camera";
|
||||
type M4ThreatMediaSelection = M4ThreatMediaMode | "none";
|
||||
@@ -132,6 +133,7 @@ export function M4ReplayThreatVisual({
|
||||
const [showMediaSemantic, setShowMediaSemantic] = useState(true);
|
||||
const [showSpatialSemantic, setShowSpatialSemantic] = useState(true);
|
||||
const [showMediaPoints, setShowMediaPoints] = useState(false);
|
||||
const [showStaticObstacles, setShowStaticObstacles] = useState(true);
|
||||
const [splitPrimarySize, setSplitPrimarySize] = useState(50);
|
||||
const [splitOrientation, setSplitOrientation] = useState<SplitPaneOrientation>(() => (
|
||||
typeof window !== "undefined" && window.matchMedia("(max-width: 900px)").matches
|
||||
@@ -287,9 +289,26 @@ export function M4ReplayThreatVisual({
|
||||
};
|
||||
});
|
||||
}, [frame, metadata.timeline, reviewAnchors, showReviewAnchorBoxes]);
|
||||
const staticObstacleBoxes = useMemo<readonly RecordedEvidenceBox[]>(() => {
|
||||
const timeline = metadata.timeline;
|
||||
if (
|
||||
!frame
|
||||
|| !timeline?.cameraObstacleProjectionDelivery
|
||||
|| !showStaticObstacles
|
||||
) return [];
|
||||
return buildM4StaticObstacleBoxes(
|
||||
frame.metricObstacles,
|
||||
timeline.imageWidth,
|
||||
timeline.imageHeight,
|
||||
);
|
||||
}, [frame, metadata.timeline, showStaticObstacles]);
|
||||
const activeBoxes = useMemo(
|
||||
() => [...boxes(frame?.cameraProposals ?? []), ...reviewAnchorBoxes],
|
||||
[frame, reviewAnchorBoxes],
|
||||
() => [
|
||||
...boxes(frame?.cameraProposals ?? []),
|
||||
...staticObstacleBoxes,
|
||||
...reviewAnchorBoxes,
|
||||
],
|
||||
[frame, reviewAnchorBoxes, staticObstacleBoxes],
|
||||
);
|
||||
const semanticClasses = useMemo<readonly RecordedEvidenceSemanticClass[]>(
|
||||
() => semantic?.taxonomy.map((item) => ({
|
||||
@@ -359,7 +378,8 @@ export function M4ReplayThreatVisual({
|
||||
(item) => item.state === "retained",
|
||||
) ?? [];
|
||||
const lowStepObstacles = spatialFrame?.metricObstacles.filter(
|
||||
(item) => item.occupancySource !== "baseline",
|
||||
(item) => item.occupancySource !== "baseline"
|
||||
&& (item.state === "current" || item.state === "retained"),
|
||||
) ?? [];
|
||||
const nearest = spatialFrame?.metricObstacles
|
||||
.map((item) => item.assessment.closestApproachM)
|
||||
@@ -456,7 +476,9 @@ export function M4ReplayThreatVisual({
|
||||
</div>
|
||||
);
|
||||
|
||||
const mediaLayerControls = semantic || metadata.timeline?.cameraPointDelivery ? (
|
||||
const mediaLayerControls = semantic
|
||||
|| metadata.timeline?.cameraPointDelivery
|
||||
|| metadata.timeline?.cameraObstacleProjectionDelivery ? (
|
||||
<div
|
||||
className="m4-replay-threat-visual__pane-layer-controls"
|
||||
role="group"
|
||||
@@ -485,6 +507,18 @@ export function M4ReplayThreatVisual({
|
||||
POINTS
|
||||
</Button>
|
||||
) : null}
|
||||
{metadata.timeline?.cameraObstacleProjectionDelivery ? (
|
||||
<Button
|
||||
size="compact"
|
||||
shape="pill"
|
||||
variant={showStaticObstacles ? "primary" : "secondary"}
|
||||
aria-pressed={showStaticObstacles}
|
||||
title="Автоматические рамки занятых LiDAR-компонентов · без ручной разметки"
|
||||
onClick={() => setShowStaticObstacles((visible) => !visible)}
|
||||
>
|
||||
OBSTACLES
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { RecordedEvidenceBox } from "../../components/laboratory/RecordedEvidenceVideoScene";
|
||||
import type { M4ThreatMetricVisual } from "../../core/laboratory/m4ReplayThreat";
|
||||
|
||||
const MINIMUM_BOX_SIZE_PX = 12;
|
||||
|
||||
function shortComponentId(componentId: string): string {
|
||||
const finalSegment = componentId.split(/[-_]/).pop();
|
||||
return finalSegment && /^\d+$/.test(finalSegment)
|
||||
? finalSegment
|
||||
: componentId.slice(-6).toUpperCase();
|
||||
}
|
||||
|
||||
function clamp(value: number, minimum: number, maximum: number): number {
|
||||
return Math.min(maximum, Math.max(minimum, value));
|
||||
}
|
||||
|
||||
function visibleBox(
|
||||
box: readonly [number, number, number, number],
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
): readonly [number, number, number, number] | null {
|
||||
const [sourceLeft, sourceTop, sourceRight, sourceBottom] = box;
|
||||
if (
|
||||
sourceRight < 0
|
||||
|| sourceBottom < 0
|
||||
|| sourceLeft >= imageWidth
|
||||
|| sourceTop >= imageHeight
|
||||
) return null;
|
||||
const centerX = (sourceLeft + sourceRight) / 2;
|
||||
const centerY = (sourceTop + sourceBottom) / 2;
|
||||
const halfWidth = Math.max((sourceRight - sourceLeft) / 2, MINIMUM_BOX_SIZE_PX / 2);
|
||||
const halfHeight = Math.max((sourceBottom - sourceTop) / 2, MINIMUM_BOX_SIZE_PX / 2);
|
||||
const left = clamp(centerX - halfWidth, 0, imageWidth - 1);
|
||||
const top = clamp(centerY - halfHeight, 0, imageHeight - 1);
|
||||
const right = clamp(centerX + halfWidth, left + 1, imageWidth);
|
||||
const bottom = clamp(centerY + halfHeight, top + 1, imageHeight);
|
||||
return [left, top, right, bottom];
|
||||
}
|
||||
|
||||
function tone(obstacle: M4ThreatMetricVisual): RecordedEvidenceBox["tone"] {
|
||||
if (obstacle.assessment.decision === "threat") return "danger";
|
||||
if (obstacle.assessment.decision === "not-threat") return "success";
|
||||
return "warning";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build camera evidence from the same world-state components shown in 3D.
|
||||
* No image detector or manual review extent participates in these boxes.
|
||||
*/
|
||||
export function buildM4StaticObstacleBoxes(
|
||||
obstacles: readonly M4ThreatMetricVisual[],
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
): readonly RecordedEvidenceBox[] {
|
||||
const result: RecordedEvidenceBox[] = [];
|
||||
for (const obstacle of obstacles) {
|
||||
if (
|
||||
obstacle.occupancySource === "baseline"
|
||||
|| (obstacle.state !== "current" && obstacle.state !== "retained")
|
||||
|| obstacle.cameraProjection === null
|
||||
) continue;
|
||||
const projection = obstacle.cameraProjection;
|
||||
const boxXyxy = visibleBox(projection.bboxXyxy, imageWidth, imageHeight);
|
||||
if (!boxXyxy) continue;
|
||||
const depth = projection.nearestDepthM.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
result.push({
|
||||
boxXyxy,
|
||||
label: `OBS #${shortComponentId(obstacle.componentId)} · ${depth} м`,
|
||||
tone: tone(obstacle),
|
||||
dashed: false,
|
||||
});
|
||||
}
|
||||
return result.sort((left, right) => {
|
||||
const leftArea = (left.boxXyxy[2] - left.boxXyxy[0]) * (left.boxXyxy[3] - left.boxXyxy[1]);
|
||||
const rightArea = (right.boxXyxy[2] - right.boxXyxy[0]) * (right.boxXyxy[3] - right.boxXyxy[1]);
|
||||
return rightArea - leftArea;
|
||||
});
|
||||
}
|
||||
@@ -130,6 +130,7 @@ export function useM4ThreatTimelineFrame({
|
||||
void fetchM4ThreatTimelineChunk(resultId, start, chunkSize, {
|
||||
signal: controller.signal,
|
||||
endpointRoot,
|
||||
cameraObstacleProjectionDelivery: timeline.cameraObstacleProjectionDelivery,
|
||||
})
|
||||
.then((chunk) => {
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
Reference in New Issue
Block a user