Skip to content

Commit 9944127

Browse files
authored
Merge branch 'dev' into fix-6840
2 parents 13feb1d + e72145c commit 9944127

File tree

18 files changed

+336
-28
lines changed

18 files changed

+336
-28
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242

4343
# Initializes the CodeQL tools for scanning.
4444
- name: Initialize CodeQL
45-
uses: github/codeql-action/init@v3
45+
uses: github/codeql-action/init@v4
4646
with:
4747
languages: ${{ matrix.language }}
4848
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -72,4 +72,4 @@ jobs:
7272
BUILD_MONAI=1 ./runtests.sh --build
7373
7474
- name: Perform CodeQL Analysis
75-
uses: github/codeql-action/analyze@v3
75+
uses: github/codeql-action/analyze@v4

.github/workflows/docker.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
python setup.py build
3838
cat build/lib/monai/_version.py
3939
- name: Upload version
40-
uses: actions/upload-artifact@v4
40+
uses: actions/upload-artifact@v5
4141
with:
4242
name: _version.py
4343
path: build/lib/monai/_version.py
@@ -57,7 +57,7 @@ jobs:
5757
with:
5858
ref: dev
5959
- name: Download version
60-
uses: actions/download-artifact@v5
60+
uses: actions/download-artifact@v6
6161
with:
6262
name: _version.py
6363
- name: docker_build

.github/workflows/integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
run: ./runtests.sh --build --net
7575

7676
- name: Add reaction
77-
uses: peter-evans/create-or-update-comment@v4
77+
uses: peter-evans/create-or-update-comment@v5
7878
with:
7979
token: ${{ secrets.PR_MAINTAIN }}
8080
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}
@@ -154,7 +154,7 @@ jobs:
154154
python -m tests.test_integration_gpu_customization
155155
156156
- name: Add reaction
157-
uses: peter-evans/create-or-update-comment@v4
157+
uses: peter-evans/create-or-update-comment@v5
158158
with:
159159
token: ${{ secrets.PR_MAINTAIN }}
160160
repository: ${{ github.event.client_payload.github.payload.repository.full_name }}

.github/workflows/pythonapp-min.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ jobs:
8888
run: |
8989
which python
9090
python -m pip install --user --upgrade pip setuptools wheel
91+
python -m pip install --user more-itertools>=8.0
9192
- name: cache weekly timestamp
9293
id: pip-cache
9394
run: |
@@ -136,6 +137,7 @@ jobs:
136137
run: |
137138
which python
138139
python -m pip install --user --upgrade pip setuptools wheel
140+
python -m pip install --user more-itertools>=8.0
139141
- name: cache weekly timestamp
140142
id: pip-cache
141143
run: |

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666

6767
- if: matrix.python-version == '3.9' && startsWith(github.ref, 'refs/tags/')
6868
name: Upload artifacts
69-
uses: actions/upload-artifact@v4
69+
uses: actions/upload-artifact@v5
7070
with:
7171
name: dist
7272
path: dist/
@@ -109,7 +109,7 @@ jobs:
109109
python setup.py build
110110
cat build/lib/monai/_version.py
111111
- name: Upload version
112-
uses: actions/upload-artifact@v4
112+
uses: actions/upload-artifact@v5
113113
with:
114114
name: _version.py
115115
path: build/lib/monai/_version.py
@@ -127,7 +127,7 @@ jobs:
127127
steps:
128128
- uses: actions/checkout@v5
129129
- name: Download version
130-
uses: actions/download-artifact@v5
130+
uses: actions/download-artifact@v6
131131
with:
132132
name: _version.py
133133
- name: Set tag

docs/source/transforms.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ Generic Interfaces
3737
.. autoclass:: MultiSampleTrait
3838
:members:
3939

40+
`ReduceTrait`
41+
^^^^^^^^^^^^^^^^^^
42+
.. autoclass:: ReduceTrait
43+
:members:
44+
4045
`Randomizable`
4146
^^^^^^^^^^^^^^
4247
.. autoclass:: Randomizable
@@ -1252,6 +1257,12 @@ Utility
12521257
:members:
12531258
:special-members: __call__
12541259

1260+
`FlattenSequence`
1261+
""""""""""""""""""""""""
1262+
.. autoclass:: FlattenSequence
1263+
:members:
1264+
:special-members: __call__
1265+
12551266
Dictionary Transforms
12561267
---------------------
12571268

