hackvermin's picture
Upload 26 files
8931de0 verified
Raw History Blame Contribute Delete
34.4 kB
from typing_extensions import Self
from src.datasources import JIF_S2_MEAN, JIF_S2_STD, S2_ALL_12BANDS
import numpy as np
import torch
import torch.nn.functional as F
from math import log2
from kornia.geometry.transform import Resize
from torch import nn, Tensor
from typing import Tuple, Optional
from src.transforms import Shift, WarpPerspective
###############################################################################
# Layers
###############################################################################
class OneHot(nn.Module):
""" One-hot encoder. """
def __init__(self, num_classes):
""" Initialize the OneHot layer.
Parameters
----------
num_classes : int
The number of classes to encode.
"""
super().__init__()
self.num_classes = num_classes
def forward(self, x):
""" Forward pass.
Parameters
----------
x : Tensor
The input tensor.
Returns
-------
Tensor
The encoded tensor.
"""
# Current shape of x: (..., 1, H, W).
x = x.to(torch.int64)
# Remove the empty dimension: (..., H, W).
x = x.squeeze(-3)
# One-hot encode: (..., H, W, num_classes)
x = F.one_hot(x, num_classes=self.num_classes)
# Permute the dimensions so the number of classes is before the height and width.
if x.ndim == 5:
x = x.permute(0, 1, 4, 2, 3) # (..., num_classes, H, W)
elif x.ndim == 4:
x = x.permute(0, 3, 1, 2)
return x
class DoubleConv2d(nn.Module):
""" Two-layer 2D convolutional block with a PReLU activation in between. """
# TODO: verify if we still need reflect padding-mode. If we do, set via constructor.
def __init__(self, in_channels, out_channels, kernel_size=3, use_batchnorm=False):
""" Initialize the DoubleConv2d layer.
Parameters
----------
in_channels : int
The number of input channels.
out_channels : int
The number of output channels.
kernel_size : int, optional
The kernel size, by default 3.
use_batchnorm : bool, optional
Whether to use batch normalization, by default False.
"""
super().__init__()
self.doubleconv2d = nn.Sequential(
# ------- First block -------
# First convolutional layer.
nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
padding="same",
bias=not use_batchnorm,
padding_mode="reflect",
),
# Batch normalization, if requested.
nn.BatchNorm2d(out_channels) if use_batchnorm else nn.Identity(),
# Parametric ReLU activation.
nn.PReLU(),
# Dropout regularization, keep probability 0.5.
nn.Dropout(p=0.5),
# ------- Second block -------
# Second convolutional layer.
nn.Conv2d(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=kernel_size,
padding="same",
bias=not use_batchnorm,
padding_mode="reflect",
),
# Batch normalization, if requested.
nn.BatchNorm2d(out_channels) if use_batchnorm else nn.Identity(),
# Parametric ReLU activation.
nn.PReLU(),
# Dropout regularization, keep probability 0.5.
nn.Dropout(p=0.5),
)
def forward(self, x: Tensor) -> Tensor:
""" Forward pass of the DoubleConv2d layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
The output tensor of shape (batch_size, out_channels, height, width).
"""
return self.doubleconv2d(x)
class ResidualBlock(nn.Module):
""" Two-layer 2D convolutional block (DoubleConv2d)
with a skip-connection to a sum."""
def __init__(self, in_channels, kernel_size=3, **kws):
""" Initialize the ResidualBlock layer.
Parameters
----------
in_channels : int
The number of input channels.
kernel_size : int, optional
The kernel size, by default 3.
"""
super().__init__()
self.residualblock = DoubleConv2d(
in_channels=in_channels,
out_channels=in_channels,
kernel_size=kernel_size,
**kws,
)
def forward(self, x: Tensor) -> Tensor:
""" Forward pass of the ResidualBlock layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
The output tensor of shape (batch_size, in_channels, height, width).
"""
x = x + self.residualblock(x)
return x
class DenseBlock(ResidualBlock):
""" Two-layer 2D convolutional block (DoubleConv2d) with a skip-connection
to a concatenation (instead of a sum used in ResidualBlock)."""
def forward(self, x: Tensor) -> Tensor:
""" Forward pass of the DenseBlock layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
The output tensor of shape (batch_size, in_channels, height, width).
"""
return torch.cat([x, self.residualblock(x)], dim=1)
class FusionBlock(nn.Module):
""" A block that fuses two revisits into one. """
def __init__(self, in_channels, kernel_size=3, use_batchnorm=False):
""" Initialize the FusionBlock layer.
Fuse workflow:
xx ---> xx ---> x
| ^
|-------^
Parameters
----------
in_channels : int
The number of input channels.
kernel_size : int, optional
The kernel size, by default 3.
use_batchnorm : bool, optional
Whether to use batch normalization, by default False.
"""
super().__init__()
# TODO: it might be better to fuse the encodings in groups - one per channel.
number_of_revisits_to_fuse = 2
self.fuse = nn.Sequential(
# A two-layer 2D convolutional block with a skip-connection to a sum.
ResidualBlock(
number_of_revisits_to_fuse * in_channels,
kernel_size,
use_batchnorm=use_batchnorm,
),
# A 2D convolutional layer.
nn.Conv2d(
in_channels=number_of_revisits_to_fuse * in_channels,
out_channels=in_channels,
kernel_size=kernel_size,
padding="same",
bias=not use_batchnorm,
padding_mode="reflect",
),
# Batch normalization, if requested.
nn.BatchNorm2d(in_channels) if use_batchnorm else nn.Identity(),
# Parametric ReLU activation.
nn.PReLU(),
)
@staticmethod
def split(x):
""" Split the input tensor (revisits) into two parts/halves.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, revisits, in_channels, height, width).
Returns
-------
tuple of Tensor
The two output tensors of shape (batch_size, revisits//2, in_channels, height, width).
"""
number_of_revisits = x.shape[1]
assert number_of_revisits % 2 == 0, f"number_of_revisits={number_of_revisits}"
# (batch_size, revisits//2, in_channels, height, width)
first_half = x[:, : number_of_revisits // 2].contiguous()
second_half = x[:, number_of_revisits // 2 :].contiguous()
# TODO: return a carry-encoding?
return first_half, second_half
def forward(self, x):
""" Forward pass of the FusionBlock layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, revisits, in_channels, height, width).
Revisits encoding of the input.
Returns
-------
Tensor
The output tensor of shape (batch_size, revisits/2, in_channels, height, width).
Fused encoding of the input.
"""
first_half, second_half = self.split(x)
batch_size, half_revisits, channels, height, width = first_half.shape
first_half = first_half.view(
batch_size * half_revisits, channels, height, width
)
second_half = second_half.view(
batch_size * half_revisits, channels, height, width
)
# Current shape of x: (batch_size * revisits//2, 2*in_channels, height, width)
x = torch.cat([first_half, second_half], dim=-3)
# Fused shape of x: (batch_size * revisits//2, in_channels, height, width)
fused_x = self.fuse(x)
# Fused encoding shape of x: (batch_size, revisits/2, channels, width, height)
fused_x = fused_x.view(batch_size, half_revisits, channels, height, width)
return fused_x
class RecursiveFusion(nn.Module):
""" Recursively fuses a set of encodings. """
def __init__(self, in_channels, kernel_size, revisits):
""" Initialize the RecursiveFusion layer.
Parameters
----------
in_channels : int
The number of input channels.
kernel_size : int
The kernel size.
revisits : int
The number of revisits.
"""
super().__init__()
log2_revisits = log2(revisits)
if log2_revisits % 1 == 0:
num_fusion_layers = int(log2_revisits)
else:
num_fusion_layers = int(log2_revisits) + 1
pairwise_fusion = FusionBlock(in_channels, kernel_size, use_batchnorm=False)
self.fusion = nn.Sequential(
*(pairwise_fusion for _ in range(num_fusion_layers))
)
@staticmethod
def pad(x):
""" Pad the input tensor with black revisits to a power of 2.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, revisits, in_channels, height, width).
Returns
-------
Tensor
The output tensor of shape (batch_size, revisits, in_channels, height, width).
"""
# TODO: should we pad with copies of revisits instead of zeros?
# TODO: move to transforms.py
batch_size, revisits, channels, height, width = x.shape
log2_revisits = torch.log2(torch.tensor(revisits))
if log2_revisits % 1 > 0:
nextpower = torch.ceil(log2_revisits)
number_of_black_padding_revisits = int(2 ** nextpower - revisits)
black_revisits = torch.zeros(
batch_size,
number_of_black_padding_revisits,
channels,
height,
width,
dtype=x.dtype,
device=x.device,
)
x = torch.cat([x, black_revisits], dim=1)
return x
def forward(self, x):
""" Forward pass of the RecursiveFusion layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, revisits, in_channels, height, width).
Returns
-------
Tensor
The fused output tensor of shape (batch_size, in_channels, height, width).
"""
x = self.pad(x) # Zero-pad if neccessary to ensure power of 2 revisits
x = self.fusion(x) # (batch_size, 1, channels, height, width)
return x.squeeze(1) # (batch_size, channels, height, width)
class ConvTransposeBlock(nn.Module):
""" Upsampler block with ConvTranspose2d. """
def __init__(
self,
in_channels,
out_channels,
kernel_size,
sr_kernel_size,
zoom_factor,
use_batchnorm=False,
):
""" Initialize the ConvTransposeBlock layer.
Parameters
----------
in_channels : int
The number of input channels.
out_channels : int
The number of output channels.
kernel_size : int
The kernel size.
sr_kernel_size : int
The kernel size of the SR convolution.
zoom_factor : int
The zoom factor.
use_batchnorm : bool, optional
Whether to use batchnorm, by default False.
"""
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = kernel_size
# TODO: check if sr_kernel_size is the correct name
self.sr_kernel_size = sr_kernel_size
self.zoom_factor = zoom_factor
self.use_batchnorm = use_batchnorm
self.upsample = nn.Sequential(
nn.ConvTranspose2d(
in_channels=self.in_channels,
out_channels=self.in_channels,
kernel_size=self.kernel_size,
stride=self.zoom_factor,
padding=0,
),
nn.PReLU(),
nn.Conv2d(
in_channels=self.in_channels,
out_channels=self.in_channels,
kernel_size=self.kernel_size,
stride=1,
padding="same",
bias=not use_batchnorm,
padding_mode="reflect",
),
nn.BatchNorm2d(self.in_channels) if use_batchnorm else nn.Identity(),
nn.PReLU(),
nn.Conv2d(
in_channels=self.in_channels,
out_channels=self.out_channels,
kernel_size=self.sr_kernel_size,
stride=1,
padding="same",
bias=not use_batchnorm,
padding_mode="reflect",
),
nn.BatchNorm2d(self.out_channels) if use_batchnorm else nn.Identity(),
nn.PReLU(),
)
def forward(self, x):
""" Forward pass of the ConvTransposeBlock layer.
Parameters
----------
x : Tensor
The input tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
The output tensor of shape (batch_size, out_channels, height, width).
"""
return self.upsample(x)
class PixelShuffleBlock(ConvTransposeBlock):
"""PixelShuffle block with ConvTranspose2d for sub-pixel convolutions. """
# TODO: add a Dropout layer between the convolution layers?
def __init__(self, **kws):
super().__init__(**kws)
assert self.in_channels % self.zoom_factor ** 2 == 0
self.in_channels = self.in_channels // self.zoom_factor ** 2
self.upsample = nn.Sequential(
nn.PixelShuffle(self.zoom_factor),
nn.Conv2d(
in_channels=self.in_channels,
out_channels=self.in_channels,
kernel_size=self.sr_kernel_size,
stride=1,
padding="same",
bias=not self.use_batchnorm,
padding_mode="reflect",
),
nn.BatchNorm2d(self.in_channels) if self.use_batchnorm else nn.Identity(),
nn.PReLU(),
nn.Conv2d(
in_channels=self.in_channels,
out_channels=self.out_channels,
kernel_size=self.sr_kernel_size,
stride=1,
padding="same",
bias=not self.use_batchnorm,
padding_mode="reflect",
),
nn.BatchNorm2d(self.out_channels) if self.use_batchnorm else nn.Identity(),
nn.PReLU(),
)
class HomographyNet(nn.Module):
""" A network that estimates a parametric geometric transformation matrix
between two given images.
Source: https://github.com/mazenmel/Deep-homography-estimation-Pytorch
Reference: https://arxiv.org/pdf/1606.03798.pdf
"""
def __init__(self, input_size, in_channels, fc_size, type="translation"):
""" Initialize the HomographyNet layer.
Parameters
----------
input_size : tuple of int
The input size.
in_channels : int
The number of input channels.
fc_size : int
The number of hidden channels.
type : str, optional
The type of transformation, by default 'translation'.
Raises
------
ValueError
If the type of transformation is not supported.
"""
super().__init__()
if type not in ["translation", "homography"]:
raise ValueError(
f"Expected ['translation'|'homography'] for 'type'. Got {type}."
)
self.type = type
self.input_size = input_size
hidden_channels = fc_size // 2
kernel_size = 3
stride = 1
padding = kernel_size // 2
if self.kind == "translation":
n_transform_params = 2
self.transform = Shift()
elif self.kind == "homography":
n_transform_params = 8
self.transform = WarpPerspective()
def convblock(in_channels, out_channels, kernel_size, stride, padding):
return nn.Sequential(
nn.Conv2d(
in_channels,
out_channels,
kernel_size,
stride,
padding,
bias=False,
padding_mode="reflect",
),
nn.BatchNorm2d(out_channels),
nn.PReLU(),
nn.Conv2d(
out_channels,
out_channels,
kernel_size,
stride,
padding,
bias=False,
padding_mode="reflect",
),
nn.BatchNorm2d(out_channels),
nn.PReLU(),
)
self.cnn = nn.Sequential(
convblock(
in_channels=2 * in_channels,
out_channels=hidden_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
),
nn.MaxPool2d(2),
convblock(
in_channels=hidden_channels,
out_channels=hidden_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
),
nn.MaxPool2d(2),
convblock(
in_channels=hidden_channels,
out_channels=2 * hidden_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
),
nn.MaxPool2d(2),
convblock(
in_channels=2 * hidden_channels,
out_channels=2 * hidden_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
),
)
self.dropout = nn.Dropout(p=0.5)
self.fc = nn.Sequential(
nn.Linear(in_features=fc_size, out_features=n_transform_params, bias=False)
)
def register(self, image1, image2):
""" Register two images.
Parameters
----------
image1 : Tensor
The first image tensor of shape (batch_size, in_channels, height, width).
image2 : Tensor
The second image tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
Parametric transformations on image1 with respect to image2 (batch_size, 2).
"""
# Center each image by its own mean.
image1 = image1 - torch.mean(image1, dim=(2, 3), keepdim=True)
image2 = image2 - torch.mean(image2, dim=(2, 3), keepdim=True)
# Concat channels (batch_size, 2 * channels, height, width)
x = torch.cat([image1, image2], dim=1)
x = self.cnn(x)
# (batch_size, channels) global average pooling
x = x.mean(dim=(2, 3))
x = self.dropout(x)
transformation_parameters = self.fc(x)
transformation_parameters = torch.sigmoid(transformation_parameters) * 2 - 1
return transformation_parameters
def forward(self, image1, image2):
""" Forward pass (register and transform two images).
Parameters
----------
image1 : Tensor
The first image tensor of shape (batch_size, in_channels, height, width).
image2 : Tensor
The second image tensor of shape (batch_size, in_channels, height, width).
Returns
-------
Tensor
The transformed image tensor of shape (batch_size, in_channels, height, width).
"""
with torch.autograd.set_detect_anomaly(True):
transform_params = self.register(image1, image2)
return self.transform(image1, transform_params)
###############################################################################
# Models
###############################################################################
class SRCNN(nn.Module):
""" Super-resolution CNN.
Uses no recursive function, revisits are treated as channels.
"""
def __init__(
self,
in_channels,
mask_channels,
revisits,
hidden_channels,
out_channels,
kernel_size,
residual_layers,
output_size,
zoom_factor,
sr_kernel_size,
registration_kind,
homography_fc_size,
use_reference_frame=False,
**kws,
) -> None:
""" Initialize the Super-resolution CNN.
Parameters
----------
in_channels : int
The number of input channels.
mask_channels : int
The number of mask channels.
revisits : int
The number of revisits.
hidden_channels : int
The number of hidden channels.
out_channels : int
The number of output channels.
kernel_size : int
The kernel size.
residual_layers : int
The number of residual layers.
output_size : tuple of int
The output size.
zoom_factor : int
The zoom factor.
sr_kernel_size : int
The kernel size of the SR convolution.
registration_kind : str, optional
The kind of registration.
homography_fc_size : int
The size of the fully connected layer for the homography.
use_reference_frame : bool, optional
Whether to use the reference frame, by default False.
"""
super().__init__()
self.in_channels = 2 * in_channels if use_reference_frame else in_channels
self.mask_channels = mask_channels
self.revisits = revisits
self.hidden_channels = hidden_channels
self.out_channels = out_channels
self.kernel_size = kernel_size
self.residual_layers = residual_layers
self.output_size = output_size
self.zoom_factor = zoom_factor
self.sr_kernel_size = sr_kernel_size
self.registration_kind = registration_kind
self.homography_fc_size = homography_fc_size
self.use_batchnorm = False
self.use_reference_frame = use_reference_frame
# Image encoder
self.encoder = DoubleConv2d(
in_channels=self.in_channels,
out_channels=hidden_channels,
kernel_size=kernel_size,
use_batchnorm=self.use_batchnorm,
)
# Mask encoder
self.mask_encoder = nn.Sequential(
OneHot(num_classes=12),
DoubleConv2d(in_channels=self.mask_channels, out_channels=1, kernel_size=3),
nn.Sigmoid(),
)
# TODO: does it make sense to reuse the same Res-Block object in nn.Sequential?
# Fusion
self.doubleconv2d = DoubleConv2d(
in_channels=hidden_channels * revisits, # revisits as channels
out_channels=hidden_channels,
kernel_size=kernel_size,
use_batchnorm=self.use_batchnorm,
)
self.residualblocks = nn.Sequential(
*(
ResidualBlock(
in_channels=hidden_channels,
kernel_size=kernel_size,
use_batchnorm=self.use_batchnorm,
)
for _ in range(residual_layers)
)
)
self.fusion = nn.Sequential(self.doubleconv2d, self.residualblocks)
## Super-resolver (upsampler + renderer)
self.sr = PixelShuffleBlock(
in_channels=hidden_channels,
out_channels=out_channels,
kernel_size=kernel_size,
sr_kernel_size=sr_kernel_size,
zoom_factor=zoom_factor,
use_batchnorm=self.use_batchnorm,
)
self.resize = Resize(
self.output_size,
interpolation="bilinear",
align_corners=False,
antialias=True,
)
# TODO: cleanest as reference frame
# TODO: filter for pad-revisits before taking the median
def reference_frame(self, x: Tensor, mask: Optional[Tensor] = None) -> Tensor:
""" Compute the reference frame as the median of all low-res revisits.
Parameters
----------
x : Tensor
The input tensor (low-res revisits).
mask : Tensor, optional
The mask tensor of shape.
Returns
-------
Tensor
The reference frame.
"""
return x.median(dim=-4, keepdim=True).values
def forward(
self, x: Tensor, y: Optional[Tensor] = None, mask: Optional[Tensor] = None,
) -> Tensor:
""" Forward pass of the Super-resolution CNN.
Parameters
----------
x : Tensor
The input tensor (low-res revisits).
Shape: (batch_size, revisits, channels, height, width).
y : Tensor, optional
The target tensor (high-res revisits).
Shape: (batch_size, 1, channels, height, width).
mask : Tensor, optional
The mask tensor.y
Shape: (batch_size, revisits, mask_channels, height, width).
Returns
-------
Tensor
The output tensor (super-resolved images).
Shape: (batch_size, 1, channels, height, width).
"""
if self.use_reference_frame:
# Concatenated shape: (batch_size, revisits, 2*channels, height, width)
x = self.compute_and_concat_reference_frame_to_input(x)
batch_size, revisits, channels, height, width = x.shape
hidden_channels = self.hidden_channels
x = x.view(batch_size * revisits, channels, height, width)
# Encoded shape: (batch_size * revisits, hidden_channels, height, width)
x = self.encoder(x)
# Concatenated shape:
# (batch_size * revisits, hidden_channels+mask_channels, height, width)
x, mask_channels = self.encode_and_concatenate_masks_to_input(
x, mask, batch_size, revisits, height, width
)
x = x.view(
batch_size, revisits * (hidden_channels + mask_channels), height, width
)
# Fused shape: (batch_size, hidden_channels, height, width)
x = self.fusion(x)
x = self.sr(x)
# Ensure output size of (batch_size, channels, height, width)
x = self.resize(x)
# Pad with empty revisit dimension: (batch_size, 1, channels, height, width)
x = x[:, None]
return x
def compute_and_concat_reference_frame_to_input(self, x):
# Current shape: (batch_size, revisits, channels, height, width)
reference_frame = self.reference_frame(x).expand_as(x)
# Concatenated shape: (batch_size, revisits, 2*channels, height, width)
x = torch.cat([x, reference_frame], dim=-3)
return x
def encode_and_concatenate_masks_to_input(
self, x, mask, batch_size, revisits, height, width
):
if mask is not None:
mask, mask_channels = mask, self.mask_channels
mask = mask.view(batch_size * revisits, mask_channels, height, width)
# Encoded shape: (batch_size * revisits, mask_channels, height, width)
mask = self.mask_encoder(mask)
mask_channels = mask.shape[-3]
# Concatenated shape:
# (batch_size * revisits, hidden_channels+mask_channels, height, width)
x = torch.cat([x, mask], dim=-3)
else:
mask_channels = 0
return x, mask_channels
class HighResNet(SRCNN):
""" High-resolution CNN.
Inherits as many elements from SRCNN as possible for as fair a comparison:
- DoubleConv2d: the in_channels are doubled by use_reference_frame in SRCNN init
- Encoder: nn.Sequential(DoubleConv2d, ResidualBlock)
"""
def __init__(self, skip_paddings=True, **kws) -> None:
super().__init__(**kws)
self.skip_paddings = skip_paddings
self.fusion = RecursiveFusion(
in_channels=self.hidden_channels,
kernel_size=self.kernel_size,
# skip_paddings=skip_paddings,
revisits=self.revisits,
)
def forward(self, x, y=None, mask=None):
""" Forward pass of the High-resolution CNN.
Parameters
----------
x : Tensor
The input tensor (low-res revisits).
Shape: (batch_size, revisits, channels, height, width).
y : Tensor, optional
The target tensor (high-res revisits).
Shape: (batch_size, 1, channels, height, width).
mask : Tensor, optional
The mask tensor.
Shape: (batch_size, revisits, mask_channels, height, width).
Returns
-------
Tensor
The output tensor (super-resolved images).
Shape: (batch_size, 1, channels, height, width).
"""
hidden_channels = self.hidden_channels
if self.use_reference_frame:
x = self.compute_and_concat_reference_frame_to_input(x)
# Note: we could put all these layers in a Sequential, but we
# would lose the logging abilities for inspection and lose
# on readability.
batch_size, revisits, channels, height, width = x.shape
x = x.view(batch_size * revisits, channels, height, width)
# Encoded shape: (batch_size * revisits, hidden_channels, height, width)
x = self.encoder(x)
x, mask_channels = self.encode_and_concatenate_masks_to_input(
x, mask, batch_size, revisits, height, width
)
x = x.view(batch_size, revisits, hidden_channels + mask_channels, height, width)
# Fused shape: (batch_size, hidden_channels, height, width)
x = self.fusion(x)
# Super-resolved shape:
# (batch_size, out_channels, height * zoom_factor, width * zoom_factor)
x = self.sr(x)
# Ensure output size of (batch_size, channels, height, width)
x = self.resize(x)
# Pad with empty revisit dimension: (batch_size, 1, channels, height, width)
x = x[:, None]
return x
class BicubicUpscaledBaseline(nn.Module):
""" Bicubic upscaled single-image baseline. """
def __init__(
self, input_size, output_size, chip_size, interpolation="bicubic", device=None, **kws
):
""" Initialize the BicubicUpscaledBaseline.
Parameters
----------
input_size : tuple of int
The input size.
output_size : tuple of int
The output size.
chip_size : tuple of int
The chip size.
interpolation : str, optional
The interpolation method, by default 'bicubic'.
Available methods: 'nearest', 'bilinear', 'bicubic'.
"""
super().__init__()
assert interpolation in ["bilinear", "bicubic", "nearest"]
self.resize = Resize(output_size, interpolation=interpolation)
self.output_size = output_size
self.input_size = input_size
self.chip_size = chip_size
self.lr_bands = np.array(S2_ALL_12BANDS["true_color"]) - 1
self.mean = JIF_S2_MEAN[self.lr_bands]
self.std = JIF_S2_STD[self.lr_bands]
self.device = device
def forward(self, x: Tensor) -> Tensor:
""" Forward pass of the BicubicUpscaledBaseline.
Parameters
----------
x : Tensor
The input tensor (a batch of low-res revisits).
Shape: (batch_size, revisits, channels, height, width).
Returns
-------
Tensor
The output tensor (a single upscaled low-res revisit).
"""
# If all bands are used, get only the RGB bands for WandB image logging
if x.shape[2] > 3:
x = x[:, :, S2_ALL_12BANDS["true_color_zero_index"]]
# Select the first revisit
x = x[:, 0, :]
# Pad with empty revisit dimension: (batch_size, 1, channels, height, width)
x = x[:, None, :]
# Normalisation on the channel axis:
# Add the mean and multiply by the standard deviation
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') if self.device is None else self.device
x += torch.as_tensor(self.mean[None, None, ..., None, None]).to(device)
x *= torch.as_tensor(self.std[None, None, ..., None, None]).to(device)
# Convert to float, and scale to [0, 1]:
x = x.float()
x /= torch.max(x)
torch.clamp_(x, 0, 1)
# Upscale to the output size:
x = self.resize(x) # upscale (..., T, C, H_o, W_o)
return x