Skip to content

MRG: test fixes and doc updates for multiframe code #442

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 4 commits into from
Apr 11, 2016
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
23 changes: 19 additions & 4 deletions nibabel/nicom/dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ..onetime import setattr_on_read as one_time
from ..pydicom_compat import pydicom


class WrapperError(Exception):
pass

Expand Down Expand Up @@ -412,7 +413,18 @@ def b_vector(self):
class MultiframeWrapper(Wrapper):
"""Wrapper for Enhanced MR Storage SOP Class

tested with Philips' Enhanced DICOM implementation
Tested with Philips' Enhanced DICOM implementation.

The specification for the Enhanced MR image IOP / SOP began life as `DICOM
supplement 49 <ftp://medical.nema.org/medical/dicom/final/sup49_ft.pdf>`_,
but as of 2016 it is part of the standard. In particular see:

* `A.36 Enhanced MR Information Object Definitions
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_A.36>`_;
* `C.7.6.16 Multi-Frame Functional Groups Module
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.16>`_;
* `C.7.6.17 Multi-Frame Dimension Module
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17>`_.

Attributes
----------
Expand Down Expand Up @@ -480,9 +492,12 @@ def image_shape(self):

References
----------

* C.7.6.16 Multi-Frame Functional Groups Module: http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.16
* C.7.6.17 Multi-Frame Dimension Module: http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17
* C.7.6.16 Multi-Frame Functional Groups Module:
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.16
* C.7.6.17 Multi-Frame Dimension Module:
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17
* Diagram of DimensionIndexSequence and DimensionIndexValues:
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#figure_C.7.6.17-1
"""
rows, cols = self.get('Rows'), self.get('Columns')
if None in (rows, cols):
Expand Down
158 changes: 80 additions & 78 deletions nibabel/nicom/tests/test_dicomwrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import gzip
from hashlib import sha1
from decimal import Decimal
from copy import copy
from copy import copy

import numpy as np

Expand Down Expand Up @@ -379,54 +379,54 @@ class Fake(object):
setattr(fake_frame, seq_name, [fake_element])
frames.append(fake_frame)
return frames
def fake_shape_dependents(div_seq, sid_seq=None, sid_dim=None):
""" Make a fake dictionary of data that ``image_shape`` is dependent on.
Parameters
----------
div_seq : list of tuples
list of values to use for the `DimensionIndexValues` of each frame.
sid_seq : list of int
list of values to use for the `StackID` of each frame.
sid_dim : int
the index of the column in 'div_seq' to use as 'sid_seq'
"""
class DimIdxSeqElem(object):
def __init__(self, dip=(0, 0), fgp=None):
self.DimensionIndexPointer = dip
if fgp is not None:
self.FunctionalGroupPointer = fgp
class FrmContSeqElem(object):
def __init__(self, div, sid):
self.DimensionIndexValues = div
self.StackID = sid
class PerFrmFuncGrpSeqElem(object):
def __init__(self, div, sid):
self.FrameContentSequence = [FrmContSeqElem(div, sid)]
# if no StackID values passed in then use the values at index 'sid_dim' in
# the value for DimensionIndexValues for it
if sid_seq is None:
if sid_dim is None:
sid_dim = 0
sid_seq = [div[sid_dim] for div in div_seq]
# create the DimensionIndexSequence
num_of_frames = len(div_seq)
dim_idx_seq = [DimIdxSeqElem()] * num_of_frames
# add an entry for StackID into the DimensionIndexSequence
if sid_dim is not None:
sid_tag = pydicom.datadict.tag_for_name('StackID')
fcs_tag = pydicom.datadict.tag_for_name('FrameContentSequence')
dim_idx_seq[sid_dim] = DimIdxSeqElem(sid_tag, fcs_tag)
# create the PerFrameFunctionalGroupsSequence
frames = [PerFrmFuncGrpSeqElem(div, sid)
for div, sid in zip(div_seq, sid_seq)]
return {'NumberOfFrames' : num_of_frames,
'DimensionIndexSequence' : dim_idx_seq,
'PerFrameFunctionalGroupsSequence' : frames}


def fake_shape_dependents(div_seq, sid_seq=None, sid_dim=None):
""" Make a fake dictionary of data that ``image_shape`` is dependent on.

Parameters
----------
div_seq : list of tuples
list of values to use for the `DimensionIndexValues` of each frame.
sid_seq : list of int
list of values to use for the `StackID` of each frame.
sid_dim : int
the index of the column in 'div_seq' to use as 'sid_seq'
"""
class DimIdxSeqElem(object):
def __init__(self, dip=(0, 0), fgp=None):
self.DimensionIndexPointer = dip
if fgp is not None:
self.FunctionalGroupPointer = fgp
class FrmContSeqElem(object):
def __init__(self, div, sid):
self.DimensionIndexValues = div
self.StackID = sid
class PerFrmFuncGrpSeqElem(object):
def __init__(self, div, sid):
self.FrameContentSequence = [FrmContSeqElem(div, sid)]
# if no StackID values passed in then use the values at index 'sid_dim' in
# the value for DimensionIndexValues for it
if sid_seq is None:
if sid_dim is None:
sid_dim = 0
sid_seq = [div[sid_dim] for div in div_seq]
# create the DimensionIndexSequence
num_of_frames = len(div_seq)
dim_idx_seq = [DimIdxSeqElem()] * num_of_frames
# add an entry for StackID into the DimensionIndexSequence
if sid_dim is not None:
sid_tag = pydicom.datadict.tag_for_name('StackID')
fcs_tag = pydicom.datadict.tag_for_name('FrameContentSequence')
dim_idx_seq[sid_dim] = DimIdxSeqElem(sid_tag, fcs_tag)
# create the PerFrameFunctionalGroupsSequence
frames = [PerFrmFuncGrpSeqElem(div, sid)
for div, sid in zip(div_seq, sid_seq)]
return {'NumberOfFrames' : num_of_frames,
'DimensionIndexSequence' : dim_idx_seq,
'PerFrameFunctionalGroupsSequence' : frames}


