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
4 changes: 3 additions & 1 deletion client/dive-common/apispec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,11 @@ interface DatasetConfigMutable {
cameraTransformTypes?: CameraTransformTypes;
/** Producer provenance of the camera registration (see RegistrationSource). */
cameraRegistrationSource?: RegistrationSource | null;
/** Frame label mode label set defined by the dataset, in hotkey order. */
frameLabels?: string[];
error?: string;
}
const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource'];
const DatasetConfigMutableKeys = ['attributes', 'confidenceFilters', 'timeFilters', 'imageEnhancements', 'customTypeStyling', 'customGroupStyling', 'attributeTrackFilters', 'datasetInfo', 'cameraHomographies', 'cameraCorrespondences', 'cameraTransformTypes', 'cameraRegistrationSource', 'frameLabels'];
/**
* Mutable keys the multicam/stereo viewer loads from the parent dataset.
* Camera-targeted imports sync only these onto the parent — not per-camera
Expand Down
182 changes: 182 additions & 0 deletions client/dive-common/components/FrameLabelPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useTrackStyleManager } from 'vue-media-annotator/provides';
import { clientSettings } from 'dive-common/store/settings';

export default defineComponent({
name: 'FrameLabelPanel',

props: {
value: {
type: Boolean,
default: false,
},
labels: {
type: Array as () => string[],
default: () => [],
},
/**
* True when the label set comes from the dataset itself; the list is
* fixed and the global-list editing controls are hidden.
*/
datasetDefined: {
type: Boolean,
default: false,
},
activeLabel: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
},

setup() {
const { typeStyling } = useTrackStyleManager();
const newLabel = ref('');

function addLabel() {
const label = newLabel.value.trim();
const stored = clientSettings.frameLabelSettings.labels;
if (label && !stored.includes(label) && stored.length < 9) {
stored.push(label);
}
newLabel.value = '';
}

function removeLabel(label: string) {
const index = clientSettings.frameLabelSettings.labels.indexOf(label);
if (index >= 0) {
clientSettings.frameLabelSettings.labels.splice(index, 1);
}
}

return {
newLabel,
addLabel,
removeLabel,
typeStyling,
};
},
});
</script>

<template>
<div class="px-2 py-1">
<v-divider />
<div class="d-flex align-center">
<v-switch
:input-value="value"
:disabled="disabled || labels.length === 0"
label="Frame label mode"
dense
hide-details
class="my-1 py-0"
@change="$emit('input', !!$event)"
/>
<v-spacer />
<v-tooltip
open-delay="200"
bottom
max-width="300"
>
<template #activator="{ on }">
<v-icon
small
class="mr-1"
v-on="on"
>
mdi-help-circle
</v-icon>
</template>
<span>
While enabled, keys 1-9 label all frames from the current frame
forward with the corresponding label, until the next labeled event
or the end of the video. Press 0 to end the current label without
starting a new one. Labels are saved as full-frame tracks.
</span>
</v-tooltip>
</div>
<div
v-if="value && activeLabel !== null"
class="text-caption mb-1"
>
Current frame:
<v-chip
x-small
:color="typeStyling.color(activeLabel)"
class="ml-1"
>
{{ activeLabel }}
</v-chip>
</div>
<div
v-for="(label, index) in labels"
:key="label"
class="d-flex align-center my-1"
>
<v-chip
x-small
outlined
class="mr-2 px-1 hotkey-chip"
>
{{ index + 1 }}
</v-chip>
<span
class="text-body-2 text-truncate"
:style="{ color: typeStyling.color(label) }"
>
{{ label }}
</span>
<v-spacer />
<v-btn
v-if="!datasetDefined"
icon
x-small
:disabled="disabled"
@click="removeLabel(label)"
>
<v-icon x-small>
mdi-close
</v-icon>
</v-btn>
</div>
<div
v-if="datasetDefined"
class="text-caption text--secondary mb-1"
>
Labels defined by this dataset
</div>
<v-text-field
v-else
v-model="newLabel"
:disabled="disabled || labels.length >= 9"
label="Add label"
dense
hide-details
class="my-1"
@keydown.enter="addLabel"
>
<template #append>
<v-btn
icon
x-small
:disabled="!newLabel.trim()"
@click="addLabel"
>
<v-icon small>
mdi-plus
</v-icon>
</v-btn>
</template>
</v-text-field>
</div>
</template>

