diff --git a/monailabel/interfaces/tasks/infer.py b/monailabel/interfaces/tasks/infer.py index e4bce36e9..0e46145fc 100644 --- a/monailabel/interfaces/tasks/infer.py +++ b/monailabel/interfaces/tasks/infer.py @@ -378,7 +378,7 @@ def _get_network(self, device): model_state_dict = checkpoint.get(self.model_state_dict, checkpoint) network.load_state_dict(model_state_dict, strict=self.load_strict) else: - network = torch.jit.load(path, map_location=torch.device(device)).to(torch.device) + network = torch.jit.load(path, map_location=torch.device(device)) network.eval() self._networks[device] = (network, statbuf.st_mtime if statbuf else 0) diff --git a/monailabel/scribbles/infer.py b/monailabel/scribbles/infer.py index 62e5a96d2..7350c6a65 100644 --- a/monailabel/scribbles/infer.py +++ b/monailabel/scribbles/infer.py @@ -9,15 +9,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from monai.transforms import ( - Compose, - EnsureChannelFirstd, - FromMetaTensord, - LoadImaged, - ScaleIntensityRanged, - Spacingd, - ToMetaTensord, -) +from monai.transforms import Compose, EnsureChannelFirstd, LoadImaged, ScaleIntensityRanged, Spacingd from monailabel.interfaces.tasks.infer import InferTask, InferType from monailabel.scribbles.transforms import ( @@ -72,13 +64,11 @@ def pre_transforms(self, data): return [ LoadImaged(keys=["image", "label"]), EnsureChannelFirstd(keys=["image", "label"]), - FromMetaTensord(keys=["image", "label"]), AddBackgroundScribblesFromROId( scribbles="label", scribbles_bg_label=self.scribbles_bg_label, scribbles_fg_label=self.scribbles_fg_label, ), - ToMetaTensord(keys=["image", "label"]), Spacingd(keys=["image", "label"], pixdim=self.pix_dim, mode=["bilinear", "nearest"]), ScaleIntensityRanged( keys="image", @@ -112,7 +102,6 @@ def post_transforms(self, data): lamda=self.lamda, sigma=self.sigma, ), - FromMetaTensord(keys=["image"]), Restored(keys="pred", ref_image="image"), BoundingBoxd(keys="pred", result="result", bbox="bbox"), ] diff --git a/monailabel/scribbles/transforms.py b/monailabel/scribbles/transforms.py index db97f0b5e..2c99e0297 100644 --- a/monailabel/scribbles/transforms.py +++ b/monailabel/scribbles/transforms.py @@ -14,6 +14,7 @@ import numpy as np import torch +from monai.data import MetaTensor from monai.networks.blocks import CRF from monai.transforms import Transform from scipy.special import softmax @@ -39,7 +40,7 @@ def _fetch_data(self, data, key): if key not in data.keys(): raise ValueError(f"Key {key} not found, present keys {data.keys()}") - return data[key].numpy() if isinstance(data[key], torch.Tensor) else data[key] + return data[key].array if isinstance(data[key], MetaTensor) else data[key] def _normalise_logits(self, data, axis=0): # check if logits is a true prob, if not then apply softmax diff --git a/monailabel/transform/post.py b/monailabel/transform/post.py index 4fff975c5..efaa8a007 100644 --- a/monailabel/transform/post.py +++ b/monailabel/transform/post.py @@ -15,6 +15,7 @@ import numpy as np import skimage.measure as measure from monai.config import KeysCollection +from monai.data import MetaTensor from monai.transforms import MapTransform, Resize, generate_spatial_bounding_box, get_extreme_points from monai.utils import InterpolateMode, ensure_tuple_rep @@ -102,11 +103,16 @@ def __init__( def __call__(self, data): d = dict(data) - meta_dict = d[f"{self.ref_image}_{self.meta_key_postfix}"] + meta_dict = ( + d[self.ref_image].meta + if d.get(self.ref_image) is not None and isinstance(d[self.ref_image], MetaTensor) + else d.get(f"{self.ref_image}_{self.meta_key_postfix}", {}) + ) + for idx, key in enumerate(self.keys): result = d[key] current_size = result.shape[1:] if self.has_channel else result.shape - spatial_shape = meta_dict["spatial_shape"] + spatial_shape = meta_dict.get("spatial_shape", current_size) spatial_size = spatial_shape[-len(current_size) :] # Undo Spacing diff --git a/monailabel/transform/writer.py b/monailabel/transform/writer.py index 9b95698cc..74f9ed4dd 100644 --- a/monailabel/transform/writer.py +++ b/monailabel/transform/writer.py @@ -16,7 +16,7 @@ import nrrd import numpy as np import torch -from monai.data import write_nifti +from monai.data import MetaTensor, write_nifti from monailabel.utils.others.generic import file_ext from monailabel.utils.others.pathology import create_asap_annotations_xml, create_dsa_annotations_json @@ -185,13 +185,17 @@ def __call__(self, data): ext = ext if ext else ".nii.gz" logger.info(f"Result ext: {ext}; write_to_file: {write_to_file}; dtype: {dtype}") - image_np = data[self.label] - if isinstance(image_np, torch.Tensor): - image_np = image_np.numpy() + if isinstance(data[self.label], MetaTensor): + image_np = data[self.label].array + else: + image_np = data[self.label] + + # Always using Restored as the last transform before writing meta_dict = data.get(f"{self.ref_image}_{self.meta_key_postfix}") affine = meta_dict.get("affine") if meta_dict else None - if isinstance(affine, torch.Tensor): - affine = affine.numpy() + if affine is None and isinstance(data[self.ref_image], MetaTensor): + affine = data[self.ref_image].affine + logger.debug(f"Image: {image_np.shape}; Data Image: {data[self.label].shape}") output_file = None diff --git a/requirements.txt b/requirements.txt index 1ecb1d51b..8f3969e91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ torch>=1.7 -monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide]>=0.9.1rc3 +monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide]>=0.9.1rc4 uvicorn==0.17.6 pydantic==1.9.1 python-dotenv==0.20.0 diff --git a/runtests.sh b/runtests.sh index 28f53267e..570783e41 100755 --- a/runtests.sh +++ b/runtests.sh @@ -426,20 +426,20 @@ function check_server_running() { echo ${code} } -# network training/inference/eval integration tests -if [ $doNetTests = true ]; then + +function run_integration_tests() { echo "${separator}${blue}integration${noColor}" torch_validate ${cmdPrefix}${PY_EXE} tests/setup.py - echo "Starting MONAILabel server..." + echo "$1 - Starting MONAILabel server..." rm -rf tests/data/apps - monailabel apps -n radiology -o tests/data/apps -d - monailabel start_server -a tests/data/apps/radiology -c models all -s tests/data/dataset/local/spleen -p ${MONAILABEL_SERVER_PORT:-8000} & + monailabel apps -n $1 -o tests/data/apps -d + monailabel start_server -a tests/data/apps/$1 -c models all -s $2 -p ${MONAILABEL_SERVER_PORT:-8000} & wait_time=0 server_is_up=0 - start_time_out=180 + start_time_out=240 while [[ $wait_time -le ${start_time_out} ]]; do if [ "$(check_server_running)" == "200" ]; then @@ -448,18 +448,24 @@ if [ $doNetTests = true ]; then fi sleep 5 wait_time=$((wait_time + 5)) - echo "Waiting for MONAILabel to be up and running..." + echo "$1 - Waiting for MONAILabel to be up and running..." done echo "" if [ "$server_is_up" == "1" ]; then - echo "MONAILabel server is up and running." + echo "$1 - MONAILabel server is up and running." else - echo "Failed to start MONAILabel server. Exiting..." + echo "$1 - Failed to start MONAILabel server. Exiting..." exit 1 fi - ${cmdPrefix}${cmd} -m pytest -v tests/integration --no-summary -x - echo "Finished All Integration Tests; Stop/Kill MONAILabel Server..." + ${cmdPrefix}${cmd} -m pytest -v tests/integration/$1 --no-summary -x + echo "$1 - Finished All Integration Tests; Stop/Kill MONAILabel Server..." kill -9 $(ps -ef | grep monailabel | grep start_server | grep -v grep | awk '{print $2}') +} + +# network training/inference/eval integration tests +if [ $doNetTests = true ]; then + run_integration_tests "radiology" "tests/data/dataset/local/spleen" + run_integration_tests "pathology" "tests/data/pathology" fi diff --git a/sample-apps/endoscopy/lib/net/__init__.py b/sample-apps/endoscopy/lib/net/__init__.py index e69de29bb..1e97f8940 100644 --- a/sample-apps/endoscopy/lib/net/__init__.py +++ b/sample-apps/endoscopy/lib/net/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/sample-apps/radiology/lib/activelearning/__init__.py b/sample-apps/radiology/lib/activelearning/__init__.py index e69de29bb..78a5a9d57 100644 --- a/sample-apps/radiology/lib/activelearning/__init__.py +++ b/sample-apps/radiology/lib/activelearning/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .first import First diff --git a/sample-apps/radiology/lib/infers/deepedit.py b/sample-apps/radiology/lib/infers/deepedit.py index b6e9bba78..ed064c436 100644 --- a/sample-apps/radiology/lib/infers/deepedit.py +++ b/sample-apps/radiology/lib/infers/deepedit.py @@ -22,13 +22,11 @@ AsDiscreted, EnsureChannelFirstd, EnsureTyped, - FromMetaTensord, LoadImaged, Orientationd, Resized, ScaleIntensityRanged, SqueezeDimd, - ToMetaTensord, ToNumpyd, ) @@ -85,22 +83,18 @@ def pre_transforms(self, data=None): AddGuidanceFromPointsDeepEditd(ref_image="image", guidance="guidance", label_names=self.labels), Resized(keys="image", spatial_size=self.spatial_size, mode="area"), ResizeGuidanceMultipleLabelDeepEditd(guidance="guidance", ref_image="image"), - FromMetaTensord(keys="image"), AddGuidanceSignalDeepEditd( keys="image", guidance="guidance", number_intensity_ch=self.number_intensity_ch ), - ToMetaTensord(keys="image"), ] ) else: t.extend( [ Resized(keys="image", spatial_size=self.spatial_size, mode="area"), - FromMetaTensord(keys="image"), DiscardAddGuidanced( keys="image", label_names=self.labels, number_intensity_ch=self.number_intensity_ch ), - ToMetaTensord(keys="image"), ] ) @@ -120,6 +114,5 @@ def post_transforms(self, data=None) -> Sequence[Callable]: AsDiscreted(keys="pred", argmax=True), SqueezeDimd(keys="pred", dim=0), ToNumpyd(keys="pred"), - FromMetaTensord(keys="image"), Restored(keys="pred", ref_image="image"), ] diff --git a/sample-apps/radiology/main.py b/sample-apps/radiology/main.py index 5a7c49fe8..789573e30 100644 --- a/sample-apps/radiology/main.py +++ b/sample-apps/radiology/main.py @@ -15,7 +15,7 @@ from typing import Dict import lib.configs -from lib.activelearning.first import First +from lib.activelearning import First from monailabel.interfaces.app import MONAILabelApp from monailabel.interfaces.config import TaskConfig diff --git a/setup.cfg b/setup.cfg index e040cbf2c..bdd73cb75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ setup_requires = ninja install_requires = torch>=1.7 - monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide]>=0.9.1rc3 + monai[nibabel, skimage, pillow, tensorboard, gdown, ignite, torchvision, itk, tqdm, lmdb, psutil, openslide]>=0.9.1rc4 uvicorn==0.17.6 pydantic==1.9.1 python-dotenv==0.20.0 diff --git a/tests/integration/pathology/__init__.py b/tests/integration/pathology/__init__.py new file mode 100644 index 000000000..1e97f8940 --- /dev/null +++ b/tests/integration/pathology/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/integration/pathology/test_infer.py b/tests/integration/pathology/test_infer.py new file mode 100644 index 000000000..c01da1d7f --- /dev/null +++ b/tests/integration/pathology/test_infer.py @@ -0,0 +1,79 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest + +import requests +import torch + +from tests.integration import SERVER_URI + + +class EndPointInfer(unittest.TestCase): + def test_deepedit_nuclei(self): + if not torch.cuda.is_available(): + return + + model = "deepedit_nuclei" + image = "JP2K-33003-1" + body = { + "level": 0, + "location": [2206, 4925], + "size": [360, 292], + "tile_size": [2048, 2048], + "min_poly_area": 30, + "params": {"foreground": [], "background": []}, + } + + response = requests.post(f"{SERVER_URI}/infer/wsi/{model}?image={image}&output=dsa", json=body) + assert response.status_code == 200 + + def test_segmentation_nuclei(self): + if not torch.cuda.is_available(): + return + + model = "segmentation_nuclei" + image = "JP2K-33003-1" + body = { + "level": 0, + "location": [2206, 4925], + "size": [360, 292], + "tile_size": [2048, 2048], + "min_poly_area": 30, + "params": {"foreground": [], "background": []}, + } + + response = requests.post(f"{SERVER_URI}/infer/wsi/{model}?image={image}&output=asap", json=body) + assert response.status_code == 200 + + def test_nuclick(self): + if not torch.cuda.is_available(): + return + + model = "nuclick" + image = "JP2K-33003-1" + body = { + "level": 0, + "location": [2206, 4925], + "size": [360, 292], + "tile_size": [2048, 2048], + "min_poly_area": 30, + "params": { + "foreground": [[2427, 4976], [2341, 5033], [2322, 5207], [2305, 5212], [2268, 5182]], + "background": [], + }, + } + + response = requests.post(f"{SERVER_URI}/infer/wsi/{model}?image={image}&output=asap", json=body) + assert response.status_code == 200 + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/test_info.py b/tests/integration/pathology/test_info.py similarity index 96% rename from tests/integration/test_info.py rename to tests/integration/pathology/test_info.py index f79b3d853..8a4b997ff 100644 --- a/tests/integration/test_info.py +++ b/tests/integration/pathology/test_info.py @@ -13,7 +13,7 @@ import requests -from . import SERVER_URI +from tests.integration import SERVER_URI class EndPointInfo(unittest.TestCase): diff --git a/tests/integration/radiology/__init__.py b/tests/integration/radiology/__init__.py new file mode 100644 index 000000000..1e97f8940 --- /dev/null +++ b/tests/integration/radiology/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/integration/test_infer.py b/tests/integration/radiology/test_infer.py similarity index 97% rename from tests/integration/test_infer.py rename to tests/integration/radiology/test_infer.py index 7f8a6c6cf..eedb4f4fd 100644 --- a/tests/integration/test_infer.py +++ b/tests/integration/radiology/test_infer.py @@ -14,7 +14,7 @@ import requests import torch -from . import SERVER_URI +from tests.integration import SERVER_URI class EndPointInfer(unittest.TestCase): diff --git a/tests/integration/radiology/test_info.py b/tests/integration/radiology/test_info.py new file mode 100644 index 000000000..8a4b997ff --- /dev/null +++ b/tests/integration/radiology/test_info.py @@ -0,0 +1,31 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import requests + +from tests.integration import SERVER_URI + + +class EndPointInfo(unittest.TestCase): + def test_info(self): + response = requests.get(f"{SERVER_URI}/info/") + assert response.status_code == 200 + + # check if following fields exist in the response + res = response.json() + for f in ["version", "name", "labels"]: + assert res[f] + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/integration/test_session.py b/tests/integration/radiology/test_session.py similarity index 100% rename from tests/integration/test_session.py rename to tests/integration/radiology/test_session.py diff --git a/tests/setup.py b/tests/setup.py index 3544c0446..3be893ec2 100644 --- a/tests/setup.py +++ b/tests/setup.py @@ -24,6 +24,12 @@ def run_main(): download_url(url=dataset_url, filepath=dataset_file) extractall(filepath=dataset_file, output_dir=TEST_DATA) + pathology_file = os.path.join(TEST_DATA, "pathology", "JP2K-33003-1.svs") + pathology_url = "https://demo.kitware.com/histomicstk/api/v1/item/5d5c07509114c049342b66f8/download" + if not os.path.exists(os.path.join(TEST_DATA, "pathology")): + if not os.path.exists(pathology_file): + download_url(url=pathology_url, filepath=pathology_file) + if __name__ == "__main__": run_main()