Skip to content
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
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