@@ -364,7 +364,7 @@ def test_decimal_rescale():
364
364
assert dw .get_data ().dtype != np .dtype (object )
365
365
366
366
367
- def fake_frames (seq_name , field_name , value_seq ):
367
+ def fake_frames (seq_name , field_name , value_seq , frame_seq = None ):
368
368
"""Make fake frames for multiframe testing
369
369
370
370
Parameters
@@ -375,6 +375,8 @@ def fake_frames(seq_name, field_name, value_seq):
375
375
name of field within sequence
376
376
value_seq : length N sequence
377
377
sequence of values
378
+ frame_seq : length N list
379
+ previous result from this function to update
378
380
379
381
Returns
380
382
-------
@@ -386,19 +388,28 @@ def fake_frames(seq_name, field_name, value_seq):
386
388
class Fake :
387
389
pass
388
390
389
- frames = []
390
- for value in value_seq :
391
- fake_frame = Fake ()
391
+ if frame_seq == None :
392
+ frame_seq = [ Fake () for _ in range ( len ( value_seq ))]
393
+ for value , fake_frame in zip ( value_seq , frame_seq ):
392
394
fake_element = Fake ()
393
395
setattr (fake_element , field_name , value )
394
396
setattr (fake_frame , seq_name , [fake_element ])
395
- frames .append (fake_frame )
396
- return frames
397
+ return frame_seq
397
398
398
399
399
- def fake_shape_dependents (div_seq , sid_seq = None , sid_dim = None ):
400
+ def fake_shape_dependents (
401
+ div_seq ,
402
+ sid_seq = None ,
403
+ sid_dim = None ,
404
+ ipp_seq = None ,
405
+ slice_dim = None ,
406
+ flip_ipp_idx_corr = False ,
407
+ ):
400
408
"""Make a fake dictionary of data that ``image_shape`` is dependent on.
401
409
410
+ If you are providing the ``ipp_seq`` argument, they should be generated using
411
+ a slice normal aligned with the z-axis (i.e. iop == (0, 1, 0, 1, 0, 0)).
412
+
402
413
Parameters
403
414
----------
404
415
div_seq : list of tuples
@@ -407,39 +418,86 @@ def fake_shape_dependents(div_seq, sid_seq=None, sid_dim=None):
407
418
list of values to use for the `StackID` of each frame.
408
419
sid_dim : int
409
420
the index of the column in 'div_seq' to use as 'sid_seq'
421
+ ipp_seq : list of tuples
422
+ list of values to use for `ImagePositionPatient` for each frame
423
+ slice_dim : int
424
+ the index of the column in 'div_seq' corresponding to slices
425
+ flip_ipp_idx_corr : bool
426
+ generate ipp values so slice location is negatively correlated with slice index
410
427
"""
411
428
412
- class DimIdxSeqElem :
429
+ class PrintBase :
430
+ def __repr__ (self ):
431
+ attr_strs = []
432
+ for attr in dir (self ):
433
+ if attr [0 ].isupper ():
434
+ attr_strs .append (f'{ attr } ={ getattr (self , attr )} ' )
435
+ return f"{ self .__class__ .__name__ } ({ ', ' .join (attr_strs )} )"
436
+
437
+ class DimIdxSeqElem (PrintBase ):
413
438
def __init__ (self , dip = (0 , 0 ), fgp = None ):
414
439
self .DimensionIndexPointer = dip
415
440
if fgp is not None :
416
441
self .FunctionalGroupPointer = fgp
417
442
418
- class FrmContSeqElem :
443
+ class FrmContSeqElem ( PrintBase ) :
419
444
def __init__ (self , div , sid ):
420
445
self .DimensionIndexValues = div
421
446
self .StackID = sid
422
447
423
- class PerFrmFuncGrpSeqElem :
424
- def __init__ (self , div , sid ):
448
+ class PlnPosSeqElem (PrintBase ):
449
+ def __init__ (self , ipp ):
450
+ self .ImagePositionPatient = ipp
451
+
452
+ class PlnOrientSeqElem (PrintBase ):
453
+ def __init__ (self , iop ):
454
+ self .ImageOrientationPatient = iop
455
+
456
+ class PerFrmFuncGrpSeqElem (PrintBase ):
457
+ def __init__ (self , div , sid , ipp , iop ):
425
458
self .FrameContentSequence = [FrmContSeqElem (div , sid )]
459
+ self .PlanePositionSequence = [PlnPosSeqElem (ipp )]
460
+ self .PlaneOrientationSequence = [PlnOrientSeqElem (iop )]
426
461
427
462
# if no StackID values passed in then use the values at index 'sid_dim' in
428
463
# the value for DimensionIndexValues for it
464
+ n_indices = len (div_seq [0 ])
429
465
if sid_seq is None :
430
466
if sid_dim is None :
431
467
sid_dim = 0
432
468
sid_seq = [div [sid_dim ] for div in div_seq ]
433
- # create the DimensionIndexSequence
469
+ # Determine slice_dim and create per-slice ipp information
470
+ if slice_dim is None :
471
+ slice_dim = 1 if sid_dim == 0 else 0
434
472
num_of_frames = len (div_seq )
435
- dim_idx_seq = [DimIdxSeqElem ()] * num_of_frames
473
+ frame_slc_indices = np .array (div_seq )[:, slice_dim ]
474
+ uniq_slc_indices = np .unique (frame_slc_indices )
475
+ n_slices = len (uniq_slc_indices )
476
+ assert num_of_frames % n_slices == 0
477
+ iop_seq = [(0.0 , 1.0 , 0.0 , 1.0 , 0.0 , 0.0 ) for _ in range (num_of_frames )]
478
+ if ipp_seq is None :
479
+ slc_locs = np .linspace (- 1.0 , 1.0 , n_slices )
480
+ if flip_ipp_idx_corr :
481
+ slc_locs = slc_locs [::- 1 ]
482
+ slc_idx_loc = {
483
+ div_idx : slc_locs [arr_idx ] for arr_idx , div_idx in enumerate (np .sort (uniq_slc_indices ))
484
+ }
485
+ ipp_seq = [(- 1.0 , - 1.0 , slc_idx_loc [idx ]) for idx in frame_slc_indices ]
486
+ else :
487
+ assert flip_ipp_idx_corr is False # caller can flip it themselves
488
+ assert len (ipp_seq ) == num_of_frames
489
+ # create the DimensionIndexSequence
490
+ dim_idx_seq = [DimIdxSeqElem ()] * n_indices
436
491
# add an entry for StackID into the DimensionIndexSequence
437
492
if sid_dim is not None :
438
493
sid_tag = pydicom .datadict .tag_for_keyword ('StackID' )
439
494
fcs_tag = pydicom .datadict .tag_for_keyword ('FrameContentSequence' )
440
495
dim_idx_seq [sid_dim ] = DimIdxSeqElem (sid_tag , fcs_tag )
441
496
# create the PerFrameFunctionalGroupsSequence
442
- frames = [PerFrmFuncGrpSeqElem (div , sid ) for div , sid in zip (div_seq , sid_seq )]
497
+ frames = [
498
+ PerFrmFuncGrpSeqElem (div , sid , ipp , iop )
499
+ for div , sid , ipp , iop in zip (div_seq , sid_seq , ipp_seq , iop_seq )
500
+ ]
443
501
return {
444
502
'NumberOfFrames' : num_of_frames ,
445
503
'DimensionIndexSequence' : dim_idx_seq ,
@@ -480,7 +538,15 @@ def test_shape(self):
480
538
# PerFrameFunctionalGroupsSequence does not match NumberOfFrames
481
539
with pytest .raises (AssertionError ):
482
540
dw .image_shape
483
- # check 3D shape when StackID index is 0
541
+ # check 2D shape with StackID index is 0
542
+ div_seq = ((1 , 1 ),)
543
+ fake_mf .update (fake_shape_dependents (div_seq , sid_dim = 0 ))
544
+ assert MFW (fake_mf ).image_shape == (32 , 64 )
545
+ # Check 2D shape with extraneous extra indices
546
+ div_seq = ((1 , 1 , 2 ),)
547
+ fake_mf .update (fake_shape_dependents (div_seq , sid_dim = 0 ))
548
+ assert MFW (fake_mf ).image_shape == (32 , 64 )
549
+ # Check 3D shape when StackID index is 0
484
550
div_seq = ((1 , 1 ), (1 , 2 ), (1 , 3 ), (1 , 4 ))
485
551
fake_mf .update (fake_shape_dependents (div_seq , sid_dim = 0 ))
486
552
assert MFW (fake_mf ).image_shape == (32 , 64 , 4 )
@@ -541,6 +607,18 @@ def test_shape(self):
541
607
div_seq = ((1 , 1 , 1 ), (2 , 1 , 1 ), (1 , 1 , 2 ), (2 , 1 , 2 ), (1 , 1 , 3 ), (2 , 1 , 3 ))
542
608
fake_mf .update (fake_shape_dependents (div_seq , sid_dim = 1 ))
543
609
assert MFW (fake_mf ).image_shape == (32 , 64 , 2 , 3 )
610
+ # Test with combo indices, here with the last two needing to be combined into
611
+ # a single index corresponding to [(1, 1), (1, 1), (2, 1), (2, 1), (2, 2), (2, 2)]
612
+ div_seq = (
613
+ (1 , 1 , 1 , 1 ),
614
+ (1 , 2 , 1 , 1 ),
615
+ (1 , 1 , 2 , 1 ),
616
+ (1 , 2 , 2 , 1 ),
617
+ (1 , 1 , 2 , 2 ),
618
+ (1 , 2 , 2 , 2 ),
619
+ )
620
+ fake_mf .update (fake_shape_dependents (div_seq , sid_dim = 0 ))
621
+ assert MFW (fake_mf ).image_shape == (32 , 64 , 2 , 3 )
544
622
545
623
def test_iop (self ):
546
624
# Test Image orient patient for multiframe
@@ -608,22 +686,30 @@ def test_image_position(self):
608
686
with pytest .raises (didw .WrapperError ):
609
687
dw .image_position
610
688
# Make a fake frame
611
- fake_frame = fake_frames (
612
- 'PlanePositionSequence' , 'ImagePositionPatient' , [[- 2.0 , 3.0 , 7 ]]
613
- )[0 ]
614
- fake_mf ['SharedFunctionalGroupsSequence' ] = [fake_frame ]
689
+ iop = [0 , 1 , 0 , 1 , 0 , 0 ]
690
+ frames = fake_frames ('PlaneOrientationSequence' , 'ImageOrientationPatient' , [iop ])
691
+ frames = fake_frames (
692
+ 'PlanePositionSequence' , 'ImagePositionPatient' , [[- 2.0 , 3.0 , 7 ]], frames
693
+ )
694
+ fake_mf ['SharedFunctionalGroupsSequence' ] = frames
615
695
assert_array_equal (MFW (fake_mf ).image_position , [- 2 , 3 , 7 ])
616
696
fake_mf ['SharedFunctionalGroupsSequence' ] = [None ]
617
697
with pytest .raises (didw .WrapperError ):
618
698
MFW (fake_mf ).image_position
619
- fake_mf ['PerFrameFunctionalGroupsSequence' ] = [ fake_frame ]
699
+ fake_mf ['PerFrameFunctionalGroupsSequence' ] = frames
620
700
assert_array_equal (MFW (fake_mf ).image_position , [- 2 , 3 , 7 ])
621
701
# Check lists of Decimals work
622
- fake_frame .PlanePositionSequence [0 ].ImagePositionPatient = [
702
+ frames [ 0 ] .PlanePositionSequence [0 ].ImagePositionPatient = [
623
703
Decimal (str (v )) for v in [- 2 , 3 , 7 ]
624
704
]
625
705
assert_array_equal (MFW (fake_mf ).image_position , [- 2 , 3 , 7 ])
626
706
assert MFW (fake_mf ).image_position .dtype == float
707
+ # We should get minimum along slice normal with multiple frames
708
+ frames = fake_frames ('PlaneOrientationSequence' , 'ImageOrientationPatient' , [iop ] * 2 )
709
+ ipps = [[- 2.0 , 3.0 , 7 ], [- 2.0 , 3.0 , 6 ]]
710
+ frames = fake_frames ('PlanePositionSequence' , 'ImagePositionPatient' , ipps , frames )
711
+ fake_mf ['PerFrameFunctionalGroupsSequence' ] = frames
712
+ assert_array_equal (MFW (fake_mf ).image_position , [- 2 , 3 , 6 ])
627
713
628
714
@dicom_test
629
715
@pytest .mark .xfail (reason = 'Not packaged in install' , raises = FileNotFoundError )
@@ -644,7 +730,7 @@ def test_data_real(self):
644
730
if endian_codes [data .dtype .byteorder ] == '>' :
645
731
data = data .byteswap ()
646
732
dat_str = data .tobytes ()
647
- assert sha1 (dat_str ).hexdigest () == '149323269b0af92baa7508e19ca315240f77fa8c '
733
+ assert sha1 (dat_str ).hexdigest () == 'dc011bb49682fb78f3cebacf965cb65cc9daba7d '
648
734
649
735
@dicom_test
650
736
def test_slicethickness_fallback (self ):
@@ -665,7 +751,7 @@ def test_data_derived_shape(self):
665
751
def test_data_trace (self ):
666
752
# Test that a standalone trace volume is found and not dropped
667
753
dw = didw .wrapper_from_file (DATA_FILE_SIEMENS_TRACE )
668
- assert dw .image_shape == (72 , 72 , 39 , 1 )
754
+ assert dw .image_shape == (72 , 72 , 39 )
669
755
670
756
@dicom_test
671
757
@needs_nibabel_data ('nitest-dicom' )
0 commit comments