Skip to content

Commit 6e70166

Browse files
committed
Merge branch 'dev' into compose-resampling
Signed-off-by: Wenqi Li <[email protected]>
2 parents c888882 + 009af55 commit 6e70166

29 files changed

+569
-246
lines changed

.github/workflows/cron.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
python -c 'import torch; print(torch.rand(5, 3, device=torch.device("cuda:0")))'
5151
BUILD_MONAI=1 ./runtests.sh --build --coverage --unittests --disttests # unit tests with coverage report
5252
BUILD_MONAI=1 ./runtests.sh --build --coverage --net # integration tests with coverage report
53-
coverage xml
53+
coverage xml --ignore-errors
5454
if pgrep python; then pkill python; fi
5555
- name: Upload coverage
5656
uses: codecov/codecov-action@v1
@@ -93,7 +93,7 @@ jobs:
9393
python -c 'import torch; print(torch.rand(5, 3, device=torch.device("cuda:0")))'
9494
BUILD_MONAI=1 ./runtests.sh --build --coverage --unittests --disttests # unit tests with coverage report
9595
BUILD_MONAI=1 ./runtests.sh --build --coverage --net # integration tests with coverage report
96-
coverage xml
96+
coverage xml --ignore-errors
9797
if pgrep python; then pkill python; fi
9898
- name: Upload coverage
9999
uses: codecov/codecov-action@v1
@@ -192,7 +192,7 @@ jobs:
192192
ngc --version
193193
BUILD_MONAI=1 ./runtests.sh --build --coverage --pytype --unittests --disttests # unit tests with pytype checks, coverage report
194194
BUILD_MONAI=1 ./runtests.sh --build --coverage --net # integration tests with coverage report
195-
coverage xml
195+
coverage xml --ignore-errors
196196
if pgrep python; then pkill python; fi
197197
- name: Upload coverage
198198
uses: codecov/codecov-action@v1

.github/workflows/pythonapp-gpu.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ jobs:
142142
# test the clang-format tool downloading once
143143
coverage run -m tests.clang_format_utils
144144
fi
145-
coverage xml
145+
coverage xml --ignore-errors
146146
if pgrep python; then pkill python; fi
147147
shell: bash
148148
- name: Upload coverage

.github/workflows/setupapp.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ jobs:
6161
python -c 'import torch; print(torch.rand(5, 3, device=torch.device("cuda:0")))'
6262
BUILD_MONAI=1 ./runtests.sh --build --coverage --unittests --disttests # unit tests with coverage report
6363
BUILD_MONAI=1 ./runtests.sh --build --coverage --net # integration tests with coverage report
64-
coverage xml
64+
coverage xml --ignore-errors
6565
if pgrep python; then pkill python; fi
6666
shell: bash
6767
- name: Upload coverage
@@ -105,7 +105,7 @@ jobs:
105105
python -m pip list
106106
python -c 'import torch; print(torch.__version__); print(torch.rand(5,3))'
107107
BUILD_MONAI=1 ./runtests.sh --build --quick --unittests --disttests
108-
coverage xml
108+
coverage xml --ignore-errors
109109
- name: Upload coverage
110110
uses: codecov/codecov-action@v1
111111
with:

monai/engines/evaluator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def get_stats(self, *vars):
161161
stats = {
162162
ESKeys.RANK: self.state.rank,
163163
ESKeys.BEST_VALIDATION_EPOCH: self.state.best_metric_epoch,
164-
ESKeys.BEST_VALIDATION_METRTC: self.state.best_metric,
164+
ESKeys.BEST_VALIDATION_METRIC: self.state.best_metric,
165165
}
166166
for k in vars:
167167
stats[k] = getattr(self.state, k, None)

