Skip to content

ENH: Support for Philips DICOMs w/ derived volume #727

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

Merged
merged 12 commits into from
Mar 19, 2019
21 changes: 16 additions & 5 deletions nibabel/nicom/dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .dwiparams import B2q, nearest_pos_semi_def, q2bg
from ..openers import ImageOpener
from ..onetime import setattr_on_read as one_time
from ..pydicom_compat import tag_for_keyword
from ..pydicom_compat import tag_for_keyword, Sequence


class WrapperError(Exception):
Expand Down Expand Up @@ -461,10 +461,19 @@ def __init__(self, dcm_data):
Wrapper.__init__(self, dcm_data)
self.dcm_data = dcm_data
self.frames = dcm_data.get('PerFrameFunctionalGroupsSequence')
self._nframes = self.get('NumberOfFrames')
try:
self.frames[0]
except TypeError:
raise WrapperError("PerFrameFunctionalGroupsSequence is empty.")
# DWI image where derived isotropic, ADC or trace volume was appended to the series
if self.frames[0].get([0x18, 0x9117]):
self.frames = Sequence(
frame for frame in self.frames if
frame.get([0x18, 0x9117])[0].get([0x18, 0x9075]).value
!= 'ISOTROPIC'
)
self._nframes = len(self.frames)
try:
self.shared = dcm_data.get('SharedFunctionalGroupsSequence')[0]
except TypeError:
Expand Down Expand Up @@ -503,8 +512,7 @@ def image_shape(self):
if None in (rows, cols):
raise WrapperError("Rows and/or Columns are empty.")
# Check number of frames
n_frames = self.get('NumberOfFrames')
assert len(self.frames) == n_frames
assert len(self.frames) == self._nframes
frame_indices = np.array(
[frame.FrameContentSequence[0].DimensionIndexValues
for frame in self.frames])
Expand All @@ -528,12 +536,15 @@ def image_shape(self):
# Store frame indices
self._frame_indices = frame_indices
if n_dim < 4: # 3D volume
return rows, cols, n_frames
return rows, cols, self._nframes
# More than 3 dimensions
ns_unique = [len(np.unique(row)) for row in self._frame_indices.T]
if len(ns_unique) == 3:
# derived volume is included
ns_unique.pop(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really follow this guard. Can you elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this addresses the problem properly / gracefully, but I'm unsure how to proceed. I've come across a derived image included in the diffusion DICOM, which is currently not properly handled. I thought this meant 5D data wasn't yet handled, but in fact there is a test specifically for this introduced in 3f04e3c (maybe @matthew-brett can share some wisdom)

# 5D!
dim_idxs = [
[1, 4, 2, 1],
[1, 2, 2, 1],
[1, 3, 2, 1],
[1, 1, 2, 1],
[1, 4, 2, 2],
[1, 2, 2, 2],
[1, 3, 2, 2],
[1, 1, 2, 2],
[1, 4, 1, 1],
[1, 2, 1, 1],
[1, 3, 1, 1],
[1, 1, 1, 1],
[1, 4, 1, 2],
[1, 2, 1, 2],
[1, 3, 1, 2],
[1, 1, 1, 2]]
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
shape = (2, 3, 4, 2, 2)
data = np.arange(np.prod(shape)).reshape(shape)
sorted_data = data.reshape(shape[:2] + (-1,), order='F')
order = [11, 9, 10, 8, 3, 1, 2, 0,
15, 13, 14, 12, 7, 5, 6, 4]
sorted_data = sorted_data[..., np.argsort(order)]
fake_mf['pixel_array'] = np.rollaxis(sorted_data, 2)
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)

shape = (rows, cols) + tuple(ns_unique)
n_vols = np.prod(shape[3:])
if n_frames != n_vols * shape[2]:
if self._nframes != n_vols * shape[2]:
raise WrapperError("Calculated shape does not match number of "
"frames.")
return tuple(shape)
Expand Down
Binary file not shown.
8 changes: 8 additions & 0 deletions nibabel/nicom/tests/test_dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
DATA_FILE_SLC_NORM = pjoin(IO_DATA_PATH, 'csa_slice_norm.dcm')
DATA_FILE_DEC_RSCL = pjoin(IO_DATA_PATH, 'decimal_rescale.dcm')
DATA_FILE_4D = pjoin(IO_DATA_PATH, '4d_multiframe_test.dcm')
DATA_FILE_4D_DERIVED = pjoin(IO_DATA_PATH, '4d_multiframe_with_derived.dcm')

# This affine from our converted image was shown to match our image spatially
# with an image from SPM DICOM conversion. We checked the matching with SPM
Expand Down Expand Up @@ -616,6 +617,13 @@ def test_data_real(self):
assert_equal(sha1(dat_str).hexdigest(),
'149323269b0af92baa7508e19ca315240f77fa8c')

@dicom_test
def test_data_derived_shape(self):
# Test 4D diffusion data with an additional trace volume included
# Excludes the trace volume and generates the correct shape
dw = didw.wrapper_from_file(DATA_FILE_4D_DERIVED)
assert_equal(dw.image_shape, (96, 96, 60, 33))

@dicom_test
def test_data_fake(self):
# Test algorithm for get_data
Expand Down
1 change: 1 addition & 0 deletions nibabel/pydicom_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
read_file = pydicom.read_file

if have_dicom:
from pydicom.sequence import Sequence
try:
# Versions >= 1.0
tag_for_keyword = pydicom.datadict.tag_for_keyword
Expand Down