From bb4c78d77349dc24a734ec754766611864daa427 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 11:15:25 -0500 Subject: [PATCH 01/11] initial implementation --- .github/tasks.md | 11 + api_spec.md | 48 ++++ demo/multi-class.html | 1 + index.d.ts | 41 ++- src/annotation_operators.ts | 109 ++++++-- src/configuration.ts | 19 ++ src/index.js | 14 + src/toolbox.ts | 408 +++++++++++++++++++++++++++-- tests/annotation_operators.test.js | 136 ++++++++++ tests/e2e/slider-api.spec.js | 38 +++ tests/types/index.test-d.ts | 1 + 11 files changed, 795 insertions(+), 31 deletions(-) create mode 100644 tests/annotation_operators.test.js diff --git a/.github/tasks.md b/.github/tasks.md index d51ecef5..1532ec91 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -1,2 +1,13 @@ ## Tasks +### Generalize Confidence Slider + +- [x] Add helpers to `src/annotation_operators.ts` (`get_spatial_annotations_with_confidence`, `get_annotation_confidence_for_class`, `findAllClassDefinitions`) +- [x] Add `ConfidenceFilterItem` to `src/toolbox.ts` and refactor `KeypointSliderItem` into a deprecated subclass +- [x] Add `AllowedToolboxItem.ConfidenceFilter`, `toolbox_map` entry, `DEFAULT_CONFIDENCE_FILTER_CONFIG`, and `confidence_filter_toolbox_item` field to `src/configuration.ts` +- [x] Add `ConfidenceFilterConfig` / `ConfidenceFilterClasses` types and `spatial_confidence_filter` deprecated key to `index.d.ts` +- [x] Add `get_confidence_filter_value()` public API to `src/index.js` +- [x] Update `api_spec.md` documentation +- [x] Add/extend tests +- [x] Run `npm run lint` and `npm run build` + diff --git a/api_spec.md b/api_spec.md index dcd90f80..2eb0d42b 100644 --- a/api_spec.md +++ b/api_spec.md @@ -346,6 +346,7 @@ enum AllowedToolboxItem { ImageFilters, // 10 AnnotationList, // 11 Keybinds, // 12 + ConfidenceFilter, // 13 } ``` You can access the AllowedToolboxItem enum by calling the static method: @@ -434,6 +435,45 @@ The `AnnotationList` toolbox item displays all annotations in the current subtas This toolbox item requires no configuration and can be added to the `toolbox_order` array using `AllowedToolboxItem.AnnotationList`. +### `confidence_filter_toolbox_item` + +The `ConfidenceFilter` toolbox item (added to `toolbox_order` via `AllowedToolboxItem.ConfidenceFilter`) deprecates (hides) or shows spatial annotations based on their confidence values. Unlike the deprecated `KeypointSlider`, it works with **all** spatial annotation types that have a confidence payload (`bbox`, `bbox3`, `polygon`, `polyline`, `contour`, `tbar`, and `point`), across every subtask. + +It supports two modes: + +- **Single-class mode** (default): a single slider applies one confidence threshold to every targeted spatial annotation across all subtasks, using each annotation's highest confidence value. +- **Multi-class mode**: one slider is shown per targeted class id. Each slider only filters annotations whose assigned (highest-confidence) class matches that slider, using that class's confidence value. Enable multi-class mode via the "Multi-Class Filtering" checkbox in the item's options. + +Any annotation with a confidence at or above the threshold is shown; any below is deprecated. Thresholds are expressed as percentages (0–100). + +Configuration object with the following custom definitions: +```javascript +type ConfidenceThreshold = { + confidence: number // Percentage threshold (0-100) +} + +type ConfidenceFilterClasses = { + "all": ConfidenceThreshold, // value used by the single-class slider + [key: number]?: ConfidenceThreshold // per-class-id values used in multi-class mode +} + +type ConfidenceFilterConfig = { + "name"?: string, // Default: "Filter Annotations By Confidence" + "filter_min"?: number, // Default: 0 (%) + "filter_max"?: number, // Default: 100 (%) + "default_values"?: ConfidenceFilterClasses, // Default: {"all": {"confidence": 0}} + "step_value"?: number, // Default: 1 (%) + "multi_class_mode"?: boolean, // Default: false + "disable_multi_class_mode"?: boolean, // Default: false + "filter_on_load"?: boolean, // Default: true + "show_options"?: boolean, // Default: true + // The spatial types to filter. Defaults to all confidence-filterable spatial types. + "target_spatial_types"?: ULabelSpatialType[], + // The class ids to create sliders for in multi-class mode. Defaults to all class ids. + "target_class_ids"?: number[], +} +``` + ### `reset_zoom_keybind` Keybind to reset the zoom level to the `initial_crop`. Default is `r`. @@ -452,6 +492,8 @@ Keybind to delete a vertex of a polygon or polyline annotation. The vertex must ### `keypoint_slider_default_value` Default value for the keypoint slider. Must be a number between 0 and 1. Default is `0`. +> **Deprecated:** The `KeypointSlider` toolbox item only filters `point` annotations. Use the `ConfidenceFilter` toolbox item (`confidence_filter_toolbox_item`) instead, which filters all spatial annotation types and supports per-class targeting and multiple sliders. + ### `filter_annotations_on_load` If true, the annotations will be filtered on load based on the `keypoint_slider_default_value`. Default is `true`. @@ -563,10 +605,16 @@ Sets the zoom to focus on the provided annotation, and switches to its subtask i *() => number | null* -- Returns the current keypoint slider value as a number between 0 and 1. Returns `null` if the KeypointSlider toolbox item is not active or the slider element is not found. +> **Deprecated:** Prefer `get_confidence_filter_value()` with the `ConfidenceFilter` toolbox item. + ### `get_distance_filter_value()` *() => object | null* -- Returns an object mapping class identifiers to their distance filter values (in pixels). The object always includes a `closest_row` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the FilterDistance toolbox item is not active or no sliders are found. +### `get_confidence_filter_value()` + +*() => object | null* -- Returns an object mapping class identifiers to their confidence threshold values (as percentages, 0–100). The object always includes an `all` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the ConfidenceFilter toolbox item is not active or no sliders are found. + ## Generic Callbacks Callbacks can be provided by calling `.on(fn, callback)` on a `ULabel` object. diff --git a/demo/multi-class.html b/demo/multi-class.html index 2d18a3c2..5887b96e 100644 --- a/demo/multi-class.html +++ b/demo/multi-class.html @@ -128,6 +128,7 @@ AllowedToolboxItem.ClassCounter, AllowedToolboxItem.AnnotationResize, AllowedToolboxItem.KeypointSlider, + AllowedToolboxItem.ConfidenceFilter, AllowedToolboxItem.FilterDistance, AllowedToolboxItem.RecolorActive, ], diff --git a/index.d.ts b/index.d.ts index 767bb9ed..e6a0fa19 100644 --- a/index.d.ts +++ b/index.d.ts @@ -36,12 +36,13 @@ export type Offset = { /** * Valid keys for the DeprecatedBy type */ -export type ValidDeprecatedBy = "human" | "confidence_filter" | "distance_from_row"; +export type ValidDeprecatedBy = "human" | "confidence_filter" | "distance_from_row" | "spatial_confidence_filter"; export type DeprecatedBy = { human?: boolean; confidence_filter?: boolean; distance_from_row?: boolean; + spatial_confidence_filter?: boolean; }; /** @@ -118,6 +119,43 @@ export type FilterDistanceConfig = { filter_during_polyline_move?: boolean; }; +export type ConfidenceThreshold = { + confidence: number; // Percentage threshold (0-100) +}; + +/** + * Stores the current confidence filter threshold values. + * The key is the class id. "all" is a special key that stores the threshold applied to + * all annotations in single-class mode. + */ +export type ConfidenceFilterClasses = { + all: ConfidenceThreshold; + [key: string]: ConfidenceThreshold; +}; + +/** + * Config object for the ConfidenceFilterItem ToolboxItem. + */ +export type ConfidenceFilterConfig = { + name?: string; + filter_min?: number; + filter_max?: number; + default_values?: ConfidenceFilterClasses; + step_value?: number; + multi_class_mode?: boolean; + disable_multi_class_mode?: boolean; + filter_on_load?: boolean; + show_options?: boolean; + // The spatial types to filter. Defaults to all confidence-filterable spatial types. + target_spatial_types?: ULabelSpatialType[]; + // The class ids to create sliders for in multi-class mode. Defaults to all class ids. + target_class_ids?: number[]; + keybinds?: { + increment: string; + decrement: string; + }; +}; + export type ULabelSubmitButton = { name: string; hook: (submit_data: ULabelSubmitData) => void; @@ -367,6 +405,7 @@ export class ULabel { ): void; public get_keypoint_slider_value(): number | null; public get_distance_filter_value(): DistanceFromPolylineClasses | null; + public get_confidence_filter_value(): ConfidenceFilterClasses | null; public fly_to_next_annotation(increment: number, max_zoom?: number): boolean; public fly_to_annotation_id(annotation_id: string, subtask_key?: string | null, max_zoom?: number): boolean; public fly_to_annotation(annotation: ULabelAnnotation, subtask_key?: string, max_zoom?: number): boolean; diff --git a/src/annotation_operators.ts b/src/annotation_operators.ts index 49e4cb93..b69ba1fd 100644 --- a/src/annotation_operators.ts +++ b/src/annotation_operators.ts @@ -30,6 +30,22 @@ export function get_annotation_confidence(annotation: ULabelAnnotation) { return current_confidence; } +/** + * Returns the confidence that the passed in ULabelAnnotation is a specific class id. + * + * @param annotation ULabelAnnotation + * @param class_id The class id to get the confidence for + * @returns The confidence for the given class id, or -1 if the class id is not present + */ +export function get_annotation_confidence_for_class(annotation: ULabelAnnotation, class_id: number): number { + for (const payload of annotation.classification_payloads!) { + if (payload.class_id === class_id) { + return payload.confidence; + } + } + return -1; +} + /** * Returns the class id of a ULabelAnnotation as a string. * @@ -417,6 +433,49 @@ export function get_point_and_line_annotations(ulabel: ULabel): [ULabelAnnotatio return [point_annotations, line_annotations]; } +/** + * The spatial annotation types that can be filtered by confidence. + * Excludes the non-spatial modes (`whole-image` and `global`). + */ +export const CONFIDENCE_FILTERABLE_SPATIAL_TYPES: ULabelSpatialType[] = [ + "contour", "polygon", "polyline", "bbox", "tbar", "bbox3", "point", +]; + +/** + * Gathers all spatial annotations across every subtask that have a confidence payload and whose + * spatial type is included in `spatial_types`. Each returned annotation has its `subtask_key` set. + * + * @param ulabel ULabel object + * @param spatial_types The spatial types to include. Defaults to all confidence-filterable spatial types. + * @returns A list of spatial annotations that can be filtered by confidence + */ +export function get_spatial_annotations_with_confidence( + ulabel: ULabel, + spatial_types: ULabelSpatialType[] = CONFIDENCE_FILTERABLE_SPATIAL_TYPES, +): ULabelAnnotation[] { + const annotations: ULabelAnnotation[] = []; + + // Loop through each subtask + for (const [subtask_key, subtask] of Object.entries(ulabel.subtasks) as [string, ULabelSubtask][]) { + // Then go through each annotation in the subtask + for (const annotation_key in subtask.annotations.access) { + const annotation: ULabelAnnotation = subtask.annotations.access[annotation_key]; + + // Skip annotations without a spatial type we're targeting + if (annotation.spatial_type == null || !spatial_types.includes(annotation.spatial_type)) continue; + + // Skip annotations that lack a classification payload with confidence + if (annotation.classification_payloads == null || annotation.classification_payloads.length === 0) continue; + + // Note the annotation's subtask and add it to the set + annotation.subtask_key = subtask_key; + annotations.push(annotation); + } + } + + return annotations; +} + /** * Using the value of the FilterPointDistanceFromRow's slider, filter all point annotations based on their distance * from a polyline annotation. @@ -567,27 +626,47 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis } /** - * Goes through all subtasks and finds all classes that polylines can be. Then returns a list of them. + * Goes through all subtasks and finds all class definitions that annotations can be. Optionally + * restricts to subtasks that allow at least one of the provided `allowed_modes`. Class definitions + * are de-duplicated by id, and the reserved delete class is skipped. * - * @returns A list of all classes which can be polylines + * @param ulabel ULabel object + * @param allowed_modes If provided, only include subtasks that allow at least one of these modes + * @returns A de-duplicated list of class definitions */ -export function findAllPolylineClassDefinitions(ulabel: ULabel) { - // Initialize potential class definitions - const potential_class_defs: ClassDefinition[] = []; +export function findAllClassDefinitions(ulabel: ULabel, allowed_modes: ULabelSpatialType[] | null = null): ClassDefinition[] { + // Initialize potential class definitions and a set to track seen ids + const class_defs: ClassDefinition[] = []; + const seen_ids = new Set(); - // Check each subtask to see if polyline is one of its allowed modes for (const subtask_key in ulabel.subtasks) { - // Grab the subtask const subtask = ulabel.subtasks[subtask_key]; - if (subtask.allowed_modes.includes("polyline")) { - // Loop through all the classes in the subtask - subtask.class_defs.forEach((current_class_def) => { - // Skip the reserved delete class - if (current_class_def.id === DELETE_CLASS_ID) return; - potential_class_defs.push(current_class_def); - }); + // If allowed_modes is provided, skip subtasks that don't allow any of them + if (allowed_modes !== null && !allowed_modes.some((mode) => subtask.allowed_modes.includes(mode))) { + continue; } + + // Loop through all the classes in the subtask + subtask.class_defs.forEach((current_class_def) => { + // Skip the reserved delete class + if (current_class_def.id === DELETE_CLASS_ID) return; + // Skip classes we've already added (de-duplicate by id) + if (seen_ids.has(current_class_def.id)) return; + + seen_ids.add(current_class_def.id); + class_defs.push(current_class_def); + }); } - return potential_class_defs; + + return class_defs; +} + +/** + * Goes through all subtasks and finds all classes that polylines can be. Then returns a list of them. + * + * @returns A list of all classes which can be polylines + */ +export function findAllPolylineClassDefinitions(ulabel: ULabel) { + return findAllClassDefinitions(ulabel, ["polyline"]); } diff --git a/src/configuration.ts b/src/configuration.ts index 47473c04..09eefc9f 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,5 +1,6 @@ import type { FilterDistanceConfig, + ConfidenceFilterConfig, ImageFiltersConfig, InitialCrop, ImageData, @@ -15,6 +16,7 @@ import { AnnotationResizeItem, RecolorActiveItem, KeypointSliderItem, + ConfidenceFilterItem, FilterPointDistanceFromRow, BrushToolboxItem, ToolboxItem, @@ -40,6 +42,7 @@ export enum AllowedToolboxItem { ImageFilters, // 10 AnnotationList, // 11 Keybinds, // 12 + ConfidenceFilter, // 13 } /* eslint-enable @stylistic/no-multi-spaces */ @@ -61,6 +64,18 @@ export const DEFAULT_FILTER_DISTANCE_CONFIG: FilterDistanceConfig = { filter_during_polyline_move: true, }; +export const DEFAULT_CONFIDENCE_FILTER_CONFIG: ConfidenceFilterConfig = { + name: "Filter Annotations By Confidence", + filter_min: 0, + filter_max: 100, + default_values: { all: { confidence: 0 } }, + step_value: 1, + multi_class_mode: false, + disable_multi_class_mode: false, + filter_on_load: true, + show_options: true, +}; + export const DEFAULT_IMAGE_FILTERS_CONFIG: ImageFiltersConfig = { default_values: { brightness: 100, @@ -165,6 +180,7 @@ export class Configuration { [AllowedToolboxItem.ImageFilters, ImageFiltersToolboxItem], [AllowedToolboxItem.AnnotationList, AnnotationListToolboxItem], [AllowedToolboxItem.Keybinds, KeybindsToolboxItem], + [AllowedToolboxItem.ConfidenceFilter, ConfidenceFilterItem], ]); // Default toolbox order used when the user doesn't specify one @@ -191,6 +207,9 @@ export class Configuration { // Config for FilterDistanceToolboxItem public distance_filter_toolbox_item: FilterDistanceConfig = DEFAULT_FILTER_DISTANCE_CONFIG; + // Config for ConfidenceFilterToolboxItem + public confidence_filter_toolbox_item: ConfidenceFilterConfig = DEFAULT_CONFIDENCE_FILTER_CONFIG; + // Config for ImageFiltersToolboxItem public image_filters_toolbox_item: ImageFiltersConfig = DEFAULT_IMAGE_FILTERS_CONFIG; diff --git a/src/index.js b/src/index.js index 3ed5acd5..e66fcece 100644 --- a/src/index.js +++ b/src/index.js @@ -1046,6 +1046,20 @@ export class ULabel { return item.get_current_values(); } + /** + * Get the current confidence filter slider values. + * + * @returns {object|null} An object mapping class identifiers (and "all") to their confidence + * threshold values, or null if the ConfidenceFilter toolbox item is not active or no sliders + * are found + */ + get_confidence_filter_value() { + if (!this.config.toolbox_order.includes(AllowedToolboxItem.ConfidenceFilter)) return null; + const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "ConfidenceFilter"); + if (item === undefined) return null; + return item.get_current_values(); + } + // Show annotation mode show_annotation_mode(el = null) { if (el === null) { diff --git a/src/toolbox.ts b/src/toolbox.ts index 06c453e1..2eaed52b 100644 --- a/src/toolbox.ts +++ b/src/toolbox.ts @@ -1,21 +1,29 @@ import type { DistanceFromPolylineClasses, FilterDistanceConfig, + ConfidenceFilterConfig, + ConfidenceFilterClasses, + ULabelSpatialType, RecolorActiveConfig, ValidDeprecatedBy, } from "../index"; // Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; -import { DEFAULT_FILTER_DISTANCE_CONFIG } from "./configuration"; +import { DEFAULT_FILTER_DISTANCE_CONFIG, DEFAULT_CONFIDENCE_FILTER_CONFIG } from "./configuration"; import { ULabelAnnotation } from "./annotation"; import { ULabelSubtask } from "./subtask"; import { get_annotation_confidence, + get_annotation_confidence_for_class, + get_annotation_class_id, + get_spatial_annotations_with_confidence, value_is_lower_than_filter, mark_deprecated, filter_points_distance_from_line, findAllPolylineClassDefinitions, + findAllClassDefinitions, get_point_and_line_annotations, + CONFIDENCE_FILTERABLE_SPATIAL_TYPES, } from "./annotation_operators"; import { SliderHandler, get_idd_string } from "./html_builder"; import { FilterDistanceOverlay } from "./overlays"; @@ -1787,11 +1795,31 @@ export class RecolorActiveItem extends ToolboxItem { } } -export class KeypointSliderItem extends ToolboxItem { - public html!: string; - public inner_HTML: string; - public name: string; - public slider_bar_id: string; +/** + * ToolboxItem for filtering (deprecating/showing) spatial annotations by their confidence. + * + * Supports two modes: + * - Single-class mode: one slider applies a confidence threshold to all targeted spatial + * annotations across every subtask, using each annotation's highest confidence. + * - Multi-class mode: one slider per targeted class id. Each slider filters only the annotations + * whose assigned (highest-confidence) class matches, using that class's confidence. + */ +export class ConfidenceFilterItem extends ToolboxItem { + public ulabel: ULabel; + public config!: ConfidenceFilterConfig; + public name!: string; + public filter_min!: number; + public filter_max!: number; + public step_value!: number; + public default_values!: ConfidenceFilterClasses; + public multi_class_mode!: boolean; + public disable_multi_class_mode!: boolean; + public show_options!: boolean; + public filter_on_load!: boolean; + public target_spatial_types!: ULabelSpatialType[]; + public target_class_ids!: number[] | null; + public collapse_options: boolean = false; + public filter_function: (value: number, filter: number) => boolean; public get_confidence: (annotation: ULabelAnnotation) => number; public mark_deprecated: ( @@ -1800,8 +1828,360 @@ export class KeypointSliderItem extends ToolboxItem { deprecated_by_key?: ValidDeprecatedBy, ) => void; + // The deprecated_by key this item uses. Overridden by deprecated subclasses. + protected deprecated_by_key: ValidDeprecatedBy = "spatial_confidence_filter"; + + // DOM naming used to build and read this item's sliders + protected component_prefix: string = "confidence-filter"; + protected slider_class: string = "confidence-filter-slider"; + + // TODO (joshua-dean): See if we can narrow this any + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + constructor(ulabel: ULabel, kwargs: { [name: string]: any } | null = null) { + super(); + this.ulabel = ulabel; + + // Default the deprecating utilities. Subclasses may override in their own constructor. + this.filter_function = value_is_lower_than_filter; + this.get_confidence = get_annotation_confidence; + this.mark_deprecated = mark_deprecated; + + // Deprecated subclasses (e.g. KeypointSliderItem) perform their own setup. + if (this.get_toolbox_item_type() !== "ConfidenceFilter") return; + + // Get this component's config from ulabel's config, defaulting any missing keys + this.config = this.ulabel.config.confidence_filter_toolbox_item ?? {}; + for (const key in DEFAULT_CONFIDENCE_FILTER_CONFIG) { + if (!Object.prototype.hasOwnProperty.call(this.config, key)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.config as any)[key] = (DEFAULT_CONFIDENCE_FILTER_CONFIG as any)[key]; + } + } + + // Assign config properties to this instance + this.name = this.config.name!; + this.filter_min = this.config.filter_min!; + this.filter_max = this.config.filter_max!; + this.step_value = this.config.step_value!; + this.default_values = this.config.default_values!; + this.multi_class_mode = this.config.multi_class_mode!; + this.disable_multi_class_mode = this.config.disable_multi_class_mode!; + this.show_options = this.config.show_options!; + this.filter_on_load = this.config.filter_on_load!; + this.target_spatial_types = this.config.target_spatial_types ?? CONFIDENCE_FILTERABLE_SPATIAL_TYPES; + this.target_class_ids = this.config.target_class_ids ?? null; + + // Force disable multi-class mode if the config doesn't allow it + if (this.disable_multi_class_mode) this.multi_class_mode = false; + + this.add_styles(); + this.add_event_listeners(); + + // Filter on load if configured + if (this.filter_on_load) { + this.filter_annotations(false); + } + } + + /** + * Create the css for this ToolboxItem and append it to the page. + */ + protected add_styles() { + const css = ` + #toolbox div.confidence-filter { + text-align: left; + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options { + display: inline-block; + position: relative; + left: 1rem; + margin-bottom: 0.5rem; + font-size: 80%; + user-select: none; + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options legend { + border-radius: 0.1rem; + padding: 0.1rem 0.3rem; + cursor: pointer; + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options legend:hover { + background-color: rgba(128, 128, 128, 0.3); + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options.ulabel-collapsed { + border: none; + margin-bottom: 0; + padding: 0; + padding-left: calc(0.75em + 2px); + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options.ulabel-collapsed :not(legend) { + display: none; + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options input[type="checkbox"] { + margin: 0; + } + + #toolbox div.confidence-filter fieldset.confidence-filter-options label { + position: relative; + top: -0.2rem; + font-size: smaller; + }`; + + const style_id = "confidence-filter-toolbox-item-styles"; + + // Don't add the style tag if its already been added once + if (document.getElementById(style_id)) return; + + const head = document.head || document.querySelector("head"); + const style = document.createElement("style"); + style.appendChild(document.createTextNode(css)); + style.id = style_id; + head.appendChild(style); + } + + private add_event_listeners(): void { + // Toggle the options fieldset when its legend is clicked + $(document).on("click.ulabel", "fieldset.confidence-filter-options > legend", () => this.toggleCollapsedOptions()); + + // Switch between single-class and multi-class filtering when the checkbox is clicked + $(document).on("click.ulabel", `#${this.component_prefix}-multi-checkbox`, (event) => { + this.multi_class_mode = event.currentTarget.checked; + $(`#${this.component_prefix}-single-class-mode`).toggleClass("ulabel-hidden"); + $(`#${this.component_prefix}-multi-class-mode`).toggleClass("ulabel-hidden"); + this.filter_annotations(true); + }); + } + + private toggleCollapsedOptions(): void { + $("fieldset.confidence-filter-options").toggleClass("ulabel-collapsed"); + this.collapse_options = !this.collapse_options; + } + + /** + * Get the class definitions that should receive a slider in multi-class mode. + */ + private get_target_class_defs() { + let class_defs = findAllClassDefinitions(this.ulabel, this.target_spatial_types); + if (this.target_class_ids !== null) { + class_defs = class_defs.filter((class_def) => this.target_class_ids!.includes(class_def.id)); + } + return class_defs; + } + + /** + * Read the current threshold values, preferring the DOM sliders and falling back to defaults. + * + * @returns A map of class identifiers (and "all") to their threshold values + */ + public get_filter_values(): ConfidenceFilterClasses { + // Seed with defaults so filtering works before the sliders are rendered (e.g. on load) + const values: ConfidenceFilterClasses = { all: { confidence: this.default_values.all.confidence } }; + for (const key in this.default_values) { + if (key === "all") continue; + values[key] = { confidence: this.default_values[key].confidence }; + } + + // Read the single-class slider if present + const all_slider = document.querySelector(`#${this.component_prefix}-all`); + if (all_slider !== null) { + values.all = { confidence: all_slider.valueAsNumber }; + } + + // Read the per-class sliders if present + const sliders = document.querySelectorAll(`.${this.slider_class}`); + for (let idx = 0; idx < sliders.length; idx++) { + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; + if (slider_class_name === "all") continue; + values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + } + + return values; + } + + /** + * Deprecate or show every targeted spatial annotation across all subtasks based on the + * current threshold values and mode. + * + * @param redraw whether or not to redraw the annotations after filtering + */ + public filter_annotations(redraw: boolean = false): void { + const values = this.get_filter_values(); + const annotations = get_spatial_annotations_with_confidence(this.ulabel, this.target_spatial_types); + + // Store which annotations need to be redrawn, organized by subtask key + const annotations_ids_to_redraw_by_subtask: { [key: string]: string[] } = {}; + for (const subtask_key in this.ulabel.subtasks) { + annotations_ids_to_redraw_by_subtask[subtask_key] = []; + } + + for (const annotation of annotations) { + let should_deprecate = false; + + if (this.multi_class_mode) { + // In multi-class mode, only filter annotations whose assigned class has a slider + const class_id = Number(get_annotation_class_id(annotation)); + if (values[class_id] !== undefined) { + const confidence = Math.round(get_annotation_confidence_for_class(annotation, class_id) * 100); + should_deprecate = this.filter_function(confidence, values[class_id].confidence); + } + } else { + // In single-class mode, filter all targeted annotations by their highest confidence + const confidence = Math.round(this.get_confidence(annotation) * 100); + should_deprecate = this.filter_function(confidence, values.all.confidence); + } + + // Mark deprecated and, if the visible state changed, queue a redraw + const was_deprecated = annotation.deprecated; + this.mark_deprecated(annotation, should_deprecate, this.deprecated_by_key); + if (annotation.deprecated !== was_deprecated) { + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); + } + } + + if (redraw) { + for (const subtask_key in annotations_ids_to_redraw_by_subtask) { + this.ulabel.redraw_multiple_spatial_annotations(annotations_ids_to_redraw_by_subtask[subtask_key], subtask_key); + } + this.ulabel.toolbox.redraw_update_items(this.ulabel); + } + } + + /** + * Builds one slider per targeted class for multi-class mode. + */ + private createMultiFilterHTML(): string { + const class_defs = this.get_target_class_defs(); + + let multi_class_html = ``; + for (const class_def of class_defs) { + const default_value = ( + this.default_values[class_def.id] !== undefined ? + this.default_values[class_def.id].confidence : + this.default_values.all.confidence + ).toString(); + + const slider = new SliderHandler({ + id: `${this.component_prefix}-${class_def.id}`, + class: `${this.slider_class} ${this.slider_class}-class`, + min: this.filter_min.toString(), + max: this.filter_max.toString(), + default_value: default_value, + step: this.step_value.toString(), + label_units: "%", + main_label: class_def.name, + slider_event: () => this.filter_annotations(true), + }); + + multi_class_html += slider.getSliderHTML(); + } + + return multi_class_html; + } + + public get_html(): string { + const multi_class_html = this.createMultiFilterHTML(); + + const single_class_slider = new SliderHandler({ + id: `${this.component_prefix}-all`, // "all" is extracted using regex + class: this.slider_class, + default_value: this.default_values.all.confidence.toString(), + min: this.filter_min.toString(), + max: this.filter_max.toString(), + step: this.step_value.toString(), + label_units: "%", + slider_event: () => this.filter_annotations(true), + }); + + let options_html = ``; + if (!this.disable_multi_class_mode) { + options_html = ` +
+ Options ˅ +
+ + +
+
`; + } + + return ` +
+

