Source code for physicsnemo.diffusion.multi_diffusion.patching

# SPDX-FileCopyrightText: Copyright (c) 2023 - 2026 NVIDIA CORPORATION & AFFILIATES.
# SPDX-FileCopyrightText: All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.

"""Utilities for multi-diffusion (patching and fusion)."""

import math
import warnings
from abc import ABC, abstractmethod
from typing import Optional, Tuple, Union

import torch
from einops import rearrange
from jaxtyping import Float, Int
from torch import Tensor


[docs] class BasePatching2D(torch.nn.Module, ABC): r"""Abstract base class for 2D image patching operations. Provides the common interface and validation logic for patching strategies that decompose a batch of 2D images into smaller spatial tiles (patches). Concrete subclasses must implement :meth:`forward` to define the actual patching logic. Parameters ---------- img_shape : Tuple[int, int] Height and width of the full input images :math:`(H, W)`. patch_shape : Tuple[int, int] Height and width of the patches to extract :math:`(H_p, W_p)`. """ def __init__( self, img_shape: Tuple[int, int], patch_shape: Tuple[int, int] ) -> None: super().__init__() if len(img_shape) != 2: raise ValueError(f"img_shape must be 2D, got {len(img_shape)}D") if len(patch_shape) != 2: raise ValueError(f"patch_shape must be 2D, got {len(patch_shape)}D") if any(p > i for p, i in zip(patch_shape, img_shape)): warnings.warn( f"Patch shape {patch_shape} is larger than " f"image shape {img_shape}. " f"Patches will be cropped to fit within the image." ) self.img_shape = img_shape self.patch_shape = tuple(min(p, i) for p, i in zip(patch_shape, img_shape)) def forward( self, input: Float[Tensor, "B C H W"], **kwargs ) -> Float[Tensor, "P_times_B C Hp Wp"]: r"""Forward pass. Delegates to :meth:`apply` by default.""" return self.apply(input, **kwargs)
[docs] @abstractmethod def apply( self, input: Float[Tensor, "B C H W"], *args, **kwargs, ) -> Float[Tensor, "P_times_B C Hp Wp"]: r"""Apply the patching operation to a batch of full images. Subclasses **must** override this method. Parameters ---------- input : Tensor Batch of full images of shape :math:`(B, C, H, W)`. *args : tuple Additional positional arguments. **kwargs : dict Additional keyword arguments. Returns ------- Tensor Patched tensor of shape :math:`(P \times B, C, H_p, W_p)`. """ pass
[docs] def fuse( self, input: Float[Tensor, "P_times_B C Hp Wp"], **kwargs ) -> Float[Tensor, "B C H W"]: r"""Fuse patches back into a complete image. Parameters ---------- input : Tensor Patched tensor. Shape depends on the subclass. **kwargs : dict Additional keyword arguments specific to the subclass. Returns ------- Tensor Fused image tensor. Raises ------ NotImplementedError If the subclass does not implement this method. """ raise NotImplementedError("'fuse' method must be implemented in subclasses.")
[docs] def global_index( self, batch_size: int = 1, device: Union[torch.device, str] = "cpu" ) -> Int[Tensor, "P 2 Hp Wp"]: r"""Return the global :math:`(y, x)` grid coordinates for each patch. Returns a **new tensor** (clone) each time, so the caller owns the result and it will not be mutated by subsequent calls to :meth:`~RandomPatching2D.reset_patch_indices`. For zero-copy access to the underlying buffer, use ``self._global_index`` directly. Parameters ---------- batch_size : int, default=1 Kept for backward compatibility. Ignored. device : Union[torch.device, str], default="cpu" Kept for backward compatibility. The buffer follows the module device (use ``.to(device)`` to move the module). Returns ------- Tensor Integer tensor of shape :math:`(P, 2, H_p, W_p)`. Channel 0 holds y-coordinates, channel 1 holds x-coordinates. """ if hasattr(self, "_global_index") and self._global_index is not None: return self._global_index.clone() return self._compute_global_index()
def _compute_global_index(self) -> Int[Tensor, "P 2 Hp Wp"]: r"""Compute the global-index tensor from current patch positions. The ``global_index`` tensor is created and computed on the same device as ``patch_indices`` (if the buffer exists) to avoid cross-device indexing errors. Returns ------- Tensor Integer tensor of shape :math:`(P, 2, H_p, W_p)`. """ device = None if hasattr(self, "patch_indices") and isinstance(self.patch_indices, Tensor): device = self.patch_indices.device Ny = torch.arange(self.img_shape[0], device=device).int() Nx = torch.arange(self.img_shape[1], device=device).int() grid = torch.stack(torch.meshgrid(Ny, Nx, indexing="ij"), dim=0).unsqueeze( 0 ) # (1, 2, H, W) return self(grid).long()
[docs] class RandomPatching2D(BasePatching2D): r"""Randomly extract patches from 2D images. Maintains a set of :math:`P` random upper-left corner positions and extracts the corresponding patches from every batch element. Positions are drawn at construction time and can be refreshed by calling :meth:`reset_patch_indices`. Parameters ---------- img_shape : Tuple[int, int] Height and width :math:`(H, W)` of the full input images. patch_shape : Tuple[int, int] Height and width :math:`(H_p, W_p)` of the patches to extract. patch_num : int Number of patches :math:`P` to extract per image. Attributes ---------- patch_indices : Tensor Buffer of shape :math:`(P, 2)` with the :math:`(y, x)` upper-left corner of each patch. See Also -------- :class:`~physicsnemo.diffusion.multi_diffusion.GridPatching2D` : Deterministic grid-based patching strategy. Examples -------- Extract patches, re-draw random positions, then extract again: >>> import torch >>> from physicsnemo.diffusion.multi_diffusion import RandomPatching2D >>> rp = RandomPatching2D(img_shape=(16, 16), patch_shape=(8, 8), patch_num=6) >>> x = torch.randn(2, 3, 16, 16) >>> # First extraction: 6 patches per sample, batch of 2 >>> rp.apply(x).shape torch.Size([12, 3, 8, 8]) >>> # Re-draw random positions (e.g. between training steps) >>> rp.reset_patch_indices() >>> rp.apply(x).shape torch.Size([12, 3, 8, 8]) Retrieve the global (y, x) coordinates for each patch: >>> gi = rp.global_index() >>> gi.shape # (P, 2, Hp, Wp) — channel 0 = y, channel 1 = x torch.Size([6, 2, 8, 8]) """ def __init__( self, img_shape: Tuple[int, int], patch_shape: Tuple[int, int], patch_num: int ) -> None: super().__init__(img_shape, patch_shape) self._patch_num = patch_num self.reset_patch_indices() @property def patch_num(self) -> int: r"""Number of patches :math:`P` to extract.""" return self._patch_num
[docs] def set_patch_num(self, value: int) -> None: r"""Set the number of patches and re-draw positions. Parameters ---------- value : int New number of patches :math:`P`. """ self._patch_num = value self.reset_patch_indices()
[docs] def reset_patch_indices( self, *, generator: torch.Generator | None = None, ) -> None: r"""Re-draw random upper-left corner positions for all patches. The cached ``_global_index`` buffer is invalidated and will be lazily recomputed on the next call to :meth:`global_index`. Parameters ---------- generator : torch.Generator, optional Pseudo-random number generator for reproducible sampling. """ has_buffer = hasattr(self, "patch_indices") and isinstance( self.patch_indices, Tensor ) device = self.patch_indices.device if has_buffer else None max_y = self.img_shape[0] - self.patch_shape[0] max_x = self.img_shape[1] - self.patch_shape[1] py = torch.randint( 0, max_y + 1, (self.patch_num,), dtype=torch.long, device=device, generator=generator, ) px = torch.randint( 0, max_x + 1, (self.patch_num,), dtype=torch.long, device=device, generator=generator, ) new_indices = torch.stack([py, px], dim=1) if has_buffer and new_indices.shape == self.patch_indices.shape: self.patch_indices.copy_(new_indices) else: self.register_buffer("patch_indices", new_indices, persistent=False) self._global_index_needs_update = True
[docs] def global_index( self, batch_size: int = 1, device: Union[torch.device, str] = "cpu" ) -> Int[Tensor, "P 2 Hp Wp"]: r"""Return global :math:`(y, x)` grid coordinates for each patch. Recomputes lazily if patch positions have changed since the last call. Parameters ---------- batch_size : int, default=1 Kept for backward compatibility. Ignored. device : Union[torch.device, str], default="cpu" Kept for backward compatibility. The buffer follows the module device (use ``.to(device)`` to move the module). Returns ------- Tensor Integer tensor of shape :math:`(P, 2, H_p, W_p)`. """ if getattr(self, "_global_index_needs_update", True): new_global_index = self._compute_global_index() if ( hasattr(self, "_global_index") and isinstance(self._global_index, Tensor) and new_global_index.shape == self._global_index.shape ): self._global_index.copy_(new_global_index) else: self.register_buffer( "_global_index", new_global_index, persistent=False ) self._global_index_needs_update = False return self._global_index.clone()
def forward( self, input: Float[Tensor, "B C H W"], additional_input: Optional[Float[Tensor, "B C_add H_add W_add"]] = None, ) -> Float[Tensor, "P_times_B C_out Hp Wp"]: r"""Extract random patches from the input tensor.""" B, C, H, W = input.shape Hp, Wp = self.patch_shape P = self.patch_num K = Hp * Wp patch_indices = self.patch_indices.to(input.device) py = patch_indices[:, 0] # (P,) px = patch_indices[:, 1] # (P,) dy = torch.arange(Hp, device=input.device) dx = torch.arange(Wp, device=input.device) base = (py * W + px).reshape(P, 1, 1) # (P, 1, 1) rel = (dy[:, None] * W + dx[None, :]).reshape(1, 1, K) # (1, 1, K) idx = (base + rel).expand(P, B, K) # (P, B, Hp*Wp) x_flat = input.reshape(B, C, H * W) # (B, C, HW) gathered = torch.gather( x_flat.unsqueeze(0).expand(P, B, C, H * W), dim=3, index=idx.unsqueeze(2).expand(P, B, C, K), ) # (P, B, C, Hp*Wp) out = gathered.reshape(P * B, C, Hp, Wp) if input.is_contiguous(memory_format=torch.channels_last): out = out.to(memory_format=torch.channels_last) if additional_input is not None: add_input_interp = torch.nn.functional.interpolate( input=additional_input, size=self.patch_shape, mode="bilinear" ) # (B, C_add, Hp, Wp) out = torch.cat( (out, add_input_interp.repeat(P, 1, 1, 1)), dim=1 ) # (P*B, C+C_add, Hp, Wp) return out
[docs] def apply( self, input: Float[Tensor, "B C H W"], additional_input: Optional[Float[Tensor, "B C_add H_add W_add"]] = None, ) -> Float[Tensor, "P_times_B C_out Hp Wp"]: r"""Extract random patches. Parameters ---------- input : Tensor Full images of shape :math:`(B, C, H, W)`. additional_input : Tensor, optional Interpolated to :math:`(H_p, W_p)` and concatenated channel-wise to each patch. Should have shape :math:`(B, C_add, H_add, W_add)`. Returns ------- Tensor Shape :math:`(P \times B, C [+ C_{add}], H_p, W_p)`. """ if isinstance(input, Tensor): return self(input, additional_input=additional_input) return super().apply(input)
[docs] class GridPatching2D(BasePatching2D): r"""Deterministically extract patches from 2D images in a grid pattern. Tiles the image with a regular grid of :math:`P = P_y \times P_x` patches, with configurable overlap and boundary padding. Supports reconstructing the full image from patches via :meth:`fuse`. Parameters ---------- img_shape : Tuple[int, int] Height and width of the full input images :math:`(H, W)`. patch_shape : Tuple[int, int] Height and width of the patches to extract :math:`(H_p, W_p)`. overlap_pix : int, optional, default=0 Number of overlapping pixels between adjacent patches. boundary_pix : int, optional, default=0 Number of boundary pixels to pad on each side. Attributes ---------- patch_num : int Total number of patches :math:`P = P_y \times P_x`. See Also -------- :class:`~physicsnemo.diffusion.multi_diffusion.RandomPatching2D` : Random patching strategy. Examples -------- Patch an image and fuse it back (roundtrip): >>> import torch >>> from physicsnemo.diffusion.multi_diffusion import GridPatching2D >>> gp = GridPatching2D(img_shape=(16, 16), patch_shape=(8, 8)) >>> x = torch.randn(2, 3, 16, 16) >>> patches = gp.apply(x) >>> patches.shape # (P*B, C, Hp, Wp) torch.Size([8, 3, 8, 8]) >>> # Fuse is the inverse of apply >>> reconstructed = gp.fuse(patches, batch_size=2) >>> reconstructed.shape torch.Size([2, 3, 16, 16]) >>> torch.allclose(x, reconstructed) True Retrieve the global (y, x) coordinates for each patch: >>> gi = gp.global_index() >>> gi.shape # (P, 2, Hp, Wp) torch.Size([4, 2, 8, 8]) """ def __init__( self, img_shape: Tuple[int, int], patch_shape: Tuple[int, int], overlap_pix: int = 0, boundary_pix: int = 0, ): super().__init__(img_shape, patch_shape) self.overlap_pix = overlap_pix self.boundary_pix = boundary_pix patch_num_x = math.ceil( img_shape[1] / (patch_shape[1] - overlap_pix - boundary_pix) ) patch_num_y = math.ceil( img_shape[0] / (patch_shape[0] - overlap_pix - boundary_pix) ) self.patch_num = patch_num_x * patch_num_y self.register_buffer( "_overlap_count", self.get_overlap_count( self.patch_shape, self.img_shape, self.overlap_pix, self.boundary_pix ), persistent=False, ) self.register_buffer( "_global_index", self._compute_global_index(), persistent=False ) def forward( self, input: Float[Tensor, "B C H W"], additional_input: Optional[Float[Tensor, "B C_add H_add W_add"]] = None, ) -> Float[Tensor, "P_times_B C_out Hp Wp"]: r"""Extract grid patches from the input tensor.""" if additional_input is not None: add_input_interp = torch.nn.functional.interpolate( input=additional_input, size=self.patch_shape, mode="bilinear" ) else: add_input_interp = None return image_batching( input=input, patch_shape_y=self.patch_shape[0], patch_shape_x=self.patch_shape[1], overlap_pix=self.overlap_pix, boundary_pix=self.boundary_pix, input_interp=add_input_interp, )
[docs] def apply( self, input: Float[Tensor, "B C H W"], additional_input: Optional[Float[Tensor, "B C_add H_add W_add"]] = None, ) -> Float[Tensor, "P_times_B C_out Hp Wp"]: r"""Apply deterministic grid patching. Splits the input tensor into patches in a grid-like pattern. Extracted patches are batched along the first dimension. For any patch index ``i``, ``out[B * i : B * (i + 1)]`` corresponds to the *same patch* extracted from every batch element. Parameters ---------- input : Tensor Full images of shape :math:`(B, C, H, W)`. additional_input : Tensor, optional, default=None Additional data of shape :math:`(B, C_{add}, H', W')`. Interpolated to :math:`(H_p, W_p)` and channel-wise concatenated to each patch. Not decomposed. Returns ------- Tensor Shape :math:`(P \times B, C [+ C_{add}], H_p, W_p)`. See Also -------- :func:`~physicsnemo.diffusion.multi_diffusion.image_batching` : Low-level function used internally. """ if isinstance(input, Tensor): return self(input, additional_input=additional_input) return super().apply(input)
[docs] def fuse( self, input: Float[Tensor, "P_times_B C Hp Wp"], batch_size: int, ) -> Float[Tensor, "B C H W"]: r"""Fuse patches back into a complete image. Reconstructs the original image by stitching together patches. Overlapping regions are averaged with a uniform weight. Parameters ---------- input : Tensor Patches of shape :math:`(P \times B, C, H_p, W_p)`, with the same batch layout as returned by :meth:`apply`. batch_size : int Original batch size :math:`B` before patching. Returns ------- Tensor Reconstructed image of shape :math:`(B, C, H, W)`. See Also -------- :func:`~physicsnemo.diffusion.multi_diffusion.image_fuse` : Low-level function used internally. """ return image_fuse( input=input, img_shape_y=self.img_shape[0], img_shape_x=self.img_shape[1], batch_size=batch_size, overlap_pix=self.overlap_pix, boundary_pix=self.boundary_pix, overlap_count=self._overlap_count, )
[docs] @staticmethod def get_overlap_count( patch_shape: tuple[int, int], img_shape: tuple[int, int], overlap_pix: int, boundary_pix: int, ) -> Float[Tensor, "1 1 Hpad Wpad"]: r"""Compute per-pixel overlap count for patch reconstruction. Calculates how many patches cover each pixel in the padded image. Used to normalise the reconstructed image after folding. Parameters ---------- patch_shape : tuple[int, int] Patch dimensions :math:`(H_p, W_p)`. img_shape : tuple[int, int] Full image dimensions :math:`(H, W)`. overlap_pix : int Overlap between adjacent patches in pixels. boundary_pix : int Boundary padding in pixels. Returns ------- Tensor Overlap count of shape :math:`(1, 1, H_{pad}, W_{pad})`. """ patch_shape_y, patch_shape_x = patch_shape img_shape_y, img_shape_x = img_shape patch_num_x = math.ceil( img_shape_x / (patch_shape_x - overlap_pix - boundary_pix) ) patch_num_y = math.ceil( img_shape_y / (patch_shape_y - overlap_pix - boundary_pix) ) padded_shape_x = ( (patch_shape_x - overlap_pix - boundary_pix) * (patch_num_x - 1) + patch_shape_x + boundary_pix ) padded_shape_y = ( (patch_shape_y - overlap_pix - boundary_pix) * (patch_num_y - 1) + patch_shape_y + boundary_pix ) stride = ( patch_shape_y - overlap_pix - boundary_pix, patch_shape_x - overlap_pix - boundary_pix, ) kernel = (patch_shape_y, patch_shape_x) ones = torch.ones(1, 1, padded_shape_y, padded_shape_x) overlap_count = torch.nn.functional.fold( input=torch.nn.functional.unfold( input=ones, kernel_size=kernel, stride=stride ), output_size=(padded_shape_y, padded_shape_x), kernel_size=kernel, stride=stride, ) return overlap_count
# --------------------------------------------------------------------------- # Standalone functions # ---------------------------------------------------------------------------
[docs] def image_batching( input: Float[Tensor, "B C H W"], patch_shape_y: int, patch_shape_x: int, overlap_pix: int, boundary_pix: int, input_interp: Optional[Float[Tensor, "B C_add Hp Wp"]] = None, ) -> Float[Tensor, "P_times_B C_out Hp Wp"]: r"""Split a batch of images into a batch of patches. Adds reflection padding where necessary and extracts patches in a regular grid using ``torch.nn.functional.unfold``. Parameters ---------- input : Tensor Batch of full images of shape :math:`(B, C, H, W)`. patch_shape_y : int Patch height :math:`H_p`. patch_shape_x : int Patch width :math:`W_p`. overlap_pix : int Overlap between adjacent patches in pixels. boundary_pix : int Boundary padding in pixels. input_interp : Tensor, optional Pre-interpolated additional data of shape :math:`(B, C_{add}, H_p, W_p)`. Channel-wise concatenated to every extracted patch (not decomposed). Returns ------- Tensor Patches of shape :math:`(P \times B, C [+ C_{add}], H_p, W_p)`. """ batch_size, _, img_shape_y, img_shape_x = input.shape # Validate patch / overlap / boundary compatibility if (patch_shape_x - overlap_pix - boundary_pix) < 1: raise ValueError( f"patch_shape_x must verify patch_shape_x ({patch_shape_x}) >= " f"1 + overlap_pix ({overlap_pix}) + boundary_pix ({boundary_pix})" ) if (patch_shape_y - overlap_pix - boundary_pix) < 1: raise ValueError( f"patch_shape_y must verify patch_shape_y ({patch_shape_y}) >= " f"1 + overlap_pix ({overlap_pix}) + boundary_pix ({boundary_pix})" ) if input_interp is not None: if input_interp.shape[0] != batch_size: raise ValueError( f"input_interp batch size ({input_interp.shape[0]}) must match " f"input batch size ({batch_size})" ) if (input_interp.shape[2] != patch_shape_y) or ( input_interp.shape[3] != patch_shape_x ): raise ValueError( f"input_interp patch shape ({input_interp.shape[2]}, {input_interp.shape[3]}) " f"must match specified patch shape ({patch_shape_y}, {patch_shape_x})" ) if patch_shape_x <= overlap_pix + 2 * boundary_pix: raise ValueError( f"patch_shape_x ({patch_shape_x}) must verify " f"patch_shape_x ({patch_shape_x}) > " f"overlap_pix ({overlap_pix}) + 2 * boundary_pix ({boundary_pix})" ) if patch_shape_y <= overlap_pix + 2 * boundary_pix: raise ValueError( f"patch_shape_y ({patch_shape_y}) must verify " f"patch_shape_y ({patch_shape_y}) > " f"overlap_pix ({overlap_pix}) + 2 * boundary_pix ({boundary_pix})" ) # Grid layout stride_x = patch_shape_x - overlap_pix - boundary_pix stride_y = patch_shape_y - overlap_pix - boundary_pix patch_num_x = math.ceil(img_shape_x / stride_x) patch_num_y = math.ceil(img_shape_y / stride_y) padded_shape_x = stride_x * (patch_num_x - 1) + patch_shape_x + boundary_pix padded_shape_y = stride_y * (patch_num_y - 1) + patch_shape_y + boundary_pix patch_num = patch_num_x * patch_num_y # Reflection-pad to fit the grid. Use the functional form (not # ``torch.nn.ReflectionPad2d(...)(input)``) to avoid instantiating a fresh # nn.Module on every call, which is much less friendly to ``torch.compile`` # / AOT autograd tracing. pad_x_right = padded_shape_x - img_shape_x - boundary_pix pad_y_right = padded_shape_y - img_shape_y - boundary_pix input_padded = torch.nn.functional.pad( input, (boundary_pix, pad_x_right, boundary_pix, pad_y_right), mode="reflect", ) # Integer dtypes are not supported by unfold — cast temporarily if input.dtype == torch.int32: input_padded = input_padded.view(torch.float32) elif input.dtype == torch.int64: input_padded = input_padded.view(torch.float64) # Extract patches via unfold x_unfold = torch.nn.functional.unfold( input=input_padded, kernel_size=(patch_shape_y, patch_shape_x), stride=(stride_y, stride_x), ) if input.dtype in [torch.int32, torch.int64]: x_unfold = x_unfold.view(input.dtype) # Rearrange to patch-major batch layout: (P*B, C, Hp, Wp) x_unfold = rearrange( x_unfold, "b (c p_h p_w) (nb_p_h nb_p_w) -> (nb_p_w nb_p_h b) c p_h p_w", p_h=patch_shape_y, p_w=patch_shape_x, nb_p_h=patch_num_y, nb_p_w=patch_num_x, ) if input_interp is not None: input_interp_repeated = input_interp.repeat(patch_num, 1, 1, 1) return torch.cat((x_unfold, input_interp_repeated), dim=1) return x_unfold
[docs] def image_fuse( input: Float[Tensor, "P_times_B C Hp Wp"], img_shape_y: int, img_shape_x: int, batch_size: int, overlap_pix: int, boundary_pix: int, overlap_count: Optional[Float[Tensor, "1 1 Hpad Wpad"]] = None, ) -> Float[Tensor, "B C H W"]: r"""Reconstruct a full image from a batch of grid patches. Reverts the operation performed by :func:`~physicsnemo.diffusion.multi_diffusion.image_batching`. Overlapping regions are averaged with a uniform weight. Parameters ---------- input : Tensor Patches of shape :math:`(P \times B, C, H_p, W_p)`. img_shape_y : int Height :math:`H` of the original full image. img_shape_x : int Width :math:`W` of the original full image. batch_size : int Original batch size :math:`B`. overlap_pix : int Overlap between adjacent patches in pixels. boundary_pix : int Boundary padding in pixels. overlap_count : Tensor, optional Pre-computed overlap count of shape :math:`(1, 1, H_{pad}, W_{pad})`. Computed internally if not provided. Returns ------- Tensor Reconstructed image of shape :math:`(B, C, H, W)`. """ patch_shape_y, patch_shape_x = input.shape[2], input.shape[3] stride_x = patch_shape_x - overlap_pix - boundary_pix stride_y = patch_shape_y - overlap_pix - boundary_pix patch_num_x = math.ceil(img_shape_x / stride_x) patch_num_y = math.ceil(img_shape_y / stride_y) padded_shape_x = stride_x * (patch_num_x - 1) + patch_shape_x + boundary_pix padded_shape_y = stride_y * (patch_num_y - 1) + patch_shape_y + boundary_pix pad_x_right = padded_shape_x - img_shape_x - boundary_pix pad_y_right = padded_shape_y - img_shape_y - boundary_pix pad = (boundary_pix, pad_x_right, boundary_pix, pad_y_right) if overlap_count is None: overlap_count = GridPatching2D.get_overlap_count( (patch_shape_y, patch_shape_x), (img_shape_y, img_shape_x), overlap_pix, boundary_pix, ) if overlap_count.device != input.device: overlap_count = overlap_count.to(input.device) # Rearrange patches back to fold-compatible layout x = rearrange( input, "(nb_p_w nb_p_h b) c p_h p_w -> b (c p_h p_w) (nb_p_h nb_p_w)", p_h=patch_shape_y, p_w=patch_shape_x, nb_p_h=patch_num_y, nb_p_w=patch_num_x, ) # Integer dtypes are not supported by fold — cast temporarily if input.dtype == torch.int32: x = x.view(torch.float32) elif input.dtype == torch.int64: x = x.view(torch.float64) # Stitch patches by summing over overlapping regions x_folded = torch.nn.functional.fold( input=x, output_size=(padded_shape_y, padded_shape_x), kernel_size=(patch_shape_y, patch_shape_x), stride=(stride_y, stride_x), ) if input.dtype in [torch.int32, torch.int64]: x_folded = x_folded.view(input.dtype) # Crop padding and normalise by overlap count x_no_padding = x_folded[ ..., pad[2] : pad[2] + img_shape_y, pad[0] : pad[0] + img_shape_x ] overlap_count_no_padding = overlap_count[ ..., pad[2] : pad[2] + img_shape_y, pad[0] : pad[0] + img_shape_x ] return x_no_padding / overlap_count_no_padding