AndreasLH's picture
upload repo
56bd2b5
import numpy as np
import torch
from cubercnn import util
'''
coordinate system is assumed to have origin in the upper left
(0,0) _________________(N,0)
|
|
|
|
|
(0,M)
'''
"""
class Cube:
'''
3D box in the format [c1, c2, c3, w, h, l, R]
Args:
c1: The x coordinate of the center of the box.
c2: The y coordinate of the center of the box.
c3: The z coordinate of the center of the box.
w: The width of the box in meters.
h: The height of the box in meters.
l: The length of the box in meters.
R: The 3D rotation matrix of the box.
```
_____________________
/| /|
/ | / |
/ | / |
/___|_________________/ |
| | | | h
| | | |
| | | |
| | (c1,c2,c3) | |
| |_________________|___|
| / | /
| / | /
| / | / l
|/_____________________|/
w
```
'''
def __init__(self,tensor: torch.Tensor, R: torch.Tensor, score=None, label=None) -> None:
self.tensor = tensor
self.center = tensor[:3]
self.dimensions = tensor[3:6]
self.rotation = R
# score and label are meant as auxiliary information
self.score = score
self.label = label
def get_cube(self):
color = [c/255.0 for c in util.get_color()]
return util.mesh_cuboid(torch.cat((self.center,self.dimensions)), self.rotation, color=color)
def get_all_corners(self):
'''wrap ``util.get_cuboid_verts_faces``
Returns:
verts: the 3D vertices of the cuboid in camera space'''
verts, _ = util.get_cuboid_verts_faces(torch.cat((self.center,self.dimensions)), self.rotation)
return verts
def get_bube_corners(self,K) -> torch.Tensor:
cube_corners = self.get_all_corners()
cube_corners = torch.mm(K, cube_corners.t()).t()
return cube_corners[:,:2]/cube_corners[:,2].unsqueeze(1)
def get_volume(self) -> float:
return self.dimensions.prod().item()
def __repr__(self) -> str:
return f'Cube({self.center}, {self.dimensions}, {self.rotation})'
def to_device(self, device):
'''
Move all tensors of the instantiated class to the specified device.
Args:
device: The device to move the tensors to (e.g., 'cuda', 'cpu').
'''
self.tensor = self.tensor.to(device)
self.center = self.center.to(device)
self.dimensions = self.dimensions.to(device)
self.rotation = self.rotation.to(device)
return self
"""
class Cubes:
'''
3D boxes in the format [[c1, c2, c3, w, h, l, R1...R9]]
inspired by `detectron2.structures.Boxes`
Args:
tensor: torch.tensor(
c1: The x coordinates of the center of the boxes.
c2: The y coordinates of the center of the boxes.
c3: The z coordinates of the center of the boxes.
w: The width of the boxes in meters.
h: The height of the boxes in meters.
l: The length of the boxes in meters.
R: The flattened 3D rotation matrix of the boxes (i.e. the rows are next to each other).
)
of shape (N, 15).
```
_____________________
/| /|
/ | / |
/ | / |
/___|_________________/ |
| | | | h
| | | |
| | | |
| | (c1,c2,c3) | |
| |_________________|___|
| / | /
| / | /
| / | / l
|/_____________________|/
w
```
'''
def __init__(self,tensor: torch.Tensor, scores=None, labels=None) -> None:
# score and label are meant as auxiliary information
if scores is not None:
assert scores.ndim == 2, f"scores.shape must be (n_instances, n_proposals), but was {scores.shape}"
self.scores = scores
self.labels = labels
if not isinstance(tensor, torch.Tensor):
if not isinstance(tensor, np.ndarray):
tensor = np.asarray(tensor)
tensor = torch.as_tensor(tensor, dtype=torch.float32, device=torch.device("cpu"))
else:
tensor = tensor.to(torch.float32)
if tensor.numel() == 0:
tensor = tensor.reshape((-1, 15)).to(dtype=torch.float32)
self.tensor = tensor
if self.tensor.dim() == 1:
self.tensor = self.tensor.unsqueeze(0)
if self.tensor.dim() == 2:
self.tensor = self.tensor.unsqueeze(0)
@property
def centers(self):
return self.tensor[:, :, :3]
@property
def dimensions(self):
return self.tensor[:, :, 3:6]
@property
def rotations(self):
shape = self.tensor.shape
return self.tensor[:, :, 6:].reshape(shape[0],shape[1], 3, 3)
@property
def device(self):
return self.tensor.device
@property
def num_instances(self):
return self.tensor.shape[0]
@property
def shape(self):
return self.tensor.shape
def clone(self) -> "Cubes":
"""
Clone the Cubes.
Returns:
Cubes
"""
return Cubes(self.tensor.clone())
def get_cubes(self):
color = [c/255.0 for c in util.get_color()]
return util.mesh_cuboid(torch.cat((self.centers.squeeze(0),self.dimensions.squeeze(0)),dim=1), self.rotations.squeeze(0), color=color)
def get_all_corners(self):
'''wrap ``util.get_cuboid_verts_faces``
Returns:
verts: the 3D vertices of the cuboid in camera space'''
verts_list = []
for i in range(self.num_instances):
verts_next_instance, _ = util.get_cuboid_verts_faces(self.tensor[i, :, :6], self.rotations[i])
verts_list.append(verts_next_instance)
verts = torch.stack(verts_list, dim=0)
return verts
def get_cuboids_verts_faces(self):
'''wrap ``util.get_cuboid_verts_faces``
Returns:
verts: the 3D vertices of the cuboid in camera space
faces: the faces of the cuboid in camera space'''
verts_list = []
faces_list = []
for i in range(self.num_instances):
verts_next_instance, faces = util.get_cuboid_verts_faces(self.tensor[i, :, :6], self.rotations[i])
verts_list.append(verts_next_instance)
faces_list.append(faces)
verts = torch.stack(verts_list, dim=0)
faces = torch.stack(faces_list, dim=0)
return verts, faces
def get_bube_corners(self, K, clamp:tuple=None) -> torch.Tensor:
'''This assumes that all the cubes have the same camera intrinsic matrix K
clamp is a typically the image shape (width, height) to truncate the boxes to image frame, this avoids huge projected boxes
Returns:
num_instances x N x 8 x 2'''
cube_corners = self.get_all_corners() # num_instances x N x 8 x 3
num_prop = cube_corners.shape[1]
cube_corners = cube_corners.reshape(self.num_instances * num_prop, 8, 3)
K_repeated = K.repeat(self.num_instances * num_prop,1,1)
cube_corners = torch.matmul(K_repeated, cube_corners.transpose(2,1))
cube_corners = cube_corners[:, :2, :]/cube_corners[:, 2, :].unsqueeze(-2)
cube_corners = cube_corners.transpose(2,1)
cube_corners = cube_corners.reshape(self.num_instances, num_prop, 8, 2)
# we must clamp and then stack, otherwise the gradient is fucked
if clamp is not None:
x = torch.clamp(cube_corners[..., 0], int(-clamp[0]/2+1), int(clamp[0]-1+clamp[0]))
y = torch.clamp(cube_corners[..., 1], int(-clamp[1]/2+1), int(clamp[1]-1+clamp[1]))
cube_corners = torch.stack((x, y), dim=-1)
return cube_corners # num_instances x num_proposals x 8 x 2
def get_volumes(self) -> float:
return self.get_dimensions().prod(1).item()
def __len__(self) -> int:
return self.tensor.shape[0]
def __repr__(self) -> str:
return f'Cubes({self.tensor})'
def to(self, device: torch.device):
# Cubes are assumed float32 and does not support to(dtype)
if isinstance(self.scores, torch.Tensor):
self.scores = self.scores.to(device=device)
if isinstance(self.labels, torch.Tensor):
self.labels = self.labels.to(device=device)
return Cubes(self.tensor.to(device=device), self.scores, self.labels)
def __getitem__(self, item) -> "Cubes":
"""
Args:
item: int, slice, or a BoolTensor
Returns:
Cubes: Create a new :class:`Cubes` by indexing.
The following usage are allowed:
1. `new_cubes = cubes[3]`: return a `Cubes` which contains only one box.
2. `new_cubes = cubes[2:10]`: return a slice of cubes.
3. `new_cubes = cubes[vector]`, where vector is a torch.BoolTensor
with `length = len(cubes)`. Nonzero elements in the vector will be selected.
Note that the returned Cubes might share storage with this Cubes,
subject to Pytorch's indexing semantics.
"""
if isinstance(item, int):
prev_n_prop = self.tensor.shape[1]
return Cubes(self.tensor[item].view(1, prev_n_prop, -1))
elif isinstance(item, tuple):
return Cubes(self.tensor[item[0],item[1]].view(1, 1, -1))
b = self.tensor[item]
assert b.dim() == 2, "Indexing on Cubes with {} failed to return a matrix!".format(item)
return Cubes(b)
@classmethod
def cat(cls, cubes_list: list["Cubes"]) -> "Cubes":
"""
Concatenates a list of Cubes into a single Cubes
Arguments:
cubes_list (list[Cubes])
Returns:
Cubes: the concatenated Cubes
"""
assert isinstance(cubes_list, (list, tuple))
if len(cubes_list) == 0:
return cls(torch.empty(0))
assert all([isinstance(box, Cubes) for box in cubes_list])
# use torch.cat (v.s. layers.cat) so the returned cubes never share storage with input
cat_cubes = cls(torch.cat([b.tensor for b in cubes_list], dim=0))
return cat_cubes
@torch.jit.unused
def __iter__(self):
"""
Yield a cube as a Tensor of shape (15,) at a time.
"""
yield from self.tensor
def split(self, split_size: int, dim=1) -> tuple["Cubes"]:
"""same behaviour as torch.split, return a tuple of chunksize Cubes"""
return tuple(Cubes(x) for x in self.tensor.split(split_size, dim=dim))
def reshape(self, *args) -> "Cubes":
"""
Returns:
Cubes: reshaped Cubes
"""
return Cubes(self.tensor.reshape(*args), self.scores, self.labels)