${this.name}

+ ${options_html} +
+ ${single_class_slider.getSliderHTML()} +
+
+ ${multi_class_html} +
+
+ `; + } + + /** + * Get the current confidence filter slider values by reading the DOM slider elements. + * + * @returns A map of class identifiers to their threshold values, or null if no sliders are found + */ + public get_current_values(): ConfidenceFilterClasses | null { + const all_slider = document.querySelector(`#${this.component_prefix}-all`); + if (all_slider === null) return null; + + const values: ConfidenceFilterClasses = { all: { confidence: all_slider.valueAsNumber } }; + + // In multi-class mode, also read the per-class sliders + if (this.multi_class_mode) { + const sliders = document.querySelectorAll(`.${this.slider_class}`); + for (let idx = 0; idx < sliders.length; idx++) { + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; + if (slider_class_name === "all") continue; + values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + } + } + + return values; + } + + public after_init() { + // This toolbox item doesn't need to do anything after initialization + } + + public get_toolbox_item_type() { + return "ConfidenceFilter"; + } +} + +/** + * ToolboxItem for filtering point ("keypoint") annotations by their confidence. + * + * @deprecated Use {@link ConfidenceFilterItem} (`AllowedToolboxItem.ConfidenceFilter`) instead, + * which supports all spatial annotation types, per-class targeting, and multiple sliders. + * This item is retained for backwards compatibility and only filters point annotations. + */ +export class KeypointSliderItem extends ConfidenceFilterItem { + public html!: string; + public inner_HTML: string; + public slider_bar_id: string; + filter_value: number = 0; - ulabel: ULabel; keybinds: { increment: string; decrement: string; @@ -1810,7 +2190,7 @@ export class KeypointSliderItem extends ToolboxItem { // TODO (joshua-dean): See if we can narrow this any // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(ulabel: ULabel, kwargs: { [name: string]: any }) { - super(); + super(ulabel); this.inner_HTML = `

Keypoint Slider