class TestMultiFrameWrapper(TestCase):
# Test MultiframeWrapper
MINIMAL_MF = {
Expand All @@ -435,6 +435,7 @@ class TestMultiFrameWrapper(TestCase):
'SharedFunctionalGroupsSequence': [None]}
WRAPCLASS = didw.MultiframeWrapper

@dicom_test
def test_shape(self):
# Check the shape algorithm
fake_mf = copy(self.MINIMAL_MF)
Expand All @@ -452,65 +453,65 @@ def test_shape(self):
assert_raises(AssertionError, getattr, dw, 'image_shape')
fake_mf['NumberOfFrames'] = 4
# PerFrameFunctionalGroupsSequence does not match NumberOfFrames
assert_raises(AssertionError, getattr, dw, 'image_shape')
# check 3D shape when StackID index is 0
div_seq = ((1, 1), (1, 2), (1, 3), (1, 4))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_raises(AssertionError, getattr, dw, 'image_shape')
# check 3D shape when StackID index is 0
div_seq = ((1, 1), (1, 2), (1, 3), (1, 4))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
# Check stack number matching when StackID index is 0
div_seq = ((1, 1), (1, 2), (1, 3), (2, 4))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
# Make some fake frame data for 4D when StackID index is 0
# Make some fake frame data for 4D when StackID index is 0
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
(1, 1, 3), (1, 2, 3))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
# Check stack number matching for 4D when StackID index is 0
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
(1, 1, 3), (2, 2, 3))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
# Check indices can be non-contiguous when StackID index is 0
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 3), (1, 2, 3))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
# Check indices can include zero when StackID index is 0
div_seq = ((1, 1, 0), (1, 2, 0), (1, 1, 3), (1, 2, 3))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
# check 3D shape when there is no StackID index
# check 3D shape when there is no StackID index
div_seq = ((1,), (2,), (3,), (4,))
sid_seq = (1, 1, 1, 1)
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
# check 3D stack number matching when there is no StackID index
div_seq = ((1,), (2,), (3,), (4,))
sid_seq = (1, 1, 1, 2)
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
# check 4D shape when there is no StackID index
# check 4D shape when there is no StackID index
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
sid_seq = (1, 1, 1, 1, 1, 1)
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
# check 4D stack number matching when there is no StackID index
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
sid_seq = (1, 1, 1, 1, 1, 2)
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
# check 3D shape when StackID index is 1
# check 3D shape when StackID index is 1
div_seq = ((1, 1), (2, 1), (3, 1), (4, 1))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
# Check stack number matching when StackID index is 1
div_seq = ((1, 1), (2, 1), (3, 2), (4, 1))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
div_seq = ((1, 1), (2, 1), (3, 2), (4, 1))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
# Make some fake frame data for 4D when StackID index is 1
# Make some fake frame data for 4D when StackID index is 1
div_seq = ((1, 1, 1), (2, 1, 1), (1, 1, 2), (2, 1, 2),
(1, 1, 3), (2, 1, 3))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))

def test_iop(self):
Expand Down Expand Up @@ -614,6 +615,7 @@ def test_data_real(self):
assert_equal(sha1(dat_str).hexdigest(),
'149323269b0af92baa7508e19ca315240f77fa8c')

@dicom_test
def test_data_fake(self):
# Test algorithm for get_data
fake_mf = copy(self.MINIMAL_MF)
Expand All @@ -627,9 +629,9 @@ def test_data_fake(self):
assert_raises(didw.WrapperError, dw.get_data)
# Make shape and indices
fake_mf['Rows'] = 2
fake_mf['Columns'] = 3
dim_idxs = ((1, 1), (1, 2), (1, 3), (1, 4))
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
fake_mf['Columns'] = 3
dim_idxs = ((1, 1), (1, 2), (1, 3), (1, 4))
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
assert_equal(MFW(fake_mf).image_shape, (2, 3, 4))
# Still fails - no data
assert_raises(didw.WrapperError, dw.get_data)
Expand All @@ -644,9 +646,9 @@ def test_data_fake(self):
fake_mf['RescaleSlope'] = 2.0
fake_mf['RescaleIntercept'] = -1
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
# Check slice sorting
dim_idxs = ((1, 4), (1, 2), (1, 3), (1, 1))
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
# Check slice sorting
dim_idxs = ((1, 4), (1, 2), (1, 3), (1, 1))
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
sorted_data = data[..., [3, 1, 2, 0]]
fake_mf['pixel_array'] = np.rollaxis(sorted_data, 2)
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
Expand All @@ -668,7 +670,7 @@ def test_data_fake(self):
[1, 2, 1, 2],
[1, 3, 1, 2],
[1, 1, 1, 2]]
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
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')
Expand Down