Skip to content

Commit

Permalink
Merge branch 'Project-MONAI:dev' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Rilla authored Jul 26, 2024
2 parents 6e1970f + 2e53df7 commit 94195e9
Show file tree
Hide file tree
Showing 21 changed files with 196 additions and 36 deletions.
8 changes: 5 additions & 3 deletions docs/source/mb_specification.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ This file contains the metadata information relating to the model, including wha
* **monai_version**: version of MONAI the bundle was generated on, later versions expected to work.
* **pytorch_version**: version of Pytorch the bundle was generated on, later versions expected to work.
* **numpy_version**: version of Numpy the bundle was generated on, later versions expected to work.
* **optional_packages_version**: dictionary relating optional package names to their versions, these packages are not needed but are recommended to be installed with this stated minimum version.
* **required_packages_version**: dictionary relating required package names to their versions. These are packages in addition to the base requirements of MONAI which this bundle absolutely needs. For example, if the bundle must load Nifti files the Nibabel package will be required.
* **task**: plain-language description of what the model is meant to do.
* **description**: longer form plain-language description of what the model is, what it does, etc.
* **authors**: state author(s) of the model.
* **copyright**: state model copyright.
* **network_data_format**: defines the format, shape, and meaning of inputs and outputs to the model, contains keys "inputs" and "outputs" relating named inputs/outputs to their format specifiers (defined below).
* **network_data_format**: defines the format, shape, and meaning of inputs and outputs to the (primary) model, contains keys "inputs" and "outputs" relating named inputs/outputs to their format specifiers (defined below). There is also an optional "post_processed_outputs" key stating the format of "outputs" after postprocessing transforms are applied, this is used to describe the final output from the bundle if it varies from the raw network output. These keys can also relate to primitive values (number, string, boolean), instead of the tensor format specified below.

Tensor format specifiers are used to define input and output tensors and their meanings, and must be a dictionary containing at least these keys:

Expand All @@ -89,6 +89,8 @@ Optional keys:
* **data_source**: description of where training/validation can be sourced.
* **data_type**: type of source data used for training/validation.
* **references**: list of published referenced relating to the model.
* **supported_apps**: list of supported applications which use bundles, eg. 'monai-label' would be present if the bundle is compatible with MONAI Label applications.
* **\*_data_format**: defines the format, shape, and meaning of inputs and outputs to additional models which are secondary to the main model. This contains the same sort of information as **network_data_format** which describes networks providing secondary functionality, eg. a localisation network used to identify ROI in an image for cropping before data is sent to the primary network of this bundle.

The format for tensors used as inputs and outputs can be used to specify semantic meaning of these values, and later is used by software handling bundles to determine how to process and interpret this data. There are various types of image data that MONAI is uses, and other data types such as point clouds, dictionary sequences, time signals, and others. The following list is provided as a set of supported definitions of what a tensor "format" is but is not exhaustive and users can provide their own which would be left up to the model users to interpret:

Expand Down Expand Up @@ -124,7 +126,7 @@ An example JSON metadata file:
"monai_version": "0.9.0",
"pytorch_version": "1.10.0",
"numpy_version": "1.21.2",
"optional_packages_version": {"nibabel": "3.2.1"},
"required_packages_version": {"nibabel": "3.2.1"},
"task": "Decathlon spleen segmentation",
"description": "A pre-trained model for volumetric (3D) segmentation of the spleen from CT image",
"authors": "MONAI team",
Expand Down
2 changes: 1 addition & 1 deletion monai/auto3dseg/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe

unique_label = unique(ndas_label)
if isinstance(ndas_label, (MetaTensor, torch.Tensor)):
unique_label = unique_label.data.cpu().numpy()
unique_label = unique_label.data.cpu().numpy() # type: ignore[assignment]

unique_label = unique_label.astype(np.int16).tolist()

Expand Down
138 changes: 111 additions & 27 deletions monai/bundle/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import torch
from torch.cuda import is_available

from monai.apps.mmars.mmars import _get_all_ngc_models
from monai._version import get_versions
from monai.apps.utils import _basename, download_url, extractall, get_logger
from monai.bundle.config_item import ConfigComponent
from monai.bundle.config_parser import ConfigParser
Expand Down Expand Up @@ -67,6 +67,9 @@
DEFAULT_DOWNLOAD_SOURCE = os.environ.get("BUNDLE_DOWNLOAD_SRC", "monaihosting")
PPRINT_CONFIG_N = 5

