Skip to content

Commit 64333e6

Browse files
committed
Merge pull request #442 from matthew-brett/more-multiframe-links
MRG: test fixes and doc updates for multiframe code Fixes to some new test failures at : https://travis-ci.org/matthew-brett/nibabel/builds/121846880 More links to sections and diagrams on multi-frame / enhanced MR image IOP. Follow-up to PR #439.
2 parents b3bfcec + bf0b018 commit 64333e6

File tree

2 files changed

+99
-82
lines changed

2 files changed

+99
-82
lines changed

nibabel/nicom/dicomwrappers.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ..onetime import setattr_on_read as one_time
2424
from ..pydicom_compat import pydicom
2525

26+
2627
class WrapperError(Exception):
2728
pass
2829

@@ -412,7 +413,18 @@ def b_vector(self):
412413
class MultiframeWrapper(Wrapper):
413414
"""Wrapper for Enhanced MR Storage SOP Class
414415
415-
tested with Philips' Enhanced DICOM implementation
416+
Tested with Philips' Enhanced DICOM implementation.
417+
418+
The specification for the Enhanced MR image IOP / SOP began life as `DICOM
419+
supplement 49 <ftp://medical.nema.org/medical/dicom/final/sup49_ft.pdf>`_,
420+
but as of 2016 it is part of the standard. In particular see:
421+
422+
* `A.36 Enhanced MR Information Object Definitions
423+
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_A.36>`_;
424+
* `C.7.6.16 Multi-Frame Functional Groups Module
425+
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.16>`_;
426+
* `C.7.6.17 Multi-Frame Dimension Module
427+
<http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17>`_.
416428
417429
Attributes
418430
----------
@@ -480,9 +492,12 @@ def image_shape(self):
480492
481493
References
482494
----------
483-
484-
* 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
485-
* C.7.6.17 Multi-Frame Dimension Module: http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17
495+
* C.7.6.16 Multi-Frame Functional Groups Module:
496+
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.16
497+
* C.7.6.17 Multi-Frame Dimension Module:
498+
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#sect_C.7.6.17
499+
* Diagram of DimensionIndexSequence and DimensionIndexValues:
500+
http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf#figure_C.7.6.17-1
486501
"""
487502
rows, cols = self.get('Rows'), self.get('Columns')
488503
if None in (rows, cols):

nibabel/nicom/tests/test_dicomwrappers.py

Lines changed: 80 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import gzip
66
from hashlib import sha1
77
from decimal import Decimal
8-
from copy import copy
8+
from copy import copy
99

1010
import numpy as np
1111

