Skip to content
Open
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 @@ -78,6 +79,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 and drives its prefill (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 @@ -87,6 +104,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 @@ -270,9 +293,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
245 changes: 245 additions & 0 deletions client/dive-common/components/PipelineCameraAssignDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<script lang="ts">
import {
computed, defineComponent, PropType, ref, watch,
} from 'vue';
import {
CameraRole, CAMERA_ROLE_LABELS, describeMissingRegistration, missingRegistrations, roleOfToken,
} 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 comes from the
* dataset's camera roles / names and is what runs when the user confirms
* without changes.
*/
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]));

/** 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,
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="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