MONAI_HOSTING_BASE_URL = "https://api.ngc.nvidia.com/v2/models/nvidia/monaihosting"
NGC_BASE_URL = "https://api.ngc.nvidia.com/v2/models/nvidia/monaitoolkit"


def update_kwargs(args: str | dict | None = None, ignore_none: bool = True, **kwargs: Any) -> dict:
"""
Expand Down Expand Up @@ -169,16 +172,19 @@ def _get_git_release_url(repo_owner: str, repo_name: str, tag_name: str, filenam


def _get_ngc_bundle_url(model_name: str, version: str) -> str:
return f"https://api.ngc.nvidia.com/v2/models/nvidia/monaitoolkit/{model_name.lower()}/versions/{version}/zip"
return f"{NGC_BASE_URL}/{model_name.lower()}/versions/{version}/zip"


def _get_ngc_private_base_url(repo: str) -> str:
return f"https://api.ngc.nvidia.com/v2/{repo}/models"


def _get_ngc_private_bundle_url(model_name: str, version: str, repo: str) -> str:
return f"https://api.ngc.nvidia.com/v2/{repo}/models/{model_name.lower()}/versions/{version}/zip"
return f"{_get_ngc_private_base_url(repo)}/{model_name.lower()}/versions/{version}/zip"


def _get_monaihosting_bundle_url(model_name: str, version: str) -> str:
monaihosting_root_path = "https://api.ngc.nvidia.com/v2/models/nvidia/monaihosting"
return f"{monaihosting_root_path}/{model_name.lower()}/versions/{version}/files/{model_name}_v{version}.zip"
return f"{MONAI_HOSTING_BASE_URL}/{model_name.lower()}/versions/{version}/files/{model_name}_v{version}.zip"


def _download_from_github(repo: str, download_path: Path, filename: str, progress: bool = True) -> None:
Expand Down Expand Up @@ -267,8 +273,7 @@ def _get_ngc_token(api_key, retry=0):


def _get_latest_bundle_version_monaihosting(name):
url = "https://api.ngc.nvidia.com/v2/models/nvidia/monaihosting"
full_url = f"{url}/{name.lower()}"
full_url = f"{MONAI_HOSTING_BASE_URL}/{name.lower()}"
requests_get, has_requests = optional_import("requests", name="get")
if has_requests:
resp = requests_get(full_url)
Expand All @@ -279,36 +284,114 @@ def _get_latest_bundle_version_monaihosting(name):
return model_info["model"]["latestVersionIdStr"]


def _get_latest_bundle_version_private_registry(name, repo, headers=None):
url = f"https://api.ngc.nvidia.com/v2/{repo}/models"
full_url = f"{url}/{name.lower()}"
requests_get, has_requests = optional_import("requests", name="get")
if has_requests:
headers = {} if headers is None else headers
resp = requests_get(full_url, headers=headers)
resp.raise_for_status()
else:
raise ValueError("NGC API requires requests package. Please install it.")
def _examine_monai_version(monai_version: str) -> tuple[bool, str]:
"""Examine if the package version is compatible with the MONAI version in the metadata."""
version_dict = get_versions()
package_version = version_dict.get("version", "0+unknown")
if package_version == "0+unknown":
return False, "Package version is not available. Skipping version check."
if monai_version == "0+unknown":
return False, "MONAI version is not specified in the bundle. Skipping version check."
# treat rc versions as the same as the release version
package_version = re.sub(r"rc\d.*", "", package_version)
monai_version = re.sub(r"rc\d.*", "", monai_version)
if package_version < monai_version:
return (
False,
f"Your MONAI version is {package_version}, but the bundle is built on MONAI version {monai_version}.",
)
return True, ""


def _check_monai_version(bundle_dir: PathLike, name: str) -> None:
"""Get the `monai_version` from the metadata.json and compare if it is smaller than the installed `monai` package version"""
metadata_file = Path(bundle_dir) / name / "configs" / "metadata.json"
if not metadata_file.exists():
logger.warning(f"metadata file not found in {metadata_file}.")
return
with open(metadata_file) as f:
metadata = json.load(f)
is_compatible, msg = _examine_monai_version(metadata.get("monai_version", "0+unknown"))
if not is_compatible:
logger.warning(msg)


def _list_latest_versions(data: dict, max_versions: int = 3) -> list[str]:
"""
Extract the latest versions from the data dictionary.
Args:
data: the data dictionary.
max_versions: the maximum number of versions to return.
Returns:
versions of the latest models in the reverse order of creation date, e.g. ['1.0.0', '0.9.0', '0.8.0'].
"""
# Check if the data is a dictionary and it has the key 'modelVersions'
if not isinstance(data, dict) or "modelVersions" not in data:
raise ValueError("The data is not a dictionary or it does not have the key 'modelVersions'.")

# Extract the list of model versions
model_versions = data["modelVersions"]

if (
not isinstance(model_versions, list)
or len(model_versions) == 0
or "createdDate" not in model_versions[0]
or "versionId" not in model_versions[0]
):
raise ValueError(
"The model versions are not a list or it is empty or it does not have the keys 'createdDate' and 'versionId'."
)

# Sort the versions by the 'createdDate' in descending order
sorted_versions = sorted(model_versions, key=lambda x: x["createdDate"], reverse=True)
return [v["versionId"] for v in sorted_versions[:max_versions]]


def _get_latest_bundle_version_ngc(name: str, repo: str | None = None, headers: dict | None = None) -> str:
base_url = _get_ngc_private_base_url(repo) if repo else NGC_BASE_URL
version_endpoint = base_url + f"/{name.lower()}/versions/"

if not has_requests:
raise ValueError("requests package is required, please install it.")

version_header = {"Accept-Encoding": "gzip, deflate"} # Excluding 'zstd' to fit NGC requirements
if headers:
version_header.update(headers)
resp = requests_get(version_endpoint, headers=version_header)
resp.raise_for_status()
model_info = json.loads(resp.text)
return model_info["model"]["latestVersionIdStr"]
latest_versions = _list_latest_versions(model_info)

for version in latest_versions:
file_endpoint = base_url + f"/{name.lower()}/versions/{version}/files/configs/metadata.json"
resp = requests_get(file_endpoint, headers=headers)
metadata = json.loads(resp.text)
resp.raise_for_status()
# if the package version is not available or the model is compatible with the package version
is_compatible, _ = _examine_monai_version(metadata["monai_version"])
if is_compatible:
if version != latest_versions[0]:
logger.info(f"Latest version is {latest_versions[0]}, but the compatible version is {version}.")
return version

# if no compatible version is found, return the latest version
return latest_versions[0]


def _get_latest_bundle_version(
source: str, name: str, repo: str, **kwargs: Any
) -> dict[str, list[str] | str] | Any | None:
if source == "ngc":
name = _add_ngc_prefix(name)
model_dict = _get_all_ngc_models(name)
for v in model_dict.values():
if v["name"] == name:
return v["latest"]
return None
return _get_latest_bundle_version_ngc(name)
elif source == "monaihosting":
return _get_latest_bundle_version_monaihosting(name)
elif source == "ngc_private":
headers = kwargs.pop("headers", {})
name = _add_ngc_prefix(name)
return _get_latest_bundle_version_private_registry(name, repo, headers)
return _get_latest_bundle_version_ngc(name, repo=repo, headers=headers)
elif source == "github":
repo_owner, repo_name, tag_name = repo.split("/")
return get_bundle_versions(name, repo=f"{repo_owner}/{repo_name}", tag=tag_name)["latest_version"]
Expand Down Expand Up @@ -470,9 +553,8 @@ def download(
if version_ is None:
version_ = _get_latest_bundle_version(source=source_, name=name_, repo=repo_, headers=headers)
if source_ == "github":
if version_ is not None:
name_ = "_v".join([name_, version_])
_download_from_github(repo=repo_, download_path=bundle_dir_, filename=name_, progress=progress_)
name_ver = "_v".join([name_, version_]) if version_ is not None else name_
_download_from_github(repo=repo_, download_path=bundle_dir_, filename=name_ver, progress=progress_)
elif source_ == "monaihosting":
_download_from_monaihosting(download_path=bundle_dir_, filename=name_, version=version_, progress=progress_)
elif source_ == "ngc":
Expand Down Expand Up @@ -501,6 +583,8 @@ def download(
f"got source: {source_}."
)

_check_monai_version(bundle_dir_, name_)


@deprecated_arg("net_name", since="1.2", removed="1.5", msg_suffix="please use ``model`` instead.")
@deprecated_arg("net_kwargs", since="1.2", removed="1.5", msg_suffix="please use ``model`` instead.")
Expand Down
2 changes: 2 additions & 0 deletions monai/data/test_time_augmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class TestTimeAugmentation:
mode, mean, std, vvc = tt_aug(test_data)
"""

