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
44 changes: 44 additions & 0 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,50 @@ export interface TextQueryResponse {
fallback?: boolean;
}

export interface AutoRegisterRequest {
/** Path to camera A's image for the chosen frame */
imagePathA: string;
/** Path to camera B's image for the chosen frame */
imagePathB: string;
options?: {
/** RANSAC reprojection threshold in matcher-resolution pixels (default 2.0) */
ransacThreshold?: number;
/** Minimum raw matches before RANSAC (default 100) */
minMatches?: number;
/** Minimum RANSAC inliers (default 30) */
minInliers?: number;
/** Minimum inlier ratio (default 0.15) */
minInlierRatio?: number;
/** How many spatially-spread inlier correspondences to return (default 24) */
topK?: number;
/** Matcher coarse confidence threshold (default 0.2) */
matchThreshold?: number;
};
}

export interface AutoRegisterResponse {
/** Whether alignment succeeded (quality gate passed) */
success: boolean;
/** Machine-readable failure code:
* insufficient_matches | low_confidence | degenerate_homography */
code?: string;
/** Human-readable error, actionable for the user */
error?: string;
/** 3x3 homography mapping camera A native pixels -> camera B native pixels */
homography?: number[][];
/** Spatially-spread inlier correspondences [ax, ay, bx, by][] in native px */
inliers?: [number, number, number, number][];
numMatches?: number;
numInliers?: number;
inlierRatio?: number;
/** Native [width, height] of each input image */
imageSizeA?: [number, number];
imageSizeB?: [number, number];
/** Matcher identifier (e.g. 'minima_loftr') */
model?: string;
elapsedMs?: number;
}

