From ca785f36e68d85fa52735b21fe0c6443354b9f2b Mon Sep 17 00:00:00 2001 From: John Wilkie Date: Wed, 11 Dec 2024 15:48:23 +0000 Subject: [PATCH 1/4] Initial commit --- darwin/dataset/remote_dataset_v2.py | 29 ++++++++--- darwin/exporter/formats/nifti.py | 43 +++++++++++----- darwin/importer/formats/nifti.py | 51 +++++++++++++------ tests/darwin/dataset/remote_dataset_test.py | 33 ++++++++++-- .../importer/formats/import_nifti_test.py | 20 ++++++-- 5 files changed, 133 insertions(+), 43 deletions(-) diff --git a/darwin/dataset/remote_dataset_v2.py b/darwin/dataset/remote_dataset_v2.py index 306b7b4c9..8956a8a3a 100644 --- a/darwin/dataset/remote_dataset_v2.py +++ b/darwin/dataset/remote_dataset_v2.py @@ -11,7 +11,7 @@ Tuple, Union, ) - +import numpy as np from pydantic import ValidationError from requests.models import Response @@ -873,10 +873,15 @@ def register_multi_slotted( print(f"Reistration complete. Check your items in the dataset: {self.slug}") return results - def _get_remote_files_that_require_legacy_scaling(self) -> List[Path]: + def _get_remote_files_that_require_legacy_scaling( + self, + ) -> Dict[str, Dict[str, Any]]: """ Get all remote files that have been scaled upon upload. These files require that - NifTI annotations are similarly scaled during import + NifTI annotations are similarly scaled during import. + + The in-platform affines are returned for each legacy file, as this is required + to properly re-orient the annotations during import. Parameters ---------- @@ -885,21 +890,31 @@ def _get_remote_files_that_require_legacy_scaling(self) -> List[Path]: Returns ------- - List[Path] - A list of full remote paths of dataset items that require NifTI annotations to be scaled + Dict[str, Dict[str, Any]] + A dictionary of remote file full paths to their slot affine maps """ - remote_files_that_require_legacy_scaling = [] + remote_files_that_require_legacy_scaling = {} remote_files = self.fetch_remote_files( filters={"statuses": ["new", "annotate", "review", "complete", "archived"]} ) for remote_file in remote_files: + if not remote_file.slots[0].get("metadata", {}).get("medical", {}): + continue if not ( remote_file.slots[0] .get("metadata", {}) .get("medical", {}) .get("handler") ): - remote_files_that_require_legacy_scaling.append(remote_file.full_path) + slot_affine_map = {} + for slot in remote_file.slots: + slot_affine_map[slot["slot_name"]] = np.array( + slot["metadata"]["medical"]["affine"], + dtype=np.float64, + ) + remote_files_that_require_legacy_scaling[ + Path(remote_file.full_path) + ] = slot_affine_map return remote_files_that_require_legacy_scaling diff --git a/darwin/exporter/formats/nifti.py b/darwin/exporter/formats/nifti.py index 6d2eeaf02..36699b3e5 100644 --- a/darwin/exporter/formats/nifti.py +++ b/darwin/exporter/formats/nifti.py @@ -25,7 +25,6 @@ def _console_theme() -> Theme: console = Console(theme=_console_theme()) try: import nibabel as nib - from nibabel.orientations import io_orientation, ornt_transform except ImportError: import_fail_string = r""" You must install darwin-py with pip install darwin-py\[medical] @@ -128,7 +127,11 @@ def export( polygon_annotations, slot_map, output_volumes, legacy=legacy ) write_output_volume_to_disk( - output_volumes, image_id=image_id, output_dir=output_dir, legacy=legacy + output_volumes, + image_id=image_id, + output_dir=output_dir, + legacy=legacy, + filename=video_annotation.filename, ) # Need to map raster layers to SeriesInstanceUIDs if mask_present: @@ -456,6 +459,7 @@ def write_output_volume_to_disk( image_id: str, output_dir: Union[str, Path], legacy: bool = False, + filename: str = None, ) -> None: """Writes the given output volumes to disk. @@ -470,6 +474,8 @@ def write_output_volume_to_disk( legacy : bool, default=False If ``True``, the exporter will use the legacy calculation. If ``False``, the exporter will use the new calculation by dividing with pixdims. + filename: str + The filename of the dataset item Returns ------- @@ -489,18 +495,10 @@ def unnest_dict_to_list(d: Dict) -> List: volumes = unnest_dict_to_list(output_volumes) for volume in volumes: img = nib.Nifti1Image( - dataobj=np.flip(volume.pixel_array, (0, 1, 2)).astype(np.int16), + dataobj=volume.pixel_array.astype(np.int16), affine=volume.affine, ) - if legacy and volume.original_affine is not None: - orig_ornt = io_orientation( - volume.original_affine - ) # Get orientation of current affine - img_ornt = io_orientation(volume.affine) # Get orientation of RAS affine - from_canonical = ornt_transform( - img_ornt, orig_ornt - ) # Get transform from RAS to current affine - img = img.as_reoriented(from_canonical) + img = get_reoriented_nifti_image(img, volume) if volume.from_raster_layer: output_path = Path(output_dir) / f"{image_id}_{volume.class_name}_m.nii.gz" else: @@ -510,6 +508,27 @@ def unnest_dict_to_list(d: Dict) -> List: nib.save(img=img, filename=output_path) +def get_reoriented_nifti_image(img: nib.Nifti1Image, volume: Dict) -> nib.Nifti1Image: + """ + Reorients the given NIfTI image based on the original affine. + + Parameters + ---------- + img: nib.Nifti1Image + The NIfTI image to be reoriented + volume: Dict + The volume containing the affine and original affine + """ + if volume.original_affine is not None: + img_ax_codes = nib.orientations.aff2axcodes(volume.affine) + orig_ax_codes = nib.orientations.aff2axcodes(volume.original_affine) + img_ornt = nib.orientations.axcodes2ornt(img_ax_codes) + orig_ornt = nib.orientations.axcodes2ornt(orig_ax_codes) + transform = nib.orientations.ornt_transform(img_ornt, orig_ornt) + img = img.as_reoriented(transform) + return img + + def shift_polygon_coords( polygon: List[Dict], pixdim: List[Number], legacy: bool = False ) -> List: diff --git a/darwin/importer/formats/nifti.py b/darwin/importer/formats/nifti.py index 103b599af..888ed36dd 100644 --- a/darwin/importer/formats/nifti.py +++ b/darwin/importer/formats/nifti.py @@ -3,7 +3,7 @@ import warnings from collections import OrderedDict, defaultdict from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Any from rich.console import Console @@ -31,8 +31,7 @@ def parse_path( path: Path, - legacy: bool = False, - remote_files_that_require_legacy_scaling: Optional[List] = [], + remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]] = {}, ) -> Optional[List[dt.AnnotationFile]]: """ Parses the given ``nifti`` file and returns a ``List[dt.AnnotationFile]`` with the parsed @@ -42,9 +41,8 @@ def parse_path( ---------- path : Path The ``Path`` to the ``nifti`` file. - legacy : bool, default: False - If ``True``, the function will not attempt to resize the annotations to isotropic pixel dimensions. - If ``False``, the function will resize the annotations to isotropic pixel dimensions. + remote_files_that_require_legacy_scaling : Optional[Dict[Path, Dict[str, Any]]] + A dictionary of remote file full paths to their slot affine maps Returns ------- @@ -78,16 +76,20 @@ def parse_path( return None annotation_files = [] for nifti_annotation in nifti_annotations: - legacy = nifti_annotation["image"] in remote_files_that_require_legacy_scaling + remote_file_path = Path(nifti_annotation["image"]) + if not str(remote_file_path).startswith("/"): + remote_file_path = Path("/" + str(remote_file_path)) + annotation_file = _parse_nifti( Path(nifti_annotation["label"]), - nifti_annotation["image"], + Path(nifti_annotation["image"]), path, class_map=nifti_annotation.get("class_map"), mode=nifti_annotation.get("mode", "image"), slot_names=nifti_annotation.get("slot_names", []), is_mpr=nifti_annotation.get("is_mpr", False), - legacy=legacy, + remote_file_path=remote_file_path, + remote_files_that_require_legacy_scaling=remote_files_that_require_legacy_scaling, ) annotation_files.append(annotation_file) return annotation_files @@ -101,10 +103,16 @@ def _parse_nifti( mode: str, slot_names: List[str], is_mpr: bool, - legacy: bool = False, + remote_file_path: Path, + remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]] = {}, ) -> dt.AnnotationFile: - img, pixdims = process_nifti(nib.load(nifti_path)) + img, pixdims = process_nifti( + nib.load(nifti_path), + remote_file_path=remote_file_path, + remote_files_that_require_legacy_scaling=remote_files_that_require_legacy_scaling, + ) + legacy = remote_file_path in remote_files_that_require_legacy_scaling processed_class_map = process_class_map(class_map) video_annotations = [] if mode == "instances": # For each instance produce a video annotation @@ -159,11 +167,12 @@ def _parse_nifti( dt.AnnotationClass(class_name, "mask", "mask") for class_name in class_map.values() } - + remote_path = "/" if filename.parent == "." else filename.parent + filename = Path(filename.name) return dt.AnnotationFile( path=json_path, filename=str(filename), - remote_path="/", + remote_path=str(remote_path), annotation_classes=annotation_classes, annotations=video_annotations, slots=[ @@ -353,7 +362,7 @@ def nifti_to_video_polygon_annotation( if len(all_frame_ids) == 1: segments = [[all_frame_ids[0], all_frame_ids[0] + 1]] elif len(all_frame_ids) > 1: - segments = [[min(all_frame_ids), max(all_frame_ids)]] + segments = [[min(all_frame_ids), max(all_frame_ids) + 1]] video_annotation = dt.make_video_annotation( frame_annotations, keyframes={f_id: True for f_id in all_frame_ids}, @@ -513,6 +522,8 @@ def correct_nifti_header_if_necessary(img_nii): def process_nifti( input_data: nib.nifti1.Nifti1Image, ornt: Optional[List[List[float]]] = [[0.0, -1.0], [1.0, -1.0], [2.0, -1.0]], + remote_file_path: Path = Path("/"), + remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]] = {}, ) -> Tuple[np.ndarray, Tuple[float]]: """ Converts a nifti object of any orientation to the passed ornt orientation. @@ -520,9 +531,13 @@ def process_nifti( Args: input_data: nibabel nifti object. - ornt: (n,2) orientation array. It defines a transformation from RAS. + ornt: (n,2) orientation array. It defines a transformation to LPI ornt[N,1] is a flip of axis N of the array, where 1 means no flip and -1 means flip. ornt[:,0] is the transpose that needs to be done to the implied array, as in arr.transpose(ornt[:,0]). + remote_file_path: Path + The full path of the remote file + remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]] + A dictionary of remote file full paths to their slot affine maps Returns: data_array: pixel array with orientation ornt. @@ -531,9 +546,13 @@ def process_nifti( img = correct_nifti_header_if_necessary(input_data) orig_ax_codes = nib.orientations.aff2axcodes(img.affine) orig_ornt = nib.orientations.axcodes2ornt(orig_ax_codes) + if remote_file_path in remote_files_that_require_legacy_scaling: + slot_affine_map = remote_files_that_require_legacy_scaling[remote_file_path] + affine = slot_affine_map[next(iter(slot_affine_map))] # Take the 1st slot + ax_codes = nib.orientations.aff2axcodes(affine) + ornt = nib.orientations.axcodes2ornt(ax_codes) transform = nib.orientations.ornt_transform(orig_ornt, ornt) reoriented_img = img.as_reoriented(transform) - data_array = reoriented_img.get_fdata() pixdims = reoriented_img.header.get_zooms() diff --git a/tests/darwin/dataset/remote_dataset_test.py b/tests/darwin/dataset/remote_dataset_test.py index 07881d7e8..0fb12d703 100644 --- a/tests/darwin/dataset/remote_dataset_test.py +++ b/tests/darwin/dataset/remote_dataset_test.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any, Dict from unittest.mock import MagicMock, patch +import numpy as np import orjson as json import pytest @@ -1906,7 +1907,14 @@ def mock_remote_files(self): seq=1, current_workflow_id=None, path="/path/to/file", - slots=[{"metadata": {"medical": {"handler": "MONAI"}}}], + slots=[ + { + "slot_name": "0", + "metadata": { + "medical": {"handler": "MONAI", "affine": [1, 0, 0, 0]} + }, + } + ], layout={}, current_workflow=None, ), @@ -1921,7 +1929,21 @@ def mock_remote_files(self): seq=2, current_workflow_id=None, path="/path/to/file", - slots=[{"metadata": {"medical": {}}}], + slots=[ + { + "slot_name": "0", + "metadata": { + "medical": { + "affine": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, -1, 0], + [0, 0, 0, 1], + ] + } + }, + } + ], layout={}, current_workflow=None, ), @@ -1941,6 +1963,7 @@ def test_get_remote_files_that_require_legacy_scaling( ) result = remote_dataset._get_remote_files_that_require_legacy_scaling() - - assert len(result) == 1 - assert result[0] == "/path/to/file/filename" + assert Path("/path/to/file/filename") in result + np.testing.assert_array_equal( + result[Path("/path/to/file/filename")]["0"], np.array([[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) # type: ignore + ) diff --git a/tests/darwin/importer/formats/import_nifti_test.py b/tests/darwin/importer/formats/import_nifti_test.py index 902819a13..e7e91db6a 100644 --- a/tests/darwin/importer/formats/import_nifti_test.py +++ b/tests/darwin/importer/formats/import_nifti_test.py @@ -190,7 +190,13 @@ def test_image_annotation_nifti_import_single_slot_to_mask_legacy( with patch("darwin.importer.formats.nifti.zoom") as mock_zoom: mock_zoom.side_effect = ndimage.zoom - remote_files_that_require_legacy_scaling = ["vol0 (1).nii"] + remote_files_that_require_legacy_scaling = { + Path("/vol0 (1).nii"): { + "0": np.array( + [[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]] + ) + } + } annotation_files = parse_path( path=upload_json, remote_files_that_require_legacy_scaling=remote_files_that_require_legacy_scaling, @@ -516,7 +522,11 @@ def test_parse_path_nifti_with_legacy_scaling(): ) adjust_nifti_label_filepath(nifti_annotation_filepath, nifti_filepath) expected_annotations = parse_darwin_json(expected_annotations_filepath) - remote_files_that_require_legacy_scaling = ["BRAINIX_NIFTI_ROI.nii.gz"] + remote_files_that_require_legacy_scaling = { + Path("/BRAINIX_NIFTI_ROI.nii.gz"): { + "0": np.array([[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) + } + } parsed_annotations = parse_path( nifti_annotation_filepath, remote_files_that_require_legacy_scaling=remote_files_that_require_legacy_scaling, @@ -551,9 +561,13 @@ def test_parse_path_nifti_without_legacy_scaling(): / "no-legacy" / "BRAINIX_NIFTI_ROI.nii.json" ) + remote_files_that_require_legacy_scaling = {} adjust_nifti_label_filepath(nifti_annotation_filepath, nifti_filepath) expected_annotations = parse_darwin_json(expected_annotations_filepath) - parsed_annotations = parse_path(nifti_annotation_filepath, legacy=False) + parsed_annotations = parse_path( + nifti_annotation_filepath, + remote_files_that_require_legacy_scaling=remote_files_that_require_legacy_scaling, + ) for frame_idx in expected_annotations.annotations[0].frames: expected_annotation = ( expected_annotations.annotations[0].frames[frame_idx].data["paths"] From ddcbbc8e91377e10a2a38764f6da1436b1e09b96 Mon Sep 17 00:00:00 2001 From: John Wilkie Date: Thu, 12 Dec 2024 14:57:27 +0000 Subject: [PATCH 2/4] flip axes of legacy imports / exports for `NifTI` dataset items --- darwin/exporter/formats/nifti.py | 21 +++++++++++++++++++-- darwin/importer/formats/nifti.py | 6 ++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/darwin/exporter/formats/nifti.py b/darwin/exporter/formats/nifti.py index 36699b3e5..60b077647 100644 --- a/darwin/exporter/formats/nifti.py +++ b/darwin/exporter/formats/nifti.py @@ -164,6 +164,7 @@ def export( image_id=image_id, output_dir=output_dir, legacy=legacy, + filename=video_annotation.filename, ) @@ -498,7 +499,7 @@ def unnest_dict_to_list(d: Dict) -> List: dataobj=volume.pixel_array.astype(np.int16), affine=volume.affine, ) - img = get_reoriented_nifti_image(img, volume) + img = get_reoriented_nifti_image(img, volume, legacy, filename) if volume.from_raster_layer: output_path = Path(output_dir) / f"{image_id}_{volume.class_name}_m.nii.gz" else: @@ -508,16 +509,27 @@ def unnest_dict_to_list(d: Dict) -> List: nib.save(img=img, filename=output_path) -def get_reoriented_nifti_image(img: nib.Nifti1Image, volume: Dict) -> nib.Nifti1Image: +def get_reoriented_nifti_image( + img: nib.Nifti1Image, volume: Dict, legacy: bool, filename: str +) -> nib.Nifti1Image: """ Reorients the given NIfTI image based on the original affine. + For files that require legacy scaling, we flip all axes of the image to be aligned + with the target dataset item. + Parameters ---------- img: nib.Nifti1Image The NIfTI image to be reoriented volume: Dict The volume containing the affine and original affine + legacy: bool + If ``True``, the exporter will flip all axes of the image if the dataset item + is not a DICOM + If ``False``, the exporter will not flip the axes + filename: str + The filename of the dataset item """ if volume.original_affine is not None: img_ax_codes = nib.orientations.aff2axcodes(volume.affine) @@ -526,6 +538,11 @@ def get_reoriented_nifti_image(img: nib.Nifti1Image, volume: Dict) -> nib.Nifti1 orig_ornt = nib.orientations.axcodes2ornt(orig_ax_codes) transform = nib.orientations.ornt_transform(img_ornt, orig_ornt) img = img.as_reoriented(transform) + is_dicom = filename.lower().endswith(".dcm") + if legacy and not is_dicom: + img = nib.Nifti1Image( + np.flip(img.get_fdata(), (0, 1, 2)).astype(np.int16), img.affine + ) return img diff --git a/darwin/importer/formats/nifti.py b/darwin/importer/formats/nifti.py index 888ed36dd..497130ec4 100644 --- a/darwin/importer/formats/nifti.py +++ b/darwin/importer/formats/nifti.py @@ -529,6 +529,9 @@ def process_nifti( Converts a nifti object of any orientation to the passed ornt orientation. The default ornt is LPI. + For files that require legacy scaling, we flip all axes of the image to be aligned + with the target dataset item. + Args: input_data: nibabel nifti object. ornt: (n,2) orientation array. It defines a transformation to LPI @@ -547,10 +550,13 @@ def process_nifti( orig_ax_codes = nib.orientations.aff2axcodes(img.affine) orig_ornt = nib.orientations.axcodes2ornt(orig_ax_codes) if remote_file_path in remote_files_that_require_legacy_scaling: + is_dicom = remote_file_path.suffix.lower() == ".dcm" slot_affine_map = remote_files_that_require_legacy_scaling[remote_file_path] affine = slot_affine_map[next(iter(slot_affine_map))] # Take the 1st slot ax_codes = nib.orientations.aff2axcodes(affine) ornt = nib.orientations.axcodes2ornt(ax_codes) + if not is_dicom: + img = nib.Nifti1Image(np.flip(img.get_fdata(), (0, 1, 2)), affine) transform = nib.orientations.ornt_transform(orig_ornt, ornt) reoriented_img = img.as_reoriented(transform) data_array = reoriented_img.get_fdata() From 021cbca6a38ba9dd0f6b689f5df635fd6f5550c7 Mon Sep 17 00:00:00 2001 From: John Wilkie Date: Fri, 13 Dec 2024 13:58:41 +0000 Subject: [PATCH 3/4] Tests --- .../data/nifti/legacy/sample_nifti.nii.json | 2148 +++++++++++++++++ tests/darwin/data/nifti/nifti.json | 2 +- tests/darwin/data/nifti/sample_nifti.nii | Bin 0 -> 1325536 bytes .../importer/formats/import_nifti_test.py | 41 +- tests/data.zip | Bin 303807 -> 308332 bytes 5 files changed, 2175 insertions(+), 16 deletions(-) create mode 100644 tests/darwin/data/nifti/legacy/sample_nifti.nii.json create mode 100644 tests/darwin/data/nifti/sample_nifti.nii diff --git a/tests/darwin/data/nifti/legacy/sample_nifti.nii.json b/tests/darwin/data/nifti/legacy/sample_nifti.nii.json new file mode 100644 index 000000000..abb0cf524 --- /dev/null +++ b/tests/darwin/data/nifti/legacy/sample_nifti.nii.json @@ -0,0 +1,2148 @@ +{ + "version": "2.0", + "schema_ref": "https://darwin-public.s3.eu-west-1.amazonaws.com/darwin_json/2.0/schema.json", + "item": { + "name": "2044737.fat.nii.gz", + "path": "/", + "source_info": { + "item_id": "0192eee1-7767-3bcc-1b02-7bd3a435b59d", + "team": { + "name": "V7 John", + "slug": "v7-john" + }, + "dataset": { + "name": "MED_2D_VIEWER_OFF", + "slug": "med_2d_viewer_off", + "dataset_management_url": "https://darwin.v7labs.com/datasets/1354682/dataset-management" + }, + "workview_url": "https://darwin.v7labs.com/workview?dataset=1354682&item=0192eee1-7767-3bcc-1b02-7bd3a435b59d" + }, + "slots": [ + { + "type": "dicom", + "slot_name": "0", + "width": 224, + "height": 174, + "fps": null, + "thumbnail_url": "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/thumbnail", + "source_files": [ + { + "file_name": "2044737.fat.nii.gz", + "url": "https://darwin.v7labs.com/api/v2/teams/v7-john/uploads/e760518f-563b-467a-be4d-e85eee725e45" + } + ], + "frame_count": 17, + "frame_urls": [ + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/0", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/1", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/2", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/3", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/4", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/5", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/6", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/7", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/8", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/9", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/10", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/11", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/12", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/13", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/14", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/15", + "https://darwin.v7labs.com/api/v2/teams/v7-john/files/8c754a67-b65a-4aad-aff1-2fdcb36a1669/sections/16" + ], + "metadata": { + "handler": null, + "shape": [ + 1, + 224, + 174, + 17 + ], + "SeriesInstanceUID": "1.2.826.0.1.3680043.8.498.89599582585125995121795967413768680340", + "affine": "[[2.232142925262451, 0.0, 0.0, -247.7678723335266], [0.0, 2.232142925262451, 0.0, -191.96429443359375], [0.0, 0.0, 3.0, -21.0], [0.0, 0.0, 0.0, 1.0]]", + "colorspace": "RG16", + "original_affine": [ + [ + "-2.232142925262451", + "-0.0", + "0.0", + "250.0" + ], + [ + "-0.0", + "2.232142925262451", + "-0.0", + "-191.96429443359375" + ], + [ + "0.0", + "0.0", + "3.0", + "-21.0" + ], + [ + "0.0", + "0.0", + "0.0", + "1.0" + ] + ], + "pixdim": "(2.232143, 2.232143, 3.0)" + } + } + ] + }, + "annotations": [ + { + "frames": { + "1": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "2": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "3": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "4": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "5": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "6": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "7": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "8": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "9": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "10": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "11": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "12": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "13": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "14": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "15": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + }, + "16": { + "bounding_box": { + "h": 18.0, + "w": 27.0, + "x": 52.0, + "y": 74.0 + }, + "keyframe": true, + "polygon": { + "paths": [ + [ + { + "x": 52.0, + "y": 91.0 + }, + { + "x": 65.0, + "y": 91.0 + }, + { + "x": 66.0, + "y": 91.0 + }, + { + "x": 67.0, + "y": 92.0 + }, + { + "x": 78.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 92.0 + }, + { + "x": 79.0, + "y": 91.0 + }, + { + "x": 79.0, + "y": 90.0 + }, + { + "x": 78.0, + "y": 89.0 + }, + { + "x": 77.0, + "y": 88.0 + }, + { + "x": 77.0, + "y": 87.0 + }, + { + "x": 76.0, + "y": 86.0 + }, + { + "x": 75.0, + "y": 85.0 + }, + { + "x": 75.0, + "y": 84.0 + }, + { + "x": 74.0, + "y": 83.0 + }, + { + "x": 73.0, + "y": 82.0 + }, + { + "x": 73.0, + "y": 81.0 + }, + { + "x": 72.0, + "y": 80.0 + }, + { + "x": 71.0, + "y": 79.0 + }, + { + "x": 71.0, + "y": 78.0 + }, + { + "x": 70.0, + "y": 77.0 + }, + { + "x": 69.0, + "y": 76.0 + }, + { + "x": 69.0, + "y": 75.0 + }, + { + "x": 68.0, + "y": 74.0 + }, + { + "x": 61.0, + "y": 81.0 + }, + { + "x": 60.0, + "y": 82.0 + }, + { + "x": 60.0, + "y": 83.0 + }, + { + "x": 52.0, + "y": 91.0 + } + ] + ] + } + } + }, + "hidden_areas": [], + "id": "d75a7a42-9600-4299-9b66-d27809b5db64", + "name": "Reference_sBAT", + "properties": [], + "ranges": [ + [ + 1, + 17 + ] + ], + "slot_names": [ + "0" + ] + } + ], + "properties": [] +} \ No newline at end of file diff --git a/tests/darwin/data/nifti/nifti.json b/tests/darwin/data/nifti/nifti.json index becf8807e..ccf69a692 100644 --- a/tests/darwin/data/nifti/nifti.json +++ b/tests/darwin/data/nifti/nifti.json @@ -1,7 +1,7 @@ { "data": [ { - "image": "BRAINIX_NIFTI_ROI.nii.gz", + "image": "2044737.fat.nii.gz", "label": "/Users/john/Documents/code/development/darwin-py/tests/darwin/data/nifti/BRAINIX_NIFTI_ROI.nii.gz", "class_map": { "1": "Reference_sBAT" diff --git a/tests/darwin/data/nifti/sample_nifti.nii b/tests/darwin/data/nifti/sample_nifti.nii new file mode 100644 index 0000000000000000000000000000000000000000..4a25937430c6e6fe8bf9528b827af17297c041fd GIT binary patch literal 1325536 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 F;9t--EU*9o literal 0 HcmV?d00001 diff --git a/tests/darwin/importer/formats/import_nifti_test.py b/tests/darwin/importer/formats/import_nifti_test.py index e7e91db6a..62104c916 100644 --- a/tests/darwin/importer/formats/import_nifti_test.py +++ b/tests/darwin/importer/formats/import_nifti_test.py @@ -168,12 +168,12 @@ def test_image_annotation_nifti_import_single_slot_to_mask_legacy( / "releases" / "latest" / "annotations" - / "vol0_brain.nii.gz" + / "sample_nifti.nii" ) input_dict = { "data": [ { - "image": "vol0 (1).nii", + "image": "2044737.fat.nii", "label": str(label_path), "class_map": {"1": "brain"}, "mode": "mask", @@ -191,9 +191,14 @@ def test_image_annotation_nifti_import_single_slot_to_mask_legacy( mock_zoom.side_effect = ndimage.zoom remote_files_that_require_legacy_scaling = { - Path("/vol0 (1).nii"): { + Path("/2044737.fat.nii"): { "0": np.array( - [[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]] + [ + [2.23214293, 0, 0, -247.76787233], + [0, 2.23214293, 0, -191.96429443], + [0, 0, 3, -21], + [0, 0, 0, 1], + ] ) } } @@ -210,7 +215,7 @@ def test_image_annotation_nifti_import_single_slot_to_mask_legacy( Path(tmpdir) / team_slug_darwin_json_v2 / "nifti" - / "vol0_annotation_file_to_mask.json", + / "sample_nifti.nii.json", "r", ) ) @@ -222,17 +227,18 @@ def test_image_annotation_nifti_import_single_slot_to_mask_legacy( ] [ frame.get("raster_layer", {}).pop("mask_annotation_ids_mapping") - for frame in expected_json_string["annotations"][0][ + for frame in expected_json_string["annotations"][1][ "frames" ].values() ] - assert mock_zoom.call_count == len( - expected_json_string["annotations"][0]["frames"] + assert ( + mock_zoom.call_count + == expected_json_string["item"]["slots"][0]["frame_count"] ) assert ( output_json_string["annotations"][0]["frames"] - == expected_json_string["annotations"][0]["frames"] + == expected_json_string["annotations"][1]["frames"] ) @@ -510,21 +516,26 @@ def test_parse_path_nifti_with_legacy_scaling(): nifti_annotation_filepath = ( Path(__file__).parents[2] / "data" / "nifti" / "nifti.json" ) - nifti_filepath = ( - Path(__file__).parents[2] / "data" / "nifti" / "BRAINIX_NIFTI_ROI.nii.gz" - ) + nifti_filepath = Path(__file__).parents[2] / "data" / "nifti" / "sample_nifti.nii" expected_annotations_filepath = ( Path(__file__).parents[2] / "data" / "nifti" / "legacy" - / "BRAINIX_NIFTI_ROI.nii.json" + / "sample_nifti.nii.json" ) adjust_nifti_label_filepath(nifti_annotation_filepath, nifti_filepath) expected_annotations = parse_darwin_json(expected_annotations_filepath) remote_files_that_require_legacy_scaling = { - Path("/BRAINIX_NIFTI_ROI.nii.gz"): { - "0": np.array([[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]]) + Path("/2044737.fat.nii.gz"): { + "0": np.array( + [ + [2.23214293, 0, 0, -247.76787233], + [0, 2.23214293, 0, -191.96429443], + [0, 0, 3, -21], + [0, 0, 0, 1], + ] + ) } } parsed_annotations = parse_path( diff --git a/tests/data.zip b/tests/data.zip index b38cedda19d9c005905bd897c52cb946925191d3..cd5a2f149af973a81dc3d7f440bb03b19e2fdcfa 100644 GIT binary patch delta 19861 zcmbt*d00)~_kZWy(|DUT&l3$QrBN!HC27(`Nv0&pm@*K57j+H22yFE{VXerc7J@bzM0asv3Db2Bry zNf@xJM}$Q^g+KUjPLqT}&Q>vug^R*R5~2P0vqcp5EKzY}du*+JS8Q}7i=>cXfF4xD3bFL}%wZ>o3{AFX4zd5KXYHo|c;h!D1 ziWOY0wRZ?dR(#wyydi5t0_qQAIYD?CB63$b4en;!u zt@D!vt;7qW3p3n5PiuZXaa(Nr&N4S^qaE=l+=5Jk%Vw=JJ-zUY)gGJc!#0hro9SHi zk*#_4t>YO^^z;{R7p^|3cP2Yx$CsV0%bs@BJ)AgwlE<^pFKiY^cw}0&MUNZz_>X_s zZ@a{b)K}jxUra<4OXUyU^xEKjU>DjVWY>3|d%)LVt@eYSk}3te>=7Yn4s=e++B|n2 zTitV5#?vahJqNulU8c28P|-I@$(2e89&y=4d&Hgtr{k%5{dZi?>b^6+ z2EAskXX-muR`*6ZoqK-na@b_q#=q$T8c)rwL(_monet0#k0+np#vkZ1d?VoNB`ngM zxV)w`=e_N6sS`mp;_A6PQD{1l%ZM*;{$GOk_HnWSJrqL8{GG-dE{wqYG^Xq z+tbt7Gf+}Q$nd>IhNp1##RkQn>*f(&&CO7p7Digj*_Ah&@{N_xS}ys1VP@!nI~lXU zSz}ZQl*~niI7=gA7cH0*u`qh>vS`zV(b1;!m&Pq52HtghIA6gZK9wv_U<)8vCn6v~ z%xP_5)8gu_<EZY0JEZG2ly7@bdyiGgX6;(=p!kSr;(hON;R{t)Mf|YbEmr;;?ps01D8U~#-@|(*zQ_*#WRhWcaPECEi_JPqdoRv6nR+VZtiiNND^l#kqa1%9 z)SL9%b{Z#al-R=~3X8N`)ReZrd^X;q!KS1h?9gT}|y$`Pav-{jXW6 zBU@NQ{Q93w+nX+A z99w-=Vq3J*UHg1HN6zqFqKf4UZi$zN%qTL$gv42#jSGYp)bINmX!SLASiAhB3KQ?z z1?7s~_8&7|+Hl^qoeEz3FlF__xIr%mzP-2xtH;-q)}4lows(=u1WL7@_#kN zeb%U4ef~VKqxg4PcUJfJ5;BBvm9%>#B&(7H^c%a)smJC&cG zoEaE2h8TP7%SuBUzQ@W<(Dm6ND-8?$My8muiYRJcT?%O;0WNW^bk>cYV zO|GQ-pGLPp%id*4FXj7k`@E{=U$lwz%vLz1le1=ZTX07Wsufpw=(P3WSKrG{?AxkX zx5s8u*twp<1)Pvo_B9j4TGyp?@88OCG&m)d*1GOor<30r-z-Oi;8~kKiLc1~E@>%b zU#0YY{H*(|OmnH;n5u}2X8O6Aj|GI1D`ID3JKI%XEIHOxRAp_PbjobmS^4s=$O4Or zVtJ>Z?2>;q>wDPy-!uJW5{#dWeI!0ZWAwYVkIpr&yi&ibVWq`RkKO}Ld+uy`^VRyt zbIzJcL7`XQhRVcVI`wP!tn9P1-@LqcqQ&gw*G*GRxLz*8`~PM|3|nxnG9uS>+Ln)x z43!^kP`t6rA!zx67-7>!&&M~6zXo}oP8`|iy^;Of*}f7;}E(r?c+b3#SNOFF3(2 zFjF?Z5+~rlE8z9?QKGu};^E#NGwe>}3F~SepFDf{ZJ}AUvG=FON|jGh6uffZ z^C?%o2Wo`B~w`s-oiVD&Sk@@KJB3YnN+%^iBp-4u*KuxNTjRTDmZ*P4(mHs|!}n zm^ahL+wI1P>}T5_jXL>wL@{4SJop3WF)ti4q3($hbncljG(E{I<$JXa- zdY^NOK6_Xv{gCBSS$N2KtI_%mVrjQB{B|W6`FwKU z#ZE%3H_TSDU-Z0=b=tns$ey!WedP>Wvn353Hk0i}Zw)Dyxu%_XQE$<6zbo4?Jp*0Q zy&IK18)tepo^mPF6`kPgE^8}ckzH~(yCk&isfX;={TX_TROad>rw>p5*<0jl!M>k= zC&SoGZI|}BlJW6Em49-~Zfse;d9|yDf>PvEv)j7ECZziNUw;-otIef@hm}uCAk(q zmY*#NWY75Xd~{ihh?dT>)ZX%vSLSXTU9`=OALWH=U09qH`P*%FUT}+0W#aNUoog@S z=TBSK*)a;=a#Z#HLL-C)1mN23H5O3=3UUZs{mJtI_r_Rcf*>eVMyN%ftE_^^2~mKP zn~_Z{a9tM(rixVbkTP0ps)$2MW7RAJHbT@Ccy`W_(75+(y%f0vt6y z_a7U05N3o>yKPWyf)3-JHA2LwJ@@EAzQ~&(6DeQjnjlUX^;Kw&IHAn1{}9k_g?KQF znh_W`;uHo9Z4u`|^jmEip=`88q)-`nq@Z?7xULa6V2g|Z5)qj2kcScwJQj(dY{KR^ zBS?KMbwXJ5*BW=^H!>gt5P6kr<%zV@H5xe?(W1X#GhPz}5+MwBvpnRiA<#=<6s4sg zMyvr+?zIibNBRpk{agBLdnVE(Hh?6}Tkl4G(gq>BB)L6>or?=Q$!!#fJVGW6=x~D{ zA+HhQ%w^Vu5a+aEAg&b=928bifjCoIjTMLrU2UWixj!HR@WT~6tU*?Q^eRM>S!@a( zS0Q=KlAYjYEixRKK7qg|War2A3E)=H9fa}dyMn1tkqu0JS77vvw`33StwBcdVxTiT zTK)7!p^%joM+XLA4tstol!NA?Oh$$7prcNXtlbV$o3YNV?#<4l@9Nqup!XxrqlD(iA zaU|I3ixU+f^Z{|FJPG)KG?7@FR>WbDC%an_*8wc)iPmpqguJckqJ>e!DQ9&!h#Y8C z=+ZH8<`biH!IF(T+hISC{XA6Z$S*^cPMgGtC&f@PWa&8X!B}(3VM=*o$Y7W*$o~e- z_b&o~!iIkzs#Ns{Z|O1c$%mKo7?|eEUs67(M5>dYr_hgA>k`wYV_>U4ulN{HmPC2n zjt5y?j8=lJugv|?g*>D>7_plCW17+Y!{60>dl0@MHUmXvz=>XD6!_hPBv9@VGsjn9 zlnu`OMK1CdXZ9h|;CL@`W>9f4PsxrXl(zsZMbOJU5CF~dVWfpnT@cSgb9sU87{6+! zI2?m6g6J+@Ak@FXTLM+JfvzyRleZYwu8_B+S_HkuTQU$G3m_F#YPl(k+Ec}FD8$2iLTT_=0ad`S5{K=K#3b2rKR3)8*fKpFhW1|__KkvVfn$KC z0?Ou_K&U+p*r!awG^u zWr&9JnH8rmU3l?i7A(6-#ovmmmc#QH_!P5 zy1BHg;*OH%c31B^b+4~13DXqaEQt!F(QjfOaHkYmwmeR0Kxcpe4wy3A`h6Mb0l~nXwc-%$p z`TAk(3wZ~u$Bh&U_)QeR5k}%Zl@!HNzGMZqW^XvRp54di;3ZsEo8Ndkj@NBkd0x z9SIEVbTC`_pWx3A=Zzno|63io#&Z7VpqJw5?)m#ZS1q^7yQ1)JUz)wj?F};F=Uv@$ zB=pB`oBj42*Q;7@jpeDMRu8^BIx%BRR(ezR_wR`u>r)Nq3|kv6&-q)rpL6}{s-@ml z5$B6PG&*IOh@Gl`u2Fwxgw@}ovEAhhQxrZ-dbF)h*kHG))#Qarb65Du>MoOPd|Iv+ zR$^DljUO}$vbX?FMg?;JHNYe8IgUM$vj*)~;J*t_miR@2?Nx2gg! zdfUi*RbR|I^)m0qh~2M7culsBUT61l@p|KlF%RP{#?HB|7e96NiY@b$4qEG_mURc3 zo1Q#9HH2&I5S{q)^tR*+%I98ZKMk-PK0+?m!1vF!u^*Ftr+t{o$xKZ4(>kxSAo8)S zQ|ghJ(Pe_p70QZJoIuyr2c?{$^p?; z+CqQEFI%@;Tz=xxt1IXIS{{^oqCnJP_YyUB$HOu9727sF+H_q)*?n*2Th>kA)-RvF zeEpaix!1ZmK2+Xhj%%T!NgZdeV`8{yxVOHQ|2wZu^L4!UHroYWyb?J%qQ>~md()6J z$5n&wL@!92Yf)%brXF$OdPml^hqbY<&gnjR@x=OL6?bb(;^tmUFXbt-XH_243BB!b zaQ9ZL&BhCAH_bF!9Qd{B&Y>^s9J6x6Qr4tzn0a9RUF6bO^T*%6(1w(Sh{3##iziOs;F95@!dTWjX%pZgpc^9KH^HGv42^bR^^H>`l^`q81A7H zdlY}!PPrr7vN855P`v8?%CBke)XU3NM=i_QF)91NE=Q3kZ(nZJ5`W>q-E^d8Pqx*P z;G@yMJ0()8VkFZyzOtM6%b;htNt{me<224Tm9eLs{Ec60?RZ=jEPC3wewIzBzUR63 zu3r{%b&C#W8q5yX<2EnM>JqOrJ=&@%vg4ZKSggac+^tMJyFPm`NKrP8UK{hYtXp@;*a#WLBg6Q;(!Hu@7#q&!HZ6zXZulUB= zyJ6`!iynk?Yxw1I>22or0r$6?$LwBmBt>*|?k@F7>!TlRo6%6SZf19joWkq*yKAEq zf}&>yynFm%r)p40%PV~+?UiC(v1?-AXyhF{_~^BGIOn`uqgkKP4cqb+FSfbVc^*h` z^ZYom_g99-(&blEv4_GTxxW*NKi}AMGiP{qso=s(84-_U&o-`HXdN>3f$Yke_a6MH zIjm}{l#T{#RP7v#BnA9h>&nc?f zXZE|Bn|e3c-)e8>G^SelisnWN*Y)0(P8Ld#o*HQrdAXt;`Qs-b>Z|Q(n}@RQHTflo zYozLnR7%Mlski4yX5ISv4ZSXQrd0LYi0cyiP2mZej!9cYe%?%qiEq&8^dipf+i-u_qZKnD{@Sfj50KO^=JYkB)@g9gR1+)a>ZA?I=A!^M= z;R8Yc4?cmZ;0JtZU}uEV7YJ*O(Bb4lc*GcW;alP8H5M-Yo_sOz~U_f7;d!{Do9^pytYAE)EAiSf*Nq8?9ey#*%_Xu zUV$omG;n|bEb>Ivxtq&q5+VYHi|RobCcbeW{9Fp52g+* z5~4{7t9!iA7K#)c4?vf5|9Ycas3LBT51%6%-0Obm2O0o%Xh*mg1Nltob3X>7uW5Ki zD3gzHvTqvGZ&)!?uIzO58LiV6fwoa!peYi~p+W~gEP&<(mNY1yiBgwFIK~2un}t%h zW%x$(6<9eNoid=xeKs4thsgZLcvfp1BL`pkWNx80gC5n0lV%HiOT&NoP~qRJJv|$% z<{_}LTD|V9c5Tyrd0&BgUkT%+Z4dqQj4eE9nEk`1@^T6$S+*`q%#M$_9W zQ{%1{_xx;6Z{W85t!PtT`=|R&XY=d7y+9MJBR>gLAMhgQ!cb12W%00(b_bjJ& zjLiLP_~Kwh?%LSyn&S|ZA5So-o|^e@0%*cb{iMpDR^_Mjs?RJaJUSyCs`J>u-Bele zZyVYeYl2GOzvXx>R8P(Lw+}=m#Qr}BpoIxaZtqH3tBCNu?116(zG>I_vg7kpNjwHF z)=BL#t1XP~RiC~3u1zlN_0qPEX?2F3a$#WYoTJa5#(8(|*f-thraT_D+*PqJ@%T+A zd9SC=GyG`D^D?);x{t4-{1Bz8D>Rbb9WpH7^m|o~vQW)q4g1 zO9yYjn&{f2YB=0ochSOo$<>RF?c1o{y~82Ur?eAqWU3+e#jiO*@VeN?C~a|nU~6mC zF>-8}PpKH}O)+EN_}oot={|n>($ntfD}|g0-`f{kYd>hdI2w_^wq^XyrTgEEscN^M zCyblYA9dX|AHi3umugqj=EJL9eB6;Mm56|d0CPmj!S`w({N3GIi~pIMXG9LX#B-4E zx>`a%l->HD;-wWr$~3gH;8Q#%uQ+4I)_=anZ#?0^x}za3-XpU7%H{`S#+RQq`X8_De@=Wp;@~SOSO;-o z5kt?@g*)Mmvm64l=A!EdrW=ra3svThjzVSWt6HxZREYixi9@YK2jJZ8323P>3$EHA zBnb^*uF%{|NoWx@9dct+&@d@Vt!f);D#E^OM5P{aMfaflg~7%Gbm;)-(jioyHl%hM z6{p!o6{0=#W%KBB=mYAqnTy^)6X<(zSRX|SiPuh*d-X1=POUH8H)ZHjY6F1-RJcn7 zS!-#)Z(-K*aRW|qmwK@LD0ULwJc)IHhG+V)%Izrlj5JoD^uQMhf4+ycjE4K|V|B?> z0JtEyfSmIz>b`I<~JVySaWuCkue zMZ`iwupF*q*@NO@7Tr3}idjD>B)6iJrAI}O+jWN(Ov{4GSnugCr-!UC>I)b(v(&gH z<*ZDqh&!c{RX}|K2v^}MJz=F%McgCLSO@7ZpIT;Mf7h|vX&J222kHyJ>Z)^pH?SU1 zMclHt3}?8|8Af!Tx3IobNC3^KaX)=xrO`!+ZLDonz~P4-@czo8cS2{oSP5M1cGd-| zM*>)604|GRzT7F_S?7l;-}sYti7qGNulb86~ zn~f@%E-KuAOOKBZ7w}UJo6aoh-)ZHlV}aC8nAm9i6%C4poCkmiS_UK zMRhRprEK!pT-Y&eesm$@#Bi&Q+u87VV zjYG5sv+(p-v~)D#-Oy^5gSpV8tFdt7V-8Z8iNy(hhd+R}WH-|vWn z-6$)RyRSyW0JV(A1Ehf8Z7BW2-vA5fk)IQVlLEUe=*L|;;8hlougHag8!`ie>Ymcl z&OBX2$(0Ol)mTgxY#EF2_0}3o=*B%yUJM&da62~HnAz@(zClE=)e7=F z^JQS5Ed}6ejSXY+sDV}15V#?T0&0QB)|d(}1`Z(&^x#wqqX;zo5#I3)j^LQbGbjvE zIVteRhB0gi&#Y}BusMwajsS~N5I(2w;TX9r3e!&p$DlCK8N*+NkTDP~lsyER9ULa9xF(8-696tRshB|t%ibC7)hS|s- zl2tqfr7oBvkWzrJjE;{O?Mij&69KRbcGgfh=|Qi8Wbc!8fP5j(ahwM5bzt;nCpSzB zY<9p@&^5KB_{(F)7=&UwbkYk_a;=hAq6MNHF%?7=__@IM$h0cP;2fY8m(pSM`3uQk zNo;sEKbBxB1|>L?$rC6YxDA9%#?rhb+v*8_c0T1L;fF=OBHgay!x8lr=i@YgO@jA7 z``@I<8xpsuh8IW10BkAKcDhB4;1d8mQpG7QCd>f6t?tdSJ&#>E493`AhdoMC%; zG?8MSR2o{W{I=gwX4=d^v#6k-39&IRFVPRKz9SWRvTLBC9lH5FDdEYop)H0qlVYAk z8$!|O11aH2vmqq^^-Fk?Y={vGw@(-t=MF1+zl9togYs4e`a8ajEsT)h&!i+C*}k=bmd=0;8TDS%z$Hb z@KEr`95IZ8ig!oDascrOSNb!nJQap7MqwCY!R=-Gb`OxB@{bEP&p|p{+ zpwS=G21P?lBNYNMIpPeAX9#0>5H53YOp*ix(~j};wBwj?Nd~4|kRKBf3Nb!X3{0^QKc*7L z*h@1oCxsb_Z-hUNQz1rEhJncv;m4fDF;cP&%vw=?jM_Aa>0x4G#P~7kIOgLp22WQ# z|8}Pv$JEPF7(TIV;%b!a)?B=A&I%`#dPS^ zB2^w7edr^`vReeC8l#3o^!anfh;>biKQ(+LV_|G_I;w%I@}#iS%i-BbSRZLkP#T9> z@Mn>cH6gYHH)g;#;AjGwc)|y|5_n+#5Boo~aWxaLNSQZDjn9c`IzW5H5KhRBGLr<%838zyTt1RkCIZOFP$QkdA0-@S!ALBDCwY8Y zg?4m^a4_-+$Z%Yr1zY2NKg`z%rO%I4A90usnZm6|CJ2!ql*LC^o#;>Z`>``_u(XG6 z1nW6?AFw7V;e1Xc4}zosjCyP#DF2U^xMWKc{12aqKA|~=!VbzNF7zp@ABGPF zBDSwSQ3;JhyEKN;Hf>JA_WfbPhZ29XBORvVC>YYUG}dHLn25Px(mdEr{}3<}kYtIf zQQc%fEY5RpaUmHv;Py?xj0-F!YR5zjyRc|jy-|+D$N*=eG5TqS5WmI6_Tx$MqRki^ zjGxbF4FVH2A7b2{NlfNd7Mq>@gN_aX`GP}yT}jBe7{->65HE{?;xso>e505p&hF-C z=*A(ygM@5N$Hdu%{E(yt5K`btLQI_)ivnRpFcykWdy(StWzbDueugv%K`w)mILs2f zj^&Sq^g>8**M}tV_2B2ap5{i)?(6j{l-B<@LV|2bP*^A8=LjnL_%DW-zX9yTl5F1>eEl3*0U;K#IE1$aweZJr z=Ce8PmG_i5a(=)kpM}kjkw?I z5-2C=*H%J2Dglb0tfs^|U_%1`W!5zuQ=LR%%z@TQMq3d)fh!?KE{($I03Q#Q2uMs| z>=$q#@qGSyC7cW$vq^B$Y7G8YgcwYM`JhhvzH}8NZOkEYizcybz?)V4hKD6WjCTRU z@EHy!2o@$XHt2+h$8liFNd~Yl5mRTr%acZ1S3_L(dCI6Qcwd0>?PxOAKupQ?ey%kb zouW>d(pU?{r8g*3qk%*rs!H`r0&@$;RF_hi{(xz5Fa=&309@ATvm+rsf{Xnhl47``(_+F= z!!%e|_X<)>OoYAf`3!K=AVbPy9AeFrN&yH1W5Ucl!fVdQo?G;;JAX#}q3 za98nx&&cEq$Rb!vvh2@b9K{JEHMqFF4j0oiIUxzpgpz<)BsW~ihltR79D+8G+>Vuu z5D*LtvY_}>6Df|1A)d@?@1$DpYby!Zh_+752P3_=|lK{ZiSG;A4wy9i-t0i zhC}v#Az2zF2Se=8P3e&A3xc5j2{%H`nu4*XsUZp;B(huag3X@EPqPh& zIH}+e-W?QB*b5~~)kp)xXqd=oR3aREaR|H!AX*lV2EKVqdmj|nY2gemyi@N{Trygh z;w3`YQ)9mG&39J9@b5M?8hffxlvjEcjV z9X^B^<5D?5+_n&l1J6Z0u{cEShqHe>6G*HO13+Sh5IO)cz=W5qf20iDl@mT?9DqJq zn^Hh3qmNwlbPhuC+EJ9)1mKe}sM5qRlm4dx{#yFG!GI*(I7M`3Q5S2AwxwW${1y8K$NM0b}}R~Wz2Mpy+S-5 ziAQEK7E+m>nQxta?mfdT-}k-W?>@h?)_?!k+Iz3H=jv)({#D*mb5Nj!(omN4=$9kG0DM=Z~NqG_DDuZz@+`DA~^b>}54(i$yrp;`#x z98I>0awn|tRp3Vz&V)4z^P)6#h7HwJQe}LQ+2RWyS9WNgRZ~Ue9bY0$rF-)wN>$vN zS6HdKH>+SB^sykDI>@+xj*t~@)$uvc_m%8)+LZ&mbnc8BqMs=RdS*jx^ zvqo=6Q0l0$JEhz#ZFat9H>~K*wk~i@;+z_!6>lq93{L&iwOb+skpJT1rAh?#iBM9) zVemHhTEBCShNXYaz7@H=a`D;Hz|s}VlE3e4GxqgH)dL|P9V3su$t~Y&^ya13_{zg2 zOV&J*eo^;c**a#!z+Hz^HtSv0K09qn$$^-xPLoxX3Z3S3R8mnIlu@Ogp;rE$aM1V3 znsrAf(WiBabtm_`HG_XRKd;u>7W^zYU{(IPrIPn$@vARa1(GkXlzU`{CB4(GNSa@^ zYxJTvqXvu{aphmnF^V@qY*P6FqcOJf`Kx)ZBbC;NFGH$&G!E)y!QM zkWw>eTgC^MYjevBtuB<992u;C#77#lt^1~`xgBgm_S9cV>^Jpfr~?{t=9t$8{o2>x zUOt-fJ@50v867tSwm3FFVZTNFp?fb>%NXU0H>{eA-bGE_v{kEaDdg=Dl=WPk>G2cXe>C@INuW4(< zZ0$8U<&*KKfiDavrFOj@SOmr1SC==93-I4I(@BAMvg=e0P>>6u&#_N-A!SJc+La>flbep*^02HirVa4=F^wAh>FO^dWi*Jw(2~#CWdc9$!M6 zt@4>(gpUx1eQD!IjN`D-`hAsAWdJde6M6R|Bz%YT9Y}m8#wwi7JDkwzApb(pa|dbVmBc(!E}~Oc6R-HLg^pe>LBG}!qd3609O8+({0m#B ziLJy3-XNGLwnKX>i7|2%4R)j)DKe`ijA_rC#5sZ)DAD&c;Zw;6DCrHMfhwO7Z4rH&(BgE-4~aa4ZWETsb~L3W?Z!j+L*a^WPPUXMCI zh1$%~A^F(@~lQse>jHqyb{)GPHR()l%*g4Ly9H>s$|ExFg@!#ArE=>rQXZ z?XoY#9I7a$d<5!INA!?Zj|q#CRWw_(n=!quj`*7;j;$xWF)=^i8KdjLQ4>jH6N%$zMdtb@lD%KV7+TnzyHSTQMZ+5vB(hp)hKqqRao7(r zy-eKj2XsWUB(=ieCBmc5buOmwj_PfS!FVw6Z@)=_1p)>jA#LAu(2iC1?^HL=Qq)n zh$X-%MImbBVX@d-ojfj<08kfo)*!V}pp@J!)|qRN3basO1DR=(d&R(Cn&d9A1d3Xt zUs`0ISS&BEkLtC_98L$dbwlTM$ZebiS9zl@c}gq+Zx#FpLux3c6?s4mgx0b|se0rl zu^1fHL6h`JBo+hO0QEEYQzb%Cs4t;|j<)8zlm6A3yu${;T_Z9;IP&1hq>nK1l3oX`w*d2aV%jo?(vs?ZSUO%d7SZcWNp_i!ep^9{V$k&S5*5^FE}p`; z?FcfnAp0o#lijF}&2TGz`o0AzTN?qfuMjU{vPPB5C<}COhIluC>v*#b$CNeqWcL;h1-`;Uy7s^@RGdG7bJPGf3Kj~v{tYObeT&@(jShQX^(^M+j> z9(Z7T{`9(svG>=Vc{qMdLms zWjDGgYaI1&OQiMryJwpK^&6R87lxX9#y3`%EnYT7W4dPO?3iii7OQICwyOJS$n?KeNZ8ZvCn#i3rQqXW8S9(g%Uwad(VyY4JsUl_b| ze84cLeMLpHN}s=Ityd8-vDm+E^WO)(TNe9uj7;_0_P8SM^-1G@Mz1J-nC{uK+~(@Z zsI>HUCIgb%Tsj}QCG>jRg>4IWcDYoSKd5Y`ee3eX6}|g3?ApDjXphF;&~XcoJ3EdV zK4h5Dok2t9e{sB+6S6m^c6t5qR(Xrgn=OcP@tt)j>czVU&$6!^H@5yPJuzl&eBJb$sN-Fim% zs}Fv9(jd4#d%peG#4VBI{NA0m|4VD+z3E#KnxreTjb$Dm+%^JLK5B zt;g@IG&xyx?9Gk{yOUKO1;^%=ww-l9`chg*%%JvOJ1*rNv5Dz6i=LW$en48oD}Bvg zH&dTi7I+mIX5Z@SP#BuC`JmU^s=s~iY2J;!^vW(v?^$}+q1WTfH+@VGOPv2QV*C8$ zVE-upJ6U^eQU(Owp0IjPi`ajTZ@IOkFf^Fb+@?EQ&#c#hEp%m{TJoFPPpz*9m2%3W zM`+LWwVs+BQoGo;PI6r{%6Vwx#JE}0>f=I|6l%A9Q~{pjivpZ6*X=_YJ&S$W(s zP5W$p{pPVHJ!<+7-27Ml@%=km<{2fHmmJ@9VMx)WOF>1^htF+1xxZCz_L&I_cf^jk zI$(V8${{-*SY;nud&IPzL*BCo8=SvBm@=#NH}vRa^xJPIuU-v3M(4-Xjz0Tgq2KDf z?HU~pU3cBuw`%%C{l*Jezt2_IKHsNLGb$5Y-0l&_W8 zmAgMrJaRNXtyH`3^LBG;vmWLx8Cbk=$lK;}iLHt?xX4kW~~twsz*OG@A;|Q?n$UXYYKNaN+)>@P(4jA2ZekWfbIk?p{+`;ZLWH$l2Fo&gnijhti{M4!5n? z+tPLDsy*ROMZV!j%-;4=F}4Z4_it@?_kfSnRTi!&>4mNzs2!+eOKC;*(p~FgXHOA1 zXFKd#>b`54N{e0|btzhjwe8*0d`;W-O(F~mEj6`Edo?VAoV#F0lHG)Kqbpx>XGXaV`RAt7r~Z<|X_Ld} zI6u6ibhv%sDoJ+2MU9GGPAA_ux8HF)H!<){Rm_joCcVekJY43dmf%B8-JKY7x?`7$ ze&~YH%Kayy{P%=K}&6$HS$+D%f$8P+UYSXWLW%i3bKi_oU+Gk@;RL7r3O;?m%z4>87_pSXmHbj|x zZCCL~&v9m``uB#Yw3CgAy}x~^oj+S`jBjG1?u~0pzb<**af^D_vr+FI@2JHrd8u2Q za^`t~Wy?eNhu*eSQ!QQc{fT42cSFw>Ysz%Hs}C&AUh@8$&W-zP6V=A1K5Oi7nwWNW za=gi-WS2?u@_HGWi0bsMYS*BS#2iswc-{AY0@ZBymB@Nc~%EoSD8>;5hL-~Nwo z42y5StbL;Q*^QGco~&POm{&BV*Q6h(GF7!W=ik5K`-_F-{khEP zEnX}tKK$2F)xP(TnvctG-O}R?n>$V(`SR_LZ(m(XJ$}!x`Mu%CkKfwNp+ite!}3ll zaExTDq{JU8*3J_yzLk{FN@vnb`rN+LMA;8dSg?=hjwk4zE~E{+Q;v2edo@1+!qjeq z8qZ7qFag5KP&RqvH6Tph+|C5S{AxlU>`JodYV7O{D5L*<;R zxjG#>n0(C1X`tZEqL|TS99cx1fddF! zIvghOp3aOAGBb5y9Mqu8Cy)>L(oEfHsQW!mL>EsY-x6G}RZbyY(E4+*|E!IM^{9MX zgT=VZ7fm4ph2nI1Khkgj;>gjmuJ4H<4-;rrJZw|sX97<~ON?ln1hS>zRAwQm!hZ!O zldfuVIK3>LEK`-dnl}jrW|2drHISdOAH6$^JjmIj!`70c`Qxy2+sIBWq$glW$^0Or z=^uN@Evjhc9#TX37n~^%M^omKJyGy#%8Y}(vhS3wm2a2mMT*NdPnsQEU*htUG5U^=-Al4rslPcf!_1N*Du<&tMU2xN-1rOhDK4}nY(S$?9yz~ zLhhDy+HAA?#1o_4{<~xSzn&nzIJ4j5<>7t$Gfm=7` z?;o6S@`mTE&wfvTBqnQRsn2v?5vZYI;N)B`eIB)Tt)5=!t2aCKB6GICUT3h=z^0~V zUE#}hwcBfIc1p%5&0evn-=c)E0~ST3FN{jxR}edM-sFsg6Z7Lwqws_!fs^Ls6ws0J zGYTU^(q}~E4ctB|+TV9kaGFE^>`@Vu162MF3q2Q41PuM2QCD5PKecmeSL?yGZ5F>; z`b|Z}Ahjy$W$)d+(*km;DvU~#0*^*9qo`^N^$}r3-Jv0W5h&v@naFjR!4XoQ?>^@< zqz2zd{w3rO?yQaOdzQS!oz~HYSIKzp91%g37}Lc!$e(;JV9~W(q&X{k2z~nTHaUws zyJgEP*Ox5f%FvpIlDHn6YiYwJ!JG*i_)hW2ZQ~?Y+&Sc&JjpN}`4>xES0tIu!zb>O ze9;%C0Zy6-u>|Lf?|Dg(SUl!|L<1Qy~6VubX;X@|^z~eor!* z`$9gSB&KxL1Ia30box1nQXfl}aUxppsic7a0>D=OE3!tY(|j#y;B*LTWJX)Qm0aRP zG}QYg|Ml>_z!@5x;aiBV`z(3KBcFeh?BLG75zI81T%*H>I~xB>!p}Iq29!C{A>huc zp#?RF?)F=9gbPx{C}~h?a^zUV;78FLCDbt`&Qzf~H!Y`5gB`)d047?| zno`PuhxV|deCTLRN|zU5)H-d-SR^u~Z|G9mya)=;qa*aFJDgiI6tw37m}Z$FWhfHi z`j4W`O(=z`<94A5GX;Oy_M*F32;?-_9?Y}CR|2iuR_IBi4He7-pmAE!p|+GMFT&1c zcAz?usy$?f#(1jvYEQ+AB{-S(IZz|TVxLY_q&!2|oLJtOijYg_cb%z@?2^sGnPTsL zmiCj31z65>rzlnZ4pTodu?{VtRz*2wRYxq=V7 zQ6{K!7pi^p1(Ui^;`3;(Ab4n!6Qs^Fwk z5M_y4bff&`Ez46S`9iH$ccX0n$S1yh!mZo8JH;(b6{sq^|B1@Njq=AvWasJ#Q>}2L z_)~jEq6W9gBk@fmYUNJ#kjgKV;a|2yf;(kSzUhvfx=?yZXDz9cU7D|-?dE39RSpwt zc>wHtsu+eEJSgr$kELtx3Apu}MK}jBmAw~0ezZc@J;5TpCg87Fu%%9YDIIjdi_#(s zjW~=3+S7*MuUIfJ&>MIzTW}Z?RBcNbif#SXpVHBBSF+@877CLOS#u>c&_0ug1K0K}&ef6Nw=WzO6G3F29Rsm!a=*!U zHirFYhn#$=*65&wRBMXGb=LI-i-S8bIJ>5PGtoyD+hdj-HL{tDT1-;IkVqZaX7F)` zquGudo_mX&{xyX`@qqf~_ONXCb~$Qc~(5h=I4hFHmw%N*`7C zBFsdIit7gPse|JEApnCsSa*B$p={A~KSEdBMqm7)jlTPE7&8>mn`$e@Wb_71o8BCz zCF(p-5GUB6v=3m^`f?cVl$mKqck*Nqr)i1u11K%ju7EI*#s*E7U%9dE4T)n;-VPRn zmC>tFghB#dVt|qn9MB3`gexY_$N<0?r*If-xxKh2Uf3ds0!0h9{Q+}g6NkaIQAdw=@NW&+_JOeg z+UFt%#8uKmSM3G;3PZa)N&bVJLa5d`HwhEY9WF?(PJkfoa1|p^bs(HzDBgy%)?5dG zwN)5rj*KZ%3vH<)xQ!jM8Uum6cK`(Pp$dk}Tzp54)g}W$oqUZ|i|)#?I-OAus$|vc zM^oBdM#;%FkjfwkW9#cOihJ-zLcWRRDL%;5Uz(`F0U5=G8!W{?_xWcQY#n$^&c7^X z!vjSO)KkV2b(6(Rd8mltc!;j(Nf2d-4#H^!@_3|(!?jkaVrit071g|{yE4q9CKyby z`!-9lzgm&v^$=*+1$S7r_^zMnG^`$QmsO9{6+Q*v`qc6ss}?-~koSr)*&tJk9s*!B z?(&lRY-!Ph0IbHgS3O|V_Trwz1;;&P3yR(b@VuQKv1-v90XzbY9S*_z^AVdGO`Zwx z(-aPV>n9wp>BTJ@{Jlp2U#bj;-@~nVEg1E{C3|#f1cc_Erz{s>47mz1F0pS5>_67lQeI1wm>_&j-Xu z$|prG|1(B0`CrRn1)TD?khn?7P~cpHah53OouKx`VKBxVdxe(-;&%#IGwU_05j|Ca z7IP;Z-!K|0@skCtsh4R)PZzLeZyj4k^n?LxqTjL_(NhMjv8`t`ow@$N!k1WRg+#9! zu%@tqB@(@Ez?z6gRulbI@GybVz_S*sP^gJrn0VB3Cmyki4MGKOmW}TjE=weO`+zkg zKd>6n^9QUk_{eHRFCehyUzsLM_y_`PGCrZ=2&xlTc}7SCAs?pUMUNmbTKzLy!;3ML zrl<&c5o;_(?;&tt44VLfvE&QC)I$(#&8!@8t{wWu;q*l-6{|=H2>tKeQV-3K6fA+D zjV;lUNJt*{A3UUovfyn7hFC`dr2S7GGA2s!%mYKR7>LO)Iiv-;+5|#Qq>Kq|WHla^ zRE=uzaKdY%BFjvGkfaiNHxt(U7_+Yl<|%{u#AA%O2E~pHo&@Trgn(kzBtN@mBlbGAd&qP0gvw#=;NU+1nLb!HExy4DL~<;hA}x)1%oKo_LvGNEKPt0!N<>WLeT>tbOvCUgbhq)wFTAC_d%(;)L^%D}MpH$=y#iIc`2 zeV+zhyS*h#xW$BQ$Mq`S^-rA+&c(K3)y+OR;U~+`nc&+4eF3)lmnU3hg6Bd3e*>;S zTT#a2!Q;UUsKOBpX^)uKE~wIi`I}1)iaS2kk;OKDg9)kOPF|Kla8}?z$05*sYK)49 zR_Ns-s}z$*xWxYv3{RL+ClyMyYT6Vyou?&0HgxO4n)t)bryQt(ROdlz{i}dB=kg0j zTjD%u+$pXseP8mmH4(?(8FlD%vgr+^e1fzMp}D%|_-TF!#|TyN{Zx0#AucNyHkVFE6lZD8}BB5vGbz>OZx z;W~>0;sv9gG*V#Bq7Y~nDAGhP0o=?`j>Zy|EP*wzd|QDdqLINQjuCKGd;`}D6}jS; z0&c}P1}7WTb0-N_jyTg|i(!4DnhKc~7tR3{9wT6*)FC*X>m?Yik0h%{uqAvPsPp5byq8CkHA8gxe?w%aZT$g~M81c(2Z=L(QJX#mNH zLg86dPvo91I9G$EuNg{7r_9L5(agNTpPj!;hn#p6%a-Y!Oe&mB?_2?tR`Kjy^K%7d z%yq+zd|-pw$ZI8xet}6W0=q7_V`p=qd?k>)O=fHaBF9yN^#g9!$W@>omB}*XZzIh( z8(96FQLkCgs&DQg`TKU5_OJn~QkYTs`((JB7xYnKa()4cV&=kgXT{5U*1L79fq%g^ z#=HLL+hN5Pa9acFqP?v7HESp%v3LJ4>OK3|_OmDCRM;?FDjwDl3h&ynP(htP~d%8pqR{&7_B7nMjj%$8_=R6rNj!&sw#gk$9g z14t@M*Va6*|xrySJ z-Pp?aO+e9bfu#uA4Es3wAdl54n?Zf|6062jwcvpQ>x6nfK)kN7R`4vX0C6YlDp&%T zo5gK`Z5V9GA(*w_GD)S!{9%ff_!XZVXiV*-WEU# z@NQ#s)Gna8|4~5WqpUb5tz9dCc;^9PtlmmULY!076S3IRZp=?PB6f)sw^;PpI|h_?c}`=)E(7j zJQ>MUYiXgPt#b@!jE2w_&XazMwgUD8FjGrTwqMa$1*1QwBhyn2+t7n?(-pw(bpLCB#S0!2ykr&*)Tp);qPz;1uDZkY9%EC P{yUJQr1Z^-g0KGpHg_dN From 30d503fe074b6a54e1fdc450f9e264ae037163ac Mon Sep 17 00:00:00 2001 From: John Wilkie Date: Mon, 16 Dec 2024 12:07:07 +0000 Subject: [PATCH 4/4] Fixed import logic issue & updated docstrings --- darwin/exporter/formats/nifti.py | 13 ++++++++----- darwin/importer/formats/nifti.py | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/darwin/exporter/formats/nifti.py b/darwin/exporter/formats/nifti.py index 60b077647..011b1e4e8 100644 --- a/darwin/exporter/formats/nifti.py +++ b/darwin/exporter/formats/nifti.py @@ -499,7 +499,7 @@ def unnest_dict_to_list(d: Dict) -> List: dataobj=volume.pixel_array.astype(np.int16), affine=volume.affine, ) - img = get_reoriented_nifti_image(img, volume, legacy, filename) + img = _get_reoriented_nifti_image(img, volume, legacy, filename) if volume.from_raster_layer: output_path = Path(output_dir) / f"{image_id}_{volume.class_name}_m.nii.gz" else: @@ -509,14 +509,17 @@ def unnest_dict_to_list(d: Dict) -> List: nib.save(img=img, filename=output_path) -def get_reoriented_nifti_image( +def _get_reoriented_nifti_image( img: nib.Nifti1Image, volume: Dict, legacy: bool, filename: str ) -> nib.Nifti1Image: """ - Reorients the given NIfTI image based on the original affine. + Reorients the given NIfTI image based on the affine of the originally uploaded file. - For files that require legacy scaling, we flip all axes of the image to be aligned - with the target dataset item. + Files that were uploaded before the `MED_2D_VIEWER` feature are `legacy`. Non-legacy + files are uploaded and re-oriented to the `LPI` orientation. Legacy NifTI + files were treated differently. These files were re-oriented to `LPI`, but their + affine was stored as `RAS`, which is the opposite orientation. We therefore need to + flip the axes of these images to ensure alignment. Parameters ---------- diff --git a/darwin/importer/formats/nifti.py b/darwin/importer/formats/nifti.py index 497130ec4..cb6f748a7 100644 --- a/darwin/importer/formats/nifti.py +++ b/darwin/importer/formats/nifti.py @@ -526,14 +526,22 @@ def process_nifti( remote_files_that_require_legacy_scaling: Dict[Path, Dict[str, Any]] = {}, ) -> Tuple[np.ndarray, Tuple[float]]: """ - Converts a nifti object of any orientation to the passed ornt orientation. + Converts a NifTI object of any orientation to the passed ornt orientation. The default ornt is LPI. - For files that require legacy scaling, we flip all axes of the image to be aligned - with the target dataset item. + Files that were uploaded before the `MED_2D_VIEWER` feature are `legacy`. Non-legacy + files are uploaded and re-oriented to the `LPI` orientation. Legacy files + files were treated differently: + - Legacy NifTI files were re-oriented to `LPI`, but their + affine was stored as `RAS`, which is the opposite orientation. However, because + their pixel data is stored in `LPI`, we can treat them the same way as non-legacy + files. + - Legacy DICOM files were not always re-oriented to `LPI`. We therefore use the + affine of the dataset item from `slot_affine_map` to re-orient the NifTI file to + be imported Args: - input_data: nibabel nifti object. + input_data: nibabel NifTI object. ornt: (n,2) orientation array. It defines a transformation to LPI ornt[N,1] is a flip of axis N of the array, where 1 means no flip and -1 means flip. ornt[:,0] is the transpose that needs to be done to the implied array, as in arr.transpose(ornt[:,0]). @@ -549,14 +557,12 @@ def process_nifti( img = correct_nifti_header_if_necessary(input_data) orig_ax_codes = nib.orientations.aff2axcodes(img.affine) orig_ornt = nib.orientations.axcodes2ornt(orig_ax_codes) - if remote_file_path in remote_files_that_require_legacy_scaling: - is_dicom = remote_file_path.suffix.lower() == ".dcm" + is_dicom = remote_file_path.suffix.lower() == ".dcm" + if remote_file_path in remote_files_that_require_legacy_scaling and is_dicom: slot_affine_map = remote_files_that_require_legacy_scaling[remote_file_path] affine = slot_affine_map[next(iter(slot_affine_map))] # Take the 1st slot ax_codes = nib.orientations.aff2axcodes(affine) ornt = nib.orientations.axcodes2ornt(ax_codes) - if not is_dicom: - img = nib.Nifti1Image(np.flip(img.get_fdata(), (0, 1, 2)), affine) transform = nib.orientations.ornt_transform(orig_ornt, ornt) reoriented_img = img.as_reoriented(transform) data_array = reoriented_img.get_fdata()