monai/fl/utils/exchange_object.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(
4040
self.metrics = metrics
4141
self.weight_type = weight_type
4242
self.statistics = statistics
43+
self._summary: Dict = {}
4344

4445
@property
4546
def metrics(self):
@@ -81,5 +82,26 @@ def is_valid_weights(self):
8182
return False
8283
return True
8384

85+
def _add_to_summary(self, key, value):
86+
if value:
87+
if isinstance(value, dict):
88+
self._summary[key] = len(value)
89+
elif isinstance(value, WeightType):
90+
self._summary[key] = value
91+
else:
92+
self._summary[key] = type(value)
93+
8494
def summary(self):
85-
return dir(self)
95+
self._summary.update(self)
96+
for k, v in zip(
97+
["weights", "optim", "metrics", "weight_type", "statistics"],
98+
[self.weights, self.optim, self.metrics, self.weight_type, self.statistics],
99+
):
100+
self._add_to_summary(k, v)
101+
return self._summary
102+
103+
def __repr__(self):
104+
return str(self.summary())
105+
106+
def __str__(self):
107+
return str(self.summary())

monai/transforms/spatial/array.py

Lines changed: 252 additions & 125 deletions
Large diffs are not rendered by default.

monai/transforms/spatial/dictionary.py

Lines changed: 87 additions & 33 deletions
Large diffs are not rendered by default.

monai/transforms/utility/array.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -201,31 +201,33 @@ def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor:
201201

202202
class EnsureChannelFirst(Transform):
203203
"""
204-
Automatically adjust or add the channel dimension of input data to ensure `channel_first` shape.
204+
Adjust or add the channel dimension of input data to ensure `channel_first` shape.
205205
206206
This extracts the `original_channel_dim` info from provided meta_data dictionary or MetaTensor input. This value
207207
should state which dimension is the channel dimension so that it can be moved forward, or contain "no_channel" to
208208
state no dimension is the channel and so a 1-size first dimension is to be added.
209209
210210
Args:
211211
strict_check: whether to raise an error when the meta information is insufficient.
212-
add_channel_default: If True and the input image `img` is not a MetaTensor and `meta_dict` is not given,
213-
or `img` is a MetaTensor but doesn't specify channel dimension, use the value is `no_channel`
212+
channel_dim: If the input image `img` is not a MetaTensor or `meta_dict` is not given,
213+
this argument can be used to specify the original channel dimension (integer) of the input array.
214+
If the input array doesn't have a channel dim, this value should be ``'no_channel'`` (default).
215+
If this is set to `None`, this class relies on `img` or `meta_dict` to provide the channel dimension.
214216
"""
215217

216218
backend = [TransformBackends.TORCH, TransformBackends.NUMPY]
217219

218-
def __init__(self, strict_check: bool = True, add_channel_default=True):
220+
def __init__(self, strict_check: bool = True, channel_dim: Union[None, str, int] = "no_channel"):
219221
self.strict_check = strict_check
220-
self.add_channel_default = add_channel_default
222+
self.input_channel_dim = channel_dim
221223

222224
def __call__(self, img: torch.Tensor, meta_dict: Optional[Mapping] = None) -> torch.Tensor:
223225
"""
224226
Apply the transform to `img`.
225227
"""
226228
if not isinstance(img, MetaTensor) and not isinstance(meta_dict, Mapping):
227-
if not self.add_channel_default:
228-
msg = "Metadata not available and default add channel is disabled, EnsureChannelFirst is not in use."
229+
if self.input_channel_dim is None:
230+
msg = "Metadata not available and channel_dim=None, EnsureChannelFirst is not in use."
229231
if self.strict_check:
230232
raise ValueError(msg)
231233
warnings.warn(msg)
@@ -236,16 +238,18 @@ def __call__(self, img: torch.Tensor, meta_dict: Optional[Mapping] = None) -> to
236238
if isinstance(img, MetaTensor):
237239
meta_dict = img.meta
238240

239-
channel_dim = meta_dict.get("original_channel_dim") # type: ignore
241+
channel_dim = meta_dict.get("original_channel_dim", None) if isinstance(meta_dict, Mapping) else None
240242

241243
if channel_dim is None:
242-
if not self.add_channel_default:
243-
msg = "Unknown original_channel_dim in the MetaTensor meta dict or `meta_dict`."
244+
if self.input_channel_dim is None:
245+
msg = "Unknown original_channel_dim in the MetaTensor meta dict or `meta_dict` or `channel_dim`."
244246
if self.strict_check:
245247
raise ValueError(msg)
246248
warnings.warn(msg)
247249
return img
248-
meta_dict["original_channel_dim"] = channel_dim = "no_channel" # type: ignore
250+
channel_dim = self.input_channel_dim
251+
if isinstance(meta_dict, dict):
252+
meta_dict["original_channel_dim"] = self.input_channel_dim
249253

250254
if channel_dim == "no_channel":
251255
result = img[None]

monai/transforms/utility/dictionary.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,19 +301,21 @@ def __init__(
301301
meta_key_postfix: str = DEFAULT_POST_FIX,
302302
strict_check: bool = True,
303303
allow_missing_keys: bool = False,
304-
add_channel_default=True,
304+
channel_dim="no_channel",
305305
) -> None:
306306
"""
307307
Args:
308308
keys: keys of the corresponding items to be transformed.
309309
See also: :py:class:`monai.transforms.compose.MapTransform`
310310
strict_check: whether to raise an error when the meta information is insufficient.
311311
allow_missing_keys: don't raise exception if key is missing.
312-
add_channel_default: If True and the input image `img` is not a MetaTensor and `meta_dict` is not given,
313-
or `img` is a MetaTensor but doesn't specify channel dimension, use the value is `no_channel`
312+
channel_dim: If the input image `img` is not a MetaTensor or `meta_dict` is not given,
313+
this argument can be used to specify the original channel dimension (integer) of the input array.
314+
If the input array doesn't have a channel dim, this value should be ``'no_channel'`` (default).
315+
If this is set to `None`, this class relies on `img` or `meta_dict` to provide the channel dimension.
314316
"""
315317
super().__init__(keys, allow_missing_keys)
316-
self.adjuster = EnsureChannelFirst(strict_check=strict_check, add_channel_default=add_channel_default)
318+
self.adjuster = EnsureChannelFirst(strict_check=strict_check, channel_dim=channel_dim)
317319
self.meta_keys = ensure_tuple_rep(meta_keys, len(self.keys))
318320
self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys))
319321

