AndreasLH's picture
upload repo
56bd2b5
# Copyright (c) Meta Platforms, Inc. and affiliates
import math
import numpy as np
import pandas as pd
from typing import Tuple, List
from pytorch3d.renderer.lighting import PointLights
from pytorch3d.renderer.mesh.renderer import MeshRenderer
from pytorch3d.renderer.mesh.shader import SoftPhongShader
from pytorch3d.transforms.math import acos_linear_extrapolation
import torch
from pytorch3d.structures import Meshes
from detectron2.structures import BoxMode
from pytorch3d.renderer import TexturesVertex
from pytorch3d.structures.meshes import (
Meshes,
)
from pytorch3d.renderer import (
PerspectiveCameras,
RasterizationSettings,
MeshRasterizer
)
from pytorch3d.renderer import (
PerspectiveCameras,
SoftSilhouetteShader,
RasterizationSettings,
MeshRasterizer
)
from detectron2.data import (
MetadataCatalog,
)
from pytorch3d.transforms import axis_angle_to_matrix
from pytorch3d.renderer import MeshRenderer as MR
UNIT_CUBE = np.array([
[-0.5, -0.5, -0.5],
[ 0.5, -0.5, -0.5],
[ 0.5, 0.5, -0.5],
[-0.5, 0.5, -0.5],
[-0.5, -0.5, 0.5],
[ 0.5, -0.5, 0.5],
[ 0.5, 0.5, 0.5],
[-0.5, 0.5, 0.5]
])
def upto_2Pi(val):
out = val
# constrain between [0, 2pi)
while out >= 2*math.pi: out -= math.pi * 2
while out < 0: out += math.pi * 2
return out
def upto_Pi(val):
out = val
# constrain between [0, pi)
while out >= math.pi: out -= math.pi
while out < 0: out += math.pi
return out
# Calculates rotation matrix to euler angles
# The result is the same as MATLAB except the order
# of the euler angles ( x and z are swapped ).
# adopted from https://www.learnopencv.com/rotation-matrix-to-euler-angles/
def mat2euler(R):
sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0])
#singular = sy < 1e-6
x = math.atan2(R[2, 1], R[2, 2])
y = math.atan2(-R[2, 0], sy)
z = math.atan2(R[1, 0], R[0, 0])
return np.array([x, y, z])
# Calculates Rotation Matrix given euler angles.
# adopted from https://www.learnopencv.com/rotation-matrix-to-euler-angles/
def euler2mat(euler):
R_x = np.array([[1, 0, 0],
[0, math.cos(euler[0]), -math.sin(euler[0])],
[0, math.sin(euler[0]), math.cos(euler[0])]
])
R_y = np.array([[math.cos(euler[1]), 0, math.sin(euler[1])],
[0, 1, 0],
[-math.sin(euler[1]), 0, math.cos(euler[1])]
])
R_z = np.array([[math.cos(euler[2]), -math.sin(euler[2]), 0],
[math.sin(euler[2]), math.cos(euler[2]), 0],
[0, 0, 1]
])
R = np.dot(R_z, np.dot(R_y, R_x))
return R
def euler2mat_torch(euler):
R_x = torch.stack([
torch.tensor([[1, 0, 0],
[0, torch.cos(angle), -torch.sin(angle)],
[0, torch.sin(angle), torch.cos(angle)]])
for angle in euler[:, 0]
])
R_y = torch.stack([
torch.tensor([[torch.cos(angle), 0, torch.sin(angle)],
[0, 1, 0],
[-torch.sin(angle), 0, torch.cos(angle)]])
for angle in euler[:, 1]
])
R_z = torch.stack([
torch.tensor([[torch.cos(angle), -torch.sin(angle), 0],
[torch.sin(angle), torch.cos(angle), 0],
[0, 0, 1]])
for angle in euler[:, 2]
])
R = torch.matmul(R_z, torch.matmul(R_y, R_x))
# (n x 3 x 3 out tensor)
return R
def to_float_tensor(input):
data_type = type(input)
if data_type != torch.Tensor:
input = torch.tensor(input)
return input.float()
def get_cuboid_verts_faces(box3d=None, R=None):
"""
Computes vertices and faces from a 3D cuboid representation.
Args:
bbox3d (flexible): [[X Y Z W H L]]
R (flexible): [np.array(3x3)]
Returns:
verts: the 3D vertices of the cuboid in camera space
faces: the vertex indices per face
"""
if box3d is None:
box3d = [0, 0, 0, 1, 1, 1]
# make sure types are correct
box3d = to_float_tensor(box3d)
if R is not None:
R = to_float_tensor(R)
squeeze = len(box3d.shape) == 1
if squeeze:
box3d = box3d.unsqueeze(0)
if R is not None:
R = R.unsqueeze(0)
n = len(box3d)
x3d = box3d[:, 0].unsqueeze(1)
y3d = box3d[:, 1].unsqueeze(1)
z3d = box3d[:, 2].unsqueeze(1)
w3d = box3d[:, 3].unsqueeze(1)
h3d = box3d[:, 4].unsqueeze(1)
l3d = box3d[:, 5].unsqueeze(1)
'''
v4_____________________v5
/| /|
/ | / |
/ | / |
/___|_________________/ |
v0| | |v1 |
| | | |
| | | |
| | | |
| |_________________|___|
| / v7 | /v6
| / | /
| / | /
|/_____________________|/
v3 v2
'''
verts = to_float_tensor(torch.zeros([n, 3, 8], device=box3d.device))
# setup X
verts[:, 0, [0, 3, 4, 7]] = -l3d / 2
verts[:, 0, [1, 2, 5, 6]] = l3d / 2
# setup Y
verts[:, 1, [0, 1, 4, 5]] = -h3d / 2
verts[:, 1, [2, 3, 6, 7]] = h3d / 2
# setup Z
verts[:, 2, [0, 1, 2, 3]] = -w3d / 2
verts[:, 2, [4, 5, 6, 7]] = w3d / 2
if R is not None:
# rotate
verts = R @ verts
# translate
verts[:, 0, :] += x3d
verts[:, 1, :] += y3d
verts[:, 2, :] += z3d
verts = verts.transpose(1, 2)
faces = torch.tensor([
[0, 1, 2], # front TR
[2, 3, 0], # front BL
[1, 5, 6], # right TR
[6, 2, 1], # right BL
[4, 0, 3], # left TR
[3, 7, 4], # left BL
[5, 4, 7], # back TR
[7, 6, 5], # back BL
[4, 5, 1], # top TR
[1, 0, 4], # top BL
[3, 2, 6], # bottom TR
[6, 7, 3], # bottom BL
]).float().unsqueeze(0).repeat([n, 1, 1])
if squeeze:
verts = verts.squeeze()
faces = faces.squeeze()
return verts, faces.to(verts.device)
def get_cuboid_verts(K, box3d, R=None, view_R=None, view_T=None):
# make sure types are correct
K = to_float_tensor(K)
box3d = to_float_tensor(box3d)
if R is not None:
R = to_float_tensor(R)
squeeze = len(box3d.shape) == 1
if squeeze:
box3d = box3d.unsqueeze(0)
if R is not None:
R = R.unsqueeze(0)
n = len(box3d)
if len(K.shape) == 2:
K = K.unsqueeze(0).repeat([n, 1, 1])
corners_3d, _ = get_cuboid_verts_faces(box3d, R)
if view_T is not None:
corners_3d -= view_T.view(1, 1, 3)
if view_R is not None:
corners_3d = (view_R @ corners_3d[0].T).T.unsqueeze(0)
if view_T is not None:
corners_3d[:, :, -1] += view_T.view(1, 1, 3)[:, :, -1]*1.25
# project to 2D
corners_2d = K @ corners_3d.transpose(1, 2)
corners_2d[:, :2, :] = corners_2d[:, :2, :] / corners_2d[:, 2, :].unsqueeze(1)
corners_2d = corners_2d.transpose(1, 2)
if squeeze:
corners_3d = corners_3d.squeeze()
corners_2d = corners_2d.squeeze()
return corners_2d, corners_3d
def approx_eval_resolution(h, w, scale_min=0, scale_max=1e10):
"""
Approximates the resolution an image with h x w resolution would
run through a model at which constrains the scale to a min and max.
Args:
h (int): input resolution height
w (int): input resolution width
scale_min (int): minimum scale allowed to resize too
scale_max (int): maximum scale allowed to resize too
Returns:
h (int): output resolution height
w (int): output resolution width
sf (float): scaling factor that was applied
which can convert from original --> network resolution.
"""
orig_h = h
# first resize to min
sf = scale_min / min(h, w)
h *= sf
w *= sf
# next resize to max
sf = min(scale_max / max(h, w), 1.0)
h *= sf
w *= sf
return h, w, h/orig_h
def compute_priors(cfg, datasets, max_cluster_rounds=1000, min_points_for_std=5, n_bins=None):
"""
Computes priors via simple averaging or a custom K-Means clustering.
"""
annIds = datasets.getAnnIds()
anns = datasets.loadAnns(annIds)
data_raw = []
category_names = MetadataCatalog.get('omni3d_model').thing_classes
virtual_depth = cfg.MODEL.ROI_CUBE_HEAD.VIRTUAL_DEPTH
virtual_focal = cfg.MODEL.ROI_CUBE_HEAD.VIRTUAL_FOCAL
test_scale_min = cfg.INPUT.MIN_SIZE_TEST
test_scale_max = cfg.INPUT.MAX_SIZE_TEST
'''
Accumulate the annotations while discarding the 2D center information
(hence, keeping only the 2D and 3D scale information, and properties.)
'''
for ann_idx, ann in enumerate(anns):
category_name = ann['category_name'].lower()
ignore = ann['ignore']
dataset_id = ann['dataset_id']
image_id = ann['image_id']
fy = datasets.imgs[image_id]['K'][1][1]
im_h = datasets.imgs[image_id]['height']
im_w = datasets.imgs[image_id]['width']
f = 2 * fy / im_h
if cfg.DATASETS.MODAL_2D_BOXES and 'bbox2D_tight' in ann and ann['bbox2D_tight'][0] != -1:
x, y, w, h = BoxMode.convert(ann['bbox2D_tight'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
elif cfg.DATASETS.TRUNC_2D_BOXES and 'bbox2D_trunc' in ann and not np.all([val==-1 for val in ann['bbox2D_trunc']]):
x, y, w, h = BoxMode.convert(ann['bbox2D_trunc'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
elif 'bbox2D_proj' in ann:
x, y, w, h = BoxMode.convert(ann['bbox2D_proj'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
else:
continue
x3d, y3d, z3d = ann['center_cam']
w3d, h3d, l3d = ann['dimensions']
test_h, test_w, sf = approx_eval_resolution(im_h, im_w, test_scale_min, test_scale_max)
# scale everything to test resolution
h *= sf
w *= sf
if virtual_depth:
virtual_to_real = compute_virtual_scale_from_focal_spaces(fy, im_h, virtual_focal, test_h)
real_to_virtual = 1/virtual_to_real
z3d *= real_to_virtual
scale = np.sqrt(h**2 + w**2)
if (not ignore) and category_name in category_names:
data_raw.append([category_name, w, h, x3d, y3d, z3d, w3d, h3d, l3d, w3d*h3d*l3d, dataset_id, image_id, fy, f, scale])
# TODO pandas is fairly inefficient to rely on for large scale.
df_raw = pd.DataFrame(data_raw, columns=[
'name',
'w', 'h', 'x3d', 'y3d', 'z3d',
'w3d', 'h3d', 'l3d', 'volume',
'dataset', 'image',
'fy', 'f', 'scale'
])
# ^ the elements ending in w/h/l3d are the actual sizes, while the x/y/z3d are the camera perspective sizes.
priors_bins = []
priors_dims_per_cat = []
priors_z3d_per_cat = []
priors_y3d_per_cat = []
# compute priors for z and y globally
priors_z3d = [df_raw.z3d.mean(), df_raw.z3d.std()]
priors_y3d = [df_raw.y3d.mean(), df_raw.y3d.std()]
if n_bins is None:
n_bins = cfg.MODEL.ROI_CUBE_HEAD.CLUSTER_BINS
# Each prior is pre-computed per category
for cat in category_names:
df_cat = df_raw[df_raw.name == cat]
'''
First compute static variable statistics
'''
scales = torch.FloatTensor(np.array(df_cat.scale))
n = len(scales)
if n > 0:
priors_dims_per_cat.append([[df_cat.w3d.mean(), df_cat.h3d.mean(), df_cat.l3d.mean()], [df_cat.w3d.std(), df_cat.h3d.std(), df_cat.l3d.std()]])
priors_z3d_per_cat.append([df_cat.z3d.mean(), df_cat.z3d.std()])
priors_y3d_per_cat.append([df_cat.y3d.mean(), df_cat.y3d.std()])
else:
# dummy data.
priors_dims_per_cat.append([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])
priors_z3d_per_cat.append([50, 50])
priors_y3d_per_cat.append([1, 10])
'''
Next compute Z cluster statistics based on y and area
'''
def compute_cluster_scale_mean(scales, assignments, n_bins, match_quality):
cluster_scales = []
for bin in range(n_bins):
in_cluster = assignments==bin
if in_cluster.sum() < min_points_for_std:
in_cluster[match_quality[:, bin].topk(min_points_for_std)[1]] = True
scale = scales[in_cluster].mean()
cluster_scales.append(scale.item())
return torch.FloatTensor(cluster_scales)
if n_bins > 1:
if n < min_points_for_std:
print('Warning {} category has only {} valid samples...'.format(cat, n))
# dummy data since category doesn't have available samples.
max_scale = cfg.MODEL.ANCHOR_GENERATOR.SIZES[-1][-1]
min_scale = cfg.MODEL.ANCHOR_GENERATOR.SIZES[0][0]
base = (max_scale / min_scale) ** (1 / (n_bins - 1))
cluster_scales = np.array([min_scale * (base ** i) for i in range(0, n_bins)])
# default values are unused anyways in training. but range linearly
# from 100 to 1 and ascend with 2D scale.
bin_priors_z = [[b, 15] for b in np.arange(100, 1, -(100-1)/n_bins)]
priors_bins.append((cat, cluster_scales.tolist(), bin_priors_z))
assert len(bin_priors_z) == n_bins, 'Broken default bin scaling.'
else:
max_scale = scales.max()
min_scale = scales.min()
base = (max_scale / min_scale) ** (1 / (n_bins - 1))
cluster_scales = torch.FloatTensor([min_scale * (base ** i) for i in range(0, n_bins)])
best_score = -np.inf
for round in range(max_cluster_rounds):
# quality scores for gts and clusters (n x n_bins)
match_quality = -(cluster_scales.unsqueeze(0) - scales.unsqueeze(1)).abs()
# assign to best clusters
scores, assignments_round = match_quality.max(1)
round_score = scores.mean().item()
if np.round(round_score, 5) > best_score:
best_score = round_score
assignments = assignments_round
# make new clusters
cluster_scales = compute_cluster_scale_mean(scales, assignments, n_bins, match_quality)
else:
break
bin_priors_z = []
for bin in range(n_bins):
in_cluster = assignments == bin
# not enough in the cluster to compute reliable stats?
# fill it with the topk others
if in_cluster.sum() < min_points_for_std:
in_cluster[match_quality[:, bin].topk(min_points_for_std)[1]] = True
# move to numpy for indexing pandas
in_cluster = in_cluster.numpy()
z3d_mean = df_cat.z3d[in_cluster].mean()
z3d_std = df_cat.z3d[in_cluster].std()
bin_priors_z.append([z3d_mean, z3d_std])
priors_bins.append((cat, cluster_scales.numpy().tolist(), bin_priors_z))
priors = {
'priors_dims_per_cat': priors_dims_per_cat,
'priors_z3d_per_cat': priors_z3d_per_cat,
'priors_y3d_per_cat': priors_y3d_per_cat,
'priors_bins': priors_bins,
'priors_y3d': priors_y3d,
'priors_z3d': priors_z3d,
}
return priors
def compute_priors_custom(cfg, datasets, max_cluster_rounds=1000, min_points_for_std=5):
"""
simplification of the standard compute_priors function
Computes priors via simple averaging
"""
annIds = datasets.getAnnIds()
anns = datasets.loadAnns(annIds)
data_raw = []
category_names = MetadataCatalog.get('omni3d_model').thing_classes
virtual_depth = cfg.MODEL.ROI_CUBE_HEAD.VIRTUAL_DEPTH
virtual_focal = cfg.MODEL.ROI_CUBE_HEAD.VIRTUAL_FOCAL
test_scale_min = cfg.INPUT.MIN_SIZE_TEST
test_scale_max = cfg.INPUT.MAX_SIZE_TEST
'''
Accumulate the annotations while discarding the 2D center information
(hence, keeping only the 2D and 3D scale information, and properties.)
'''
for ann_idx, ann in enumerate(anns):
category_name = ann['category_name'].lower()
ignore = ann['ignore']
dataset_id = ann['dataset_id']
image_id = ann['image_id']
fy = datasets.imgs[image_id]['K'][1][1]
im_h = datasets.imgs[image_id]['height']
im_w = datasets.imgs[image_id]['width']
f = 2 * fy / im_h
if cfg.DATASETS.MODAL_2D_BOXES and 'bbox2D_tight' in ann and ann['bbox2D_tight'][0] != -1:
x, y, w, h = BoxMode.convert(ann['bbox2D_tight'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
elif cfg.DATASETS.TRUNC_2D_BOXES and 'bbox2D_trunc' in ann and not np.all([val==-1 for val in ann['bbox2D_trunc']]):
x, y, w, h = BoxMode.convert(ann['bbox2D_trunc'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
elif 'bbox2D_proj' in ann:
x, y, w, h = BoxMode.convert(ann['bbox2D_proj'], BoxMode.XYXY_ABS, BoxMode.XYWH_ABS)
else:
continue
x3d, y3d, z3d = ann['center_cam']
w3d, h3d, l3d = ann['dimensions']
test_h, test_w, sf = approx_eval_resolution(im_h, im_w, test_scale_min, test_scale_max)
# scale everything to test resolution
h *= sf
w *= sf
if virtual_depth:
virtual_to_real = compute_virtual_scale_from_focal_spaces(fy, im_h, virtual_focal, test_h)
real_to_virtual = 1/virtual_to_real
z3d *= real_to_virtual
scale = np.sqrt(h**2 + w**2)
if (not ignore) and category_name in category_names:
data_raw.append([category_name, w, h, x3d, y3d, z3d, w3d, h3d, l3d, w3d*h3d*l3d, dataset_id, image_id, fy, f, scale])
# TODO pandas is fairly inefficient to rely on for large scale.
df_raw = pd.DataFrame(data_raw, columns=[
'name',
'w', 'h', 'x3d', 'y3d', 'z3d',
'w3d', 'h3d', 'l3d', 'volume',
'dataset', 'image',
'fy', 'f', 'scale'
])
# ^ the elements ending in w/h/l3d are the actual sizes, while the x/y/z3d are the camera perspective sizes.
priors_bins = []
priors_dims_per_cat = []
priors_z3d_per_cat = []
priors_y3d_per_cat = []
# compute priors for z and y globally
priors_z3d = [df_raw.z3d.mean(), df_raw.z3d.std()]
priors_y3d = [df_raw.y3d.mean(), df_raw.y3d.std()]
# Each prior is pre-computed per category
for cat in category_names:
df_cat = df_raw[df_raw.name == cat]
'''
First compute static variable statistics
'''
scales = torch.FloatTensor(np.array(df_cat.scale))
n = len(scales)
if None:
priors_dims_per_cat.append([[df_cat.w3d.mean(), df_cat.h3d.mean(), df_cat.l3d.mean()], [df_cat.w3d.std(), df_cat.h3d.std(), df_cat.l3d.std()]])
priors_z3d_per_cat.append([df_cat.z3d.mean(), df_cat.z3d.std()])
priors_y3d_per_cat.append([df_cat.y3d.mean(), df_cat.y3d.std()])
else:
# dummy data.
priors_dims_per_cat.append([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
priors_z3d_per_cat.append([0, 0])
priors_y3d_per_cat.append([0, 0])
priors = {
'priors_dims_per_cat': priors_dims_per_cat,
'priors_z3d_per_cat': priors_z3d_per_cat,
'priors_y3d_per_cat': priors_y3d_per_cat,
'priors_bins': priors_bins,
'priors_y3d': priors_y3d,
'priors_z3d': priors_z3d,
}
return priors
def convert_3d_box_to_2d(K, box3d, R=None, clipw=0, cliph=0, XYWH=True, min_z=0.20):
"""
Converts a 3D box to a 2D box via projection.
Args:
K (np.array): intrinsics matrix 3x3
bbox3d (flexible): [[X Y Z W H L]]
R (flexible): [np.array(3x3)]
clipw (int): clip invalid X to the image bounds. Image width is usually used here.
cliph (int): clip invalid Y to the image bounds. Image height is usually used here.
XYWH (bool): returns in XYWH if true, otherwise XYXY format.
min_z: the threshold for how close a vertex is allowed to be before being
considered as invalid for projection purposes.
Returns:
box2d (flexible): the 2D box results.
behind_camera (bool): whether the projection has any points behind the camera plane.
fully_behind (bool): all points are behind the camera plane.
"""
# bounds used for vertices behind image plane
topL_bound = torch.tensor([[0, 0, 0]]).float()
topR_bound = torch.tensor([[clipw-1, 0, 0]]).float()
botL_bound = torch.tensor([[0, cliph-1, 0]]).float()
botR_bound = torch.tensor([[clipw-1, cliph-1, 0]]).float()
# make sure types are correct
K = to_float_tensor(K)
box3d = to_float_tensor(box3d)
if R is not None:
R = to_float_tensor(R)
squeeze = len(box3d.shape) == 1
if squeeze:
box3d = box3d.unsqueeze(0)
if R is not None:
R = R.unsqueeze(0)
n = len(box3d)
verts2d, verts3d = get_cuboid_verts(K, box3d, R)
# any boxes behind camera plane?
verts_behind = verts2d[:, :, 2] <= min_z
behind_camera = verts_behind.any(1)
verts_signs = torch.sign(verts3d)
# check for any boxes projected behind image plane corners
topL = verts_behind & (verts_signs[:, :, 0] < 0) & (verts_signs[:, :, 1] < 0)
topR = verts_behind & (verts_signs[:, :, 0] > 0) & (verts_signs[:, :, 1] < 0)
botL = verts_behind & (verts_signs[:, :, 0] < 0) & (verts_signs[:, :, 1] > 0)
botR = verts_behind & (verts_signs[:, :, 0] > 0) & (verts_signs[:, :, 1] > 0)
# clip values to be in bounds for invalid points
verts2d[topL] = topL_bound
verts2d[topR] = topR_bound
verts2d[botL] = botL_bound
verts2d[botR] = botR_bound
x, xi = verts2d[:, :, 0].min(1)
y, yi = verts2d[:, :, 1].min(1)
x2, x2i = verts2d[:, :, 0].max(1)
y2, y2i = verts2d[:, :, 1].max(1)
fully_behind = verts_behind.all(1)
width = x2 - x
height = y2 - y
if XYWH:
box2d = torch.cat((x.unsqueeze(1), y.unsqueeze(1), width.unsqueeze(1), height.unsqueeze(1)), dim=1)
else:
box2d = torch.cat((x.unsqueeze(1), y.unsqueeze(1), x2.unsqueeze(1), y2.unsqueeze(1)), dim=1)
if squeeze:
box2d = box2d.squeeze()
behind_camera = behind_camera.squeeze()
fully_behind = fully_behind.squeeze()
return box2d, behind_camera, fully_behind
#
def compute_virtual_scale_from_focal_spaces(f, H, f0, H0):
"""
Computes the scaling factor of depth from f0, H0 to f, H
Args:
f (float): the desired [virtual] focal length (px)
H (float): the desired [virtual] height (px)
f0 (float): the initial [real] focal length (px)
H0 (float): the initial [real] height (px)
Returns:
the scaling factor float to convert form (f0, H0) --> (f, H)
"""
return (H0 * f) / (f0 * H)
def R_to_allocentric(K, R, u=None, v=None):
"""
Convert a rotation matrix or series of rotation matrices to allocentric
representation given a 2D location (u, v) in pixels.
When u or v are not available, we fall back on the principal point of K.
"""
if type(K) == torch.Tensor:
fx = K[:, 0, 0]
fy = K[:, 1, 1]
sx = K[:, 0, 2]
sy = K[:, 1, 2]
n = len(K)
oray = torch.stack(((u - sx)/fx, (v - sy)/fy, torch.ones_like(u))).T
oray = oray / torch.linalg.norm(oray, dim=1).unsqueeze(1)
angle = torch.acos(oray[:, -1])
axis = torch.zeros_like(oray)
axis[:, 0] = axis[:, 0] - oray[:, 1]
axis[:, 1] = axis[:, 1] + oray[:, 0]
norms = torch.linalg.norm(axis, dim=1)
valid_angle = angle > 0
M = axis_angle_to_matrix(angle.unsqueeze(1)*axis/norms.unsqueeze(1))
R_view = R.clone()
R_view[valid_angle] = torch.bmm(M[valid_angle].transpose(2, 1), R[valid_angle])
else:
fx = K[0][0]
fy = K[1][1]
sx = K[0][2]
sy = K[1][2]
if u is None:
u = sx
if v is None:
v = sy
oray = np.array([(u - sx)/fx, (v - sy)/fy, 1])
oray = oray / np.linalg.norm(oray)
cray = np.array([0, 0, 1])
angle = math.acos(cray.dot(oray))
if angle != 0:
axis = np.cross(cray, oray)
axis_torch = torch.from_numpy(angle*axis/np.linalg.norm(axis)).float()
R_view = np.dot(axis_angle_to_matrix(axis_torch).numpy().T, R)
else:
R_view = R
return R_view
def R_from_allocentric(K, R_view, u=None, v=None):
"""
Convert a rotation matrix or series of rotation matrices to egocentric
representation given a 2D location (u, v) in pixels.
When u or v are not available, we fall back on the principal point of K.
"""
if type(K) == torch.Tensor:
fx = K[:, 0, 0]
fy = K[:, 1, 1]
sx = K[:, 0, 2]
sy = K[:, 1, 2]
n = len(K)
oray = torch.stack(((u - sx)/fx, (v - sy)/fy, torch.ones_like(u))).T
oray = oray / torch.linalg.norm(oray, dim=1).unsqueeze(1)
angle = torch.acos(oray[:, -1])
axis = torch.zeros_like(oray)
axis[:, 0] = axis[:, 0] - oray[:, 1]
axis[:, 1] = axis[:, 1] + oray[:, 0]
norms = torch.linalg.norm(axis, dim=1)
valid_angle = angle > 0
M = axis_angle_to_matrix(angle.unsqueeze(1)*axis/norms.unsqueeze(1))
R = R_view.clone()
R[valid_angle] = torch.bmm(M[valid_angle], R_view[valid_angle])
else:
fx = K[0][0]
fy = K[1][1]
sx = K[0][2]
sy = K[1][2]
if u is None:
u = sx
if v is None:
v = sy
oray = np.array([(u - sx)/fx, (v - sy)/fy, 1])
oray = oray / np.linalg.norm(oray)
cray = np.array([0, 0, 1])
angle = math.acos(cray.dot(oray))
if angle != 0:
#axis = np.cross(cray, oray)
axis = np.array([-oray[1], oray[0], 0])
axis_torch = torch.from_numpy(angle*axis/np.linalg.norm(axis)).float()
R = np.dot(axis_angle_to_matrix(axis_torch).numpy(), R_view)
else:
R = R_view
return R
def render_depth_map(K, box3d, pose, width, height, device=None):
cameras = get_camera(K, width, height)
renderer = get_basic_renderer(cameras, width, height)
mesh = mesh_cuboid(box3d, pose)
if device is not None:
cameras = cameras.to(device)
renderer = renderer.to(device)
mesh = mesh.to(device)
im_rendered, fragment = renderer(mesh)
silhouettes = im_rendered[:, :, :, -1] > 0
zbuf = fragment.zbuf[:, :, :, 0]
zbuf[zbuf==-1] = math.inf
depth_map, depth_map_inds = zbuf.min(dim=0)
return silhouettes, depth_map, depth_map_inds
def estimate_visibility(K, box3d, pose, width, height, device=None):
silhouettes, depth_map, depth_map_inds = render_depth_map(K, box3d, pose, width, height, device=device)
n = silhouettes.shape[0]
visibilies = []
for annidx in range(n):
area = silhouettes[annidx].sum()
visible = (depth_map_inds[silhouettes[annidx]] == annidx).sum()
visibilies.append((visible / area).item())
return visibilies
def estimate_truncation(K, box3d, R, imW, imH):
box2d, out_of_bounds, fully_behind = convert_3d_box_to_2d(K, box3d, R, imW, imH)
if fully_behind:
return 1.0
box2d = box2d.detach().cpu().numpy().tolist()
box2d_XYXY = BoxMode.convert(box2d, BoxMode.XYWH_ABS, BoxMode.XYXY_ABS)
image_box = np.array([0, 0, imW-1, imH-1])
truncation = 1 - iou(np.array(box2d_XYXY)[np.newaxis], image_box[np.newaxis], ign_area_b=True)
return truncation.item()
def mesh_cuboid(box3d=None, R=None, color=None):
verts, faces = get_cuboid_verts_faces(box3d, R)
if verts.ndim == 2:
verts = to_float_tensor(verts).unsqueeze(0)
faces = to_float_tensor(faces).unsqueeze(0)
ninstances = len(verts)
if (isinstance(color, Tuple) or isinstance(color, List)) and len(color) == 3:
color = torch.tensor(color).view(1, 1, 3).expand(ninstances, 8, 3).float()
# pass in a tensor of colors per box
elif color.ndim == 2:
color = to_float_tensor(color).unsqueeze(1).expand(ninstances, 8, 3).float()
device = verts.device
mesh = Meshes(verts=verts, faces=faces, textures=None if color is None else TexturesVertex(verts_features=color).to(device))
return mesh
def get_camera(K, width, height, switch_hands=True, R=None, T=None):
K = to_float_tensor(K)
if switch_hands:
K = K @ torch.tensor([
[-1, 0, 0],
[0, -1, 0],
[0, 0, 1]
]).float()
fx = K[0, 0]
fy = K[1, 1]
px = K[0, 2]
py = K[1, 2]
if R is None:
camera = PerspectiveCameras(
focal_length=((fx, fy),), principal_point=((px, py),),
image_size=((height, width),), in_ndc=False
)
else:
camera = PerspectiveCameras(
focal_length=((fx, fy),), principal_point=((px, py),),
image_size=((height, width),), in_ndc=False, R=R, T=T
)
return camera
def get_basic_renderer(cameras, width, height, use_color=False):
raster_settings = RasterizationSettings(
image_size=(height, width),
blur_radius=0 if use_color else np.log(1. / 1e-4 - 1.) * 1e-4,
faces_per_pixel=1,
perspective_correct=False,
)
if use_color:
# SoftPhongShader, HardPhongShader, HardFlatShader, SoftGouraudShader
lights = PointLights(location=[[0.0, 0.0, 0.0]])
shader = SoftPhongShader(cameras=cameras, lights=lights)
else:
shader = SoftSilhouetteShader()
renderer = MeshRenderer(
rasterizer=MeshRasterizer(
cameras=cameras,
raster_settings=raster_settings,
),
shader=shader
)
return renderer
class MeshRenderer(MR):
def __init__(self, rasterizer, shader):
super().__init__(rasterizer, shader)
def forward(self, meshes_world, **kwargs) -> torch.Tensor:
fragments = self.rasterizer(meshes_world, **kwargs)
images = self.shader(fragments, meshes_world, **kwargs)
return images, fragments
def iou(box_a, box_b, mode='cross', ign_area_b=False):
"""
Computes the amount of Intersection over Union (IoU) between two different sets of boxes.
Args:
box_a (array or tensor): Mx4 boxes, defined by [x1, y1, x2, y2]
box_a (array or tensor): Nx4 boxes, defined by [x1, y1, x2, y2]
mode (str): either 'cross' or 'list', where cross will check all combinations of box_a and
box_b hence MxN array, and list expects the same size list M == N, hence returns Mx1 array.
ign_area_b (bool): if true then we ignore area of b. e.g., checking % box a is inside b
"""
data_type = type(box_a)
# this mode computes the IoU in the sense of cross.
# i.e., box_a = M x 4, box_b = N x 4 then the output is M x N
if mode == 'cross':
inter = intersect(box_a, box_b, mode=mode)
area_a = ((box_a[:, 2] - box_a[:, 0]) *
(box_a[:, 3] - box_a[:, 1]))
area_b = ((box_b[:, 2] - box_b[:, 0]) *
(box_b[:, 3] - box_b[:, 1]))
# torch.Tensor
if data_type == torch.Tensor:
union = area_a.unsqueeze(0)
if not ign_area_b:
union = union + area_b.unsqueeze(1) - inter
return (inter / union).permute(1, 0)
# np.ndarray
elif data_type == np.ndarray:
union = np.expand_dims(area_a, 0)
if not ign_area_b:
union = union + np.expand_dims(area_b, 1) - inter
return (inter / union).T
# unknown type
else:
raise ValueError('unknown data type {}'.format(data_type))
# this mode compares every box in box_a with target in box_b
# i.e., box_a = M x 4 and box_b = M x 4 then output is M x 1
elif mode == 'list':
inter = intersect(box_a, box_b, mode=mode)
area_a = (box_a[:, 2] - box_a[:, 0]) * (box_a[:, 3] - box_a[:, 1])
area_b = (box_b[:, 2] - box_b[:, 0]) * (box_b[:, 3] - box_b[:, 1])
union = area_a + area_b - inter
return inter / union
else:
raise ValueError('unknown mode {}'.format(mode))
def intersect(box_a, box_b, mode='cross'):
"""
Computes the amount of intersect between two different sets of boxes.
Args:
box_a (nparray): Mx4 boxes, defined by [x1, y1, x2, y2]
box_a (nparray): Nx4 boxes, defined by [x1, y1, x2, y2]
mode (str): either 'cross' or 'list', where cross will check all combinations of box_a and
box_b hence MxN array, and list expects the same size list M == N, hence returns Mx1 array.
data_type (type): either torch.Tensor or np.ndarray, we automatically determine otherwise
"""
# determine type
data_type = type(box_a)
# this mode computes the intersect in the sense of cross.
# i.e., box_a = M x 4, box_b = N x 4 then the output is M x N
if mode == 'cross':
# np.ndarray
if data_type == np.ndarray:
max_xy = np.minimum(box_a[:, 2:4], np.expand_dims(box_b[:, 2:4], axis=1))
min_xy = np.maximum(box_a[:, 0:2], np.expand_dims(box_b[:, 0:2], axis=1))
inter = np.clip((max_xy - min_xy), a_min=0, a_max=None)
elif data_type == torch.Tensor:
max_xy = torch.min(box_a[:, 2:4], box_b[:, 2:4].unsqueeze(1))
min_xy = torch.max(box_a[:, 0:2], box_b[:, 0:2].unsqueeze(1))
inter = torch.clamp((max_xy - min_xy), 0)
# unknown type
else:
raise ValueError('type {} is not implemented'.format(data_type))
return inter[:, :, 0] * inter[:, :, 1]
# this mode computes the intersect in the sense of list_a vs. list_b.
# i.e., box_a = M x 4, box_b = M x 4 then the output is Mx1
elif mode == 'list':
# torch.Tesnor
if data_type == torch.Tensor:
max_xy = torch.min(box_a[:, 2:], box_b[:, 2:])
min_xy = torch.max(box_a[:, :2], box_b[:, :2])
inter = torch.clamp((max_xy - min_xy), 0)
# np.ndarray
elif data_type == np.ndarray:
max_xy = np.min(box_a[:, 2:], box_b[:, 2:])
min_xy = np.max(box_a[:, :2], box_b[:, :2])
inter = np.clip((max_xy - min_xy), a_min=0, a_max=None)
# unknown type
else:
raise ValueError('unknown data type {}'.format(data_type))
return inter[:, 0] * inter[:, 1]
else:
raise ValueError('unknown mode {}'.format(mode))
def scaled_sigmoid(vals, min=0.0, max=1.0):
"""
Simple helper function for a scaled sigmoid.
The output is bounded by (min, max)
Args:
vals (Tensor): input logits to scale
min (Tensor or float): the minimum value to scale to.
max (Tensor or float): the maximum value to scale to.
"""
return min + (max-min)*torch.sigmoid(vals)
def so3_relative_angle_batched(
R: torch.Tensor,
cos_angle: bool = False,
cos_bound: float = 1e-4,
eps: float = 1e-4,
) -> torch.Tensor:
"""
Calculates the relative angle (in radians) between pairs of
rotation matrices `R1` and `R2` with `angle = acos(0.5 * (Trace(R1 R2^T)-1))`
.. note::
This corresponds to a geodesic distance on the 3D manifold of rotation
matrices.
Args:
R1: Batch of rotation matrices of shape `(minibatch, 3, 3)`.
R2: Batch of rotation matrices of shape `(minibatch, 3, 3)`.
cos_angle: If==True return cosine of the relative angle rather than
the angle itself. This can avoid the unstable calculation of `acos`.
cos_bound: Clamps the cosine of the relative rotation angle to
[-1 + cos_bound, 1 - cos_bound] to avoid non-finite outputs/gradients
of the `acos` call. Note that the non-finite outputs/gradients
are returned when the angle is requested (i.e. `cos_angle==False`)
and the rotation angle is close to 0 or π.
eps: Tolerance for the valid trace check of the relative rotation matrix
in `so3_rotation_angle`.
Returns:
Corresponding rotation angles of shape `(minibatch,)`.
If `cos_angle==True`, returns the cosine of the angles.
Raises:
ValueError if `R1` or `R2` is of incorrect shape.
ValueError if `R1` or `R2` has an unexpected trace.
"""
N = R.shape[0]
n_pairs = N * (N - 1) // 2
Rleft = torch.zeros((n_pairs, 3, 3))
Rright = torch.zeros((n_pairs, 3, 3))
global_idx = 0
for i in range(1, N):
for j in range(i):
p1 = R[i]
p2 = R[j]
Rleft[global_idx] = p1
Rright[global_idx] = p2
global_idx += 1
# gather up the pairs
R12 = torch.matmul(Rleft, Rright.permute(0, 2, 1))
return so3_rotation_angle(R12, cos_angle=cos_angle, cos_bound=cos_bound, eps=eps)
def so3_rotation_angle(
R: torch.Tensor,
eps: float = 1e-4,
cos_angle: bool = False,
cos_bound: float = 1e-4,
) -> torch.Tensor:
"""
Calculates angles (in radians) of a batch of rotation matrices `R` with
`angle = acos(0.5 * (Trace(R)-1))`. The trace of the
input matrices is checked to be in the valid range `[-1-eps,3+eps]`.
The `eps` argument is a small constant that allows for small errors
caused by limited machine precision.
Args:
R: Batch of rotation matrices of shape `(minibatch, 3, 3)`.
eps: Tolerance for the valid trace check.
cos_angle: If==True return cosine of the rotation angles rather than
the angle itself. This can avoid the unstable
calculation of `acos`.
cos_bound: Clamps the cosine of the rotation angle to
[-1 + cos_bound, 1 - cos_bound] to avoid non-finite outputs/gradients
of the `acos` call. Note that the non-finite outputs/gradients
are returned when the angle is requested (i.e. `cos_angle==False`)
and the rotation angle is close to 0 or π.
Returns:
Corresponding rotation angles of shape `(minibatch,)`.
If `cos_angle==True`, returns the cosine of the angles.
Raises:
ValueError if `R` is of incorrect shape.
ValueError if `R` has an unexpected trace.
"""
N, dim1, dim2 = R.shape
if dim1 != 3 or dim2 != 3:
raise ValueError("Input has to be a batch of 3x3 Tensors.")
rot_trace = R[:, 0, 0] + R[:, 1, 1] + R[:, 2, 2]
if ((rot_trace < -1.0 - eps) + (rot_trace > 3.0 + eps)).any():
raise ValueError("A matrix has trace outside valid range [-1-eps,3+eps].")
# phi ... rotation angle
phi_cos = (rot_trace - 1.0) * 0.5
if cos_angle:
return phi_cos
else:
if cos_bound > 0.0:
bound = 1.0 - cos_bound
return acos_linear_extrapolation(phi_cos, (-bound, bound))
else:
return torch.acos(phi_cos)