From 3a2134d4ed5d5bdf2348abb887eb19abba5d84cb Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Tue, 21 Jul 2026 21:24:03 -0400 Subject: [PATCH 1/3] Hide the Import Registration section on stereo datasets Per-camera registration import is meant for multicam rigs; stereo datasets use the calibration (camera file) import instead, so the Import menu no longer shows the registration buttons when the dataset subtype is stereo. Co-Authored-By: Claude Fable 5 --- client/dive-common/components/ImportAnnotations.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/dive-common/components/ImportAnnotations.vue b/client/dive-common/components/ImportAnnotations.vue index 70fcc365c..f657324ae 100644 --- a/client/dive-common/components/ImportAnnotations.vue +++ b/client/dive-common/components/ImportAnnotations.vue @@ -85,10 +85,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 From 2e65c875db0e804ce1a9a2c89ff8ecfe82facf96 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Tue, 21 Jul 2026 21:36:58 -0400 Subject: [PATCH 2/3] Align Warp to All with Overwrite; stereo warp via interactive stereo The Import menu's "Warp to All" checkbox now lines up with the Overwrite checkbox instead of sitting partially below it. On stereo datasets the option now works through the interactive stereo service: when interactive stereo is enabled and a camera calibration is present, checking it warps every imported detection from the active camera to the other one (head/tail lines as lines, which also computes the stereo measurement; polygons as polygons; plain boxes as boxes), skipping frames the other camera already has. When interactive stereo is off or no calibration is loaded the option is greyed out, with the hint explaining what is missing. Multi-camera (non-stereo) datasets keep the registration-based warp. Co-Authored-By: Claude Fable 5 --- .../components/ImportAnnotations.vue | 59 +++++++++---- .../frontend/components/ViewerLoader.vue | 85 ++++++++++++++++++- 2 files changed, 126 insertions(+), 18 deletions(-) diff --git a/client/dive-common/components/ImportAnnotations.vue b/client/dive-common/components/ImportAnnotations.vue index f657324ae..014339f9d 100644 --- a/client/dive-common/components/ImportAnnotations.vue +++ b/client/dive-common/components/ImportAnnotations.vue @@ -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'; @@ -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; @@ -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` @@ -197,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(); + } } } } @@ -502,7 +529,7 @@ export default defineComponent({ - +
diff --git a/client/platform/desktop/frontend/components/ViewerLoader.vue b/client/platform/desktop/frontend/components/ViewerLoader.vue index edc887870..17c1b12ae 100644 --- a/client/platform/desktop/frontend/components/ViewerLoader.vue +++ b/client/platform/desktop/frontend/components/ViewerLoader.vue @@ -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 @@ -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 @@ -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. @@ -1829,6 +1908,7 @@ export default defineComponent({ stereoLengthMessage, closeStereoLoadingDialog, handleStereoAnnotationComplete, + handleStereoWarpImported, handleStereoAnnotationReset, handleStereoSegmentationFinalize, handleStereoTrackLinked, @@ -1897,6 +1977,7 @@ export default defineComponent({ v-bind="{ buttonOptions, menuOptions, readOnlyMode }" block-on-unsaved @calibration-imported="onCalibrationImported" + @stereo-warp-imported="handleStereoWarpImported" /> Date: Tue, 21 Jul 2026 21:44:26 -0400 Subject: [PATCH 3/3] Put Warp to All on the same line as Overwrite The persistent hint under the checkbox stretched it and wrapped the row, leaving Warp to All below Overwrite. The two checkboxes now sit side by side in a no-wrap row (dense, no reserved detail space) and the availability hint moved into a hover tooltip. Co-Authored-By: Claude Fable 5 --- .../components/ImportAnnotations.vue | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/client/dive-common/components/ImportAnnotations.vue b/client/dive-common/components/ImportAnnotations.vue index 014339f9d..ad0d99fb2 100644 --- a/client/dive-common/components/ImportAnnotations.vue +++ b/client/dive-common/components/ImportAnnotations.vue @@ -529,22 +529,38 @@ export default defineComponent({ - + - + bottom + :disabled="!warpToAllCamerasHint" + open-delay="200" + > + + {{ warpToAllCamerasHint }} +