monai/transforms/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,9 @@ def create_grid(
586586
"""
587587
compute a `spatial_size` mesh.
588588
589+
- when ``homogeneous=True``, the output shape is (N+1, dim_size_1, dim_size_2, ..., dim_size_N)
590+
- when ``homogeneous=False``, the output shape is (N, dim_size_1, dim_size_2, ..., dim_size_N)
591+
589592
Args:
590593
spatial_size: spatial size of the grid.
591594
spacing: same len as ``spatial_size``, defaults to 1.0 (dense grid).

monai/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@
3636
MetaKeys,
3737
Method,
3838
MetricReduction,
39+
NdimageMode,
3940
NumpyPadMode,
4041
PostFix,
4142
ProbMapKeys,
4243
PytorchPadMode,
4344
SkipMode,
4445
SpaceKeys,
46+
SplineMode,
4547
StrEnum,
4648
TraceKeys,
4749
TransformBackends,

monai/utils/enums.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
"StrEnum",
2020
"NumpyPadMode",
2121
"GridSampleMode",
22+
"SplineMode",
2223
"InterpolateMode",
2324
"UpsampleMode",
2425
"BlendMode",
2526
"PytorchPadMode",
27+
"NdimageMode",
2628
"GridSamplePadMode",
2729
"Average",
2830
"MetricReduction",
@@ -92,6 +94,22 @@ class NumpyPadMode(StrEnum):
9294
EMPTY = "empty"
9395

9496

97+
class NdimageMode(StrEnum):
98+
"""
99+
The available options determine how the input array is extended beyond its boundaries when interpolating.
100+
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
101+
"""
102+
103+
REFLECT = "reflect"
104+
GRID_MIRROR = "grid-mirror"
105+
CONSTANT = "constant"
106+
GRID_CONSTANT = "grid-constant"
107+
NEAREST = "nearest"
108+
MIRROR = "mirror"
109+
GRID_WRAP = "grid-wrap"
110+
WRAP = "wrap"
111+
112+
95113
class GridSampleMode(StrEnum):
96114
"""
97115
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
@@ -110,6 +128,21 @@ class GridSampleMode(StrEnum):
110128
BICUBIC = "bicubic"
111129

112130

131+
class SplineMode(StrEnum):
132+
"""
133+
Order of spline interpolation.
134+
135+
See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
136+
"""
137+
138+
ZERO = 0
139+
ONE = 1
140+
TWO = 2
141+
THREE = 3
142+
FOUR = 4
143+
FIVE = 5
144+
145+
113146
class InterpolateMode(StrEnum):
114147
"""
115148
See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html
@@ -493,4 +526,4 @@ class EngineStatsKeys(StrEnum):
493526
TOTAL_EPOCHS = "total_epochs"
494527
TOTAL_ITERATIONS = "total_iterations"
495528
BEST_VALIDATION_EPOCH = "best_validation_epoch"
496-
BEST_VALIDATION_METRTC = "best_validation_metric"
529+
BEST_VALIDATION_METRIC = "best_validation_metric"

monai/utils/type_conversion.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,13 @@ def convert_to_numpy(data, dtype: DtypeLike = None, wrap_sequence: bool = False)
175175
elif has_cp and isinstance(data, cp_ndarray):
176176
data = cp.asnumpy(data).astype(dtype, copy=False)
177177
elif isinstance(data, (np.ndarray, float, int, bool)):
178+
# Convert into a contiguous array first if the current dtype's size is smaller than the target dtype's size.
179+
# This help improve the performance because (convert to contiguous array) -> (convert dtype) is faster
180+
# than (convert dtype) -> (convert to contiguous array) when src dtype (e.g., uint8) is smaller than
181+
# target dtype(e.g., float32) and we are going to convert it to contiguous array anyway later in this
182+
# method.
183+
if isinstance(data, np.ndarray) and data.ndim > 0 and data.dtype.itemsize < np.dtype(dtype).itemsize:
184+
data = np.ascontiguousarray(data)
178185
data = np.asarray(data, dtype=dtype)
179186
elif isinstance(data, list):
180187
list_ret = [convert_to_numpy(i, dtype=dtype) for i in data]

tests/min_tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def run_testsuit():
136136
"test_rand_zoom",
137137
"test_rand_zoomd",
138138
"test_randtorchvisiond",
139+
"test_resample_backends",
139140
"test_resize",
140141
"test_resized",
141142
"test_resample_to_match",

tests/test_bundle_ckpt_export.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import json
1313
import os
14-
import subprocess
1514
import tempfile
1615
import unittest
1716

@@ -20,7 +19,7 @@
2019
from monai.bundle import ConfigParser
2120
from monai.data import load_net_with_metadata
2221
from monai.networks import save_state
23-
from tests.utils import skip_if_windows
22+
from tests.utils import command_line_tests, skip_if_windows
2423

2524
TEST_CASE_1 = [""]
2625

@@ -49,7 +48,7 @@ def test_export(self, key_in_ckpt):
4948
cmd = ["coverage", "run", "-m", "monai.bundle", "ckpt_export", "network_def", "--filepath", ts_file]
5049
cmd += ["--meta_file", meta_file, "--config_file", f"['{config_file}','{def_args_file}']", "--ckpt_file"]
5150
cmd += [ckpt_file, "--key_in_ckpt", key_in_ckpt, "--args_file", def_args_file]
52-
subprocess.check_call(cmd)
51+
command_line_tests(cmd)
5352
self.assertTrue(os.path.exists(ts_file))
5453

5554
_, metadata, extra_files = load_net_with_metadata(

tests/test_bundle_download.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import json
1313
import os
14-
import subprocess
1514
import tempfile
1615
import unittest
1716

@@ -21,7 +20,13 @@
2120
import monai.networks.nets as nets
2221
from monai.apps import check_hash
2322
from monai.bundle import ConfigParser, load
24-
from tests.utils import SkipIfBeforePyTorchVersion, skip_if_downloading_fails, skip_if_quick, skip_if_windows
23+
from tests.utils import (
24+
SkipIfBeforePyTorchVersion,
25+
command_line_tests,
26+
skip_if_downloading_fails,
27+
skip_if_quick,
28+
skip_if_windows,
29+
)
2530

2631
TEST_CASE_1 = [
2732
["model.pt", "model.ts", "network.json", "test_output.pt", "test_input.pt"],
@@ -64,7 +69,7 @@ def test_download_bundle(self, bundle_files, bundle_name, repo, hash_val):
6469
with tempfile.TemporaryDirectory() as tempdir:
6570
cmd = ["coverage", "run", "-m", "monai.bundle", "download", "--name", bundle_name, "--source", "github"]
6671
cmd += ["--bundle_dir", tempdir, "--repo", repo, "--progress", "False"]
67-
subprocess.check_call(cmd)
72+
command_line_tests(cmd)
6873
for file in bundle_files:
6974
file_path = os.path.join(tempdir, bundle_name, file)
7075
self.assertTrue(os.path.exists(file_path))
@@ -83,7 +88,7 @@ def test_url_download_bundle(self, bundle_files, bundle_name, url, hash_val):
8388
parser.export_config_file(config=def_args, filepath=def_args_file)
8489
cmd = ["coverage", "run", "-m", "monai.bundle", "download", "--args_file", def_args_file]
8590
cmd += ["--url", url]
86-
subprocess.check_call(cmd)
91+
command_line_tests(cmd)
8792
for file in bundle_files:
8893
file_path = os.path.join(tempdir, bundle_name, file)
8994
self.assertTrue(os.path.exists(file_path))

tests/test_bundle_init_bundle.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
# limitations under the License.
1111

1212
import os
13-
import subprocess
1413
import tempfile
1514
import unittest
1615

1716
import torch
1817

1918
from monai.networks.nets import UNet
20-
from tests.utils import skip_if_windows
19+
from tests.utils import command_line_tests, skip_if_windows
2120

2221

2322
@skip_if_windows
@@ -30,7 +29,7 @@ def test_bundle(self):
3029
bundle_root = tempdir + "/test_bundle"
3130

3231
cmd = ["coverage", "run", "-m", "monai.bundle", "init_bundle", bundle_root, tempdir + "/test.pt"]
33-
subprocess.check_call(cmd)
32+
command_line_tests(cmd)
3433

3534
self.assertTrue(os.path.exists(bundle_root + "/configs/metadata.json"))
3635
self.assertTrue(os.path.exists(bundle_root + "/configs/inference.json"))

0 commit comments

Comments
 (0)