From d51172fc683c588964d4017e68713643da8dee0a Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 11:12:39 +0000 Subject: [PATCH 01/16] Add RAFT --- torchvision/models/__init__.py | 1 + torchvision/models/optical_flow/__init__.py | 1 + torchvision/models/optical_flow/_raft/raft.py | 538 ++++++++++++++++++ .../models/optical_flow/_raft/utils.py | 42 ++ 4 files changed, 582 insertions(+) create mode 100644 torchvision/models/optical_flow/__init__.py create mode 100644 torchvision/models/optical_flow/_raft/raft.py create mode 100644 torchvision/models/optical_flow/_raft/utils.py diff --git a/torchvision/models/__init__.py b/torchvision/models/__init__.py index 516e47feb19..c9d11f88f01 100644 --- a/torchvision/models/__init__.py +++ b/torchvision/models/__init__.py @@ -12,6 +12,7 @@ from .regnet import * from . import detection from . import feature_extraction +from . import optical_flow from . import quantization from . import segmentation from . import video diff --git a/torchvision/models/optical_flow/__init__.py b/torchvision/models/optical_flow/__init__.py new file mode 100644 index 00000000000..72380e46fef --- /dev/null +++ b/torchvision/models/optical_flow/__init__.py @@ -0,0 +1 @@ +from ._raft.raft import RAFT, raft, raft_small diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py new file mode 100644 index 00000000000..a10c90bba39 --- /dev/null +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -0,0 +1,538 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.nn.modules.batchnorm import BatchNorm2d +from torch.nn.modules.instancenorm import InstanceNorm2d +from torchvision.ops import ConvNormActivation + +from .utils import grid_sample, make_coords_grid, upsample_flow + + +class ResidualBlock(nn.Module): + # This is pretty similar to resnet.BasicBlock except for one call to relu, and the bias terms + def __init__(self, in_channels, out_channels, *norm_layer, stride=1): + super().__init__() + + # Note regarding bias=True: + # Usually we can pass bias=False in conv layers followed by a norm layer. + # But in the RAFT training reference, the BatchNorm2d layers are only activated for the first dataset, + # and frozen for the rest of the training process (i.e. set as eval()). The bias term is thus still useful + # for the rest of the datasets. Technically, we could remove the bias for other norm layers like Instance norm + # because these aren't frozen, but we don't bother (also, we woudn't be able to load the original weights). + self.convnormrelu1 = ConvNormActivation( + in_channels, out_channels, norm_layer=norm_layer, kernel_size=3, stride=stride, bias=True + ) + self.convnormrelu2 = ConvNormActivation( + out_channels, out_channels, norm_layer=norm_layer, kernel_size=3, bias=True + ) + + if stride == 1: + self.downsample = None + else: + self.downsample = ConvNormActivation( + in_channels, + out_channels, + norm_layer=norm_layer, + kernel_size=1, + stride=stride, + bias=True, + activation_layer=None, + ) + + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + y = x + y = self.convnormrelu1(y) + y = self.convnormrelu2(y) + + if self.downsample is not None: + x = self.downsample(x) + + return self.relu(x + y) + + +class BottleneckBlock(nn.Module): + def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): + super(BottleneckBlock, self).__init__() + + # See note in ResidualBlock for the reason behind bias=True + self.convnormrelu1 = ConvNormActivation( + in_channels, out_channels // 4, norm_layer=norm_layer, kernel_size=1, bias=True + ) + self.convnormrelu2 = ConvNormActivation( + out_channels // 4, out_channels // 4, norm_layer=norm_layer, kernel_size=3, stride=stride, bias=True + ) + self.convnormrelu3 = ConvNormActivation( + out_channels // 4, out_channels, norm_layer=norm_layer, kernel_size=1, bias=True + ) + self.relu = nn.ReLU(inplace=True) + + if stride == 1: + self.downsample = None + else: + self.downsample = ConvNormActivation( + in_channels, + out_channels, + norm_layer=norm_layer, + kernel_size=1, + stride=stride, + bias=True, + activation_layer=None, + ) + + def forward(self, x): + y = x + y = self.convnormrelu1(y) + y = self.convnormrelu2(y) + y = self.convnormrelu3(y) + + if self.downsample is not None: + x = self.downsample(x) + + return self.relu(x + y) + + +class FeatureEncoder(nn.Module): + def __init__(self, *, block=ResidualBlock, layers=(64, 64, 96, 128, 256), norm_layer=nn.BatchNorm2d): + super().__init__() + + assert len(layers) == 5 + + # See note in ResidualBlock for the reason behind bias=True + self.convnormrelu = ConvNormActivation(3, layers[0], norm_layer=norm_layer, kernel_size=7, stride=2, bias=True) + + self.layer1 = self._make_2_blocks(block, layers[0], layers[1], norm_layer=norm_layer, first_stride=1) + self.layer2 = self._make_2_blocks(block, layers[1], layers[2], norm_layer=norm_layer, first_stride=2) + self.layer3 = self._make_2_blocks(block, layers[2], layers[3], norm_layer=norm_layer, first_stride=2) + + self.conv = nn.Conv2d(layers[3], layers[4], kernel_size=1) + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + elif isinstance(m, (nn.BatchNorm2d, nn.InstanceNorm2d)): + if m.weight is not None: + nn.init.constant_(m.weight, 1) + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def _make_2_blocks(self, block, in_channels, out_channels, norm_layer, first_stride): + block1 = block(in_channels, out_channels, norm_layer=norm_layer, stride=first_stride) + block2 = block(out_channels, out_channels, norm_layer=norm_layer, stride=1) + return nn.Sequential(block1, block2) + + def forward(self, x): + x = self.convnormrelu(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + + x = self.conv(x) + + return x + + +class MotionEncoder(nn.Module): + def __init__(self, *, in_channels_corr, corr_layers=(256, 192), flow_layers=(128, 64), out_channels=128): + super().__init__() + + assert len(flow_layers) == 2 + assert len(corr_layers) in (1, 2) + + self.convcorr1 = ConvNormActivation(in_channels_corr, corr_layers[0], norm_layer=None, kernel_size=1) + if len(corr_layers) == 2: + self.convcorr2 = ConvNormActivation(corr_layers[0], corr_layers[1], norm_layer=None, kernel_size=3) + else: + self.convcorr2 = None + + self.convflow1 = ConvNormActivation(2, flow_layers[0], norm_layer=None, kernel_size=7) + self.convflow2 = ConvNormActivation(flow_layers[0], flow_layers[1], norm_layer=None, kernel_size=3) + + # out_channels - 2 because we cat the flow (2 channels) at the end + self.conv = ConvNormActivation( + corr_layers[-1] + flow_layers[-1], out_channels - 2, norm_layer=None, kernel_size=3 + ) + + self.out_channels = out_channels + + def forward(self, flow, corr_features): + corr = self.convcorr1(corr_features) + if self.convcorr2 is not None: + corr = self.convcorr2(corr) + + flow_orig = flow + flow = self.convflow1(flow) + flow = self.convflow2(flow) + + corr_flow = torch.cat([corr, flow], dim=1) + corr_flow = self.conv(corr_flow) + return torch.cat([corr_flow, flow_orig], dim=1) + + +class ConvGRU(nn.Module): + def __init__(self, *, input_size, hidden_size, kernel_size, padding): + super().__init__() + self.convz = nn.Conv2d(hidden_size + input_size, hidden_size, kernel_size=kernel_size, padding=padding) + self.convr = nn.Conv2d(hidden_size + input_size, hidden_size, kernel_size=kernel_size, padding=padding) + self.convq = nn.Conv2d(hidden_size + input_size, hidden_size, kernel_size=kernel_size, padding=padding) + + def forward(self, h, x): + hx = torch.cat([h, x], dim=1) + z = torch.sigmoid(self.convz(hx)) + r = torch.sigmoid(self.convr(hx)) + q = torch.tanh(self.convq(torch.cat([r * h, x], dim=1))) + h = (1 - z) * h + z * q + return h + + +class RecurrentBlock(nn.Module): + def __init__(self, *, input_size, hidden_size, kernel_size=((1, 5), (5, 1)), padding=((0, 2), (2, 0))): + super().__init__() + + assert len(kernel_size) == len(padding) + assert len(kernel_size) in (1, 2) + + self.convgru1 = ConvGRU( + input_size=input_size, hidden_size=hidden_size, kernel_size=kernel_size[0], padding=padding[0] + ) + if len(kernel_size) == 2: + self.convgru2 = ConvGRU( + input_size=input_size, hidden_size=hidden_size, kernel_size=kernel_size[1], padding=padding[1] + ) + else: + self.convgru2 = None + + self.hidden_size = hidden_size + + def forward(self, h, x): + h = self.convgru1(h, x) + if self.convgru2 is not None: + h = self.convgru2(h, x) + return h + + +class FlowHead(nn.Module): + def __init__(self, *, in_channels, hidden_size): + super().__init__() + self.conv1 = nn.Conv2d(in_channels, hidden_size, 3, padding=1) + self.conv2 = nn.Conv2d(hidden_size, 2, 3, padding=1) + self.relu = nn.ReLU(inplace=True) + + def forward(self, x): + return self.conv2(self.relu(self.conv1(x))) + + +class UpdateBlock(nn.Module): + def __init__(self, *, motion_encoder, recurrent_block, flow_head): + super().__init__() + self.motion_encoder = motion_encoder + self.recurrent_block = recurrent_block + self.flow_head = flow_head + + self.hidden_state_size = recurrent_block.hidden_size + + def forward(self, hidden_state, context, corr_features, flow): + motion_features = self.motion_encoder(flow, corr_features) + x = torch.cat([context, motion_features], dim=1) + + hidden_state = self.recurrent_block(hidden_state, x) + delta_flow = self.flow_head(hidden_state) + return hidden_state, delta_flow + + +class MaskPredictor(nn.Module): + def __init__(self, *, in_channels, hidden_size, multiplier=0.25): + super().__init__() + self.conv1 = nn.Conv2d(in_channels, hidden_size, kernel_size=3, padding=1) + self.relu = nn.ReLU(inplace=True) + # 8 * 8 * 9 because the predicted flow is downsampled by 8, from the downsampling of the initial FeatureEncoder + # and we interpolate with all 9 surrounding neighbors. See paper and appendix B. + self.conv2 = nn.Conv2d(hidden_size, 8 * 8 * 9, 1, padding=0) + + # In the original code, they use a factor of 0.25 to "downweight the gradients" of that branch. + # See e.g. https://github.com/princeton-vl/RAFT/issues/119#issuecomment-953950419 + # or https://github.com/princeton-vl/RAFT/issues/24. + # It doesn't seem to affect epe significantly and can likely be set to 1. + self.multiplier = multiplier + + def forward(self, x): + x = self.conv1(x) + x = self.relu(x) + x = self.conv2(x) + return self.multiplier * x + + +class CorrBlock: + def __init__(self, *, num_levels=4, radius=4): + self.num_levels = num_levels + self.radius = radius + + # The neighborhood of a centroid pixel x' is {x' + delta, ||delta||_inf <= radius} + # so it's a square surrounding x', and its sides have a length of 2 * radius + 1 + # The paper claims that it's ||.||_1 instead of ||.||_inf but it's a typo: + # https://github.com/princeton-vl/RAFT/issues/122 + self.out_channels = num_levels * (2 * radius + 1) ** 2 + + def build_pyramid(self, fmap1, fmap2): + """Build the correlation pyramid from two feature maps. + + The correlation volume is first computed as the dot product of each pair (pixel_in_fmap1, pixel_in_fmap2) + The last 2 dimensions of the correlation volume are then pooled num_levels times at different resolutions + to build the correlation pyramid. + """ + + def compute_corr_volume(fmap1, fmap2): + batch_size, num_channels, h, w = fmap1.shape + fmap1 = fmap1.view(batch_size, num_channels, h * w) + fmap2 = fmap2.view(batch_size, num_channels, h * w) + + corr = torch.matmul(fmap1.transpose(1, 2), fmap2) + corr = corr.view(batch_size, h, w, 1, h, w) + return corr / torch.sqrt(torch.tensor(num_channels)) + + torch._assert(fmap1.shape == fmap2.shape, "Input feature maps should have the same shapes") + corr_volume = compute_corr_volume(fmap1, fmap2) + + batch_size, h, w, num_channels, _, _ = corr_volume.shape # _, _ = h, w + corr_volume = corr_volume.reshape(batch_size * h * w, num_channels, h, w) + self.corr_pyramid = [corr_volume] + for _ in range(self.num_levels - 1): + corr_volume = F.avg_pool2d(corr_volume, kernel_size=2, stride=2) + self.corr_pyramid.append(corr_volume) + + def index_pyramid(self, centroids_coords): + """Return correlation features by indexing from the pyramid.""" + neighborhood_side_len = 2 * self.radius + 1 # see note in __init__ about out_channels + di = torch.linspace(-self.radius, self.radius, neighborhood_side_len) + dj = torch.linspace(-self.radius, self.radius, neighborhood_side_len) + delta = torch.stack(torch.meshgrid(di, dj, indexing="ij"), axis=-1).to(centroids_coords.device) + delta = delta.view(1, neighborhood_side_len, neighborhood_side_len, 2) + + batch_size, _, h, w = centroids_coords.shape # _ = 2 + centroids_coords = centroids_coords.permute(0, 2, 3, 1).reshape(batch_size * h * w, 1, 1, 2) + + indexed_pyramid = [] + for corr_volume in self.corr_pyramid: + sampling_coords = centroids_coords + delta # end shape is (batch_size * h * w, side_len, side_len, 2) + indexed_corr_volume = grid_sample(corr_volume, sampling_coords, align_corners=True, mode="bilinear").view( + batch_size, h, w, -1 + ) + indexed_pyramid.append(indexed_corr_volume) + centroids_coords = centroids_coords / 2 + + corr_features = torch.cat(indexed_pyramid, dim=-1).permute(0, 3, 1, 2).contiguous() + + expected_output_shape = (batch_size, self.out_channels, h, w) + torch._assert( + corr_features.shape == expected_output_shape, + f"Output shape of index pyramid is incorrect. Should be {expected_output_shape}, got {corr_features.shape}", + ) + + return corr_features + + +class RAFT(nn.Module): + def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block, mask_predictor=None): + super().__init__() + + self.feature_encoder = feature_encoder + self.context_encoder = context_encoder + self.corr_block = corr_block + self.update_block = update_block + + self.mask_predictor = mask_predictor + + if not hasattr(self.update_block, "hidden_state_size"): + raise ValueError("The update_block parameter should expose a 'hidden_state_size' attribute.") + + def forward(self, image1, image2, *, num_flow_updates=12): + + batch_size, _, h, w = image1.shape + torch._assert((h, w) == image2.shape[-2:], "input images should have the same shape") + torch._assert((h % 8 == 0) and (w % 8 == 0), "input image H and W should be divisible by 8") + + fmaps = self.feature_encoder(torch.cat([image1, image2], dim=0)) + fmap1, fmap2 = torch.chunk(fmaps, chunks=2, dim=0) + torch._assert(fmap1.shape[-2:] == (h / 8, w / 8), "The feature encoder should downsample H and W by 8") + + self.corr_block.build_pyramid(fmap1, fmap2) + + context_out = self.context_encoder(image1) + torch._assert(context_out.shape[-2:] == (h / 8, w / 8), "The context encoder should downsample H and W by 8") + + # As in the original paper, the actual output of the context encoder is split in 2 parts: + # - one part is used to initialize the hidden state of the reccurent units of the update block + # - the rest is the "actual" context. + hidden_state_size = self.update_block.hidden_state_size + out_channels_context = context_out.shape[1] - hidden_state_size + torch._assert( + out_channels_context > 0, + f"The context encoder outputs {context_out.shape[1]} channels, but it should have at least " + f"hidden_state={hidden_state_size} channels", + ) + hidden_state, context = torch.split(context_out, [hidden_state_size, out_channels_context], dim=1) + hidden_state = torch.tanh(hidden_state) + context = torch.relu(context) + + coords0 = make_coords_grid(batch_size, h // 8, w // 8).cuda() + coords1 = make_coords_grid(batch_size, h // 8, w // 8).cuda() + + flow_predictions = [] + for _ in range(num_flow_updates): + coords1 = coords1.detach() # Don't backpropagate gradients through this branch, see paper + corr_features = self.corr_block.index_pyramid(centroids_coords=coords1) + + flow = coords1 - coords0 + hidden_state, delta_flow = self.update_block(hidden_state, context, corr_features, flow) + + coords1 = coords1 + delta_flow + + up_mask = None if self.mask_predictor is None else self.mask_predictor(hidden_state) + upsampled_flow = upsample_flow(flow=(coords1 - coords0), up_mask=up_mask) + flow_predictions.append(upsampled_flow) + + return flow_predictions + + +def _raft( + *, + # Feature encoder + feature_encoder_layers, + feature_encoder_block, + feature_encoder_norm_layer, + # Context encoder + context_encoder_layers, + context_encoder_block, + context_encoder_norm_layer, + # Correlation block + corr_block_num_levels, + corr_block_radius, + # Motion encoder + motion_encoder_corr_layers, + motion_encoder_flow_layers, + motion_encoder_out_channels, + # Recurrent block + recurrent_block_hidden_state_size, + recurrent_block_kernel_size, + recurrent_block_padding, + # Flow Head + flow_head_hidden_size, + # Mask predictor + use_mask_predictor, + **kwargs, +): + feature_encoder = kwargs.pop("feature_encoder", None) or FeatureEncoder( + block=feature_encoder_block, layers=feature_encoder_layers, norm_layer=feature_encoder_norm_layer + ) + context_encoder = kwargs.pop("context_encoder", None) or FeatureEncoder( + block=context_encoder_block, layers=context_encoder_layers, norm_layer=context_encoder_norm_layer + ) + + corr_block = kwargs.pop("corr_block", None) or CorrBlock(num_levels=corr_block_num_levels, radius=corr_block_radius) + + update_block = kwargs.pop("update_block", None) + if update_block is None: + motion_encoder = MotionEncoder( + in_channels_corr=corr_block.out_channels, + corr_layers=motion_encoder_corr_layers, + flow_layers=motion_encoder_flow_layers, + out_channels=motion_encoder_out_channels, + ) + + # See comments in forward pass of RAFT class about why we split the output of the context encoder + out_channels_context = context_encoder_layers[-1] - recurrent_block_hidden_state_size + recurrent_block = RecurrentBlock( + input_size=motion_encoder.out_channels + out_channels_context, + hidden_size=recurrent_block_hidden_state_size, + kernel_size=recurrent_block_kernel_size, + padding=recurrent_block_padding, + ) + + flow_head = FlowHead(in_channels=recurrent_block_hidden_state_size, hidden_size=flow_head_hidden_size) + + update_block = UpdateBlock(motion_encoder=motion_encoder, recurrent_block=recurrent_block, flow_head=flow_head) + + mask_predictor = kwargs.pop("mask_predictor", None) + if mask_predictor is None and use_mask_predictor: + mask_predictor = MaskPredictor( + in_channels=recurrent_block_hidden_state_size, + hidden_size=256, + multiplier=0.25, # See comment in MaskPredictor about this + ) + + return RAFT( + feature_encoder=feature_encoder, + context_encoder=context_encoder, + corr_block=corr_block, + update_block=update_block, + mask_predictor=mask_predictor, + **kwargs, # not really needed, all params should be consumed by now + ) + + +def raft(*, weights=None, progress=True, **kwargs): + + if weights is not None or progress is not None: + raise NotImplemented("Pretrained weights aren't available yet") + + return _raft( + # Feature encoder + feature_encoder_layers=(64, 64, 96, 128, 256), + feature_encoder_block=ResidualBlock, + feature_encoder_norm_layer=InstanceNorm2d, + # Context encoder + context_encoder_layers=(64, 64, 96, 128, 256), + context_encoder_block=ResidualBlock, + context_encoder_norm_layer=BatchNorm2d, + # Correlation block + corr_block_num_levels=4, + corr_block_radius=4, + # Motion encoder + motion_encoder_corr_layers=(256, 192), + motion_encoder_flow_layers=(128, 64), + motion_encoder_out_channels=128, + # Reccurrent block + recurrent_block_hidden_state_size=128, + recurrent_block_kernel_size=((1, 5), (5, 1)), + recurrent_block_padding=((0, 2), (2, 0)), + # Flow head + flow_head_hidden_size=256, + # Mask predictor + use_mask_predictor=True, + **kwargs, + ) + + +def raft_small(*, weights=None, progress=True, **kwargs): + + if weights is not None or progress is not None: + raise NotImplemented("Pretrained weights aren't available yet") + + return _raft( + # Feature encoder + feature_encoder_layers=(32, 32, 64, 96, 128), + feature_encoder_block=BottleneckBlock, + feature_encoder_norm_layer=InstanceNorm2d, + # Context encoder + context_encoder_layers=(32, 32, 64, 96, 160), + context_encoder_block=BottleneckBlock, + context_encoder_norm_layer=None, + # Correlation block + corr_block_num_levels=4, + corr_block_radius=3, + # Motion encoder + motion_encoder_corr_layers=(96,), + motion_encoder_flow_layers=(64, 32), + motion_encoder_out_channels=82, + # Reccurrent block + recurrent_block_hidden_state_size=96, + recurrent_block_kernel_size=(3,), + recurrent_block_padding=(1,), + # Flow head + flow_head_hidden_size=128, + # Mask predictor + use_mask_predictor=False, + **kwargs, + ) diff --git a/torchvision/models/optical_flow/_raft/utils.py b/torchvision/models/optical_flow/_raft/utils.py new file mode 100644 index 00000000000..1c3d1703133 --- /dev/null +++ b/torchvision/models/optical_flow/_raft/utils.py @@ -0,0 +1,42 @@ +import torch +import torch.nn.functional as F + + +def grid_sample(img, absolute_grid, *args, **kwargs): + """Same as torch's grid_sample, with absolute pixel coordinates instead of normalized coordinates.""" + h, w = img.shape[-2:] + + xgrid, ygrid = absolute_grid.split([1, 1], dim=-1) + xgrid = 2 * xgrid / (w - 1) - 1 + ygrid = 2 * ygrid / (h - 1) - 1 + normalized_grid = torch.cat([xgrid, ygrid], dim=-1) + + return F.grid_sample(img, normalized_grid, *args, **kwargs) + + +def make_coords_grid(batch_size, h, w): + coords = torch.meshgrid(torch.arange(h), torch.arange(w), indexing="ij") + coords = torch.stack(coords[::-1], dim=0).float() + return coords[None].repeat(batch_size, 1, 1, 1) + + +def upsample_flow(flow, up_mask=None): + """Upsample flow by a factor of 8. + + If up_mask is None we just interpolate. + If up_mask is specified, we upsample using a convex combination of its weights. See paper page 8 and appendix B. + Note that in appendix B the picture assumes a downsample factor of 4 instead of 8. + """ + batch_size, _, h, w = flow.shape + new_h, new_w = h * 8, w * 8 + + if up_mask is None: + return 8 * F.interpolate(flow, size=(new_h, new_w), mode="bilinear", align_corners=True) + + up_mask = up_mask.view(batch_size, 1, 9, 8, 8, h, w) + up_mask = torch.softmax(up_mask, dim=2) # "convex" == weights sum to 1 + + upsampled_flow = F.unfold(8 * flow, kernel_size=3, padding=1).view(batch_size, 2, 9, 1, 1, h, w) + upsampled_flow = torch.sum(up_mask * upsampled_flow, dim=2) + + return upsampled_flow.permute(0, 1, 4, 2, 5, 3).reshape(batch_size, 2, new_h, new_w) From 578373be71f24a3c8baa110925b23ca79e390049 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 12:38:39 +0000 Subject: [PATCH 02/16] Minor fixes --- torchvision/models/optical_flow/_raft/raft.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index a10c90bba39..029f925e061 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -10,7 +10,7 @@ class ResidualBlock(nn.Module): # This is pretty similar to resnet.BasicBlock except for one call to relu, and the bias terms - def __init__(self, in_channels, out_channels, *norm_layer, stride=1): + def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): super().__init__() # Note regarding bias=True: @@ -474,8 +474,8 @@ def _raft( def raft(*, weights=None, progress=True, **kwargs): - if weights is not None or progress is not None: - raise NotImplemented("Pretrained weights aren't available yet") + if weights is not None: + raise NotImplementedError("Pretrained weights aren't available yet") return _raft( # Feature encoder @@ -507,8 +507,8 @@ def raft(*, weights=None, progress=True, **kwargs): def raft_small(*, weights=None, progress=True, **kwargs): - if weights is not None or progress is not None: - raise NotImplemented("Pretrained weights aren't available yet") + if weights is not None: + raise NotImplementedError("Pretrained weights aren't available yet") return _raft( # Feature encoder From 19629a341420a0cfa46ee1ae6c8ef11549c73b08 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 12:47:09 +0000 Subject: [PATCH 03/16] add _init_weights() method --- torchvision/models/optical_flow/_raft/raft.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index 029f925e061..ffb0232ade6 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -108,6 +108,9 @@ def __init__(self, *, block=ResidualBlock, layers=(64, 64, 96, 128, 256), norm_l self.conv = nn.Conv2d(layers[3], layers[4], kernel_size=1) + self._init_weights() + + def _init_weights(self): for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") From 44b429059d28642937bb183bb449f069b78c66a1 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 12:52:47 +0000 Subject: [PATCH 04/16] weights -> pretrained --- torchvision/models/optical_flow/_raft/raft.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index ffb0232ade6..95fb3764e0e 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -475,9 +475,9 @@ def _raft( ) -def raft(*, weights=None, progress=True, **kwargs): +def raft(*, pretrained=False, progress=True, **kwargs): - if weights is not None: + if pretrained: raise NotImplementedError("Pretrained weights aren't available yet") return _raft( @@ -508,9 +508,9 @@ def raft(*, weights=None, progress=True, **kwargs): ) -def raft_small(*, weights=None, progress=True, **kwargs): +def raft_small(*, pretrained=False, progress=True, **kwargs): - if weights is not None: + if pretrained: raise NotImplementedError("Pretrained weights aren't available yet") return _raft( From 7ea67ea1d5de0ddb8316e764096b98cb206673e9 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 13:11:06 +0000 Subject: [PATCH 05/16] Use ConvNormActivation in MaskPredictor --- torchvision/models/optical_flow/_raft/raft.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index 95fb3764e0e..e47ee81448b 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -248,11 +248,10 @@ def forward(self, hidden_state, context, corr_features, flow): class MaskPredictor(nn.Module): def __init__(self, *, in_channels, hidden_size, multiplier=0.25): super().__init__() - self.conv1 = nn.Conv2d(in_channels, hidden_size, kernel_size=3, padding=1) - self.relu = nn.ReLU(inplace=True) + self.convrelu = ConvNormActivation(in_channels, hidden_size, norm_layer=None, kernel_size=3) # 8 * 8 * 9 because the predicted flow is downsampled by 8, from the downsampling of the initial FeatureEncoder # and we interpolate with all 9 surrounding neighbors. See paper and appendix B. - self.conv2 = nn.Conv2d(hidden_size, 8 * 8 * 9, 1, padding=0) + self.conv = nn.Conv2d(hidden_size, 8 * 8 * 9, 1, padding=0) # In the original code, they use a factor of 0.25 to "downweight the gradients" of that branch. # See e.g. https://github.com/princeton-vl/RAFT/issues/119#issuecomment-953950419 @@ -261,9 +260,8 @@ def __init__(self, *, in_channels, hidden_size, multiplier=0.25): self.multiplier = multiplier def forward(self, x): - x = self.conv1(x) - x = self.relu(x) - x = self.conv2(x) + x = self.convrelu(x) + x = self.conv(x) return self.multiplier * x From 6b3af060d32509b067746c78b08c89e6235b2fac Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 13:20:57 +0000 Subject: [PATCH 06/16] Use nn.Identity instead of checking for None layers --- torchvision/models/optical_flow/_raft/raft.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index e47ee81448b..487d5c7a38c 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -27,7 +27,7 @@ def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): ) if stride == 1: - self.downsample = None + self.downsample = nn.Identity() else: self.downsample = ConvNormActivation( in_channels, @@ -46,8 +46,7 @@ def forward(self, x): y = self.convnormrelu1(y) y = self.convnormrelu2(y) - if self.downsample is not None: - x = self.downsample(x) + x = self.downsample(x) return self.relu(x + y) @@ -69,7 +68,7 @@ def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): self.relu = nn.ReLU(inplace=True) if stride == 1: - self.downsample = None + self.downsample = nn.Identity() else: self.downsample = ConvNormActivation( in_channels, @@ -87,8 +86,7 @@ def forward(self, x): y = self.convnormrelu2(y) y = self.convnormrelu3(y) - if self.downsample is not None: - x = self.downsample(x) + x = self.downsample(x) return self.relu(x + y) @@ -148,7 +146,7 @@ def __init__(self, *, in_channels_corr, corr_layers=(256, 192), flow_layers=(128 if len(corr_layers) == 2: self.convcorr2 = ConvNormActivation(corr_layers[0], corr_layers[1], norm_layer=None, kernel_size=3) else: - self.convcorr2 = None + self.convcorr2 = nn.Identity() self.convflow1 = ConvNormActivation(2, flow_layers[0], norm_layer=None, kernel_size=7) self.convflow2 = ConvNormActivation(flow_layers[0], flow_layers[1], norm_layer=None, kernel_size=3) @@ -162,8 +160,7 @@ def __init__(self, *, in_channels_corr, corr_layers=(256, 192), flow_layers=(128 def forward(self, flow, corr_features): corr = self.convcorr1(corr_features) - if self.convcorr2 is not None: - corr = self.convcorr2(corr) + corr = self.convcorr2(corr) flow_orig = flow flow = self.convflow1(flow) @@ -205,14 +202,13 @@ def __init__(self, *, input_size, hidden_size, kernel_size=((1, 5), (5, 1)), pad input_size=input_size, hidden_size=hidden_size, kernel_size=kernel_size[1], padding=padding[1] ) else: - self.convgru2 = None + self.convgru2 = lambda h, _: h # identity self.hidden_size = hidden_size def forward(self, h, x): h = self.convgru1(h, x) - if self.convgru2 is not None: - h = self.convgru2(h, x) + h = self.convgru2(h, x) return h From 173ccdeaa5f127e273a63981b1b9aad266adeaed Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 13:23:47 +0000 Subject: [PATCH 07/16] Extract out _compute_corr_volume method --- torchvision/models/optical_flow/_raft/raft.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index 487d5c7a38c..033e6f32bef 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -280,17 +280,8 @@ def build_pyramid(self, fmap1, fmap2): to build the correlation pyramid. """ - def compute_corr_volume(fmap1, fmap2): - batch_size, num_channels, h, w = fmap1.shape - fmap1 = fmap1.view(batch_size, num_channels, h * w) - fmap2 = fmap2.view(batch_size, num_channels, h * w) - - corr = torch.matmul(fmap1.transpose(1, 2), fmap2) - corr = corr.view(batch_size, h, w, 1, h, w) - return corr / torch.sqrt(torch.tensor(num_channels)) - torch._assert(fmap1.shape == fmap2.shape, "Input feature maps should have the same shapes") - corr_volume = compute_corr_volume(fmap1, fmap2) + corr_volume = self._compute_corr_volume(fmap1, fmap2) batch_size, h, w, num_channels, _, _ = corr_volume.shape # _, _ = h, w corr_volume = corr_volume.reshape(batch_size * h * w, num_channels, h, w) @@ -329,6 +320,16 @@ def index_pyramid(self, centroids_coords): return corr_features + def _compute_corr_volume(self, fmap1, fmap2): + batch_size, num_channels, h, w = fmap1.shape + fmap1 = fmap1.view(batch_size, num_channels, h * w) + fmap2 = fmap2.view(batch_size, num_channels, h * w) + + corr = torch.matmul(fmap1.transpose(1, 2), fmap2) + corr = corr.view(batch_size, h, w, 1, h, w) + return corr / torch.sqrt(torch.tensor(num_channels)) + + class RAFT(nn.Module): def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block, mask_predictor=None): From 8e444fd3da02dd9f978d42cc95bc9df6419dbbaf Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 13:35:07 +0000 Subject: [PATCH 08/16] Use F.relu instead of torch.relu --- torchvision/models/optical_flow/_raft/raft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/_raft/raft.py index 033e6f32bef..e865dc452b0 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/_raft/raft.py @@ -372,7 +372,7 @@ def forward(self, image1, image2, *, num_flow_updates=12): ) hidden_state, context = torch.split(context_out, [hidden_state_size, out_channels_context], dim=1) hidden_state = torch.tanh(hidden_state) - context = torch.relu(context) + context = F.relu(context) coords0 = make_coords_grid(batch_size, h // 8, w // 8).cuda() coords1 = make_coords_grid(batch_size, h // 8, w // 8).cuda() From c05b8720408e5763a909f0a296dbcf6b8830cf70 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 13:49:12 +0000 Subject: [PATCH 09/16] Re-organize file structure and rename raft() into raft_large --- torchvision/models/optical_flow/__init__.py | 2 +- .../models/optical_flow/{_raft/utils.py => _utils.py} | 0 torchvision/models/optical_flow/{_raft => }/raft.py | 11 +++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) rename torchvision/models/optical_flow/{_raft/utils.py => _utils.py} (100%) rename torchvision/models/optical_flow/{_raft => }/raft.py (99%) diff --git a/torchvision/models/optical_flow/__init__.py b/torchvision/models/optical_flow/__init__.py index 72380e46fef..9dd32f25dec 100644 --- a/torchvision/models/optical_flow/__init__.py +++ b/torchvision/models/optical_flow/__init__.py @@ -1 +1 @@ -from ._raft.raft import RAFT, raft, raft_small +from .raft import RAFT, raft_large, raft_small diff --git a/torchvision/models/optical_flow/_raft/utils.py b/torchvision/models/optical_flow/_utils.py similarity index 100% rename from torchvision/models/optical_flow/_raft/utils.py rename to torchvision/models/optical_flow/_utils.py diff --git a/torchvision/models/optical_flow/_raft/raft.py b/torchvision/models/optical_flow/raft.py similarity index 99% rename from torchvision/models/optical_flow/_raft/raft.py rename to torchvision/models/optical_flow/raft.py index e865dc452b0..b1a4132d785 100644 --- a/torchvision/models/optical_flow/_raft/raft.py +++ b/torchvision/models/optical_flow/raft.py @@ -5,7 +5,14 @@ from torch.nn.modules.instancenorm import InstanceNorm2d from torchvision.ops import ConvNormActivation -from .utils import grid_sample, make_coords_grid, upsample_flow +from ._utils import grid_sample, make_coords_grid, upsample_flow + + +__all__ = ( + "RAFT", + "raft_large", + "raft_small", +) class ResidualBlock(nn.Module): @@ -470,7 +477,7 @@ def _raft( ) -def raft(*, pretrained=False, progress=True, **kwargs): +def raft_large(*, pretrained=False, progress=True, **kwargs): if pretrained: raise NotImplementedError("Pretrained weights aren't available yet") From 6678cff37a2c9dcb9d4716375b24c32b2485f7ea Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 16:20:09 +0000 Subject: [PATCH 10/16] Added support for torchscript, and added expect test --- .../ModelTester.test_raft_large_expect.pkl | Bin 0 -> 46827 bytes .../ModelTester.test_raft_small_expect.pkl | Bin 0 -> 46827 bytes test/test_models.py | 31 +++++++++++++++++- torchvision/models/optical_flow/_utils.py | 11 ++++--- torchvision/models/optical_flow/raft.py | 26 ++++++++++----- 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 test/expect/ModelTester.test_raft_large_expect.pkl create mode 100644 test/expect/ModelTester.test_raft_small_expect.pkl diff --git a/test/expect/ModelTester.test_raft_large_expect.pkl b/test/expect/ModelTester.test_raft_large_expect.pkl new file mode 100644 index 0000000000000000000000000000000000000000..a6aad285f59c5159ce39affbddd71a4d49c72ba2 GIT binary patch literal 46827 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 literal 0 HcmV?d00001 diff --git a/test/expect/ModelTester.test_raft_small_expect.pkl b/test/expect/ModelTester.test_raft_small_expect.pkl new file mode 100644 index 0000000000000000000000000000000000000000..bd0ee65add948ac71f29e43f17e52c5f16f258e5 GIT binary patch literal 46827 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%0ASTsJ7GGbr7oGlB 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 literal 0 HcmV?d00001 diff --git a/test/test_models.py b/test/test_models.py index b5500ef08b4..c622ea59e11 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -110,7 +110,7 @@ def _assert_expected(output, name, prec): MAX_PICKLE_SIZE = 50 * 1000 # 50 KB binary_size = os.path.getsize(expected_file) if binary_size > MAX_PICKLE_SIZE: - raise RuntimeError(f"The output for {filename}, is larger than 50kb") + raise RuntimeError(f"The output for {filename}, is larger than 50kb - got {binary_size}kb") else: expected = torch.load(expected_file) rtol = atol = prec @@ -818,5 +818,34 @@ def test_detection_model_trainable_backbone_layers(model_fn, disable_weight_load assert n_trainable_params == _model_tests_values[model_name]["n_trn_params_per_layer"] +from torchvision.models.optical_flow import raft_large, raft_small + + +@needs_cuda +@pytest.mark.parametrize("model_builder", (raft_large, raft_small)) +@pytest.mark.parametrize("scripted", (False, True)) +def test_raft(model_builder, scripted): + + torch.manual_seed(0) + + # We need very small images, otherwise the pickle size would exceed the 50KB + # As a resut we need to override the correlation pyramid to not downsample + # too much, otherwise we would get nan values (effective H and W would be + # reduced to 1) + corr_block = models.optical_flow.raft.CorrBlock(num_levels=2, radius=2) + + model = model_builder(corr_block=corr_block).eval().to("cuda") + if scripted: + model = torch.jit.script(model) + + bs = 1 + img1 = torch.rand(bs, 3, 80, 72).cuda() + img2 = torch.rand(bs, 3, 80, 72).cuda() + + preds = model(img1, img2) + flow_pred = preds[-1] + _assert_expected(flow_pred, name=model_builder.__name__, prec=1e-6) + + if __name__ == "__main__": pytest.main([__file__]) diff --git a/torchvision/models/optical_flow/_utils.py b/torchvision/models/optical_flow/_utils.py index 1c3d1703133..693b3f14009 100644 --- a/torchvision/models/optical_flow/_utils.py +++ b/torchvision/models/optical_flow/_utils.py @@ -1,8 +1,11 @@ +from typing import Optional + import torch import torch.nn.functional as F +from torch import Tensor -def grid_sample(img, absolute_grid, *args, **kwargs): +def grid_sample(img: Tensor, absolute_grid: Tensor, mode: str = "bilinear", align_corners: Optional[bool] = None): """Same as torch's grid_sample, with absolute pixel coordinates instead of normalized coordinates.""" h, w = img.shape[-2:] @@ -11,16 +14,16 @@ def grid_sample(img, absolute_grid, *args, **kwargs): ygrid = 2 * ygrid / (h - 1) - 1 normalized_grid = torch.cat([xgrid, ygrid], dim=-1) - return F.grid_sample(img, normalized_grid, *args, **kwargs) + return F.grid_sample(img, normalized_grid, mode=mode, align_corners=align_corners) -def make_coords_grid(batch_size, h, w): +def make_coords_grid(batch_size: int, h: int, w: int): coords = torch.meshgrid(torch.arange(h), torch.arange(w), indexing="ij") coords = torch.stack(coords[::-1], dim=0).float() return coords[None].repeat(batch_size, 1, 1, 1) -def upsample_flow(flow, up_mask=None): +def upsample_flow(flow, up_mask: Optional[Tensor] = None): """Upsample flow by a factor of 8. If up_mask is None we just interpolate. diff --git a/torchvision/models/optical_flow/raft.py b/torchvision/models/optical_flow/raft.py index b1a4132d785..34fcbad26a4 100644 --- a/torchvision/models/optical_flow/raft.py +++ b/torchvision/models/optical_flow/raft.py @@ -1,6 +1,9 @@ +from typing import List + import torch import torch.nn as nn import torch.nn.functional as F +from torch import Tensor from torch.nn.modules.batchnorm import BatchNorm2d from torch.nn.modules.instancenorm import InstanceNorm2d from torchvision.ops import ConvNormActivation @@ -194,6 +197,11 @@ def forward(self, h, x): return h +def _pass_through_h(h, _): + # Declared here for torchscript + return h + + class RecurrentBlock(nn.Module): def __init__(self, *, input_size, hidden_size, kernel_size=((1, 5), (5, 1)), padding=((0, 2), (2, 0))): super().__init__() @@ -209,7 +217,7 @@ def __init__(self, *, input_size, hidden_size, kernel_size=((1, 5), (5, 1)), pad input_size=input_size, hidden_size=hidden_size, kernel_size=kernel_size[1], padding=padding[1] ) else: - self.convgru2 = lambda h, _: h # identity + self.convgru2 = _pass_through_h self.hidden_size = hidden_size @@ -268,11 +276,14 @@ def forward(self, x): return self.multiplier * x -class CorrBlock: - def __init__(self, *, num_levels=4, radius=4): +class CorrBlock(nn.Module): + def __init__(self, *, num_levels: int = 4, radius: int = 4): + super().__init__() self.num_levels = num_levels self.radius = radius + self.corr_pyramid: List[Tensor] = [torch.tensor(0)] # useless, but torchscript is otherwise confused :') + # The neighborhood of a centroid pixel x' is {x' + delta, ||delta||_inf <= radius} # so it's a square surrounding x', and its sides have a length of 2 * radius + 1 # The paper claims that it's ||.||_1 instead of ||.||_inf but it's a typo: @@ -302,7 +313,7 @@ def index_pyramid(self, centroids_coords): neighborhood_side_len = 2 * self.radius + 1 # see note in __init__ about out_channels di = torch.linspace(-self.radius, self.radius, neighborhood_side_len) dj = torch.linspace(-self.radius, self.radius, neighborhood_side_len) - delta = torch.stack(torch.meshgrid(di, dj, indexing="ij"), axis=-1).to(centroids_coords.device) + delta = torch.stack(torch.meshgrid(di, dj, indexing="ij"), dim=-1).to(centroids_coords.device) delta = delta.view(1, neighborhood_side_len, neighborhood_side_len, 2) batch_size, _, h, w = centroids_coords.shape # _ = 2 @@ -337,7 +348,6 @@ def _compute_corr_volume(self, fmap1, fmap2): return corr / torch.sqrt(torch.tensor(num_channels)) - class RAFT(nn.Module): def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block, mask_predictor=None): super().__init__() @@ -352,7 +362,7 @@ def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block if not hasattr(self.update_block, "hidden_state_size"): raise ValueError("The update_block parameter should expose a 'hidden_state_size' attribute.") - def forward(self, image1, image2, *, num_flow_updates=12): + def forward(self, image1, image2, num_flow_updates: int = 12): batch_size, _, h, w = image1.shape torch._assert((h, w) == image2.shape[-2:], "input images should have the same shape") @@ -360,12 +370,12 @@ def forward(self, image1, image2, *, num_flow_updates=12): fmaps = self.feature_encoder(torch.cat([image1, image2], dim=0)) fmap1, fmap2 = torch.chunk(fmaps, chunks=2, dim=0) - torch._assert(fmap1.shape[-2:] == (h / 8, w / 8), "The feature encoder should downsample H and W by 8") + torch._assert(fmap1.shape[-2:] == (h // 8, w // 8), "The feature encoder should downsample H and W by 8") self.corr_block.build_pyramid(fmap1, fmap2) context_out = self.context_encoder(image1) - torch._assert(context_out.shape[-2:] == (h / 8, w / 8), "The context encoder should downsample H and W by 8") + torch._assert(context_out.shape[-2:] == (h // 8, w // 8), "The context encoder should downsample H and W by 8") # As in the original paper, the actual output of the context encoder is split in 2 parts: # - one part is used to initialize the hidden state of the reccurent units of the update block From b1375ab90da5014cbeeb83bcb66ab4f00ff521bc Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 16:21:25 +0000 Subject: [PATCH 11/16] avoid import --- test/test_models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_models.py b/test/test_models.py index c622ea59e11..17644699717 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -818,11 +818,8 @@ def test_detection_model_trainable_backbone_layers(model_fn, disable_weight_load assert n_trainable_params == _model_tests_values[model_name]["n_trn_params_per_layer"] -from torchvision.models.optical_flow import raft_large, raft_small - - @needs_cuda -@pytest.mark.parametrize("model_builder", (raft_large, raft_small)) +@pytest.mark.parametrize("model_builder", (models.optical_flow.raft_large, models.optical_flow.raft_small)) @pytest.mark.parametrize("scripted", (False, True)) def test_raft(model_builder, scripted): From 6f9bc8550c9e8a42acb6ab590a3affabd160b152 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Thu, 2 Dec 2021 16:22:50 +0000 Subject: [PATCH 12/16] ValueError instead of NotImplementedError --- torchvision/models/optical_flow/raft.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/torchvision/models/optical_flow/raft.py b/torchvision/models/optical_flow/raft.py index 34fcbad26a4..3ccf9e0e804 100644 --- a/torchvision/models/optical_flow/raft.py +++ b/torchvision/models/optical_flow/raft.py @@ -490,7 +490,7 @@ def _raft( def raft_large(*, pretrained=False, progress=True, **kwargs): if pretrained: - raise NotImplementedError("Pretrained weights aren't available yet") + raise ValueError("Pretrained weights aren't available yet") return _raft( # Feature encoder @@ -523,7 +523,7 @@ def raft_large(*, pretrained=False, progress=True, **kwargs): def raft_small(*, pretrained=False, progress=True, **kwargs): if pretrained: - raise NotImplementedError("Pretrained weights aren't available yet") + raise ValueError("Pretrained weights aren't available yet") return _raft( # Feature encoder From f655ec6e45b637c6bc251035dcdad865d82ee163 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Fri, 3 Dec 2021 11:49:56 +0000 Subject: [PATCH 13/16] Allow higher tolerance for expectTest --- test/test_models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_models.py b/test/test_models.py index 17644699717..2e0ed783849 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -93,7 +93,7 @@ def _get_expected_file(name=None): return expected_file -def _assert_expected(output, name, prec): +def _assert_expected(output, name, prec=None, atol=None, rtol=None): """Test that a python value matches the recorded contents of a file based on a "check" name. The value must be pickable with `torch.save`. This file @@ -113,7 +113,8 @@ def _assert_expected(output, name, prec): raise RuntimeError(f"The output for {filename}, is larger than 50kb - got {binary_size}kb") else: expected = torch.load(expected_file) - rtol = atol = prec + rtol = rtol or prec # keeping prec param for legacy reason, but could be removed ideally + atol = atol or prec torch.testing.assert_close(output, expected, rtol=rtol, atol=atol, check_dtype=False) @@ -841,7 +842,9 @@ def test_raft(model_builder, scripted): preds = model(img1, img2) flow_pred = preds[-1] - _assert_expected(flow_pred, name=model_builder.__name__, prec=1e-6) + # Tolerance is fairly high, but there are 2 * H * W outputs to check + # The .pkl were generated on the AWS cluter, on the CI it looks like the resuts are slightly different + _assert_expected(flow_pred, name=model_builder.__name__, atol=1e-2, rtol=1) if __name__ == "__main__": From a85b6b41158406fd4075408e364c5f355e8e06a3 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Fri, 3 Dec 2021 12:48:24 +0000 Subject: [PATCH 14/16] The docssssssss --- docs/source/models.rst | 15 ++- torchvision/models/optical_flow/raft.py | 116 +++++++++++++++++++++++- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/docs/source/models.rst b/docs/source/models.rst index dbb1400e11e..1ff6f18c126 100644 --- a/docs/source/models.rst +++ b/docs/source/models.rst @@ -7,7 +7,7 @@ Models and pre-trained weights The ``torchvision.models`` subpackage contains definitions of models for addressing different tasks, including: image classification, pixelwise semantic segmentation, object detection, instance segmentation, person -keypoint detection and video classification. +keypoint detection, video classification, and optical flow. .. note :: Backward compatibility is guaranteed for loading a serialized @@ -798,3 +798,16 @@ ResNet (2+1)D :template: function.rst torchvision.models.video.r2plus1d_18 + +Optical flow +============ + +Raft +---- + +.. autosummary:: + :toctree: generated/ + :template: function.rst + + torchvision.models.optical_flow.raft_large + torchvision.models.optical_flow.raft_small \ No newline at end of file diff --git a/torchvision/models/optical_flow/raft.py b/torchvision/models/optical_flow/raft.py index 3ccf9e0e804..0964423ba42 100644 --- a/torchvision/models/optical_flow/raft.py +++ b/torchvision/models/optical_flow/raft.py @@ -19,7 +19,8 @@ class ResidualBlock(nn.Module): - # This is pretty similar to resnet.BasicBlock except for one call to relu, and the bias terms + """Slightly modified Residual block with extra relu and biases.""" + def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): super().__init__() @@ -62,6 +63,8 @@ def forward(self, x): class BottleneckBlock(nn.Module): + """Slightly modified BottleNeck block (extra relu and biases)""" + def __init__(self, in_channels, out_channels, *, norm_layer, stride=1): super(BottleneckBlock, self).__init__() @@ -102,6 +105,11 @@ def forward(self, x): class FeatureEncoder(nn.Module): + """The feature encoder, used both as the actual feature encoder, and as the context encoder. + + It must downsample its input by 8. + """ + def __init__(self, *, block=ResidualBlock, layers=(64, 64, 96, 128, 256), norm_layer=nn.BatchNorm2d): super().__init__() @@ -146,6 +154,11 @@ def forward(self, x): class MotionEncoder(nn.Module): + """The motion encoder, part of the update block. + + Takes the current predicted flow and the correlation features as input and returns an encoded version of these. + """ + def __init__(self, *, in_channels_corr, corr_layers=(256, 192), flow_layers=(128, 64), out_channels=128): super().__init__() @@ -182,6 +195,8 @@ def forward(self, flow, corr_features): class ConvGRU(nn.Module): + """Convolutional Gru unit.""" + def __init__(self, *, input_size, hidden_size, kernel_size, padding): super().__init__() self.convz = nn.Conv2d(hidden_size + input_size, hidden_size, kernel_size=kernel_size, padding=padding) @@ -203,6 +218,12 @@ def _pass_through_h(h, _): class RecurrentBlock(nn.Module): + """Recurrent block, part of the update block. + + Takes the current hidden state and the concatenation of (motion encoder output, context) as input. + Returns an updated hidden state. + """ + def __init__(self, *, input_size, hidden_size, kernel_size=((1, 5), (5, 1)), padding=((0, 2), (2, 0))): super().__init__() @@ -228,6 +249,11 @@ def forward(self, h, x): class FlowHead(nn.Module): + """Flow head, part of the update block. + + Takes the hidden state of the recurrent unit as input, and outputs the predicted "delta flow". + """ + def __init__(self, *, in_channels, hidden_size): super().__init__() self.conv1 = nn.Conv2d(in_channels, hidden_size, 3, padding=1) @@ -239,6 +265,11 @@ def forward(self, x): class UpdateBlock(nn.Module): + """The update block which contains the motion encoder, the recurrent block, and the flow head. + + It must expose a ``hidden_state_size`` attribute which is the hidden state size of its recurrent block. + """ + def __init__(self, *, motion_encoder, recurrent_block, flow_head): super().__init__() self.motion_encoder = motion_encoder @@ -257,6 +288,12 @@ def forward(self, hidden_state, context, corr_features, flow): class MaskPredictor(nn.Module): + """Mask predictor to be used when upsampling the predicted flow. + + It takes the hidden state of the recurrent unit as input and outputs the mask. + This is not used in the raft-small model. + """ + def __init__(self, *, in_channels, hidden_size, multiplier=0.25): super().__init__() self.convrelu = ConvNormActivation(in_channels, hidden_size, norm_layer=None, kernel_size=3) @@ -277,6 +314,15 @@ def forward(self, x): class CorrBlock(nn.Module): + """The correlation block. + + Creates a correlation pyramid with ``num_levels`` levels from the outputs of the feature encoder, + and then indexes from this pyramid to create correlation features. + The "indexing" of a given centroid pixel x' is done by concatenating its surrounding neighbors that + are within a ``radius``, according to the infinity norm (see paper section 3.2). + Note: typo in the paper, it should be infinity norm, not 1-norm. + """ + def __init__(self, *, num_levels: int = 4, radius: int = 4): super().__init__() self.num_levels = num_levels @@ -350,6 +396,41 @@ def _compute_corr_volume(self, fmap1, fmap2): class RAFT(nn.Module): def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block, mask_predictor=None): + """RAFT model from + `RAFT: Recurrent All Pairs Field Transforms for Optical Flow `_. + + args: + feature_encoder (nn.Module): The feature encoder. It must downsample the input by 8. + Its input is the concatenation of ``image1`` and ``image2``. + context_encoder (nn.Module): The context encoder. It must downsample the input by 8. + Its input is ``image1``. As in the original implementation, its output will be split into 2 parts: + + - one part will be used as the actual "context", passed to the recurrent unit of the ``update_block`` + - one part will be used to initialize the hidden state of the of the recurrent unit of + the ``update_block`` + + These 2 parts are split according to the ``hidden_state_size`` of the ``update_block``, so the output + of the ``context_encoder`` must be strictly greater than ``hidden_state_size``. + + clorr_block (nn.Module): The correlation block, which creates a correlation pyramid from the output of the + ``feature_encoder``, and then indexes from this pyramid to create correlation features. It must expose + 2 methods: + + - a ``build_pyramid`` method that takes ``feature_map_1`` and ``feature_map_2`` as input (these are the + output of the ``feature_encoder``). + - a ``index_pyramid`` method that takes the coordinates of the centroid pixels as input, and returns + the correlation features. See paper section 3.2. + + It must expose an ``out_channels`` attribute. + + update_block (nn.Module): The update block, which contains the motion encoder, the recurrent unit, and the + flow head. It takes as input the hidden state of its recurrent unit, the context, the correlation + features, and the current predicted flow. It outputs an updated hidden state, and the ``delta_flow`` + prediction (see paper appendix A). It must expose a ``hidden_state_size`` attribute. + maks_predictor (nn.Module, optional): Predicts the mask that will be used to upsample the predicted flow. + The output channel must be 8 * 8 * 9 - see paper section 3.3, and Appendix B. + If ``None`` (default), the flow is upsampled using interpolation. + """ super().__init__() self.feature_encoder = feature_encoder @@ -378,13 +459,13 @@ def forward(self, image1, image2, num_flow_updates: int = 12): torch._assert(context_out.shape[-2:] == (h // 8, w // 8), "The context encoder should downsample H and W by 8") # As in the original paper, the actual output of the context encoder is split in 2 parts: - # - one part is used to initialize the hidden state of the reccurent units of the update block + # - one part is used to initialize the hidden state of the recurent units of the update block # - the rest is the "actual" context. hidden_state_size = self.update_block.hidden_state_size out_channels_context = context_out.shape[1] - hidden_state_size torch._assert( out_channels_context > 0, - f"The context encoder outputs {context_out.shape[1]} channels, but it should have at least " + f"The context encoder outputs {context_out.shape[1]} channels, but it should have at strictly more than" f"hidden_state={hidden_state_size} channels", ) hidden_state, context = torch.split(context_out, [hidden_state_size, out_channels_context], dim=1) @@ -488,6 +569,18 @@ def _raft( def raft_large(*, pretrained=False, progress=True, **kwargs): + """RAFT model from + `RAFT: Recurrent All Pairs Field Transforms for Optical Flow `_. + + Args: + pretrained (bool): TODO not implemented yet + progress (bool): If True, displays a progress bar of the download to stderr + kwargs (dict): Parameters that will be passed to the :class:`~torchvision.models.optical_flow.RAFT` class + to override any default. + + Returns: + nn.Module: The model. + """ if pretrained: raise ValueError("Pretrained weights aren't available yet") @@ -508,7 +601,7 @@ def raft_large(*, pretrained=False, progress=True, **kwargs): motion_encoder_corr_layers=(256, 192), motion_encoder_flow_layers=(128, 64), motion_encoder_out_channels=128, - # Reccurrent block + # Recurrent block recurrent_block_hidden_state_size=128, recurrent_block_kernel_size=((1, 5), (5, 1)), recurrent_block_padding=((0, 2), (2, 0)), @@ -521,6 +614,19 @@ def raft_large(*, pretrained=False, progress=True, **kwargs): def raft_small(*, pretrained=False, progress=True, **kwargs): + """RAFT "small" model from + `RAFT: Recurrent All Pairs Field Transforms for Optical Flow `_. + + Args: + pretrained (bool): TODO not implemented yet + progress (bool): If True, displays a progress bar of the download to stderr + kwargs (dict): Parameters that will be passed to the :class:`~torchvision.models.optical_flow.RAFT` class + to override any default. + + Returns: + nn.Module: The model. + + """ if pretrained: raise ValueError("Pretrained weights aren't available yet") @@ -541,7 +647,7 @@ def raft_small(*, pretrained=False, progress=True, **kwargs): motion_encoder_corr_layers=(96,), motion_encoder_flow_layers=(64, 32), motion_encoder_out_channels=82, - # Reccurrent block + # Recurrent block recurrent_block_hidden_state_size=96, recurrent_block_kernel_size=(3,), recurrent_block_padding=(1,), From c891a93ce3eca1d68700cd9af6f1f3c4d6133a81 Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Fri, 3 Dec 2021 13:27:41 +0000 Subject: [PATCH 15/16] fix hooks --- docs/source/models.rst | 3 ++- torchvision/models/optical_flow/raft.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/models.rst b/docs/source/models.rst index 1ff6f18c126..03641f65383 100644 --- a/docs/source/models.rst +++ b/docs/source/models.rst @@ -810,4 +810,5 @@ Raft :template: function.rst torchvision.models.optical_flow.raft_large - torchvision.models.optical_flow.raft_small \ No newline at end of file + torchvision.models.optical_flow.raft_small + \ No newline at end of file diff --git a/torchvision/models/optical_flow/raft.py b/torchvision/models/optical_flow/raft.py index 0964423ba42..02705a7ebdb 100644 --- a/torchvision/models/optical_flow/raft.py +++ b/torchvision/models/optical_flow/raft.py @@ -412,7 +412,7 @@ def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block These 2 parts are split according to the ``hidden_state_size`` of the ``update_block``, so the output of the ``context_encoder`` must be strictly greater than ``hidden_state_size``. - clorr_block (nn.Module): The correlation block, which creates a correlation pyramid from the output of the + corr_block (nn.Module): The correlation block, which creates a correlation pyramid from the output of the ``feature_encoder``, and then indexes from this pyramid to create correlation features. It must expose 2 methods: @@ -427,7 +427,7 @@ def __init__(self, *, feature_encoder, context_encoder, corr_block, update_block flow head. It takes as input the hidden state of its recurrent unit, the context, the correlation features, and the current predicted flow. It outputs an updated hidden state, and the ``delta_flow`` prediction (see paper appendix A). It must expose a ``hidden_state_size`` attribute. - maks_predictor (nn.Module, optional): Predicts the mask that will be used to upsample the predicted flow. + mask_predictor (nn.Module, optional): Predicts the mask that will be used to upsample the predicted flow. The output channel must be 8 * 8 * 9 - see paper section 3.3, and Appendix B. If ``None`` (default), the flow is upsampled using interpolation. """ From 9ae9e38a9dfcd90c2b004f6f394d4ac09da02fad Mon Sep 17 00:00:00 2001 From: Nicolas Hug Date: Fri, 3 Dec 2021 13:38:25 +0000 Subject: [PATCH 16/16] Fix hooks -- remastered --- docs/source/models.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/models.rst b/docs/source/models.rst index 03641f65383..ee8503a0857 100644 --- a/docs/source/models.rst +++ b/docs/source/models.rst @@ -811,4 +811,3 @@ Raft torchvision.models.optical_flow.raft_large torchvision.models.optical_flow.raft_small - \ No newline at end of file