Skip to content

Commit a1805cf

Browse files
Recentre the other stereo camera on the same object when synchronised panning settles (#1934)
* Recentre the other stereo camera on the same object when synchronised panning settles * Invalidate outstanding stereo pan lookups when settings or views change * accounting for resizing and not triggering the schedule of linked viewing --------- Co-authored-by: Bryon Lewis <Bryon.Lewis@kitware.com>
1 parent 670c3b5 commit a1805cf

10 files changed

Lines changed: 299 additions & 8 deletions

File tree

‎client/dive-common/components/Viewer.vue‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ import AlignedViewToggle from './AlignedViewToggle.vue';
100100
import PrimaryAttributeTrackFilter from './PrimaryAttributeTrackFilter.vue';
101101
import UserSettingsDialog from './UserSettingsDialog.vue';
102102
103+
export interface StereoViewLinkParams {
104+
camera: string;
105+
frameNum: number;
106+
point: [number, number];
107+
}
108+
export type StereoViewLinkFunc = (params: StereoViewLinkParams) => Promise<[number, number] | null>;
109+
103110
export interface ImageDataItem {
104111
url: string;
105112
filename: string;
@@ -185,6 +192,15 @@ export default defineComponent({
185192
type: Number as PropType<number | undefined>,
186193
default: undefined,
187194
},
195+
/**
196+
* Where a point on one stereo camera lands on the other, using the loaded
197+
* stereo matcher; null when it cannot be found. Lets synchronised panning
198+
* follow the same object on both cameras.
199+
*/
200+
stereoViewLink: {
201+
type: Function as PropType<StereoViewLinkFunc | undefined>,
202+
default: undefined,
203+
},
188204
},
189205
setup(props, { emit }) {
190206
const { prompt, visible } = usePrompt();
@@ -801,6 +817,32 @@ export default defineComponent({
801817
provideAutoRegisterJob(autoRegisterJob);
802818
onBeforeUnmount(() => autoRegisterJob.dispose());
803819
820+
// Linked panning: with camera controls synchronised and auto-compute on,
821+
// the other pane recentres on where this pane's centre is on its camera.
822+
const stereoViewLinkResolver = async (camera: string, point: [number, number]) => {
823+
if (!props.stereoViewLink) return null;
824+
let frameNum: number;
825+
try {
826+
frameNum = aggregateController.value.getController(camera).frame.value;
827+
} catch {
828+
return null;
829+
}
830+
return props.stereoViewLink({ camera, frameNum, point });
831+
};
832+
watch(
833+
[
834+
() => clientSettings.stereoSettings.autoComputeOtherCamera,
835+
() => props.stereoViewLink,
836+
() => multiCamList.value.length,
837+
],
838+
([autoCompute, link, cameras]) => {
839+
aggregateController.value.setViewLinkResolver(
840+
autoCompute && link && cameras === 2 ? stereoViewLinkResolver : null,
841+
);
842+
},
843+
{ immediate: true },
844+
);
845+
804846
// Provides wrappers for actions to integrate with settings
805847
const {
806848
linkingTrack,

‎client/dive-common/use/stereo/useStereoOnnxTransfer.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,10 +568,28 @@ export default function useStereoOnnxTransfer(config: StereoOnnxTransferConfig)
568568
return counts;
569569
}
570570

571+
/**
572+
* Where one point on `sourceCamera` lands on the other camera at `frameNum`,
573+
* or null when the match is rejected or the stereo setup is incomplete.
574+
* Used for linked panning, so it never reports errors to the user.
575+
*/
576+
async function warpPoint(point: Point, sourceCamera: string, frameNum: number): Promise<Point | null> {
577+
const otherCamera = getMultiCamList().find((c) => c !== sourceCamera);
578+
if (!otherCamera || getMultiCamList().length !== 2) return null;
579+
try {
580+
const [result] = await warp([point], sourceCamera, otherCamera, frameNum);
581+
return result?.accepted && Number.isFinite(result.x) && Number.isFinite(result.y)
582+
? [result.x, result.y] : null;
583+
} catch {
584+
return null;
585+
}
586+
}
587+
571588
return {
572589
handleStereoAnnotationComplete,
573590
handleStereoTrackLinked,
574591
warpAllFromCamera,
592+
warpPoint,
575593
measureAtFrame,
576594
precomputeFrame,
577595
};

‎client/platform/desktop/frontend/components/ViewerLoader.vue‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { ANNOTATION_SOURCE_QUERY } from 'dive-common/scoring/viewerNavigation';
1010
import { parseViewerFocus } from 'dive-common/review/viewerNavigation';
1111
import { useRoute, useRouter } from 'vue-router/composables';
12-
import Viewer from 'dive-common/components/Viewer.vue';
12+
import Viewer, { StereoViewLinkParams } from 'dive-common/components/Viewer.vue';
1313
import RunPipelineMenu from 'dive-common/components/RunPipelineMenu.vue';
1414
import ImportAnnotations from 'dive-common//components/ImportAnnotations.vue';
1515
import CalibrationMenu from 'dive-common/components/CalibrationMenu.vue';
@@ -1508,6 +1508,36 @@ export default defineComponent({
15081508
* result status instead — used by bulk import "Warp to All" so failures
15091509
* can be aggregated rather than cleared by the next job.
15101510
*/
1511+
/**
1512+
* Where `point` on `camera` lands on the other stereo camera, for linked
1513+
* panning. Uses whatever matcher the stereo service loaded; null when the
1514+
* service is off or the match is rejected.
1515+
*/
1516+
async function stereoViewLink(params: StereoViewLinkParams): Promise<[number, number] | null> {
1517+
if (!stereoEnabled.value) return null;
1518+
const cameras = Object.keys(stereoImagePathGetters.value);
1519+
if (cameras.length !== 2 || !cameras.includes(params.camera)) return null;
1520+
if (!(await ensureStereoFrame(params.frameNum))) return null;
1521+
const fps = stereoCameraFps.value[cameras[0]] || stereoDatasetFps || Object.values(stereoCameraFps.value)[0];
1522+
try {
1523+
const response = await stereoTransferPoints({
1524+
points: [params.point],
1525+
strict: true,
1526+
sourceCamera: params.camera === cameras[0] ? 'left' : 'right',
1527+
leftImagePath: stereoImagePathGetters.value[cameras[0]](params.frameNum),
1528+
rightImagePath: stereoImagePathGetters.value[cameras[1]](params.frameNum),
1529+
frameTime: fps ? params.frameNum / fps : undefined,
1530+
});
1531+
const point = response.transferredPoints?.[0];
1532+
if (!response.success || response.validMatches?.[0] !== true || !point?.every(Number.isFinite)) {
1533+
return null;
1534+
}
1535+
return [point[0], point[1]];
1536+
} catch {
1537+
return null;
1538+
}
1539+
}
1540+
15111541
async function handleStereoAnnotationComplete(
15121542
params: StereoAnnotationCompleteParams,
15131543
forceAutoCompute = false,
@@ -2188,6 +2218,7 @@ export default defineComponent({
21882218
stereoLengthMessage,
21892219
closeStereoLoadingDialog,
21902220
handleStereoAnnotationComplete,
2221+
stereoViewLink,
21912222
handleStereoWarpImported,
21922223
handleStereoAnnotationReset,
21932224
handleStereoSegmentationFinalize,
@@ -2215,6 +2246,7 @@ export default defineComponent({
22152246
:initial-track-id="viewerFocus.trackId"
22162247
:text-query-enabled="true"
22172248
:text-query-available="textQueryAvailable"
2249+
:stereo-view-link="stereoViewLink"
22182250
@return-to-current-annotations="returnToCurrentAnnotations"
22192251
@change-camera="changeCamera"
22202252
@large-image-warning="largeImageWarning()"

‎client/platform/web-girder/useStereoOnnxWeb.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,16 @@ export default function useStereoOnnxWeb(opts: StereoOnnxWebOptions) {
424424
return getTransfer()?.warpAllFromCamera(cameraName);
425425
}
426426

427+
async function stereoViewLink(params: { camera: string; frameNum: number; point: [number, number] }) {
428+
if (!clientSettings.stereoSettings.autoComputeOtherCamera) return null;
429+
return (await getTransfer()?.warpPoint(params.point, params.camera, params.frameNum)) ?? null;
430+
}
431+
427432
return {
428433
handleStereoAnnotationComplete,
429434
handleStereoTrackLinked,
430435
warpAllFromCamera,
436+
stereoViewLink,
431437
precomputeCurrentFrame,
432438
invalidateCalibration,
433439
};

‎client/platform/web-girder/views/ViewerLoader.vue‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ export default defineComponent({
177177
178178
const {
179179
handleStereoAnnotationComplete, handleStereoTrackLinked, warpAllFromCamera,
180-
invalidateCalibration,
180+
invalidateCalibration, stereoViewLink,
181181
} = useStereoOnnxWeb({
182182
getViewer: () => viewerRef.value,
183183
getDatasetId: () => parentDatasetId(props.id),
@@ -518,6 +518,7 @@ export default defineComponent({
518518
exportDatasetIds,
519519
handleStereoAnnotationComplete,
520520
handleStereoTrackLinked,
521+
stereoViewLink,
521522
stereoBusyMessage,
522523
stereoDownloadProgress,
523524
stereoDownloadPercent,
@@ -550,6 +551,7 @@ export default defineComponent({
550551
:annotation-source-returnable="annotationSourceReturnable"
551552
:initial-frame="viewerFocus.frame"
552553
:initial-track-id="viewerFocus.trackId"
554+
:stereo-view-link="stereoViewLink"
553555
@return-to-current-annotations="returnToCurrentAnnotations"
554556
@large-image-warning="largeImageWarning()"
555557
@update:set="routeSet"

‎client/src/components/annotators/mediaControllerType.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ export interface AggregateMediaController {
5252
* True only while onResize is applying its programmatic size()/resetZoom()
5353
* to the panes. resetZoom emits GeoJS pan/zoom events synchronously; the
5454
* linked-viewer navigation (useAlignedNavigation / useRegistrationNavigation)
55-
* must ignore those so one pane's native-space reset isn't broadcast to the
56-
* others as if it were a shared-space move (which parks warped panes on an
57-
* empty corner). The resizeTrigger bump that follows re-snaps every pane from
55+
* and stereo view-link must ignore those so one pane's native-space reset
56+
* isn't broadcast to the others as if it were a shared-space move (which
57+
* parks warped panes on an empty corner, or recentres the stereo pair on the
58+
* wrong object). The resizeTrigger bump that follows re-snaps every pane from
5859
* the reference once the reset has settled.
5960
*/
6061
resizing: Readonly<Ref<boolean>>;
@@ -75,6 +76,15 @@ export interface AggregateMediaController {
7576
setSpeed: (speed: number) => void;
7677
getController: (cameraName: string) => MediaController;
7778
toggleSynchronizeCameras: (sync: boolean) => void;
79+
/**
80+
* While cameras are synchronised, recentre the other panes on where the
81+
* source pane's centre lands on their camera (null to disable). The
82+
* resolver returns null when the point cannot be found there.
83+
*/
84+
setViewLinkResolver: (
85+
resolver: ((sourceCamera: string, point: [number, number]) =>
86+
Promise<[number, number] | null>) | null,
87+
) => void;
7888
/**
7989
* Seeks so that `camera` lands on its own local frame `localFrame` (e.g.
8090
* jumping to a track's stored begin/end, which is in local-frame units).

‎client/src/components/annotators/useMediaController.ts‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Vue, {
77
ref, shallowRef, reactive, provide, toRef, Ref, UnwrapRef, computed, watch,
88
} from 'vue';
99
import { map, over } from 'lodash';
10+
import createViewLink from './viewLink';
1011

1112
import { use } from '../../provides';
1213
import type {
@@ -113,10 +114,27 @@ export function useMediaController() {
113114
let state: Record<string, UnwrapRef<MediaControllerReactiveData>> = {};
114115
let cameraControllerSymbols: Record<string, symbol> = {};
115116
const synchronizeCameras: Ref<boolean> = ref(false);
117+
118+
// Installed by the viewer while stereo auto-compute is on; otherwise
119+
// synchronised panes only copy each other's screen motion.
120+
const viewLink = createViewLink({
121+
center: (key) => geoViewers[key]?.value?.center(),
122+
cameraName: (key) => state[key]?.cameraName,
123+
synced: () => synchronizeCameras.value,
124+
recenter: (sourceKey, point) => {
125+
allowCameraTrigger = false;
126+
Object.entries(geoViewers).forEach(([camera, geoViewer]) => {
127+
if (geoViewer.value && camera !== sourceKey) {
128+
geoViewer.value.center({ x: point[0], y: point[1] });
129+
}
130+
});
131+
allowCameraTrigger = true;
132+
},
133+
});
116134
const resizeTrigger: Ref<number> = ref(0);
117135
// Raised only while onResize applies its programmatic resetZoom, so the
118-
// linked-viewer navigation ignores the resulting pan/zoom events (see
119-
// AggregateMediaController.resizing).
136+
// linked-viewer navigation and stereo view-link ignore the resulting
137+
// pan/zoom events (see AggregateMediaController.resizing).
120138
const resizing: Ref<boolean> = ref(false);
121139
// shallowRef: an AlignedFrameResolver carries nested Refs (slotCount, frameRate)
122140
// that must NOT be deep-reactive-converted/auto-unwrapped by a plain ref().
@@ -150,6 +168,7 @@ export function useMediaController() {
150168
currentTime: emptyControllerCurrentTime,
151169
getController,
152170
toggleSynchronizeCameras,
171+
setViewLinkResolver,
153172
cameraSync: synchronizeCameras,
154173
resizeTrigger,
155174
resizing,
@@ -287,6 +306,12 @@ export function useMediaController() {
287306
synchronizeCameras.value = val;
288307
}
289308

309+
function setViewLinkResolver(
310+
resolver: Parameters<typeof viewLink.setResolver>[0],
311+
) {
312+
viewLink.setResolver(resolver);
313+
}
314+
290315
/**
291316
* Optional replacement for the aggregate "reset pan and zoom" behavior,
292317
* installed by the aligned-view navigation link (useAlignedNavigation).
@@ -320,6 +345,12 @@ export function useMediaController() {
320345
}
321346
});
322347
allowCameraTrigger = true;
348+
// onResize's resetZoom emits pan/zoom in native space; skip the stereo
349+
// lookup so a pane's native center isn't warped onto the other camera
350+
// after the resize settles (same guard as aligned/registration nav).
351+
if (!resizing.value) {
352+
viewLink.schedule(camEvent.camera);
353+
}
323354
}
324355
});
325356

@@ -333,6 +364,9 @@ export function useMediaController() {
333364
}
334365
});
335366
allowCameraTrigger = true;
367+
if (!resizing.value) {
368+
viewLink.schedule(camEvent.camera);
369+
}
336370
}
337371
});
338372
/**
@@ -654,6 +688,7 @@ export function useMediaController() {
654688
getController,
655689
resetMapDimensions,
656690
toggleSynchronizeCameras,
691+
setViewLinkResolver,
657692
cameraSync: synchronizeCameras,
658693
resizeTrigger,
659694
resizing,
@@ -820,6 +855,7 @@ export function useMediaController() {
820855
currentTime: defaultController.currentTime,
821856
getController,
822857
toggleSynchronizeCameras,
858+
setViewLinkResolver,
823859
cameraSync: synchronizeCameras,
824860
resizeTrigger,
825861
resizing,

0 commit comments

Comments
 (0)