From 3e69d98513be563bbf06a80d7b7d4416e36eeb0c Mon Sep 17 00:00:00 2001 From: Adam Romlein <35015390+romleiaj@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:47:30 -0400 Subject: [PATCH 1/6] Percentile stretch Image Enhancement option (#1690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix spelling error, move duplicate filter IDs to a single variable * Add percentile stretch option to 'Image Enhancements' for thermal imagery. * Fix per-camera image enhancement state and persistence Each camera now maintains independent enhancement values. Viewer.vue loads enhancements from each camera's sub-meta instead of root meta, saves to the correct camera-scoped path via a per-camera debounce map, and flushes pending saves before switching cameras. * Fix Vue 2 reactivity, spurious save, and percentileStretch aliasing in per-camera enhancements * Sync image enhancement sliders when a new camera is selected * client/platform/desktop/backend/media/ * Fix seven correctness and quality issues in display processing backend Float32/Float64 TIFFs now use sort-based percentile instead of the integer histogram, which collapsed sub-integer values to a single bin and produced solid-color output silently. Uint8/Uint16 bin counts are now type-matched (256 vs 65536). Zero-range frames (flat field, dead sensor) now render as mid-gray; the previous check tested only raw[0] against min, so every uniform frame filled with black. Cache keys now include mtimeMs so entries are invalidated when a source file is replaced at the same path, matching the tile cache pattern. fs.statSync replaced with await fs.stat to avoid blocking the event loop on a per-frame hot path. The two try-catch blocks collapsed into one with ENOENT distinguished for a 404. Array.isArray guards added for low and high query params; duplicate params previously passed validation and silently discarded extra values. A low >= high check catches inverted percentile inputs at the boundary. encodePngRgba deduplicated — exported from displayProcessing.ts and imported by geotiffTiles.ts, removing the unused pngjs import. * Wire percentile stretch UI: toggle/sliders in ImageEnhancements, display URL remapping in Viewer * fix: percentile slider bounds, signed-16 histogram, per-camera URL debounce, stale isDefaultImage, TIFF lazy load, and in-flight decode dedup * fix: derive display URL origin from rawUrl to bypass Vite proxy in Electron * fix: stretch original TIFF via dataset id+frame, pass through non-TIFF originals * Reuse the existing viewer when imageData URLs are swapped in place The imageData watcher rebuilt the whole viewer via initializeViewer, which replaces the geojs map and orphans every feature layer and event handler other components attached to it (LayerManager annotation layers crash with 'null.api' on redraw; calibration click handlers go dead). An imageData change on a mounted annotator is a display-URL swap for the same camera (percentile stretch), so rebuild only the image cache and redraw the current frame on the existing map; drawImage already handles dimension changes via resetMapDimensions. Co-Authored-By: Claude Fable 5 * Linting * Change stretch to 0-1 on the low end, 99-100 on the high end, more realistic for IR data. * disable percentile stretch on unsupported imagery * add histogram viewer to percentile stretch * web support for percentile stretching * updating docs about image enhancements * support for multicamera IR/EO importing from parent folder for electron and web --------- Co-authored-by: Claude Fable 5 Co-authored-by: Bryon Lewis --- client/dive-common/apispec.ts | 13 +- .../components/ControlsContainer.vue | 2 +- .../ImportMultiCamAddType.vue | 4 +- .../ImportMultiCamDialog.vue | 2 +- .../ImportMultiCamSubfolders.vue | 5 +- .../multicamSubfolderLayout.spec.ts | 118 +++++- .../multicamSubfolderLayout.ts | 123 ++++++- .../useImportMultiCamDialog.ts | 48 ++- client/dive-common/components/Viewer.vue | 344 ++++++++++++++++-- client/package-lock.json | 18 - .../backend/media/displayProcessing.ts | 307 ++++++++++++++++ .../platform/desktop/backend/native/common.ts | 87 ++++- client/platform/desktop/backend/server.ts | 114 ++++++ .../desktop/backend/tiles/geotiffTiles.ts | 30 +- client/platform/web-girder/App.vue | 2 + .../web-girder/api/dataset.service.ts | 6 +- .../platform/web-girder/api/girder.service.ts | 4 +- .../web-girder/api/largeImage.service.ts | 21 ++ .../api/waitForFolderDatasetReady.ts | 73 +++- client/platform/web-girder/views/Upload.vue | 20 +- client/src/components/ImageEnhancements.vue | 251 ++++++++++++- client/src/components/LayerManager.vue | 25 +- .../components/annotators/ImageAnnotator.vue | 83 +++-- .../annotators/LargeImageAnnotator.vue | 93 ++++- .../components/annotators/VideoAnnotator.vue | 10 +- .../annotators/useMediaController.ts | 13 +- client/src/layers/BaseLayer.ts | 3 + client/src/provides.ts | 47 ++- client/src/use/useImageEnhancements.ts | 292 +++++++++++++-- docs/Annotation-User-Interface-Overview.md | 4 +- docs/DataFormats.md | 6 +- docs/Large-Image-Support.md | 15 +- docs/UI-Image-Enhancements.md | 70 ++++ docs/UI-Navigation-Editing-Bar.md | 2 +- docs/index.md | 3 +- mkdocs.yml | 3 +- server/README.md | 3 +- server/dive_server/crud_dataset.py | 35 +- server/dive_utils/constants.py | 1 + server/tests/test_validate_files.py | 29 ++ 40 files changed, 2099 insertions(+), 230 deletions(-) create mode 100644 client/platform/desktop/backend/media/displayProcessing.ts create mode 100644 docs/UI-Image-Enhancements.md create mode 100644 server/tests/test_validate_files.py diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts index 78c40b8ec..e8cba0276 100644 --- a/client/dive-common/apispec.ts +++ b/client/dive-common/apispec.ts @@ -8,6 +8,7 @@ import { Attribute } from 'vue-media-annotator/use/AttributeTypes'; import { CustomStyle } from 'vue-media-annotator/StyleManager'; import { AttributeTrackFilter } from 'vue-media-annotator/AttributeTrackFilterControls'; import { ImageEnhancements } from 'vue-media-annotator/use/useImageEnhancements'; +import type { PercentileStretch } from 'vue-media-annotator/use/useImageEnhancements'; type DatasetType = 'image-sequence' | 'video' | 'multi' | 'large-image'; type MultiTrackRecord = Record; @@ -133,9 +134,11 @@ export interface MultiCamImportFolderArgs { sourceList: Record; // path/track file per camera calibrationFile?: string; // NPZ calibation matrix file - type: 'image-sequence' | 'video'; + type: 'image-sequence' | 'video' | 'large-image'; } export interface MultiCamImportKeywordArgs { @@ -297,6 +300,12 @@ interface Api { // eslint-disable-next-line @typescript-eslint/no-explicit-any getTileURL?(itemId: string, x: number, y: number, level: number, query: Record): string; + getTileHistogram?(itemId: string, options?: { + bins?: number; + frame?: number; + width?: number; + height?: number; + }): Promise; importAnnotationFile(id: string, path: string, file?: File, additive?: boolean, additivePrepend?: string, set?: string): Promise; // Desktop-only calibration persistence functions @@ -527,3 +536,5 @@ export { MultiCamMedia, MediaImportResponse, }; + +export type { PercentileStretch }; diff --git a/client/dive-common/components/ControlsContainer.vue b/client/dive-common/components/ControlsContainer.vue index c0f135df7..1e547658b 100644 --- a/client/dive-common/components/ControlsContainer.vue +++ b/client/dive-common/components/ControlsContainer.vue @@ -322,7 +322,7 @@ export default defineComponent({