`; this.ulabel = ulabel; @@ -1831,7 +2211,6 @@ export class KeypointSliderItem extends ToolboxItem { increment: "2", decrement: "1", }; - kwargs = {}; } // Create slider bar id @@ -1849,7 +2228,7 @@ export class KeypointSliderItem extends ToolboxItem { // Check the config to see if we should update the annotations with the default filter on load if (this.ulabel.config.filter_annotations_on_load) { - this.filter_annotations(this.ulabel); + this.filter_keypoint_annotations(this.ulabel); } this.add_styles(); @@ -1882,15 +2261,14 @@ export class KeypointSliderItem extends ToolboxItem { } /** - * Given the ulabel object and a filter value, go through each annotation and decide whether or - * not to deprecate it. + * Given the ulabel object and a filter value, go through each point annotation and decide + * whether or not to deprecate it. * * @param ulabel ULabel object * @param filter_value The number between 0-100 which annotation's confidence is compared against * @param redraw whether or not to redraw the annotations after filtering - * @returns Annotations that were modified, organized by subtask key */ - private filter_annotations(ulabel: ULabel, filter_value: number | null = null, redraw: boolean = false): void { + private filter_keypoint_annotations(ulabel: ULabel, filter_value: number | null = null, redraw: boolean = false): void { if (filter_value === null) { // Use stored filter value if none is passed in filter_value = Math.round(this.filter_value * 100); @@ -1943,7 +2321,7 @@ export class KeypointSliderItem extends ToolboxItem { label_units: "%", slider_event: (slider_value: number | string) => { // Filter the annotations, then redraw them - this.filter_annotations(this.ulabel, Number(slider_value), true); + this.filter_keypoint_annotations(this.ulabel, Number(slider_value), true); }, }); diff --git a/tests/annotation_operators.test.js b/tests/annotation_operators.test.js new file mode 100644 index 00000000..eaa05551 --- /dev/null +++ b/tests/annotation_operators.test.js @@ -0,0 +1,136 @@ +// Unit tests for confidence-filter-related annotation operators +const { + get_annotation_confidence_for_class, + get_spatial_annotations_with_confidence, + findAllClassDefinitions, + findAllPolylineClassDefinitions, +} = require("../build/annotation_operators"); + +function make_annotation(spatial_type, classification_payloads, extra = {}) { + return { + spatial_type, + classification_payloads, + deprecated: false, + ...extra, + }; +} + +function make_ulabel(subtasks) { + return { subtasks }; +} + +describe("get_annotation_confidence_for_class", () => { + test("returns the confidence for the matching class id", () => { + const annotation = make_annotation("bbox", [ + { class_id: 1, confidence: 0.9 }, + { class_id: 2, confidence: 0.1 }, + ]); + expect(get_annotation_confidence_for_class(annotation, 1)).toBe(0.9); + expect(get_annotation_confidence_for_class(annotation, 2)).toBe(0.1); + }); + + test("returns -1 when the class id is not present", () => { + const annotation = make_annotation("bbox", [{ class_id: 1, confidence: 0.9 }]); + expect(get_annotation_confidence_for_class(annotation, 99)).toBe(-1); + }); +}); + +describe("get_spatial_annotations_with_confidence", () => { + test("collects spatial annotations with confidence across all subtasks and sets subtask_key", () => { + const bbox = make_annotation("bbox", [{ class_id: 1, confidence: 0.8 }], { id: "a" }); + const polygon = make_annotation("polygon", [{ class_id: 2, confidence: 0.5 }], { id: "b" }); + const ulabel = make_ulabel({ + st1: { annotations: { access: { a: bbox } } }, + st2: { annotations: { access: { b: polygon } } }, + }); + + const result = get_spatial_annotations_with_confidence(ulabel); + + expect(result).toHaveLength(2); + expect(bbox.subtask_key).toBe("st1"); + expect(polygon.subtask_key).toBe("st2"); + }); + + test("excludes non-spatial modes (whole-image and global)", () => { + const whole_image = make_annotation("whole-image", [{ class_id: 1, confidence: 1 }], { id: "a" }); + const global = make_annotation("global", [{ class_id: 1, confidence: 1 }], { id: "b" }); + const point = make_annotation("point", [{ class_id: 1, confidence: 1 }], { id: "c" }); + const ulabel = make_ulabel({ + st1: { annotations: { access: { a: whole_image, b: global, c: point } } }, + }); + + const result = get_spatial_annotations_with_confidence(ulabel); + + expect(result).toHaveLength(1); + expect(result[0].spatial_type).toBe("point"); + }); + + test("excludes annotations without a classification payload", () => { + const with_payload = make_annotation("bbox", [{ class_id: 1, confidence: 1 }], { id: "a" }); + const without_payload = make_annotation("bbox", [], { id: "b" }); + const null_payload = make_annotation("bbox", null, { id: "c" }); + const ulabel = make_ulabel({ + st1: { annotations: { access: { a: with_payload, b: without_payload, c: null_payload } } }, + }); + + const result = get_spatial_annotations_with_confidence(ulabel); + + expect(result).toHaveLength(1); + expect(result[0].id).toBe("a"); + }); + + test("respects the spatial_types filter argument", () => { + const bbox = make_annotation("bbox", [{ class_id: 1, confidence: 1 }], { id: "a" }); + const polygon = make_annotation("polygon", [{ class_id: 2, confidence: 1 }], { id: "b" }); + const ulabel = make_ulabel({ + st1: { annotations: { access: { a: bbox, b: polygon } } }, + }); + + const result = get_spatial_annotations_with_confidence(ulabel, ["polygon"]); + + expect(result).toHaveLength(1); + expect(result[0].spatial_type).toBe("polygon"); + }); +}); + +describe("findAllClassDefinitions", () => { + const ulabel = make_ulabel({ + st1: { + allowed_modes: ["bbox", "polygon"], + class_defs: [ + { name: "Car", id: 1, color: "red", keybind: "1" }, + { name: "Truck", id: 2, color: "blue", keybind: "2" }, + { name: "Delete", id: -1, color: "gray", keybind: null }, + ], + }, + st2: { + allowed_modes: ["polyline"], + class_defs: [ + { name: "Car", id: 1, color: "red", keybind: "1" }, // duplicate id + { name: "Row", id: 3, color: "green", keybind: "3" }, + ], + }, + }); + + test("returns de-duplicated class defs across subtasks and skips the delete class", () => { + const result = findAllClassDefinitions(ulabel); + const ids = result.map((cd) => cd.id); + + expect(ids).toEqual([1, 2, 3]); + expect(ids).not.toContain(-1); + }); + + test("restricts to subtasks that allow one of the provided modes", () => { + const result = findAllClassDefinitions(ulabel, ["polyline"]); + const ids = result.map((cd) => cd.id); + + expect(ids).toEqual([1, 3]); + }); + + test("findAllPolylineClassDefinitions only includes polyline subtasks", () => { + const result = findAllPolylineClassDefinitions(ulabel); + const ids = result.map((cd) => cd.id); + + expect(ids).toEqual([1, 3]); + }); +}); diff --git a/tests/e2e/slider-api.spec.js b/tests/e2e/slider-api.spec.js index 66a5b361..5e7a86f3 100644 --- a/tests/e2e/slider-api.spec.js +++ b/tests/e2e/slider-api.spec.js @@ -77,4 +77,42 @@ test.describe("Slider Public API", () => { expect(typeof value.closest_row.distance).toBe("number"); }); }); + + test.describe("get_confidence_filter_value", () => { + test("should return default confidence filter value after init", async ({ page }) => { + await wait_for_ulabel_init(page); + + const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + + // Should have the "all" key with the default threshold of 0 + expect(value).not.toBeNull(); + expect(value.all).toBeDefined(); + expect(value.all.confidence).toBe(0); + }); + + test("should reflect value after moving the slider", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Set the single-class confidence slider to 60 + await page.evaluate(() => { + const slider = document.querySelector("#confidence-filter-all"); + slider.value = "60"; + slider.dispatchEvent(new Event("input", { bubbles: true })); + }); + + const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + + expect(value.all.confidence).toBe(60); + }); + + test("should return an object with an all key", async ({ page }) => { + await wait_for_ulabel_init(page); + + const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + + expect(typeof value).toBe("object"); + expect(value).toHaveProperty("all"); + expect(typeof value.all.confidence).toBe("number"); + }); + }); }); diff --git a/tests/types/index.test-d.ts b/tests/types/index.test-d.ts index 048e8127..3478ad27 100644 --- a/tests/types/index.test-d.ts +++ b/tests/types/index.test-d.ts @@ -86,4 +86,5 @@ const all_items: AllowedToolboxItem[] = [ AllowedToolboxItem.ImageFilters, AllowedToolboxItem.AnnotationList, AllowedToolboxItem.Keybinds, + AllowedToolboxItem.ConfidenceFilter, ]; From 2b96473b947fb4478625b86edf5c46229bb83b05 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 12:41:03 -0500 Subject: [PATCH 02/11] Rename to ConfidenceSlider and add configurable step size and modes --- .github/tasks.md | 18 +- api_spec.md | 34 +- demo/multi-class.html | 4 +- demo/resume-from.html | 24 +- demo/row-filtering-example.html | 10 +- index.d.ts | 27 +- src/configuration.ts | 20 +- src/index.js | 10 +- src/toolbox.ts | 589 +------------------------ src/toolbox_items/confidence_slider.ts | 585 ++++++++++++++++++++++++ tests/annotation_operators.test.js | 2 +- tests/e2e/slider-api.spec.js | 12 +- tests/types/index.test-d.ts | 2 +- 13 files changed, 698 insertions(+), 639 deletions(-) create mode 100644 src/toolbox_items/confidence_slider.ts diff --git a/.github/tasks.md b/.github/tasks.md index 1532ec91..2cb4c3aa 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -2,12 +2,12 @@ ### Generalize Confidence Slider -- [x] Add helpers to `src/annotation_operators.ts` (`get_spatial_annotations_with_confidence`, `get_annotation_confidence_for_class`, `findAllClassDefinitions`) -- [x] Add `ConfidenceFilterItem` to `src/toolbox.ts` and refactor `KeypointSliderItem` into a deprecated subclass -- [x] Add `AllowedToolboxItem.ConfidenceFilter`, `toolbox_map` entry, `DEFAULT_CONFIDENCE_FILTER_CONFIG`, and `confidence_filter_toolbox_item` field to `src/configuration.ts` -- [x] Add `ConfidenceFilterConfig` / `ConfidenceFilterClasses` types and `spatial_confidence_filter` deprecated key to `index.d.ts` -- [x] Add `get_confidence_filter_value()` public API to `src/index.js` -- [x] Update `api_spec.md` documentation -- [x] Add/extend tests -- [x] Run `npm run lint` and `npm run build` - +- [x] Discuss with the user: Naming options for the new toolbox item. +- [x] Replace all instances of `KeypointSlider` in our demos with the new toolbox item. +- [x] Add confidences to the annotations in the resume-from demo and add a confidence slider to the toolbox. +- [x] Refactor new toolbox item into its own file under src/toolbox_items +- [x] Add support for configurable step size and min/max values for the confidence slider +- [x] Add config options such that users can either enforce, disable, or leave toggleable the multi-class mode for the confidence slider +- [x] Should we raise a warning if a KeypointSlider is used alongside a ConfidenceFilter? +- [ ] Discuss with the user: How could we implement an option to only filter specific classes rather than either all at once or all classes individually? Basically, I want a multi-class option where the user can enable/disable specific classes in the config +- [ ] I notice that if no toolbox order is provided, the Keypoint Slider is included. Can we replace it with a Confidence Slider? diff --git a/api_spec.md b/api_spec.md index 2eb0d42b..dbecefce 100644 --- a/api_spec.md +++ b/api_spec.md @@ -346,7 +346,7 @@ enum AllowedToolboxItem { ImageFilters, // 10 AnnotationList, // 11 Keybinds, // 12 - ConfidenceFilter, // 13 + ConfidenceSlider, // 13 } ``` You can access the AllowedToolboxItem enum by calling the static method: @@ -435,16 +435,21 @@ The `AnnotationList` toolbox item displays all annotations in the current subtas This toolbox item requires no configuration and can be added to the `toolbox_order` array using `AllowedToolboxItem.AnnotationList`. -### `confidence_filter_toolbox_item` +### `confidence_slider_toolbox_item` -The `ConfidenceFilter` toolbox item (added to `toolbox_order` via `AllowedToolboxItem.ConfidenceFilter`) deprecates (hides) or shows spatial annotations based on their confidence values. Unlike the deprecated `KeypointSlider`, it works with **all** spatial annotation types that have a confidence payload (`bbox`, `bbox3`, `polygon`, `polyline`, `contour`, `tbar`, and `point`), across every subtask. +The `ConfidenceSlider` toolbox item (added to `toolbox_order` via `AllowedToolboxItem.ConfidenceSlider`) deprecates (hides) or shows spatial annotations based on their confidence values. Unlike the deprecated `KeypointSlider`, it works with **all** spatial annotation types that have a confidence payload (`bbox`, `bbox3`, `polygon`, `polyline`, `contour`, `tbar`, and `point`), across every subtask. It supports two modes: - **Single-class mode** (default): a single slider applies one confidence threshold to every targeted spatial annotation across all subtasks, using each annotation's highest confidence value. -- **Multi-class mode**: one slider is shown per targeted class id. Each slider only filters annotations whose assigned (highest-confidence) class matches that slider, using that class's confidence value. Enable multi-class mode via the "Multi-Class Filtering" checkbox in the item's options. +- **Multi-class mode**: one slider is shown per targeted class id. Each slider only filters annotations whose assigned (highest-confidence) class matches that slider, using that class's confidence value. -Any annotation with a confidence at or above the threshold is shown; any below is deprecated. Thresholds are expressed as percentages (0–100). +The `multi_class_mode` config controls whether these modes are user-toggleable: +- `"toggle"` (default): a "Multi-Class Filtering" checkbox lets the user switch between the two modes. +- `"single-only"`: only the single global slider is shown. +- `"multi-only"`: only the per-class sliders are shown. + +Any annotation with a confidence at or above the threshold is shown; any below is deprecated. Thresholds are expressed as percentages, and the slider's range and increment are configurable via `filter_min`, `filter_max`, and `step_value`. Configuration object with the following custom definitions: ```javascript @@ -452,19 +457,18 @@ type ConfidenceThreshold = { confidence: number // Percentage threshold (0-100) } -type ConfidenceFilterClasses = { +type ConfidenceSliderClasses = { "all": ConfidenceThreshold, // value used by the single-class slider [key: number]?: ConfidenceThreshold // per-class-id values used in multi-class mode } -type ConfidenceFilterConfig = { - "name"?: string, // Default: "Filter Annotations By Confidence" +type ConfidenceSliderConfig = { + "name"?: string, // Default: "Confidence Filter" "filter_min"?: number, // Default: 0 (%) "filter_max"?: number, // Default: 100 (%) - "default_values"?: ConfidenceFilterClasses, // Default: {"all": {"confidence": 0}} + "default_values"?: ConfidenceSliderClasses, // Default: {"all": {"confidence": 0}} "step_value"?: number, // Default: 1 (%) - "multi_class_mode"?: boolean, // Default: false - "disable_multi_class_mode"?: boolean, // Default: false + "multi_class_mode"?: "toggle" | "single-only" | "multi-only", // Default: "toggle" "filter_on_load"?: boolean, // Default: true "show_options"?: boolean, // Default: true // The spatial types to filter. Defaults to all confidence-filterable spatial types. @@ -492,7 +496,7 @@ Keybind to delete a vertex of a polygon or polyline annotation. The vertex must ### `keypoint_slider_default_value` Default value for the keypoint slider. Must be a number between 0 and 1. Default is `0`. -> **Deprecated:** The `KeypointSlider` toolbox item only filters `point` annotations. Use the `ConfidenceFilter` toolbox item (`confidence_filter_toolbox_item`) instead, which filters all spatial annotation types and supports per-class targeting and multiple sliders. +> **Deprecated:** The `KeypointSlider` toolbox item only filters `point` annotations. Use the `ConfidenceSlider` toolbox item (`confidence_slider_toolbox_item`) instead, which filters all spatial annotation types and supports per-class targeting and multiple sliders. ### `filter_annotations_on_load` If true, the annotations will be filtered on load based on the `keypoint_slider_default_value`. Default is `true`. @@ -605,15 +609,15 @@ Sets the zoom to focus on the provided annotation, and switches to its subtask i *() => number | null* -- Returns the current keypoint slider value as a number between 0 and 1. Returns `null` if the KeypointSlider toolbox item is not active or the slider element is not found. -> **Deprecated:** Prefer `get_confidence_filter_value()` with the `ConfidenceFilter` toolbox item. +> **Deprecated:** Prefer `get_confidence_slider_value()` with the `ConfidenceSlider` toolbox item. ### `get_distance_filter_value()` *() => object | null* -- Returns an object mapping class identifiers to their distance filter values (in pixels). The object always includes a `closest_row` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the FilterDistance toolbox item is not active or no sliders are found. -### `get_confidence_filter_value()` +### `get_confidence_slider_value()` -*() => object | null* -- Returns an object mapping class identifiers to their confidence threshold values (as percentages, 0–100). The object always includes an `all` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the ConfidenceFilter toolbox item is not active or no sliders are found. +*() => object | null* -- Returns an object mapping class identifiers to their confidence threshold values (as percentages, 0–100). The object always includes an `all` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the ConfidenceSlider toolbox item is not active or no sliders are found. ## Generic Callbacks diff --git a/demo/multi-class.html b/demo/multi-class.html index 5887b96e..db1e2cb7 100644 --- a/demo/multi-class.html +++ b/demo/multi-class.html @@ -127,8 +127,8 @@ AllowedToolboxItem.AnnotationID, AllowedToolboxItem.ClassCounter, AllowedToolboxItem.AnnotationResize, - AllowedToolboxItem.KeypointSlider, - AllowedToolboxItem.ConfidenceFilter, + AllowedToolboxItem.KeypointSlider, + AllowedToolboxItem.ConfidenceSlider, AllowedToolboxItem.FilterDistance, AllowedToolboxItem.RecolorActive, ], diff --git a/demo/resume-from.html b/demo/resume-from.html index bef0f67a..ba506d29 100644 --- a/demo/resume-from.html +++ b/demo/resume-from.html @@ -36,7 +36,7 @@ "spatial_payload": [[300, 300]], "classification_payloads": [ {"class_id": 12, "confidence": 0.0}, - {"class_id": 11, "confidence": 1.0}, + {"class_id": 11, "confidence": 0.9}, {"class_id": 10, "confidence": 0.0}, ], }, @@ -45,7 +45,7 @@ "spatial_type": "point", "spatial_payload": [[400, 300]], "classification_payloads": [ - {"class_id": 10, "confidence": 1.0}, + {"class_id": 10, "confidence": 0.25}, ], }, { @@ -53,7 +53,7 @@ "spatial_type": "point", "spatial_payload": [[300, 400]], "classification_payloads": [ - {"class_id": 12, "confidence": 1.0}, + {"class_id": 12, "confidence": 0.55}, ], }, { @@ -102,7 +102,7 @@ ] ], "classification_payloads": [ - {"class_id": 11, "confidence": 1.0}, + {"class_id": 11, "confidence": 0.75}, {"class_id": -1, "confidence": 0.0} ], }, @@ -175,7 +175,7 @@ "classification_payloads": [ { "class_id": 10, - "confidence": 1 + "confidence": 0.15 }, { "class_id": 11, @@ -209,7 +209,7 @@ "classification_payloads": [ { "class_id": 10, - "confidence": 1 + "confidence": 0.6 }, { "class_id": 11, @@ -275,6 +275,7 @@ }; // Initial ULabel configuration + const AllowedToolboxItem = ULabel.get_allowed_toolbox_item_enum(); let ulabel = new ULabel({ "container_id": "container", "image_data": "https://ulabel.s3.us-east-2.amazonaws.com/cs-demo-0.png", @@ -285,7 +286,16 @@ "allow_annotations_outside_image": false, "fly_to_next_annotation_keybind": "w", "fly_to_previous_annotation_keybind": "`", - "fly_to_max_zoom": 6 + "fly_to_max_zoom": 6, + "toolbox_order": [ + AllowedToolboxItem.ModeSelect, + AllowedToolboxItem.ZoomPan, + AllowedToolboxItem.AnnotationID, + AllowedToolboxItem.ClassCounter, + AllowedToolboxItem.AnnotationResize, + AllowedToolboxItem.ConfidenceSlider, + AllowedToolboxItem.SubmitButtons, + ], }); // Wait for ULabel instance to finish initialization ulabel.init(function() { diff --git a/demo/row-filtering-example.html b/demo/row-filtering-example.html index b4e37e03..b944a414 100644 --- a/demo/row-filtering-example.html +++ b/demo/row-filtering-example.html @@ -745,7 +745,7 @@ AllowedToolboxItem.ZoomPan, AllowedToolboxItem.AnnotationID, AllowedToolboxItem.ClassCounter, - AllowedToolboxItem.KeypointSlider, + AllowedToolboxItem.ConfidenceSlider, AllowedToolboxItem.FilterDistance, AllowedToolboxItem.AnnotationResize, AllowedToolboxItem.SubmitButtons, @@ -761,7 +761,13 @@ // "disable_multi_class_mode": true, // "filter_during_polyline_move": false, }, - "keypoint_slider_default_value": 0.2, + "confidence_slider_toolbox_item": { + "default_values": { + "all": { + "confidence": 20, // % + } + }, + }, }); // Wait for ULabel instance to finish initialization ulabel.init(function () { diff --git a/index.d.ts b/index.d.ts index e6a0fa19..78f796fe 100644 --- a/index.d.ts +++ b/index.d.ts @@ -36,13 +36,13 @@ export type Offset = { /** * Valid keys for the DeprecatedBy type */ -export type ValidDeprecatedBy = "human" | "confidence_filter" | "distance_from_row" | "spatial_confidence_filter"; +export type ValidDeprecatedBy = "human" | "confidence_filter" | "distance_from_row" | "confidence_slider"; export type DeprecatedBy = { human?: boolean; confidence_filter?: boolean; distance_from_row?: boolean; - spatial_confidence_filter?: boolean; + confidence_slider?: boolean; }; /** @@ -124,26 +124,33 @@ export type ConfidenceThreshold = { }; /** - * Stores the current confidence filter threshold values. + * Stores the current confidence slider threshold values. * The key is the class id. "all" is a special key that stores the threshold applied to * all annotations in single-class mode. */ -export type ConfidenceFilterClasses = { +export type ConfidenceSliderClasses = { all: ConfidenceThreshold; [key: string]: ConfidenceThreshold; }; /** - * Config object for the ConfidenceFilterItem ToolboxItem. + * Controls the multi-class mode of the ConfidenceSlider ToolboxItem: + * - "toggle": a checkbox lets the user switch between single- and multi-class filtering + * - "single-only": only a single global slider is shown + * - "multi-only": only per-class sliders are shown */ -export type ConfidenceFilterConfig = { +export type ConfidenceSliderMultiClassMode = "toggle" | "single-only" | "multi-only"; + +/** + * Config object for the ConfidenceSlider ToolboxItem. + */ +export type ConfidenceSliderConfig = { name?: string; filter_min?: number; filter_max?: number; - default_values?: ConfidenceFilterClasses; + default_values?: ConfidenceSliderClasses; step_value?: number; - multi_class_mode?: boolean; - disable_multi_class_mode?: boolean; + multi_class_mode?: ConfidenceSliderMultiClassMode; filter_on_load?: boolean; show_options?: boolean; // The spatial types to filter. Defaults to all confidence-filterable spatial types. @@ -405,7 +412,7 @@ export class ULabel { ): void; public get_keypoint_slider_value(): number | null; public get_distance_filter_value(): DistanceFromPolylineClasses | null; - public get_confidence_filter_value(): ConfidenceFilterClasses | null; + public get_confidence_slider_value(): ConfidenceSliderClasses | null; public fly_to_next_annotation(increment: number, max_zoom?: number): boolean; public fly_to_annotation_id(annotation_id: string, subtask_key?: string | null, max_zoom?: number): boolean; public fly_to_annotation(annotation: ULabelAnnotation, subtask_key?: string, max_zoom?: number): boolean; diff --git a/src/configuration.ts b/src/configuration.ts index 09eefc9f..88d8e571 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,6 +1,6 @@ import type { FilterDistanceConfig, - ConfidenceFilterConfig, + ConfidenceSliderConfig, ImageFiltersConfig, InitialCrop, ImageData, @@ -15,12 +15,11 @@ import { ClassCounterToolboxItem, AnnotationResizeItem, RecolorActiveItem, - KeypointSliderItem, - ConfidenceFilterItem, FilterPointDistanceFromRow, BrushToolboxItem, ToolboxItem, } from "./toolbox"; +import { ConfidenceSlider, KeypointSliderItem } from "./toolbox_items/confidence_slider"; import { SubmitButtons } from "./toolbox_items/submit_buttons"; import { ImageFiltersToolboxItem } from "./toolbox_items/image_filters"; import { AnnotationListToolboxItem } from "./toolbox_items/annotation_list"; @@ -42,7 +41,7 @@ export enum AllowedToolboxItem { ImageFilters, // 10 AnnotationList, // 11 Keybinds, // 12 - ConfidenceFilter, // 13 + ConfidenceSlider, // 13 } /* eslint-enable @stylistic/no-multi-spaces */ @@ -64,14 +63,13 @@ export const DEFAULT_FILTER_DISTANCE_CONFIG: FilterDistanceConfig = { filter_during_polyline_move: true, }; -export const DEFAULT_CONFIDENCE_FILTER_CONFIG: ConfidenceFilterConfig = { - name: "Filter Annotations By Confidence", +export const DEFAULT_CONFIDENCE_SLIDER_CONFIG: ConfidenceSliderConfig = { + name: "Confidence Filter", filter_min: 0, filter_max: 100, default_values: { all: { confidence: 0 } }, step_value: 1, - multi_class_mode: false, - disable_multi_class_mode: false, + multi_class_mode: "toggle", filter_on_load: true, show_options: true, }; @@ -180,7 +178,7 @@ export class Configuration { [AllowedToolboxItem.ImageFilters, ImageFiltersToolboxItem], [AllowedToolboxItem.AnnotationList, AnnotationListToolboxItem], [AllowedToolboxItem.Keybinds, KeybindsToolboxItem], - [AllowedToolboxItem.ConfidenceFilter, ConfidenceFilterItem], + [AllowedToolboxItem.ConfidenceSlider, ConfidenceSlider], ]); // Default toolbox order used when the user doesn't specify one @@ -207,8 +205,8 @@ export class Configuration { // Config for FilterDistanceToolboxItem public distance_filter_toolbox_item: FilterDistanceConfig = DEFAULT_FILTER_DISTANCE_CONFIG; - // Config for ConfidenceFilterToolboxItem - public confidence_filter_toolbox_item: ConfidenceFilterConfig = DEFAULT_CONFIDENCE_FILTER_CONFIG; + // Config for ConfidenceSlider + public confidence_slider_toolbox_item: ConfidenceSliderConfig = DEFAULT_CONFIDENCE_SLIDER_CONFIG; // Config for ImageFiltersToolboxItem public image_filters_toolbox_item: ImageFiltersConfig = DEFAULT_IMAGE_FILTERS_CONFIG; diff --git a/src/index.js b/src/index.js index e66fcece..f3123b28 100644 --- a/src/index.js +++ b/src/index.js @@ -1047,15 +1047,15 @@ export class ULabel { } /** - * Get the current confidence filter slider values. + * Get the current confidence slider values. * * @returns {object|null} An object mapping class identifiers (and "all") to their confidence - * threshold values, or null if the ConfidenceFilter toolbox item is not active or no sliders + * threshold values, or null if the ConfidenceSlider toolbox item is not active or no sliders * are found */ - get_confidence_filter_value() { - if (!this.config.toolbox_order.includes(AllowedToolboxItem.ConfidenceFilter)) return null; - const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "ConfidenceFilter"); + get_confidence_slider_value() { + if (!this.config.toolbox_order.includes(AllowedToolboxItem.ConfidenceSlider)) return null; + const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "ConfidenceSlider"); if (item === undefined) return null; return item.get_current_values(); } diff --git a/src/toolbox.ts b/src/toolbox.ts index 2eaed52b..942b575f 100644 --- a/src/toolbox.ts +++ b/src/toolbox.ts @@ -1,29 +1,17 @@ import type { DistanceFromPolylineClasses, FilterDistanceConfig, - ConfidenceFilterConfig, - ConfidenceFilterClasses, - ULabelSpatialType, RecolorActiveConfig, - ValidDeprecatedBy, } from "../index"; // Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; -import { DEFAULT_FILTER_DISTANCE_CONFIG, DEFAULT_CONFIDENCE_FILTER_CONFIG } from "./configuration"; +import { DEFAULT_FILTER_DISTANCE_CONFIG, AllowedToolboxItem } from "./configuration"; import { ULabelAnnotation } from "./annotation"; import { ULabelSubtask } from "./subtask"; import { - get_annotation_confidence, - get_annotation_confidence_for_class, - get_annotation_class_id, - get_spatial_annotations_with_confidence, - value_is_lower_than_filter, - mark_deprecated, filter_points_distance_from_line, findAllPolylineClassDefinitions, - findAllClassDefinitions, get_point_and_line_annotations, - CONFIDENCE_FILTERABLE_SPATIAL_TYPES, } from "./annotation_operators"; import { SliderHandler, get_idd_string } from "./html_builder"; import { FilterDistanceOverlay } from "./overlays"; @@ -84,6 +72,22 @@ export class Toolbox { throw new Error("No Toolbox Items Given"); } + // Warn if both the deprecated KeypointSlider and the ConfidenceSlider are enabled, since + // they overlap in functionality and use different deprecation keys. + const toolbox_keys = toolbox_item_order.map((item) => + typeof item === "number" ? item : (item as [number, object])[0], + ); + if ( + toolbox_keys.includes(AllowedToolboxItem.KeypointSlider) && + toolbox_keys.includes(AllowedToolboxItem.ConfidenceSlider) + ) { + log_message( + "Both the deprecated KeypointSlider and the ConfidenceSlider toolbox items are enabled. " + + "The KeypointSlider is deprecated; consider using only the ConfidenceSlider.", + LogLevel.WARNING, + ); + } + this.add_styles(); const toolbox_instance_list = []; @@ -1795,563 +1799,8 @@ export class RecolorActiveItem extends ToolboxItem { } } -/** - * ToolboxItem for filtering (deprecating/showing) spatial annotations by their confidence. - * - * Supports two modes: - * - Single-class mode: one slider applies a confidence threshold to all targeted spatial - * annotations across every subtask, using each annotation's highest confidence. - * - Multi-class mode: one slider per targeted class id. Each slider filters only the annotations - * whose assigned (highest-confidence) class matches, using that class's confidence. - */ -export class ConfidenceFilterItem extends ToolboxItem { - public ulabel: ULabel; - public config!: ConfidenceFilterConfig; - public name!: string; - public filter_min!: number; - public filter_max!: number; - public step_value!: number; - public default_values!: ConfidenceFilterClasses; - public multi_class_mode!: boolean; - public disable_multi_class_mode!: boolean; - public show_options!: boolean; - public filter_on_load!: boolean; - public target_spatial_types!: ULabelSpatialType[]; - public target_class_ids!: number[] | null; - public collapse_options: boolean = false; - - public filter_function: (value: number, filter: number) => boolean; - public get_confidence: (annotation: ULabelAnnotation) => number; - public mark_deprecated: ( - annotation: ULabelAnnotation, - deprecated: boolean, - deprecated_by_key?: ValidDeprecatedBy, - ) => void; - - // The deprecated_by key this item uses. Overridden by deprecated subclasses. - protected deprecated_by_key: ValidDeprecatedBy = "spatial_confidence_filter"; - - // DOM naming used to build and read this item's sliders - protected component_prefix: string = "confidence-filter"; - protected slider_class: string = "confidence-filter-slider"; - - // TODO (joshua-dean): See if we can narrow this any - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - constructor(ulabel: ULabel, kwargs: { [name: string]: any } | null = null) { - super(); - this.ulabel = ulabel; - - // Default the deprecating utilities. Subclasses may override in their own constructor. - this.filter_function = value_is_lower_than_filter; - this.get_confidence = get_annotation_confidence; - this.mark_deprecated = mark_deprecated; - - // Deprecated subclasses (e.g. KeypointSliderItem) perform their own setup. - if (this.get_toolbox_item_type() !== "ConfidenceFilter") return; - - // Get this component's config from ulabel's config, defaulting any missing keys - this.config = this.ulabel.config.confidence_filter_toolbox_item ?? {}; - for (const key in DEFAULT_CONFIDENCE_FILTER_CONFIG) { - if (!Object.prototype.hasOwnProperty.call(this.config, key)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.config as any)[key] = (DEFAULT_CONFIDENCE_FILTER_CONFIG as any)[key]; - } - } - - // Assign config properties to this instance - this.name = this.config.name!; - this.filter_min = this.config.filter_min!; - this.filter_max = this.config.filter_max!; - this.step_value = this.config.step_value!; - this.default_values = this.config.default_values!; - this.multi_class_mode = this.config.multi_class_mode!; - this.disable_multi_class_mode = this.config.disable_multi_class_mode!; - this.show_options = this.config.show_options!; - this.filter_on_load = this.config.filter_on_load!; - this.target_spatial_types = this.config.target_spatial_types ?? CONFIDENCE_FILTERABLE_SPATIAL_TYPES; - this.target_class_ids = this.config.target_class_ids ?? null; - - // Force disable multi-class mode if the config doesn't allow it - if (this.disable_multi_class_mode) this.multi_class_mode = false; - - this.add_styles(); - this.add_event_listeners(); - - // Filter on load if configured - if (this.filter_on_load) { - this.filter_annotations(false); - } - } - - /** - * Create the css for this ToolboxItem and append it to the page. - */ - protected add_styles() { - const css = ` - #toolbox div.confidence-filter { - text-align: left; - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options { - display: inline-block; - position: relative; - left: 1rem; - margin-bottom: 0.5rem; - font-size: 80%; - user-select: none; - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options legend { - border-radius: 0.1rem; - padding: 0.1rem 0.3rem; - cursor: pointer; - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options legend:hover { - background-color: rgba(128, 128, 128, 0.3); - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options.ulabel-collapsed { - border: none; - margin-bottom: 0; - padding: 0; - padding-left: calc(0.75em + 2px); - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options.ulabel-collapsed :not(legend) { - display: none; - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options input[type="checkbox"] { - margin: 0; - } - - #toolbox div.confidence-filter fieldset.confidence-filter-options label { - position: relative; - top: -0.2rem; - font-size: smaller; - }`; - - const style_id = "confidence-filter-toolbox-item-styles"; - - // Don't add the style tag if its already been added once - if (document.getElementById(style_id)) return; - - const head = document.head || document.querySelector("head"); - const style = document.createElement("style"); - style.appendChild(document.createTextNode(css)); - style.id = style_id; - head.appendChild(style); - } - - private add_event_listeners(): void { - // Toggle the options fieldset when its legend is clicked - $(document).on("click.ulabel", "fieldset.confidence-filter-options > legend", () => this.toggleCollapsedOptions()); - - // Switch between single-class and multi-class filtering when the checkbox is clicked - $(document).on("click.ulabel", `#${this.component_prefix}-multi-checkbox`, (event) => { - this.multi_class_mode = event.currentTarget.checked; - $(`#${this.component_prefix}-single-class-mode`).toggleClass("ulabel-hidden"); - $(`#${this.component_prefix}-multi-class-mode`).toggleClass("ulabel-hidden"); - this.filter_annotations(true); - }); - } - - private toggleCollapsedOptions(): void { - $("fieldset.confidence-filter-options").toggleClass("ulabel-collapsed"); - this.collapse_options = !this.collapse_options; - } - - /** - * Get the class definitions that should receive a slider in multi-class mode. - */ - private get_target_class_defs() { - let class_defs = findAllClassDefinitions(this.ulabel, this.target_spatial_types); - if (this.target_class_ids !== null) { - class_defs = class_defs.filter((class_def) => this.target_class_ids!.includes(class_def.id)); - } - return class_defs; - } - - /** - * Read the current threshold values, preferring the DOM sliders and falling back to defaults. - * - * @returns A map of class identifiers (and "all") to their threshold values - */ - public get_filter_values(): ConfidenceFilterClasses { - // Seed with defaults so filtering works before the sliders are rendered (e.g. on load) - const values: ConfidenceFilterClasses = { all: { confidence: this.default_values.all.confidence } }; - for (const key in this.default_values) { - if (key === "all") continue; - values[key] = { confidence: this.default_values[key].confidence }; - } - - // Read the single-class slider if present - const all_slider = document.querySelector(`#${this.component_prefix}-all`); - if (all_slider !== null) { - values.all = { confidence: all_slider.valueAsNumber }; - } - - // Read the per-class sliders if present - const sliders = document.querySelectorAll(`.${this.slider_class}`); - for (let idx = 0; idx < sliders.length; idx++) { - const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; - if (slider_class_name === "all") continue; - values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; - } - - return values; - } - - /** - * Deprecate or show every targeted spatial annotation across all subtasks based on the - * current threshold values and mode. - * - * @param redraw whether or not to redraw the annotations after filtering - */ - public filter_annotations(redraw: boolean = false): void { - const values = this.get_filter_values(); - const annotations = get_spatial_annotations_with_confidence(this.ulabel, this.target_spatial_types); - - // Store which annotations need to be redrawn, organized by subtask key - const annotations_ids_to_redraw_by_subtask: { [key: string]: string[] } = {}; - for (const subtask_key in this.ulabel.subtasks) { - annotations_ids_to_redraw_by_subtask[subtask_key] = []; - } - - for (const annotation of annotations) { - let should_deprecate = false; - - if (this.multi_class_mode) { - // In multi-class mode, only filter annotations whose assigned class has a slider - const class_id = Number(get_annotation_class_id(annotation)); - if (values[class_id] !== undefined) { - const confidence = Math.round(get_annotation_confidence_for_class(annotation, class_id) * 100); - should_deprecate = this.filter_function(confidence, values[class_id].confidence); - } - } else { - // In single-class mode, filter all targeted annotations by their highest confidence - const confidence = Math.round(this.get_confidence(annotation) * 100); - should_deprecate = this.filter_function(confidence, values.all.confidence); - } - - // Mark deprecated and, if the visible state changed, queue a redraw - const was_deprecated = annotation.deprecated; - this.mark_deprecated(annotation, should_deprecate, this.deprecated_by_key); - if (annotation.deprecated !== was_deprecated) { - annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); - } - } - - if (redraw) { - for (const subtask_key in annotations_ids_to_redraw_by_subtask) { - this.ulabel.redraw_multiple_spatial_annotations(annotations_ids_to_redraw_by_subtask[subtask_key], subtask_key); - } - this.ulabel.toolbox.redraw_update_items(this.ulabel); - } - } - - /** - * Builds one slider per targeted class for multi-class mode. - */ - private createMultiFilterHTML(): string { - const class_defs = this.get_target_class_defs(); - - let multi_class_html = ``; - for (const class_def of class_defs) { - const default_value = ( - this.default_values[class_def.id] !== undefined ? - this.default_values[class_def.id].confidence : - this.default_values.all.confidence - ).toString(); - - const slider = new SliderHandler({ - id: `${this.component_prefix}-${class_def.id}`, - class: `${this.slider_class} ${this.slider_class}-class`, - min: this.filter_min.toString(), - max: this.filter_max.toString(), - default_value: default_value, - step: this.step_value.toString(), - label_units: "%", - main_label: class_def.name, - slider_event: () => this.filter_annotations(true), - }); - - multi_class_html += slider.getSliderHTML(); - } - - return multi_class_html; - } - - public get_html(): string { - const multi_class_html = this.createMultiFilterHTML(); - - const single_class_slider = new SliderHandler({ - id: `${this.component_prefix}-all`, // "all" is extracted using regex - class: this.slider_class, - default_value: this.default_values.all.confidence.toString(), - min: this.filter_min.toString(), - max: this.filter_max.toString(), - step: this.step_value.toString(), - label_units: "%", - slider_event: () => this.filter_annotations(true), - }); - - let options_html = ``; - if (!this.disable_multi_class_mode) { - options_html = ` -
- Options ˅ -
- - -
-
`; - } - - return ` -
-

