Skip to content

Commit 7397e60

Browse files
committed
Merge pull request #439 from embaker/bf-stackid-assumption
MRG: Remove assumption about StackID index In determining the shape of a multi-frame DICOM, the first value in the DimensionIndexValues was taken to be the StackID. The DICOM standard does not appear to guarantee this to always be true. Replaced the assumption by using the DimensionIndexSequence to determine the position of the StackID in the DimensionIndexValues.
2 parents be9b778 + 9eb7fd5 commit 7397e60

File tree

2 files changed

+147
-57
lines changed

2 files changed

+147
-57
lines changed

nibabel/nicom/dicomwrappers.py

100644100755
Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .dwiparams import B2q, nearest_pos_semi_def, q2bg
2222
from ..openers import ImageOpener
2323
from ..onetime import setattr_on_read as one_time
24-
24+
from ..pydicom_compat import pydicom
2525

2626
class WrapperError(Exception):
2727
pass
@@ -461,7 +461,29 @@ def __init__(self, dcm_data):
461461

462462
@one_time
463463
def image_shape(self):
464-
"""The array shape as it will be returned by ``get_data()``"""
464+
"""The array shape as it will be returned by ``get_data()``
465+
466+
The shape is determined by the *Rows* DICOM attribute, *Columns*
467+
DICOM attribute, and the set of frame indices given by the
468+
*FrameContentSequence[0].DimensionIndexValues* DICOM attribute of each
469+
element in the *PerFrameFunctionalGroupsSequence*. The first two
470+
axes of the returned shape correspond to the rows, and columns
471+
respectively. The remaining axes correspond to those of the frame
472+
indices with order preserved.
473+
474+
What each axis in the frame indices refers to is given by the
475+
corresponding entry in the *DimensionIndexSequence* DICOM attribute.
476+
**WARNING**: Any axis refering to the *StackID* DICOM attribute will
477+
have been removed from the frame indices in determining the shape. This
478+
is because only a file containing a single stack is currently allowed by
479+
this wrapper.
480+
481+
References
482+
----------
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
486+
"""
465487
rows, cols = self.get('Rows'), self.get('Columns')
466488
if None in (rows, cols):
467489
raise WrapperError("Rows and/or Columns are empty.")
@@ -471,13 +493,25 @@ def image_shape(self):
471493
frame_indices = np.array(
472494
[frame.FrameContentSequence[0].DimensionIndexValues
473495
for frame in self.frames])
474-
n_dim = frame_indices.shape[1] + 1
475-
# Check there is only one multiframe stack index
476-
if np.any(np.diff(frame_indices[:, 0])):
477-
raise WrapperError("File contains more than one StackID. Cannot "
478-
"handle multi-stack files")
496+
# Check that there is only one multiframe stack index
497+
stack_ids = set(frame.FrameContentSequence[0].StackID
498+
for frame in self.frames)
499+
if len(stack_ids) > 1:
500+
raise WrapperError("File contains more than one StackID. "
501+
"Cannot handle multi-stack files")
502+
# Determine if one of the dimension indices refers to the stack id
503+
dim_seq = [dim.DimensionIndexPointer
504+
for dim in self.get('DimensionIndexSequence')]
505+
stackid_tag = pydicom.datadict.tag_for_name('StackID')
506+
# remove the stack id axis if present
507+
if stackid_tag in dim_seq:
508+
stackid_dim_idx = dim_seq.index(stackid_tag)
509+
frame_indices = np.delete(frame_indices, stackid_dim_idx, axis=1)
510+
# account for the 2 additional dimensions (row and column) not included
511+
# in the indices
512+
n_dim = frame_indices.shape[1] + 2
479513
# Store frame indices
480-
self._frame_indices = frame_indices[:, 1:]
514+
self._frame_indices = frame_indices
481515
if n_dim < 4: # 3D volume
482516
return rows, cols, n_frames
483517
# More than 3 dimensions

nibabel/nicom/tests/test_dicomwrappers.py