__test__ = False # indicate to pytest that this class is not intended for collection

def __init__(
self,
transform: InvertibleTransform,
Expand Down
2 changes: 2 additions & 0 deletions monai/metrics/cumulative_average.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def get_current(self, to_numpy: bool = True) -> NdarrayOrTensor:
if self.val is None:
return 0

val: NdarrayOrTensor
val = self.val.clone()
val[~torch.isfinite(val)] = 0

Expand Down Expand Up @@ -96,6 +97,7 @@ def aggregate(self, to_numpy: bool = True) -> NdarrayOrTensor:
dist.all_reduce(sum)
dist.all_reduce(count)

val: NdarrayOrTensor
val = torch.where(count > 0, sum / count, sum)

if to_numpy:
Expand Down
2 changes: 1 addition & 1 deletion monai/metrics/panoptic_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def _get_paired_iou(

return paired_iou, paired_true, paired_pred

pairwise_iou = pairwise_iou.cpu().numpy()
pairwise_iou = pairwise_iou.cpu().numpy() # type: ignore[assignment]
paired_true, paired_pred = linear_sum_assignment(-pairwise_iou)
paired_iou = pairwise_iou[paired_true, paired_pred]
paired_true = torch.as_tensor(list(paired_true[paired_iou > match_iou_threshold] + 1), device=device)
Expand Down
4 changes: 2 additions & 2 deletions monai/metrics/rocauc.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ def _calculate(y_pred: torch.Tensor, y: torch.Tensor) -> float:

n = len(y)
indices = y_pred.argsort()
y = y[indices].cpu().numpy()
y_pred = y_pred[indices].cpu().numpy()
y = y[indices].cpu().numpy() # type: ignore[assignment]
y_pred = y_pred[indices].cpu().numpy() # type: ignore[assignment]
nneg = auc = tmp_pos = tmp_neg = 0.0

for i in range(n):
Expand Down
2 changes: 1 addition & 1 deletion monai/transforms/croppad/functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def _np_pad(img: NdarrayTensor, pad_width: list[tuple[int, int]], mode: str, **k
warnings.warn(f"Padding: moving img {img.shape} from cuda to cpu for dtype={img.dtype} mode={mode}.")
img_np = img.detach().cpu().numpy()
else:
img_np = img
img_np = np.asarray(img)
mode = convert_pad_mode(dst=img_np, mode=mode).value
if mode == "constant" and "value" in kwargs:
kwargs["constant_values"] = kwargs.pop("value")
Expand Down
4 changes: 3 additions & 1 deletion monai/visualize/img2tensorboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ def plot_2d_or_3d_image(
# as the `d` data has no batch dim, reduce the spatial dim index if positive
frame_dim = frame_dim - 1 if frame_dim > 0 else frame_dim

d: np.ndarray = data_index.detach().cpu().numpy() if isinstance(data_index, torch.Tensor) else data_index
d: np.ndarray = (
data_index.detach().cpu().numpy() if isinstance(data_index, torch.Tensor) else np.asarray(data_index)
)

if d.ndim == 2:
d = rescale_array(d, 0, 1) # type: ignore
Expand Down
1 change: 1 addition & 0 deletions tests/test_arraydataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@


class TestCompose(Compose):
__test__ = False # indicate to pytest that this class is not intended for collection

def __call__(self, input_, lazy=False):
img = self.transforms[0](input_)
Expand Down
6 changes: 6 additions & 0 deletions tests/test_auto3dseg.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class TestOperations(Operations):
Test example for user operation
"""

__test__ = False # indicate to pytest that this class is not intended for collection

def __init__(self) -> None:
self.data = {"max": np.max, "mean": np.mean, "min": np.min}

Expand All @@ -132,6 +134,8 @@ class TestAnalyzer(Analyzer):
Test example for a simple Analyzer
"""

__test__ = False # indicate to pytest that this class is not intended for collection

def __init__(self, key, report_format, stats_name="test"):
self.key = key
super().__init__(stats_name, report_format)
Expand All @@ -149,6 +153,8 @@ class TestImageAnalyzer(Analyzer):
Test example for a simple Analyzer
"""

__test__ = False # indicate to pytest that this class is not intended for collection

def __init__(self, image_key="image", stats_name="test_image"):
self.image_key = image_key
report_format = {"test_stats": None}
Expand Down
Loading

0 comments on commit 94195e9

Please sign in to comment.