Skip to content

Commit

Permalink
Merge pull request #31 from KitwareMedical/dicom-multi-volume-series
Browse files Browse the repository at this point in the history
Dicom multi volume series
  • Loading branch information
floryst authored Apr 15, 2021
2 parents 62ddef1 + c84d845 commit e90323e
Show file tree
Hide file tree
Showing 13 changed files with 403 additions and 295 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "vue-cli-service lint",
"prettify": "prettier --write src",
"build:dicom": "itk-js build src/io/itk-dicom/",
"build:dicom:debug": "itk-js build src/io/itk-dicom/ -- -DCMAKE_BUILD_TYPE=Debug",
"build:all": "npm run build:dicom && npm run build"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions src/assets/styles/vtk-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
/* increase kerning to compensate for border */
letter-spacing: 1px;
font-size: 14px;
/* handle text overflow */
overflow: hidden;
text-overflow: ellipsis;
}

.vtk-view .js-sw {
Expand Down
76 changes: 36 additions & 40 deletions src/components/PatientBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,9 @@
</div>
<div id="patient-data-list">
<item-group :value="selectedBaseImage" @change="setSelection">
<template v-if="!patientName">
No patient selected
</template>
<template v-if="!patientName"> No patient selected </template>
<template v-else-if="patientName === IMAGES">
<div v-if="imageList.length === 0">
No non-dicom images available
</div>
<div v-if="imageList.length === 0">No non-dicom images available</div>
<groupable-item
v-for="imgID in imageList"
:key="imgID"
Expand Down Expand Up @@ -90,41 +86,41 @@
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
<div class="my-2 series-list">
<div class="my-2 volume-list">
<groupable-item
v-for="series in getSeries(study.StudyInstanceUID)"
:key="series.SeriesInstanceUID"
v-for="volInfo in getVolumesForStudy(
study.StudyInstanceUID
)"
:key="volInfo.VolumeID"
v-slot:default="{ active, select }"
:value="dicomSeriesToID[series.SeriesInstanceUID]"
:value="dicomVolumeToDataID[volInfo.VolumeID]"
>
<v-card
outlined
ripple
:color="active ? 'light-blue lighten-4' : ''"
class="series-card"
:title="series.SeriesDescription"
class="volume-card"
:title="volInfo.SeriesDescription"
@click="select"
>
<v-img
contain
height="100px"
:src="dicomThumbnails[series.SeriesInstanceUID]"
:src="dicomThumbnails[volInfo.VolumeID]"
/>
<v-card-text
class="text--primary caption text-center series-desc mt-n3"
>
<div>[{{ series.NumberOfSlices }}]</div>
<div>[{{ volInfo.NumberOfSlices }}]</div>
<div class="text-ellipsis">
{{ series.SeriesDescription || '(no description)' }}
{{ volInfo.SeriesDescription || '(no description)' }}
</div>
<div class="actions">
<v-btn
small
icon
@click.stop="
removeData(
dicomSeriesToID[series.SeriesInstanceUID]
)
removeData(dicomVolumeToDataID[volInfo.VolumeID])
"
>
<v-icon>mdi-delete</v-icon>
Expand Down Expand Up @@ -207,7 +203,7 @@ export default {
return {
patientName: '',
imageThumbnails: {}, // dataID -> Image
dicomThumbnails: {}, // seriesUID -> Image
dicomThumbnails: {}, // volumeID -> Image
pendingDicomThumbnails: {},
IMAGES, // symbol
Expand All @@ -217,7 +213,7 @@ export default {
computed: {
...mapState({
selectedBaseImage: 'selectedBaseImage',
dicomSeriesToID: 'dicomSeriesToID',
dicomVolumeToDataID: 'dicomVolumeToDataID',
imageList: (state) => state.data.imageIDs,
dataIndex: (state) => state.data.index,
vtkCache: (state) => state.data.vtkCache,
Expand All @@ -226,8 +222,8 @@ export default {
patientIndex: 'patientIndex',
patientStudies: 'patientStudies',
studyIndex: 'studyIndex',
studySeries: 'studySeries',
seriesIndex: 'seriesIndex',
studyVolumes: 'studyVolumes',
volumeIndex: 'volumeIndex',
}),
patients(state) {
const seen = new Set();
Expand Down Expand Up @@ -303,15 +299,15 @@ export default {
},
methods: {
getSeries(studyUID) {
const seriesList = (this.studySeries[studyUID] ?? []).map(
(seriesUID) => this.seriesIndex[seriesUID]
getVolumesForStudy(studyUID) {
const volumeList = (this.studyVolumes[studyUID] ?? []).map(
(volID) => this.volumeIndex[volID]
);
// trigger a background job fetch thumbnails
this.doBackgroundDicomThumbnails(seriesList);
this.doBackgroundDicomThumbnails(volumeList);
return seriesList;
return volumeList;
},
async setSelection(sel) {
Expand All @@ -321,23 +317,23 @@ export default {
}
},
async doBackgroundDicomThumbnails(seriesList) {
seriesList.forEach(async (series) => {
const uid = series.SeriesInstanceUID;
async doBackgroundDicomThumbnails(volumeList) {
volumeList.forEach(async (volInfo) => {
const id = volInfo.VolumeID;
if (
!(uid in this.dicomThumbnails || uid in this.pendingDicomThumbnails)
!(id in this.dicomThumbnails || id in this.pendingDicomThumbnails)
) {
this.$set(this.pendingDicomThumbnails, uid, true);
this.$set(this.pendingDicomThumbnails, id, true);
try {
const middleSlice = Math.round(Number(series.NumberOfSlices) / 2);
const thumbItkImage = await this.getSeriesImage({
seriesKey: uid,
const middleSlice = Math.round(Number(volInfo.NumberOfSlices) / 2);
const thumbItkImage = await this.getVolumeSlice({
volumeID: id,
slice: middleSlice,
asThumbnail: true,
});
this.$set(this.dicomThumbnails, uid, itkImageToURI(thumbItkImage));
this.$set(this.dicomThumbnails, id, itkImageToURI(thumbItkImage));
} finally {
delete this.pendingDicomThumbnails[uid];
delete this.pendingDicomThumbnails[id];
}
}
});
Expand Down Expand Up @@ -373,7 +369,7 @@ export default {
...mapActions(['selectBaseImage', 'removeData']),
...mapActions('visualization', ['updateScene']),
...mapActions('dicom', ['getSeriesImage']),
...mapActions('dicom', ['getVolumeSlice']),
},
};
</script>
Expand Down Expand Up @@ -413,14 +409,14 @@ export default {
border: 1px solid #ddd;
}
.series-list {
.volume-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
grid-template-rows: 180px;
justify-content: center;
}
.series-card {
.volume-card {
padding: 8px;
cursor: pointer;
}
Expand Down
8 changes: 4 additions & 4 deletions src/components/VtkTwoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ export default {
if (selectedBaseImage in state.data.index) {
const dataInfo = state.data.index[selectedBaseImage];
if (dataInfo.type === DataTypes.Dicom) {
const { patientKey, studyKey, seriesKey } = dataInfo;
const { patientKey, studyKey, volumeKey } = dataInfo;
return {
patient: state.dicom.patientIndex[patientKey],
study: state.dicom.studyIndex[studyKey],
series: state.dicom.seriesIndex[seriesKey],
volume: state.dicom.volumeIndex[volumeKey],
};
}
}
Expand Down Expand Up @@ -347,8 +347,8 @@ export default {
? [
`StudyID: ${dicomInfo.value.study.StudyID}`,
dicomInfo.value.study.StudyDescription,
`Series #: ${dicomInfo.value.series.SeriesNumber}`,
dicomInfo.value.series.SeriesDescription,
`Series #: ${dicomInfo.value.volume.SeriesNumber}`,
dicomInfo.value.volume.SeriesDescription,
].join('<br>')
: ''
);
Expand Down
46 changes: 23 additions & 23 deletions src/io/dicom.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default class DicomIO {
* Imports files
* @async
* @param {File[]} files
* @returns SeriesUIDs
* @returns VolumeID[] a list of volumes parsed from the files
*/
async importFiles(files) {
await this.initialize();
Expand Down Expand Up @@ -105,37 +105,37 @@ export default class DicomIO {
}

/**
* Builds the series slice order.
* Builds the volume slice order.
*
* This should be done prior to readSeriesTags or buildVolume.
* @param {String} gdcmSeriesUID
* This should be done prior to readTags or buildVolume.
* @param {String} volumeID
*/
async buildSeriesOrder(gdcmSeriesUID) {
async buildVolumeList(volumeID) {
const result = await this.addTask(
'dicom',
['buildSeries', 'output.json', gdcmSeriesUID],
['buildVolumeList', 'output.json', volumeID],
[{ path: 'output.json', type: IOTypes.Text }],
[]
);
return JSON.parse(result.outputs[0].data);
}

/**
* Reads a list of tags out from a given series UID.
* Reads a list of tags out from a given volume ID.
*
* @param {String} seriesUID
* @param {String} volumeID
* @param {[]Tag} tags
* @param {Integer} slice Defaults to 0 (first slice)
*/
async readSeriesTags(seriesUID, tags, slice = 0) {
async readTags(volumeID, tags, slice = 0) {
const tagsArgs = tags.map((t) => {
const { strconv, tag } = t;
return `${strconv ? '@' : ''}${tag}`;
});

const results = await this.addTask(
'dicom',
['readTags', 'output.json', seriesUID, String(slice), ...tagsArgs],
['readTags', 'output.json', volumeID, String(slice), ...tagsArgs],
[{ path: 'output.json', type: IOTypes.Text }],
[]
);
Expand All @@ -151,22 +151,22 @@ export default class DicomIO {
}

/**
* Retrieves a slice of a series.
* Retrieves a slice of a volume.
* @async
* @param {String} seriesUID the ITK-GDCM series UID
* @param {String} volumeID the volume ID
* @param {Number} slice the slice to retrieve
* @param {Boolean} asThumbnail cast image to unsigned char. Defaults to false.
* @returns ItkImage
*/
async getSeriesImage(seriesUID, slice, asThumbnail = false) {
async getVolumeSlice(volumeID, slice, asThumbnail = false) {
await this.initialize();

const result = await this.addTask(
'dicom',
[
'getSliceImage',
'output.json',
seriesUID,
volumeID,
String(slice),
asThumbnail ? '1' : '0',
],
Expand All @@ -179,37 +179,37 @@ export default class DicomIO {
}

/**
* Builds a volume for a given series.
* Builds a volume for a given volume ID.
* @async
* @param {String} seriesUID the ITK-GDCM series UID
* @param {String} volumeID the volume ID
* @returns ItkImage
*/
async buildSeriesVolume(seriesUID) {
async buildVolume(volumeID) {
await this.initialize();

const result = await this.addTask(
'dicom',
['buildSeriesVolume', 'output.json', seriesUID],
['buildVolume', 'output.json', volumeID],
[{ path: 'output.json', type: IOTypes.Image }],
[],
10 // building volumes is high priority
);

// TEMPORARY tranpose until itk.js consistently outputs col-major
// FIXME tranpose until itk.js consistently outputs col-major
// and ITKHelper is updated.
const image = result.outputs[0].data;
mat3.transpose(image.direction.data, image.direction.data);
return image;
}

/**
* Deletes all files associated with a series.
* Deletes all files associated with a volume.
* @async
* @param {String} seriesUID the series UID
* @param {String} volumeID the volume ID
*/
async deleteSeries(seriesUID) {
async deleteVolume(volumeID) {
await this.initialize();
await this.addTask('dicom', ['deleteSeries', seriesUID], [], []);
await this.addTask('dicom', ['deleteVolume', volumeID], [], []);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/io/itk-dicom/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ find_package(ITK REQUIRED
ITKImageIntensity
# for GDCMSeriesFileNames.h
ITKIOGDCM
ITKGDCM
# spatial objects
ITKMesh
ITKSpatialObjects
Expand Down Expand Up @@ -107,3 +108,7 @@ add_dependencies(iconv ${ICONV})
add_executable(dicom ${dicom_SRCS})
target_include_directories(dicom PRIVATE ${ICONV_DIR}/include)
target_link_libraries(dicom PRIVATE ${ITK_LIBRARIES} iconv nlohmann_json::nlohmann_json)

if(NOT EMSCRIPTEN)
target_link_libraries(dicom PRIVATE stdc++fs)
endif()
Loading

0 comments on commit e90323e

Please sign in to comment.