Skip to content

Commit 0faf755

Browse files
authored
Merge branch 'dev' into patch-1
2 parents 0307f30 + 8690ae7 commit 0faf755

9 files changed

Lines changed: 140 additions & 22 deletions

File tree

CONTRIBUTING.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ By making a contribution to this project, I certify that:
303303
this project or the open source license(s) involved.
304304
```
305305

306+
> **Tip:** If you need to add a DCO remediation commit (e.g., after a force-push
307+
> or rebase), include `[skip ci]` in the commit message so the remediation
308+
> does not trigger unnecessary CI pipelines:
309+
> ```bash
310+
> git commit -s --allow-empty -m 'DCO Remediation Commit for... [skip ci]'
311+
> ```
312+
306313
#### Utility functions
307314
308315
MONAI provides a set of generic utility functions and frequently used routines.
@@ -358,6 +365,90 @@ Ideally, the new branch should be based on the latest `dev` branch.
358365
1. Reviewer and contributor may have discussions back and forth until all comments addressed.
359366
1. Wait for the pull request to be merged.
360367
368+
## Skipping CI
369+
370+
MONAI's CI pipelines run automatically on every push and pull request.
371+
These pipelines can be resource-intensive, especially the full premerge matrix
372+
which spans multiple OSes, Python versions, and PyTorch versions.
373+
374+
To reduce unnecessary resource consumption and speed up iteration, you can
375+
skip CI on commits that don't need automated validation — for example,
376+
documentation-only changes, README updates, workflow YAML changes, or WIP
377+
commits during development.
378+
379+
### Mechanism
380+
381+
GitHub Actions natively supports skipping `push` and `pull_request` workflows
382+
when the commit message contains any of the following strings:
383+
384+
- `[skip ci]`
385+
- `[ci skip]`
386+
- `[no ci]`
387+
- `[skip actions]`
388+
- `[actions skip]`
389+
390+
These are case-insensitive. `[skip ci]` is the recommended convention for
391+
this repository.
392+
393+
Alternatively, you can add a `skip-checks: true` trailer at the end of the
394+
commit message, preceded by two blank lines:
395+
396+
```
397+
commit message
398+
399+
skip-checks: true
400+
```
401+
402+
### Usage
403+
404+
Add the keyword anywhere in the commit message when committing:
405+
406+
```bash
407+
git commit -s -m 'update docs [skip ci]'
408+
```
409+
410+
If the HEAD commit of a pull request contains the skip instruction,
411+
the entire PR's pull_request-triggered workflows are skipped.
412+
413+
### Which workflows are affected
414+
415+
The skip instruction applies only to workflows triggered by `on: push` or
416+
`on: pull_request` events. All other workflows — those using `issue_comment`,
417+
`repository_dispatch`, `schedule`, or `workflow_dispatch` — use different
418+
event types and are **not** affected by `[skip ci]`.
419+
420+
### Important caveat
421+
422+
If a workflow is skipped via `[skip ci]`, its associated checks remain in
423+
"Pending" state. If your pull request requires those checks to pass before
424+
merging, you will need to push a new commit **without** the skip instruction
425+
to trigger the CI pipelines.
426+
427+
### When to use
428+
429+
Use `[skip ci]` for commits that are safe to skip CI:
430+
431+
- Documentation-only changes (`docs/`, `README.md`, docstrings)
432+
- Workflow configuration changes (`.github/`)
433+
- Repository metadata (`.gitignore`, `CONTRIBUTING.md`, `LICENSE`)
434+
- WIP or draft commits during local development
435+
436+
Do **not** use `[skip ci]` for commits that change:
437+
438+
- Source code in `monai/`
439+
- Test files in `tests/`
440+
- Dependencies (`requirements*.txt`, `setup.cfg`, `setup.py`)
441+
- Anything that could affect correctness or compatibility
442+
443+
### Quick example
444+
445+
```bash
446+
git commit -s -m 'fix typo in README [skip ci]'
447+
```
448+
449+
This commit will be recorded in the repository history but will not
450+
consume CI minutes.
451+
361452
## The code reviewing process
362453
363454
### Reviewing pull requests

docs/source/lazy_resampling.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ so the user must set lazy=True on the transforms that they still wish to execute
253253

254254
.. figure:: ../images/lazy_resampling_none_example.svg
255255

256-
Figure shwoing the effect of using ``lazy=False`` when ``Compose`` is being executed with ``lazy=None``. Note that
256+
Figure showing the effect of using ``lazy=False`` when ``Compose`` is being executed with ``lazy=None``. Note that
257257
the additional resamples that occur due to ``RandRotate90d`` being executed in a non-lazy fashion.
258258

259259

@@ -270,4 +270,4 @@ the following transform is a lazy transform, or is configured to execute lazily.
270270
.. figure:: ../images/lazy_resampling_apply_pending_example.svg
271271

272272
Figure showing the use of :class:`ApplyPendingd<monai.transforms.lazy.dictionary.ApplyPendingd>` to cause
273-
resampling to occur in the midele of a chain of lazy transforms.
273+
resampling to occur in the middle of a chain of lazy transforms.

docs/source/modules.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ The workflow and some of MONAI event handlers are shown as below [[Workflow exam
205205

206206
### EnsembleEvaluator
207207

208-
A typical ensemble procoess is implemented as a ready-to-use workflow [[Cross validation and model ensemble tutorial]](https://github.com/Project-MONAI/tutorials/blob/main/modules/cross_validation_models_ensemble.ipynb):
208+
A typical ensemble process is implemented as a ready-to-use workflow [[Cross validation and model ensemble tutorial]](https://github.com/Project-MONAI/tutorials/blob/main/modules/cross_validation_models_ensemble.ipynb):
209209
1. Split all the training dataset into K folds.
210210
2. Train K models with every K-1 folds data.
211211
3. Execute inference on the test data with all the K models.

docs/source/whatsnew_1_5.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
- Support numpy 2.x and Pytorch 2.6
55
- MAISI inference accelerate
6-
- Bundles storage changed to huggingface and correspoinding api updated in core
6+
- Bundles storage changed to huggingface and corresponding api updated in core
77
- Ported remaining generative tutorials and bundles
88
- New tutorials:
99
- [2d_regression/image_restoration.ipynb](https://github.com/Project-MONAI/tutorials/blob/main/2d_regression/image_restoration.ipynb)

monai/data/meta_tensor.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,8 @@ def astype(self, dtype, device=None, *_args, **_kwargs):
495495
_kwargs: additional kwargs (currently unused).
496496
497497
Returns:
498-
data array instance
498+
``MetaTensor`` when a torch dtype is given (metadata is preserved),
499+
or ``np.ndarray`` when a numpy dtype is given.
499500
"""
500501
if isinstance(dtype, str):
501502
mod_str, *dtype = dtype.split(".", 1)
@@ -506,7 +507,7 @@ def astype(self, dtype, device=None, *_args, **_kwargs):
506507

507508
out_type: type[torch.Tensor] | type[np.ndarray] | None
508509
if mod_str == "torch":
509-
out_type = torch.Tensor
510+
out_type = type(self)
510511
elif mod_str in ("numpy", "np"):
511512
out_type = np.ndarray
512513
else:

monai/handlers/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,15 @@ class mean median max 5percentile 95percentile notnans
122122

123123
# add the average value of all classes to v
124124
if class_labels is None:
125-
class_labels = ["class" + str(i) for i in range(v.shape[1])]
125+
labels = ["class" + str(i) for i in range(v.shape[1])]
126126
else:
127-
class_labels = [str(i) for i in class_labels] # ensure to have a list of str
127+
labels = [str(i) for i in class_labels] # ensure to have a list of str
128128

129-
class_labels += ["mean"]
129+
labels += ["mean"]
130130
v = np.concatenate([v, np.nanmean(v, axis=1, keepdims=True)], axis=1)
131131

132132
with open(os.path.join(save_dir, f"{k}_raw.csv"), "w") as f:
133-
f.write(f"filename{deli}{deli.join(class_labels)}\n")
133+
f.write(f"filename{deli}{deli.join(labels)}\n")
134134
for i, b in enumerate(v):
135135
f.write(
136136
f"{images[i] if images is not None else str(i)}{deli}"
@@ -164,7 +164,7 @@ def _compute_op(op: str, d: np.ndarray) -> Any:
164164
with open(os.path.join(save_dir, f"{k}_summary.csv"), "w") as f:
165165
f.write(f"class{deli}{deli.join(ops)}\n")
166166
for i, c in enumerate(np.transpose(v)):
167-
f.write(f"{class_labels[i]}{deli}{deli.join([f'{_compute_op(k, c):.4f}' for k in ops])}\n")
167+
f.write(f"{labels[i]}{deli}{deli.join([f'{_compute_op(k, c):.4f}' for k in ops])}\n")
168168

169169

170170
def from_engine(keys: KeysCollection, first: bool = False) -> Callable:

monai/transforms/utils.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,15 +1224,15 @@ def get_largest_connected_component_mask(
12241224
if num_features <= num_components:
12251225
out = img_.astype(bool)
12261226
else:
1227-
# ignore background
1228-
nonzeros = features[lib.nonzero(features)]
1229-
# get number voxels per feature (bincount). argsort[::-1] to get indices
1230-
# of largest components.
1231-
features_to_keep = lib.argsort(lib.bincount(nonzeros))[::-1]
1232-
# only keep the first n non-background indices
1233-
features_to_keep = features_to_keep[:num_components]
1234-
# generate labelfield. True if in list of features to keep
1235-
out = lib.isin(features, features_to_keep)
1227+
# bincount counts every label; index 0 is background, so drop it before ranking
1228+
counts = lib.bincount(features.reshape(-1))
1229+
counts[0] = 0
1230+
# argsort[::-1] gives labels of the largest components; keep the first n
1231+
features_to_keep = lib.argsort(counts)[::-1][:num_components]
1232+
# boolean lookup-table gather over the label field, cheaper than isin
1233+
keep = lib.zeros(counts.shape[0], dtype=bool)
1234+
keep[features_to_keep] = True
1235+
out = keep[features]
12361236

12371237
return convert_to_dst_type(out, dst=img, dtype=out.dtype)[0]
12381238

tests/data/meta_tensor/test_meta_tensor.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,12 @@ def test_astype(self):
435435
for np_types in ("float32", "np.float32", "numpy.float32", np.float32, float, "int", np.uint16):
436436
self.assertIsInstance(t.astype(np_types), np.ndarray)
437437
for pt_types in ("torch.float", torch.float, "torch.float64"):
438-
self.assertIsInstance(t.astype(pt_types), torch.Tensor)
439-
self.assertIsInstance(t.astype("torch.float", device="cpu"), torch.Tensor)
438+
result = t.astype(pt_types)
439+
self.assertIsInstance(result, MetaTensor)
440+
self.assertEqual(result.meta.get("fname"), "filename")
441+
result = t.astype("torch.float", device="cpu")
442+
self.assertIsInstance(result, MetaTensor)
443+
self.assertEqual(result.meta.get("fname"), "filename")
440444

441445
def test_transforms(self):
442446
key = "im"

tests/handlers/test_write_metrics_reports.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ def test_content(self):
6363
self.assertTrue(os.path.exists(os.path.join(tempdir, "metric4_raw.csv")))
6464
self.assertTrue(os.path.exists(os.path.join(tempdir, "metric4_summary.csv")))
6565

66+
def test_multi_metric_details_headers(self):
67+
with tempfile.TemporaryDirectory() as tempdir:
68+
write_metrics_reports(
69+
save_dir=Path(tempdir),
70+
images=["img1", "img2"],
71+
metrics=None,
72+
metric_details={
73+
"m1": torch.tensor([[1, 2, 3], [4, 5, 6]]),
74+
"m2": torch.tensor([[7, 8], [9, 10]]),
75+
"m3": torch.tensor([[11, 12, 13, 14], [15, 16, 17, 18]]),
76+
},
77+
summary_ops=None,
78+
deli=",",
79+
output_type="csv",
80+
)
81+
for name, nclass in [("m1", 3), ("m2", 2), ("m3", 4)]:
82+
path = os.path.join(tempdir, f"{name}_raw.csv")
83+
self.assertTrue(os.path.exists(path))
84+
with open(path) as f:
85+
header = f.readline().strip().split(",")
86+
self.assertEqual(header, ["filename"] + [f"class{i}" for i in range(nclass)] + ["mean"])
87+
6688

6789
if __name__ == "__main__":
6890
unittest.main()

0 commit comments

Comments
 (0)