Skip to content

Commit

Permalink
feat(dicom): run readOverlappingSegmentation for SEG
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Oct 3, 2024
1 parent b5e53d9 commit 9a3982a
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 44 deletions.
21 changes: 18 additions & 3 deletions src/io/dicom.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -183,6 +199,5 @@ export async function buildImage(seriesFiles: File[]) {
inputImages,
singleSortedSeries: false,
});

return result.outputImage;
return result;
}
10 changes: 7 additions & 3 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({}));
Expand Down
81 changes: 55 additions & 26 deletions src/store/datasets-dicom.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<string, Record<string, Image>>;
Expand All @@ -58,7 +90,7 @@ interface State {
needsRebuild: Record<string, boolean>;

// Avoid recomputing image data for the same volume by checking this for existing buildVolume tasks
volumeImageData: Record<string, Promise<vtkImageData>>;
volumeBuildResults: Record<string, ReturnType<typeof constructImage>>;

// patientKey -> patient info
patientInfo: Record<string, PatientInfo>;
Expand Down Expand Up @@ -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: {},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
},
},
});
12 changes: 0 additions & 12 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: [
Expand Down

0 comments on commit 9a3982a

Please sign in to comment.