@@ -467,6 +467,25 @@ def __init__(self, dcm_data):
467
467
self .shared = dcm_data .get ('SharedFunctionalGroupsSequence' )[0 ]
468
468
except TypeError :
469
469
raise WrapperError ('SharedFunctionalGroupsSequence is empty.' )
470
+ # Try to determine slice order and minimal image position patient
471
+ self ._frame_slc_ord = self ._ipp = None
472
+ try :
473
+ frame_ipps = [f .PlanePositionSequence [0 ].ImagePositionPatient for f in self .frames ]
474
+ except AttributeError :
475
+ try :
476
+ frame_ipps = [self .shared .PlanePositionSequence [0 ].ImagePositionPatient ]
477
+ except AttributeError :
478
+ frame_ipps = None
479
+ if frame_ipps is not None and all (ipp is not None for ipp in frame_ipps ):
480
+ frame_ipps = [np .array (list (map (float , ipp ))) for ipp in frame_ipps ]
481
+ frame_slc_pos = [np .inner (ipp , self .slice_normal ) for ipp in frame_ipps ]
482
+ rnd_slc_pos = np .round (frame_slc_pos , 4 )
483
+ uniq_slc_pos = np .unique (rnd_slc_pos )
484
+ pos_ord_map = {
485
+ val : order for val , order in zip (uniq_slc_pos , np .argsort (uniq_slc_pos ))
486
+ }
487
+ self ._frame_slc_ord = [pos_ord_map [pos ] for pos in rnd_slc_pos ]
488
+ self ._ipp = frame_ipps [np .argmin (frame_slc_pos )]
470
489
self ._shape = None
471
490
472
491
@cached_property
@@ -509,14 +528,16 @@ def image_shape(self):
509
528
if hasattr (first_frame , 'get' ) and first_frame .get ([0x18 , 0x9117 ]):
510
529
# DWI image may include derived isotropic, ADC or trace volume
511
530
try :
512
- anisotropic = pydicom .Sequence (
513
- frame
514
- for frame in self .frames
515
- if frame .MRDiffusionSequence [0 ].DiffusionDirectionality != 'ISOTROPIC'
516
- )
531
+ aniso_frames = pydicom .Sequence ()
532
+ aniso_slc_ord = []
533
+ for slc_ord , frame in zip (self ._frame_slc_ord , self .frames ):
534
+ if frame .MRDiffusionSequence [0 ].DiffusionDirectionality != 'ISOTROPIC' :
535
+ aniso_frames .append (frame )
536
+ aniso_slc_ord .append (slc_ord )
517
537
# Image contains DWI volumes followed by derived images; remove derived images
518
- if len (anisotropic ) != 0 :
519
- self .frames = anisotropic
538
+ if len (aniso_frames ) != 0 :
539
+ self .frames = aniso_frames
540
+ self ._frame_slc_ord = aniso_slc_ord
520
541
except IndexError :
521
542
# Sequence tag is found but missing items!
522
543
raise WrapperError ('Diffusion file missing information' )
@@ -554,23 +575,85 @@ def image_shape(self):
554
575
raise WrapperError ('Missing information, cannot remove indices with confidence.' )
555
576
derived_dim_idx = dim_seq .index (derived_tag )
556
577
frame_indices = np .delete (frame_indices , derived_dim_idx , axis = 1 )
557
- # account for the 2 additional dimensions (row and column) not included
558
- # in the indices
559
- n_dim = frame_indices .shape [1 ] + 2
578
+ dim_seq .pop (derived_dim_idx )
579
+ # Determine the shape and which indices to use
580
+ shape = [rows , cols ]
581
+ curr_parts = n_frames
582
+ frames_per_part = 1
583
+ del_indices = {}
584
+ stackpos_tag = pydicom .datadict .tag_for_keyword ('InStackPositionNumber' )
585
+ slice_dim_idx = dim_seq .index (stackpos_tag )
586
+ for row_idx , row in enumerate (frame_indices .T ):
587
+ unique = np .unique (row )
588
+ count = len (unique )
589
+ if curr_parts == 1 or (count == 1 and row_idx != slice_dim_idx ):
590
+ del_indices [row_idx ] = count
591
+ continue
592
+ # Replace slice indices with order determined from slice positions along normal
593
+ if row_idx == slice_dim_idx :
594
+ if len (shape ) > 2 :
595
+ raise WrapperError ('Non-singular index precedes the slice index' )
596
+ row = self ._frame_slc_ord
597
+ frame_indices .T [row_idx , :] = row
598
+ unique = np .unique (row )
599
+ if len (unique ) != count :
600
+ raise WrapperError ("Number of slice indices and positions don't match" )
601
+ elif count == n_frames :
602
+ if shape [- 1 ] == 'remaining' :
603
+ raise WrapperError ('At most one index have ambiguous size' )
604
+ shape .append ('remaining' )
605
+ continue
606
+ new_parts , leftover = divmod (curr_parts , count )
607
+ expected = new_parts * frames_per_part
608
+ if leftover != 0 or any (np .count_nonzero (row == val ) != expected for val in unique ):
609
+ if row_idx == slice_dim_idx :
610
+ raise WrapperError ('Missing slices from multiframe' )
611
+ del_indices [row_idx ] = count
612
+ continue
613
+ if shape [- 1 ] == 'remaining' :
614
+ shape [- 1 ] = new_parts
615
+ frames_per_part *= shape [- 1 ]
616
+ new_parts = 1
617
+ frames_per_part *= count
618
+ shape .append (count )
619
+ curr_parts = new_parts
620
+ if shape [- 1 ] == 'remaining' :
621
+ if curr_parts > 1 :
622
+ shape [- 1 ] = curr_parts
623
+ curr_parts = 1
624
+ else :
625
+ del_indices [len (shape )] = 1
626
+ shape = shape [:- 1 ]
627
+ if del_indices :
628
+ if curr_parts > 1 :
629
+ ns_failed = [k for k , v in del_indices .items () if v != 1 ]
630
+ if len (ns_failed ) > 1 :
631
+ # If some indices weren't used yet but we still have unaccounted for
632
+ # partitions, try combining indices into single tuple and using that
633
+ tup_dtype = np .dtype (',' .join (['I' ] * len (ns_failed )))
634
+ row = [tuple (x for x in vals ) for vals in frame_indices [:, ns_failed ]]
635
+ row = np .array (row , dtype = tup_dtype )
636
+ frame_indices = np .delete (frame_indices , np .array (list (del_indices .keys ())), axis = 1 )
637
+ if curr_parts > 1 and len (ns_failed ) > 1 :
638
+ unique = np .unique (row , axis = 0 )
639
+ count = len (unique )
640
+ new_parts , rem = divmod (curr_parts , count )
641
+ allowed_val_counts = [new_parts * frames_per_part , n_frames ]
642
+ if rem == 0 and all (
643
+ np .count_nonzero (row == val ) in allowed_val_counts for val in unique
644
+ ):
645
+ shape .append (count )
646
+ curr_parts = new_parts
647
+ ord_vals = np .argsort (unique )
648
+ order = {tuple (unique [i ]): ord_vals [i ] for i in range (count )}
649
+ ord_row = np .array ([order [tuple (v )] for v in row ])
650
+ frame_indices = np .hstack (
651
+ [frame_indices , np .array (ord_row ).reshape ((n_frames , 1 ))]
652
+ )
653
+ if curr_parts > 1 :
654
+ raise WrapperError ('Unable to determine sorting of final dimension(s)' )
560
655
# Store frame indices
561
656
self ._frame_indices = frame_indices
562
- if n_dim < 4 : # 3D volume
563
- return rows , cols , n_frames
564
- # More than 3 dimensions
565
- ns_unique = [len (np .unique (row )) for row in self ._frame_indices .T ]
566
- shape = (rows , cols ) + tuple (ns_unique )
567
- n_vols = np .prod (shape [3 :])
568
- n_frames_calc = n_vols * shape [2 ]
569
- if n_frames != n_frames_calc :
570
- raise WrapperError (
571
- f'Calculated # of frames ({ n_frames_calc } ={ n_vols } *{ shape [2 ]} ) '
572
- f'of shape { shape } does not match NumberOfFrames { n_frames } .'
573
- )
574
657
return tuple (shape )
575
658
576
659
@cached_property
@@ -610,18 +693,11 @@ def voxel_sizes(self):
610
693
# Ensure values are float rather than Decimal
611
694
return tuple (map (float , list (pix_space ) + [zs ]))
612
695
613
- @cached_property
696
+ @property
614
697
def image_position (self ):
615
- try :
616
- ipp = self .shared .PlanePositionSequence [0 ].ImagePositionPatient
617
- except AttributeError :
618
- try :
619
- ipp = self .frames [0 ].PlanePositionSequence [0 ].ImagePositionPatient
620
- except AttributeError :
621
- raise WrapperError ('Cannot get image position from dicom' )
622
- if ipp is None :
623
- return None
624
- return np .array (list (map (float , ipp )))
698
+ if self ._ipp is None :
699
+ raise WrapperError ('Not enough information for image_position_patient' )
700
+ return self ._ipp
625
701
626
702
@cached_property
627
703
def series_signature (self ):
@@ -640,10 +716,11 @@ def get_data(self):
640
716
raise WrapperError ('No valid information for image shape' )
641
717
data = self .get_pixel_array ()
642
718
# Roll frames axis to last
643
- data = data .transpose ((1 , 2 , 0 ))
644
- # Sort frames with first index changing fastest, last slowest
645
- sorted_indices = np .lexsort (self ._frame_indices .T )
646
- data = data [..., sorted_indices ]
719
+ if len (data .shape ) > 2 :
720
+ data = data .transpose ((1 , 2 , 0 ))
721
+ # Sort frames with first index changing fastest, last slowest
722
+ sorted_indices = np .lexsort (self ._frame_indices .T )
723
+ data = data [..., sorted_indices ]
647
724
data = data .reshape (shape , order = 'F' )
648
725
return self ._scale_data (data )
649
726
0 commit comments