export interface RefineDetectionsRequest {
/** Path to the image file */
imagePath: string;
Expand Down
119 changes: 113 additions & 6 deletions client/dive-common/components/CameraRegistration/RegistrationTools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { buildPerCameraRegistrationFiles } from 'vue-media-annotator/alignedView
import TooltipBtn from 'vue-media-annotator/components/TooltipButton.vue';
import { useApi } from 'dive-common/apispec';
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { useAutoRegister } from 'dive-common/use/useAutoRegister';

export default defineComponent({
name: 'CameraRegistration',
Expand Down Expand Up @@ -64,7 +65,7 @@ export default defineComponent({
return {
icon: complete ? 'mdi-check-circle' : 'mdi-alert',
color: complete ? 'success' : 'warning',
text: `${total - unresolvedCount}/${total} cameras registered`,
text: `${total - unresolvedCount}/${total} cameras ready`,
};
});
const camLeft = ref<string | null>(null);
Expand All @@ -79,7 +80,7 @@ export default defineComponent({
/**
* Author-vs-review posture: picking defaults on for a pair that still
* needs points, and off for one whose transform came from a registration
* file (review it; the "Pick points" toggle opts back in to refine).
* file (review it; the "Edit points" toggle opts back in to refine).
* Re-applied whenever the active pair changes identity, so it overrides a
* manual toggle on pair switch -- each pair opens in its own posture.
*/
Expand Down Expand Up @@ -369,6 +370,57 @@ export default defineComponent({
}
}

/**
* Auto Register: ask the platform's deep matcher (interactive service) for
* correspondences between the selected pair on the current frame, then
* inject them as ordinary point pairs and fit a homography. The service
* is null / unavailable on platforms without the capability (web), which
* hides the button entirely.
*/
const autoRegisterService = useAutoRegister();
const autoRegisterAvailable = computed(() => !!autoRegisterService?.available.value);
const autoRegistering = ref(false);
const autoRegisterError = ref<string | null>(null);
const autoRegisterSummary = ref<string | null>(null);

async function runAutoRegister() {
if (!autoRegisterService || !camLeft.value || !camRight.value) {
return;
}
if (correspondences.value.length > 0 || hasLoadedTransform.value) {
const confirmed = await prompt({
title: 'Auto Register',
text: `This will replace the existing points/transform for ${camLeft.value} → `
+ `${camRight.value} with automatically matched points. Continue?`,
confirm: true,
});
if (!confirmed) {
return;
}
}
autoRegistering.value = true;
autoRegisterError.value = null;
autoRegisterSummary.value = null;
try {
const result = await autoRegisterService.run(camLeft.value, camRight.value);
if (!result.success || !result.inliers || result.inliers.length === 0) {
autoRegisterError.value = result.error
|| 'Auto-register could not compute an alignment for this frame.';
return;
}
registration.applyAutoRegistration(camLeft.value, camRight.value, result.inliers);
const consensus = result.inlierRatio !== undefined
? ` (${Math.round(result.inlierRatio * 100)}% match consensus)` : '';
autoRegisterSummary.value = `Aligned with ${result.inliers.length} matched `
+ `points${consensus}. Review the points and overlay warp, refine if `
+ 'needed, then save.';
} catch (err) {
autoRegisterError.value = err instanceof Error ? err.message : String(err);
} finally {
autoRegistering.value = false;
}
}

return {
cameras,
cameraAlignmentStatuses,
Expand Down Expand Up @@ -404,6 +456,11 @@ export default defineComponent({
setTransformType,
setAlignmentMode,
save,
autoRegisterAvailable,
autoRegistering,
autoRegisterError,
autoRegisterSummary,
runAutoRegister,
};
},
});
Expand All @@ -429,14 +486,23 @@ export default defineComponent({
>
Source: {{ sourceReadout }}
</span>
<!-- Persistent divergence status (survives save by design); only the
action hint tracks the save state so it never asks for a save
that's already done. -->
<span
v-if="refinedFromSource"
class="text-caption warning--text d-block"
>
This pair has been refined in-app since the source registration was
produced. Save, then download the camera's registration from the
Export menu to hand the refinement (and its points) back to the
producer.
produced.
<template v-if="dirty">
Save, then download the camera's registration from the Export menu
to hand the refinement (and its points) back to the producer.
</template>
<template v-else>
Download the camera's registration from the Export menu to hand the
refinement (and its points) back to the producer.
</template>
</span>

<div
Expand Down Expand Up @@ -522,6 +588,47 @@ export default defineComponent({
points is optional: fitting {{ minPoints }} or more pairs replaces it.
</span>

<v-tooltip
v-if="autoRegisterAvailable"
bottom
open-delay="200"
>
<template #activator="{ on }">
<v-btn
block
outlined
small
color="primary"
:disabled="!camLeft || !camRight || camLeft === camRight || autoRegistering"
:loading="autoRegistering"
class="mt-2"
v-on="on"
@click="runAutoRegister"
>
<v-icon
small
left
>
mdi-auto-fix
</v-icon>
Auto Register
</v-btn>
</template>
<span>Automatically match points between the two cameras on the current frame</span>
</v-tooltip>
<span
v-if="autoRegisterError"
class="text-caption error--text d-block mt-1"
>
{{ autoRegisterError }}
</span>
<span
v-else-if="autoRegisterSummary"
class="text-caption success--text d-block mt-1"
>
{{ autoRegisterSummary }}
</span>

<v-checkbox
:input-value="linkedNav"
:disabled="!hasTransform"
Expand All @@ -537,7 +644,7 @@ export default defineComponent({

<v-switch
v-model="pickingEnabled"
label="Pick points"
label="Edit points"
dense
hide-details
class="mt-0"
Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/ImportAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default defineComponent({
const warpToAllCamerasHint = computed(() => {
const progress = alignedView.registrationProgress.value;
return progress
? `${progress.registered}/${progress.total} cameras registered`
? `${progress.registered}/${progress.total} cameras ready`
: '';
});
const activeCameraName = computed(() => {
Expand Down
34 changes: 31 additions & 3 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ import ControlsContainer from 'dive-common/components/ControlsContainer.vue';
import Sidebar from 'dive-common/components/Sidebar.vue';
import BottomPanel from 'dive-common/components/BottomPanel.vue';
import { useModeManager, useSave, useLassoMode } from 'dive-common/use';
import { provideAutoRegister } from 'dive-common/use/useAutoRegister';
import type {
StereoAnnotationCompleteParams,
StereoAnnotationResetParams,
StereoSegmentationFinalizeParams,
} from 'dive-common/use/useModeManager';
import clientSettingsSetup, { clientSettings, isStereoInteractiveModeEnabled } from 'dive-common/store/settings';
import { useApi, FrameImage, DatasetType } from 'dive-common/apispec';
import {
useApi, FrameImage, DatasetType, AutoRegisterResponse,
} from 'dive-common/apispec';
import { orderedMultiCamCameraNames } from 'dive-common/multicamDisplay';
import {
buildAlignedTimeline, buildInverseAlignedIndex, computeGapSlots, TimelineResult,
Expand Down Expand Up @@ -159,6 +162,17 @@ export default defineComponent({
type: Boolean,
default: false,
},
/**
* Platform-supplied auto-register runner for the Camera Registration panel:
* resolves each camera's image for a frame and computes an alignment via
* the interactive service. Desktop-only; when null (web) the panel hides
* its Auto Register button.
*/
autoRegisterHandler: {
type: Function as PropType<
((cameraA: string, cameraB: string, frameNum: number) => Promise<unknown>) | null>,
default: null,
},
},
setup(props, { emit }) {
const { prompt, visible } = usePrompt();
Expand Down Expand Up @@ -478,7 +492,7 @@ export default defineComponent({
});
// Publish how much of the rig resolves so UI outside the viewer core
// (e.g. the import menu's "Import to all cameras" checkbox) shows the
// same "N/M cameras registered" status as the Align View toggle.
// same "N/M cameras ready" status as the Align View toggle.
const registrationProgress = computed(() => {
if (!isMultiCameraDataset.value) {
return null;
Expand Down Expand Up @@ -510,7 +524,7 @@ export default defineComponent({
* Camera panes currently displayed. While the Camera Registration panel is
* open with an active pair on a 3+ camera dataset, only the pair's two
* panes show, so the left/right alignment flow reads without unrelated
* panes in between (regardless of whether Pick points is toggled on).
* panes in between (regardless of whether Edit points is toggled on).
* Panes are hidden (v-show), not unmounted, so their viewers keep state.
*/
const displayedCameras = computed(() => {
Expand Down Expand Up @@ -573,6 +587,20 @@ export default defineComponent({
const lassoMode = useLassoMode();
provide(LassoModeSymbol, lassoMode);

// Auto-register bridge for the Camera Registration panel: injects the current
// frame into the platform handler (mirrors onTextQuerySubmit). Provided
// unconditionally; `available` tracks the handler prop, which the desktop
// platform sets once its availability probe resolves (never on web).
provideAutoRegister({
available: computed(() => !!props.autoRegisterHandler),
run: (cameraA: string, cameraB: string) => {
if (!props.autoRegisterHandler) {
return Promise.reject(new Error('Auto-register is not available on this platform'));
}
return props.autoRegisterHandler(cameraA, cameraB, aggregateController.value.frame.value) as Promise<AutoRegisterResponse>;
},
});

// Provides wrappers for actions to integrate with settings
const {
linkingTrack,
Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/alignedViewTooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('alignedViewTooltipText', () => {
enabled: false,
sourceIsMixed: false,
progress: { registered: 1, total: 3 },
})).toBe('Align View — 1/3 cameras registered');
})).toBe('Align View — 1/3 cameras ready');
});

it('appends a mixed-calibration warning', () => {
Expand Down
2 changes: 1 addition & 1 deletion client/dive-common/components/alignedViewTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function alignedViewTooltipText(options: {
return `Align View on (draw/edit on any camera)${mixedNote}`;
}
if (progress && progress.registered < progress.total) {
return `Align View — ${progress.registered}/${progress.total} cameras registered${mixedNote}`;
return `Align View — ${progress.registered}/${progress.total} cameras ready${mixedNote}`;
}
return `Align View${mixedNote}`;
}
31 changes: 31 additions & 0 deletions client/dive-common/use/useAutoRegister.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { provide, inject, Ref } from 'vue';
import type { AutoRegisterResponse } from 'dive-common/apispec';

/**
* Auto-register service bridge for the Camera Registration panel.
*
* The platform layer (desktop ViewerLoader) implements the actual call — it
* resolves each camera's image path for the current frame and invokes the
* interactive service. Viewer.vue provides it (wrapping in the current frame
* number); RegistrationTools.vue injects it. On platforms without the
* capability (web) nothing is provided and the panel hides the button.
*/
export interface AutoRegisterService {
/**
* Whether auto-register is usable right now. Reactive: the platform's
* availability probe (matcher weights installed?) resolves after mount.
*/
available: Readonly<Ref<boolean>>;
/** Compute an alignment from camera A to camera B on the current frame. */
run: (cameraA: string, cameraB: string) => Promise<AutoRegisterResponse>;
}

const AutoRegisterSymbol = Symbol('autoRegister');

export function provideAutoRegister(service: AutoRegisterService) {
provide(AutoRegisterSymbol, service);
}

export function useAutoRegister(): AutoRegisterService | null {
return inject<AutoRegisterService | null>(AutoRegisterSymbol, null);
}
Loading
Loading