Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update @itk-wasm/dicom for new SEG pipeline #653

Merged
merged 4 commits into from
Oct 8, 2024
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
57 changes: 30 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.435.0",
"@itk-wasm/dicom": "6.0.1",
"@itk-wasm/image-io": "1.1.1",
"@itk-wasm/dicom": "7.2.2",
"@itk-wasm/image-io": "^1.3.0",
"@kitware/vtk.js": "^29.0.0",
"@netlify/edge-functions": "^2.0.0",
"@sentry/vue": "^7.54.0",
Expand All @@ -39,7 +39,7 @@
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.5",
"gl-matrix": "3.4.3",
"itk-wasm": "1.0.0-b.171",
"itk-wasm": "1.0.0-b.178",
"jszip": "3.10.0",
"mitt": "^3.0.0",
"nanoid": "^4.0.1",
Expand Down
37 changes: 34 additions & 3 deletions src/io/dicom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { runPipeline, TextStream, InterfaceTypes, Image } from 'itk-wasm';

import { readDicomTags, readImageDicomFileSeries } from '@itk-wasm/dicom';
import {
readDicomTags,
readImageDicomFileSeries,
readOverlappingSegmentation,
ReadOverlappingSegmentationResult,
} 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 +175,33 @@ export async function readVolumeSlice(
return result.outputs[0].data as Image;
}

type Segment = {
SegmentLabel: string;
labelID: number;
recommendedDisplayRGBValue: [number, number, number];
};

type ReadOverlappingSegmentationMeta = {
segmentAttributes: Segment[][];
};

type ReadOverlappingSegmentationResultWithRealMeta =
ReadOverlappingSegmentationResult & {
metaInfo: ReadOverlappingSegmentationMeta;
};

export async function buildLabelMap(file: File) {
const inputImage = sanitizeFile(file);
const result = (await readOverlappingSegmentation(inputImage, {
webWorker: getWorker(),
mergeSegments: true,
})) as ReadOverlappingSegmentationResultWithRealMeta;
return {
...result,
outputImage: result.segImage,
};
}

/**
* Builds a volume for a set of files.
* @async
Expand All @@ -183,6 +215,5 @@ export async function buildImage(seriesFiles: File[]) {
inputImages,
singleSortedSeries: false,
});

return result.outputImage;
return result;
}
7 changes: 3 additions & 4 deletions src/io/itk-dicom/emscripten-build/dicom.js

Large diffs are not rendered by default.

Binary file modified src/io/itk-dicom/emscripten-build/dicom.wasm
Binary file not shown.
Binary file modified src/io/itk-dicom/emscripten-build/dicom.wasm.zst
Binary file not shown.
7 changes: 3 additions & 4 deletions src/io/resample/emscripten-build/resample.js

Large diffs are not rendered by default.

Binary file modified src/io/resample/emscripten-build/resample.wasm
Binary file not shown.
Binary file modified src/io/resample/emscripten-build/resample.wasm.zst
Binary file not shown.
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
97 changes: 71 additions & 26 deletions src/store/datasets-dicom.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
import * as DICOM from '@/src/io/dicom';
import { pullComponent0 } from '@/src/utils/images';
import { identity, pick, removeFromArray } from '../utils';
import { useImageStore } from './datasets-images';
import { useFileStore } from './datasets-files';
import { StateFile, DatasetType } from '../io/state-file/schema';
import { serializeData } from '../io/state-file/utils';
import { useMessageStore } from './messages';

export const ANONYMOUS_PATIENT = 'Anonymous';
export const ANONYMOUS_PATIENT_ID = 'ANONYMOUS';
Expand Down Expand Up @@ -50,6 +51,47 @@ export interface VolumeInfo {
WindowWidth: string;
}

const buildImage = async (seriesFiles: File[], modality: string) => {
const messages: string[] = [];
if (modality === 'SEG') {
const segFile = seriesFiles[0];
const results = await DICOM.buildLabelMap(segFile);
if (results.outputImage.imageType.components !== 1) {
messages.push(
`${segFile.name} SEG file has overlapping segments. Using first set.`
);
results.outputImage = pullComponent0(results.segImage);
}
if (seriesFiles.length > 1)
messages.push(
'SEG image has multiple components. Using only the first component.'
);
return {
modality: 'SEG',
builtImageResults: results,
messages,
};
}
return {
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
);
return {
...results,
image,
};
};

interface State {
// volumeKey -> imageCacheMultiKey -> ITKImage
sliceData: Record<string, Record<string, Image>>;
Expand All @@ -58,7 +100,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 +185,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 +228,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 +317,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 +423,44 @@ 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;
const messageStore = useMessageStore();
volumeBuildResults.messages.forEach((message) => {
console.warn(message);
messageStore.addWarning(message);
});

return volumeBuildResults;
},
},
});
Loading
Loading