Source code for pimm.models.sparse_unet.spconv_unet_v1m3_pdnorm

"""
SparseUNet V1M3

Enable Prompt-Driven Normalization for Point Prompt Training

Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
Please cite our work if the code is helpful to you.
"""

from functools import partial
from collections import OrderedDict

import torch
import torch.nn as nn

import spconv.pytorch as spconv
from torch_geometric.utils import scatter

from timm.layers import trunc_normal_

from pimm.models.builder import MODELS
from pimm.models.utils import offset2batch


class PDBatchNorm(torch.nn.Module):
    def __init__(
        self,
        num_features,
        context_channels=256,
        eps=1e-3,
        momentum=0.01,
        conditions=("ScanNet", "S3DIS", "Structured3D"),
        decouple=True,
        adaptive=False,
        affine=True,
    ):
        super().__init__()
        self.conditions = conditions
        self.decouple = decouple
        self.adaptive = adaptive
        self.affine = affine
        if self.decouple:
            self.bns = nn.ModuleList(
                [
                    nn.BatchNorm1d(
                        num_features=num_features,
                        eps=eps,
                        momentum=momentum,
                        affine=affine,
                    )
                    for _ in conditions
                ]
            )
        else:
            self.bn = nn.BatchNorm1d(
                num_features=num_features, eps=eps, momentum=momentum, affine=affine
            )
        if self.adaptive:
            self.modulation = nn.Sequential(
                nn.SiLU(), nn.Linear(context_channels, 2 * num_features, bias=True)
            )

    def forward(self, feat, condition=None, context=None):
        if self.decouple:
            assert condition in self.conditions
            bn = self.bns[self.conditions.index(condition)]
        else:
            bn = self.bn
        feat = bn(feat)
        if self.adaptive:
            assert context is not None
            shift, scale = self.modulation(context).chunk(2, dim=1)
            feat = feat * (1.0 + scale) + shift
        return feat


class BasicBlock(spconv.SparseModule):
    expansion = 1

    def __init__(
        self,
        in_channels,
        embed_channels,
        stride=1,
        norm_fn=None,
        indice_key=None,
        bias=False,
    ):
        super().__init__()

        assert norm_fn is not None

        self.in_channels = in_channels
        self.embed_channels = embed_channels
        if in_channels == embed_channels:
            self.proj = spconv.SparseSequential(nn.Identity())
        else:
            # TODO remove norm after project
            self.proj_conv = spconv.SubMConv3d(
                in_channels, embed_channels, kernel_size=1, bias=False
            )
            self.proj_norm = norm_fn(embed_channels)

        self.conv1 = spconv.SubMConv3d(
            in_channels,
            embed_channels,
            kernel_size=3,
            stride=stride,
            padding=1,
            bias=bias,
            indice_key=indice_key,
        )
        self.bn1 = norm_fn(embed_channels)
        self.relu = nn.ReLU()
        self.conv2 = spconv.SubMConv3d(
            embed_channels,
            embed_channels,
            kernel_size=3,
            stride=stride,
            padding=1,
            bias=bias,
            indice_key=indice_key,
        )
        self.bn2 = norm_fn(embed_channels)
        self.stride = stride

    def forward(self, x):
        x, condition, context = x
        residual = x

        out = self.conv1(x)
        out = out.replace_feature(self.bn1(out.features, condition, context))
        out = out.replace_feature(self.relu(out.features))

        out = self.conv2(out)
        out = out.replace_feature(self.bn2(out.features, condition, context))

        if self.in_channels == self.embed_channels:
            residual = self.proj(residual)
        else:
            residual = residual.replace_feature(
                self.proj_norm(self.proj_conv(residual).features, condition, context)
            )
        out = out.replace_feature(out.features + residual.features)
        out = out.replace_feature(self.relu(out.features))
        return out, condition, context


class SPConvDown(nn.Module):
    def __init__(
        self,
        in_channels,
        out_channels,
        indice_key,
        kernel_size=2,
        bias=False,
        norm_fn=None,
    ):
        super().__init__()
        self.conv = spconv.SparseConv3d(
            in_channels,
            out_channels,
            kernel_size=kernel_size,
            stride=kernel_size,
            bias=bias,
            indice_key=indice_key,
        )
        self.bn = norm_fn(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x):
        x, condition, context = x
        out = self.conv(x)
        out = out.replace_feature(self.bn(out.features, condition, context))
        out = out.replace_feature(self.relu(out.features))
        return out


class SPConvUp(nn.Module):
    def __init__(
        self,
        in_channels,
        out_channels,
        indice_key,
        kernel_size=2,
        bias=False,
        norm_fn=None,
    ):
        super().__init__()
        self.conv = spconv.SparseInverseConv3d(
            in_channels,
            out_channels,
            kernel_size=kernel_size,
            bias=bias,
            indice_key=indice_key,
        )
        self.bn = norm_fn(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x):
        x, condition, context = x
        out = self.conv(x)
        out = out.replace_feature(self.bn(out.features, condition, context))
        out = out.replace_feature(self.relu(out.features))
        return out


