Skip to content

Commit e90323e

Browse files
authored
Merge pull request #31 from KitwareMedical/dicom-multi-volume-series
Dicom multi volume series
2 parents 62ddef1 + c84d845 commit e90323e

13 files changed

Lines changed: 403 additions & 295 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "vue-cli-service lint",
1010
"prettify": "prettier --write src",
1111
"build:dicom": "itk-js build src/io/itk-dicom/",
12+
"build:dicom:debug": "itk-js build src/io/itk-dicom/ -- -DCMAKE_BUILD_TYPE=Debug",
1213
"build:all": "npm run build:dicom && npm run build"
1314
},
1415
"dependencies": {

src/assets/styles/vtk-view.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
/* increase kerning to compensate for border */
5555
letter-spacing: 1px;
5656
font-size: 14px;
57+
/* handle text overflow */
58+
overflow: hidden;
59+
text-overflow: ellipsis;
5760
}
5861

5962
.vtk-view .js-sw {

src/components/PatientBrowser.vue

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,9 @@
1919
</div>
2020
<div id="patient-data-list">
2121
<item-group :value="selectedBaseImage" @change="setSelection">
22-
<template v-if="!patientName">
23-
No patient selected
24-
</template>
22+
<template v-if="!patientName"> No patient selected </template>
2523
<template v-else-if="patientName === IMAGES">
26-
<div v-if="imageList.length === 0">
27-
No non-dicom images available
28-
</div>
24+
<div v-if="imageList.length === 0">No non-dicom images available</div>
2925
<groupable-item
3026
v-for="imgID in imageList"
3127
:key="imgID"
@@ -90,41 +86,41 @@
9086
</div>
9187
</v-expansion-panel-header>
9288
<v-expansion-panel-content>
93-
<div class="my-2 series-list">
89+
<div class="my-2 volume-list">
9490
<groupable-item
95-
v-for="series in getSeries(study.StudyInstanceUID)"
96-
:key="series.SeriesInstanceUID"
91+
v-for="volInfo in getVolumesForStudy(
92+
study.StudyInstanceUID
93+
)"
94+
:key="volInfo.VolumeID"
9795
v-slot:default="{ active, select }"
98-
:value="dicomSeriesToID[series.SeriesInstanceUID]"
96+
:value="dicomVolumeToDataID[volInfo.VolumeID]"
9997
>
10098
<v-card
10199
outlined
102100
ripple
103101
:color="active ? 'light-blue lighten-4' : ''"
104-
class="series-card"
105-
:title="series.SeriesDescription"
102+
class="volume-card"
103+
:title="volInfo.SeriesDescription"
106104
@click="select"
107105
>
108106
<v-img
109107
contain
110108
height="100px"
111-
:src="dicomThumbnails[series.SeriesInstanceUID]"
109+
:src="dicomThumbnails[volInfo.VolumeID]"
112110
/>
113111
<v-card-text
114112
class="text--primary caption text-center series-desc mt-n3"
115113
>
116-
<div>[{{ series.NumberOfSlices }}]</div>
114+
<div>[{{ volInfo.NumberOfSlices }}]</div>
117115
<div class="text-ellipsis">
118-
{{ series.SeriesDescription || '(no description)' }}
116+
{{ volInfo.SeriesDescription || '(no description)' }}
119117
</div>
120118
<div class="actions">
121119
<v-btn
122120
small
123121
icon
124122
@click.stop="
125-
removeData(
126-
dicomSeriesToID[series.SeriesInstanceUID]
127-
)
123+
removeData(dicomVolumeToDataID[volInfo.VolumeID])
128124
"
129125
>
130126
<v-icon>mdi-delete</v-icon>
@@ -207,7 +203,7 @@ export default {
207203
return {
208204
patientName: '',
209205
imageThumbnails: {}, // dataID -> Image
210-
dicomThumbnails: {}, // seriesUID -> Image
206+
dicomThumbnails: {}, // volumeID -> Image
211207
pendingDicomThumbnails: {},
212208
213209
IMAGES, // symbol
@@ -217,7 +213,7 @@ export default {
217213
computed: {
218214
...mapState({
219215
selectedBaseImage: 'selectedBaseImage',
220-
dicomSeriesToID: 'dicomSeriesToID',
216+
dicomVolumeToDataID: 'dicomVolumeToDataID',
221217
imageList: (state) => state.data.imageIDs,
222218
dataIndex: (state) => state.data.index,
223219
vtkCache: (state) => state.data.vtkCache,
@@ -226,8 +222,8 @@ export default {
226222
patientIndex: 'patientIndex',
227223
patientStudies: 'patientStudies',
228224
studyIndex: 'studyIndex',
229-
studySeries: 'studySeries',
230-
seriesIndex: 'seriesIndex',
225+
studyVolumes: 'studyVolumes',
226+
volumeIndex: 'volumeIndex',
231227
}),
232228
patients(state) {
233229
const seen = new Set();
@@ -303,15 +299,15 @@ export default {
303299
},
304300
305301
methods: {
306-
getSeries(studyUID) {
307-
const seriesList = (this.studySeries[studyUID] ?? []).map(
308-
(seriesUID) => this.seriesIndex[seriesUID]
302+
getVolumesForStudy(studyUID) {
303+
const volumeList = (this.studyVolumes[studyUID] ?? []).map(
304+
(volID) => this.volumeIndex[volID]
309305
);
310306
311307
// trigger a background job fetch thumbnails
312-
this.doBackgroundDicomThumbnails(seriesList);
308+
this.doBackgroundDicomThumbnails(volumeList);
313309
314-
return seriesList;
310+
return volumeList;
315311
},
316312
317313
async setSelection(sel) {
@@ -321,23 +317,23 @@ export default {
321317
}
322318
},
323319
324-
async doBackgroundDicomThumbnails(seriesList) {
325-
seriesList.forEach(async (series) => {
326-
const uid = series.SeriesInstanceUID;
320+
async doBackgroundDicomThumbnails(volumeList) {
321+
volumeList.forEach(async (volInfo) => {
322+
const id = volInfo.VolumeID;
327323
if (
328-
!(uid in this.dicomThumbnails || uid in this.pendingDicomThumbnails)
324+
!(id in this.dicomThumbnails || id in this.pendingDicomThumbnails)
329325
) {
330-
this.$set(this.pendingDicomThumbnails, uid, true);
326+
this.$set(this.pendingDicomThumbnails, id, true);
331327
try {
332-
const middleSlice = Math.round(Number(series.NumberOfSlices) / 2);
333-
const thumbItkImage = await this.getSeriesImage({
334-
seriesKey: uid,
328+
const middleSlice = Math.round(Number(volInfo.NumberOfSlices) / 2);
329+
const thumbItkImage = await this.getVolumeSlice({
330+
volumeID: id,
335331
slice: middleSlice,
336332
asThumbnail: true,
337333
});
338-
this.$set(this.dicomThumbnails, uid, itkImageToURI(thumbItkImage));
334+
this.$set(this.dicomThumbnails, id, itkImageToURI(thumbItkImage));
339335
} finally {
340-
delete this.pendingDicomThumbnails[uid];
336+
delete this.pendingDicomThumbnails[id];
341337
}
342338
}
343339
});
@@ -373,7 +369,7 @@ export default {
373369
374370
...mapActions(['selectBaseImage', 'removeData']),
375371
...mapActions('visualization', ['updateScene']),
376-
...mapActions('dicom', ['getSeriesImage']),
372+
...mapActions('dicom', ['getVolumeSlice']),
377373
},
378374
};
379375
</script>
@@ -413,14 +409,14 @@ export default {
413409
border: 1px solid #ddd;
414410
}
415411
416-
.series-list {
412+
.volume-list {
417413
display: grid;
418414
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
419415
grid-template-rows: 180px;
420416
justify-content: center;
421417
}
422418
423-
.series-card {
419+
.volume-card {
424420
padding: 8px;
425421
cursor: pointer;
426422
}

src/components/VtkTwoView.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ export default {
192192
if (selectedBaseImage in state.data.index) {
193193
const dataInfo = state.data.index[selectedBaseImage];
194194
if (dataInfo.type === DataTypes.Dicom) {
195-
const { patientKey, studyKey, seriesKey } = dataInfo;
195+
const { patientKey, studyKey, volumeKey } = dataInfo;
196196
return {
197197
patient: state.dicom.patientIndex[patientKey],
198198
study: state.dicom.studyIndex[studyKey],
199-
series: state.dicom.seriesIndex[seriesKey],
199+
volume: state.dicom.volumeIndex[volumeKey],
200200
};
201201
}
202202
}
@@ -347,8 +347,8 @@ export default {
347347
? [
348348
`StudyID: ${dicomInfo.value.study.StudyID}`,
349349
dicomInfo.value.study.StudyDescription,
350-
`Series #: ${dicomInfo.value.series.SeriesNumber}`,
351-
dicomInfo.value.series.SeriesDescription,
350+
`Series #: ${dicomInfo.value.volume.SeriesNumber}`,
351+
dicomInfo.value.volume.SeriesDescription,
352352
].join('<br>')
353353
: ''
354354
);

src/io/dicom.js

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default class DicomIO {
7171
* Imports files
7272
* @async
7373
* @param {File[]} files
74-
* @returns SeriesUIDs
74+
* @returns VolumeID[] a list of volumes parsed from the files
7575
*/
7676
async importFiles(files) {
7777
await this.initialize();
@@ -105,37 +105,37 @@ export default class DicomIO {
105105
}
106106

107107
/**
108-
* Builds the series slice order.
108+
* Builds the volume slice order.
109109
*
110-
* This should be done prior to readSeriesTags or buildVolume.
111-
* @param {String} gdcmSeriesUID
110+
* This should be done prior to readTags or buildVolume.
111+
* @param {String} volumeID
112112
*/
113-
async buildSeriesOrder(gdcmSeriesUID) {
113+
async buildVolumeList(volumeID) {
114114
const result = await this.addTask(
115115
'dicom',
116-
['buildSeries', 'output.json', gdcmSeriesUID],
116+
['buildVolumeList', 'output.json', volumeID],
117117
[{ path: 'output.json', type: IOTypes.Text }],
118118
[]
119119
);
120120
return JSON.parse(result.outputs[0].data);
121121
}
122122

123123
/**
124-
* Reads a list of tags out from a given series UID.
124+
* Reads a list of tags out from a given volume ID.
125125
*
126-
* @param {String} seriesUID
126+
* @param {String} volumeID
127127
* @param {[]Tag} tags
128128
* @param {Integer} slice Defaults to 0 (first slice)
129129
*/
130-
async readSeriesTags(seriesUID, tags, slice = 0) {
130+
async readTags(volumeID, tags, slice = 0) {
131131
const tagsArgs = tags.map((t) => {
132132
const { strconv, tag } = t;
133133
return `${strconv ? '@' : ''}${tag}`;
134134
});
135135

136136
const results = await this.addTask(
137137
'dicom',
138-
['readTags', 'output.json', seriesUID, String(slice), ...tagsArgs],
138+
['readTags', 'output.json', volumeID, String(slice), ...tagsArgs],
139139
[{ path: 'output.json', type: IOTypes.Text }],
140140
[]
141141
);
@@ -151,22 +151,22 @@ export default class DicomIO {
151151
}
152152

153153
/**
154-
* Retrieves a slice of a series.
154+
* Retrieves a slice of a volume.
155155
* @async
156-
* @param {String} seriesUID the ITK-GDCM series UID
156+
* @param {String} volumeID the volume ID
157157
* @param {Number} slice the slice to retrieve
158158
* @param {Boolean} asThumbnail cast image to unsigned char. Defaults to false.
159159
* @returns ItkImage
160160
*/
161-
async getSeriesImage(seriesUID, slice, asThumbnail = false) {
161+
async getVolumeSlice(volumeID, slice, asThumbnail = false) {
162162
await this.initialize();
163163

164164
const result = await this.addTask(
165165
'dicom',
166166
[
167167
'getSliceImage',
168168
'output.json',
169-
seriesUID,
169+
volumeID,
170170
String(slice),
171171
asThumbnail ? '1' : '0',
172172
],
@@ -179,37 +179,37 @@ export default class DicomIO {
179179
}
180180

181181
/**
182-
* Builds a volume for a given series.
182+
* Builds a volume for a given volume ID.
183183
* @async
184-
* @param {String} seriesUID the ITK-GDCM series UID
184+
* @param {String} volumeID the volume ID
185185
* @returns ItkImage
186186
*/
187-
async buildSeriesVolume(seriesUID) {
187+
async buildVolume(volumeID) {
188188
await this.initialize();
189189

190190
const result = await this.addTask(
191191
'dicom',
192-
['buildSeriesVolume', 'output.json', seriesUID],
192+
['buildVolume', 'output.json', volumeID],
193193
[{ path: 'output.json', type: IOTypes.Image }],
194194
[],
195195
10 // building volumes is high priority
196196
);
197197

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

205205
/**
206-
* Deletes all files associated with a series.
206+
* Deletes all files associated with a volume.
207207
* @async
208-
* @param {String} seriesUID the series UID
208+
* @param {String} volumeID the volume ID
209209
*/
210-
async deleteSeries(seriesUID) {
210+
async deleteVolume(volumeID) {
211211
await this.initialize();
212-
await this.addTask('dicom', ['deleteSeries', seriesUID], [], []);
212+
await this.addTask('dicom', ['deleteVolume', volumeID], [], []);
213213
}
214214

215215
/**

src/io/itk-dicom/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ find_package(ITK REQUIRED
2929
ITKImageIntensity
3030
# for GDCMSeriesFileNames.h
3131
ITKIOGDCM
32+
ITKGDCM
3233
# spatial objects
3334
ITKMesh
3435
ITKSpatialObjects
@@ -107,3 +108,7 @@ add_dependencies(iconv ${ICONV})
107108
add_executable(dicom ${dicom_SRCS})
108109
target_include_directories(dicom PRIVATE ${ICONV_DIR}/include)
109110
target_link_libraries(dicom PRIVATE ${ITK_LIBRARIES} iconv nlohmann_json::nlohmann_json)
111+
112+
if(NOT EMSCRIPTEN)
113+
target_link_libraries(dicom PRIVATE stdc++fs)
114+
endif()

0 commit comments

Comments
 (0)