Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"build": "vue-tsc --noEmit && vite build",
"build:analyze": "cross-env ANALYZE_BUNDLE=1 npm run build",
"test": "vitest",
"test:e2e:chrome": "npm run build && wdio run ./wdio.chrome.conf.ts",
"test:e2e:chrome": "cross-env VITE_SHOW_SAMPLE_DATA=true npm run build && wdio run ./wdio.chrome.conf.ts",
"test:e2e:chrome:skip-build": "wdio run ./wdio.chrome.conf.ts",
"test:e2e:dev": "concurrently -P \"npm run dev\" \"wdio run ./wdio.dev.conf.ts --watch {@}\"",
"test:e2e:dev": "cross-env VITE_SHOW_SAMPLE_DATA=true concurrently -P \"npm run dev\" \"wdio run ./wdio.dev.conf.ts --watch {@}\"",
"lint": "vue-tsc --noEmit && eslint \"src/**/*.{js,ts,vue}\" \"tests/**/*.{js,ts}\"",
"build:all": "npm run build:dicom && npm run build:resample && npm run build",
"build:dicom": "itk-wasm -s src/io/itk-dicom/ build ",
Expand Down
61 changes: 30 additions & 31 deletions src/store/view-configs/windowing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { markRaw, reactive, ref } from 'vue';
import { createEventHook } from '@vueuse/core';
import {
DoubleKeyRecord,
deleteSecondKey,
Expand All @@ -14,13 +15,17 @@ import { ViewConfig } from '@/src/io/state-file/schema';
import { WindowLevelConfig } from '@/src/store/view-configs/types';
import { isDicomImage } from '@/src/utils/dataSelection';
import { getWindowLevels, useDICOMStore } from '@/src/store/datasets-dicom';
import { createEventHook } from '@vueuse/core';

type WindowLevel = {
width: number;
level: number;
};

const minMaxToWidthLevel = (min: number, max: number) => ({
width: max - min,
level: (max + min) / 2,
});

export const defaultWindowLevelConfig = () =>
({
width: 1,
Expand All @@ -39,39 +44,34 @@ export const useWindowingStore = defineStore('windowing', () => {

const WindowingUpdateEvent = markRaw(createEventHook<[string, string]>());

const computeDefaultConfig = (dataID: string): WindowLevelConfig => {
const getDicomWindowLevel = (dataID: string) => {
if (!isDicomImage(dataID)) return undefined;
const wls = getWindowLevels(dicomStore.volumeInfo[dataID]);
return wls[0];
};

const getStatsWindowLevel = (dataID: string) => {
const stats = imageStatsStore.stats[dataID];
const min = stats?.scalarMin ?? 0;
const max = stats?.scalarMax ?? 1;
return minMaxToWidthLevel(min, max);
};

const computeDefaultConfig = (dataID: string) => {
const defaults = defaultWindowLevelConfig();
let { width, level } = defaults;
let useAuto = false;

if (isDicomImage(dataID)) {
const wls = getWindowLevels(dicomStore.volumeInfo[dataID]);
const wl = wls[0];
if (wl) {
({ width, level } = wl);
}
} else {
// use FullRange auto values
useAuto = true;

// rely on the scalarMin+Max for now to prevent a flash of white
const stats = imageStatsStore.stats[dataID];
const min = stats?.scalarMin ?? 0;
const max = stats?.scalarMax ?? 1;
width = max - min;
level = (max + min) / 2;

const runtimeWL = runtimeConfigWindowLevel.value;
if (runtimeWL) {
return { ...defaults, ...runtimeWL, useAuto: false };
}

if (runtimeConfigWindowLevel.value) {
({ width, level } = runtimeConfigWindowLevel.value);
const dicomWL = getDicomWindowLevel(dataID);
if (dicomWL) {
return { ...defaults, ...dicomWL, useAuto: false };
}

return {
useAuto,
auto: WL_AUTO_DEFAULT,
width,
level,
};
const statsWL = getStatsWindowLevel(dataID);
return { ...defaults, ...statsWL, useAuto: true };
};

const getConfig = (viewID: string, dataID: string): WindowLevelConfig => {
Expand All @@ -89,8 +89,7 @@ export const useWindowingStore = defineStore('windowing', () => {
const [min, max] = autoValues[autoKey];
return {
...internalConfig,
width: max - min,
level: (max + min) / 2,
...minMaxToWidthLevel(min, max),
};
}
return { ...internalConfig };
Expand Down
16 changes: 16 additions & 0 deletions tests/pageobjects/volview.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ class VolViewPage extends Page {
const views2D = $$('div[data-testid="vtk-view vtk-two-view"]');
return views2D;
}

async waitForLoadingIndicator(
view: ChainablePromiseElement,
timeout = DOWNLOAD_TIMEOUT
) {
await browser.waitUntil(
async () => {
const loadingIndicator = await view.$('.loading-indicator');
return !(await loadingIndicator.isDisplayed());
},
{
timeout,
timeoutMsg: 'Expected loading indicator to disappear',
}
);
}
}

export const volViewPage = new VolViewPage();
Expand Down
100 changes: 100 additions & 0 deletions tests/specs/windowing-config.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { volViewPage } from '../pageobjects/volview.page';
import { openUrls, writeManifestToFile } from './utils';

describe('VolView windowing configuration', () => {
it('should use runtime config window level over DICOM window level', async () => {
const runtimeWindowLevel = {
width: 2000,
level: 1000,
};

const config = {
windowing: runtimeWindowLevel,
};

const configFileName = 'windowing-config.json';
await writeManifestToFile(config, configFileName);

const manifest = {
resources: [
{ url: `/tmp/${configFileName}` },
{
url: 'https://data.kitware.com/api/v1/file/6566aa81c5a2b36857ad1783/download/CT000085.dcm',
name: 'CT000085.dcm',
},
],
};

const manifestFileName = 'windowing-manifest.json';
await writeManifestToFile(manifest, manifestFileName);

await volViewPage.open(`?urls=[tmp/${manifestFileName}]`);
await volViewPage.waitForViews();

const view = await $('div[data-testid="vtk-view vtk-two-view"]');

await volViewPage.waitForLoadingIndicator(view);

const viewAnnotations = await view.$('.view-annotations');
const wlText = await viewAnnotations.getText();

const match = wlText.match(/W\/L:\s*([\d.]+)\s*\/\s*([\d.]+)/);
expect(match).not.toBeNull();

const displayedWidth = parseFloat(match![1]);
const displayedLevel = parseFloat(match![2]);

expect(displayedWidth).toBe(runtimeWindowLevel.width);
expect(displayedLevel).toBe(runtimeWindowLevel.level);
});

it('should use DICOM window level when no runtime config is provided', async () => {
await openUrls([
{
url: 'https://data.kitware.com/api/v1/file/6566aa81c5a2b36857ad1783/download/CT000085.dcm',
name: 'CT000085.dcm',
},
]);

const view = await $('div[data-testid="vtk-view vtk-two-view"]');

await volViewPage.waitForLoadingIndicator(view);

const viewAnnotations = await view.$('.view-annotations');
const wlText = await viewAnnotations.getText();

const match = wlText.match(/W\/L:\s*([\d.]+)\s*\/\s*([\d.]+)/);
expect(match).not.toBeNull();

const displayedWidth = parseFloat(match![1]);
const displayedLevel = parseFloat(match![2]);

expect(displayedWidth).toBe(410);
expect(displayedLevel).toBe(70);
});

it('should use auto windowing for DICOM without window/level metadata', async () => {
await openUrls([
{
url: 'https://data.kitware.com/api/v1/file/68e9807dbf0f869935e36481/download/minimal.dcm',
name: 'minimal.dcm',
},
]);

const view = await $('div[data-testid="vtk-view vtk-two-view"]');

await volViewPage.waitForLoadingIndicator(view);

const viewAnnotations = await view.$('.view-annotations');
const wlText = await viewAnnotations.getText();

const match = wlText.match(/W\/L:\s*([\d.]+)\s*\/\s*([\d.]+)/);
expect(match).not.toBeNull();

const displayedWidth = parseFloat(match![1]);
const displayedLevel = parseFloat(match![2]);

expect(displayedWidth).toBe(1900);
expect(displayedLevel).toBe(1050);
});
});
Loading