${this.name}

- ${options_html} -
- ${single_class_slider.getSliderHTML()} -
-
- ${multi_class_html} -
-
- `; - } - - /** - * Get the current confidence filter slider values by reading the DOM slider elements. - * - * @returns A map of class identifiers to their threshold values, or null if no sliders are found - */ - public get_current_values(): ConfidenceFilterClasses | null { - const all_slider = document.querySelector(`#${this.component_prefix}-all`); - if (all_slider === null) return null; - - const values: ConfidenceFilterClasses = { all: { confidence: all_slider.valueAsNumber } }; - - // In multi-class mode, also read the per-class sliders - if (this.multi_class_mode) { - const sliders = document.querySelectorAll(`.${this.slider_class}`); - for (let idx = 0; idx < sliders.length; idx++) { - const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; - if (slider_class_name === "all") continue; - values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; - } - } - - return values; - } - - public after_init() { - // This toolbox item doesn't need to do anything after initialization - } - - public get_toolbox_item_type() { - return "ConfidenceFilter"; - } -} - -/** - * ToolboxItem for filtering point ("keypoint") annotations by their confidence. - * - * @deprecated Use {@link ConfidenceFilterItem} (`AllowedToolboxItem.ConfidenceFilter`) instead, - * which supports all spatial annotation types, per-class targeting, and multiple sliders. - * This item is retained for backwards compatibility and only filters point annotations. - */ -export class KeypointSliderItem extends ConfidenceFilterItem { - public html!: string; - public inner_HTML: string; - public slider_bar_id: string; - - filter_value: number = 0; - keybinds: { - increment: string; - decrement: string; - }; - - // TODO (joshua-dean): See if we can narrow this any - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(ulabel: ULabel, kwargs: { [name: string]: any }) { - super(ulabel); - this.inner_HTML = `

Keypoint Slider

`; - this.ulabel = ulabel; - - // Use properties in kwargs if kwargs is present - if (kwargs !== undefined) { - this.name = kwargs.name; - this.filter_function = kwargs.filter_function; - this.get_confidence = kwargs.confidence_function; - this.mark_deprecated = kwargs.mark_deprecated; - this.keybinds = kwargs.keybinds; - } else { - // Otherwise use defaults - this.name = "Keypoint Slider"; - this.filter_function = value_is_lower_than_filter; - this.get_confidence = get_annotation_confidence; - this.mark_deprecated = mark_deprecated; - this.keybinds = { - increment: "2", - decrement: "1", - }; - } - - // Create slider bar id - this.slider_bar_id = this.name.replaceLowerConcat(" ", "-"); - - // If the config has a default value override the filter_value - const has_filter_override = Object.prototype.hasOwnProperty.call( - this.ulabel.config, - this.name.replaceLowerConcat(" ", "_", "_default_value"), - ); - if (has_filter_override) { - // Set the filter value - this.filter_value = this.ulabel.config[this.name.replaceLowerConcat(" ", "_", "_default_value")]; - } - - // Check the config to see if we should update the annotations with the default filter on load - if (this.ulabel.config.filter_annotations_on_load) { - this.filter_keypoint_annotations(this.ulabel); - } - - this.add_styles(); - } - - /** - * Create the css for this ToolboxItem and append it to the page. - */ - protected add_styles() { - // Define the css - const css = ` - /* Component has no css?? */ - `; - // Create an id so this specific style tag can be referenced - const style_id = "keypoint-slider-toolbox-item-styles"; - - // Don't add the style tag if its already been added once - if (document.getElementById(style_id)) return; - - // Grab the document's head and create a style tag - const head = document.head || document.querySelector("head"); - const style = document.createElement("style"); - - // Add the css and id to the style tag - style.appendChild(document.createTextNode(css)); - style.id = style_id; - - // Add the style tag to the document's head - head.appendChild(style); - } - - /** - * Given the ulabel object and a filter value, go through each point annotation and decide - * whether or not to deprecate it. - * - * @param ulabel ULabel object - * @param filter_value The number between 0-100 which annotation's confidence is compared against - * @param redraw whether or not to redraw the annotations after filtering - */ - private filter_keypoint_annotations(ulabel: ULabel, filter_value: number | null = null, redraw: boolean = false): void { - if (filter_value === null) { - // Use stored filter value if none is passed in - filter_value = Math.round(this.filter_value * 100); - } - // Store which annotations need to be redrawn - const annotations_ids_to_redraw_by_subtask: { [key: string]: string[] } = {}; - // Initialize the object with the subtask keys - for (const subtask_key in ulabel.subtasks) { - annotations_ids_to_redraw_by_subtask[subtask_key] = []; - } - - // Get all point annotations - const point_and_line_annotations = get_point_and_line_annotations(ulabel); - for (const annotation of point_and_line_annotations[0]) { - // Get the annotation's confidence as decimal between 0-1 - let confidence: number = this.get_confidence(annotation); - - // filter_value will be a number between 0-100, so convert the confidence to a percentage as well - confidence = Math.round(confidence * 100); - - // Compare the confidence value against the filter value - const should_deprecate: boolean = this.filter_function(confidence, filter_value); - // Check if an annotation should be deprecated or undeprecated, else do nothing - if ( - (should_deprecate && !annotation.deprecated) || - (!should_deprecate && annotation.deprecated) - ) { - // Mark this annotation as either deprecated or undeprecated by the confidence filter - this.mark_deprecated(annotation, should_deprecate, "confidence_filter"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); - } - } - - if (redraw) { - // Redraw each subtask's annotations - for (const subtask_key in annotations_ids_to_redraw_by_subtask) { - ulabel.redraw_multiple_spatial_annotations(annotations_ids_to_redraw_by_subtask[subtask_key], subtask_key); - } - // Update class counter - ulabel.toolbox.redraw_update_items(ulabel); - } - } - - public get_html() { - // Create a SliderHandler instance to handle slider interactions - const slider_handler = new SliderHandler({ - id: this.name.replaceLowerConcat(" ", "-"), - class: "keypoint-slider", - default_value: Math.round(this.filter_value * 100).toString(), - label_units: "%", - slider_event: (slider_value: number | string) => { - // Filter the annotations, then redraw them - this.filter_keypoint_annotations(this.ulabel, Number(slider_value), true); - }, - }); - - return ` -
-

${this.name}

- ` + slider_handler.getSliderHTML() + ` -
- `; - } - - /** - * Get the current keypoint slider value by reading the DOM slider element. - * - * @returns The current slider value as a number between 0 and 1, or null if the slider is not found - */ - public get_current_value(): number | null { - const slider = document.querySelector(`#${this.slider_bar_id}`); - if (slider === null) return null; - return slider.valueAsNumber / 100; - } - - public after_init() { - // This toolbox item doesn't need to do anything after initialization - } - - public get_toolbox_item_type() { - return "KeypointSlider"; - } -} +/* ConfidenceSlider and the deprecated KeypointSliderItem now live in + * ./toolbox_items/confidence_slider.ts */ export class FilterPointDistanceFromRow extends ToolboxItem { name!: string; // Component name shown to users diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts new file mode 100644 index 00000000..f879a0a3 --- /dev/null +++ b/src/toolbox_items/confidence_slider.ts @@ -0,0 +1,585 @@ +import type { + ULabel, + ConfidenceSliderConfig, + ConfidenceSliderClasses, + ULabelSpatialType, + ValidDeprecatedBy, +} from "../../index"; +import { ToolboxItem } from "../toolbox"; +import { ULabelAnnotation } from "../annotation"; +import { DEFAULT_CONFIDENCE_SLIDER_CONFIG } from "../configuration"; +import { SliderHandler } from "../html_builder"; +import { + get_annotation_confidence, + get_annotation_confidence_for_class, + get_annotation_class_id, + get_spatial_annotations_with_confidence, + value_is_lower_than_filter, + mark_deprecated, + findAllClassDefinitions, + get_point_and_line_annotations, + CONFIDENCE_FILTERABLE_SPATIAL_TYPES, +} from "../annotation_operators"; + +/** + * ToolboxItem for filtering (deprecating/showing) spatial annotations by their confidence. + * + * Supports two modes: + * - Single-class mode: one slider applies a confidence threshold to all targeted spatial + * annotations across every subtask, using each annotation's highest confidence. + * - Multi-class mode: one slider per targeted class id. Each slider filters only the annotations + * whose assigned (highest-confidence) class matches, using that class's confidence. + * + * The `multi_class_mode` config controls whether the mode is user-toggleable (`"toggle"`), or + * locked to `"single-only"` or `"multi-only"`. + */ +export class ConfidenceSlider extends ToolboxItem { + public ulabel: ULabel; + public config!: ConfidenceSliderConfig; + public name!: string; + public filter_min!: number; + public filter_max!: number; + public step_value!: number; + public default_values!: ConfidenceSliderClasses; + public show_options!: boolean; + public filter_on_load!: boolean; + public target_spatial_types!: ULabelSpatialType[]; + public target_class_ids!: number[] | null; + public collapse_options: boolean = false; + + // Whether the multi-class toggle is shown, and the current runtime mode + public show_multi_toggle!: boolean; + public is_multi_class!: boolean; + + public filter_function: (value: number, filter: number) => boolean; + public get_confidence: (annotation: ULabelAnnotation) => number; + public mark_deprecated: ( + annotation: ULabelAnnotation, + deprecated: boolean, + deprecated_by_key?: ValidDeprecatedBy, + ) => void; + + // The deprecated_by key this item uses. Overridden by deprecated subclasses. + protected deprecated_by_key: ValidDeprecatedBy = "confidence_slider"; + + // DOM naming used to build and read this item's sliders + protected component_prefix: string = "confidence-slider"; + protected slider_class: string = "confidence-slider-input"; + + // TODO (joshua-dean): See if we can narrow this any + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + constructor(ulabel: ULabel, kwargs: { [name: string]: any } | null = null) { + super(); + this.ulabel = ulabel; + + // Default the deprecating utilities. Subclasses may override in their own constructor. + this.filter_function = value_is_lower_than_filter; + this.get_confidence = get_annotation_confidence; + this.mark_deprecated = mark_deprecated; + + // Deprecated subclasses (e.g. KeypointSliderItem) perform their own setup. + if (this.get_toolbox_item_type() !== "ConfidenceSlider") return; + + // Get this component's config from ulabel's config, defaulting any missing keys + this.config = this.ulabel.config.confidence_slider_toolbox_item ?? {}; + for (const key in DEFAULT_CONFIDENCE_SLIDER_CONFIG) { + if (!Object.prototype.hasOwnProperty.call(this.config, key)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.config as any)[key] = (DEFAULT_CONFIDENCE_SLIDER_CONFIG as any)[key]; + } + } + + // Assign config properties to this instance + this.name = this.config.name!; + this.filter_min = this.config.filter_min!; + this.filter_max = this.config.filter_max!; + this.step_value = this.config.step_value!; + this.default_values = this.config.default_values!; + this.show_options = this.config.show_options!; + this.filter_on_load = this.config.filter_on_load!; + this.target_spatial_types = this.config.target_spatial_types ?? CONFIDENCE_FILTERABLE_SPATIAL_TYPES; + this.target_class_ids = this.config.target_class_ids ?? null; + + // Resolve the multi-class mode option into runtime state + const multi_class_mode = this.config.multi_class_mode!; + this.show_multi_toggle = multi_class_mode === "toggle"; + this.is_multi_class = multi_class_mode === "multi-only"; + + this.add_styles(); + this.add_event_listeners(); + + // Filter on load if configured + if (this.filter_on_load) { + this.filter_annotations(false); + } + } + + /** + * Create the css for this ToolboxItem and append it to the page. + */ + protected add_styles() { + const css = ` + #toolbox div.confidence-slider { + text-align: left; + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options { + display: inline-block; + position: relative; + left: 1rem; + margin-bottom: 0.5rem; + font-size: 80%; + user-select: none; + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options legend { + border-radius: 0.1rem; + padding: 0.1rem 0.3rem; + cursor: pointer; + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options legend:hover { + background-color: rgba(128, 128, 128, 0.3); + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options.ulabel-collapsed { + border: none; + margin-bottom: 0; + padding: 0; + padding-left: calc(0.75em + 2px); + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options.ulabel-collapsed :not(legend) { + display: none; + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options input[type="checkbox"] { + margin: 0; + } + + #toolbox div.confidence-slider fieldset.confidence-slider-options label { + position: relative; + top: -0.2rem; + font-size: smaller; + }`; + + const style_id = "confidence-slider-toolbox-item-styles"; + + // Don't add the style tag if its already been added once + if (document.getElementById(style_id)) return; + + const head = document.head || document.querySelector("head"); + const style = document.createElement("style"); + style.appendChild(document.createTextNode(css)); + style.id = style_id; + head.appendChild(style); + } + + private add_event_listeners(): void { + // Toggle the options fieldset when its legend is clicked + $(document).on("click.ulabel", "fieldset.confidence-slider-options > legend", () => this.toggleCollapsedOptions()); + + // Switch between single-class and multi-class filtering when the checkbox is clicked + $(document).on("click.ulabel", `#${this.component_prefix}-multi-checkbox`, (event) => { + this.is_multi_class = event.currentTarget.checked; + $(`#${this.component_prefix}-single-class-mode`).toggleClass("ulabel-hidden"); + $(`#${this.component_prefix}-multi-class-mode`).toggleClass("ulabel-hidden"); + this.filter_annotations(true); + }); + } + + private toggleCollapsedOptions(): void { + $("fieldset.confidence-slider-options").toggleClass("ulabel-collapsed"); + this.collapse_options = !this.collapse_options; + } + + /** + * Get the class definitions that should receive a slider in multi-class mode. + */ + private get_target_class_defs() { + let class_defs = findAllClassDefinitions(this.ulabel, this.target_spatial_types); + if (this.target_class_ids !== null) { + class_defs = class_defs.filter((class_def) => this.target_class_ids!.includes(class_def.id)); + } + return class_defs; + } + + /** + * Read the current threshold values, preferring the DOM sliders and falling back to defaults. + * + * @returns A map of class identifiers (and "all") to their threshold values + */ + public get_filter_values(): ConfidenceSliderClasses { + // Seed with defaults so filtering works before the sliders are rendered (e.g. on load) + const values: ConfidenceSliderClasses = { all: { confidence: this.default_values.all.confidence } }; + for (const key in this.default_values) { + if (key === "all") continue; + values[key] = { confidence: this.default_values[key].confidence }; + } + + // Read the single-class slider if present + const all_slider = document.querySelector(`#${this.component_prefix}-all`); + if (all_slider !== null) { + values.all = { confidence: all_slider.valueAsNumber }; + } + + // Read the per-class sliders if present + const sliders = document.querySelectorAll(`.${this.slider_class}`); + for (let idx = 0; idx < sliders.length; idx++) { + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; + if (slider_class_name === "all") continue; + values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + } + + return values; + } + + /** + * Deprecate or show every targeted spatial annotation across all subtasks based on the + * current threshold values and mode. + * + * @param redraw whether or not to redraw the annotations after filtering + */ + public filter_annotations(redraw: boolean = false): void { + const values = this.get_filter_values(); + const annotations = get_spatial_annotations_with_confidence(this.ulabel, this.target_spatial_types); + + // Store which annotations need to be redrawn, organized by subtask key + const annotations_ids_to_redraw_by_subtask: { [key: string]: string[] } = {}; + for (const subtask_key in this.ulabel.subtasks) { + annotations_ids_to_redraw_by_subtask[subtask_key] = []; + } + + for (const annotation of annotations) { + let should_deprecate = false; + + if (this.is_multi_class) { + // In multi-class mode, only filter annotations whose assigned class has a slider + const class_id = Number(get_annotation_class_id(annotation)); + if (values[class_id] !== undefined) { + const confidence = Math.round(get_annotation_confidence_for_class(annotation, class_id) * 100); + should_deprecate = this.filter_function(confidence, values[class_id].confidence); + } + } else { + // In single-class mode, filter all targeted annotations by their highest confidence + const confidence = Math.round(this.get_confidence(annotation) * 100); + should_deprecate = this.filter_function(confidence, values.all.confidence); + } + + // Mark deprecated and, if the visible state changed, queue a redraw + const was_deprecated = annotation.deprecated; + this.mark_deprecated(annotation, should_deprecate, this.deprecated_by_key); + if (annotation.deprecated !== was_deprecated) { + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); + } + } + + if (redraw) { + for (const subtask_key in annotations_ids_to_redraw_by_subtask) { + this.ulabel.redraw_multiple_spatial_annotations(annotations_ids_to_redraw_by_subtask[subtask_key], subtask_key); + } + this.ulabel.toolbox.redraw_update_items(this.ulabel); + } + } + + /** + * Builds one slider per targeted class for multi-class mode. + */ + private createMultiFilterHTML(): string { + const class_defs = this.get_target_class_defs(); + + let multi_class_html = ``; + for (const class_def of class_defs) { + const default_value = ( + this.default_values[class_def.id] !== undefined ? + this.default_values[class_def.id].confidence : + this.default_values.all.confidence + ).toString(); + + const slider = new SliderHandler({ + id: `${this.component_prefix}-${class_def.id}`, + class: `${this.slider_class} ${this.slider_class}-class`, + min: this.filter_min.toString(), + max: this.filter_max.toString(), + default_value: default_value, + step: this.step_value.toString(), + label_units: "%", + main_label: class_def.name, + slider_event: () => this.filter_annotations(true), + }); + + multi_class_html += slider.getSliderHTML(); + } + + return multi_class_html; + } + + public get_html(): string { + const multi_class_html = this.createMultiFilterHTML(); + + const single_class_slider = new SliderHandler({ + id: `${this.component_prefix}-all`, // "all" is extracted using regex + class: this.slider_class, + default_value: this.default_values.all.confidence.toString(), + min: this.filter_min.toString(), + max: this.filter_max.toString(), + step: this.step_value.toString(), + label_units: "%", + slider_event: () => this.filter_annotations(true), + }); + + let options_html = ``; + if (this.show_multi_toggle) { + options_html = ` +
+ Options ˅ +
+ + +
+
`; + } + + return ` +
+

