Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
0adb257
Pass DIVE camera registrations to 2-cam/3-cam VIAME pipelines
romleiaj Jul 15, 2026
1c98387
Write pipeline registration files at format v2
romleiaj Aug 17, 2026
2872b90
Fix typecheck and lint failures in the registration pipeline args
romleiaj Aug 17, 2026
f424178
Let 2-cam/3-cam pipes declare their camera order
romleiaj Aug 17, 2026
af96849
Confirm multicam camera placement before a run; carry camera roles on…
romleiaj Aug 17, 2026
7cc4784
Check camera registrations before a multicam run, not at pipe configu…
romleiaj Aug 17, 2026
3183147
Point at the Camera Registration tab, not "Aligned View", in the regi…
romleiaj Aug 17, 2026
d13d63c
Wrap the registration-missing message under the line limit
romleiaj Aug 17, 2026
c8bffb5
Simplify the multicam camera-order plumbing
romleiaj Aug 17, 2026
6769ed7
Read the persisted camera order in the desktop pipeline fallbacks
romleiaj Aug 31, 2026
5da653a
have the dialog default to the camera order, but allow changes before…
BryonLewis Sep 2, 2026
7334e19
save CameraRoles in desktop version
BryonLewis Sep 2, 2026
b60581b
align desktop/web inference of camera roles
BryonLewis Sep 2, 2026
fb122bf
align server/desktop fitted registration, server was more limiting, f…
BryonLewis Sep 2, 2026
450049b
update docs to reflect role/name matching, make sure rolematching hap…
BryonLewis Sep 2, 2026
b9725c0
better warning for mistmatched saving that doesn't match roles
BryonLewis Sep 2, 2026
98fd055
update tests
BryonLewis Sep 2, 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
32 changes: 31 additions & 1 deletion client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ImageEnhancements } from 'vue-media-annotator/use/useImageEnhancements'
import type {
CameraHomographies, CameraCorrespondences, CameraTransformTypes, RegistrationSource,
} from 'vue-media-annotator/alignedView/CameraRegistrationStore';
import type { CameraRole } from 'dive-common/pipelineCameraOrder';
import type { PercentileStretch } from 'vue-media-annotator/use/useImageEnhancements';

type DatasetType = 'image-sequence' | 'video' | 'multi' | 'large-image';
Expand Down Expand Up @@ -80,6 +81,22 @@ interface PipeMetadata {
* the two conventional keys are used.
*/
calibrationKeys?: string[];
/**
* Camera role per pipeline input for 2-cam/3-cam pipes (e.g. ["EO", "UV", "IR"]:
* input1 is optical, input2 ultraviolet, input3 thermal), parsed from a
* `# Camera Order: <cam> [cam...]` header. Labels the slots of the pre-run
* camera-assignment step (dive-common/pipelineCameraOrder.ts); pipes without
* it show bare input1..N slots.
*/
cameraOrder?: string[];
/**
* Input positions whose detections/images the pipe warps onto camera 1
* (`process warpN :: warp_detections | warp_image` in the pipe body), e.g.
* [2, 3]. Each such camera needs a fitted registration (Camera Registration tab) onto
* camera 1; DIVE checks that before the run instead of letting the pipe
* fail at configure time on a missing file.
*/
registrationWarps?: number[];
}

