Skip to content

Commit

Permalink
feat: Implement no isotropic annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
shernshiou committed Jun 13, 2024
1 parent 714a041 commit 3daa22f
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 8 deletions.
1 change: 1 addition & 0 deletions darwin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def _run(args: Namespace, parser: ArgumentParser) -> None:
args.import_annotators,
args.import_reviewers,
args.overwrite,
isotropic=args.isotropic,
cpu_limit=args.cpu_limit,
)
elif args.action == "convert":
Expand Down
9 changes: 9 additions & 0 deletions darwin/cli_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import sys
import traceback
from functools import partial
from glob import glob
from itertools import tee
from pathlib import Path
Expand Down Expand Up @@ -852,6 +853,7 @@ def dataset_import(
import_annotators: bool = False,
import_reviewers: bool = False,
overwrite: bool = False,
isotropic: bool = False,
use_multi_cpu: bool = False,
cpu_limit: Optional[int] = None,
) -> None:
Expand Down Expand Up @@ -885,6 +887,9 @@ def dataset_import(
overwrite : bool, default: False
If ``True`` it will bypass a warning that the import will overwrite the current annotations if any are present.
If ``False`` this warning will be skipped and the import will overwrite the current annotations without warning.
isotropic : bool, default: False
If ``True`` it will not resize the annotations to be isotropic.
If ``False`` it will resize the annotations to be isotropic.
use_multi_cpu : bool, default: False
If ``True`` it will use all multiple CPUs to speed up the import process.
cpu_limit : Optional[int], default: Core count - 2
Expand All @@ -895,6 +900,10 @@ def dataset_import(

try:
importer: ImportParser = get_importer(format)

if format == "nifti" and isotropic:
importer = partial(importer, isotropic=True)

dataset: RemoteDataset = client.get_remote_dataset(
dataset_identifier=dataset_slug
)
Expand Down
36 changes: 30 additions & 6 deletions darwin/importer/formats/nifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
from darwin.importer.formats.nifti_schemas import nifti_import_schema


def parse_path(path: Path) -> Optional[List[dt.AnnotationFile]]:
def parse_path(
path: Path, isotropic: bool = False
) -> Optional[List[dt.AnnotationFile]]:
"""
Parses the given ``nifti`` file and returns a ``List[dt.AnnotationFile]`` with the parsed
information.
Expand All @@ -38,6 +40,9 @@ def parse_path(path: Path) -> Optional[List[dt.AnnotationFile]]:
----------
path : Path
The ``Path`` to the ``nifti`` file.
isotropic : 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.
Returns
-------
Expand All @@ -52,6 +57,11 @@ def parse_path(path: Path) -> Optional[List[dt.AnnotationFile]]:
"Skipping file: {} (not a json file)".format(path), style="bold yellow"
)
return None
if isotropic:
console.print(
"Isotropic flag is set to True. Annotations will be resized to isotropic pixel dimensions.",
style="bold blue",
)
data = attempt_decode(path)
try:
validate(data, schema=nifti_import_schema)
Expand Down Expand Up @@ -79,6 +89,7 @@ def parse_path(path: Path) -> Optional[List[dt.AnnotationFile]]:
mode=nifti_annotation.get("mode", "image"),
slot_names=nifti_annotation.get("slot_names", []),
is_mpr=nifti_annotation.get("is_mpr", False),
isotropic=isotropic,
)
annotation_files.append(annotation_file)
return annotation_files
Expand All @@ -92,6 +103,7 @@ def _parse_nifti(
mode: str,
slot_names: List[str],
is_mpr: bool,
isotropic: bool = False,
) -> dt.AnnotationFile:
img, pixdims = process_nifti(nib.load(nifti_path))

Expand Down Expand Up @@ -135,6 +147,7 @@ def _parse_nifti(
processed_class_map,
slot_names,
pixdims=pixdims,
isotropic=isotropic,
)
if mode in ["video", "instances"]:
annotation_classes = {
Expand Down Expand Up @@ -203,6 +216,7 @@ def get_mask_video_annotations(
processed_class_map: Dict,
slot_names: List[str],
pixdims: Tuple[int, int, int] = (1, 1, 1),
isotropic: bool = False,
) -> Optional[List[dt.VideoAnnotation]]:
"""
The function takes a volume and a class map and returns a list of video annotations
Expand All @@ -212,7 +226,7 @@ def get_mask_video_annotations(
Assumptions:
- Importing annotation from Axial view only (view_idx=2)
"""
new_size = get_new_axial_size(volume, pixdims)
new_size = get_new_axial_size(volume, pixdims, isotropic=isotropic)

frame_annotations = OrderedDict()
all_mask_annotations = defaultdict(lambda: OrderedDict())
Expand Down Expand Up @@ -249,9 +263,12 @@ def get_mask_video_annotations(
slice_mask = volume[:, :, i].astype(
np.uint8
) # Product requirement: We only support 255 classes!
slice_mask = zoom(
slice_mask, (new_size[0] / volume.shape[0], new_size[1] / volume.shape[1])
)

if isotropic:
slice_mask = zoom(
slice_mask,
(new_size[0] / volume.shape[0], new_size[1] / volume.shape[1]),
)

# We need to convert from nifti_idx to raster_idx
slice_mask = np.vectorize(
Expand Down Expand Up @@ -522,18 +539,25 @@ def convert_to_dense_rle(raster: np.ndarray) -> List[int]:


def get_new_axial_size(
volume: np.ndarray, pixdims: Tuple[int, int, int]
volume: np.ndarray, pixdims: Tuple[int, int, int], isotropic: bool = False
) -> Tuple[int, int]:
"""Get the new size of the Axial plane after resizing to isotropic pixel dimensions.
Args:
volume: Input volume.
pixdims: The pixel dimensions / spacings of the volume.
no_isotropic: bool, default: True
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.
Returns:
Tuple[int, int]: The new size of the Axial plane.
"""
original_size = volume.shape

if not isotropic:
return original_size[0], original_size[1]

original_spacing = pixdims
min_spacing = min(pixdims[0], pixdims[1])
return (
Expand Down
5 changes: 5 additions & 0 deletions darwin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ def __init__(self) -> None:
action="store_true",
help="Bypass warnings about overwiting existing annotations.",
)
parser_import.add_argument(
"--isotropic",
action="store_true",
help="Importing annotation files with isotropic transformation.",
)

# Cpu limit for multiprocessing tasks
def cpu_default_types(input: Any) -> Optional[int]: # type: ignore
Expand Down
11 changes: 9 additions & 2 deletions tests/darwin/importer/formats/import_nifti_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_image_annotation_nifti_import_incorrect_number_slot(
parse_path(path=upload_json)


def test_image_annotation_nifti_import_single_slot_to_mask(
def test_image_annotation_nifti_import_single_slot_to_mask_isotropic(
team_slug_darwin_json_v2: str,
):
with tempfile.TemporaryDirectory() as tmpdir:
Expand Down Expand Up @@ -188,7 +188,7 @@ def test_image_annotation_nifti_import_single_slot_to_mask(
with patch("darwin.importer.formats.nifti.zoom") as mock_zoom:
mock_zoom.side_effect = ndimage.zoom

annotation_files = parse_path(path=upload_json)
annotation_files = parse_path(path=upload_json, isotropic=True)
annotation_file = annotation_files[0]
output_json_string = json.loads(
serialise_annotation_file(annotation_file, as_dict=False)
Expand Down Expand Up @@ -228,6 +228,13 @@ def test_get_new_axial_size():
volume = np.zeros((10, 10, 10))
pixdims = (1, 0.5, 0.5)
new_size = get_new_axial_size(volume, pixdims)
assert new_size == (10, 10)


def test_get_new_axial_size_with_isotropic():
volume = np.zeros((10, 10, 10))
pixdims = (1, 0.5, 0.5)
new_size = get_new_axial_size(volume, pixdims, isotropic=True)
assert new_size == (20, 10)


Expand Down

0 comments on commit 3daa22f

Please sign in to comment.