${this.name}

+ ${options_html} +
+ ${single_class_slider.getSliderHTML()} +
+
+ ${multi_class_html} +
+
+ `; + } + + /** + * Get the current confidence slider values by reading the DOM slider elements. + * + * @returns A map of class identifiers to their threshold values, or null if no sliders are found + */ + public get_current_values(): ConfidenceSliderClasses | null { + const all_slider = document.querySelector(`#${this.component_prefix}-all`); + if (all_slider === null) return null; + + const values: ConfidenceSliderClasses = { all: { confidence: all_slider.valueAsNumber } }; + + // In multi-class mode, also read the per-class sliders + if (this.is_multi_class) { + const sliders = document.querySelectorAll(`.${this.slider_class}`); + for (let idx = 0; idx < sliders.length; idx++) { + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; + if (slider_class_name === "all") continue; + values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + } + } + + return values; + } + + public after_init() { + // This toolbox item doesn't need to do anything after initialization + } + + public get_toolbox_item_type() { + return "ConfidenceSlider"; + } +} + +/** + * ToolboxItem for filtering point ("keypoint") annotations by their confidence. + * + * @deprecated Use {@link ConfidenceSlider} (`AllowedToolboxItem.ConfidenceSlider`) instead, + * which supports all spatial annotation types, per-class targeting, and multiple sliders. + * This item is retained for backwards compatibility and only filters point annotations. + */ +export class KeypointSliderItem extends ConfidenceSlider { + public html!: string; + public inner_HTML: string; + public slider_bar_id: string; + + filter_value: number = 0; + keybinds: { + increment: string; + decrement: string; + }; + + // TODO (joshua-dean): See if we can narrow this any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(ulabel: ULabel, kwargs: { [name: string]: any }) { + super(ulabel); + this.inner_HTML = `

Keypoint Slider

`; + this.ulabel = ulabel; + + // Use properties in kwargs if kwargs is present + if (kwargs !== undefined) { + this.name = kwargs.name; + this.filter_function = kwargs.filter_function; + this.get_confidence = kwargs.confidence_function; + this.mark_deprecated = kwargs.mark_deprecated; + this.keybinds = kwargs.keybinds; + } else { + // Otherwise use defaults + this.name = "Keypoint Slider"; + this.filter_function = value_is_lower_than_filter; + this.get_confidence = get_annotation_confidence; + this.mark_deprecated = mark_deprecated; + this.keybinds = { + increment: "2", + decrement: "1", + }; + } + + // Create slider bar id + this.slider_bar_id = this.name.replaceLowerConcat(" ", "-"); + + // If the config has a default value override the filter_value + const has_filter_override = Object.prototype.hasOwnProperty.call( + this.ulabel.config, + this.name.replaceLowerConcat(" ", "_", "_default_value"), + ); + if (has_filter_override) { + // Set the filter value + this.filter_value = this.ulabel.config[this.name.replaceLowerConcat(" ", "_", "_default_value")]; + } + + // Check the config to see if we should update the annotations with the default filter on load + if (this.ulabel.config.filter_annotations_on_load) { + this.filter_keypoint_annotations(this.ulabel); + } + + this.add_styles(); + } + + /** + * Create the css for this ToolboxItem and append it to the page. + */ + protected add_styles() { + // Define the css + const css = ` + /* Component has no css?? */ + `; + // Create an id so this specific style tag can be referenced + const style_id = "keypoint-slider-toolbox-item-styles"; + + // Don't add the style tag if its already been added once + if (document.getElementById(style_id)) return; + + // Grab the document's head and create a style tag + const head = document.head || document.querySelector("head"); + const style = document.createElement("style"); + + // Add the css and id to the style tag + style.appendChild(document.createTextNode(css)); + style.id = style_id; + + // Add the style tag to the document's head + head.appendChild(style); + } + + /** + * Given the ulabel object and a filter value, go through each point annotation and decide + * whether or not to deprecate it. + * + * @param ulabel ULabel object + * @param filter_value The number between 0-100 which annotation's confidence is compared against + * @param redraw whether or not to redraw the annotations after filtering + */ + private filter_keypoint_annotations(ulabel: ULabel, filter_value: number | null = null, redraw: boolean = false): void { + if (filter_value === null) { + // Use stored filter value if none is passed in + filter_value = Math.round(this.filter_value * 100); + } + // Store which annotations need to be redrawn + const annotations_ids_to_redraw_by_subtask: { [key: string]: string[] } = {}; + // Initialize the object with the subtask keys + for (const subtask_key in ulabel.subtasks) { + annotations_ids_to_redraw_by_subtask[subtask_key] = []; + } + + // Get all point annotations + const point_and_line_annotations = get_point_and_line_annotations(ulabel); + for (const annotation of point_and_line_annotations[0]) { + // Get the annotation's confidence as decimal between 0-1 + let confidence: number = this.get_confidence(annotation); + + // filter_value will be a number between 0-100, so convert the confidence to a percentage as well + confidence = Math.round(confidence * 100); + + // Compare the confidence value against the filter value + const should_deprecate: boolean = this.filter_function(confidence, filter_value); + // Check if an annotation should be deprecated or undeprecated, else do nothing + if ( + (should_deprecate && !annotation.deprecated) || + (!should_deprecate && annotation.deprecated) + ) { + // Mark this annotation as either deprecated or undeprecated by the confidence filter + this.mark_deprecated(annotation, should_deprecate, "confidence_filter"); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); + } + } + + if (redraw) { + // Redraw each subtask's annotations + for (const subtask_key in annotations_ids_to_redraw_by_subtask) { + ulabel.redraw_multiple_spatial_annotations(annotations_ids_to_redraw_by_subtask[subtask_key], subtask_key); + } + // Update class counter + ulabel.toolbox.redraw_update_items(ulabel); + } + } + + public get_html() { + // Create a SliderHandler instance to handle slider interactions + const slider_handler = new SliderHandler({ + id: this.name.replaceLowerConcat(" ", "-"), + class: "keypoint-slider", + default_value: Math.round(this.filter_value * 100).toString(), + label_units: "%", + slider_event: (slider_value: number | string) => { + // Filter the annotations, then redraw them + this.filter_keypoint_annotations(this.ulabel, Number(slider_value), true); + }, + }); + + return ` +
+

${this.name}