interface PipelineRuntimeParams {
Expand All @@ -89,6 +106,12 @@ interface PipelineRuntimeParams {
interface PipelineParams {
kwiverParams?: Record<string, string>;
runtimeParams?: PipelineRuntimeParams;
/**
* 2-cam/3-cam pipes: the dataset camera to feed each inputN, in order, as
* confirmed by the user before the run. When omitted (API callers) the
* dataset's stored camera order is used.
*/
cameraOrder?: string[];
/** Filter / transcode / disparity pipelines: name for the newly created dataset. */
outputDatasetName?: string;
/**
Expand Down Expand Up @@ -272,9 +295,16 @@ interface DatasetConfigMutable {
cameraTransformTypes?: CameraTransformTypes;
/** Producer provenance of the camera registration (see RegistrationSource). */
cameraRegistrationSource?: RegistrationSource | null;
/**
* Sensor role per multicam camera name (eo / ir / uv), inferred at import
* from the camera and image names and editable afterwards; used to place
* cameras onto a pipeline's declared camera slots. Cameras with no known
* role are absent.
*/
cameraRoles?: Record<string, CameraRole>;
error?: string;
}
const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource', 'typeHierarchy'];
const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource', 'typeHierarchy', 'cameraRoles'];
/**
* Cross-dataset color/style overrides, reused across every dataset when the
* "shared" color scope is enabled (see clientSettings.typeSettings.colorScope).
Expand Down
286 changes: 286 additions & 0 deletions client/dive-common/components/PipelineCameraAssignDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
<script lang="ts">
import {
computed, defineComponent, PropType, ref, watch,
} from 'vue';
import {
CameraRole, CAMERA_ROLE_LABELS, describeMissingRegistration, describeSlotRoleMismatch,
missingRegistrations, roleOfToken, slotRoleMismatches,
} from 'dive-common/pipelineCameraOrder';

/**
* One request for the user to place a dataset's cameras onto a multicam
* pipeline's inputs. Slots come from the pipe's `# Camera Order:` header
* (`EO`, `UV`, `IR`) or are bare `input1..N`; the proposal matches by
* role/name when it can and otherwise follows display order.
*/
export interface PipelineCameraAssignRequest {
datasetName: string;
pipelineName: string;
slots: string[];
cameras: string[];
proposed: (string | null)[];
/** Current dataset roles, shown next to each camera in the pickers. */
roles: Record<string, CameraRole>;
/** Input positions the pipe warps onto camera 1 (metadata.registrationWarps). */
registrationWarps: number[];
/** Fitted registration pair keys (`a::b`) the dataset holds. */
fittedPairs: string[];
}

export interface PipelineCameraAssignResult {
order: string[];
/** Roles to persist on the dataset (slot role -> chosen camera), when asked. */
roles: Record<string, CameraRole> | null;
}

export default defineComponent({
name: 'PipelineCameraAssignDialog',
props: {
value: {
type: Boolean,
default: false,
},
request: {
type: Object as PropType<PipelineCameraAssignRequest | null>,
default: null,
},
},
emits: ['cancel', 'confirm'],
setup(props, { emit }) {
const selection = ref<(string | null)[]>([]);
const remember = ref(true);

watch(() => props.request, (request) => {
selection.value = request ? [...request.proposed] : [];
remember.value = true;
}, { immediate: true });

const slotRoles = computed(() => (props.request?.slots ?? []).map(roleOfToken));
const anyRoleSlots = computed(() => slotRoles.value.some((role) => role !== null));

function slotLabel(index: number): string {
const slot = props.request?.slots[index] ?? '';
const role = slotRoles.value[index];
const roleLabel = role ? CAMERA_ROLE_LABELS[role] : slot;
return `Camera ${index + 1} (input${index + 1}) — ${roleLabel}`;
}

function cameraLabel(camera: string): string {
const role = props.request?.roles[camera];
return role ? `${camera} · ${CAMERA_ROLE_LABELS[role]}` : camera;
}

const problems = computed(() => {
const chosen = selection.value;
const issues: string[] = [];
chosen.forEach((camera, index) => {
if (!camera) {
issues.push(`Camera ${index + 1} has no dataset camera assigned.`);
}
});
const seen = new Map<string, number[]>();
chosen.forEach((camera, index) => {
if (camera) {
seen.set(camera, [...(seen.get(camera) ?? []), index + 1]);
}
});
seen.forEach((slots, camera) => {
if (slots.length > 1) {
issues.push(`"${camera}" is assigned to cameras ${slots.join(' and ')}.`);
}
});
return issues;
});

const changed = computed(() => (props.request?.proposed ?? [])
.some((camera, index) => camera !== selection.value[index]));

const roleMismatches = computed(() => slotRoleMismatches(
props.request?.slots ?? [],
selection.value,
props.request?.roles ?? {},
));
const roleMismatchMessages = computed(() => roleMismatches.value
.map((entry) => describeSlotRoleMismatch(entry)));
const showRoleMismatchWarning = computed(() => !changed.value && roleMismatches.value.length > 0);

/** Warped inputs whose chosen camera has no registration onto camera 1. */
const missing = computed(() => missingRegistrations(
selection.value,
props.request?.registrationWarps,
props.request?.fittedPairs ?? [],
));
const missingMessages = computed(() => missing.value
.map((entry) => describeMissingRegistration(entry)));
function rowRegistrationHint(index: number): string | null {
const warps = props.request?.registrationWarps ?? [];
const target = selection.value[0];
const camera = selection.value[index];
if (index === 0 || !warps.includes(index + 1) || !camera || !target) {
return null;
}
const isMissing = missing.value.some((entry) => entry.input === index + 1);
return isMissing
? `No registration of ${camera} onto ${target}`
: `Registered onto ${target}`;
}
function rowMissing(index: number): boolean {
return missing.value.some((entry) => entry.input === index + 1);
}
const blocked = computed(() => problems.value.length > 0 || missing.value.length > 0);

function confirm() {
if (blocked.value || !props.request) {
return;
}
const order = selection.value as string[];
let roles: Record<string, CameraRole> | null = null;
if (remember.value && anyRoleSlots.value) {
roles = {};
order.forEach((camera, index) => {
const role = slotRoles.value[index];
if (role) {
roles![camera] = role;
}
});
}
emit('confirm', { order, roles } as PipelineCameraAssignResult);
}

return {
selection,
remember,
anyRoleSlots,
slotLabel,
cameraLabel,
problems,
missingMessages,
rowRegistrationHint,
rowMissing,
blocked,
changed,
roleMismatchMessages,
showRoleMismatchWarning,
confirm,
};
},
});
</script>

<template>
<v-dialog
:value="value && request !== null"
max-width="560"
persistent
>
<v-card v-if="request">
<v-card-title class="text-h6">
Assign cameras
</v-card-title>
<v-card-text>
<p class="mb-1">
<strong>{{ request.pipelineName }}</strong> on <strong>{{ request.datasetName }}</strong>
</p>
<p class="text-body-2 mb-4">
Which dataset camera feeds each pipeline camera. Camera 1 is the frame the
pipeline warps the other cameras' detections onto.
</p>
<v-select
v-for="(slot, index) in request.slots"
:key="slot + index"
v-model="selection[index]"
:items="request.cameras.map((camera) => ({ text: cameraLabel(camera), value: camera }))"
:label="slotLabel(index)"
:hint="rowRegistrationHint(index) || undefined"
:persistent-hint="!!rowRegistrationHint(index)"
:error="rowMissing(index)"
outlined
dense
class="mb-3"
/>
<v-checkbox
v-if="anyRoleSlots"
v-model="remember"
dense
hide-details
class="mt-1"
label="Save these as the dataset's camera roles"
/>
<v-alert
v-if="showRoleMismatchWarning"
type="warning"
prominent
class="mt-3 mb-0"
>
<div class="font-weight-medium mb-1">
Default order does not match saved camera roles
</div>
<div
v-for="message in roleMismatchMessages"
:key="message"
>
{{ message }}
</div>
<div
v-if="remember"
class="mt-2"
>
Running will overwrite saved roles using the slot labels above. Rearrange the
assignments or uncheck "Save these as the dataset's camera roles" if this mapping
is wrong.
</div>
<div
v-else
class="mt-2"
>
Review the camera assignments before running.
</div>
</v-alert>
<v-alert
v-if="problems.length"
type="warning"
dense
text
class="mt-3 mb-0"
>
<div
v-for="problem in problems"
:key="problem"
>
{{ problem }}
</div>
</v-alert>
<v-alert
v-if="missingMessages.length"
type="error"
dense
text
class="mt-3 mb-0"
>
<div
v-for="message in missingMessages"
:key="message"
>
{{ message }}
</div>
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
text
@click="$emit('cancel')"
>
Cancel
</v-btn>
<v-btn
color="primary"
:disabled="blocked"
@click="confirm"
>
{{ changed ? 'Run with these cameras' : 'Run' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
Loading
Loading