<style scoped>
.hotkey-chip {
font-family: monospace;
}
</style>
61 changes: 61 additions & 0 deletions client/dive-common/components/Viewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { cloneDeep, debounce } from 'lodash';
/* VUE MEDIA ANNOTATOR */
import {
useAttributes,
useFrameLabelMode,
useImageEnhancements,
useLineChart,
useTimeObserver,
Expand Down Expand Up @@ -93,6 +94,7 @@ import MultiCamToolbar from './MultiCamToolbar.vue';
import AlignedViewToggle from './AlignedViewToggle.vue';
import PrimaryAttributeTrackFilter from './PrimaryAttributeTrackFilter.vue';
import UserSettingsDialog from './UserSettingsDialog.vue';
import FrameLabelPanel from './FrameLabelPanel.vue';

export interface ImageDataItem {
url: string;
Expand Down Expand Up @@ -127,6 +129,7 @@ export default defineComponent({
ConfidenceSubsection,
AttributeSubsection,
AttributeEditor,
FrameLabelPanel,
},

// TODO: remove this in vue 3
Expand Down Expand Up @@ -624,6 +627,47 @@ export default defineComponent({
alignedView.setSuspended(picking);
}, { immediate: true });

// A dataset may define its own frame label set (e.g. presets applied at
// import); it takes precedence over the user's global label list.
const datasetFrameLabels = ref([] as string[]);
const frameLabels = computed(() => (datasetFrameLabels.value.length
? datasetFrameLabels.value
: clientSettings.frameLabelSettings.labels));
const frameLabelMode = useFrameLabelMode({
cameraStore,
selectedCamera,
labels: frameLabels,
getMaxFrame: () => aggregateController.value.maxFrame.value,
getFullFrameBounds: () => {
const bounds = aggregateController.value
.getController(selectedCamera.value).originalBounds.value;
return [bounds.left, bounds.top, bounds.right, bounds.bottom];
},
});
const frameLabelMousetrap = computed(() => {
if (!frameLabelMode.enabled.value || readonlyState.value) {
return [];
}
const binds = frameLabels.value.slice(0, 9)
.map((label, index) => ({
bind: `${index + 1}`,
handler: () => frameLabelMode.labelFrame(label, time.frame.value),
}));
binds.push({ bind: '0', handler: () => frameLabelMode.endLabel(time.frame.value) });
return binds;
});
const frameLabelActive = computed(() => {
if (!frameLabelMode.enabled.value) {
return null;
}
// pendingSaveCount registers annotation edits as a dependency; the
// track stores themselves are not deeply reactive
if (pendingSaveCount.value < 0) {
return null;
}
return frameLabelMode.labelAtFrame(time.frame.value);
});

/**
* Every camera pane calls updateTime() from its own seek/play/pause, but
* useTime()'s frame/flick is a single shared value consumed app-wide as
Expand Down Expand Up @@ -1404,6 +1448,7 @@ export default defineComponent({
resetMulticamAlignment();
}
/* Otherwise, complete loading of the dataset */
datasetFrameLabels.value = meta.frameLabels ?? [];
trackStyleManager.populateTypeStyles(meta.customTypeStyling);
groupStyleManager.populateTypeStyles(meta.customGroupStyling);
if (meta.customTypeStyling) {
Expand Down Expand Up @@ -1978,6 +2023,11 @@ export default defineComponent({
editingMode,
editingDetails,
eventChartData,
frameLabelEnabled: frameLabelMode.enabled,
frameLabelMousetrap,
frameLabelActive,
frameLabels,
datasetFrameLabels,
groupChartData,
imageData,
lineChartData,
Expand Down Expand Up @@ -2310,6 +2360,15 @@ export default defineComponent({
@track-seek="seekToFrame($event)"
>
<template>
<v-divider />
<frame-label-panel
:value="frameLabelEnabled"
:labels="frameLabels"
:dataset-defined="datasetFrameLabels.length > 0"
:active-label="frameLabelActive"
:disabled="readonlyState"
@input="frameLabelEnabled = $event"
/>
<v-divider />
<primary-attribute-track-filter
:toggle="context.toggle"
Expand Down Expand Up @@ -2342,6 +2401,7 @@ export default defineComponent({
{ bind: 'r', handler: () => resetAggregateZoom() },
{ bind: 'esc', handler: () => handler.trackAbort() },
{ bind: 'e', handler: () => multiCamList.length === 1 && selectedTrackId !== null && handler.trackEdit(selectedTrackId) },
...frameLabelMousetrap,
]"
class="d-flex flex-column grow"
>
Expand Down Expand Up @@ -2444,6 +2504,7 @@ export default defineComponent({
{ bind: 'esc', handler: () => handler.trackAbort() },
{ bind: 'e', handler: () => multiCamList.length === 1 && selectedTrackId !== null && handler.trackEdit(selectedTrackId) },
{ bind: 'a', handler: () => sidebarMode === 'bottom' && toggleBottomRightPanel() },
...frameLabelMousetrap,
]"
class="d-flex flex-column grow"
style="min-height: 0; min-width: 0;"
Expand Down
7 changes: 7 additions & 0 deletions client/dive-common/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ interface AnnotationSettings {
loading: boolean;
loadingMessage: string;
};
frameLabelSettings: {
// Track types managed by frame label mode, in hotkey (1-9) order
labels: string[];
};
}

const defaultSettings: AnnotationSettings = {
Expand Down Expand Up @@ -183,6 +187,9 @@ const defaultSettings: AnnotationSettings = {
loading: false,
loadingMessage: '',
},
frameLabelSettings: {
labels: [],
},
};
const MIN_AUTO_SAVE_DELAY_SECONDS = 10;

Expand Down
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"build:web": "vite build",
"build:electron": "electron-vite build && electron-builder --config electron-builder.json",
"build:electron:dir": "electron-vite build && electron-builder --config electron-builder.json --dir",
"build:cli": "esbuild platform/desktop/backend/cli.ts --bundle --platform=node --target=node18 --format=cjs --external:electron --inject:platform/desktop/backend/importMetaUrlShim.js --define:import.meta.url=import_meta_url --alias:yargs=./node_modules/yargs/index.cjs --alias:vue-media-annotator=./src --alias:platform=./platform --alias:dive-common=./dive-common --outfile=bin/platform/desktop/backend/cli.js",
"divecli": "node ./bin/platform/desktop/backend/cli.js",
"lint": "eslint --ext .js,.ts,.vue src/ dive-common/ platform/",
"lint:templates": "eslint --ext vue src/ dive-common/ platform/",
Expand Down
Loading
Loading