Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 70 additions & 25 deletions client/dive-common/components/ImportAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from 'vue';
import { useApi } from 'dive-common/apispec';
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { clientSettings } from 'dive-common/store/settings';
import { clientSettings, isStereoInteractiveModeEnabled } from 'dive-common/store/settings';
import clearLengthAttributes from 'dive-common/utils/clearLengthAttributes';
import warpAnnotationsAcrossCameras from 'dive-common/utils/warpAnnotationsAcrossCameras';
import { cloneDeep } from 'lodash';
Expand Down Expand Up @@ -47,7 +47,7 @@ export default defineComponent({
default: false,
},
},
emits: ['calibration-imported'],
emits: ['calibration-imported', 'stereo-warp-imported'],
setup(props, { emit }) {
const api = useApi();
const { openFromDisk, importAnnotationFile } = api;
Expand All @@ -57,13 +57,35 @@ export default defineComponent({
const selectedCamera = useSelectedCamera();
const alignedView = useAlignedView();
const isMulticamDataset = computed(() => cameraStore.camMap.value.size > 1);
// Warping detections onto other cameras requires the whole rig to be
// registered (a native->reference transform for every camera).
const canWarpToAllCameras = computed(
() => isMulticamDataset.value && alignedView.available.value,
const isStereoDataset = computed(() => props.subType === 'stereo');
// Stereo warping goes through the interactive stereo service, which
// needs its features enabled and a camera calibration loaded.
const stereoWarpAvailable = computed(
() => isStereoDataset.value && isStereoInteractiveModeEnabled()
&& !!props.calibrationFile,
);
// Multicam (non-stereo) warping instead requires the whole rig to be
// registered (a native->reference transform for every camera).
const canWarpToAllCameras = computed(() => {
if (!isMulticamDataset.value) {
return false;
}
if (isStereoDataset.value) {
return stereoWarpAvailable.value;
}
return alignedView.available.value;
});
const warpToAllCameras = ref(false);
const warpToAllCamerasHint = computed(() => {
if (isStereoDataset.value) {
if (!isStereoInteractiveModeEnabled()) {
return 'Requires interactive stereo to be enabled';
}
if (!props.calibrationFile) {
return 'Requires a camera calibration';
}
return 'Warps imported detections to the other camera';
}
const progress = alignedView.registrationProgress.value;
return progress
? `${progress.registered}/${progress.total} cameras registered`
Expand All @@ -85,10 +107,12 @@ export default defineComponent({
() => !!api.importCalibrationFile && props.subType === 'stereo',
);
// Camera registration import (per-camera transform files) is meaningful
// for any multicam dataset.
// for multicam datasets; stereo rigs use the calibration (camera file)
// import instead, so the registration section is hidden there.
const cameraRegistration = useCameraRegistration();
const registrationSupported = computed(
() => !!api.importCameraRegistration && isMulticamDataset.value,
() => !!api.importCameraRegistration && isMulticamDataset.value
&& props.subType !== 'stereo',
);
// One import button per non-reference camera pair, labeled in the
// direction of the mapping -- "Import ir → eo" registers ir onto the
Expand Down Expand Up @@ -195,15 +219,20 @@ export default defineComponent({
warpToAllCameras.value
&& canWarpToAllCameras.value
&& activeCameraName.value
&& alignedView.toReference.value
) {
const warped = warpAnnotationsAcrossCameras(
cameraStore,
alignedView.toReference.value,
activeCameraName.value,
);
if (warped.tracks > 0) {
await save();
if (isStereoDataset.value) {
// The platform (desktop) performs the warp through the
// interactive stereo service, one detection at a time.
emit('stereo-warp-imported', activeCameraName.value);
} else if (alignedView.toReference.value) {
const warped = warpAnnotationsAcrossCameras(
cameraStore,
alignedView.toReference.value,
activeCameraName.value,
);
if (warped.tracks > 0) {
await save();
}
}
}
}
Expand Down Expand Up @@ -500,22 +529,38 @@ export default defineComponent({
</template>
</v-combobox>
</v-row>
<v-row>
<v-row
class="flex-nowrap"
align="center"
>
<v-checkbox
:input-value="!additive"
label="Overwrite"
class="mt-2"
dense
hide-details
@change="additive = !$event"
/>
<v-checkbox
<v-tooltip
v-if="isMulticamDataset"
v-model="warpToAllCameras"
:disabled="!canWarpToAllCameras"
label="Warp to All"
:hint="warpToAllCamerasHint"
persistent-hint
class="ml-4"
/>
bottom
:disabled="!warpToAllCamerasHint"
open-delay="200"
>
<template #activator="{ on }">
<div v-on="on">
<v-checkbox
v-model="warpToAllCameras"
:disabled="!canWarpToAllCameras"
label="Warp to All"
class="mt-2 ml-2"
dense
hide-details
/>
</div>
</template>
<span>{{ warpToAllCamerasHint }}</span>
</v-tooltip>
</v-row>
<div v-if="additive">
<div
Expand Down
85 changes: 83 additions & 2 deletions client/platform/desktop/frontend/components/ViewerLoader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1356,7 +1356,10 @@ export default defineComponent({
* Handle stereo annotation complete event from Viewer
* Warps annotation from source camera to the other camera
*/
async function handleStereoAnnotationComplete(params: StereoAnnotationCompleteParams) {
async function handleStereoAnnotationComplete(
params: StereoAnnotationCompleteParams,
forceAutoCompute = false,
) {
// This handler only fires on human edits — the stereo warp writes
// geometry directly, bypassing the annotation-complete event — so the
// camera the user just drew/edited is now human-authored. Mark it
Expand Down Expand Up @@ -1397,7 +1400,10 @@ export default defineComponent({
// - autoCompute: warp the annotation to the other camera when it has no
// detection for it yet (or its line is still machine-generated).
const updateLengths = clientSettings.stereoSettings.updateLengthsOnModify;
const autoCompute = clientSettings.stereoSettings.autoComputeOtherCamera;
// forceAutoCompute bypasses the auto-compute preference for explicit
// requests (the Import menu's "Warp to All").
const autoCompute = forceAutoCompute
|| clientSettings.stereoSettings.autoComputeOtherCamera;

if (params.type === 'line') {
const otherIsHuman = otherHasFeature
Expand Down Expand Up @@ -1688,6 +1694,79 @@ export default defineComponent({
}
}

/**
* Import menu "Warp to All" on a stereo dataset: warp every detection
* loaded on the source camera to the other camera through the
* interactive stereo service. Reuses the per-annotation transfer
* (service-ready wait, frame preparation) with the auto-compute
* preference bypassed — the user asked for the warp explicitly. Frames
* the other camera already has are left untouched. Head/tail lines
* transfer as lines (which also computes the stereo measurement),
* polygons as polygons, and plain boxes as boxes.
*/
async function handleStereoWarpImported(sourceCamera: string) {
const viewer = viewerRef.value;
if (!viewer) return;
const { cameraStore, multiCamList } = viewer;
if (multiCamList.length < 2) return;
const otherCamera = multiCamList.find((c: string) => c !== sourceCamera);
const store = cameraStore.camMap.value.get(sourceCamera)?.trackStore;
if (!store || !otherCamera) return;
const jobs: StereoAnnotationCompleteParams[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
store.annotationMap.forEach((track: any) => {
if (typeof track.getFeature !== 'function') return;
(track.featureIndex || []).forEach((frameNum: number) => {
const [feature] = track.getFeature(frameNum);
if (!feature || !feature.keyframe || !feature.bounds) return;
// Only fill in missing counterparts; never touch geometry the
// other camera already has for this frame.
const otherTrack = cameraStore.getPossibleTrack(track.id, otherCamera);
const [otherFeature] = otherTrack ? otherTrack.getFeature(frameNum) : [null];
if (otherFeature) return;
const geoFeatures = feature.geometry?.features || [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const line = geoFeatures.find((g: any) => g.geometry?.type === 'LineString'
&& g.geometry.coordinates?.length === 2);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const poly = geoFeatures.find((g: any) => g.geometry?.type === 'Polygon');
const base = { camera: sourceCamera, trackId: track.id, frameNum };
if (line) {
jobs.push({
...base,
type: 'line',
line: line.geometry.coordinates as [[number, number], [number, number]],
key: line.properties?.key ?? '',
});
} else if (poly) {
jobs.push({
...base,
type: 'polygon',
polygon: poly.geometry.coordinates[0] as [number, number][],
key: poly.properties?.key ?? '',
});
} else {
jobs.push({
...base,
type: 'box',
bounds: feature.bounds as [number, number, number, number],
});
}
});
});
for (let i = 0; i < jobs.length; i += 1) {
try {
// eslint-disable-next-line no-await-in-loop
await handleStereoAnnotationComplete(jobs[i], true);
} catch (err) {
console.warn('[Stereo] Import warp failed:', err);
}
}
if (jobs.length) {
await viewer.save();
}
}

/**
* Undo stereo side effects from interactive segmentation on reset.
* Restores the other camera and clears saved undo state for the frame.
Expand Down Expand Up @@ -1829,6 +1908,7 @@ export default defineComponent({
stereoLengthMessage,
closeStereoLoadingDialog,
handleStereoAnnotationComplete,
handleStereoWarpImported,
handleStereoAnnotationReset,
handleStereoSegmentationFinalize,
handleStereoTrackLinked,
Expand Down Expand Up @@ -1897,6 +1977,7 @@ export default defineComponent({
v-bind="{ buttonOptions, menuOptions, readOnlyMode }"
block-on-unsaved
@calibration-imported="onCalibrationImported"
@stereo-warp-imported="handleStereoWarpImported"
/>
<Export
v-if="datasets[id]"
Expand Down
Loading