class SPConvPatchEmbedding(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=5, norm_fn=None):
        super().__init__()
        self.conv = spconv.SubMConv3d(
            in_channels,
            out_channels,
            kernel_size=kernel_size,
            padding=1,
            bias=False,
            indice_key="stem",
        )
        self.bn = norm_fn(out_channels)
        self.relu = nn.ReLU()

    def forward(self, x):
        x, condition, context = x
        out = self.conv(x)
        out = out.replace_feature(self.bn(out.features, condition, context))
        out = out.replace_feature(self.relu(out.features))
        return out


[docs] @MODELS.register_module("SpUNet-v1m3") class SpUNetBase(nn.Module): def __init__( self, in_channels, num_classes=0, base_channels=32, context_channels=256, channels=(32, 64, 128, 256, 256, 128, 96, 96), layers=(2, 3, 4, 6, 2, 2, 2, 2), cls_mode=False, conditions=("ScanNet", "S3DIS", "Structured3D"), zero_init=True, norm_decouple=True, norm_adaptive=True, norm_affine=False, ): super().__init__() assert len(layers) % 2 == 0 assert len(layers) == len(channels) self.in_channels = in_channels self.num_classes = num_classes self.base_channels = base_channels self.channels = channels self.layers = layers self.num_stages = len(layers) // 2 self.cls_mode = cls_mode self.conditions = conditions self.zero_init = zero_init norm_fn = partial( PDBatchNorm, eps=1e-3, momentum=0.01, conditions=conditions, context_channels=context_channels, decouple=norm_decouple, adaptive=norm_adaptive, affine=norm_affine, ) block = BasicBlock self.conv_input = SPConvPatchEmbedding( in_channels, base_channels, kernel_size=5, norm_fn=norm_fn ) enc_channels = base_channels dec_channels = channels[-1] self.down = nn.ModuleList() self.up = nn.ModuleList() self.enc = nn.ModuleList() self.dec = nn.ModuleList() if not self.cls_mode else None for s in range(self.num_stages): # encode num_stages self.down.append( SPConvDown( enc_channels, channels[s], kernel_size=2, bias=False, indice_key=f"spconv{s + 1}", norm_fn=norm_fn, ) ) self.enc.append( spconv.SparseSequential( OrderedDict( [ # (f"block{i}", block(enc_channels, channels[s], norm_fn=norm_fn, indice_key=f"subm{s + 1}")) # if i == 0 else ( f"block{i}", block( channels[s], channels[s], norm_fn=norm_fn, indice_key=f"subm{s + 1}", ), ) for i in range(layers[s]) ] ) ) ) if not self.cls_mode: # decode num_stages self.up.append( SPConvUp( channels[len(channels) - s - 2], dec_channels, kernel_size=2, bias=False, indice_key=f"spconv{s + 1}", norm_fn=norm_fn, ) ) self.dec.append( spconv.SparseSequential( OrderedDict( [ ( ( f"block{i}", block( dec_channels + enc_channels, dec_channels, norm_fn=norm_fn, indice_key=f"subm{s}", ), ) if i == 0 else ( f"block{i}", block( dec_channels, dec_channels, norm_fn=norm_fn, indice_key=f"subm{s}", ), ) ) for i in range(layers[len(channels) - s - 1]) ] ) ) ) enc_channels = channels[s] dec_channels = channels[len(channels) - s - 2] final_in_channels = ( channels[-1] if not self.cls_mode else channels[self.num_stages - 1] ) self.final = ( spconv.SubMConv3d( final_in_channels, num_classes, kernel_size=1, padding=1, bias=True ) if num_classes > 0 else spconv.Identity() ) self.apply(self._init_weights) def _init_weights(self, m): if isinstance(m, nn.Linear): trunc_normal_(m.weight, std=0.02) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, spconv.SubMConv3d): trunc_normal_(m.weight, std=0.02) if m.bias is not None: nn.init.constant_(m.bias, 0) elif isinstance(m, nn.BatchNorm1d): if m.affine: nn.init.constant_(m.bias, 0) nn.init.constant_(m.weight, 1.0) elif isinstance(m, PDBatchNorm): if self.zero_init: nn.init.constant_(m.modulation[-1].weight, 0) nn.init.constant_(m.modulation[-1].bias, 0)
[docs] def forward(self, input_dict): grid_coord = input_dict["grid_coord"] feat = input_dict["feat"] offset = input_dict["offset"] condition = input_dict["condition"][0] context = input_dict["context"] if "context" in input_dict.keys() else None batch = offset2batch(offset) sparse_shape = torch.add(torch.max(grid_coord, dim=0).values, 96).tolist() x = spconv.SparseConvTensor( features=feat, indices=torch.cat( [batch.unsqueeze(-1).int(), grid_coord.int()], dim=1 ).contiguous(), spatial_shape=sparse_shape, batch_size=batch[-1].tolist() + 1, ) x = self.conv_input([x, condition, context]) skips = [x] # enc forward for s in range(self.num_stages): x = self.down[s]([x, condition, context]) x, _, _ = self.enc[s]([x, condition, context]) skips.append(x) x = skips.pop(-1) if not self.cls_mode: # dec forward for s in reversed(range(self.num_stages)): x = self.up[s]([x, condition, context]) skip = skips.pop(-1) x = x.replace_feature(torch.cat((x.features, skip.features), dim=1)) x, _, _ = self.dec[s]([x, condition, context]) x = self.final(x) if self.cls_mode: x = x.replace_feature( scatter(x.features, x.indices[:, 0].long(), reduce="mean", dim=0) ) return x.features