Skip to content
Merged
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
1 change: 1 addition & 0 deletions client/dive-common/components/BottomPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export default defineComponent({
:show-empty-types="clientSettings.typeSettings.showEmptyTypes"
:height="130"
:width="300"
:header-height="50"
:style-manager="trackStyleManager"
:filter-controls="trackFilters"
:disabled="disableAnnotationFilters"
Expand Down
34 changes: 34 additions & 0 deletions client/dive-common/components/TypeSettingsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default defineComponent({
preventCascadeTypes: 'When a track has multiple types, this will prevent the type from displaying if the max type is not visible in the type list',
filterTypesByFrame: 'Filters the type list by only types that are visible in the current frame',
maxCountButton: 'Show a max count button that will jump to the frame with the max count for the type',
suppressionType: 'Detections lying 50% or more under an annotation of this type are hidden and excluded from counts. Leave empty to disable.',
});
const importInstructions = ref('Please provide a list of types (separated by a new line) that you would like to import');
const importDialog = ref(false);
Expand Down Expand Up @@ -279,6 +280,39 @@ export default defineComponent({
</v-tooltip>
</v-col>
</v-row>
<v-row class="mt-5">
<v-col class="py-1">
<v-select
v-model="clientSettings.typeSettings.suppressionType"
:items="allTypes"
label="Suppression Region Type"
class="my-0 ml-1 pt-0"
dense
clearable
hide-details
/>
</v-col>
<v-col
cols="2"
class="py-1"
align="right"
>
<v-tooltip
open-delay="200"
bottom
>
<template #activator="{ on }">
<v-icon
small
v-on="on"
>
mdi-help
</v-icon>
</template>
<span>{{ help.suppressionType }}</span>
</v-tooltip>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-menu>
Expand Down
4 changes: 4 additions & 0 deletions client/dive-common/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ interface AnnotationSettings {
preventCascadeTypes?: boolean;
filterTypesByFrame?: boolean;
maxCountButton?: boolean;
// Detections whose geometry lies >=50% under a region of this type are
// hidden and excluded from counts. Empty string disables the feature.
suppressionType?: string;
};
trackSettings: {
newTrackSettings: {
Expand Down Expand Up @@ -127,6 +130,7 @@ const defaultSettings: AnnotationSettings = {
lockTypes: false,
preventCascadeTypes: false,
maxCountButton: false,
suppressionType: 'Suppressed',
},
rowsPerPage: 20,
annotationFPS: 10,
Expand Down
90 changes: 79 additions & 11 deletions client/src/components/FilterList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { usePrompt } from 'dive-common/vue-utilities/prompt-service';
import { clientSettings } from 'dive-common/store/settings';
import {
useCameraStore, useHandler, useReadOnlyMode, useSelectedCamera, useTime,
usePendingSaveCount,
} from '../provides';
import TooltipBtn from './TooltipButton.vue';
import TypeEditor from './TypeEditor.vue';
Expand All @@ -17,18 +18,17 @@ import BaseFilterControls from '../BaseFilterControls';
import Track from '../track';
import Group from '../Group';
import StyleManager from '../StyleManager';
import { getSuppressedTrackIds } from '../use/suppression';

interface VirtualTypeItem {
type: string;
confidenceFilterNum: number;
displayText: string;
color: string;
checked: boolean;
isSuppressionType: boolean;
}

/* Magic numbers involved in height calculation */
const TypeListHeaderHeight = 80;

export default defineComponent({
name: 'FilterList',

Expand All @@ -47,6 +47,11 @@ export default defineComponent({
type: Number,
default: 300,
},
/* Sidebar (~80) vs bottom bar (~50) header chrome */
headerHeight: {
type: Number,
default: 80,
},
filterControls: {
type: Object as PropType<BaseFilterControls<Track | Group>>,
required: true,
Expand All @@ -68,6 +73,10 @@ export default defineComponent({
const cameraStore = useCameraStore();
const selectedCamera = useSelectedCamera();
const { frame } = useTime();
// Bumped on every annotation edit (add/move/delete); read in the frame
// counts so they re-evaluate suppression when a region is moved, since a
// track's geometry is not itself a reactive dependency.
const pendingSaveCount = usePendingSaveCount();
const trackStore = cameraStore.camMap.value.get(selectedCamera.value)?.trackStore;
// Ordering of these lists should match
const sortingMethods: ('a-z' | 'count' | 'frame count')[] = ['a-z', 'count', 'frame count'];
Expand Down Expand Up @@ -137,20 +146,76 @@ export default defineComponent({
}
}

const typeCounts = computed(() => filteredTracksRef.value.reduce((acc, filteredTrack) => {
const confidencePair = filteredTrack.annotation
.getType(filteredTrack.context.confidencePairIndex);
const trackType = confidencePair;
acc.set(trackType, (acc.get(trackType) || 0) + 1);
/**
* Ids of tracks whose every keyframe detection is suppressed (a region
* covers it on each frame it appears), across all cameras - these are
* excluded from the dataset-wide type totals. Reactive to region edits via
* the save counter, since track geometry is not itself a reactive value.
*/
const fullySuppressedIds = computed(() => {
const editRevision = pendingSaveCount.value;
const suppType = clientSettings.typeSettings.suppressionType;
const excluded = new Set<number>();
if (!suppType || editRevision < 0) {
return excluded;
}
cameraStore.camMap.value.forEach(({ trackStore: store }) => {
const perFrame = new Map<number, Set<number>>();
const suppressedAt = (f: number) => {
let s = perFrame.get(f);
if (s === undefined) {
s = getSuppressedTrackIds(store, f, suppType);
perFrame.set(f, s);
}
return s;
};
store.annotationMap.forEach((annotation) => {
const track = annotation as Track;
if (typeof track.getFeature !== 'function') {
return;
}
const keyframes = track.features.filter((f) => f && f.keyframe);
if (keyframes.length > 0
&& keyframes.every((f) => suppressedAt(f.frame).has(track.id))) {
excluded.add(track.id);
}
});
});
return excluded;
});

return acc;
}, new Map<string, number>()));
const typeCounts = computed(() => {
const excluded = fullySuppressedIds.value;
return filteredTracksRef.value.reduce((acc, filteredTrack) => {
if (excluded.has(filteredTrack.annotation.id)) {
return acc;
}
const confidencePair = filteredTrack.annotation
.getType(filteredTrack.context.confidencePairIndex);
const trackType = confidencePair;
acc.set(trackType, (acc.get(trackType) || 0) + 1);

return acc;
}, new Map<string, number>());
});

const filteredTracksForFrame = computed(() => {
// Depend on the edit counter so moving/resizing a suppression region
// (which mutates geometry, not the reactive track set) re-runs the count.
// It is always >= 0, so this reads the dependency without changing logic.
const editRevision = pendingSaveCount.value;
const trackIdsForFrame = trackStore?.intervalTree
.search([frame.value, frame.value])
.map((str) => parseInt(str, 10));
// Detections suppressed by a region on this frame are dropped so the
// per-frame type counts read off the interface exclude them.
const suppressedIds = (trackStore && editRevision >= 0)
? getSuppressedTrackIds(trackStore, frame.value, clientSettings.typeSettings.suppressionType)
: new Set<number>();
const filteredKeyFrameTracks = filteredTracksRef.value.filter((track) => {
if (suppressedIds.has(track.annotation.id)) {
return false;
}
const keyframe = trackStore?.getPossible(track.annotation.id)?.getFeature(frame.value)[0];
return !!keyframe?.keyframe;
});
Expand Down Expand Up @@ -206,12 +271,14 @@ export default defineComponent({
if (filterTypesByFrame.value) {
filteredTypeList = filteredTypeList.filter((item) => frameTrackTypesDeRef.get(item));
}
const { suppressionType } = clientSettings.typeSettings;
return filteredTypeList.map((item) => ({
type: item,
confidenceFilterNum: confidenceFiltersDeRef[item] || 0,
displayText: `${typeCountsDeRef.get(item) || 0}:${frameTrackTypesDeRef.get(item) || 0} ${item}`,
color: typeStylingDeRef.color(item),
checked: checkedTypesDeRef.includes(item),
isSuppressionType: !!suppressionType && item === suppressionType,
}));
});
const headCheckState = computed(() => {
Expand Down Expand Up @@ -249,7 +316,7 @@ export default defineComponent({
}
}

const virtualHeight = computed(() => props.height - TypeListHeaderHeight);
const virtualHeight = computed(() => props.height - props.headerHeight);

const goToPeakTrackFrame = (trackType: string) => {
const frameCounts = new Map<number, number>();
Expand Down Expand Up @@ -425,6 +492,7 @@ export default defineComponent({
:width="width"
:display-max-button="showMaxFrameButton"
:disabled="disableAnnotationFilters"
:is-suppression-type="item.isSuppressionType"
@setCheckedTypes="updateCheckedType($event, item.type)"
@goToMaxFrame="goToPeakTrackFrame($event)"
@clickEdit="clickEdit"
Expand Down
11 changes: 11 additions & 0 deletions client/src/components/LayerManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import TextLayer, { FormatTextRow } from '../layers/AnnotationLayers/TextLayer';
import AttributeLayer from '../layers/AnnotationLayers/AttributeLayer';
import AttributeBoxLayer from '../layers/AnnotationLayers/AttributeBoxLayer';
import type { AnnotationId } from '../BaseAnnotation';
import { getSuppressedTrackIds } from '../use/suppression';
import { VisibleAnnotationTypes } from '../layers';
import UILayer from '../layers/UILayers/UILayer';
import ToolTipWidget from '../layers/UILayers/ToolTipWidget.vue';
Expand Down Expand Up @@ -344,6 +345,11 @@ export default defineComponent({
if (currentFrameIds === undefined) {
return;
}
// Detections lying >=50% under a suppression region on this frame are
// hidden from every layer at once (and excluded from counts elsewhere).
const suppressedIds = trackStore
? getSuppressedTrackIds(trackStore, frame, clientSettings.typeSettings.suppressionType)
: new Set<AnnotationId>();
currentFrameIds.forEach(
(trackId: AnnotationId) => {
const track = trackStore?.getPossible(trackId);
Expand All @@ -352,6 +358,9 @@ export default defineComponent({
// TODO: Find a better way to represent tracks outside of cameras
return;
}
if (suppressedIds.has(trackId)) {
return;
}

const enabledIndex = enabledTracks.findIndex(
(trackWithContext) => trackWithContext.annotation.id === trackId,
Expand Down Expand Up @@ -607,6 +616,8 @@ export default defineComponent({
toRef(props, 'colorBy'),
selectedCamera,
selectedKeyRef,
// re-render when the suppression-region type is changed
() => clientSettings.typeSettings.suppressionType,
],
() => {
refreshLayers();
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/Tracks/TrackList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ import {
useTrackStyleManager,
useMultiSelectList,
useCameraStore,
useSelectedCamera,
usePendingSaveCount,
} from '../../provides';
import useVirtualScrollTo from '../../use/useVirtualScrollTo';
import { getSuppressedTrackIds } from '../../use/suppression';
import SideBarTrackListView from './sidebar/SideBarTrackListView.vue';
import BottomBarTrackListView from './bottombar/BottomBarTrackListView.vue';

Expand Down Expand Up @@ -81,6 +84,8 @@ export default defineComponent({
const editingModeRef = useEditingMode();
const selectedTrackIdRef = useSelectedTrackId();
const cameraStore = useCameraStore();
const selectedCamera = useSelectedCamera();
const pendingSaveCount = usePendingSaveCount();
const filteredTracksRef = trackFilters.filteredAnnotations;
const typeStylingRef = useTrackStyleManager().typeStyling;
const { frame: frameRef, isPlaying } = useTime();
Expand Down Expand Up @@ -109,7 +114,18 @@ export default defineComponent({
const finalFilteredTracks = computed(() => {
let tracks = filteredTracksRef.value;
if (filterDetectionsByFrame.value && !isPlaying.value) {
// Depend on the edit counter so moving a suppression region re-runs the
// filter (geometry mutations are not reactive track-set changes).
const editRevision = pendingSaveCount.value;
const suppressCamStore = cameraStore.camMap.value.get(selectedCamera.value)?.trackStore;
const suppType = clientSettings.typeSettings.suppressionType;
const suppressedIds = (suppressCamStore && editRevision >= 0)
? getSuppressedTrackIds(suppressCamStore, frameRef.value, suppType)
: new Set<number>();
tracks = tracks.filter((track) => {
if (suppressedIds.has(track.annotation.id)) {
return false;
}
const possibleTrack = cameraStore.getAnyPossibleTrack(track.annotation.id);
if (possibleTrack) {
const [feature] = possibleTrack.getFeature(frameRef.value);
Expand Down
31 changes: 31 additions & 0 deletions client/src/components/TypeItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
isSuppressionType: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
/* Horizontal padding is the width of checkbox, scrollbar, and edit button */
Expand Down Expand Up @@ -111,6 +115,28 @@ export default defineComponent({
</template>
<span>Type has threshold set individually</span>
</v-tooltip>
<v-tooltip
v-if="isSuppressionType"
open-delay="200"
bottom
max-width="280"
>
<template #activator="{ on, attrs }">
<v-icon
small
class="ml-1 suppression-icon"
color="orange darken-2"
v-bind="attrs"
v-on="on"
>
mdi-eye-off
</v-icon>
</template>
<span>
This type is used for suppression.
Detections lying 50% or more under its regions are hidden and excluded from counts.
</span>
</v-tooltip>
</div>
</template>
</v-checkbox>
Expand Down Expand Up @@ -190,4 +216,9 @@ export default defineComponent({
padding: 0 5px;
font-size: 12px;
}

.suppression-icon {
flex-shrink: 0;
align-self: center;
}
</style>
Loading
Loading