Skip to content

Commit 3fdb892

Browse files
Ask which side of the confidence threshold a delete of every listed track reaches (#1944)
* Ask which side of the confidence threshold a delete of every listed track reaches The track list only shows what passes the confidence thresholds, so its delete button asks: above (the listed tracks), below (hidden ones), or all. * Delete whole tracks in every threshold scope * Widen the delete scope dialog to keep each option on one line * Make all tracks the first default delete scope and tighten the dialog * Shorten delete scope dialog prompt * fix below threshold delete removing non visible tracks --------- Co-authored-by: Bryon Lewis <Bryon.Lewis@kitware.com>
1 parent 899409e commit 3fdb892

11 files changed

Lines changed: 454 additions & 95 deletions

File tree

‎client/dive-common/components/Viewer.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ export default defineComponent({
743743
sorted: cameraStore.sortedTracks,
744744
remove: removeTracks,
745745
markChangesPending: (markChangesPending as MarkChangesPendingFilter),
746-
lookupGroups: cameraStore.lookupGroups,
746+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
747747
getTracks: (track: AnnotationId) => cameraStore.getTrackAll(track),
748748
renameTrackPair: (id, currentType, newType) => (
749749
cameraStore.renameTrackPair(id, currentType, newType)

‎client/src/BaseFilterControls.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ref, computed, Ref, watch,
33
} from 'vue';
4+
import { resolveConfidenceThreshold } from 'dive-common/typeHierarchy';
45
import type { AnnotationId, ConfidencePair } from './BaseAnnotation';
56
import { SortedAnnotation } from './BaseAnnotationStore';
67
import type Group from './Group';
@@ -15,6 +16,9 @@ interface MarkChangesPendingData {
1516
export type MarkChangesPendingFilter = (data?: MarkChangesPendingData) => void;
1617

1718
export const DefaultConfidence = 0.1;
19+
20+
/** Which annotations a delete-all reaches, relative to each type's threshold. */
21+
export type ThresholdScope = 'above' | 'below' | 'all';
1822
/**
1923
* AnnotationWithContext wraps an annotation with additional information
2024
* such as why the annotation was included or returned by a system
@@ -235,6 +239,21 @@ export default abstract class BaseFilterControls<T extends Track | Group> {
235239
});
236240
}
237241

242+
/**
243+
* Tracks with enabled classes, none of which reach their confidence threshold.
244+
* TrackFilterControls overrides this to also apply time, group, and attribute filters.
245+
*/
246+
annotationIdsBelowThreshold(types: string[]): AnnotationId[] {
247+
const wanted = new Set(types);
248+
const filters = this.confidenceFilters.value;
249+
return this.sorted.value.filter((annotation) => {
250+
const matching = annotation.confidencePairs.filter(([type]) => wanted.has(type));
251+
return matching.length > 0 && matching.every(([type, confidence]) => (
252+
confidence < resolveConfidenceThreshold(filters, type)
253+
));
254+
}).map(({ id }) => id);
255+
}
256+
238257
updateCheckedTypes(types: string[]) {
239258
this.checkedTypes.value = types;
240259
}

‎client/src/TrackFilterControls.spec.ts‎

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ function makeTrackFilterControls(markPending: MarkChangesPendingFilter = markCha
101101
remove,
102102
markChangesPending: markPending,
103103
groupFilterControls,
104-
lookupGroups: cameraStore.lookupGroups,
104+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
105105
getTracks: (track: AnnotationId) => cameraStore.getTrackAll(track),
106106
renameTrackPair: (id, currentType, newType) => (
107107
cameraStore.renameTrackPair(id, currentType, newType)
@@ -126,7 +126,7 @@ function makePairFixture(
126126
remove: (id) => cameraStore.removeTracks(id),
127127
markChangesPending: markPending,
128128
groupFilterControls,
129-
lookupGroups: cameraStore.lookupGroups,
129+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
130130
getTracks: (id) => cameraStore.getTrackAll(id),
131131
renameTrackPair: (id, currentType, newType) => (
132132
cameraStore.renameTrackPair(id, currentType, newType)
@@ -449,6 +449,132 @@ describe('useAnnotationFilters', () => {
449449
expect(cameraStore.getTrack(1).confidencePairs).toEqual([['baz', 0.7]]);
450450
});
451451

452+
it('selects whole hidden tracks without stripping their other class scores', () => {
453+
const { cameraStore, filters } = makePairFixture([
454+
[['fish', 0.9], ['shark', 0.1]],
455+
[['fish', 0.2], ['shark', 0.1]],
456+
[['shark', 0.1]],
457+
[['fish', 0.5]],
458+
]);
459+
filters.setConfidenceFilters({ default: 0.5 });
460+
filters.updateCheckedTypes(['fish']);
461+
const ids = filters.annotationIdsBelowThreshold(['fish']);
462+
expect(ids).toEqual([1]);
463+
expect(cameraStore.getTrack(1).confidencePairs).toEqual([['fish', 0.2], ['shark', 0.1]]);
464+
ids.forEach((id) => cameraStore.removeTracks(id));
465+
expect(cameraStore.getPossibleTrack(1)).toBeUndefined();
466+
expect(cameraStore.getTrack(0).confidencePairs).toEqual([['fish', 0.9], ['shark', 0.1]]);
467+
expect(cameraStore.getPossibleTrack(2)).toBeDefined();
468+
expect(cameraStore.getPossibleTrack(3)).toBeDefined();
469+
});
470+
471+
it('does not classify a visible track by its low-scoring secondary class', () => {
472+
const { filters } = makePairFixture([
473+
[['fish', 0.9], ['shark', 0.1]],
474+
[['fish', 0.2], ['shark', 0.7]],
475+
[['fish', 0.2], ['shark', 0.1]],
476+
]);
477+
filters.setConfidenceFilters({ default: 0.5 });
478+
expect(filters.annotationIdsBelowThreshold(['fish', 'shark'])).toEqual([2]);
479+
expect(filters.annotationIdsBelowThreshold([])).toEqual([]);
480+
});
481+
482+
it('limits below-threshold delete to the active time filter', () => {
483+
const cameraStore = new CameraStore({ markChangesPending });
484+
const trackStore = cameraStore.camMap.value.get('singleCam')?.trackStore;
485+
const lateFeatures: Feature[] = [];
486+
lateFeatures[10] = { frame: 10, bounds: [0, 0, 1, 1], keyframe: true };
487+
trackStore?.insert(new Track(0, {
488+
begin: 0,
489+
end: 0,
490+
confidencePairs: [['fish', 0.2]],
491+
features,
492+
}));
493+
trackStore?.insert(new Track(1, {
494+
begin: 10,
495+
end: 10,
496+
confidencePairs: [['fish', 0.2]],
497+
features: lateFeatures,
498+
}));
499+
trackStore?.setEnableSorting();
500+
const groupFilterControls = makeGroupFilterControls(cameraStore);
501+
const filters = new TrackFilterControls({
502+
sorted: cameraStore.sortedTracks,
503+
remove: (id) => cameraStore.removeTracks(id),
504+
markChangesPending,
505+
groupFilterControls,
506+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
507+
getTracks: (id) => cameraStore.getTrackAll(id),
508+
renameTrackPair: (id, currentType, newType) => (
509+
cameraStore.renameTrackPair(id, currentType, newType)
510+
),
511+
removeTypes: (id, types) => cameraStore.removeTypes(id, types),
512+
});
513+
filters.setConfidenceFilters({ default: 0.5 });
514+
filters.setTimeFilters([0, 5]);
515+
expect(filters.annotationIdsBelowThreshold(['fish'])).toEqual([0]);
516+
filters.setTimeFilters(null);
517+
expect(filters.annotationIdsBelowThreshold(['fish'])).toEqual([0, 1]);
518+
});
519+
520+
it('limits below-threshold delete to enabled groups', () => {
521+
const cameraStore = new CameraStore({ markChangesPending });
522+
const cam = cameraStore.camMap.value.get('singleCam');
523+
cam?.trackStore.insert(new Track(0, {
524+
confidencePairs: [['fish', 0.2]],
525+
features,
526+
}));
527+
cam?.trackStore.insert(new Track(1, {
528+
confidencePairs: [['fish', 0.2]],
529+
features,
530+
}));
531+
cam?.trackStore.setEnableSorting();
532+
cam?.groupStore.insert(new Group(10, {
533+
confidencePairs: [['school', 1]],
534+
members: { 0: { ranges: [[0, 0]] } },
535+
}), { imported: true });
536+
cam?.groupStore.insert(new Group(11, {
537+
confidencePairs: [['pod', 1]],
538+
members: { 1: { ranges: [[0, 0]] } },
539+
}), { imported: true });
540+
cam?.groupStore.setEnableSorting();
541+
const groupFilterControls = makeGroupFilterControls(cameraStore);
542+
groupFilterControls.checkedTypes.value = ['school'];
543+
const filters = new TrackFilterControls({
544+
sorted: cameraStore.sortedTracks,
545+
remove: (id) => cameraStore.removeTracks(id),
546+
markChangesPending,
547+
groupFilterControls,
548+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
549+
getTracks: (id) => cameraStore.getTrackAll(id),
550+
renameTrackPair: (id, currentType, newType) => (
551+
cameraStore.renameTrackPair(id, currentType, newType)
552+
),
553+
removeTypes: (id, types) => cameraStore.removeTypes(id, types),
554+
});
555+
filters.setConfidenceFilters({ default: 0.5 });
556+
expect(filters.annotationIdsBelowThreshold(['fish'])).toEqual([0]);
557+
});
558+
559+
it('limits below-threshold delete with attribute filters', () => {
560+
const { cameraStore, filters } = makePairFixture([
561+
[['fish', 0.2]],
562+
[['fish', 0.2]],
563+
]);
564+
cameraStore.getTrack(0).attributes.quality = 'good';
565+
cameraStore.getTrack(1).attributes.quality = 'bad';
566+
filters.setConfidenceFilters({ default: 0.5 });
567+
filters.loadTrackAttributesFilter([{
568+
name: 'fish quality',
569+
type: 'track',
570+
typeFilter: ['fish'],
571+
attribute: 'quality',
572+
filter: { op: '=', val: 'good' },
573+
enabled: true,
574+
}]);
575+
expect(filters.annotationIdsBelowThreshold(['fish'])).toEqual([0]);
576+
});
577+
452578
it('returns the caller fallback without recomputing flat pair selection', () => {
453579
const { cameraStore, filters } = makePairFixture([
454580
[['root', 0.1], ['leaf', 0.9]],
@@ -655,7 +781,7 @@ describe('useAnnotationFilters', () => {
655781
remove: (id) => cameraStore.removeTracks(id),
656782
markChangesPending,
657783
groupFilterControls: groupFilters,
658-
lookupGroups: cameraStore.lookupGroups,
784+
lookupGroups: cameraStore.lookupGroups.bind(cameraStore),
659785
getTracks: (id) => cameraStore.getTrackAll(id),
660786
renameTrackPair: (id, currentType, newType) => (
661787
cameraStore.renameTrackPair(id, currentType, newType)

‎client/src/TrackFilterControls.ts‎

Lines changed: 92 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,17 @@ export default class TrackFilterControls extends BaseFilterControls<Track> {
9696

9797
private renameTrackPair: TrackFilterControlsParams['renameTrackPair'];
9898

99+
private lookupGroups: TrackFilterControlsParams['lookupGroups'];
100+
101+
private groupFilterControls: TrackFilterControlsParams['groupFilterControls'];
102+
99103
constructor(params: TrackFilterControlsParams) {
100104
super(params);
101105

102106
this.getTracks = params.getTracks;
103107
this.renameTrackPair = params.renameTrackPair;
108+
this.lookupGroups = params.lookupGroups;
109+
this.groupFilterControls = params.groupFilterControls;
104110

105111
const flatAllTypes = this.allTypes;
106112
this.typeHierarchy = ref(undefined);
@@ -125,26 +131,13 @@ export default class TrackFilterControls extends BaseFilterControls<Track> {
125131
*/
126132
this.filteredAnnotations = computed(() => {
127133
const checkedSet = this.checkedTypesSet.value;
128-
const filteredGroupsSet = new Set(params.groupFilterControls.enabledAnnotations.value
129-
.map((v) => v.annotation.id));
134+
const enabledGroupIds = this.enabledGroupIds();
130135
const confidenceFiltersVal = cloneDeep(this.confidenceFilters.value);
131136
const resultsArr: AnnotationWithContext<Track>[] = [];
132137
const resultsIds: Set<AnnotationId> = new Set();
133-
params.sorted.value.forEach((annotation) => {
134-
if (this.timeFilters.value !== null && !this.disableAnnotationFilters.value) {
135-
const [startTime, endTime] = this.timeFilters.value;
136-
if (annotation.begin > endTime || annotation.end < startTime) {
137-
return;
138-
}
139-
}
140-
let enabledInGroupFilters = true;
141-
const groups = params.lookupGroups(annotation.id);
142-
if (groups.length) {
143-
/**
144-
* This track is a member of a group,
145-
* so check that at least one of its groups is enabled
146-
*/
147-
enabledInGroupFilters = groups.some((group) => filteredGroupsSet.has(group.id));
138+
this.sorted.value.forEach((annotation) => {
139+
if (!this.passesTimeAndGroupFilters(annotation, enabledGroupIds)) {
140+
return;
148141
}
149142
let confidencePairIndex: number;
150143
if (this.hierarchyActive.value) {
@@ -165,40 +158,95 @@ export default class TrackFilterControls extends BaseFilterControls<Track> {
165158
if (
166159
(confidencePairIndex >= 0
167160
|| (!this.hierarchyActive.value && annotation.confidencePairs.length === 0))
168-
&& enabledInGroupFilters && !resultsIds.has(annotation.id)
161+
&& !resultsIds.has(annotation.id)
162+
&& this.passesAttributeFilters(
163+
annotation.id,
164+
annotation.confidencePairs[confidencePairIndex]?.[0],
165+
)
169166
) {
170-
let addValue = true;
171-
if (!this.disableAnnotationFilters.value && this.attributeFilters.value.length > 0
172-
&& this.enabledFilters.value.length > 0) {
173-
const [canonicalTrack] = params.getTracks(annotation.id);
174-
if (canonicalTrack === undefined) {
175-
addValue = false;
176-
} else {
177-
addValue = trackIdPassesFilter(
178-
annotation.id,
179-
() => canonicalTrack,
180-
this.attributeFilters.value,
181-
this.userDefinedValues.value,
182-
this.enabledFilters.value,
183-
annotation.confidencePairs[confidencePairIndex]?.[0],
184-
);
185-
}
186-
}
187-
if (addValue) {
188-
resultsIds.add(annotation.id);
189-
resultsArr.push({
190-
annotation,
191-
context: {
192-
confidencePairIndex,
193-
},
194-
});
195-
}
167+
resultsIds.add(annotation.id);
168+
resultsArr.push({
169+
annotation,
170+
context: {
171+
confidencePairIndex,
172+
},
173+
});
196174
}
197175
});
198176
return resultsArr;
199177
});
200178
}
201179

180+
private enabledGroupIds(): Set<AnnotationId> {
181+
return new Set(this.groupFilterControls.enabledAnnotations.value
182+
.map((v) => v.annotation.id));
183+
}
184+
185+
/** Time range and group-membership checks shared by the list and below-threshold delete. */
186+
private passesTimeAndGroupFilters(
187+
annotation: { id: AnnotationId; begin: number; end: number },
188+
enabledGroupIds: Set<AnnotationId>,
189+
): boolean {
190+
if (this.timeFilters.value !== null && !this.disableAnnotationFilters.value) {
191+
const [startTime, endTime] = this.timeFilters.value;
192+
if (annotation.begin > endTime || annotation.end < startTime) {
193+
return false;
194+
}
195+
}
196+
const groups = this.lookupGroups(annotation.id);
197+
if (groups.length) {
198+
return groups.some((group) => enabledGroupIds.has(group.id));
199+
}
200+
return true;
201+
}
202+
203+
private passesAttributeFilters(
204+
annotationId: AnnotationId,
205+
displayType: string | undefined,
206+
): boolean {
207+
if (this.disableAnnotationFilters.value || this.attributeFilters.value.length === 0
208+
|| this.enabledFilters.value.length === 0) {
209+
return true;
210+
}
211+
const [canonicalTrack] = this.getTracks(annotationId);
212+
if (canonicalTrack === undefined) {
213+
return false;
214+
}
215+
return trackIdPassesFilter(
216+
annotationId,
217+
() => canonicalTrack,
218+
this.attributeFilters.value,
219+
this.userDefinedValues.value,
220+
this.enabledFilters.value,
221+
displayType,
222+
);
223+
}
224+
225+
/**
226+
* Tracks with enabled classes that fail their confidence thresholds, after the
227+
* same time / group / attribute filters the track list applies.
228+
*/
229+
annotationIdsBelowThreshold(types: string[]): AnnotationId[] {
230+
const wanted = new Set(types);
231+
const filters = this.confidenceFilters.value;
232+
const enabledGroupIds = this.enabledGroupIds();
233+
return this.sorted.value.filter((annotation) => {
234+
if (!this.passesTimeAndGroupFilters(annotation, enabledGroupIds)) {
235+
return false;
236+
}
237+
const matching = annotation.confidencePairs.filter(([type]) => wanted.has(type));
238+
if (matching.length === 0 || matching.some(([type, confidence]) => (
239+
confidence >= resolveConfidenceThreshold(filters, type)
240+
))) {
241+
return false;
242+
}
243+
const displayType = matching.reduce((best, pair) => (
244+
pair[1] > best[1] ? pair : best
245+
))[0];
246+
return this.passesAttributeFilters(annotation.id, displayType);
247+
}).map(({ id }) => id);
248+
}
249+
202250
displayPairIndex(track: Readonly<Track>, flatFallbackIndex: number): number {
203251
const index = this.hierarchyIndex.value;
204252
if (index === undefined) {

0 commit comments

Comments
 (0)