100644100755
Lines changed: 105 additions & 49 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,8 +379,54 @@ class Fake(object):
379379
setattr(fake_frame, seq_name, [fake_element])
380380
frames.append(fake_frame)
381381
return frames
382-
383-
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+
384430
class TestMultiFrameWrapper(TestCase):
385431
# Test MultiframeWrapper
386432
MINIMAL_MF = {
@@ -406,47 +452,66 @@ def test_shape(self):
406452
assert_raises(AssertionError, getattr, dw, 'image_shape')
407453
fake_mf['NumberOfFrames'] = 4
408454
# PerFrameFunctionalGroupsSequence does not match NumberOfFrames
409-
assert_raises(AssertionError, getattr, dw, 'image_shape')
410-
# Make some fake frame data for 3D
411-
412-
def my_fake_frames(div_seq):
413-
return fake_frames('FrameContentSequence',
414-
'DimensionIndexValues',
415-
div_seq)
416-
div_seq = ((1, 1), (1, 2), (1, 3), (1, 4))
417-
frames = my_fake_frames(div_seq)
418-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
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))
419459
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
420-
# Check stack number matching
460+
# Check stack number matching when StackID index is 0
421461
div_seq = ((1, 1), (1, 2), (1, 3), (2, 4))
422-
frames = my_fake_frames(div_seq)
423-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
462+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
424463
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
425-
# Make some fake frame data for 4D
426-
fake_mf['NumberOfFrames'] = 6
464+
# Make some fake frame data for 4D when StackID index is 0
427465
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
428-
(1, 1, 3), (1, 2, 3))
429-
frames = my_fake_frames(div_seq)
430-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
466+
(1, 1, 3), (1, 2, 3))
467+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
431468
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
432-
# Check stack number matching for 4D
469+
# Check stack number matching for 4D when StackID index is 0
433470
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 2), (1, 2, 2),
434-
(1, 1, 3), (2, 2, 3))
435-
frames = my_fake_frames(div_seq)
436-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
471+
(1, 1, 3), (2, 2, 3))
472+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
437473
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
438-
# Check indices can be non-contiguous
474+
# Check indices can be non-contiguous when StackID index is 0
439475
div_seq = ((1, 1, 1), (1, 2, 1), (1, 1, 3), (1, 2, 3))
440-
frames = my_fake_frames(div_seq)
441-
fake_mf['NumberOfFrames'] = 4
442-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
476+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
443477
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
444-
# Check indices can include zero
478+
# Check indices can include zero when StackID index is 0
445479
div_seq = ((1, 1, 0), (1, 2, 0), (1, 1, 3), (1, 2, 3))
446-
frames = my_fake_frames(div_seq)
447-
fake_mf['NumberOfFrames'] = 4
448-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
480+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=0))
449481
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 2))
482+
# check 3D shape when there is no StackID index
483+
div_seq = ((1,), (2,), (3,), (4,))
484+
sid_seq = (1, 1, 1, 1)
485+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
486+
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
487+
# check 3D stack number matching when there is no StackID index
488+
div_seq = ((1,), (2,), (3,), (4,))
489+
sid_seq = (1, 1, 1, 2)
490+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
491+
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
492+
# check 4D shape when there is no StackID index
493+
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
494+
sid_seq = (1, 1, 1, 1, 1, 1)
495+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
496+
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
497+
# check 4D stack number matching when there is no StackID index
498+
div_seq = ((1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3))
499+
sid_seq = (1, 1, 1, 1, 1, 2)
500+
fake_mf.update(fake_shape_dependents(div_seq, sid_seq=sid_seq))
501+
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
502+
# check 3D shape when StackID index is 1
503+
div_seq = ((1, 1), (2, 1), (3, 1), (4, 1))
504+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
505+
assert_equal(MFW(fake_mf).image_shape, (32, 64, 4))
506+
# 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))
509+
assert_raises(didw.WrapperError, getattr, MFW(fake_mf), 'image_shape')
510+
# Make some fake frame data for 4D when StackID index is 1
511+
div_seq = ((1, 1, 1), (2, 1, 1), (1, 1, 2), (2, 1, 2),
512+
(1, 1, 3), (2, 1, 3))
513+
fake_mf.update(fake_shape_dependents(div_seq, sid_dim=1))
514+
assert_equal(MFW(fake_mf).image_shape, (32, 64, 2, 3))
450515

451516
def test_iop(self):
452517
# Test Image orient patient for multiframe
@@ -562,12 +627,9 @@ def test_data_fake(self):
562627
assert_raises(didw.WrapperError, dw.get_data)
563628
# Make shape and indices
564629
fake_mf['Rows'] = 2
565-
fake_mf['Columns'] = 3
566-
fake_mf['NumberOfFrames'] = 4
567-
frames = fake_frames('FrameContentSequence',
568-
'DimensionIndexValues',
569-
((1, 1), (1, 2), (1, 3), (1, 4)))
570-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
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))
571633
assert_equal(MFW(fake_mf).image_shape, (2, 3, 4))
572634
# Still fails - no data
573635
assert_raises(didw.WrapperError, dw.get_data)
@@ -582,11 +644,9 @@ def test_data_fake(self):
582644
fake_mf['RescaleSlope'] = 2.0
583645
fake_mf['RescaleIntercept'] = -1
584646
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
585-
# Check slice sorting
586-
frames = fake_frames('FrameContentSequence',
587-
'DimensionIndexValues',
588-
((1, 4), (1, 2), (1, 3), (1, 1)))
589-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
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))
590650
sorted_data = data[..., [3, 1, 2, 0]]
591651
fake_mf['pixel_array'] = np.rollaxis(sorted_data, 2)
592652
assert_array_equal(MFW(fake_mf).get_data(), data * 2.0 - 1)
@@ -608,11 +668,7 @@ def test_data_fake(self):
608668
[1, 2, 1, 2],
609669
[1, 3, 1, 2],
610670
[1, 1, 1, 2]]
611-
frames = fake_frames('FrameContentSequence',
612-
'DimensionIndexValues',
613-
dim_idxs)
614-
fake_mf['PerFrameFunctionalGroupsSequence'] = frames
615-
fake_mf['NumberOfFrames'] = len(frames)
671+
fake_mf.update(fake_shape_dependents(dim_idxs, sid_dim=0))
616672
shape = (2, 3, 4, 2, 2)
617673
data = np.arange(np.prod(shape)).reshape(shape)
618674
sorted_data = data.reshape(shape[:2] + (-1,), order='F')

0 commit comments

Comments
 (0)