Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8be8852
Add Auto Align (deep-feature matching) to the Camera Registration panel
romleiaj Jul 8, 2026
7f826cb
Document Auto Align feature plan and Phase 0 benchmark results
romleiaj Jul 8, 2026
834b732
Let Auto Align accept any multi-camera dataset, not just stereoscopic
romleiaj Jul 8, 2026
5746259
Rename Auto Align to Auto Register
romleiaj Jul 12, 2026
b9525e9
Move from 'auto_align' to 'register_images' naming
romleiaj Jul 14, 2026
d90adfc
Scrub alignment language, move to register elsewhere
romleiaj Jul 14, 2026
b3f40c1
Turn on point picking when Auto Register injects its matches
romleiaj Jul 20, 2026
aa1e0b7
Rename the "Pick points" toggle to "Edit points"
romleiaj Jul 20, 2026
7c7c117
Stop Auto Register from restamping the rig-global source
romleiaj Jul 20, 2026
953d71b
Drop the stale "Save, then" from the refined-from-source note once saved
romleiaj Jul 20, 2026
e0632be
Remove the Auto Register feature plan doc
romleiaj Jul 21, 2026
c38b40c
Type the register_images IPC boundary
romleiaj Jul 21, 2026
72835d4
Shorten the Auto Register button label so it fits the panel
romleiaj Jul 21, 2026
bb5991e
Say "cameras ready" instead of "cameras registered" in rig status
romleiaj Jul 21, 2026
fd49863
Give correspondences an image-pair observation dimension (format v2)
romleiaj Aug 7, 2026
2dd0d01
Rebuild the registration panel around frames; add timeline markers
romleiaj Aug 7, 2026
9d4a516
Update client specs for the observation model and format v2
romleiaj Aug 7, 2026
11e7805
Run auto-registration as a multi-frame pipeline job; drop the service…
romleiaj Aug 7, 2026
bcf550f
Web-girder parity for the auto-register pipeline
romleiaj Aug 7, 2026
dfa1152
Update backend spec fixtures to registration format v2
romleiaj Aug 7, 2026
f0be8d1
Add cross-camera frame translation to the aggregate controller
romleiaj Aug 14, 2026
55245c1
Draw registration timeline markers in the selected camera's space
romleiaj Aug 14, 2026
6f4aa1d
Propose auto-register candidates from aligned timeline slots
romleiaj Aug 14, 2026
32aa0cd
Re-baseline the registration store after a replace-mode run
romleiaj Aug 14, 2026
feaecbc
Say the run is waiting on confirmation, not still matching
romleiaj Aug 14, 2026
19dd108
Seek and label registration frames in the right camera space
romleiaj Aug 14, 2026
c4d5e57
Measure loop closure over real frames, judged relative to width
romleiaj Aug 14, 2026
f03bfc7
Split the registration frame list into Auto and Manual sections
romleiaj Aug 14, 2026
a23188d
Let the user queue specific captures for the matcher
romleiaj Aug 14, 2026
07becbb
Say what frame count to pick, and warn where it overfits
romleiaj Aug 14, 2026
51b3951
Fix lint and typecheck failures after rebasing onto main
romleiaj Aug 19, 2026
7f81239
Pass -update 1 when ffmpeg extracts a single registration frame
romleiaj Aug 20, 2026
3a87051
Check ffmpeg exit code, not stderr content, for extraction failure
romleiaj Aug 20, 2026
04e88e3
Only mark timeline frames that carry registration points
romleiaj Aug 20, 2026
aa1c9b5
Dev/auto register fixes (#1860)
romleiaj Aug 24, 2026
92df1d1
Escape the observation-identity NUL instead of embedding it raw
romleiaj Aug 25, 2026
4c18a1b
Report a pipeline job from the moment it is accepted
romleiaj Aug 25, 2026
5bbf442
Keep the Auto Register run visible in the registration panel
romleiaj Aug 25, 2026
4a9faef
End the Auto Register wait on job state, not on its output
romleiaj Aug 25, 2026
ac4b1a2
it was attmepting to use the source directory with project directory …
BryonLewis Sep 1, 2026
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
36 changes: 33 additions & 3 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CustomStyle } from 'vue-media-annotator/StyleManager';
import { AttributeTrackFilter } from 'vue-media-annotator/AttributeTrackFilterControls';
import { ImageEnhancements } from 'vue-media-annotator/use/useImageEnhancements';
import type {
CameraHomographies, CameraCorrespondences, CameraTransformTypes, RegistrationSource,
CameraHomographies, CameraObservations, CameraTransformTypes, RegistrationSource,
} from 'vue-media-annotator/alignedView/CameraRegistrationStore';
import type { PercentileStretch } from 'vue-media-annotator/use/useImageEnhancements';

