Skip to content

Commit 88f012c

Browse files
authored
Merge pull request #738 from PaulHax/wl-while-streaming
fix(windowing): no auto window level if user set already
2 parents 086b055 + 4cc449e commit 88f012c

14 files changed

Lines changed: 472 additions & 286 deletions

File tree

src/components/tools/windowing/WindowLevelControls.vue

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script lang="ts">
22
import { computed, defineComponent, ref } from 'vue';
33
import { useCurrentImage } from '@/src/composables/useCurrentImage';
4-
import useWindowingStore, {
4+
import {
5+
useWindowingStore,
56
defaultWindowLevelConfig,
67
} from '@/src/store/view-configs/windowing';
78
import { useViewStore } from '@/src/store/views';
8-
import { WLAutoRanges, WLPresetsCT, WL_AUTO_DEFAULT } from '@/src/constants';
9+
import { WLAutoRanges, WLPresetsCT } from '@/src/constants';
910
import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom';
1011
import { isDicomImage } from '@/src/utils/dataSelection';
1112
@@ -16,12 +17,13 @@ export default defineComponent({
1617
const viewStore = useViewStore();
1718
const dicomStore = useDICOMStore();
1819
const panel = ref(['tags', 'presets', 'auto']);
19-
const windowingDefaults = defaultWindowLevelConfig();
2020
2121
// Get the relevant view ids
2222
const viewIDs = computed(() =>
2323
viewStore.viewIDs.filter(
24-
(viewID) => !!windowingStore.getConfig(viewID, currentImageID.value)
24+
(viewID) =>
25+
currentImageID.value &&
26+
!!windowingStore.getConfig(viewID, currentImageID.value)
2527
)
2628
);
2729
@@ -48,10 +50,6 @@ export default defineComponent({
4850
return modality.value && ctTags.includes(modality.value.toLowerCase());
4951
});
5052
51-
const wlDefaults = computed(() => {
52-
return { width: windowingDefaults.width, level: windowingDefaults.level };
53-
});
54-
5553
// --- UI Selection Management --- //
5654
5755
type AutoRangeKey = keyof typeof WLAutoRanges;
@@ -61,40 +59,54 @@ export default defineComponent({
6159
// All views will have the same settings, just grab the first
6260
const viewID = viewIDs.value[0];
6361
const imageID = currentImageID.value;
64-
if (!imageID || !viewID) return windowingDefaults;
65-
return windowingStore.getConfig(viewID, imageID);
62+
if (!imageID || !viewID) return defaultWindowLevelConfig();
63+
return (
64+
windowingStore.getConfig(viewID, imageID)?.value ??
65+
defaultWindowLevelConfig()
66+
);
6667
});
6768
68-
const wlWidth = computed(
69-
() => wlConfig.value?.width ?? wlDefaults.value.width
70-
);
71-
const wlLevel = computed(
72-
() => wlConfig.value?.level ?? wlDefaults.value.level
73-
);
69+
const wlWidth = computed(() => wlConfig.value.width);
70+
const wlLevel = computed(() => wlConfig.value.level);
7471
7572
const wlOptions = computed({
7673
get() {
7774
const config = wlConfig.value;
78-
const { width, level } = config?.preset || wlDefaults.value;
79-
const { width: defaultWidth, level: defaultLevel } = wlDefaults.value;
80-
if (width !== defaultWidth && level !== defaultLevel) {
81-
return { width: wlWidth.value, level: wlLevel.value };
75+
if (config.useAuto) {
76+
return config.auto;
8277
}
83-
return config?.auto || WL_AUTO_DEFAULT;
78+
// Otherwise, a specific W/L is active (from preset or manual adjustment).
79+
return { width: wlWidth.value, level: wlLevel.value };
8480
},
8581
set(selection: AutoRangeKey | PresetValue) {
8682
const imageID = currentImageID.value;
8783
// All views will be synchronized, just set the first
8884
const viewID = viewIDs.value[0];
89-
if (imageID && viewID) {
90-
const useAuto = typeof selection !== 'object';
91-
const newValue = {
92-
preset: useAuto ? wlDefaults.value : selection,
93-
auto: useAuto ? selection : WL_AUTO_DEFAULT,
94-
};
95-
windowingStore.updateConfig(viewID, imageID, newValue);
96-
windowingStore.resetWindowLevel(viewID, imageID);
85+
if (!imageID || !viewID) {
86+
return;
9787
}
88+
89+
if (typeof selection === 'object') {
90+
windowingStore.updateConfig(
91+
viewID,
92+
imageID,
93+
{
94+
width: selection.width,
95+
level: selection.level,
96+
},
97+
true
98+
);
99+
return;
100+
}
101+
102+
windowingStore.updateConfig(
103+
viewID,
104+
imageID,
105+
{
106+
auto: selection,
107+
},
108+
true
109+
);
98110
},
99111
});
100112

src/composables/useWindowingConfig.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
1-
import useWindowingStore from '@/src/store/view-configs/windowing';
1+
import { useWindowingStore } from '@/src/store/view-configs/windowing';
22
import { Maybe } from '@/src/types';
33
import type { Vector2 } from '@kitware/vtk.js/types';
44
import { MaybeRef, unref, computed } from 'vue';
5+
import { useImageStatsStore } from '@/src/store/image-stats';
56

67
export function useWindowingConfig(
78
viewID: MaybeRef<string>,
89
imageID: MaybeRef<Maybe<string>>
910
) {
1011
const store = useWindowingStore();
11-
const config = computed(() => store.getConfig(unref(viewID), unref(imageID)));
12+
const imageStatsStore = useImageStatsStore();
13+
const config = computed(() => {
14+
const imageIdVal = unref(imageID);
15+
if (!imageIdVal) return undefined;
16+
const viewIdVal = unref(viewID);
17+
if (!viewIdVal) return undefined;
18+
return store.getConfig(viewIdVal, imageIdVal).value;
19+
});
1220

1321
const generateComputed = (prop: 'width' | 'level') => {
1422
return computed({
15-
get: () => config.value?.[prop] ?? 0,
23+
get: () => {
24+
return config.value?.[prop] ?? 0;
25+
},
1626
set: (val) => {
1727
const imageIdVal = unref(imageID);
1828
if (!imageIdVal || val == null) return;
19-
store.updateConfig(unref(viewID), imageIdVal, { [prop]: val });
29+
store.updateConfig(unref(viewID), imageIdVal, { [prop]: val }, true);
2030
},
2131
});
2232
};
2333

2434
const range = computed((): Vector2 => {
25-
const { min, max } = config.value ?? {};
26-
if (min == null || max == null) return [0, 1];
27-
return [min, max];
35+
const imageIdVal = unref(imageID);
36+
if (!imageIdVal) return [0, 1];
37+
const stats = imageStatsStore.stats[imageIdVal];
38+
if (!stats || stats.scalarMin == null || stats.scalarMax == null)
39+
return [0, 1];
40+
return [stats.scalarMin, stats.scalarMax];
2841
});
2942

3043
return {
Lines changed: 23 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,12 @@
1-
import { MaybeRef, computed, unref, watch } from 'vue';
2-
import * as Comlink from 'comlink';
3-
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
4-
import { computedAsync, watchImmediate } from '@vueuse/core';
1+
import { MaybeRef, computed, unref } from 'vue';
2+
import { watchImmediate } from '@vueuse/core';
53
import { useImage } from '@/src/composables/useCurrentImage';
6-
import { useWindowingConfig } from '@/src/composables/useWindowingConfig';
7-
import { WLAutoRanges, WL_AUTO_DEFAULT, WL_HIST_BINS } from '@/src/constants';
84
import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom';
9-
import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef';
10-
import useWindowingStore from '@/src/store/view-configs/windowing';
5+
import { useWindowingStore } from '@/src/store/view-configs/windowing';
116
import { Maybe } from '@/src/types';
127
import { useResetViewsEvents } from '@/src/components/tools/ResetViews.vue';
138
import { isDicomImage } from '@/src/utils/dataSelection';
14-
import { HistogramWorker } from '@/src/utils/histogram.worker';
15-
16-
function useAutoRangeValues(imageID: MaybeRef<Maybe<string>>) {
17-
const { imageData, isLoading: isImageLoading } = useImage(imageID);
18-
19-
const worker = Comlink.wrap<HistogramWorker>(
20-
new Worker(new URL('@/src/utils/histogram.worker.ts', import.meta.url), {
21-
type: 'module',
22-
})
23-
);
24-
25-
const scalars = vtkFieldRef(
26-
computed(() => imageData.value?.getPointData()),
27-
'scalars'
28-
);
29-
30-
const autoRangeValues = computedAsync(async () => {
31-
if (isImageLoading.value || !scalars.value) {
32-
return {};
33-
}
34-
35-
// Pre-compute the auto-range values
36-
const scalarData = scalars.value.getData();
37-
// Assumes all data is one component
38-
const { min, max } = vtkDataArray.fastComputeRange(
39-
scalarData as number[],
40-
0,
41-
1
42-
);
43-
const hist = await worker.histogram(scalarData, [min, max], WL_HIST_BINS);
44-
const cumm = hist.reduce((acc, val, idx) => {
45-
const prev = idx !== 0 ? acc[idx - 1] : 0;
46-
acc.push(val + prev);
47-
return acc;
48-
}, []);
49-
50-
const width = (max - min + 1) / WL_HIST_BINS;
51-
return Object.fromEntries(
52-
Object.entries(WLAutoRanges).map(([key, value]) => {
53-
const startIdx = cumm.findIndex(
54-
(v: number) => v >= value * 0.01 * scalarData.length
55-
);
56-
const endIdx = cumm.findIndex(
57-
(v: number) => v >= (1 - value * 0.01) * scalarData.length
58-
);
59-
const start = Math.max(min, min + width * startIdx);
60-
const end = Math.min(max, min + width * endIdx + width);
61-
return [key, [start, end]];
62-
})
63-
);
64-
}, {});
65-
66-
return { autoRangeValues };
67-
}
9+
import { WL_AUTO_DEFAULT } from '../constants';
6810

6911
export function useWindowingConfigInitializer(
7012
viewID: MaybeRef<string>,
@@ -73,18 +15,7 @@ export function useWindowingConfigInitializer(
7315
const { imageData } = useImage(imageID);
7416
const dicomStore = useDICOMStore();
7517

76-
const scalarRange = vtkFieldRef(
77-
computed(() => imageData.value?.getPointData()?.getScalars()),
78-
'range'
79-
);
80-
8118
const store = useWindowingStore();
82-
const { config: windowConfig } = useWindowingConfig(viewID, imageID);
83-
const { autoRangeValues } = useAutoRangeValues(imageID);
84-
const autoRange = computed<keyof typeof WLAutoRanges>(
85-
() => windowConfig.value?.auto || WL_AUTO_DEFAULT
86-
);
87-
8819
const firstTag = computed(() => {
8920
const id = unref(imageID);
9021
if (id && isDicomImage(id)) {
@@ -96,94 +27,52 @@ export function useWindowingConfigInitializer(
9627
return undefined;
9728
});
9829

99-
watchImmediate(windowConfig, (config) => {
100-
const image = imageData.value;
101-
const imageIdVal = unref(imageID);
102-
const viewIdVal = unref(viewID);
103-
if (config || !image || !imageIdVal) return;
104-
105-
const [min, max] = image.getPointData().getScalars().getRange();
106-
store.updateConfig(viewIdVal, imageIdVal, { min, max });
107-
store.resetWindowLevel(viewIdVal, imageIdVal);
108-
});
109-
110-
watchImmediate(scalarRange, (range) => {
111-
const imageIdVal = unref(imageID);
112-
const viewIdVal = unref(viewID);
113-
if (!range || !imageIdVal || !viewIdVal) return;
114-
store.updateConfig(viewIdVal, imageIdVal, { min: range[0], max: range[1] });
115-
});
116-
117-
function updateConfigFromAutoRangeValues() {
30+
function resetWidthLevel() {
11831
const imageIdVal = unref(imageID);
11932
const viewIdVal = unref(viewID);
12033
if (imageIdVal == null) {
12134
return;
12235
}
12336

124-
if (autoRange.value in autoRangeValues.value) {
125-
const [min, max] = autoRangeValues.value[autoRange.value];
126-
store.updateConfig(viewIdVal, imageIdVal, {
127-
min,
128-
max,
129-
});
130-
}
37+
store.updateConfig(viewIdVal, imageIdVal, {
38+
auto: WL_AUTO_DEFAULT,
39+
});
13140

13241
const firstTagVal = unref(firstTag);
13342
if (firstTagVal?.width) {
13443
store.updateConfig(viewIdVal, imageIdVal, {
135-
preset: {
136-
width: firstTagVal.width,
137-
level: firstTagVal.level,
138-
},
44+
width: firstTagVal.width,
45+
level: firstTagVal.level,
13946
});
14047
}
14148

142-
const forcedWL = store.runtimeConfigWindowLevel;
143-
if (forcedWL) {
49+
const widthLevel = store.runtimeConfigWindowLevel;
50+
if (widthLevel) {
14451
store.updateConfig(viewIdVal, imageIdVal, {
145-
preset: {
146-
...forcedWL,
147-
},
52+
...widthLevel,
14853
});
14954
}
150-
151-
store.resetWindowLevel(viewIdVal, imageIdVal);
15255
}
15356

15457
watchImmediate(
155-
[imageData, autoRangeValues],
58+
[imageData],
15659
() => {
157-
if (!imageData.value) {
60+
const imageIdValue = unref(imageID);
61+
if (!imageData.value || !imageIdValue) {
15862
return;
15963
}
16064

161-
updateConfigFromAutoRangeValues();
65+
const config = store.getConfig(unref(viewID), imageIdValue).value;
66+
if (config?.userTriggered) {
67+
return;
68+
}
69+
70+
resetWidthLevel();
16271
},
16372
{ deep: true }
16473
);
16574

166-
watch(autoRange, (percentile) => {
167-
const image = imageData.value;
168-
const imageIdVal = unref(imageID);
169-
const viewIdVal = unref(viewID);
170-
if (imageIdVal == null || windowConfig.value == null || !image) {
171-
return;
172-
}
173-
const range = autoRangeValues.value[percentile];
174-
store.updateConfig(viewIdVal, imageIdVal, {
175-
min: range[0],
176-
max: range[1],
177-
});
178-
store.resetWindowLevel(viewIdVal, imageIdVal);
179-
});
180-
18175
useResetViewsEvents().onClick(() => {
182-
const imageIdVal = unref(imageID);
183-
const viewIdVal = unref(viewID);
184-
if (imageIdVal == null || windowConfig.value == null) {
185-
return;
186-
}
187-
store.resetWindowLevel(viewIdVal, imageIdVal);
76+
resetWidthLevel();
18877
});
18978
}

src/core/streaming/dicomChunkImage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ export default class DicomChunkImage
363363
const newMin = rangeAlreadyInitialized ? Math.min(min, curRange[0]) : min;
364364
const newMax = rangeAlreadyInitialized ? Math.max(max, curRange[1]) : max;
365365
scalars.setRange({ min: newMin, max: newMax }, comp);
366+
scalars.modified(); // so image-stats will trigger update of range
366367
}
367368

368369
chunk.setUserData(DATA_RANGE_KEY, chunkDataRange);

0 commit comments

Comments
 (0)