BIMStruct3D-segmentation / pointcept /models /masked_scene_contrast /masked_scene_contrast_v1m1_base.py
mahdichamseddine's picture
Initial release: PT-v3m1 segmentation model (BIMStruct3D pipeline)
7ab05dd verified
Raw History Blame
12.3 kB
"""
Masked Scene Contrast
https://arxiv.org/abs/2303.14191
Author: Xiaoyang Wu (xiaoyang.wu.cs@gmail.com)
Please cite our work if the code is helpful to you.
"""
import random
from itertools import chain
import torch
import torch.nn as nn
import torch.distributed as dist
from torch_geometric.nn.pool import voxel_grid
from timm.layers import trunc_normal_
import pointops
from pointcept.models.builder import MODELS, build_model
from pointcept.models.utils import offset2batch
from pointcept.utils.comm import get_world_size
@MODELS.register_module("MSC-v1m1")
class MaskedSceneContrast(nn.Module):
def __init__(
self,
backbone,
backbone_in_channels,
backbone_out_channels,
mask_grid_size=0.1,
mask_rate=0.4,
view1_mix_prob=0,
view2_mix_prob=0,
matching_max_k=8,
matching_max_radius=0.03,
matching_max_pair=8192,
nce_t=0.4,
contrast_weight=1,
reconstruct_weight=1,
reconstruct_color=True,
reconstruct_normal=True,
):
super().__init__()
self.backbone = build_model(backbone)
self.mask_grid_size = mask_grid_size
self.mask_rate = mask_rate
self.view1_mix_prob = view1_mix_prob
self.view2_mix_prob = view2_mix_prob
self.matching_max_k = matching_max_k
self.matching_max_radius = matching_max_radius
self.matching_max_pair = matching_max_pair
self.nce_t = nce_t
self.contrast_weight = contrast_weight
self.reconstruct_weight = reconstruct_weight
self.reconstruct_color = reconstruct_color
self.reconstruct_normal = reconstruct_normal
self.mask_token = nn.Parameter(torch.zeros(1, backbone_in_channels))
trunc_normal_(self.mask_token, mean=0.0, std=0.02)
self.color_head = (
nn.Linear(backbone_out_channels, 3) if reconstruct_color else None
)
self.normal_head = (
nn.Linear(backbone_out_channels, 3) if reconstruct_normal else None
)
self.nce_criteria = torch.nn.CrossEntropyLoss(reduction="mean")
@torch.no_grad()
def generate_cross_masks(
self, view1_origin_coord, view1_offset, view2_origin_coord, view2_offset
):
# union origin coord
view1_batch = offset2batch(view1_offset)
view2_batch = offset2batch(view2_offset)
view1_batch_count = view1_batch.bincount()
view2_batch_count = view2_batch.bincount()
view1_origin_coord_split = view1_origin_coord.split(list(view1_batch_count))
view2_origin_coord_split = view2_origin_coord.split(list(view2_batch_count))
union_origin_coord = torch.cat(
list(
chain.from_iterable(
zip(view1_origin_coord_split, view2_origin_coord_split)
)
)
)
union_offset = torch.cat(
[view1_offset.unsqueeze(-1), view2_offset.unsqueeze(-1)], dim=-1
).sum(-1)
union_batch = offset2batch(union_offset)
# grid partition
mask_patch_coord = union_origin_coord.div(self.mask_grid_size)
mask_patch_grid_coord = torch.floor(mask_patch_coord)
mask_patch_cluster = voxel_grid(
pos=mask_patch_grid_coord, size=1, batch=union_batch, start=0
)
unique, cluster, counts = torch.unique(
mask_patch_cluster, sorted=True, return_inverse=True, return_counts=True
)
patch_num = unique.shape[0]
patch_max_point = counts.max().item()
patch2point_map = cluster.new_zeros(patch_num, patch_max_point)
patch2point_mask = torch.lt(
torch.arange(patch_max_point).cuda().unsqueeze(0), counts.unsqueeze(-1)
)
sorted_cluster_value, sorted_cluster_indices = torch.sort(cluster)
patch2point_map[patch2point_mask] = sorted_cluster_indices
# generate cross masks
assert self.mask_rate <= 0.5
patch_mask = torch.zeros(patch_num, device=union_origin_coord.device).int()
rand_perm = torch.randperm(patch_num)
mask_patch_num = int(patch_num * self.mask_rate)
# mask1 tag with 1, mask2 tag with 2
patch_mask[rand_perm[0:mask_patch_num]] = 1
patch_mask[rand_perm[mask_patch_num : mask_patch_num * 2]] = 2
point_mask = torch.zeros(
union_origin_coord.shape[0], device=union_origin_coord.device
).int()
point_mask[
patch2point_map[patch_mask == 1][patch2point_mask[patch_mask == 1]]
] = 1
point_mask[
patch2point_map[patch_mask == 2][patch2point_mask[patch_mask == 2]]
] = 2
# separate mask to view1 and view2
point_mask_split = point_mask.split(
list(
torch.cat(
[view1_batch_count.unsqueeze(-1), view2_batch_count.unsqueeze(-1)],
dim=-1,
).flatten()
)
)
view1_point_mask = torch.cat(point_mask_split[0::2]) == 1
view2_point_mask = torch.cat(point_mask_split[1::2]) == 2
return view1_point_mask, view2_point_mask
@torch.no_grad()
def match_contrastive_pair(
self, view1_coord, view1_offset, view2_coord, view2_offset, max_k, max_radius
):
index, distance = pointops.knn_query(
max_k,
view2_coord.float(),
view2_offset.int(),
view1_coord.float(),
view1_offset.int(),
)
index = torch.cat(
[
torch.arange(index.shape[0], device=index.device, dtype=torch.long)
.view(-1, 1, 1)
.expand(-1, max_k, 1),
index.view(-1, max_k, 1),
],
dim=-1,
)[distance.squeeze(-1) < max_radius]
unique, count = index[:, 0].unique(return_counts=True)
select = (
torch.cumsum(count, dim=0)
- torch.randint(count.max(), count.shape, device=count.device) % count
- 1
)
index = index[select]
if index.shape[0] > self.matching_max_pair:
index = index[torch.randperm(index.shape[0])[: self.matching_max_pair]]
return index
def compute_contrastive_loss(
self, view1_feat, view1_offset, view2_feat, view2_offset, match_index
):
assert view1_offset.shape == view2_offset.shape
view1_feat = view1_feat[match_index[:, 0]]
view2_feat = view2_feat[match_index[:, 1]]
view1_feat = view1_feat / (
torch.norm(view1_feat, p=2, dim=1, keepdim=True) + 1e-7
)
view2_feat = view2_feat / (
torch.norm(view2_feat, p=2, dim=1, keepdim=True) + 1e-7
)
sim = torch.mm(view1_feat, view2_feat.transpose(1, 0))
with torch.no_grad():
pos_sim = torch.diagonal(sim).mean()
neg_sim = sim.mean(dim=-1).mean() - pos_sim / match_index.shape[0]
labels = torch.arange(sim.shape[0], device=view1_feat.device).long()
loss = self.nce_criteria(torch.div(sim, self.nce_t), labels)
if get_world_size() > 1:
dist.all_reduce(loss)
dist.all_reduce(pos_sim)
dist.all_reduce(neg_sim)
return (
loss / get_world_size(),
pos_sim / get_world_size(),
neg_sim / get_world_size(),
)
def forward(self, data_dict):
view1_origin_coord = data_dict["view1_origin_coord"]
view1_coord = data_dict["view1_coord"]
view1_feat = data_dict["view1_feat"]
view1_offset = data_dict["view1_offset"].int()
view2_origin_coord = data_dict["view2_origin_coord"]
view2_coord = data_dict["view2_coord"]
view2_feat = data_dict["view2_feat"]
view2_offset = data_dict["view2_offset"].int()
# mask generation by union original coord (without spatial augmentation)
view1_point_mask, view2_point_mask = self.generate_cross_masks(
view1_origin_coord, view1_offset, view2_origin_coord, view2_offset
)
view1_mask_tokens = self.mask_token.expand(view1_coord.shape[0], -1)
view1_weight = view1_point_mask.unsqueeze(-1).type_as(view1_mask_tokens)
view1_feat = view1_feat * (1 - view1_weight) + view1_mask_tokens * view1_weight
view2_mask_tokens = self.mask_token.expand(view2_coord.shape[0], -1)
view2_weight = view2_point_mask.unsqueeze(-1).type_as(view2_mask_tokens)
view2_feat = view2_feat * (1 - view2_weight) + view2_mask_tokens * view2_weight
view1_data_dict = dict(
origin_coord=view1_origin_coord,
coord=view1_coord,
feat=view1_feat,
offset=view1_offset,
)
view2_data_dict = dict(
origin_coord=view2_origin_coord,
coord=view2_coord,
feat=view2_feat,
offset=view2_offset,
)
# SparseConv based method need grid coord
if "view1_grid_coord" in data_dict.keys():
view1_data_dict["grid_coord"] = data_dict["view1_grid_coord"]
if "view2_grid_coord" in data_dict.keys():
view2_data_dict["grid_coord"] = data_dict["view2_grid_coord"]
# view mixing strategy
if random.random() < self.view1_mix_prob:
view1_data_dict["offset"] = torch.cat(
[view1_offset[1:-1:2], view1_offset[-1].unsqueeze(0)], dim=0
)
if random.random() < self.view2_mix_prob:
view2_data_dict["offset"] = torch.cat(
[view2_offset[1:-1:2], view2_offset[-1].unsqueeze(0)], dim=0
)
view1_feat = self.backbone(view1_data_dict)
view2_feat = self.backbone(view2_data_dict)
match_index = self.match_contrastive_pair(
view1_origin_coord,
view1_offset,
view2_origin_coord,
view2_offset,
max_k=self.matching_max_k,
max_radius=self.matching_max_radius,
)
nce_loss, pos_sim, neg_sim = self.compute_contrastive_loss(
view1_feat, view1_offset, view2_feat, view2_offset, match_index
)
loss = nce_loss * self.contrast_weight
result_dict = dict(nce_loss=nce_loss, pos_sim=pos_sim, neg_sim=neg_sim)
if self.color_head is not None:
assert "view1_color" in data_dict.keys()
assert "view2_color" in data_dict.keys()
view1_color = data_dict["view1_color"]
view2_color = data_dict["view2_color"]
view1_color_pred = self.color_head(view1_feat[view1_point_mask])
view2_color_pred = self.color_head(view2_feat[view2_point_mask])
color_loss = (
torch.sum((view1_color_pred - view1_color[view1_point_mask]) ** 2)
+ torch.sum((view2_color_pred - view2_color[view2_point_mask]) ** 2)
) / (view1_color_pred.shape[0] + view2_color_pred.shape[0])
loss = loss + color_loss * self.reconstruct_weight
result_dict["color_loss"] = color_loss
if self.normal_head is not None:
assert "view1_normal" in data_dict.keys()
assert "view2_normal" in data_dict.keys()
view1_normal = data_dict["view1_normal"]
view2_normal = data_dict["view2_normal"]
view1_normal_pred = self.normal_head(view1_feat[view1_point_mask])
view2_normal_pred = self.normal_head(view2_feat[view2_point_mask])
view1_normal_pred = view1_normal_pred / (
torch.norm(view1_normal_pred, p=2, dim=1, keepdim=True) + 1e-10
)
view2_normal_pred = view2_normal_pred / (
torch.norm(view2_normal_pred, p=2, dim=1, keepdim=True) + 1e-10
)
normal_loss = (
torch.sum(view1_normal_pred * view1_normal[view1_point_mask])
+ torch.sum(view2_normal_pred * view2_normal[view2_point_mask])
) / (view1_normal_pred.shape[0] + view2_normal_pred.shape[0])
loss = loss + normal_loss * self.reconstruct_weight
result_dict["normal_loss"] = normal_loss
result_dict["loss"] = loss
return result_dict