+ ` + slider_handler.getSliderHTML() + ` +
+ `; + } + + /** + * Get the current keypoint slider value by reading the DOM slider element. + * + * @returns The current slider value as a number between 0 and 1, or null if the slider is not found + */ + public get_current_value(): number | null { + const slider = document.querySelector(`#${this.slider_bar_id}`); + if (slider === null) return null; + return slider.valueAsNumber / 100; + } + + public after_init() { + // This toolbox item doesn't need to do anything after initialization + } + + public get_toolbox_item_type() { + return "KeypointSlider"; + } +} diff --git a/tests/annotation_operators.test.js b/tests/annotation_operators.test.js index eaa05551..16845363 100644 --- a/tests/annotation_operators.test.js +++ b/tests/annotation_operators.test.js @@ -1,4 +1,4 @@ -// Unit tests for confidence-filter-related annotation operators +// Unit tests for confidence-slider-related annotation operators const { get_annotation_confidence_for_class, get_spatial_annotations_with_confidence, diff --git a/tests/e2e/slider-api.spec.js b/tests/e2e/slider-api.spec.js index 5e7a86f3..a5ef9e6b 100644 --- a/tests/e2e/slider-api.spec.js +++ b/tests/e2e/slider-api.spec.js @@ -78,11 +78,11 @@ test.describe("Slider Public API", () => { }); }); - test.describe("get_confidence_filter_value", () => { - test("should return default confidence filter value after init", async ({ page }) => { + test.describe("get_confidence_slider_value", () => { + test("should return default confidence slider value after init", async ({ page }) => { await wait_for_ulabel_init(page); - const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + const value = await page.evaluate(() => window.ulabel.get_confidence_slider_value()); // Should have the "all" key with the default threshold of 0 expect(value).not.toBeNull(); @@ -95,12 +95,12 @@ test.describe("Slider Public API", () => { // Set the single-class confidence slider to 60 await page.evaluate(() => { - const slider = document.querySelector("#confidence-filter-all"); + const slider = document.querySelector("#confidence-slider-all"); slider.value = "60"; slider.dispatchEvent(new Event("input", { bubbles: true })); }); - const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + const value = await page.evaluate(() => window.ulabel.get_confidence_slider_value()); expect(value.all.confidence).toBe(60); }); @@ -108,7 +108,7 @@ test.describe("Slider Public API", () => { test("should return an object with an all key", async ({ page }) => { await wait_for_ulabel_init(page); - const value = await page.evaluate(() => window.ulabel.get_confidence_filter_value()); + const value = await page.evaluate(() => window.ulabel.get_confidence_slider_value()); expect(typeof value).toBe("object"); expect(value).toHaveProperty("all"); diff --git a/tests/types/index.test-d.ts b/tests/types/index.test-d.ts index 3478ad27..bd0d7b62 100644 --- a/tests/types/index.test-d.ts +++ b/tests/types/index.test-d.ts @@ -86,5 +86,5 @@ const all_items: AllowedToolboxItem[] = [ AllowedToolboxItem.ImageFilters, AllowedToolboxItem.AnnotationList, AllowedToolboxItem.Keybinds, - AllowedToolboxItem.ConfidenceFilter, + AllowedToolboxItem.ConfidenceSlider, ]; From 2ccc9a7e66a1663b19f67a21917979f47832d7e1 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 13:03:05 -0500 Subject: [PATCH 03/11] rename multi class mode since we support single class enforcement --- .github/tasks.md | 1 - api_spec.md | 23 +++++++------- demo/multi-class.html | 3 ++ demo/row-filtering-example.html | 2 ++ index.d.ts | 15 +++++---- src/configuration.ts | 3 +- src/toolbox_items/confidence_slider.ts | 43 ++++++++++++-------------- 7 files changed, 44 insertions(+), 46 deletions(-) diff --git a/.github/tasks.md b/.github/tasks.md index 2cb4c3aa..160f5414 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -9,5 +9,4 @@ - [x] Add support for configurable step size and min/max values for the confidence slider - [x] Add config options such that users can either enforce, disable, or leave toggleable the multi-class mode for the confidence slider - [x] Should we raise a warning if a KeypointSlider is used alongside a ConfidenceFilter? -- [ ] Discuss with the user: How could we implement an option to only filter specific classes rather than either all at once or all classes individually? Basically, I want a multi-class option where the user can enable/disable specific classes in the config - [ ] I notice that if no toolbox order is provided, the Keypoint Slider is included. Can we replace it with a Confidence Slider? diff --git a/api_spec.md b/api_spec.md index dbecefce..bd054157 100644 --- a/api_spec.md +++ b/api_spec.md @@ -441,13 +441,13 @@ The `ConfidenceSlider` toolbox item (added to `toolbox_order` via `AllowedToolbo It supports two modes: -- **Single-class mode** (default): a single slider applies one confidence threshold to every targeted spatial annotation across all subtasks, using each annotation's highest confidence value. -- **Multi-class mode**: one slider is shown per targeted class id. Each slider only filters annotations whose assigned (highest-confidence) class matches that slider, using that class's confidence value. +- **"all" mode** (default): a single slider applies one confidence threshold to every targeted spatial annotation across all subtasks, using each annotation's highest confidence value. +- **Per-class mode**: one slider is shown per targeted class id. Each slider only filters annotations whose assigned (highest-confidence) class matches that slider, using that class's confidence value. -The `multi_class_mode` config controls whether these modes are user-toggleable: -- `"toggle"` (default): a "Multi-Class Filtering" checkbox lets the user switch between the two modes. -- `"single-only"`: only the single global slider is shown. -- `"multi-only"`: only the per-class sliders are shown. +The `class_filter_mode` config controls whether these modes are user-toggleable: +- `"toggle"` (default): a checkbox lets the user switch between the two modes. +- `"all-only"`: only the single global "all" slider is shown. +- `"class-only"`: only the per-class sliders are shown. Any annotation with a confidence at or above the threshold is shown; any below is deprecated. Thresholds are expressed as percentages, and the slider's range and increment are configurable via `filter_min`, `filter_max`, and `step_value`. @@ -458,8 +458,8 @@ type ConfidenceThreshold = { } type ConfidenceSliderClasses = { - "all": ConfidenceThreshold, // value used by the single-class slider - [key: number]?: ConfidenceThreshold // per-class-id values used in multi-class mode + "all": ConfidenceThreshold, // value used by the single global slider + [key: number]?: ConfidenceThreshold // per-class-id values used in per-class mode } type ConfidenceSliderConfig = { @@ -468,12 +468,11 @@ type ConfidenceSliderConfig = { "filter_max"?: number, // Default: 100 (%) "default_values"?: ConfidenceSliderClasses, // Default: {"all": {"confidence": 0}} "step_value"?: number, // Default: 1 (%) - "multi_class_mode"?: "toggle" | "single-only" | "multi-only", // Default: "toggle" + "class_filter_mode"?: "toggle" | "all-only" | "class-only", // Default: "toggle" "filter_on_load"?: boolean, // Default: true - "show_options"?: boolean, // Default: true // The spatial types to filter. Defaults to all confidence-filterable spatial types. "target_spatial_types"?: ULabelSpatialType[], - // The class ids to create sliders for in multi-class mode. Defaults to all class ids. + // The class ids to create sliders for in class-only/toggle mode. Defaults to all class ids. "target_class_ids"?: number[], } ``` @@ -617,7 +616,7 @@ Sets the zoom to focus on the provided annotation, and switches to its subtask i ### `get_confidence_slider_value()` -*() => object | null* -- Returns an object mapping class identifiers to their confidence threshold values (as percentages, 0–100). The object always includes an `all` key for the single-class slider. In multi-class mode, additional keys correspond to each class ID. Returns `null` if the ConfidenceSlider toolbox item is not active or no sliders are found. +*() => object | null* -- Returns an object mapping class identifiers to their confidence threshold values (as percentages, 0–100). The object always includes an `all` key for the single global slider. In per-class mode, additional keys correspond to each class ID. Returns `null` if the ConfidenceSlider toolbox item is not active or no sliders are found. ## Generic Callbacks diff --git a/demo/multi-class.html b/demo/multi-class.html index db1e2cb7..dc75865c 100644 --- a/demo/multi-class.html +++ b/demo/multi-class.html @@ -142,6 +142,9 @@ "brightness": 120 }, }, + "confidence_slider_toolbox_item": { + "step_value": 5, + }, }); // Wait for ULabel instance to finish initialization ulabel.init(function () { diff --git a/demo/row-filtering-example.html b/demo/row-filtering-example.html index b944a414..77db1e0c 100644 --- a/demo/row-filtering-example.html +++ b/demo/row-filtering-example.html @@ -762,6 +762,8 @@ // "filter_during_polyline_move": false, }, "confidence_slider_toolbox_item": { + "class_filter_mode": "class-only", + "target_class_ids": [10], // Plant only "default_values": { "all": { "confidence": 20, // % diff --git a/index.d.ts b/index.d.ts index 78f796fe..22814557 100644 --- a/index.d.ts +++ b/index.d.ts @@ -134,12 +134,12 @@ export type ConfidenceSliderClasses = { }; /** - * Controls the multi-class mode of the ConfidenceSlider ToolboxItem: - * - "toggle": a checkbox lets the user switch between single- and multi-class filtering - * - "single-only": only a single global slider is shown - * - "multi-only": only per-class sliders are shown + * Controls how the ConfidenceSlider ToolboxItem filters by class: + * - "toggle": a checkbox lets the user switch between the single "all" slider and per-class sliders + * - "all-only": only a single global slider (keyed "all") is shown, filtering every targeted annotation + * - "class-only": only per-class sliders are shown */ -export type ConfidenceSliderMultiClassMode = "toggle" | "single-only" | "multi-only"; +export type ConfidenceSliderClassFilterMode = "toggle" | "all-only" | "class-only"; /** * Config object for the ConfidenceSlider ToolboxItem. @@ -150,12 +150,11 @@ export type ConfidenceSliderConfig = { filter_max?: number; default_values?: ConfidenceSliderClasses; step_value?: number; - multi_class_mode?: ConfidenceSliderMultiClassMode; + class_filter_mode?: ConfidenceSliderClassFilterMode; filter_on_load?: boolean; - show_options?: boolean; // The spatial types to filter. Defaults to all confidence-filterable spatial types. target_spatial_types?: ULabelSpatialType[]; - // The class ids to create sliders for in multi-class mode. Defaults to all class ids. + // The class ids to create sliders for in class-only/toggle mode. Defaults to all class ids. target_class_ids?: number[]; keybinds?: { increment: string; diff --git a/src/configuration.ts b/src/configuration.ts index 88d8e571..05a55184 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -69,9 +69,8 @@ export const DEFAULT_CONFIDENCE_SLIDER_CONFIG: ConfidenceSliderConfig = { filter_max: 100, default_values: { all: { confidence: 0 } }, step_value: 1, - multi_class_mode: "toggle", + class_filter_mode: "toggle", filter_on_load: true, - show_options: true, }; export const DEFAULT_IMAGE_FILTERS_CONFIG: ImageFiltersConfig = { diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts index f879a0a3..c5d6e6cb 100644 --- a/src/toolbox_items/confidence_slider.ts +++ b/src/toolbox_items/confidence_slider.ts @@ -30,8 +30,8 @@ import { * - Multi-class mode: one slider per targeted class id. Each slider filters only the annotations * whose assigned (highest-confidence) class matches, using that class's confidence. * - * The `multi_class_mode` config controls whether the mode is user-toggleable (`"toggle"`), or - * locked to `"single-only"` or `"multi-only"`. + * The `class_filter_mode` config controls whether the mode is user-toggleable (`"toggle"`), or + * locked to `"all-only"` (single global slider) or `"class-only"` (per-class sliders). */ export class ConfidenceSlider extends ToolboxItem { public ulabel: ULabel; @@ -41,15 +41,14 @@ export class ConfidenceSlider extends ToolboxItem { public filter_max!: number; public step_value!: number; public default_values!: ConfidenceSliderClasses; - public show_options!: boolean; public filter_on_load!: boolean; public target_spatial_types!: ULabelSpatialType[]; public target_class_ids!: number[] | null; public collapse_options: boolean = false; - // Whether the multi-class toggle is shown, and the current runtime mode - public show_multi_toggle!: boolean; - public is_multi_class!: boolean; + // Whether the class-filter-mode toggle is shown, and whether per-class sliders are active + public show_class_toggle!: boolean; + public is_class_mode!: boolean; public filter_function: (value: number, filter: number) => boolean; public get_confidence: (annotation: ULabelAnnotation) => number; @@ -95,15 +94,14 @@ export class ConfidenceSlider extends ToolboxItem { this.filter_max = this.config.filter_max!; this.step_value = this.config.step_value!; this.default_values = this.config.default_values!; - this.show_options = this.config.show_options!; this.filter_on_load = this.config.filter_on_load!; this.target_spatial_types = this.config.target_spatial_types ?? CONFIDENCE_FILTERABLE_SPATIAL_TYPES; this.target_class_ids = this.config.target_class_ids ?? null; - // Resolve the multi-class mode option into runtime state - const multi_class_mode = this.config.multi_class_mode!; - this.show_multi_toggle = multi_class_mode === "toggle"; - this.is_multi_class = multi_class_mode === "multi-only"; + // Resolve the class-filter-mode option into runtime state + const class_filter_mode = this.config.class_filter_mode!; + this.show_class_toggle = class_filter_mode === "toggle"; + this.is_class_mode = class_filter_mode === "class-only"; this.add_styles(); this.add_event_listeners(); @@ -179,9 +177,9 @@ export class ConfidenceSlider extends ToolboxItem { // Toggle the options fieldset when its legend is clicked $(document).on("click.ulabel", "fieldset.confidence-slider-options > legend", () => this.toggleCollapsedOptions()); - // Switch between single-class and multi-class filtering when the checkbox is clicked + // Switch between the "all" slider and the per-class sliders when the checkbox is clicked $(document).on("click.ulabel", `#${this.component_prefix}-multi-checkbox`, (event) => { - this.is_multi_class = event.currentTarget.checked; + this.is_class_mode = event.currentTarget.checked; $(`#${this.component_prefix}-single-class-mode`).toggleClass("ulabel-hidden"); $(`#${this.component_prefix}-multi-class-mode`).toggleClass("ulabel-hidden"); this.filter_annotations(true); @@ -253,8 +251,8 @@ export class ConfidenceSlider extends ToolboxItem { for (const annotation of annotations) { let should_deprecate = false; - if (this.is_multi_class) { - // In multi-class mode, only filter annotations whose assigned class has a slider + if (this.is_class_mode) { + // In class-only mode, only filter annotations whose assigned class has a slider const class_id = Number(get_annotation_class_id(annotation)); if (values[class_id] !== undefined) { const confidence = Math.round(get_annotation_confidence_for_class(annotation, class_id) * 100); @@ -329,11 +327,10 @@ export class ConfidenceSlider extends ToolboxItem { }); let options_html = ``; - if (this.show_multi_toggle) { + if (this.show_class_toggle) { options_html = `
Options ˅ @@ -342,12 +339,12 @@ export class ConfidenceSlider extends ToolboxItem { type="checkbox" id="${this.component_prefix}-multi-checkbox" class="confidence-slider-options-checkbox" - ${this.is_multi_class ? "checked" : ""} + ${this.is_class_mode ? "checked" : ""} />
`; @@ -357,10 +354,10 @@ export class ConfidenceSlider extends ToolboxItem {

${this.name}

${options_html} -
+
${single_class_slider.getSliderHTML()}
-
+
${multi_class_html}
@@ -378,8 +375,8 @@ export class ConfidenceSlider extends ToolboxItem { const values: ConfidenceSliderClasses = { all: { confidence: all_slider.valueAsNumber } }; - // In multi-class mode, also read the per-class sliders - if (this.is_multi_class) { + // In class-only mode, also read the per-class sliders + if (this.is_class_mode) { const sliders = document.querySelectorAll(`.${this.slider_class}`); for (let idx = 0; idx < sliders.length; idx++) { const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; From ca0033755fd744e7684751845286f0d500a3cfb1 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 13:19:31 -0500 Subject: [PATCH 04/11] switch default from keypoint slider to confidence slider --- .github/tasks.md | 10 ---------- src/configuration.ts | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/tasks.md b/.github/tasks.md index 160f5414..d51ecef5 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -1,12 +1,2 @@ ## Tasks -### Generalize Confidence Slider - -- [x] Discuss with the user: Naming options for the new toolbox item. -- [x] Replace all instances of `KeypointSlider` in our demos with the new toolbox item. -- [x] Add confidences to the annotations in the resume-from demo and add a confidence slider to the toolbox. -- [x] Refactor new toolbox item into its own file under src/toolbox_items -- [x] Add support for configurable step size and min/max values for the confidence slider -- [x] Add config options such that users can either enforce, disable, or leave toggleable the multi-class mode for the confidence slider -- [x] Should we raise a warning if a KeypointSlider is used alongside a ConfidenceFilter? -- [ ] I notice that if no toolbox order is provided, the Keypoint Slider is included. Can we replace it with a Confidence Slider? diff --git a/src/configuration.ts b/src/configuration.ts index 05a55184..240c5d04 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -192,7 +192,7 @@ export class Configuration { AllowedToolboxItem.AnnotationID, AllowedToolboxItem.RecolorActive, AllowedToolboxItem.ClassCounter, - AllowedToolboxItem.KeypointSlider, + AllowedToolboxItem.ConfidenceSlider, AllowedToolboxItem.SubmitButtons, ]; From c2c083d438506b0ce644adb9eed6104ca15c876a Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 13:22:43 -0500 Subject: [PATCH 05/11] Bump version and update changelog --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- src/version.js | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23739dfe..2901d96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented here. ## [unreleased] +## [0.24.0] - July 22nd, 2026 +- Add `ConfidenceSlider` toolbox item (`AllowedToolboxItem.ConfidenceSlider`) that deprecates (hides) or shows spatial annotations based on their confidence values. Unlike the now-deprecated `KeypointSlider`, it works with all spatial annotation types that have a confidence payload (`bbox`, `bbox3`, `polygon`, `polyline`, `contour`, `tbar`, and `point`), across every subtask. + - Supports a single global "all" slider and/or per-class sliders, controlled by `class_filter_mode` (`"toggle"`, `"all-only"`, or `"class-only"`). + - Configurable via `confidence_slider_toolbox_item`, including `filter_min`/`filter_max`/`step_value`, per-class `default_values`, `target_spatial_types`, and `target_class_ids`. +- Add `get_confidence_slider_value()` public API method to get the current confidence slider values. +- The default `toolbox_order` (used when none is provided) now includes `ConfidenceSlider` instead of `KeypointSlider`. +- Log a warning when both the `KeypointSlider` and `ConfidenceSlider` toolbox items are enabled at once. +- Deprecate the `KeypointSlider` toolbox item and `get_keypoint_slider_value()` in favor of the `ConfidenceSlider` and `get_confidence_slider_value()`. The `KeypointSlider` is retained for backwards compatibility and continues to filter only `point` annotations. It will be removed in a future release. + ## [0.23.7] - July 21st, 2026 - Ship generated `.d.ts` declarations (`dist/types/`) and a generated type entry (`dist/index.d.ts`) instead of pointing consumers' type resolution at the raw `.ts` source. This stops downstream TypeScript projects from compiling ULabel's source under their own (stricter) `tsconfig`. - Stop publishing the `src/` directory in the npm package (`files` now ships `dist/` only). diff --git a/package.json b/package.json index 7a210dfd..273fa93f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ulabel", "description": "An image annotation tool.", - "version": "0.23.7", + "version": "0.24.0", "main": "dist/ulabel.min.js", "module": "dist/ulabel.min.js", "types": "dist/index.d.ts", diff --git a/src/version.js b/src/version.js index 655de88a..386b2d32 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const ULABEL_VERSION = "0.23.7"; +export const ULABEL_VERSION = "0.24.0"; From dd84dd17bfe393984f7130ae9d2653dccd819e51 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 13:34:07 -0500 Subject: [PATCH 06/11] update demo --- demo/live_demo.html | 87 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/demo/live_demo.html b/demo/live_demo.html index 5f5aa3a4..b821f634 100644 --- a/demo/live_demo.html +++ b/demo/live_demo.html @@ -22,7 +22,7 @@ "spatial_type": "bbox", "spatial_payload": [[871.84, 480.78], [833.71, 396.29]], "classification_payloads": [ - {"class_id": 11, "confidence": 1.0}, + {"class_id": 11, "confidence": 0.75}, ], }, { @@ -30,7 +30,90 @@ "spatial_type": "bbox", "spatial_payload": [[1257.66, 541.35], [1079.71, 411.99]], "classification_payloads": [ - {"class_id": 10, "confidence": 1.0}, + {"class_id": 10, "confidence": 0.85}, + ], + }, + // Additional annotations with varied confidences to demo the ConfidenceSlider + { + "id": "71be2a9d-5f1c-4a98-835e-e7826ebec6cf", + "spatial_type": "bbox", + "spatial_payload": [[1381.27, 510.38], [1268.86, 410.89]], + "classification_payloads": [ + {"class_id": 10, "confidence": 0.9}, + ], + }, + { + "id": "79c81e3e-0a08-4fa1-a6b0-1e58a498116f", + "spatial_type": "polygon", + "spatial_payload": [ + [ + [452.24, 540.10], [625.38, 532.35], [642.18, 483.25], + [630.55, 447.07], [591.79, 414.77], [462.58, 413.48], + [445.78, 421.23], [447.07, 469.04], [434.15, 503.92], + [421.23, 510.38], [416.06, 537.52], [452.24, 540.10] + ] + ], + "spatial_payload_holes": [false], + "spatial_payload_child_indices": [[]], + "classification_payloads": [ + {"class_id": 10, "confidence": 0.35}, + ], + }, + { + "id": "c170e38a-eafd-429d-87b1-9a1ee75e25b0", + "spatial_type": "bbox", + "spatial_payload": [[1447.58, 484.52], [1418.42, 397.04]], + "classification_payloads": [ + {"class_id": 11, "confidence": 0.55}, + ], + }, + { + "id": "ec96ec1f-2007-4d10-a286-16af43775105", + "spatial_type": "bbox", + "spatial_payload": [[1477.49, 480.78], [1453.56, 401.52]], + "classification_payloads": [ + {"class_id": 11, "confidence": 0.2}, + ], + }, + { + "id": "ca9209d9-f787-42e4-8d4b-4408e10ae1ed", + "spatial_type": "bbox", + "spatial_payload": [[1064.00, 468.07], [1031.85, 400.03]], + "classification_payloads": [ + {"class_id": 11, "confidence": 0.7}, + ], + }, + { + "id": "39f9f056-383a-4b25-9e31-e71951a1ecaf", + "spatial_type": "bbox", + "spatial_payload": [[1037.67, 477.22], [1013.63, 393.07]], + "classification_payloads": [ + {"class_id": 11, "confidence": 0.45}, + ], + }, + { + "id": "9cf8c7ef-814e-48e1-aa1a-e5e74e922938", + "spatial_type": "bbox", + "spatial_payload": [[952.93, 470.00], [898.24, 409.90]], + "classification_payloads": [ + {"class_id": 11, "confidence": 0.6}, + ], + }, + { + "id": "84ba0141-fc65-4f89-9057-38eef9d93586", + "spatial_type": "polygon", + "spatial_payload": [ + [ + [4.78, 791.18], [341.47, 702.03], [358.98, 666.22], + [374.90, 595.38], [369.32, 543.64], [332.71, 497.47], + [305.65, 486.33], [275.40, 484.74], [175.91, 423.45], + [71.64, 410.71], [0, 408.33], [4.78, 791.18] + ] + ], + "spatial_payload_holes": [false], + "spatial_payload_child_indices": [[]], + "classification_payloads": [ + {"class_id": 10, "confidence": 0.65}, ], }, ]; From 438eeb30d36fa6f4e03e34642887797af5a63a96 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 14:19:52 -0500 Subject: [PATCH 07/11] add test coverage --- jest.config.js | 5 + tests/confidence_slider.test.js | 317 ++++++++++++++++++++++++++++ tests/e2e/confidence-slider.spec.js | 108 ++++++++++ tests/testing-utils/blobs_stub.js | 5 + tests/testing-utils/version_stub.js | 4 + 5 files changed, 439 insertions(+) create mode 100644 tests/confidence_slider.test.js create mode 100644 tests/e2e/confidence-slider.spec.js create mode 100644 tests/testing-utils/blobs_stub.js create mode 100644 tests/testing-utils/version_stub.js diff --git a/jest.config.js b/jest.config.js index 3fc5e57c..a8e28b89 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,6 +18,11 @@ module.exports = { coverageReporters: ["text", "lcov", "html"], moduleNameMapper: { "^@/(.*)$": "/src/$1", + // The `build/` CJS modules reference the raw ESM sources `src/version.js` and + // `src/blobs.js`, which jest cannot transform. Stub them for unit tests that load + // `build/` modules (e.g. the ConfidenceSlider toolbox item via html_builder). + "src/version(\\.js)?$": "/tests/testing-utils/version_stub.js", + "src/blobs(\\.js)?$": "/tests/testing-utils/blobs_stub.js", }, transform: { "^.+\\.(js|ts)$": "babel-jest", diff --git a/tests/confidence_slider.test.js b/tests/confidence_slider.test.js new file mode 100644 index 00000000..f29d0628 --- /dev/null +++ b/tests/confidence_slider.test.js @@ -0,0 +1,317 @@ +// Unit tests for the ConfidenceSlider toolbox item +// NOTE: require `configuration` before `confidence_slider`. `configuration` pulls in `toolbox` +// (which defines the `ToolboxItem` base class) and then `confidence_slider`, ensuring the base +// class is initialized before `ConfidenceSlider extends ToolboxItem` evaluates. Requiring +// `confidence_slider` first hits the circular import before `ToolboxItem` is defined. +const { Configuration, AllowedToolboxItem } = require("../build/configuration"); +const { ConfidenceSlider } = require("../build/toolbox_items/confidence_slider"); + +/** + * Build a minimal annotation-like object. + */ +function make_annotation(id, spatial_type, payloads, extra = {}) { + return { + id, + spatial_type, + classification_payloads: payloads, + deprecated: false, + deprecated_by: { human: false }, + ...extra, + }; +} + +/** + * Build a minimal mock ULabel object with a single subtask. + */ +function make_ulabel(annotations, config = {}) { + const access = {}; + for (const annotation of annotations) { + access[annotation.id] = annotation; + } + return { + config: { confidence_slider_toolbox_item: config }, + subtasks: { + st: { + annotations: { access }, + allowed_modes: ["bbox", "polygon", "point", "polyline"], + class_defs: [ + { name: "Car", id: 10, color: "red", keybind: null }, + { name: "Truck", id: 11, color: "blue", keybind: null }, + ], + }, + }, + redraw_multiple_spatial_annotations: jest.fn(), + toolbox: { redraw_update_items: jest.fn() }, + }; +} + +describe("ConfidenceSlider", () => { + beforeEach(() => { + // Reset the DOM so slider lookups fall back to defaults + document.body.innerHTML = ""; + }); + + describe("construction and config resolution", () => { + test("applies defaults and identifies as ConfidenceSlider", () => { + const cs = new ConfidenceSlider(make_ulabel([], { filter_on_load: false })); + + expect(cs.get_toolbox_item_type()).toBe("ConfidenceSlider"); + expect(cs.filter_min).toBe(0); + expect(cs.filter_max).toBe(100); + expect(cs.step_value).toBe(1); + expect(cs.deprecated_by_key).toBe("confidence_slider"); + }); + + test("class_filter_mode 'toggle' shows the toggle and starts in all mode", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "toggle", filter_on_load: false })); + expect(cs.show_class_toggle).toBe(true); + expect(cs.is_class_mode).toBe(false); + }); + + test("class_filter_mode 'all-only' hides the toggle and stays in all mode", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false })); + expect(cs.show_class_toggle).toBe(false); + expect(cs.is_class_mode).toBe(false); + }); + + test("class_filter_mode 'class-only' hides the toggle and starts in class mode", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "class-only", filter_on_load: false })); + expect(cs.show_class_toggle).toBe(false); + expect(cs.is_class_mode).toBe(true); + }); + + test("merges partial config with the defaults", () => { + const cs = new ConfidenceSlider(make_ulabel([], { filter_max: 50, filter_on_load: false })); + expect(cs.filter_max).toBe(50); // provided + expect(cs.filter_min).toBe(0); // default + expect(cs.step_value).toBe(1); // default + }); + }); + + describe("filter_annotations - all mode", () => { + test("deprecates annotations below the threshold and shows those at/above", () => { + const high = make_annotation("high", "bbox", [{ class_id: 10, confidence: 0.9 }]); + const mid = make_annotation("mid", "bbox", [{ class_id: 10, confidence: 0.5 }]); + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const ulabel = make_ulabel([high, mid, low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 60 } }, + filter_on_load: false, + }); + const cs = new ConfidenceSlider(ulabel); + + cs.filter_annotations(false); + + expect(high.deprecated).toBe(false); + expect(mid.deprecated).toBe(true); + expect(low.deprecated).toBe(true); + expect(mid.deprecated_by.confidence_slider).toBe(true); + }); + + test("a threshold of 0 deprecates nothing", () => { + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const cs = new ConfidenceSlider(make_ulabel([low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 0 } }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + + expect(low.deprecated).toBe(false); + }); + + test("lowering the threshold un-deprecates previously filtered annotations", () => { + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const cs = new ConfidenceSlider(make_ulabel([low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 50 } }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + expect(low.deprecated).toBe(true); + + // Lower the threshold and re-filter + cs.default_values.all.confidence = 0; + cs.filter_annotations(false); + + expect(low.deprecated).toBe(false); + expect(low.deprecated_by.confidence_slider).toBe(false); + }); + }); + + describe("filter_annotations - class mode", () => { + test("filters each class by its own threshold and ignores untargeted classes", () => { + const car = make_annotation("car", "bbox", [{ class_id: 10, confidence: 0.5 }]); + const truck = make_annotation("truck", "bbox", [{ class_id: 11, confidence: 0.5 }]); + const other = make_annotation("other", "bbox", [{ class_id: 12, confidence: 0.5 }]); + const cs = new ConfidenceSlider(make_ulabel([car, truck, other], { + class_filter_mode: "class-only", + default_values: { all: { confidence: 0 }, 10: { confidence: 60 }, 11: { confidence: 30 } }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + + expect(car.deprecated).toBe(true); // 50 < 60 + expect(truck.deprecated).toBe(false); // 50 >= 30 + expect(other.deprecated).toBe(false); // class 12 has no slider + }); + + test("targets an annotation by its highest-confidence (argmax) class", () => { + // argmax class is 11 (0.8); the class-11 threshold should apply + const anno = make_annotation("multi", "bbox", [ + { class_id: 10, confidence: 0.3 }, + { class_id: 11, confidence: 0.8 }, + ]); + const cs = new ConfidenceSlider(make_ulabel([anno], { + class_filter_mode: "class-only", + default_values: { all: { confidence: 0 }, 10: { confidence: 90 }, 11: { confidence: 50 } }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + + // 80 >= 50 (class-11 threshold) -> shown, even though 80 would fail class 10's 90 + expect(anno.deprecated).toBe(false); + }); + }); + + describe("target_spatial_types", () => { + test("only filters the configured spatial types", () => { + const point = make_annotation("p", "point", [{ class_id: 10, confidence: 0.1 }]); + const box = make_annotation("b", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const cs = new ConfidenceSlider(make_ulabel([point, box], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 50 } }, + target_spatial_types: ["bbox"], + filter_on_load: false, + })); + + cs.filter_annotations(false); + + expect(box.deprecated).toBe(true); + expect(point.deprecated).toBe(false); + expect(point.deprecated_by.confidence_slider).toBeUndefined(); + }); + }); + + describe("deprecated_by OR-logic", () => { + test("does not un-deprecate an annotation deprecated by a human", () => { + const anno = make_annotation("h", "bbox", [{ class_id: 10, confidence: 0.9 }], { + deprecated: true, + deprecated_by: { human: true }, + }); + const cs = new ConfidenceSlider(make_ulabel([anno], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 0 } }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + + expect(anno.deprecated_by.confidence_slider).toBe(false); + expect(anno.deprecated).toBe(true); // still deprecated by human + }); + }); + + describe("filter_on_load", () => { + test("filters during construction when filter_on_load is true", () => { + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + new ConfidenceSlider(make_ulabel([low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 50 } }, + filter_on_load: true, + })); + + expect(low.deprecated).toBe(true); + }); + + test("does not filter during construction when filter_on_load is false", () => { + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + new ConfidenceSlider(make_ulabel([low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 50 } }, + filter_on_load: false, + })); + + expect(low.deprecated).toBe(false); + }); + }); + + describe("redraw", () => { + test("redraws changed annotations and updates toolbox items when redraw is true", () => { + const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const ulabel = make_ulabel([low], { + class_filter_mode: "all-only", + default_values: { all: { confidence: 50 } }, + filter_on_load: false, + }); + const cs = new ConfidenceSlider(ulabel); + + cs.filter_annotations(true); + + expect(ulabel.redraw_multiple_spatial_annotations).toHaveBeenCalledWith(["low"], "st"); + expect(ulabel.toolbox.redraw_update_items).toHaveBeenCalled(); + }); + }); + + describe("get_current_values", () => { + test("returns null when no sliders are in the DOM", () => { + const cs = new ConfidenceSlider(make_ulabel([], { filter_on_load: false })); + expect(cs.get_current_values()).toBeNull(); + }); + + test("reads the single 'all' slider value", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false })); + document.body.innerHTML = ``; + + expect(cs.get_current_values()).toEqual({ all: { confidence: 40 } }); + }); + + test("reads per-class slider values in class mode", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "class-only", filter_on_load: false })); + document.body.innerHTML = ` + + + `; + + expect(cs.get_current_values()).toEqual({ + all: { confidence: 40 }, + 10: { confidence: 30 }, + }); + }); + }); + + describe("get_html", () => { + test("renders the multi-class toggle only in toggle mode", () => { + const toggle = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "toggle", filter_on_load: false })); + expect(toggle.get_html()).toContain("confidence-slider-multi-checkbox"); + + const allOnly = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false })); + expect(allOnly.get_html()).not.toContain("confidence-slider-multi-checkbox"); + }); + + test("target_class_ids limits which per-class sliders are rendered", () => { + const cs = new ConfidenceSlider(make_ulabel([], { + class_filter_mode: "class-only", + target_class_ids: [10], + filter_on_load: false, + })); + const html = cs.get_html(); + + expect(html).toContain("confidence-slider-all"); + expect(html).toContain("confidence-slider-10"); + expect(html).not.toContain("confidence-slider-11"); + }); + }); +}); + +describe("Default toolbox order", () => { + test("includes ConfidenceSlider and not the deprecated KeypointSlider", () => { + const config = new Configuration(); + expect(config.toolbox_order).toContain(AllowedToolboxItem.ConfidenceSlider); + expect(config.toolbox_order).not.toContain(AllowedToolboxItem.KeypointSlider); + }); +}); diff --git a/tests/e2e/confidence-slider.spec.js b/tests/e2e/confidence-slider.spec.js new file mode 100644 index 00000000..0a67de91 --- /dev/null +++ b/tests/e2e/confidence-slider.spec.js @@ -0,0 +1,108 @@ +// End-to-end tests for the ConfidenceSlider toolbox item. +// These exercise browser-specific behavior (DOM wiring, modes, config, coexistence warning) +// that the jest unit tests can't cover. They run against demo/multi-class.html, which enables +// both the ConfidenceSlider and the deprecated KeypointSlider and exposes window.ulabel. +import { test, expect } from "./fixtures"; +import { wait_for_ulabel_init } from "../testing-utils/init_utils"; + +test.describe("ConfidenceSlider", () => { + test("logs a warning when KeypointSlider and ConfidenceSlider are both enabled", async ({ page }) => { + const messages = []; + page.on("console", (msg) => messages.push(msg.text())); + + await wait_for_ulabel_init(page); + + expect( + messages.some((text) => text.includes("KeypointSlider") && text.includes("ConfidenceSlider")), + ).toBe(true); + }); + + test("reflects the configured step_value on the slider", async ({ page }) => { + await wait_for_ulabel_init(page); + + // multi-class.html configures step_value: 5 + const step = await page.evaluate(() => document.querySelector("#confidence-slider-all").getAttribute("step")); + expect(step).toBe("5"); + }); + + test("deprecates annotations below the threshold when the slider is raised", async ({ page }) => { + await wait_for_ulabel_init(page); + + const subtask_key = await page.evaluate(() => window.ulabel.get_current_subtask_key()); + const class_id = await page.evaluate(() => { + const subtask = window.ulabel.get_current_subtask(); + return subtask.class_defs.find((cd) => cd.id >= 0).id; + }); + + // Inject two bboxes with known confidences + await page.evaluate(({ stk, cid }) => { + const annotations = [ + { + id: "conf-test-low", + spatial_type: "bbox", + spatial_payload: [[100, 100], [200, 200]], + classification_payloads: [{ class_id: cid, confidence: 0.2 }], + }, + { + id: "conf-test-high", + spatial_type: "bbox", + spatial_payload: [[300, 300], [400, 400]], + classification_payloads: [{ class_id: cid, confidence: 0.9 }], + }, + ]; + window.ulabel.set_annotations(annotations, stk); + }, { stk: subtask_key, cid: class_id }); + + // Raise the single "all" slider to 50% + await page.evaluate(() => { + const slider = document.querySelector("#confidence-slider-all"); + slider.value = "50"; + slider.dispatchEvent(new Event("input", { bubbles: true })); + }); + + const result = await page.evaluate((stk) => { + const annotations = window.ulabel.get_annotations(stk); + const low = annotations.find((a) => a.id === "conf-test-low"); + const high = annotations.find((a) => a.id === "conf-test-high"); + return { low: low.deprecated, high: high.deprecated }; + }, subtask_key); + + expect(result.low).toBe(true); // 20% < 50% + expect(result.high).toBe(false); // 90% >= 50% + }); + + test("shows per-class sliders when Per-Class Filtering is enabled", async ({ page }) => { + await wait_for_ulabel_init(page); + + // The per-class container starts hidden (default mode is the single "all" slider) + const hidden_before = await page.evaluate(() => + document.querySelector("#confidence-slider-multi-class-mode").classList.contains("ulabel-hidden"), + ); + expect(hidden_before).toBe(true); + + // Enable per-class filtering via the toggle checkbox + await page.click("#confidence-slider-multi-checkbox"); + + const hidden_after = await page.evaluate(() => + document.querySelector("#confidence-slider-multi-class-mode").classList.contains("ulabel-hidden"), + ); + expect(hidden_after).toBe(false); + + const class_slider_count = await page.evaluate(() => + document.querySelectorAll("#confidence-slider-multi-class-mode .confidence-slider-input").length, + ); + expect(class_slider_count).toBeGreaterThan(0); + }); + + test("get_confidence_slider_value includes per-class keys in per-class mode", async ({ page }) => { + await wait_for_ulabel_init(page); + + await page.click("#confidence-slider-multi-checkbox"); + + const value = await page.evaluate(() => window.ulabel.get_confidence_slider_value()); + + expect(value).toHaveProperty("all"); + const class_keys = Object.keys(value).filter((key) => key !== "all"); + expect(class_keys.length).toBeGreaterThan(0); + }); +}); diff --git a/tests/testing-utils/blobs_stub.js b/tests/testing-utils/blobs_stub.js new file mode 100644 index 00000000..0826c628 --- /dev/null +++ b/tests/testing-utils/blobs_stub.js @@ -0,0 +1,5 @@ +// Test stub for the ESM `src/blobs.js` module (SVG icon strings). +// The real file uses ESM `export` syntax which jest does not transform, and the SVG contents are +// irrelevant to unit tests. Any named import resolves to an empty string. See jest.config.js +// `moduleNameMapper`. +module.exports = new Proxy({}, { get: () => "" }); diff --git a/tests/testing-utils/version_stub.js b/tests/testing-utils/version_stub.js new file mode 100644 index 00000000..7a829953 --- /dev/null +++ b/tests/testing-utils/version_stub.js @@ -0,0 +1,4 @@ +// Test stub for the ESM `src/version.js` module. +// The real file uses ESM `export` syntax which jest does not transform, and its value is +// irrelevant to unit tests. See jest.config.js `moduleNameMapper`. +module.exports = { ULABEL_VERSION: "0.0.0-test" }; From f684db30bf39c23547df45e81c6816f4f76739fa Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 14:57:24 -0500 Subject: [PATCH 08/11] Address concerns from review, and simplify confidence threshold type --- api_spec.md | 10 ++-- demo/row-filtering-example.html | 4 +- index.d.ts | 10 ++-- src/configuration.ts | 2 +- src/index.js | 4 +- src/toolbox_items/confidence_slider.ts | 27 +++++----- tests/confidence_slider.test.js | 73 +++++++++++++++++++++----- tests/e2e/slider-api.spec.js | 7 ++- 8 files changed, 88 insertions(+), 49 deletions(-) diff --git a/api_spec.md b/api_spec.md index bd054157..18f8987d 100644 --- a/api_spec.md +++ b/api_spec.md @@ -453,20 +453,16 @@ Any annotation with a confidence at or above the threshold is shown; any below i Configuration object with the following custom definitions: ```javascript -type ConfidenceThreshold = { - confidence: number // Percentage threshold (0-100) -} - type ConfidenceSliderClasses = { - "all": ConfidenceThreshold, // value used by the single global slider - [key: number]?: ConfidenceThreshold // per-class-id values used in per-class mode + "all": number, // percentage threshold (0-100) used by the single global slider + [classId: string]?: number // per-class-id thresholds (class id as a string key) } type ConfidenceSliderConfig = { "name"?: string, // Default: "Confidence Filter" "filter_min"?: number, // Default: 0 (%) "filter_max"?: number, // Default: 100 (%) - "default_values"?: ConfidenceSliderClasses, // Default: {"all": {"confidence": 0}} + "default_values"?: ConfidenceSliderClasses, // Default: {"all": 0} "step_value"?: number, // Default: 1 (%) "class_filter_mode"?: "toggle" | "all-only" | "class-only", // Default: "toggle" "filter_on_load"?: boolean, // Default: true diff --git a/demo/row-filtering-example.html b/demo/row-filtering-example.html index 77db1e0c..76da73fc 100644 --- a/demo/row-filtering-example.html +++ b/demo/row-filtering-example.html @@ -765,9 +765,7 @@ "class_filter_mode": "class-only", "target_class_ids": [10], // Plant only "default_values": { - "all": { - "confidence": 20, // % - } + "all": 20, // % }, }, }); diff --git a/index.d.ts b/index.d.ts index 22814557..bcddb4bf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -119,18 +119,14 @@ export type FilterDistanceConfig = { filter_during_polyline_move?: boolean; }; -export type ConfidenceThreshold = { - confidence: number; // Percentage threshold (0-100) -}; - /** - * Stores the current confidence slider threshold values. + * Stores the current confidence slider threshold values (percentages, 0-100). * The key is the class id. "all" is a special key that stores the threshold applied to * all annotations in single-class mode. */ export type ConfidenceSliderClasses = { - all: ConfidenceThreshold; - [key: string]: ConfidenceThreshold; + all: number; + [key: string]: number; }; /** diff --git a/src/configuration.ts b/src/configuration.ts index 240c5d04..ce10a91b 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -67,7 +67,7 @@ export const DEFAULT_CONFIDENCE_SLIDER_CONFIG: ConfidenceSliderConfig = { name: "Confidence Filter", filter_min: 0, filter_max: 100, - default_values: { all: { confidence: 0 } }, + default_values: { all: 0 }, step_value: 1, class_filter_mode: "toggle", filter_on_load: true, diff --git a/src/index.js b/src/index.js index f3123b28..82a09eb1 100644 --- a/src/index.js +++ b/src/index.js @@ -1054,7 +1054,9 @@ export class ULabel { * are found */ get_confidence_slider_value() { - if (!this.config.toolbox_order.includes(AllowedToolboxItem.ConfidenceSlider)) return null; + // Note: we intentionally don't check `toolbox_order.includes(...)` because toolbox items + // can be configured as [enum, kwargs] tuples, which `includes` would miss. The built + // `toolbox.items` list is the authoritative source of active items. const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "ConfidenceSlider"); if (item === undefined) return null; return item.get_current_values(); diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts index c5d6e6cb..fb0b3a41 100644 --- a/src/toolbox_items/confidence_slider.ts +++ b/src/toolbox_items/confidence_slider.ts @@ -208,17 +208,20 @@ export class ConfidenceSlider extends ToolboxItem { * @returns A map of class identifiers (and "all") to their threshold values */ public get_filter_values(): ConfidenceSliderClasses { - // Seed with defaults so filtering works before the sliders are rendered (e.g. on load) - const values: ConfidenceSliderClasses = { all: { confidence: this.default_values.all.confidence } }; + // Seed with defaults so filtering works before the sliders are rendered (e.g. on load). + // Only seed per-class thresholds for targeted classes (those that get a slider), so that + // filtering stays consistent with what is rendered and reported by get_current_values(). + const values: ConfidenceSliderClasses = { all: this.default_values.all }; for (const key in this.default_values) { if (key === "all") continue; - values[key] = { confidence: this.default_values[key].confidence }; + if (this.target_class_ids !== null && !this.target_class_ids.includes(Number(key))) continue; + values[key] = this.default_values[key]; } // Read the single-class slider if present const all_slider = document.querySelector(`#${this.component_prefix}-all`); if (all_slider !== null) { - values.all = { confidence: all_slider.valueAsNumber }; + values.all = all_slider.valueAsNumber; } // Read the per-class sliders if present @@ -226,7 +229,7 @@ export class ConfidenceSlider extends ToolboxItem { for (let idx = 0; idx < sliders.length; idx++) { const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; if (slider_class_name === "all") continue; - values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + values[slider_class_name] = sliders[idx].valueAsNumber; } return values; @@ -256,12 +259,12 @@ export class ConfidenceSlider extends ToolboxItem { const class_id = Number(get_annotation_class_id(annotation)); if (values[class_id] !== undefined) { const confidence = Math.round(get_annotation_confidence_for_class(annotation, class_id) * 100); - should_deprecate = this.filter_function(confidence, values[class_id].confidence); + should_deprecate = this.filter_function(confidence, values[class_id]); } } else { // In single-class mode, filter all targeted annotations by their highest confidence const confidence = Math.round(this.get_confidence(annotation) * 100); - should_deprecate = this.filter_function(confidence, values.all.confidence); + should_deprecate = this.filter_function(confidence, values.all); } // Mark deprecated and, if the visible state changed, queue a redraw @@ -290,8 +293,8 @@ export class ConfidenceSlider extends ToolboxItem { for (const class_def of class_defs) { const default_value = ( this.default_values[class_def.id] !== undefined ? - this.default_values[class_def.id].confidence : - this.default_values.all.confidence + this.default_values[class_def.id] : + this.default_values.all ).toString(); const slider = new SliderHandler({ @@ -318,7 +321,7 @@ export class ConfidenceSlider extends ToolboxItem { const single_class_slider = new SliderHandler({ id: `${this.component_prefix}-all`, // "all" is extracted using regex class: this.slider_class, - default_value: this.default_values.all.confidence.toString(), + default_value: this.default_values.all.toString(), min: this.filter_min.toString(), max: this.filter_max.toString(), step: this.step_value.toString(), @@ -373,7 +376,7 @@ export class ConfidenceSlider extends ToolboxItem { const all_slider = document.querySelector(`#${this.component_prefix}-all`); if (all_slider === null) return null; - const values: ConfidenceSliderClasses = { all: { confidence: all_slider.valueAsNumber } }; + const values: ConfidenceSliderClasses = { all: all_slider.valueAsNumber }; // In class-only mode, also read the per-class sliders if (this.is_class_mode) { @@ -381,7 +384,7 @@ export class ConfidenceSlider extends ToolboxItem { for (let idx = 0; idx < sliders.length; idx++) { const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; if (slider_class_name === "all") continue; - values[slider_class_name] = { confidence: sliders[idx].valueAsNumber }; + values[slider_class_name] = sliders[idx].valueAsNumber; } } diff --git a/tests/confidence_slider.test.js b/tests/confidence_slider.test.js index f29d0628..da2c2b64 100644 --- a/tests/confidence_slider.test.js +++ b/tests/confidence_slider.test.js @@ -5,6 +5,8 @@ // `confidence_slider` first hits the circular import before `ToolboxItem` is defined. const { Configuration, AllowedToolboxItem } = require("../build/configuration"); const { ConfidenceSlider } = require("../build/toolbox_items/confidence_slider"); +// Loaded from the bundled build to exercise the ULabel public API method directly. +const { ULabel } = require("./testing-utils/build_loader"); /** * Build a minimal annotation-like object. @@ -95,7 +97,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); const ulabel = make_ulabel([high, mid, low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 60 } }, + default_values: { all: 60 }, filter_on_load: false, }); const cs = new ConfidenceSlider(ulabel); @@ -112,7 +114,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); const cs = new ConfidenceSlider(make_ulabel([low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 0 } }, + default_values: { all: 0 }, filter_on_load: false, })); @@ -125,7 +127,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); const cs = new ConfidenceSlider(make_ulabel([low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 50 } }, + default_values: { all: 50 }, filter_on_load: false, })); @@ -133,7 +135,7 @@ describe("ConfidenceSlider", () => { expect(low.deprecated).toBe(true); // Lower the threshold and re-filter - cs.default_values.all.confidence = 0; + cs.default_values.all = 0; cs.filter_annotations(false); expect(low.deprecated).toBe(false); @@ -148,7 +150,7 @@ describe("ConfidenceSlider", () => { const other = make_annotation("other", "bbox", [{ class_id: 12, confidence: 0.5 }]); const cs = new ConfidenceSlider(make_ulabel([car, truck, other], { class_filter_mode: "class-only", - default_values: { all: { confidence: 0 }, 10: { confidence: 60 }, 11: { confidence: 30 } }, + default_values: { all: 0, 10: 60, 11: 30 }, filter_on_load: false, })); @@ -167,7 +169,7 @@ describe("ConfidenceSlider", () => { ]); const cs = new ConfidenceSlider(make_ulabel([anno], { class_filter_mode: "class-only", - default_values: { all: { confidence: 0 }, 10: { confidence: 90 }, 11: { confidence: 50 } }, + default_values: { all: 0, 10: 90, 11: 50 }, filter_on_load: false, })); @@ -176,6 +178,23 @@ describe("ConfidenceSlider", () => { // 80 >= 50 (class-11 threshold) -> shown, even though 80 would fail class 10's 90 expect(anno.deprecated).toBe(false); }); + + test("does not filter classes excluded from target_class_ids, even if present in default_values", () => { + const targeted = make_annotation("targeted", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const untargeted = make_annotation("untargeted", "bbox", [{ class_id: 11, confidence: 0.1 }]); + const cs = new ConfidenceSlider(make_ulabel([targeted, untargeted], { + class_filter_mode: "class-only", + target_class_ids: [10], + // class 11 has a threshold but is NOT targeted (no slider is rendered for it) + default_values: { all: 0, 10: 50, 11: 90 }, + filter_on_load: false, + })); + + cs.filter_annotations(false); + + expect(targeted.deprecated).toBe(true); // class 10 targeted: 10% < 50% + expect(untargeted.deprecated).toBe(false); // class 11 not targeted: must be ignored + }); }); describe("target_spatial_types", () => { @@ -184,7 +203,7 @@ describe("ConfidenceSlider", () => { const box = make_annotation("b", "bbox", [{ class_id: 10, confidence: 0.1 }]); const cs = new ConfidenceSlider(make_ulabel([point, box], { class_filter_mode: "all-only", - default_values: { all: { confidence: 50 } }, + default_values: { all: 50 }, target_spatial_types: ["bbox"], filter_on_load: false, })); @@ -205,7 +224,7 @@ describe("ConfidenceSlider", () => { }); const cs = new ConfidenceSlider(make_ulabel([anno], { class_filter_mode: "all-only", - default_values: { all: { confidence: 0 } }, + default_values: { all: 0 }, filter_on_load: false, })); @@ -221,7 +240,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); new ConfidenceSlider(make_ulabel([low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 50 } }, + default_values: { all: 50 }, filter_on_load: true, })); @@ -232,7 +251,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); new ConfidenceSlider(make_ulabel([low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 50 } }, + default_values: { all: 50 }, filter_on_load: false, })); @@ -245,7 +264,7 @@ describe("ConfidenceSlider", () => { const low = make_annotation("low", "bbox", [{ class_id: 10, confidence: 0.1 }]); const ulabel = make_ulabel([low], { class_filter_mode: "all-only", - default_values: { all: { confidence: 50 } }, + default_values: { all: 50 }, filter_on_load: false, }); const cs = new ConfidenceSlider(ulabel); @@ -267,7 +286,7 @@ describe("ConfidenceSlider", () => { const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false })); document.body.innerHTML = ``; - expect(cs.get_current_values()).toEqual({ all: { confidence: 40 } }); + expect(cs.get_current_values()).toEqual({ all: 40 }); }); test("reads per-class slider values in class mode", () => { @@ -278,8 +297,8 @@ describe("ConfidenceSlider", () => { `; expect(cs.get_current_values()).toEqual({ - all: { confidence: 40 }, - 10: { confidence: 30 }, + all: 40, + 10: 30, }); }); }); @@ -315,3 +334,29 @@ describe("Default toolbox order", () => { expect(config.toolbox_order).not.toContain(AllowedToolboxItem.KeypointSlider); }); }); + +describe("get_confidence_slider_value (tuple-safe lookup)", () => { + const slider_item = { + get_toolbox_item_type: () => "ConfidenceSlider", + get_current_values: () => ({ all: 25 }), + }; + const invoke = (toolbox_order, items) => + ULabel.prototype.get_confidence_slider_value.call({ + config: { toolbox_order }, + toolbox: { items }, + }); + + test("finds the item when configured as a plain enum entry", () => { + expect(invoke([AllowedToolboxItem.ConfidenceSlider], [slider_item])) + .toEqual({ all: 25 }); + }); + + test("finds the item when configured as an [enum, kwargs] tuple", () => { + expect(invoke([[AllowedToolboxItem.ConfidenceSlider, { step_value: 5 }]], [slider_item])) + .toEqual({ all: 25 }); + }); + + test("returns null when the item is not active", () => { + expect(invoke([AllowedToolboxItem.ConfidenceSlider], [])).toBeNull(); + }); +}); diff --git a/tests/e2e/slider-api.spec.js b/tests/e2e/slider-api.spec.js index a5ef9e6b..79a12acf 100644 --- a/tests/e2e/slider-api.spec.js +++ b/tests/e2e/slider-api.spec.js @@ -86,8 +86,7 @@ test.describe("Slider Public API", () => { // Should have the "all" key with the default threshold of 0 expect(value).not.toBeNull(); - expect(value.all).toBeDefined(); - expect(value.all.confidence).toBe(0); + expect(value.all).toBe(0); }); test("should reflect value after moving the slider", async ({ page }) => { @@ -102,7 +101,7 @@ test.describe("Slider Public API", () => { const value = await page.evaluate(() => window.ulabel.get_confidence_slider_value()); - expect(value.all.confidence).toBe(60); + expect(value.all).toBe(60); }); test("should return an object with an all key", async ({ page }) => { @@ -112,7 +111,7 @@ test.describe("Slider Public API", () => { expect(typeof value).toBe("object"); expect(value).toHaveProperty("all"); - expect(typeof value.all.confidence).toBe("number"); + expect(typeof value.all).toBe("number"); }); }); }); From a1a525be855ce3fa78d9ea6b92cab1d3b6da78b1 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 15:16:21 -0500 Subject: [PATCH 09/11] another round of review --- src/annotation_operators.ts | 2 +- src/toolbox_items/confidence_slider.ts | 9 +++++++-- tests/annotation_operators.test.js | 5 +++++ tests/confidence_slider.test.js | 21 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/annotation_operators.ts b/src/annotation_operators.ts index b69ba1fd..2f38da5b 100644 --- a/src/annotation_operators.ts +++ b/src/annotation_operators.ts @@ -38,7 +38,7 @@ export function get_annotation_confidence(annotation: ULabelAnnotation) { * @returns The confidence for the given class id, or -1 if the class id is not present */ export function get_annotation_confidence_for_class(annotation: ULabelAnnotation, class_id: number): number { - for (const payload of annotation.classification_payloads!) { + for (const payload of annotation.classification_payloads ?? []) { if (payload.class_id === class_id) { return payload.confidence; } diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts index fb0b3a41..90d22b3f 100644 --- a/src/toolbox_items/confidence_slider.ts +++ b/src/toolbox_items/confidence_slider.ts @@ -93,7 +93,9 @@ export class ConfidenceSlider extends ToolboxItem { this.filter_min = this.config.filter_min!; this.filter_max = this.config.filter_max!; this.step_value = this.config.step_value!; - this.default_values = this.config.default_values!; + // Always ensure an "all" threshold exists (used by the global slider and as the per-class + // fallback), even if a user supplies default_values with only per-class keys. + this.default_values = { all: 0, ...this.config.default_values }; this.filter_on_load = this.config.filter_on_load!; this.target_spatial_types = this.config.target_spatial_types ?? CONFIDENCE_FILTERABLE_SPATIAL_TYPES; this.target_class_ids = this.config.target_class_ids ?? null; @@ -316,7 +318,10 @@ export class ConfidenceSlider extends ToolboxItem { } public get_html(): string { - const multi_class_html = this.createMultiFilterHTML(); + // Only build per-class sliders when they can actually be shown (class-only or toggle mode). + // In all-only mode the multi-class container is never revealed, so skip the wasted DOM and + // SliderHandler event listeners. + const multi_class_html = (this.is_class_mode || this.show_class_toggle) ? this.createMultiFilterHTML() : ""; const single_class_slider = new SliderHandler({ id: `${this.component_prefix}-all`, // "all" is extracted using regex diff --git a/tests/annotation_operators.test.js b/tests/annotation_operators.test.js index 16845363..dcfa36e2 100644 --- a/tests/annotation_operators.test.js +++ b/tests/annotation_operators.test.js @@ -33,6 +33,11 @@ describe("get_annotation_confidence_for_class", () => { const annotation = make_annotation("bbox", [{ class_id: 1, confidence: 0.9 }]); expect(get_annotation_confidence_for_class(annotation, 99)).toBe(-1); }); + + test("returns -1 (does not throw) when classification_payloads is missing", () => { + expect(get_annotation_confidence_for_class({ classification_payloads: null }, 1)).toBe(-1); + expect(get_annotation_confidence_for_class({}, 1)).toBe(-1); + }); }); describe("get_spatial_annotations_with_confidence", () => { diff --git a/tests/confidence_slider.test.js b/tests/confidence_slider.test.js index da2c2b64..0d120275 100644 --- a/tests/confidence_slider.test.js +++ b/tests/confidence_slider.test.js @@ -88,6 +88,18 @@ describe("ConfidenceSlider", () => { expect(cs.filter_min).toBe(0); // default expect(cs.step_value).toBe(1); // default }); + + test("defaults an 'all' threshold when default_values omits it", () => { + const cs = new ConfidenceSlider(make_ulabel([], { + class_filter_mode: "class-only", + default_values: { 10: 60 }, // no "all" key + filter_on_load: false, + })); + expect(cs.default_values.all).toBe(0); + expect(cs.default_values[10]).toBe(60); + // get_html must not throw when "all" was omitted by the user + expect(() => cs.get_html()).not.toThrow(); + }); }); describe("filter_annotations - all mode", () => { @@ -324,6 +336,15 @@ describe("ConfidenceSlider", () => { expect(html).toContain("confidence-slider-10"); expect(html).not.toContain("confidence-slider-11"); }); + + test("does not render per-class sliders in all-only mode", () => { + const cs = new ConfidenceSlider(make_ulabel([], { class_filter_mode: "all-only", filter_on_load: false })); + const html = cs.get_html(); + + expect(html).toContain("confidence-slider-all"); + expect(html).not.toContain("confidence-slider-10"); + expect(html).not.toContain("confidence-slider-11"); + }); }); }); From 766a1ea3550ee79ee95d24c4172d8bf7f6c15818 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 15:34:26 -0500 Subject: [PATCH 10/11] fix on load bug --- src/toolbox_items/confidence_slider.ts | 17 ++++++++++------- tests/confidence_slider.test.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts index 90d22b3f..48ec4cdd 100644 --- a/src/toolbox_items/confidence_slider.ts +++ b/src/toolbox_items/confidence_slider.ts @@ -211,13 +211,16 @@ export class ConfidenceSlider extends ToolboxItem { */ public get_filter_values(): ConfidenceSliderClasses { // Seed with defaults so filtering works before the sliders are rendered (e.g. on load). - // Only seed per-class thresholds for targeted classes (those that get a slider), so that - // filtering stays consistent with what is rendered and reported by get_current_values(). const values: ConfidenceSliderClasses = { all: this.default_values.all }; - for (const key in this.default_values) { - if (key === "all") continue; - if (this.target_class_ids !== null && !this.target_class_ids.includes(Number(key))) continue; - values[key] = this.default_values[key]; + + // In class mode, seed every targeted class (those that get a slider) with its explicit + // default or the "all" fallback. This matches the rendered slider defaults from + // createMultiFilterHTML(), so class-mode filter-on-load behaves consistently before the + // DOM sliders exist. + if (this.is_class_mode) { + for (const class_def of this.get_target_class_defs()) { + values[class_def.id] = this.default_values[class_def.id] ?? this.default_values.all; + } } // Read the single-class slider if present @@ -226,7 +229,7 @@ export class ConfidenceSlider extends ToolboxItem { values.all = all_slider.valueAsNumber; } - // Read the per-class sliders if present + // Read the per-class sliders if present (override the seeded defaults) const sliders = document.querySelectorAll(`.${this.slider_class}`); for (let idx = 0; idx < sliders.length; idx++) { const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; diff --git a/tests/confidence_slider.test.js b/tests/confidence_slider.test.js index 0d120275..cfc177ae 100644 --- a/tests/confidence_slider.test.js +++ b/tests/confidence_slider.test.js @@ -207,6 +207,21 @@ describe("ConfidenceSlider", () => { expect(targeted.deprecated).toBe(true); // class 10 targeted: 10% < 50% expect(untargeted.deprecated).toBe(false); // class 11 not targeted: must be ignored }); + + test("filters on load using the 'all' fallback when per-class defaults are omitted", () => { + const car = make_annotation("car", "bbox", [{ class_id: 10, confidence: 0.1 }]); + const truck = make_annotation("truck", "bbox", [{ class_id: 11, confidence: 0.1 }]); + // filter_on_load runs during construction, before any DOM sliders exist + new ConfidenceSlider(make_ulabel([car, truck], { + class_filter_mode: "class-only", + default_values: { all: 50 }, // no explicit per-class thresholds + filter_on_load: true, + })); + + // Both classes should be filtered by the "all" fallback (10% < 50%) + expect(car.deprecated).toBe(true); + expect(truck.deprecated).toBe(true); + }); }); describe("target_spatial_types", () => { From 89600ea26b36407b97bf9049c5d806fbfc1e5484 Mon Sep 17 00:00:00 2001 From: TrevorBurgoyne Date: Wed, 22 Jul 2026 16:11:38 -0500 Subject: [PATCH 11/11] fix verbose comments and better handle spatial type list --- .vscode/settings.json | 3 +++ src/actions.ts | 2 -- src/annotation.ts | 17 +++++++++++++++++ src/annotation_operators.ts | 12 +++++------- src/html_builder.ts | 1 - src/index.js | 3 --- src/initializer.ts | 1 - src/toolbox.ts | 1 - src/toolbox_items/confidence_slider.ts | 2 +- src/utilities.ts | 1 - tests/annotation_operators.test.js | 11 +++++++++++ 11 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d59504ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/src/actions.ts b/src/actions.ts index 05d41044..0ff3094a 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -4,8 +4,6 @@ import type { ULabelActionRaw, ULabelActionType, } from "../index"; -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types -// and resolve to ../src/index.js at runtime after compilation import { ULabel } from "../src/index"; import { FilterPointDistanceFromRow } from "./toolbox"; import { AllowedToolboxItem } from "./configuration"; diff --git a/src/annotation.ts b/src/annotation.ts index 8bb496b5..ea291a5e 100644 --- a/src/annotation.ts +++ b/src/annotation.ts @@ -14,6 +14,23 @@ export const DELETE_CLASS_ID = -1; export const MODES_3D = ["global", "bbox3"]; export const NONSPATIAL_MODES = ["whole-image", "global"]; +// A record enumerating every ULabelSpatialType. Adding a member to the union without updating this +// record is a compile error, which keeps ALL_SPATIAL_TYPES in sync with the type. +const SPATIAL_TYPE_SET: Record = { + "contour": true, + "polygon": true, + "polyline": true, + "bbox": true, + "tbar": true, + "bbox3": true, + "whole-image": true, + "global": true, + "point": true, +}; + +// Every ULabelSpatialType (spatial and non-spatial modes) as a runtime array. +export const ALL_SPATIAL_TYPES = Object.keys(SPATIAL_TYPE_SET) as ULabelSpatialType[]; + export type PolygonSpatialData = { // TODO (joshua-dean): validate this type spatial_payload: [number[]][]; diff --git a/src/annotation_operators.ts b/src/annotation_operators.ts index 2f38da5b..287f80fb 100644 --- a/src/annotation_operators.ts +++ b/src/annotation_operators.ts @@ -7,10 +7,9 @@ import type { ValidDeprecatedBy, ClassDefinition, } from "../index"; -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; -import { ULabelAnnotation, DELETE_CLASS_ID } from "./annotation"; +import { ULabelAnnotation, DELETE_CLASS_ID, ALL_SPATIAL_TYPES, NONSPATIAL_MODES } from "./annotation"; import { ULabelSubtask } from "./subtask"; /** @@ -434,12 +433,11 @@ export function get_point_and_line_annotations(ulabel: ULabel): [ULabelAnnotatio } /** - * The spatial annotation types that can be filtered by confidence. - * Excludes the non-spatial modes (`whole-image` and `global`). + * The spatial annotation types that can be filtered by confidence: every spatial type except the + * non-spatial modes (`whole-image` and `global`). */ -export const CONFIDENCE_FILTERABLE_SPATIAL_TYPES: ULabelSpatialType[] = [ - "contour", "polygon", "polyline", "bbox", "tbar", "bbox3", "point", -]; +export const CONFIDENCE_FILTERABLE_SPATIAL_TYPES: ULabelSpatialType[] = + ALL_SPATIAL_TYPES.filter((type) => !NONSPATIAL_MODES.includes(type)); /** * Gathers all spatial annotations across every subtask that have a confidence payload and whose diff --git a/src/html_builder.ts b/src/html_builder.ts index fe2ffd13..29820b9d 100644 --- a/src/html_builder.ts +++ b/src/html_builder.ts @@ -1,5 +1,4 @@ import type { SliderInfo } from "../index"; -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; import { Toolbox, ZoomPanToolboxItem } from "./toolbox"; import { ULABEL_VERSION } from "../src/version"; diff --git a/src/index.js b/src/index.js index 82a09eb1..fb3a4f5e 100644 --- a/src/index.js +++ b/src/index.js @@ -1054,9 +1054,6 @@ export class ULabel { * are found */ get_confidence_slider_value() { - // Note: we intentionally don't check `toolbox_order.includes(...)` because toolbox items - // can be configured as [enum, kwargs] tuples, which `includes` would miss. The built - // `toolbox.items` list is the authoritative source of active items. const item = this.toolbox.items.find((item) => item.get_toolbox_item_type() === "ConfidenceSlider"); if (item === undefined) return null; return item.get_current_values(); diff --git a/src/initializer.ts b/src/initializer.ts index 0422ff2b..4ccc4844 100644 --- a/src/initializer.ts +++ b/src/initializer.ts @@ -4,7 +4,6 @@ * This also includes "staggered" initializers to test loading. */ -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; import { initialize_annotation_canvases } from "./canvas_utils"; import { Configuration } from "./configuration"; diff --git a/src/toolbox.ts b/src/toolbox.ts index 942b575f..54ed3b8a 100644 --- a/src/toolbox.ts +++ b/src/toolbox.ts @@ -3,7 +3,6 @@ import type { FilterDistanceConfig, RecolorActiveConfig, } from "../index"; -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; import { DEFAULT_FILTER_DISTANCE_CONFIG, AllowedToolboxItem } from "./configuration"; import { ULabelAnnotation } from "./annotation"; diff --git a/src/toolbox_items/confidence_slider.ts b/src/toolbox_items/confidence_slider.ts index 48ec4cdd..93c8aa85 100644 --- a/src/toolbox_items/confidence_slider.ts +++ b/src/toolbox_items/confidence_slider.ts @@ -27,7 +27,7 @@ import { * Supports two modes: * - Single-class mode: one slider applies a confidence threshold to all targeted spatial * annotations across every subtask, using each annotation's highest confidence. - * - Multi-class mode: one slider per targeted class id. Each slider filters only the annotations + * - Class mode: one slider per targeted class id. Each slider filters only the annotations * whose assigned (highest-confidence) class matches, using that class's confidence. * * The `class_filter_mode` config controls whether the mode is user-toggleable (`"toggle"`), or diff --git a/src/utilities.ts b/src/utilities.ts index 7388f8ca..0558c5c7 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -2,7 +2,6 @@ * File for storing useful utilities that are not strictly ULabel related. */ -// Import ULabel from ../src/index - TypeScript will find ../src/index.d.ts for types import { ULabel } from "../src/index"; import { ULabelSubtask } from "./subtask"; import { DELETE_CLASS_ID, DELETE_MODES } from "./annotation"; diff --git a/tests/annotation_operators.test.js b/tests/annotation_operators.test.js index dcfa36e2..f56d2c74 100644 --- a/tests/annotation_operators.test.js +++ b/tests/annotation_operators.test.js @@ -4,6 +4,7 @@ const { get_spatial_annotations_with_confidence, findAllClassDefinitions, findAllPolylineClassDefinitions, + CONFIDENCE_FILTERABLE_SPATIAL_TYPES, } = require("../build/annotation_operators"); function make_annotation(spatial_type, classification_payloads, extra = {}) { @@ -40,6 +41,16 @@ describe("get_annotation_confidence_for_class", () => { }); }); +describe("CONFIDENCE_FILTERABLE_SPATIAL_TYPES", () => { + test("includes the spatial modes and excludes the non-spatial modes", () => { + expect(CONFIDENCE_FILTERABLE_SPATIAL_TYPES).toEqual( + expect.arrayContaining(["contour", "polygon", "polyline", "bbox", "tbar", "bbox3", "point"]), + ); + expect(CONFIDENCE_FILTERABLE_SPATIAL_TYPES).not.toContain("whole-image"); + expect(CONFIDENCE_FILTERABLE_SPATIAL_TYPES).not.toContain("global"); + }); +}); + describe("get_spatial_annotations_with_confidence", () => { test("collects spatial annotations with confidence across all subtasks and sets subtask_key", () => { const bbox = make_annotation("bbox", [{ class_id: 1, confidence: 0.8 }], { id: "a" });