Skip to content
Closed
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
31 changes: 29 additions & 2 deletions client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { Attribute } from 'vue-media-annotator/use/AttributeTypes';
import { CustomStyle } from 'vue-media-annotator/StyleManager';
import { AttributeTrackFilter } from 'vue-media-annotator/AttributeTrackFilterControls';
import { ImageEnhancements } from 'vue-media-annotator/use/useImageEnhancements';
import {
CameraHomographies, CameraCorrespondences, CameraTransformTypes, CalibrationSource,
} from 'vue-media-annotator/CameraCalibrationStore';
import type { PercentileStretch } from 'vue-media-annotator/use/useImageEnhancements';

type DatasetType = 'image-sequence' | 'video' | 'multi' | 'large-image';
Expand Down Expand Up @@ -136,6 +139,12 @@ export interface MultiCamImportFolderArgs {
sourceList: Record<string, {
sourcePath: string;
trackFile: string;
/**
* Optional alignment transform file for cameras after the first (desktop
* only): a DIVE calibration .json, parsed at import time to seed the
* dataset's saved camera calibration.
*/
transformFile?: string;
/** Per-camera media type when cameras differ (e.g. EO JPG + IR TIFF on web). */
type?: 'image-sequence' | 'video' | 'large-image';
}>; // path/track file per camera
Expand Down Expand Up @@ -186,9 +195,14 @@ interface DatasetMetaMutable {
attributes?: Readonly<Record<string, Attribute>>;
attributeTrackFilters?: Readonly<Record<string, AttributeTrackFilter>>;
datasetInfo?: Record<string, unknown>;
cameraHomographies?: CameraHomographies;
cameraCorrespondences?: CameraCorrespondences;
cameraTransformTypes?: CameraTransformTypes;
/** Producer provenance of the camera calibration (see CalibrationSource). */
cameraCalibrationSource?: CalibrationSource | null;
error?: string;
}
const DatasetMetaMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo'];
const DatasetMetaMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraCalibrationSource'];

interface DatasetMeta extends DatasetMetaMutable {
id: Readonly<string>;
Expand Down Expand Up @@ -278,7 +292,7 @@ interface Api {
saveAttributeTrackFilters(datasetId: string,
args: SaveAttributeTrackFilterArgs): Promise<unknown>;
// Non-Endpoint shared functions
openFromDisk(datasetType: DatasetType | 'bulk' | 'calibration' | 'annotation' | 'text' | 'zip', directory?: boolean):
openFromDisk(datasetType: DatasetType | 'bulk' | 'calibration' | 'annotation' | 'text' | 'zip' | 'transform', directory?: boolean):
Promise<{canceled?: boolean; filePaths: string[]; fileList?: File[]; root?: string}>;
/** Desktop: immediate child directory names under a parent folder (multicam subfolder import). */
listImmediateSubfolders?(parentPath: string): Promise<string[]>;
Expand All @@ -294,6 +308,12 @@ interface Api {
): Promise<string>;
/** Desktop: stereoscopic calibration file in a parent folder root. */
findParentFolderCalibrationFile?(parentPath: string): Promise<string | null>;
/**
* Desktop: every DIVE camera-calibration .json (alignment transforms) in a
* parent folder root: per-camera calibration_<camera>.json files first,
* then other self-identified candidates.
*/
findParentFolderTransformFiles?(parentPath: string): Promise<string[]>;
/** True when the dataset folder has an attached stereoscopic calibration file. */
hasCalibrationFile?(datasetId: string): Promise<boolean>;
/** Web: stash a calibration File for multicam upload lookup. */
Expand All @@ -315,6 +335,13 @@ interface Api {
saveCalibration?(path: string): Promise<{ savedPath: string; updatedDatasetIds: string[] }>;
/** Desktop: set the stereo camera/calibration file for a single dataset. */
importCalibrationFile?(datasetId: string, path: string): Promise<{ calibration: string }>;
/**
* Merge a DIVE camera-calibration .json (alignment transforms) into an
* existing multicam dataset's saved calibration. Web reads the provided
* File; desktop reads the path.
*/
importCameraCalibration?(datasetId: string, path: string, file?: File):
Promise<{ cameras: string[]; pairCount: number }>;
/** Desktop: copy the dataset's current camera/calibration file out to destPath. */
exportCalibrationFile?(datasetId: string, destPath: string): Promise<{ exportedPath: string }>;
/** Download/export the dataset's current calibration file (platform-specific). */
Expand Down
115 changes: 114 additions & 1 deletion client/dive-common/components/ImportAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { useApi } from 'dive-common/apispec';
import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { clientSettings } from 'dive-common/store/settings';
import clearLengthAttributes from 'dive-common/utils/clearLengthAttributes';
import warpAnnotationsAcrossCameras from 'dive-common/utils/warpAnnotationsAcrossCameras';
import { cloneDeep } from 'lodash';
import {
useAnnotationSets, useAnnotationSet, useHandler, useCameraStore, useSelectedCamera,
useAlignedView, useCameraCalibration,
} from 'vue-media-annotator/provides';
import { getResponseError } from 'vue-media-annotator/utils';
import { parentDatasetId } from 'dive-common/compositeDatasetId';

export default defineComponent({
name: 'ImportAnnotations',
Expand Down Expand Up @@ -52,7 +55,14 @@ export default defineComponent({
const { reloadAnnotations, save } = useHandler();
const cameraStore = useCameraStore();
const selectedCamera = useSelectedCamera();
const alignedView = useAlignedView();
const isMulticamDataset = computed(() => cameraStore.camMap.value.size > 1);
// Warping detections onto other cameras requires the whole rig to be
// calibrated (a native->reference transform for every camera).
const canWarpToAllCameras = computed(
() => isMulticamDataset.value && alignedView.available.value,
);
const warpToAllCameras = ref(false);
const activeCameraName = computed(() => {
if (!isMulticamDataset.value) {
return null;
Expand All @@ -68,6 +78,12 @@ export default defineComponent({
const cameraFileSupported = computed(
() => !!api.importCalibrationFile && props.subType === 'stereo',
);
// Camera-alignment calibration import (per-camera transform files) is
// meaningful for any multicam dataset.
const cameraCalibration = useCameraCalibration();
const alignmentCalibrationSupported = computed(
() => !!api.importCameraCalibration && isMulticamDataset.value,
);
const currentCalibrationName = computed(() => {
if (!props.calibrationFile) return '';
return props.calibrationFile.replace(/^.*[\\/]/, '');
Expand Down Expand Up @@ -141,9 +157,24 @@ export default defineComponent({
}

if (importFile) {
processing.value = false;
await reloadAnnotations();
if (
warpToAllCameras.value
&& canWarpToAllCameras.value
&& activeCameraName.value
&& alignedView.toReference.value
) {
const warped = warpAnnotationsAcrossCameras(
cameraStore,
alignedView.toReference.value,
activeCameraName.value,
);
if (warped.tracks > 0) {
await save();
}
}
}
processing.value = false;
}
} catch (error) {
const text = [getResponseError(error)];
Expand Down Expand Up @@ -182,6 +213,49 @@ export default defineComponent({
});
}
};
const openAlignmentCalibrationUpload = async () => {
if (!api.importCameraCalibration) return;
try {
const ret = await openFromDisk('transform');
if (ret.canceled || !ret.filePaths.length) return;
menuOpen.value = false;
processing.value = true;
const result = await api.importCameraCalibration(
props.datasetId,
ret.filePaths[0],
ret.fileList?.[0],
);
// Rehydrate the store from the freshly persisted meta so the Align
// View and mirroring pick up the new transforms immediately.
const meta = await api.loadMetadata(parentDatasetId(props.datasetId));
cameraCalibration.hydrate(
meta.cameraHomographies,
meta.cameraCorrespondences,
meta.cameraTransformTypes,
meta.cameraCalibrationSource,
);
processing.value = false;
const unknown = result.cameras.filter((name) => !cameraStore.camMap.value.has(name));
if (unknown.length) {
await prompt({
title: 'Calibration Imported',
text: [
`Imported ${result.pairCount} pair(s), but the file names camera(s) not in this dataset:`,
unknown.join(', '),
'Pair bodies name their own cameras, so these pairs will not resolve until matching cameras exist.',
],
positiveButton: 'OK',
});
}
} catch (error) {
processing.value = false;
prompt({
title: 'Calibration Import Failed',
text: [getResponseError(error)],
positiveButton: 'OK',
});
}
};
const applyLastCalibration = async () => {
if (!api.importCalibrationFile || !lastCalibrationPath.value) return;
try {
Expand Down Expand Up @@ -211,10 +285,12 @@ export default defineComponent({
return {
openUpload,
openCalibrationUpload,
openAlignmentCalibrationUpload,
applyLastCalibration,
showLastCalibrationSuggestion,
lastCalibrationFileName,
cameraFileSupported,
alignmentCalibrationSupported,
currentCalibrationName,
processing,
menuOpen,
Expand All @@ -225,6 +301,8 @@ export default defineComponent({
currentSet,
isMulticamDataset,
activeCameraName,
canWarpToAllCameras,
warpToAllCameras,
};
},
});
Expand Down Expand Up @@ -295,6 +373,16 @@ export default defineComponent({
</v-icon>
{{ activeCameraName }}
</div>
<v-checkbox
v-if="canWarpToAllCameras"
v-model="warpToAllCameras"
label="Import to all cameras"
hint="Copy the detections onto every camera,
warped with the dataset calibration"
persistent-hint
dense
class="mt-3 mb-0"
/>
</v-alert>
</v-card-text>
<v-card-text>
Expand Down Expand Up @@ -372,6 +460,31 @@ export default defineComponent({
</div>
</v-col>
</v-container>
<template v-if="alignmentCalibrationSupported">
<v-divider />
<v-card-title class="pt-3">
Import Calibration
</v-card-title>
<v-card-text class="pb-0">
Merge a DIVE camera-alignment calibration .json (e.g. a per-camera
calibration_&lt;camera&gt;.json) into this dataset. Pairs the file
names replace the current ones; other cameras' pairs are kept.
</v-card-text>
<v-container>
<v-col>
<v-row>
<v-btn
depressed
block
:disabled="!datasetId || processing"
@click="openAlignmentCalibrationUpload"
>
Import Calibration
</v-btn>
</v-row>
</v-col>
</v-container>
</template>
<template v-if="cameraFileSupported">
<v-divider />
<v-card-title class="pt-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
Optional per-camera transform/calibration file picker with clear and open
actions. Shown for cameras after the first; the desktop backend parses the
file at import time and seeds the dataset's saved camera calibration (the
same calibration the in-app panel edits and the Align button applies).
Accepts a DIVE calibration .json.
-->
<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
props: {
transformFile: String,
cameraName: String,
},
});
</script>

<template>
<v-row
class="align-center"
no-gutters
>
<v-text-field
label="Transform file (.json)"
:placeholder="`Transform mapping the first camera to ${cameraName}`"
hint="Optional: DIVE calibration .json"
persistent-hint
outlined
dense
clearable
:value="transformFile"
class="mr-3"
@click:clear="$emit('clear')"
/>
<v-btn
color="primary"
@click="$emit('open')"
>
Open
<v-icon class="ml-2">
mdi-matrix
</v-icon>
</v-btn>
</v-row>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export default defineComponent({
type: Boolean,
default: false,
},
/** Offer per-camera calibration .json transform pickers (desktop multicam only). */
enableTransformImport: {
type: Boolean,
default: false,
},
registerSubfolderCameras: {
type: Function as PropType<(assignments: {
cameraName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DatasetType } from 'dive-common/apispec';
import ImportMultiCamCameraGroup from './ImportMultiCamCameraGroup.vue';
import ImportMultiCamChooseSource from './ImportMultiCamChooseSource.vue';
import ImportMultiCamChooseAnnotation from './ImportMultiCamChooseAnnotation.vue';
import ImportMultiCamChooseTransform from './ImportMultiCamChooseTransform.vue';
import ImportMultiCamAddType from './ImportMultiCamAddType.vue';
import ImportMultiCamCameraOrderControls from './ImportMultiCamCameraOrderControls.vue';
import { importMultiCamContextProp } from './importMultiCamContext';
Expand All @@ -18,6 +19,7 @@ export default defineComponent({
ImportMultiCamCameraGroup,
ImportMultiCamChooseSource,
ImportMultiCamChooseAnnotation,
ImportMultiCamChooseTransform,
ImportMultiCamAddType,
ImportMultiCamCameraOrderControls,
},
Expand All @@ -43,6 +45,9 @@ export default defineComponent({
addNewSet: props.ctx.addNewSet,
open: props.ctx.open,
openAnnotationFile: props.ctx.openAnnotationFile,
showTransformFileField: props.ctx.showTransformFileField,
openTransformFile: props.ctx.openTransformFile,
clearTransformFile: props.ctx.clearTransformFile,
};
},
});
Expand Down Expand Up @@ -81,6 +86,14 @@ export default defineComponent({
@clear="folderList[key].trackFile = ''"
@open="openAnnotationFile(key)"
/>
<ImportMultiCamChooseTransform
v-if="folderList[key].sourcePath && showTransformFileField(key)"
:camera-name="key"
:transform-file="folderList[key].transformFile"
class="my-3"
@clear="clearTransformFile(key)"
@open="openTransformFile(key)"
/>
</ImportMultiCamCameraGroup>
<ImportMultiCamAddType
v-if="!stereo"
Expand Down
Loading
Loading