@@ -379,54 +379,54 @@ class Fake(object):
379379
setattr(fake_frame, seq_name, [fake_element])
380380
frames.append(fake_frame)
381381
return frames
382-
383-
384-
def fake_shape_dependents(div_seq, sid_seq=None, sid_dim=None):
385-
""" Make a fake dictionary of data that ``image_shape`` is dependent on.
386-
387-
Parameters
388-
----------
389-
div_seq : list of tuples
390-
list of values to use for the `DimensionIndexValues` of each frame.
391-
sid_seq : list of int
392-
list of values to use for the `StackID` of each frame.
393-
sid_dim : int
394-
the index of the column in 'div_seq' to use as 'sid_seq'
395-
"""
396-
class DimIdxSeqElem(object):
397-
def __init__(self, dip=(0, 0), fgp=None):
398-
self.DimensionIndexPointer = dip
399-
if fgp is not None:
400-
self.FunctionalGroupPointer = fgp
401-
class FrmContSeqElem(object):
402-
def __init__(self, div, sid):
403-
self.DimensionIndexValues = div
404-
self.StackID = sid
405-
class PerFrmFuncGrpSeqElem(object):
406-
def __init__(self, div, sid):
407-
self.FrameContentSequence = [FrmContSeqElem(div, sid)]
408-
# if no StackID values passed in then use the values at index 'sid_dim' in
409-
# the value for DimensionIndexValues for it
410-
if sid_seq is None:
411-
if sid_dim is None:
412-
sid_dim = 0
413-
sid_seq = [div[sid_dim] for div in div_seq]
414-
# create the DimensionIndexSequence
415-
num_of_frames = len(div_seq)
416-
dim_idx_seq = [DimIdxSeqElem()] * num_of_frames
417-
# add an entry for StackID into the DimensionIndexSequence
418-
if sid_dim is not None:
419-
sid_tag = pydicom.datadict.tag_for_name('StackID')
420-
fcs_tag = pydicom.datadict.tag_for_name('FrameContentSequence')
421-
dim_idx_seq[sid_dim] = DimIdxSeqElem(sid_tag, fcs_tag)
422-
# create the PerFrameFunctionalGroupsSequence
423-
frames = [PerFrmFuncGrpSeqElem(div, sid)
424-
for div, sid in zip(div_seq, sid_seq)]
425-
return {'NumberOfFrames' : num_of_frames,
426-
'DimensionIndexSequence' : dim_idx_seq,
427-
'PerFrameFunctionalGroupsSequence' : frames}
428-
429-
382+
383+
384+
def fake_shape_dependents(div_seq, sid_seq=None, sid_dim=None):
385+
""" Make a fake dictionary of data that ``image_shape`` is dependent on.
386+
387+
Parameters
388+
----------
389+
div_seq : list of tuples
390+
list of values to use for the `DimensionIndexValues` of each frame.
391+
sid_seq : list of int
392+
list of values to use for the `StackID` of each frame.
393+
sid_dim : int
394+
the index of the column in 'div_seq' to use as 'sid_seq'
395+
"""
396+
class DimIdxSeqElem(object):
397+
def __init__(self, dip=(0, 0), fgp=None):
398+
self.DimensionIndexPointer = dip
399+
if fgp is not None:
400+
self.FunctionalGroupPointer = fgp
401+
class FrmContSeqElem(object):
402+
def __init__(self, div, sid):
403+
self.DimensionIndexValues = div
404+
self.StackID = sid
405+
class PerFrmFuncGrpSeqElem(object):
406+
def __init__(self, div, sid):
407+
self.FrameContentSequence = [FrmContSeqElem(div, sid)]
408+
# if no StackID values passed in then use the values at index 'sid_dim' in
409+
# the value for DimensionIndexValues for it
410+
if sid_seq is None:
411+
if sid_dim is None:
412+
sid_dim = 0
413+
sid_seq = [div[sid_dim] for div in div_seq]
414+
# create the DimensionIndexSequence
415+
num_of_frames = len(div_seq)
416+
dim_idx_seq = [DimIdxSeqElem()] * num_of_frames
417+
# add an entry for StackID into the DimensionIndexSequence
418+
if sid_dim is not None:
419+
sid_tag = pydicom.datadict.tag_for_name('StackID')
420+
fcs_tag = pydicom.datadict.tag_for_name('FrameContentSequence')
421+
dim_idx_seq[sid_dim] = DimIdxSeqElem(sid_tag, fcs_tag)
422+
# create the PerFrameFunctionalGroupsSequence
423+
frames = [PerFrmFuncGrpSeqElem(div, sid)
424+
for div, sid in zip(div_seq, sid_seq)]
425+
return {'NumberOfFrames' : num_of_frames,
426+
'DimensionIndexSequence' : dim_idx_seq,
427+
'PerFrameFunctionalGroupsSequence' : frames}
428+
429+
430430
class TestMultiFrameWrapper(TestCase):
431431
# Test MultiframeWrapper
432432
MINIMAL_MF = {
@@ -435,6 +435,7 @@ class TestMultiFrameWrapper(TestCase):
435435
'SharedFunctionalGroupsSequence': [None]}
436436
WRAPCLASS = didw.MultiframeWrapper
437437

