omnipart's picture
init
491eded
#https://github.com/3DTopia/OpenLRM/blob/main/openlrm/models/modeling_lrm.py
# Copyright (c) 2023-2024, Zexin He
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import torch
import torch.nn as nn
from functools import partial
def project_onto_planes(planes, coordinates):
"""
Does a projection of a 3D point onto a batch of 2D planes,
returning 2D plane coordinates.
Takes plane axes of shape n_planes, 3, 3
# Takes coordinates of shape N, M, 3
# returns projections of shape N*n_planes, M, 2
"""
N, M, C = coordinates.shape
n_planes, _, _ = planes.shape
coordinates = coordinates.unsqueeze(1).expand(-1, n_planes, -1, -1).reshape(N*n_planes, M, 3)
inv_planes = torch.linalg.inv(planes).unsqueeze(0).expand(N, -1, -1, -1).reshape(N*n_planes, 3, 3)
projections = torch.bmm(coordinates, inv_planes)
return projections[..., :2]
def sample_from_planes(plane_features, coordinates, mode='bilinear', padding_mode='zeros', box_warp=None):
plane_axes = torch.tensor([[[1, 0, 0],
[0, 1, 0],
[0, 0, 1]],
[[1, 0, 0],
[0, 0, 1],
[0, 1, 0]],
[[0, 0, 1],
[0, 1, 0],
[1, 0, 0]]], dtype=torch.float32).cuda()
assert padding_mode == 'zeros'
N, n_planes, C, H, W = plane_features.shape
_, M, _ = coordinates.shape
plane_features = plane_features.view(N*n_planes, C, H, W)
projected_coordinates = project_onto_planes(plane_axes, coordinates).unsqueeze(1)
output_features = torch.nn.functional.grid_sample(plane_features, projected_coordinates.float(), mode=mode, padding_mode=padding_mode, align_corners=False).permute(0, 3, 2, 1).reshape(N, n_planes, M, C)
return output_features
def get_grid_coord(grid_size = 256, align_corners=False):
if align_corners == False:
coords = torch.linspace(-1 + 1/(grid_size), 1 - 1/(grid_size), steps=grid_size)
else:
coords = torch.linspace(-1, 1, steps=grid_size)
i, j, k = torch.meshgrid(coords, coords, coords, indexing='ij')
coordinates = torch.stack((i, j, k), dim=-1).reshape(-1, 3)
return coordinates
class BasicBlock(nn.Module):
"""
Transformer block that is in its simplest form.
Designed for PF-LRM architecture.
"""
# Block contains a self-attention layer and an MLP
def __init__(self, inner_dim: int, num_heads: int, eps: float,
attn_drop: float = 0., attn_bias: bool = False,
mlp_ratio: float = 4., mlp_drop: float = 0.):
super().__init__()
self.norm1 = nn.LayerNorm(inner_dim, eps=eps)
self.self_attn = nn.MultiheadAttention(
embed_dim=inner_dim, num_heads=num_heads,
dropout=attn_drop, bias=attn_bias, batch_first=True)
self.norm2 = nn.LayerNorm(inner_dim, eps=eps)
self.mlp = nn.Sequential(
nn.Linear(inner_dim, int(inner_dim * mlp_ratio)),
nn.GELU(),
nn.Dropout(mlp_drop),
nn.Linear(int(inner_dim * mlp_ratio), inner_dim),
nn.Dropout(mlp_drop),
)
def forward(self, x):
# x: [N, L, D]
before_sa = self.norm1(x)
x = x + self.self_attn(before_sa, before_sa, before_sa, need_weights=False)[0]
x = x + self.mlp(self.norm2(x))
return x
class ConditionBlock(nn.Module):
"""
Transformer block that takes in a cross-attention condition.
Designed for SparseLRM architecture.
"""
# Block contains a cross-attention layer, a self-attention layer, and an MLP
def __init__(self, inner_dim: int, cond_dim: int, num_heads: int, eps: float,
attn_drop: float = 0., attn_bias: bool = False,
mlp_ratio: float = 4., mlp_drop: float = 0.):
super().__init__()
self.norm1 = nn.LayerNorm(inner_dim, eps=eps)
self.cross_attn = nn.MultiheadAttention(
embed_dim=inner_dim, num_heads=num_heads, kdim=cond_dim, vdim=cond_dim,
dropout=attn_drop, bias=attn_bias, batch_first=True)
self.norm2 = nn.LayerNorm(inner_dim, eps=eps)
self.self_attn = nn.MultiheadAttention(
embed_dim=inner_dim, num_heads=num_heads,
dropout=attn_drop, bias=attn_bias, batch_first=True)
self.norm3 = nn.LayerNorm(inner_dim, eps=eps)
self.mlp = nn.Sequential(
nn.Linear(inner_dim, int(inner_dim * mlp_ratio)),
nn.GELU(),
nn.Dropout(mlp_drop),
nn.Linear(int(inner_dim * mlp_ratio), inner_dim),
nn.Dropout(mlp_drop),
)
def forward(self, x, cond):
# x: [N, L, D]
# cond: [N, L_cond, D_cond]
x = x + self.cross_attn(self.norm1(x), cond, cond, need_weights=False)[0]
before_sa = self.norm2(x)
x = x + self.self_attn(before_sa, before_sa, before_sa, need_weights=False)[0]
x = x + self.mlp(self.norm3(x))
return x
class TransformerDecoder(nn.Module):
def __init__(self, block_type: str,
num_layers: int, num_heads: int,
inner_dim: int, cond_dim: int = None,
eps: float = 1e-6):
super().__init__()
self.block_type = block_type
self.layers = nn.ModuleList([
self._block_fn(inner_dim, cond_dim)(
num_heads=num_heads,
eps=eps,
)
for _ in range(num_layers)
])
self.norm = nn.LayerNorm(inner_dim, eps=eps)
@property
def block_type(self):
return self._block_type
@block_type.setter
def block_type(self, block_type):
assert block_type in ['cond', 'basic'], \
f"Unsupported block type: {block_type}"
self._block_type = block_type
def _block_fn(self, inner_dim, cond_dim):
assert inner_dim is not None, f"inner_dim must always be specified"
if self.block_type == 'basic':
return partial(BasicBlock, inner_dim=inner_dim)
elif self.block_type == 'cond':
assert cond_dim is not None, f"Condition dimension must be specified for ConditionBlock"
return partial(ConditionBlock, inner_dim=inner_dim, cond_dim=cond_dim)
else:
raise ValueError(f"Unsupported block type during runtime: {self.block_type}")
def forward_layer(self, layer: nn.Module, x: torch.Tensor, cond: torch.Tensor,):
if self.block_type == 'basic':
return layer(x)
elif self.block_type == 'cond':
return layer(x, cond)
else:
raise NotImplementedError
def forward(self, x: torch.Tensor, cond: torch.Tensor = None):
# x: [N, L, D]
# cond: [N, L_cond, D_cond] or None
for layer in self.layers:
x = self.forward_layer(layer, x, cond)
x = self.norm(x)
return x
class Voxel2Triplane(nn.Module):
"""
Full model of the basic single-view large reconstruction model.
"""
def __init__(self, transformer_dim: int, transformer_layers: int, transformer_heads: int,
triplane_low_res: int, triplane_high_res: int, triplane_dim: int, voxel_feat_dim: int, normalize_vox_feat=False, voxel_dim=16):
super().__init__()
# attributes
self.triplane_low_res = triplane_low_res
self.triplane_high_res = triplane_high_res
self.triplane_dim = triplane_dim
self.voxel_feat_dim = voxel_feat_dim
# initialize pos_embed with 1/sqrt(dim) * N(0, 1)
self.pos_embed = nn.Parameter(torch.randn(1, 3*triplane_low_res**2, transformer_dim) * (1. / transformer_dim) ** 0.5)
self.transformer = TransformerDecoder(
block_type='cond',
num_layers=transformer_layers, num_heads=transformer_heads,
inner_dim=transformer_dim, cond_dim=voxel_feat_dim
)
self.upsampler = nn.ConvTranspose2d(transformer_dim, triplane_dim, kernel_size=8, stride=8, padding=0)
self.normalize_vox_feat = normalize_vox_feat
if normalize_vox_feat:
self.vox_norm = nn.LayerNorm(voxel_feat_dim, eps=1e-6)
self.vox_pos_embed = nn.Parameter(torch.randn(1, voxel_dim * voxel_dim * voxel_dim, voxel_feat_dim) * (1. / voxel_feat_dim) ** 0.5)
def forward_transformer(self, voxel_feats):
N = voxel_feats.shape[0]
x = self.pos_embed.repeat(N, 1, 1) # [N, L, D]
if self.normalize_vox_feat:
vox_pos_embed = self.vox_pos_embed.repeat(N, 1, 1) # [N, L, D]
voxel_feats = self.vox_norm(voxel_feats + vox_pos_embed)
x = self.transformer(
x,
cond=voxel_feats
)
return x
def reshape_upsample(self, tokens):
N = tokens.shape[0]
H = W = self.triplane_low_res
x = tokens.view(N, 3, H, W, -1)
x = torch.einsum('nihwd->indhw', x) # [3, N, D, H, W]
x = x.contiguous().view(3*N, -1, H, W) # [3*N, D, H, W]
x = self.upsampler(x) # [3*N, D', H', W']
x = x.view(3, N, *x.shape[-3:]) # [3, N, D', H', W']
x = torch.einsum('indhw->nidhw', x) # [N, 3, D', H', W']
x = x.contiguous()
return x
def forward(self, voxel_feats):
N = voxel_feats.shape[0]
# encode image
assert voxel_feats.shape[-1] == self.voxel_feat_dim, \
f"Feature dimension mismatch: {voxel_feats.shape[-1]} vs {self.voxel_feat_dim}"
# transformer generating planes
tokens = self.forward_transformer(voxel_feats)
planes = self.reshape_upsample(tokens)
assert planes.shape[0] == N, "Batch size mismatch for planes"
assert planes.shape[1] == 3, "Planes should have 3 channels"
return planes
class TriplaneTransformer(nn.Module):
"""
Full model of the basic single-view large reconstruction model.
"""
def __init__(self, input_dim: int, transformer_dim: int, transformer_layers: int, transformer_heads: int,
triplane_low_res: int, triplane_high_res: int, triplane_dim: int):
super().__init__()
# attributes
self.triplane_low_res = triplane_low_res
self.triplane_high_res = triplane_high_res
self.triplane_dim = triplane_dim
# initialize pos_embed with 1/sqrt(dim) * N(0, 1)
self.pos_embed = nn.Parameter(torch.randn(1, 3*triplane_low_res**2, transformer_dim) * (1. / transformer_dim) ** 0.5)
self.transformer = TransformerDecoder(
block_type='basic',
num_layers=transformer_layers, num_heads=transformer_heads,
inner_dim=transformer_dim,
)
self.downsampler = nn.Sequential(
nn.Conv2d(input_dim, transformer_dim, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2), # Reduces size from 128x128 to 64x64
nn.Conv2d(transformer_dim, transformer_dim, kernel_size=3, stride=1, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2), # Reduces size from 64x64 to 32x32
)
self.upsampler = nn.ConvTranspose2d(transformer_dim, triplane_dim, kernel_size=4, stride=4, padding=0)
self.mlp = nn.Sequential(
nn.Linear(input_dim, triplane_dim),
nn.ReLU(),
nn.Linear(triplane_dim, triplane_dim)
)
def forward_transformer(self, triplanes):
N = triplanes.shape[0]
tokens = torch.einsum('nidhw->nihwd', triplanes).reshape(N, self.pos_embed.shape[1], -1) # [N, L, D]
x = self.pos_embed.repeat(N, 1, 1) + tokens # [N, L, D]
x = self.transformer(x)
return x
def reshape_downsample(self, triplanes):
N = triplanes.shape[0]
H = W = self.triplane_high_res
x = triplanes.view(N, 3, -1, H, W)
x = torch.einsum('nidhw->indhw', x) # [3, N, D, H, W]
x = x.contiguous().view(3*N, -1, H, W) # [3*N, D, H, W]
x = self.downsampler(x) # [3*N, D', H', W']
x = x.view(3, N, *x.shape[-3:]) # [3, N, D', H', W']
x = torch.einsum('indhw->nidhw', x) # [N, 3, D', H', W']
x = x.contiguous()
return x
def reshape_upsample(self, tokens):
N = tokens.shape[0]
H = W = self.triplane_low_res
x = tokens.view(N, 3, H, W, -1)
x = torch.einsum('nihwd->indhw', x) # [3, N, D, H, W]
x = x.contiguous().view(3*N, -1, H, W) # [3*N, D, H, W]
x = self.upsampler(x) # [3*N, D', H', W']
x = x.view(3, N, *x.shape[-3:]) # [3, N, D', H', W']
x = torch.einsum('indhw->nidhw', x) # [N, 3, D', H', W']
x = x.contiguous()
return x
def forward(self, triplanes):
downsampled_triplanes = self.reshape_downsample(triplanes)
tokens = self.forward_transformer(downsampled_triplanes)
residual = self.reshape_upsample(tokens)
triplanes = triplanes.permute(0, 1, 3, 4, 2).contiguous()
triplanes = self.mlp(triplanes)
triplanes = triplanes.permute(0, 1, 4, 2, 3).contiguous()
planes = triplanes + residual
return planes