Skip to content

Commit

Permalink
Add dicom_utils, test upload_annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
phcerdan committed Oct 27, 2023
1 parent 6e13f0a commit 0dda85d
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 10 deletions.
60 changes: 60 additions & 0 deletions mdai_utils/dicom_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import json
import os
from pathlib import Path
from shutil import copyfile
from tempfile import TemporaryDirectory
from typing import NamedTuple

import SimpleITK as sitk


def file_list_from_directory(directory, extension=".dcm"):
"""Return a list of files with the given extension in the given directory."""
return [str(f) for f in Path(directory).glob(f"*{extension}")]


def write_image_and_metadata(image, metadata, output_image_filename):
"""Write the image to a file, along with the metadata."""
sitk.WriteImage(image, output_image_filename)
p = Path(output_image_filename)
metadata_filename = p.parent / (p.stem + "_SOPInstanceUIDs.json")
with open(metadata_filename, "w") as f:
json.dump(metadata, f, indent=2)


class ImageAndMetadata(NamedTuple):
image: sitk.Image
metadata: dict


def read_dicoms_into_volume(valid_dcm_file_list) -> ImageAndMetadata:
"""Convert a list of DICOM files to a image volume. Also returns metadata
(SOPInstanceUID) for each slice in the volume.
The metadata generated by this function is a map from slice index to
SOPInstanceUID. And can be used to upload annotations from non-dicom formats
back to MDai, which requires the SOPInstanceUID to identify the slice.
PRECONDTIION: the input dicom files are in the same series, they are
valid DICOM files, with no SCOUTS or other special slices.
"""
# copy to temp directory in case multiple series occupy the same directory
with TemporaryDirectory() as temp_dir:
src_file_lookup = {}
for src_file in valid_dcm_file_list:
dst_file = os.path.join(temp_dir, os.path.basename(src_file))
copyfile(src_file, dst_file)
src_file_lookup[dst_file] = src_file
reader = sitk.ImageSeriesReader()
# allow for reading of metadata
reader.SetMetaDataDictionaryArrayUpdate(True)
dicom_names = reader.GetGDCMSeriesFileNames(temp_dir)
reader.SetFileNames(dicom_names)
image = reader.Execute()
uids = [reader.GetMetaData(idx, "0008|0018") for idx in range(len(dicom_names))]
# keep info about slice, original file, and SOPInstanceUID
metadata = {
slice_idx: {"dicom_file": src_file_lookup[fn], "SOPInstanceUID": uid}
for slice_idx, (fn, uid) in enumerate(zip(dicom_names, uids))
}
return ImageAndMetadata(image=image, metadata=metadata)
5 changes: 4 additions & 1 deletion mdai_utils/upload_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ def upload_image_annotation_slice(
failed_annotations (list): List of failed annotations. If empty, all annotations were uploaded successfully.
"""
data_np = read_data_image(segmentation_image_path)
if data_np.ndim == 3:
# The perpendicular dimension is at index 0 in the numpy array.
data_np = data_np.squeeze(0)
return upload_data_annotation_slice(
data_np=data_np.T,
data_np=data_np,
mdai_client=mdai_client,
mdai_project_id=mdai_project_id,
mdai_dataset_id=mdai_dataset_id,
Expand Down
1 change: 1 addition & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pydicom>=2.4.3
itk>=5.3.0
bidict>=0.22.1
opencv-python>=4.8.1.78
simpleITK>=2.3.0
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ scipy==1.11.3
# via
# dicom2nifti
# scikit-image
simpleitk==2.3.0
# via -r requirements.in
six==1.16.0
# via
# python-dateutil
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ def mdai_setup(
with open(parameters_file) as f:
parameters = json.load(f)

fixture_dir = _current_dir / "fixtures"
fixtures_dir = _current_dir / "fixtures"
mdai_domain = parameters.get("mdai_domain") or "md.ai"
mdai_client = mdai.Client(domain=mdai_domain, access_token=token)
return {
"mdai_client": mdai_client,
"parameters": parameters,
"fixture_dir": fixture_dir,
"fixtures_dir": fixtures_dir,
}
2 changes: 1 addition & 1 deletion tests/test_parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"mdai_annotations_only": true,
"mdai_no_fixing_metadata": false,
"labels": [
"mylabel",
"mylabel"
]
}
31 changes: 25 additions & 6 deletions tests/upload_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest

from mdai_utils.upload_annotations import upload_image_annotation_slice
from mdai_utils.upload_dataset import upload_dataset


Expand All @@ -13,11 +14,11 @@ def test_pytest_fixture(mdai_setup):
reason="Only need to upload once. run pytest tests with --upload-only to run it."
)
def test_upload_dataset(mdai_setup):
mdai_parameters = mdai_setup["parameters"]
mdai_dataset_id = mdai_parameters.get("mdai_dataset_id")
fixture_dir = mdai_setup["fixture_dir"]
dicom_dir = fixture_dir / "humanct_0002_1000_1004"
assert fixture_dir.exists()
parameters = mdai_setup["parameters"]
mdai_dataset_id = parameters.get("mdai_dataset_id")
fixtures_dir = mdai_setup["fixtures_dir"]
dicom_dir = fixtures_dir / "humanct_0002_1000_1004"
assert dicom_dir.exists()
completed_process = upload_dataset(mdai_dataset_id, dicom_dir)
process_message = completed_process.stdout.strip()
print(process_message)
Expand All @@ -26,4 +27,22 @@ def test_upload_dataset(mdai_setup):


def test_upload_annotation(mdai_setup):
mdai_setup["parameters"]
parameters = mdai_setup["parameters"]
fixtures_dir = mdai_setup["fixtures_dir"]
mdai_client = mdai_setup["mdai_client"]
# sop_instance_uid can be acquired from mdai, or from the metadata generated
# by the function dicom_utils.read_dicoms_into_volume.
sop_instance_uid = "1.2.826.0.1.3680043.2.1125.1.75064541463040.2005072610414630768"
mdai_label_ids = parameters.get("mdai_label_ids")
labels_to_upload = parameters.get("labels")
label_id = mdai_label_ids.get(labels_to_upload[0])

failed_annotations = upload_image_annotation_slice(
segmentation_image_path=fixtures_dir / "humanct_0002_1000_seg.nii.gz",
sop_instance_uid=sop_instance_uid,
mdai_client=mdai_client,
mdai_project_id=parameters.get("mdai_project_id"),
mdai_dataset_id=parameters.get("mdai_dataset_id"),
mdai_label_id=label_id,
)
assert len(failed_annotations) == 0

0 comments on commit 0dda85d

Please sign in to comment.