438+
@dicom_test
438439
def test_shape(self):
439440
# Check the shape algorithm
440441
fake_mf = copy(self.MINIMAL_MF)
@@ -452,65 +453,65 @@ def test_shape(self):
452453
assert_raises(AssertionError, getattr, dw, 'image_shape')
453454
fake_mf['NumberOfFrames'] = 4
454455
# PerFrameFunctionalGroupsSequence does not match NumberOfFrames
455-
assert_raises(AssertionError, getattr, dw, 'image_shape')
456-
# check 3D shape when StackID index is 0
457-
div_seq = ((1, 1), (1, 2), (1, 3), (1, 4))
458-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
456+
assert_raises(AssertionError, getattr, dw, 'image_shape')
457+
# check 3D shape when StackID index is 0
458+
div_seq = ((1, 1), (1, 2), (1, 3), (1, 4))
459+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
459460
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
460461
# Check stack number matching when StackID index is 0
461462
div_seq = ((1, 1), (1, 2), (1, 3), (2, 4))
462-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
463+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
463464
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
464-
# Make some fake frame data for 4D when StackID index is 0
465+
# Make some fake frame data for 4D when StackID index is 0
465466
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
466467
(1, 1, 3), (1, 2, 3))
467-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
468+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
468469
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
469470
# Check stack number matching for 4D when StackID index is 0
470471
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
471472
(1, 1, 3), (2, 2, 3))
472-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
473+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
473474
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
474475
# Check indices can be non-contiguous when StackID index is 0
475476
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 3), (1, 2, 3))
476-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
477+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
477478
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
478479
# Check indices can include zero when StackID index is 0
479480
div_seq = ((1, 1, 0), (1, 2, 0), (1, 1, 3), (1, 2, 3))
480-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
481+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
481482
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
482-
# check 3D shape when there is no StackID index
483+
# check 3D shape when there is no StackID index
483484
div_seq = ((1,), (2,), (3,), (4,))
484485
sid_seq = (1, 1, 1, 1)
485-
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
486+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
486487
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
487488
# check 3D stack number matching when there is no StackID index
488489
div_seq = ((1,), (2,), (3,), (4,))
489490
sid_seq = (1, 1, 1, 2)
490-
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
491+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
491492
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
492-
# check 4D shape when there is no StackID index
493+
# check 4D shape when there is no StackID index
493494
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
494495
sid_seq = (1, 1, 1, 1, 1, 1)
495-
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
496+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
496497
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
497498
# check 4D stack number matching when there is no StackID index
498499
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
499500
sid_seq = (1, 1, 1, 1, 1, 2)
500-
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
501+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
501502
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
502-
# check 3D shape when StackID index is 1
503+
# check 3D shape when StackID index is 1
503504
div_seq = ((1, 1), (2, 1), (3, 1), (4, 1))
504-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
505+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
505506
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
506507
# Check stack number matching when StackID index is 1
507-
div_seq = ((1, 1), (2, 1), (3, 2), (4, 1))
508-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
508+
div_seq = ((1, 1), (2, 1), (3, 2), (4, 1))
509+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
509510
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
510-
# Make some fake frame data for 4D when StackID index is 1
511+
# Make some fake frame data for 4D when StackID index is 1
511512
div_seq = ((1, 1, 1), (2, 1, 1), (1, 1, 2), (2, 1, 2),
512513
(1, 1, 3), (2, 1, 3))
513-
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
514+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
514515
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
515516

516517
def test_iop(self):
@@ -614,6 +615,7 @@ def test_data_real(self):
614615
assert_equal(sha1(dat_str).hexdigest(),
615616
'149323269b0af92baa7508e19ca315240f77fa8c')
616617

618+
@dicom_test
617619
def test_data_fake(self):
618620
# Test algorithm for get_data
619621
fake_mf = copy(self.MINIMAL_MF)
@@ -627,9 +629,9 @@ def test_data_fake(self):
627629
assert_raises(didw.WrapperError, dw.get_data)
628630
# Make shape and indices
629631
fake_mf['Rows'] = 2
630-
fake_mf['Columns'] = 3
631-
dim_idxs = ((1, 1), (1, 2), (1, 3), (1, 4))
632-
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
632+
fake_mf['Columns'] = 3
633+
dim_idxs = ((1, 1), (1, 2), (1, 3), (1, 4))
634+
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
633635
assert_equal(MFW(fake_mf).image_shape, (2, 3, 4))
634636
# Still fails - no data
635637
assert_raises(didw.WrapperError, dw.get_data)
@@ -644,9 +646,9 @@ def test_data_fake(self):
644646
fake_mf['RescaleSlope'] = 2.0
645647
fake_mf['RescaleIntercept'] = -1
646648
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
647-
# Check slice sorting
648-
dim_idxs = ((1, 4), (1, 2), (1, 3), (1, 1))
649-
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
649+
# Check slice sorting
650+
dim_idxs = ((1, 4), (1, 2), (1, 3), (1, 1))
651+
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
650652
sorted_data = data[..., [3, 1, 2, 0]]
651653
fake_mf['pixel_array'] = np.rollaxis(sorted_data, 2)
652654
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
@@ -668,7 +670,7 @@ def test_data_fake(self):
668670
[1, 2, 1, 2],
669671
[1, 3, 1, 2],
670672
[1, 1, 1, 2]]
671-
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
673+
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
672674
shape = (2, 3, 4, 2, 2)
673675
data = np.arange(np.prod(shape)).reshape(shape)
674676
sorted_data = data.reshape(shape[:2] + (-1,), order='F')

0 commit comments

Comments
 (0)