@@ -2337,6 +2348,12 @@ Utility (Dict)
23372348
:members:
23382349
:special-members: __call__
23392350

2351+
`FlattenSequenced`
2352+
"""""""""""""""""""""""""
2353+
.. autoclass:: FlattenSequenced
2354+
:members:
2355+
:special-members: __call__
2356+
23402357

23412358
MetaTensor
23422359
^^^^^^^^^^

monai/data/box_utils.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,10 @@ def box_iou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTensor
826826
boxes2: bounding boxes, Mx4 or Mx6 torch tensor or ndarray. The box mode is assumed to be ``StandardMode``
827827
828828
Returns:
829-
IoU, with size of (N,M) and same data type as ``boxes1``
829+
An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
830+
floating-point with size ``(N, M)``:
831+
- if ``boxes1`` has a floating-point dtype, the same dtype is used.
832+
- if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
830833
831834
"""
832835

@@ -842,16 +845,18 @@ def box_iou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTensor
842845

843846
inter, union = _box_inter_union(boxes1_t, boxes2_t, compute_dtype=COMPUTE_DTYPE)
844847

845-
# compute IoU and convert back to original box_dtype
848+
# compute IoU and convert back to original box_dtype or torch.float32
846849
iou_t = inter / (union + torch.finfo(COMPUTE_DTYPE).eps) # (N,M)
850+
if not box_dtype.is_floating_point:
851+
box_dtype = COMPUTE_DTYPE
847852
iou_t = iou_t.to(dtype=box_dtype)
848853

849854
# check if NaN or Inf
850855
if torch.isnan(iou_t).any() or torch.isinf(iou_t).any():
851856
raise ValueError("Box IoU is NaN or Inf.")
852857

853858
# convert tensor back to numpy if needed
854-
iou, *_ = convert_to_dst_type(src=iou_t, dst=boxes1)
859+
iou, *_ = convert_to_dst_type(src=iou_t, dst=boxes1, dtype=box_dtype)
855860
return iou
856861

857862

@@ -867,7 +872,10 @@ def box_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTenso
867872
boxes2: bounding boxes, Mx4 or Mx6 torch tensor or ndarray. The box mode is assumed to be ``StandardMode``
868873
869874
Returns:
870-
GIoU, with size of (N,M) and same data type as ``boxes1``
875+
An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
876+
floating-point with size ``(N, M)``:
877+
- if ``boxes1`` has a floating-point dtype, the same dtype is used.
878+
- if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
871879
872880
Reference:
873881
https://giou.stanford.edu/GIoU.pdf
@@ -904,12 +912,15 @@ def box_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOrTenso
904912

905913
# GIoU
906914
giou_t = iou - (enclosure - union) / (enclosure + torch.finfo(COMPUTE_DTYPE).eps)
915+
if not box_dtype.is_floating_point:
916+
box_dtype = COMPUTE_DTYPE
907917
giou_t = giou_t.to(dtype=box_dtype)
918+
908919
if torch.isnan(giou_t).any() or torch.isinf(giou_t).any():
909920
raise ValueError("Box GIoU is NaN or Inf.")
910921

911922
# convert tensor back to numpy if needed
912-
giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1)
923+
giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1, dtype=box_dtype)
913924
return giou
914925

915926

@@ -925,7 +936,10 @@ def box_pair_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOr
925936
boxes2: bounding boxes, same shape with boxes1. The box mode is assumed to be ``StandardMode``
926937
927938
Returns:
928-
paired GIoU, with size of (N,) and same data type as ``boxes1``
939+
An array/tensor matching the container type of ``boxes1`` (NumPy ndarray or Torch tensor), always
940+
floating-point with size ``(N, )``:
941+
- if ``boxes1`` has a floating-point dtype, the same dtype is used.
942+
- if ``boxes1`` has an integer dtype, the result is returned as ``torch.float32``.
929943
930944
Reference:
931945
https://giou.stanford.edu/GIoU.pdf
@@ -982,12 +996,15 @@ def box_pair_giou(boxes1: NdarrayOrTensor, boxes2: NdarrayOrTensor) -> NdarrayOr
982996
enclosure = torch.prod(wh, dim=-1, keepdim=False) # (N,)
983997

984998
giou_t: torch.Tensor = iou - (enclosure - union) / (enclosure + torch.finfo(COMPUTE_DTYPE).eps) # type: ignore
999+
if not box_dtype.is_floating_point:
1000+
box_dtype = COMPUTE_DTYPE
9851001
giou_t = giou_t.to(dtype=box_dtype) # (N,spatial_dims)
1002+
9861003
if torch.isnan(giou_t).any() or torch.isinf(giou_t).any():
9871004
raise ValueError("Box GIoU is NaN or Inf.")
9881005

9891006
# convert tensor back to numpy if needed
990-
giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1)
1007+
giou, *_ = convert_to_dst_type(src=giou_t, dst=boxes1, dtype=box_dtype)
9911008
return giou
9921009

9931010

monai/networks/blocks/patchembedding.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@
1212
from __future__ import annotations
1313

1414
from collections.abc import Sequence
15+
from typing import Optional
1516

1617
import numpy as np
1718
import torch
1819
import torch.nn as nn
1920
import torch.nn.functional as F
2021
from torch.nn import LayerNorm
2122

22-
from monai.networks.blocks.pos_embed_utils import build_sincos_position_embedding
23+
from monai.networks.blocks.pos_embed_utils import build_fourier_position_embedding, build_sincos_position_embedding
2324
from monai.networks.layers import Conv, trunc_normal_
2425
from monai.utils import ensure_tuple_rep, optional_import
2526
from monai.utils.module import look_up_option
2627

2728
Rearrange, _ = optional_import("einops.layers.torch", name="Rearrange")
2829
SUPPORTED_PATCH_EMBEDDING_TYPES = {"conv", "perceptron"}
29-
SUPPORTED_POS_EMBEDDING_TYPES = {"none", "learnable", "sincos"}
30+
SUPPORTED_POS_EMBEDDING_TYPES = {"none", "learnable", "sincos", "fourier"}
3031

3132

3233
class PatchEmbeddingBlock(nn.Module):
@@ -53,6 +54,7 @@ def __init__(
5354
pos_embed_type: str = "learnable",
5455
dropout_rate: float = 0.0,
5556
spatial_dims: int = 3,
57+
pos_embed_kwargs: Optional[dict] = None,
5658
) -> None:
5759
"""
5860
Args:
@@ -65,6 +67,8 @@ def __init__(
6567
pos_embed_type: position embedding layer type.
6668
dropout_rate: fraction of the input units to drop.
6769
spatial_dims: number of spatial dimensions.
70+
pos_embed_kwargs: additional arguments for position embedding. For `sincos`, it can contain
71+
`temperature` and for fourier it can contain `scales`.
6872
"""
6973

7074
super().__init__()
@@ -105,6 +109,8 @@ def __init__(
105109
self.position_embeddings = nn.Parameter(torch.zeros(1, self.n_patches, hidden_size))
106110
self.dropout = nn.Dropout(dropout_rate)
107111

112+
pos_embed_kwargs = {} if pos_embed_kwargs is None else pos_embed_kwargs
113+
108114
if self.pos_embed_type == "none":
109115
pass
110116
elif self.pos_embed_type == "learnable":
@@ -114,7 +120,17 @@ def __init__(
114120
for in_size, pa_size in zip(img_size, patch_size):
115121
grid_size.append(in_size // pa_size)
116122

117-
self.position_embeddings = build_sincos_position_embedding(grid_size, hidden_size, spatial_dims)
123+
self.position_embeddings = build_sincos_position_embedding(
124+
grid_size, hidden_size, spatial_dims, **pos_embed_kwargs
125+
)
126+
elif self.pos_embed_type == "fourier":
127+
grid_size = []
128+
for in_size, pa_size in zip(img_size, patch_size):
129+
grid_size.append(in_size // pa_size)
130+
131+
self.position_embeddings = build_fourier_position_embedding(
132+
grid_size, hidden_size, spatial_dims, **pos_embed_kwargs
133+
)
118134
else:
119135
raise ValueError(f"pos_embed_type {self.pos_embed_type} not supported.")
120136

monai/networks/blocks/pos_embed_utils.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import torch
1919
import torch.nn as nn
2020

21-
__all__ = ["build_sincos_position_embedding"]
21+
__all__ = ["build_fourier_position_embedding", "build_sincos_position_embedding"]
2222

2323

2424
# From PyTorch internals
@@ -32,6 +32,59 @@ def parse(x):
3232
return parse
3333

3434

35+
def build_fourier_position_embedding(
36+
grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, scales: Union[float, List[float]] = 1.0
37+
) -> torch.nn.Parameter:
38+
"""
39+
Builds a (Anistropic) Fourier feature position embedding based on the given grid size, embed dimension,
40+
spatial dimensions, and scales. The scales control the variance of the Fourier features, higher values make distant
41+
points more distinguishable.
42+
Position embedding is made anistropic by allowing setting different scales for each spatial dimension.
43+
Reference: https://arxiv.org/abs/2509.02488
44+
45+
Args:
46+
grid_size (int | List[int]): The size of the grid in each spatial dimension.
47+
embed_dim (int): The dimension of the embedding.
48+
spatial_dims (int): The number of spatial dimensions (2 for 2D, 3 for 3D).
49+
scales (float | List[float]): The scale for every spatial dimension. If a single float is provided,
50+
the same scale is used for all dimensions.
51+
52+
Returns:
53+
pos_embed (nn.Parameter): The Fourier feature position embedding as a fixed parameter.
54+
"""
55+
56+
to_tuple = _ntuple(spatial_dims)
57+
grid_size_t = to_tuple(grid_size)
58+
if len(grid_size_t) != spatial_dims:
59+
raise ValueError(f"Length of grid_size ({len(grid_size_t)}) must be the same as spatial_dims.")
60+
61+
if embed_dim % 2 != 0:
62+
raise ValueError("embed_dim must be even for Fourier position embedding")
63+
64+
# Ensure scales is a tensor of shape (spatial_dims,)
65+
if isinstance(scales, float):
66+
scales_tensor = torch.full((spatial_dims,), scales, dtype=torch.float)
67+
elif isinstance(scales, (list, tuple)):
68+
if len(scales) != spatial_dims:
69+
raise ValueError(f"Length of scales {len(scales)} does not match spatial_dims {spatial_dims}")
70+
scales_tensor = torch.tensor(scales, dtype=torch.float)
71+
else:
72+
raise TypeError(f"scales must be float or list of floats, got {type(scales)}")
73+
74+
gaussians = torch.randn(embed_dim // 2, spatial_dims, dtype=torch.float32) * scales_tensor
75+
76+
position_indices = [torch.linspace(0, 1, x, dtype=torch.float32) for x in grid_size_t]
77+
positions = torch.stack(torch.meshgrid(*position_indices, indexing="ij"), dim=-1)
78+
positions = positions.flatten(end_dim=-2)
79+
80+
x_proj = (2.0 * torch.pi * positions) @ gaussians.T
81+
82+
pos_emb = torch.cat([torch.sin(x_proj), torch.cos(x_proj)], dim=-1)
83+
pos_emb = nn.Parameter(pos_emb[None, :, :], requires_grad=False)
84+
85+
return pos_emb
86+
87+
3588
def build_sincos_position_embedding(
3689
grid_size: Union[int, List[int]], embed_dim: int, spatial_dims: int = 3, temperature: float = 10000.0
3790
) -> torch.nn.Parameter:

monai/transforms/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@
506506
ZoomDict,
507507
)
508508
from .spatial.functional import spatial_resample
509-
from .traits import LazyTrait, MultiSampleTrait, RandomizableTrait, ThreadUnsafe
509+
from .traits import LazyTrait, MultiSampleTrait, RandomizableTrait, ReduceTrait, ThreadUnsafe
510510
from .transform import LazyTransform, MapTransform, Randomizable, RandomizableTransform, Transform, apply_transform
511511
from .utility.array import (
512512
AddCoordinateChannels,
@@ -521,6 +521,7 @@
521521
EnsureChannelFirst,
522522
EnsureType,
523523
FgBgToIndices,
524+
FlattenSequence,
524525
Identity,
525526
ImageFilter,
526527
IntensityStats,
@@ -593,6 +594,9 @@
593594
FgBgToIndicesd,
594595
FgBgToIndicesD,
595596
FgBgToIndicesDict,
597+
FlattenSequenced,
598+
FlattenSequenceD,
599+
FlattenSequenceDict,
596600
FlattenSubKeysd,
597601
FlattenSubKeysD,
598602
FlattenSubKeysDict,

0 commit comments

Comments
 (0)