From 6658f90e04a4ed2431a6451a436c4bc26e110124 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Sat, 13 Apr 2024 17:28:51 -0400 Subject: [PATCH 1/2] fix(windowing): more robust loading of multi-component DICOMs --- .../tools/windowing/WindowLevelControls.vue | 13 +---- .../useWindowingConfigInitializer.ts | 58 ++++++++++++++----- src/store/datasets-dicom.ts | 21 +++++++ 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/components/tools/windowing/WindowLevelControls.vue b/src/components/tools/windowing/WindowLevelControls.vue index 4a01d5f2b..0f0c41317 100644 --- a/src/components/tools/windowing/WindowLevelControls.vue +++ b/src/components/tools/windowing/WindowLevelControls.vue @@ -6,7 +6,7 @@ import useWindowingStore, { } from '@/src/store/view-configs/windowing'; import { useViewStore } from '@/src/store/views'; import { WLAutoRanges, WLPresetsCT, WL_AUTO_DEFAULT } from '@/src/constants'; -import { useDICOMStore } from '@/src/store/datasets-dicom'; +import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom'; export default defineComponent({ setup() { @@ -97,22 +97,13 @@ export default defineComponent({ }); // --- Tag WL Options --- // - - function parseTags(text: string) { - return text.split('\\'); - } - const tags = computed(() => { if ( currentImageID.value && currentImageID.value in dicomStore.imageIDToVolumeKey ) { const volKey = dicomStore.imageIDToVolumeKey[currentImageID.value]; - const { WindowWidth, WindowLevel } = dicomStore.volumeInfo[volKey]; - const levels = parseTags(WindowLevel); - return parseTags(WindowWidth).map((val, idx) => { - return { width: parseFloat(val), level: parseFloat(levels[idx]) }; - }); + return getWindowLevels(dicomStore.volumeInfo[volKey]); } return []; }); diff --git a/src/composables/useWindowingConfigInitializer.ts b/src/composables/useWindowingConfigInitializer.ts index 7174e9802..461acdc0c 100644 --- a/src/composables/useWindowingConfigInitializer.ts +++ b/src/composables/useWindowingConfigInitializer.ts @@ -1,13 +1,42 @@ import { useImage } from '@/src/composables/useCurrentImage'; import { useWindowingConfig } from '@/src/composables/useWindowingConfig'; import { WLAutoRanges, WL_AUTO_DEFAULT, WL_HIST_BINS } from '@/src/constants'; -import { useDICOMStore } from '@/src/store/datasets-dicom'; +import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom'; import useWindowingStore from '@/src/store/view-configs/windowing'; import { Maybe } from '@/src/types'; import type { TypedArray } from '@kitware/vtk.js/types'; import { watchImmediate } from '@vueuse/core'; import { MaybeRef, computed, unref, watch } from 'vue'; +// Original source from https://www.npmjs.com/package/compute-range and vtkDataArray +// Modified to assume one component array +function fastComputeRange(arr: number[] | TypedArray) { + const len = arr.length; + let min = Number.MAX_VALUE; + let max = -Number.MAX_VALUE; + let x; + let i; + + // find first non-NaN value + for (i = 0; i < len; i++) { + if (!Number.isNaN(arr[i])) { + min = arr[i]; + max = min; + break; + } + } + + for (; i < len; i++) { + x = arr[i]; + if (x < min) { + min = x; + } else if (x > max) { + max = x; + } + } + return { min, max }; +} + function useAutoRangeValues(imageID: MaybeRef>) { const { imageData } = useImage(imageID); @@ -30,7 +59,8 @@ function useAutoRangeValues(imageID: MaybeRef>) { // Pre-compute the auto-range values const scalarData = imageData.value.getPointData().getScalars(); - const [min, max] = scalarData.getRange(); + // Assumes all data is one component + const { min, max } = fastComputeRange(scalarData.getData()); const hist = histogram(scalarData.getData(), [min, max], WL_HIST_BINS); const cumm = hist.reduce((acc, val, idx) => { const prev = idx !== 0 ? acc[idx - 1] : 0; @@ -75,13 +105,12 @@ export function useWindowingConfigInitializer( const id = unref(imageID); if (id && id in dicomStore.imageIDToVolumeKey) { const volKey = dicomStore.imageIDToVolumeKey[id]; - const { WindowWidth, WindowLevel } = dicomStore.volumeInfo[volKey]; - return { - width: WindowWidth.split('\\')[0], - level: WindowLevel.split('\\')[0], - }; + const windowLevels = getWindowLevels(dicomStore.volumeInfo[volKey]); + if (windowLevels.length) { + return windowLevels[0]; + } } - return {}; + return undefined; }); watchImmediate(windowConfig, (config) => { @@ -103,16 +132,17 @@ export function useWindowingConfigInitializer( return; } - const range = autoRangeValues.value[autoRange.value]; + const [min, max] = autoRangeValues.value[autoRange.value]; store.updateConfig(viewIdVal, imageIdVal, { - min: range[0], - max: range[1], + min, + max, }); - if (firstTag.value?.width) { + const firstTagVal = unref(firstTag); + if (firstTagVal?.width) { store.updateConfig(viewIdVal, imageIdVal, { preset: { - width: parseFloat(firstTag.value.width), - level: parseFloat(firstTag.value.level), + width: firstTagVal.width, + level: firstTagVal.level, }, }); } diff --git a/src/store/datasets-dicom.ts b/src/store/datasets-dicom.ts index 7f05daae5..00f446e95 100644 --- a/src/store/datasets-dicom.ts +++ b/src/store/datasets-dicom.ts @@ -117,6 +117,27 @@ export const getDisplayName = (info: VolumeInfo) => { ); }; +export const getWindowLevels = (info: VolumeInfo) => { + const { WindowWidth, WindowLevel } = info; + if (WindowWidth === '') return []; // missing tag + const widths = WindowWidth.split('\\').map(parseFloat); + const levels = WindowLevel.split('\\').map(parseFloat); + if ( + widths.some((w) => Number.isNaN(w)) || + levels.some((l) => Number.isNaN(l)) + ) { + console.error('Invalid WindowWidth or WindowLevel DICOM tags'); + return []; + } + if (widths.length !== levels.length) { + console.error( + 'Different numbers of WindowWidth and WindowLevel DICOM tags' + ); + return []; + } + return widths.map((width, i) => ({ width, level: levels[i] })); +}; + export const useDICOMStore = defineStore('dicom', { state: (): State => ({ sliceData: {}, From 4bb28a51d0f8d1a1de6e2eff9b115c5ff776911e Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Fri, 26 Apr 2024 11:26:32 -0400 Subject: [PATCH 2/2] fix(useWindowingConfigInitializer): reuse vtk fastComputeRange --- .../useWindowingConfigInitializer.ts | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/composables/useWindowingConfigInitializer.ts b/src/composables/useWindowingConfigInitializer.ts index 461acdc0c..32971db85 100644 --- a/src/composables/useWindowingConfigInitializer.ts +++ b/src/composables/useWindowingConfigInitializer.ts @@ -1,42 +1,14 @@ +import type { TypedArray } from '@kitware/vtk.js/types'; +import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; +import { watchImmediate } from '@vueuse/core'; import { useImage } from '@/src/composables/useCurrentImage'; import { useWindowingConfig } from '@/src/composables/useWindowingConfig'; import { WLAutoRanges, WL_AUTO_DEFAULT, WL_HIST_BINS } from '@/src/constants'; import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom'; import useWindowingStore from '@/src/store/view-configs/windowing'; import { Maybe } from '@/src/types'; -import type { TypedArray } from '@kitware/vtk.js/types'; -import { watchImmediate } from '@vueuse/core'; import { MaybeRef, computed, unref, watch } from 'vue'; -// Original source from https://www.npmjs.com/package/compute-range and vtkDataArray -// Modified to assume one component array -function fastComputeRange(arr: number[] | TypedArray) { - const len = arr.length; - let min = Number.MAX_VALUE; - let max = -Number.MAX_VALUE; - let x; - let i; - - // find first non-NaN value - for (i = 0; i < len; i++) { - if (!Number.isNaN(arr[i])) { - min = arr[i]; - max = min; - break; - } - } - - for (; i < len; i++) { - x = arr[i]; - if (x < min) { - min = x; - } else if (x > max) { - max = x; - } - } - return { min, max }; -} - function useAutoRangeValues(imageID: MaybeRef>) { const { imageData } = useImage(imageID); @@ -60,7 +32,11 @@ function useAutoRangeValues(imageID: MaybeRef>) { // Pre-compute the auto-range values const scalarData = imageData.value.getPointData().getScalars(); // Assumes all data is one component - const { min, max } = fastComputeRange(scalarData.getData()); + const { min, max } = vtkDataArray.fastComputeRange( + scalarData.getData() as number[], + 0, + 1 + ); const hist = histogram(scalarData.getData(), [min, max], WL_HIST_BINS); const cumm = hist.reduce((acc, val, idx) => { const prev = idx !== 0 ? acc[idx - 1] : 0;