diff --git a/src/io/dicom.ts b/src/io/dicom.ts index fed1a7b5..b16621cf 100644 --- a/src/io/dicom.ts +++ b/src/io/dicom.ts @@ -1,6 +1,10 @@ import { runPipeline, TextStream, InterfaceTypes, Image } from 'itk-wasm'; -import { readDicomTags, readImageDicomFileSeries } from '@itk-wasm/dicom'; +import { + readDicomTags, + readImageDicomFileSeries, + readOverlappingSegmentation, +} from '@itk-wasm/dicom'; import itkConfig from '@/src/io/itk/itkConfig'; import { getDicomSeriesWorkerPool, getWorker } from '@/src/io/itk/worker'; @@ -170,6 +174,18 @@ export async function readVolumeSlice( return result.outputs[0].data as Image; } +export async function buildLabelMap(file: File) { + const inputImage = sanitizeFile(file); + const result = await readOverlappingSegmentation(inputImage, { + webWorker: getWorker(), + mergeSegments: true, + }); + return { + ...result, + outputImage: result.segImage, + }; +} + /** * Builds a volume for a set of files. * @async @@ -183,6 +199,5 @@ export async function buildImage(seriesFiles: File[]) { inputImages, singleSortedSeries: false, }); - - return result.outputImage; + return result; } diff --git a/src/main.js b/src/main.js index 98d76d5b..016aef90 100644 --- a/src/main.js +++ b/src/main.js @@ -10,9 +10,11 @@ import { createApp } from 'vue'; import VueToast from 'vue-toastification'; import { createPinia } from 'pinia'; import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; -import { setPipelinesBaseUrl, setPipelineWorkerUrl } from '@itk-wasm/image-io'; +import { setPipelinesBaseUrl, setPipelineWorkerUrl } from 'itk-wasm'; +import { setPipelinesBaseUrl as imageIoSetPipelinesBaseUrl } from '@itk-wasm/image-io'; import itkConfig from '@/src/io/itk/itkConfig'; + import App from './components/App.vue'; import vuetify from './plugins/vuetify'; import { FILE_READERS } from './io'; @@ -36,9 +38,11 @@ vtkMapper.setResolveCoincidentTopologyLineOffsetParameters(-3, -3); registerAllReaders(FILE_READERS); -// for @itk-wasm/image-io +// Must be set at runtime as new version of @itk-wasm/dicom and @itk-wasm/image-io +// do not pickup build time `../itkConfig` alias remap. +setPipelinesBaseUrl(itkConfig.pipelinesUrl); setPipelineWorkerUrl(itkConfig.pipelineWorkerUrl); -setPipelinesBaseUrl(itkConfig.imageIOUrl); +imageIoSetPipelinesBaseUrl(itkConfig.imageIOUrl); const pinia = createPinia(); pinia.use(CorePiniaProviderPlugin({})); diff --git a/src/store/datasets-dicom.ts b/src/store/datasets-dicom.ts index 41da3478..773d4efe 100644 --- a/src/store/datasets-dicom.ts +++ b/src/store/datasets-dicom.ts @@ -1,5 +1,4 @@ import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; -import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import { defineStore } from 'pinia'; import { Image } from 'itk-wasm'; import { DataSourceWithFile } from '@/src/io/import/dataSource'; @@ -50,6 +49,39 @@ export interface VolumeInfo { WindowWidth: string; } +const buildImage = async (seriesFiles: File[], modality: string) => { + if (modality === 'SEG') { + return { + modality, + builtImageResults: await DICOM.buildLabelMap(seriesFiles[0]), + messages: + seriesFiles.length > 1 + ? ['Multiple files in a SEG series. Using only the first file.'] + : [], + }; + } + return { + modality, + builtImageResults: await DICOM.buildImage(seriesFiles), + messages: [], + }; +}; + +const constructImage = async (volumeKey: string, volumeInfo: VolumeInfo) => { + const fileStore = useFileStore(); + const files = fileStore.getFiles(volumeKey); + if (!files) throw new Error('No files for volume key'); + const results = await buildImage(files, volumeInfo.Modality); + const image = vtkITKHelper.convertItkToVtkImage( + results.builtImageResults.outputImage + ); + debugger; + return { + ...results, + image, + }; +}; + interface State { // volumeKey -> imageCacheMultiKey -> ITKImage sliceData: Record>; @@ -58,7 +90,7 @@ interface State { needsRebuild: Record; // Avoid recomputing image data for the same volume by checking this for existing buildVolume tasks - volumeImageData: Record>; + volumeBuildResults: Record>; // patientKey -> patient info patientInfo: Record; @@ -143,20 +175,10 @@ export const getWindowLevels = (info: VolumeInfo) => { return widths.map((width, i) => ({ width, level: levels[i] })); }; -const constructImage = async (volumeKey: string) => { - const fileStore = useFileStore(); - const files = fileStore.getFiles(volumeKey); - if (!files) throw new Error('No files for volume key'); - const image = vtkITKHelper.convertItkToVtkImage( - await DICOM.buildImage(files) - ); - return image; -}; - export const useDICOMStore = defineStore('dicom', { state: (): State => ({ sliceData: {}, - volumeImageData: {}, + volumeBuildResults: {}, patientInfo: {}, patientStudies: {}, studyInfo: {}, @@ -196,7 +218,11 @@ export const useDICOMStore = defineStore('dicom', { Object.entries(volumeToFiles).map(async ([volumeKey, files]) => { // Read tags of first file if (!(volumeKey in this.volumeInfo)) { - const tags = await readDicomTags(files[0]); + const rawTags = await readDicomTags(files[0]); + // trim whitespace from all values + const tags = Object.fromEntries( + Object.entries(rawTags).map(([key, value]) => [key, value.trim()]) + ); // TODO parse the raw string values const patient = { PatientID: tags.PatientID || ANONYMOUS_PATIENT_ID, @@ -281,8 +307,8 @@ export const useDICOMStore = defineStore('dicom', { delete this.sliceData[volumeKey]; delete this.volumeStudy[volumeKey]; - if (volumeKey in this.volumeImageData) { - delete this.volumeImageData[volumeKey]; + if (volumeKey in this.volumeBuildResults) { + delete this.volumeBuildResults[volumeKey]; } removeFromArray(this.studyVolumes[studyKey], volumeKey); @@ -387,35 +413,38 @@ export const useDICOMStore = defineStore('dicom', { async buildVolume(volumeKey: string, forceRebuild: boolean = false) { const imageStore = useImageStore(); - const alreadyBuilt = volumeKey in this.volumeImageData; + const alreadyBuilt = volumeKey in this.volumeBuildResults; const buildNeeded = forceRebuild || this.needsRebuild[volumeKey] || !alreadyBuilt; delete this.needsRebuild[volumeKey]; // wait for old buildVolume call so we can run imageStore update side effects after const oldImagePromise = alreadyBuilt - ? [this.volumeImageData[volumeKey]] + ? [this.volumeBuildResults[volumeKey]] : []; // actually build volume or wait for existing build? - const newImagePromise = buildNeeded - ? constructImage(volumeKey) - : this.volumeImageData[volumeKey]; + const newVolumeBuildResults = buildNeeded + ? constructImage(volumeKey, this.volumeInfo[volumeKey]) + : this.volumeBuildResults[volumeKey]; // let other calls to buildVolume reuse this constructImage work - this.volumeImageData[volumeKey] = newImagePromise; - const [image] = await Promise.all([newImagePromise, ...oldImagePromise]); + this.volumeBuildResults[volumeKey] = newVolumeBuildResults; + const [volumeBuildResults] = await Promise.all([ + newVolumeBuildResults, + ...oldImagePromise, + ]); // update image store const imageExists = imageStore.dataIndex[volumeKey]; if (imageExists) { // was a rebuild - imageStore.updateData(volumeKey, image); + imageStore.updateData(volumeKey, volumeBuildResults.image); } else { const info = this.volumeInfo[volumeKey]; const name = getDisplayName(info); - imageStore.addVTKImageData(name, image, volumeKey); + imageStore.addVTKImageData(name, volumeBuildResults.image, volumeKey); } - return image; + return volumeBuildResults; }, }, }); diff --git a/vite.config.ts b/vite.config.ts index 6271bb9a..1f3bc7f4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -57,7 +57,6 @@ function resolvePath(...args: string[]) { const rootDir = resolvePath(__dirname); const distDir = resolvePath(rootDir, 'dist'); -const itkConfig = resolvePath(rootDir, 'src', 'io', 'itk', 'itkConfig.js'); const { ANALYZE_BUNDLE, SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT } = process.env; @@ -113,17 +112,6 @@ export default defineConfig({ find: '@src', replacement: resolvePath(rootDir, 'src'), }, - // Patch itk-wasm library code with image-io .wasm file paths - // itkConfig alias only applies to itk-wasm library code after "npm run build" - // During "npm run serve", itk-wasm fetches image-io .wasm files from CDN - { - find: '../itkConfig.js', - replacement: itkConfig, - }, - { - find: '../../itkConfig.js', - replacement: itkConfig, - }, ], }, plugins: [