33import warnings
44from collections import OrderedDict , defaultdict
55from pathlib import Path
6- from typing import Dict , List , Optional , Tuple
6+ from typing import Dict , List , Optional , Tuple , Any
77
88from rich .console import Console
99
3131
3232def parse_path (
3333 path : Path ,
34- legacy : bool = False ,
35- remote_files_that_require_legacy_scaling : Optional [List ] = [],
34+ remote_files_that_require_legacy_scaling : Dict [Path , Dict [str , Any ]] = {},
3635) -> Optional [List [dt .AnnotationFile ]]:
3736 """
3837 Parses the given ``nifti`` file and returns a ``List[dt.AnnotationFile]`` with the parsed
@@ -42,9 +41,8 @@ def parse_path(
4241 ----------
4342 path : Path
4443 The ``Path`` to the ``nifti`` file.
45- legacy : bool, default: False
46- If ``True``, the function will not attempt to resize the annotations to isotropic pixel dimensions.
47- If ``False``, the function will resize the annotations to isotropic pixel dimensions.
44+ remote_files_that_require_legacy_scaling : Optional[Dict[Path, Dict[str, Any]]]
45+ A dictionary of remote file full paths to their slot affine maps
4846
4947 Returns
5048 -------
@@ -78,16 +76,20 @@ def parse_path(
7876 return None
7977 annotation_files = []
8078 for nifti_annotation in nifti_annotations :
81- legacy = nifti_annotation ["image" ] in remote_files_that_require_legacy_scaling
79+ remote_file_path = Path (nifti_annotation ["image" ])
80+ if not str (remote_file_path ).startswith ("/" ):
81+ remote_file_path = Path ("/" + str (remote_file_path ))
82+
8283 annotation_file = _parse_nifti (
8384 Path (nifti_annotation ["label" ]),
84- nifti_annotation ["image" ],
85+ Path ( nifti_annotation ["image" ]) ,
8586 path ,
8687 class_map = nifti_annotation .get ("class_map" ),
8788 mode = nifti_annotation .get ("mode" , "image" ),
8889 slot_names = nifti_annotation .get ("slot_names" , []),
8990 is_mpr = nifti_annotation .get ("is_mpr" , False ),
90- legacy = legacy ,
91+ remote_file_path = remote_file_path ,
92+ remote_files_that_require_legacy_scaling = remote_files_that_require_legacy_scaling ,
9193 )
9294 annotation_files .append (annotation_file )
9395 return annotation_files
@@ -101,10 +103,16 @@ def _parse_nifti(
101103 mode : str ,
102104 slot_names : List [str ],
103105 is_mpr : bool ,
104- legacy : bool = False ,
106+ remote_file_path : Path ,
107+ remote_files_that_require_legacy_scaling : Dict [Path , Dict [str , Any ]] = {},
105108) -> dt .AnnotationFile :
106- img , pixdims = process_nifti (nib .load (nifti_path ))
109+ img , pixdims = process_nifti (
110+ nib .load (nifti_path ),
111+ remote_file_path = remote_file_path ,
112+ remote_files_that_require_legacy_scaling = remote_files_that_require_legacy_scaling ,
113+ )
107114
115+ legacy = remote_file_path in remote_files_that_require_legacy_scaling
108116 processed_class_map = process_class_map (class_map )
109117 video_annotations = []
110118 if mode == "instances" : # For each instance produce a video annotation
@@ -159,11 +167,12 @@ def _parse_nifti(
159167 dt .AnnotationClass (class_name , "mask" , "mask" )
160168 for class_name in class_map .values ()
161169 }
162-
170+ remote_path = "/" if filename .parent == "." else filename .parent
171+ filename = Path (filename .name )
163172 return dt .AnnotationFile (
164173 path = json_path ,
165174 filename = str (filename ),
166- remote_path = "/" ,
175+ remote_path = str ( remote_path ) ,
167176 annotation_classes = annotation_classes ,
168177 annotations = video_annotations ,
169178 slots = [
@@ -353,7 +362,7 @@ def nifti_to_video_polygon_annotation(
353362 if len (all_frame_ids ) == 1 :
354363 segments = [[all_frame_ids [0 ], all_frame_ids [0 ] + 1 ]]
355364 elif len (all_frame_ids ) > 1 :
356- segments = [[min (all_frame_ids ), max (all_frame_ids )]]
365+ segments = [[min (all_frame_ids ), max (all_frame_ids ) + 1 ]]
357366 video_annotation = dt .make_video_annotation (
358367 frame_annotations ,
359368 keyframes = {f_id : True for f_id in all_frame_ids },
@@ -513,16 +522,33 @@ def correct_nifti_header_if_necessary(img_nii):
513522def process_nifti (
514523 input_data : nib .nifti1 .Nifti1Image ,
515524 ornt : Optional [List [List [float ]]] = [[0.0 , - 1.0 ], [1.0 , - 1.0 ], [2.0 , - 1.0 ]],
525+ remote_file_path : Path = Path ("/" ),
526+ remote_files_that_require_legacy_scaling : Dict [Path , Dict [str , Any ]] = {},
516527) -> Tuple [np .ndarray , Tuple [float ]]:
517528 """
518- Converts a nifti object of any orientation to the passed ornt orientation.
529+ Converts a NifTI object of any orientation to the passed ornt orientation.
519530 The default ornt is LPI.
520531
532+ Files that were uploaded before the `MED_2D_VIEWER` feature are `legacy`. Non-legacy
533+ files are uploaded and re-oriented to the `LPI` orientation. Legacy files
534+ files were treated differently:
535+ - Legacy NifTI files were re-oriented to `LPI`, but their
536+ affine was stored as `RAS`, which is the opposite orientation. However, because
537+ their pixel data is stored in `LPI`, we can treat them the same way as non-legacy
538+ files.
539+ - Legacy DICOM files were not always re-oriented to `LPI`. We therefore use the
540+ affine of the dataset item from `slot_affine_map` to re-orient the NifTI file to
541+ be imported
542+
521543 Args:
522- input_data: nibabel nifti object.
523- ornt: (n,2) orientation array. It defines a transformation from RAS.
544+ input_data: nibabel NifTI object.
545+ ornt: (n,2) orientation array. It defines a transformation to LPI
524546 ornt[N,1] is a flip of axis N of the array, where 1 means no flip and -1 means flip.
525547 ornt[:,0] is the transpose that needs to be done to the implied array, as in arr.transpose(ornt[:,0]).
548+ remote_file_path: Path
549+ The full path of the remote file
550+ remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]]
551+ A dictionary of remote file full paths to their slot affine maps
526552
527553 Returns:
528554 data_array: pixel array with orientation ornt.
@@ -531,9 +557,14 @@ def process_nifti(
531557 img = correct_nifti_header_if_necessary (input_data )
532558 orig_ax_codes = nib .orientations .aff2axcodes (img .affine )
533559 orig_ornt = nib .orientations .axcodes2ornt (orig_ax_codes )
560+ is_dicom = remote_file_path .suffix .lower () == ".dcm"
561+ if remote_file_path in remote_files_that_require_legacy_scaling and is_dicom :
562+ slot_affine_map = remote_files_that_require_legacy_scaling [remote_file_path ]
563+ affine = slot_affine_map [next (iter (slot_affine_map ))] # Take the 1st slot
564+ ax_codes = nib .orientations .aff2axcodes (affine )
565+ ornt = nib .orientations .axcodes2ornt (ax_codes )
534566 transform = nib .orientations .ornt_transform (orig_ornt , ornt )
535567 reoriented_img = img .as_reoriented (transform )
536-
537568 data_array = reoriented_img .get_fdata ()
538569 pixdims = reoriented_img .header .get_zooms ()
539570
0 commit comments