Expand Down Expand Up @@ -84,6 +84,15 @@ interface PipeMetadata {

interface PipelineRuntimeParams {
frameRange?: [number, number] | null;
/**
* Multicam registration subset: camera name -> ordered image identifiers
* for exactly the frames the job should process. Row i of one camera's
* list pairs with row i of every other's. Identifiers are the camera's
* own image names (the platform backend resolves them to real paths) or
* `frame://N` pseudo-names for video cameras (the backend extracts those
* frames to temp images before the job).
*/
imagePairs?: Record<string, string[]>;
}

interface PipelineParams {
Expand Down Expand Up @@ -268,7 +277,12 @@ interface DatasetConfigMutable {
attributeTrackFilters?: Readonly<Record<string, AttributeTrackFilter>>;
datasetInfo?: DatasetInfoFields;
cameraHomographies?: CameraHomographies;
cameraCorrespondences?: CameraCorrespondences;
/**
* Per-image-pair correspondence observations, keyed by directional
* "left::right". Each entry lists the observations (image-pair identity,
* enabled flag, producer source, stats, and points) behind that pair's fit.
*/
cameraCorrespondences?: CameraObservations;
cameraTransformTypes?: CameraTransformTypes;
/** Producer provenance of the camera registration (see RegistrationSource). */
cameraRegistrationSource?: RegistrationSource | null;
Expand Down Expand Up @@ -366,9 +380,25 @@ interface DatasetCalibrationResult {
conversionError?: string;
}

/** Terminal state of a pipeline job, as reported by {@link Api.watchPipelineJob}. */
export interface PipelineJobResult {
/** True when the job exited successfully. */
ok: boolean;
/** Human-readable reason when `ok` is false. */
message?: string;
}

interface Api {
getPipelineList(): Promise<Pipelines>;
runPipeline(itemId: string, pipeline: Pipe, pipelineParams?: PipelineParams): Promise<unknown>;
/**
* Resolve once the pipeline job this dataset just launched reaches a terminal
* state, so a caller can key completion off the job instead of off whatever
* the job was expected to write. Optional: a platform without a job feed
* leaves it undefined and callers fall back to watching for the artifact,
* which cannot tell "finished, output identical" from "still running".
*/
watchPipelineJob?(datasetId: string, pipeline: Pipe): Promise<PipelineJobResult>;
deleteTrainedPipeline(pipeline: Pipe): Promise<void>;
exportTrainedPipeline(path: string, pipeline: Pipe): Promise<unknown>;
getDatasetCalibration(datasetId: string): Promise<DatasetCalibrationResult | null>;
Expand Down Expand Up @@ -700,4 +730,4 @@ export type {
MediaImportResponse,
};

export type { PercentileStretch, CameraCorrespondences };
export type { PercentileStretch, CameraObservations };
88 changes: 88 additions & 0 deletions client/dive-common/autoRegisterSelection.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest';

import proposeRegistrationFrames from './autoRegisterSelection';

describe('proposeRegistrationFrames', () => {
it('spreads candidates across every temporal bin (no single-scene bias)', () => {
const frames = proposeRegistrationFrames({
counts: [1200, 1200],
bins: 12,
perBin: 2,
});
expect(frames.length).toBe(24);
// Every 100-frame bin contributes exactly its share: a scene-rich
// stretch can never supply all the candidates.
for (let bin = 0; bin < 12; bin += 1) {
const inBin = frames.filter((f) => f >= bin * 100 && f < (bin + 1) * 100);
expect(inBin.length).toBe(2);
}
expect(frames).toEqual([...frames].sort((a, b) => a - b));
});

it('spans only the shortest camera', () => {
const frames = proposeRegistrationFrames({
counts: [1000, 300],
bins: 10,
perBin: 1,
});
expect(Math.max(...frames)).toBeLessThan(300);
expect(frames.length).toBe(10);
});

it('ranks within a bin by inter-camera timestamp skew', () => {
// Two cameras, 10 frames, one bin: frame 6 is perfectly synced, frame 3
// is close, everything else is badly skewed.
const base = 1_700_000_000;
const camA = Array.from({ length: 10 }, (_, i) => base + i);
const camB = camA.map((t, i) => {
if (i === 6) return t;
if (i === 3) return t + 0.1;
return t + 5;
});
const frames = proposeRegistrationFrames({
counts: [10, 10],
timestamps: [camA, camB],
bins: 1,
perBin: 2,
maxSkewSeconds: 0.5,
});
expect(frames).toEqual([3, 6]);
});

it('drops candidates whose skew exceeds the threshold entirely', () => {
const base = 1_700_000_000;
const camA = Array.from({ length: 4 }, (_, i) => base + i);
const camB = camA.map((t) => t + 10); // hopeless sync everywhere
const frames = proposeRegistrationFrames({
counts: [4, 4],
timestamps: [camA, camB],
bins: 1,
perBin: 2,
maxSkewSeconds: 0.5,
});
expect(frames).toEqual([]);
});

it('falls back to even spread for frames without timestamps', () => {
const frames = proposeRegistrationFrames({
counts: [100, 100],
timestamps: [
Array.from({ length: 100 }, () => undefined),
Array.from({ length: 100 }, () => undefined),
],
bins: 2,
perBin: 2,
});
expect(frames.length).toBe(4);
expect(frames.filter((f) => f < 50).length).toBe(2);
expect(frames.filter((f) => f >= 50).length).toBe(2);
});

it('handles degenerate inputs', () => {
expect(proposeRegistrationFrames({ counts: [0, 10], bins: 5, perBin: 2 })).toEqual([]);
expect(proposeRegistrationFrames({ counts: [], bins: 5, perBin: 2 })).toEqual([]);
// More bins than frames: every frame proposed once.
const tiny = proposeRegistrationFrames({ counts: [3, 3], bins: 12, perBin: 2 });
expect(tiny).toEqual([0, 1, 2]);
});
});
108 changes: 108 additions & 0 deletions client/dive-common/autoRegisterSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Candidate-frame proposal for the auto-register pipeline (stage 0 of the
* selection contract): DIVE picks for diversity and synchronization, VIAME
* picks for image quality.
*
* Stratified, not "evenly spread" and not "most featureful": the flight is
* divided into `bins` equal time bins and `perBin` candidates are proposed
* within each, so temporal spread is guaranteed structurally -- a single
* scene-rich stretch can never supply every frame, which would reconstitute
* exactly the single-scene bias the multi-pair restructure exists to remove.
* The pipeline then keeps the best candidate per bin by image quality
* (`max_frames` is the bin count), so oversampling here is deliberate.
*
* Within a bin, candidates rank by inter-camera timestamp skew when every
* camera carries per-frame timestamps: on a survey aircraft at ~100 kt,
* 100 ms of desync is ~5 m of ground motion baked straight into the "ground
* truth" points, and RANSAC cannot reject it because it is consistent
* within the frame. Without timestamps (positional alignment), candidates
* spread evenly within the bin and skew is unknowable -- no threshold
* applies.
*/

export interface ProposalOptions {
/** Per-camera usable frame counts; the proposal spans [0, min(counts)). */
counts: number[];
/**
* Optional per-camera per-frame capture timestamps (epoch seconds;
* undefined entries = unknown). Skew ranking applies only when every
* camera has a timestamp for the frame under consideration.
*/
timestamps?: (number | undefined)[][];
/** Number of temporal bins (the pipeline's max_frames budget). */
bins: number;
/** Candidates proposed per bin. */
perBin: number;
/** Candidates with a larger inter-camera skew (seconds) are dropped. */
maxSkewSeconds?: number;
}

/** Largest pairwise timestamp difference across cameras, or null if unknowable. */
function frameSkew(
timestamps: (number | undefined)[][],
frame: number,
): number | null {
const stamps: number[] = [];
for (let cam = 0; cam < timestamps.length; cam += 1) {
const t = timestamps[cam]?.[frame];
if (t === undefined) {
return null;
}
stamps.push(t);
}
return Math.max(...stamps) - Math.min(...stamps);
}

/**
* Propose candidate frame indices, sorted ascending. Returns at most
* `bins * perBin` frames; short datasets simply yield fewer.
*/
export default function proposeRegistrationFrames(options: ProposalOptions): number[] {
const usable = Math.min(...options.counts);
if (!Number.isFinite(usable) || usable <= 0 || options.bins <= 0 || options.perBin <= 0) {
return [];
}
const bins = Math.min(options.bins, usable);
const { timestamps } = options;
const maxSkew = options.maxSkewSeconds ?? 0.5;
const chosen = new Set<number>();
for (let bin = 0; bin < bins; bin += 1) {
const start = Math.floor((bin * usable) / bins);
const end = Math.floor(((bin + 1) * usable) / bins); // exclusive
// bins <= usable, so every bin spans at least one frame.
const size = end - start;
const perBin = Math.min(options.perBin, size);
if (timestamps && timestamps.length) {
// Rank the whole bin by skew; unknowable-skew frames rank after
// measured ones (evenly spread among themselves), over-threshold
// frames are dropped outright.
const measured: { frame: number; skew: number }[] = [];
const unknowable: number[] = [];
for (let frame = start; frame < end; frame += 1) {
const skew = frameSkew(timestamps, frame);
if (skew === null) {
unknowable.push(frame);
} else if (skew <= maxSkew) {
measured.push({ frame, skew });
}
}
measured.sort((a, b) => a.skew - b.skew || a.frame - b.frame);
measured.slice(0, perBin).forEach(({ frame }) => chosen.add(frame));
let still = perBin - Math.min(measured.length, perBin);
if (still > 0 && unknowable.length) {
const step = unknowable.length / still;
for (let i = 0; i < still; i += 1) {
chosen.add(unknowable[Math.floor(i * step)]);
}
still = 0;
}
} else {
// No timeline: spread evenly within the bin.
const step = size / perBin;
for (let i = 0; i < perBin; i += 1) {
chosen.add(start + Math.floor(i * step + step / 2));
}
}
}
return [...chosen].sort((a, b) => a - b);
}
Loading
Loading