|
|
|
|
|
import os |
|
import logging |
|
import numpy as np |
|
import trimesh |
|
import pyrender |
|
from PIL import Image, ImageOps |
|
from scipy.linalg import svd |
|
from matplotlib.colors import LinearSegmentedColormap |
|
import torch |
|
from torch.cuda.amp import autocast |
|
from skimage import io, color |
|
from skimage.transform import resize |
|
from ace_util import get_pixel_grid, to_homogeneous |
|
from bisect import insort |
|
|
|
logging.getLogger('trimesh').setLevel(level=logging.WARNING) |
|
_logger = logging.getLogger(__name__) |
|
|
|
THICKNESS = 0.005 * 50 |
|
|
|
|
|
origin_frustum_verts = np.array([ |
|
(0., 0., 0.), |
|
(0.375, -0.375, -1.0), |
|
(0.375, 0.375, -1.0), |
|
(-0.375, 0.375, -1.0), |
|
(-0.375, -0.375, -1.0), |
|
]) |
|
|
|
frustum_edges = np.array([ |
|
(1, 2), |
|
(1, 3), |
|
(1, 4), |
|
(1, 5), |
|
(2, 3), |
|
(3, 4), |
|
(4, 5), |
|
(5, 2), |
|
]) - 1 |
|
|
|
|
|
def normalise_vector(vect): |
|
""" |
|
Returns vector with unit length. |
|
|
|
@param vect: Vector to be normalised. |
|
@return: Normalised vector. |
|
""" |
|
length = np.sqrt((vect ** 2).sum()) |
|
return vect / length |
|
|
|
|
|
def cuboid_from_line(line_start, line_end, color=(255, 0, 255)): |
|
"""Approximates a line with a long cuboid |
|
color is a 3-element RGB tuple, with each element a uint8 value |
|
""" |
|
|
|
|
|
direction = normalise_vector(line_end - line_start) |
|
random_dir = normalise_vector(np.random.rand(3)) |
|
perpendicular_x = normalise_vector(np.cross(direction, random_dir)) |
|
perpendicular_y = normalise_vector(np.cross(direction, perpendicular_x)) |
|
|
|
vertices = [] |
|
for node in (line_start, line_end): |
|
for x_offset in (-1, 1): |
|
for y_offset in (-1, 1): |
|
vert = node + THICKNESS * (perpendicular_y * y_offset + perpendicular_x * x_offset) |
|
vertices.append(vert) |
|
|
|
faces = [ |
|
(4, 5, 1, 0), |
|
(5, 7, 3, 1), |
|
(7, 6, 2, 3), |
|
(6, 4, 0, 2), |
|
(0, 1, 3, 2), |
|
(6, 7, 5, 4), |
|
] |
|
|
|
mesh = trimesh.Trimesh(vertices=np.array(vertices), faces=np.array(faces)) |
|
|
|
for c in (0, 1, 2): |
|
mesh.visual.vertex_colors[:, c] = color[c] |
|
|
|
return mesh |
|
|
|
|
|
def generate_frustum_marker(pose, color=(255, 0, 255), size=1.): |
|
frustum_vertices = np.array([ |
|
[0., 0., 0., 1.], |
|
[1., 1., 3., 1.], |
|
[-1., 1., 3., 1.], |
|
[-1., -1., 3., 1.], |
|
[1., -1., 3., 1.] |
|
]).T |
|
|
|
frustum_vertices[:3] *= size |
|
frustum_vertices[2, :] *= -1 |
|
frustum_vertices = pose @ frustum_vertices |
|
frustum_vertices = frustum_vertices[:3].T |
|
|
|
frustum_faces = np.array([ |
|
[0, 4, 1], |
|
[0, 1, 2], |
|
[0, 2, 3], |
|
[0, 3, 4], |
|
[4, 2, 1], |
|
[4, 3, 2], |
|
]) |
|
|
|
mesh = trimesh.Trimesh(vertices=frustum_vertices, faces=frustum_faces) |
|
|
|
for c in (0, 1, 2): |
|
mesh.visual.vertex_colors[:, c] = color[c] |
|
|
|
return mesh |
|
|
|
|
|
def get_image_box( |
|
image_path, |
|
frustum_pose, |
|
cam_marker_size=1.0, |
|
flip=False |
|
): |
|
""" Gets a textured mesh of an image. |
|
|
|
@param image_path: File path of the image to be rendered. |
|
@param frustum_pose: 4x4 camera pose, OpenGL convention |
|
@param cam_marker_size: Scaling factor for the image object |
|
@param flip: flag whether to flip the image left/right |
|
@return: duple, trimesh mesh of the image and aspect ratio of the image |
|
""" |
|
|
|
pil_image = Image.open(image_path) |
|
pil_image = ImageOps.flip(pil_image) |
|
|
|
pil_image_w, pil_image_h = pil_image.size |
|
aspect_ratio = pil_image_w / pil_image_h |
|
|
|
height = 0.75 |
|
width = height * aspect_ratio |
|
width *= cam_marker_size |
|
height *= cam_marker_size |
|
|
|
if flip: |
|
pil_image = ImageOps.mirror(pil_image) |
|
width = -width |
|
|
|
vertices = np.zeros((4, 3)) |
|
vertices[0, :] = [width / 2, height / 2, -cam_marker_size] |
|
vertices[1, :] = [width / 2, -height / 2, -cam_marker_size] |
|
vertices[2, :] = [-width / 2, -height / 2, -cam_marker_size] |
|
vertices[3, :] = [-width / 2, height / 2, -cam_marker_size] |
|
|
|
faces = np.zeros((2, 3)) |
|
faces[0, :] = [0, 1, 2] |
|
faces[1, :] = [2, 3, 0] |
|
|
|
|
|
|
|
uvs = np.zeros((4, 2)) |
|
|
|
uvs[0, :] = [1.0, 0] |
|
uvs[1, :] = [1.0, 1.0] |
|
uvs[2, :] = [0, 1.0] |
|
uvs[3, :] = [0, 0] |
|
|
|
face_normals = np.zeros((2, 3)) |
|
face_normals[0, :] = [0.0, 0.0, 1.0] |
|
face_normals[1, :] = [0.0, 0.0, 1.0] |
|
|
|
material = trimesh.visual.texture.SimpleMaterial( |
|
image=pil_image, |
|
ambient=(1.0, 1.0, 1.0, 1.0), |
|
diffuse=(1.0, 1.0, 1.0, 1.0), |
|
) |
|
texture = trimesh.visual.TextureVisuals( |
|
uv=uvs, |
|
image=pil_image, |
|
material=material, |
|
) |
|
|
|
mesh = trimesh.Trimesh( |
|
vertices=vertices, |
|
faces=faces, |
|
face_normals=face_normals, |
|
visual=texture, |
|
validate=True, |
|
process=False |
|
) |
|
|
|
|
|
def transform_trimesh(mesh, transform): |
|
""" Applies a transform to a trimesh. """ |
|
np_vertices = np.array(mesh.vertices) |
|
np_vertices = (transform @ np.concatenate([np_vertices, np.ones((np_vertices.shape[0], 1))], 1).T).T |
|
np_vertices = np_vertices / np_vertices[:, 3][:, None] |
|
mesh.vertices[:, 0] = np_vertices[:, 0] |
|
mesh.vertices[:, 1] = np_vertices[:, 1] |
|
mesh.vertices[:, 2] = np_vertices[:, 2] |
|
|
|
return mesh |
|
|
|
return transform_trimesh(mesh, frustum_pose), aspect_ratio |
|
|
|
|
|
def generate_frustum_at_position(rotation, translation, color, size, aspect_ratio): |
|
"""Generates a frustum mesh at a specified (rotation, translation), with optional color |
|
: rotation is a 3x3 numpy array |
|
: translation is a 3-long numpy vector |
|
: color is a 3-long numpy vector or tuple or list; each element is a uint8 RGB value |
|
: aspect_ratio is a float of width/height |
|
""" |
|
|
|
frustum_verts = origin_frustum_verts.copy() |
|
frustum_verts[:, 0] *= aspect_ratio |
|
|
|
transformed_frustum_verts = \ |
|
size * rotation.dot(frustum_verts.T).T + translation[None, :] |
|
|
|
cuboids = [] |
|
for edge in frustum_edges: |
|
line_cuboid = cuboid_from_line(line_start=transformed_frustum_verts[edge[0]], |
|
line_end=transformed_frustum_verts[edge[1]], |
|
color=color) |
|
cuboids.append(line_cuboid) |
|
|
|
return trimesh.util.concatenate(cuboids) |
|
|
|
|
|
class LazyCamera: |
|
"""Smooth and slightly delayed scene camera. |
|
|
|
Implements a rolling average of last few camera positions. |
|
Also zooms out to display the whole scene. |
|
""" |
|
|
|
def __init__(self, |
|
camera_buffer_size=40, |
|
backwards_offset=4, |
|
camera_buffer=None): |
|
"""Constructor. |
|
|
|
Parameters: |
|
camera_buffer_size: Number of last few cameras to consider |
|
backwards_offset: Move observing camera backwards from current view, in meters |
|
camera_buffer: Optional array of camera positions to pre-fill the buffer |
|
""" |
|
|
|
|
|
if camera_buffer is None: |
|
self.m_camera_buffer = [] |
|
else: |
|
self.m_camera_buffer = camera_buffer |
|
|
|
self.m_camera_buffer_size = camera_buffer_size |
|
self.m_backwards_offset = backwards_offset |
|
|
|
def _orthonormalize_rotation(self, T): |
|
"""Takes a 4x4 matrix and orthonormalizes the upper left 3x3 using SVD |
|
|
|
Returns: |
|
T with orthonormalized upper 3x3 |
|
""" |
|
|
|
R = T[:3, :3] |
|
t = T[:3, 3] |
|
|
|
|
|
U, S, Vt = svd(R) |
|
Z = np.eye(3) |
|
Z[-1, -1] = np.sign(np.linalg.det(U @ Vt)) |
|
R = U @ Z @ Vt |
|
|
|
T = np.eye(4) |
|
T[:3, :3] = R |
|
T[:3, 3] = t |
|
|
|
return T |
|
|
|
def update_camera(self, view): |
|
"""Update lazy camera with new view. |
|
|
|
Parameters: |
|
view: New camera view, 4x4 matrix |
|
""" |
|
|
|
observing_camera = view.copy() |
|
|
|
|
|
z_vec = np.zeros((3,)) |
|
z_vec[2] = 1 |
|
offset_vector = view[:3, :3] @ z_vec |
|
observing_camera[:3, 3] += offset_vector * self.m_backwards_offset |
|
|
|
|
|
self.m_camera_buffer.append(observing_camera) |
|
|
|
if len(self.m_camera_buffer) > self.m_camera_buffer_size: |
|
self.m_camera_buffer = self.m_camera_buffer[1:] |
|
|
|
def get_current_view(self): |
|
"""Get current lazy camera view for rendering. |
|
|
|
Returns: |
|
4x4 matrix |
|
""" |
|
|
|
|
|
smooth_camera_pose = np.zeros((4, 4)) |
|
for camera_pose in self.m_camera_buffer: |
|
smooth_camera_pose += camera_pose |
|
smooth_camera_pose /= len(self.m_camera_buffer) |
|
|
|
return self._orthonormalize_rotation(smooth_camera_pose) |
|
|
|
def get_camera_buffer(self): |
|
""" |
|
Return buffered camera views, e.g. for storing state. |
|
""" |
|
return self.m_camera_buffer |
|
|
|
|
|
class PointCloudBuffer: |
|
"""Holds last N point clouds.""" |
|
|
|
def __init__(self, pc_buffer_size=500, use_mask=True): |
|
"""Constructor. |
|
|
|
Parameters: |
|
pc_buffer_size: Number of last N point clouds to hold |
|
""" |
|
|
|
self.pc_buffer_size = pc_buffer_size |
|
self.use_mask = use_mask |
|
|
|
self.pc_xyz_buffer = [] |
|
self.pc_clr_buffer = [] |
|
self.pc_err_buffer = [] |
|
self.pc_mask_buffer = [] |
|
|
|
def set_mask_buffer(self, mask_buffer): |
|
self.pc_mask_buffer = mask_buffer |
|
|
|
def update_buffer(self, pc_xyz, pc_clr, pc_errs=None, pc_mask=None): |
|
""" |
|
Add a new (partial) point cloud to the buffer. |
|
|
|
@param pc_xyz: N3, coordinates of points |
|
@param pc_clr: N3, RGB colors of points |
|
@param pc_errs: N1, scalar errors of points |
|
""" |
|
self.pc_xyz_buffer.append(pc_xyz) |
|
self.pc_clr_buffer.append(pc_clr) |
|
self.pc_mask_buffer.append(pc_mask) |
|
|
|
|
|
|
|
|
|
if pc_errs is not None: |
|
self.pc_err_buffer.append(pc_errs) |
|
|
|
|
|
if 0 < self.pc_buffer_size < len(self.pc_xyz_buffer): |
|
self.pc_xyz_buffer = self.pc_xyz_buffer[1:] |
|
self.pc_clr_buffer = self.pc_clr_buffer[1:] |
|
|
|
|
|
if 0 < self.pc_buffer_size < len(self.pc_err_buffer): |
|
self.pc_err_buffer = self.pc_err_buffer[1:] |
|
|
|
def clear_buffer(self): |
|
""" |
|
Clear the buffer. |
|
""" |
|
self.pc_xyz_buffer = [] |
|
self.pc_clr_buffer = [] |
|
self.pc_mask_buffer = [] |
|
|
|
|
|
def get_point_cloud(self): |
|
""" |
|
Merges and returns all point clouds in the buffer. |
|
|
|
@return: triple, N3 xyz + N3 colors + N1 errors |
|
""" |
|
|
|
merged_xyz = np.concatenate(self.pc_xyz_buffer) |
|
merged_clr = np.concatenate(self.pc_clr_buffer) |
|
merged_mask = np.concatenate(self.pc_mask_buffer) |
|
|
|
if len(self.pc_err_buffer) > 0: |
|
merged_errs = np.concatenate(self.pc_err_buffer) |
|
else: |
|
merged_errs = None |
|
|
|
masked_xyz = merged_xyz[merged_mask.astype(bool)] |
|
masked_clr = merged_clr[merged_mask.astype(bool)] |
|
masked_errs = merged_errs[merged_mask.astype(bool)] if merged_errs is not None else None |
|
|
|
return masked_xyz, masked_clr, masked_errs |
|
|
|
def disable_buffer_cap(self): |
|
""" |
|
Switch rolling buffer of fixed size to unconstrained buffer. |
|
""" |
|
self.pc_buffer_size = -1 |
|
|
|
|
|
def get_retro_colors(): |
|
""" |
|
Create custom color map, dark magenta to bright cyan. |
|
|
|
if you like this color map and use it in your own work, let me know |
|
https://twitter.com/eric_brachmann |
|
looking forward to seeing what you do with it :) |
|
-- Eric |
|
|
|
@return: Color lookup table, 256x3 |
|
""" |
|
|
|
cdict = {'red': [ |
|
[0.0, 0.073, 0.073], |
|
[0.4, 0.325, 0.325], |
|
[0.7, 0.286, 0.286], |
|
[0.85, 0.266, 0.266], |
|
[0.95, 0, 0], |
|
[1, 1, 1], |
|
], |
|
'green': [ |
|
[0.0, 0.0, 0.0], |
|
[0.4, 0.058, 0.058], |
|
[0.7, 0.470, 0.470], |
|
[0.85, 0.827, 0.827], |
|
[0.95, 1, 1], |
|
[1, 1, 1], |
|
], |
|
'blue': [ |
|
[0.0, 0.057, 0.057], |
|
[0.4, 0.223, 0.223], |
|
[0.7, 0.752, 0.752], |
|
[0.85, 0.988, 0.988], |
|
[0.95, 1, 1], |
|
[1, 1, 1], |
|
]} |
|
|
|
retroColorMap = LinearSegmentedColormap('retroColors', segmentdata=cdict, N=256) |
|
|
|
return retroColorMap(np.linspace(0, 1, 257))[1:, :3] |
|
|
|
|
|
def get_point_cloud_from_network(network, data_loader, filter_depth, dense_cloud=False): |
|
""" |
|
Extract a point cloud from a fully trained network. |
|
|
|
@param network: scene coordinate regression network |
|
@param data_loader: loader for the mapping sequence |
|
@param filter_depth: in meters, remove points further from the camera |
|
@param dense_cloud: if True, return all points (good to initialise splats), otherwise filter based on repro error |
|
@return: tuple, N3 coordinates + N3 RGB colors |
|
""" |
|
|
|
|
|
|
|
grad_thresholds = [0.1, 0.5, 1.0, torch.inf] |
|
|
|
|
|
|
|
pc_points_min = 100000 |
|
pc_points_max = 1000000 |
|
|
|
|
|
repro_threshold = 1 |
|
|
|
if dense_cloud: |
|
|
|
grad_thresholds = [torch.inf] |
|
repro_threshold = torch.inf |
|
|
|
pc_points_per_image_min = int(pc_points_min / len(data_loader)) |
|
pc_points_per_image_max = int(pc_points_max / len(data_loader)) |
|
|
|
pixel_grid = get_pixel_grid(network.OUTPUT_SUBSAMPLE) |
|
|
|
pc_xyz = [] |
|
pc_clr = [] |
|
pc_mask = [] |
|
file_list = [] |
|
with torch.no_grad(): |
|
|
|
|
|
for image, _, gt_inv_pose, _, K, _, _, file, _ in data_loader: |
|
|
|
|
|
image = image.cuda(non_blocking=True) |
|
gt_inv_pose = gt_inv_pose.cuda(non_blocking=True) |
|
K = K.cuda(non_blocking=True) |
|
|
|
with autocast(): |
|
scene_coords = network(image) |
|
|
|
B, C, H, W = scene_coords.shape |
|
|
|
assert B == 1, "Batch size must be 1 for point cloud extraction." |
|
|
|
|
|
pred_scene_coords_B3HW = scene_coords.float() |
|
pred_scene_coords_B4N = to_homogeneous(pred_scene_coords_B3HW.flatten(2)) |
|
pred_cam_coords_B3N = torch.matmul(gt_inv_pose[:, :3], pred_scene_coords_B4N) |
|
|
|
|
|
pred_px_B3N = torch.matmul(K, pred_cam_coords_B3N) |
|
pred_px_B3N[:, 2].clamp_(min=0.1) |
|
pred_px_B2N = pred_px_B3N[:, :2] / pred_px_B3N[:, 2, None] |
|
|
|
|
|
pixel_positions_2HW = pixel_grid[:, :H, :W].clone() |
|
pixel_positions_2N = pixel_positions_2HW.view(2, -1) |
|
|
|
reprojection_error_2N = pred_px_B2N.squeeze() - pixel_positions_2N.cuda() |
|
reprojection_error_1N = torch.norm(reprojection_error_2N, dim=0, keepdim=True, p=1) |
|
|
|
|
|
grad_x_BHW = torch.linalg.norm(pred_scene_coords_B3HW[:, :, :, 1:] - pred_scene_coords_B3HW[:, :, :, :-1], |
|
dim=1) |
|
grad_x_BHW = torch.nn.functional.pad(grad_x_BHW, (1, 0), mode='reflect') |
|
grad_y_BHW = torch.linalg.norm(pred_scene_coords_B3HW[:, :, 1:, :] - pred_scene_coords_B3HW[:, :, :-1, :], |
|
dim=1) |
|
grad_y_BHW = torch.nn.functional.pad(grad_y_BHW, (0, 0, 1, 0), mode='reflect') |
|
|
|
grad_BHW = torch.max(grad_x_BHW, grad_y_BHW) |
|
grad_1N = grad_BHW.view(B, -1) |
|
|
|
|
|
for grad_threshold in grad_thresholds: |
|
sc_grad_mask = grad_1N.squeeze() < grad_threshold |
|
if sc_grad_mask.sum() > pc_points_per_image_min: |
|
break |
|
|
|
|
|
sc_depth_mask = pred_cam_coords_B3N[0, 2] < filter_depth |
|
|
|
sc_grad_and_depth_mask = torch.logical_and(sc_grad_mask, sc_depth_mask) |
|
|
|
|
|
if sc_grad_and_depth_mask.sum() == 0: |
|
sc_grad_and_depth_mask[:] = True |
|
|
|
|
|
sc_err_mask = reprojection_error_1N.squeeze() < repro_threshold |
|
sc_err_mask = torch.logical_and(sc_err_mask, sc_grad_and_depth_mask) |
|
|
|
|
|
num_valid_points = int(sc_err_mask.sum()) |
|
|
|
if num_valid_points < pc_points_per_image_min: |
|
|
|
reprojection_error_within_range_and_smooth_1N = reprojection_error_1N.squeeze()[sc_grad_and_depth_mask] |
|
|
|
sorted_errors, _ = torch.sort(reprojection_error_within_range_and_smooth_1N) |
|
relaxed_filter_repro_error = sorted_errors[min(pc_points_per_image_min, sorted_errors.shape[0] - 1)] |
|
|
|
sc_err_mask = reprojection_error_1N.squeeze() < relaxed_filter_repro_error |
|
sc_err_mask = torch.logical_and(sc_grad_and_depth_mask, sc_err_mask) |
|
elif num_valid_points > pc_points_per_image_max: |
|
|
|
keep_ratio = pc_points_per_image_max / num_valid_points |
|
sub_sample_mask = torch.randperm(num_valid_points) < int(keep_ratio * num_valid_points) |
|
sc_err_mask_subsampled = sc_err_mask.clone() |
|
sc_err_mask_subsampled[sc_err_mask] = sub_sample_mask.cuda() |
|
sc_err_mask = sc_err_mask_subsampled |
|
|
|
|
|
rgb = io.imread(file[0]) |
|
|
|
if len(rgb.shape) < 3: |
|
rgb = color.gray2rgb(rgb) |
|
|
|
|
|
rgb = rgb.astype('float64') |
|
|
|
rgb = resize(rgb, image.shape[2:]) |
|
|
|
|
|
nn_stride = network.OUTPUT_SUBSAMPLE |
|
nn_offset = network.OUTPUT_SUBSAMPLE // 2 |
|
rgb = rgb[nn_offset::nn_stride, nn_offset::nn_stride, :] |
|
|
|
rgb = resize(rgb, scene_coords.shape[2:]) |
|
rgb = torch.from_numpy(rgb).permute(2, 0, 1) |
|
rgb = rgb.contiguous().view(3, -1) |
|
|
|
|
|
rgb = rgb[:, sc_err_mask.cpu()] |
|
xyz = pred_scene_coords_B4N[0, :3, sc_err_mask].cpu() |
|
print('pred_scene_coords_B4N_shape', pred_scene_coords_B4N.shape) |
|
pc_xyz.append(xyz.numpy()) |
|
pc_clr.append(rgb.numpy()) |
|
pc_mask.append(sc_err_mask.cpu()) |
|
file_list.append(file[0]) |
|
|
|
|
|
pc_xyz = np.concatenate(pc_xyz, axis=1) |
|
pc_clr = np.concatenate(pc_clr, axis=1) |
|
|
|
|
|
|
|
pc_xyz = np.transpose(pc_xyz) |
|
pc_clr = np.transpose(pc_clr) |
|
|
|
|
|
pc_xyz[:, 1] = -pc_xyz[:, 1] |
|
pc_xyz[:, 2] = -pc_xyz[:, 2] |
|
|
|
|
|
return pc_xyz, pc_clr, pc_mask, file_list, image.shape[2:], scene_coords.shape[2:] |
|
|
|
|
|
def get_rendering_target_path(target_base_path, map_file_name): |
|
""" |
|
Infer a folder for renderings from a base path and a map name. |
|
|
|
Creates target folder if it does not exist. |
|
|
|
@param target_base_path: Base path for all renderings. |
|
@param map_file_name: Map file name to infer folder name for renderings of this mapping run. |
|
@return: path to store renderings |
|
""" |
|
target_path = map_file_name |
|
target_path = os.path.basename(target_path) |
|
target_path = os.path.splitext(target_path)[0] |
|
target_path = target_base_path / target_path |
|
|
|
os.makedirs(target_path, exist_ok=True) |
|
|
|
return target_path |
|
|
|
|
|
class CameraTrajectoryBuffer: |
|
"""Incrementally builds a camera trajectory mesh.""" |
|
|
|
def __init__(self, |
|
frustum_skip, |
|
frustum_scale): |
|
""" |
|
Constructor. |
|
|
|
Initialises standard values. |
|
|
|
@param frustum_skip: minimum distance between placing frustums, in meters |
|
@param frustum_scale: Scaling factor for camera frustums |
|
""" |
|
self.frustum_skip = frustum_skip |
|
self.frustum_scale = frustum_scale |
|
|
|
self.trajectory = [] |
|
self.frustums = [] |
|
self.frustum_images = [] |
|
|
|
self.trajectory_previous = None |
|
self.frustum_positions = [] |
|
self.trajectory_distances = [] |
|
|
|
self.trajectory_color = (255, 255, 255) |
|
self.aspect_ratio_buffer = 4 / 3 |
|
global THICKNESS |
|
if frustum_scale < 10: |
|
THICKNESS = 0.005 |
|
|
|
def grow_camera_path(self, new_camera): |
|
""" |
|
Expand the camera trajectory line wrt new camera. |
|
|
|
Keeps track of camera movement statistics and skips the line if a camera jump is detected. |
|
|
|
@param new_camera: 4x4 camera pose, OpenGL convention |
|
""" |
|
|
|
current_pos = new_camera[:3, 3] |
|
|
|
|
|
if self.trajectory_previous is not None: |
|
|
|
current_dist = np.linalg.norm(current_pos - self.trajectory_previous) |
|
|
|
insort(self.trajectory_distances, current_dist) |
|
|
|
line_skip = 10 * self.trajectory_distances[len(self.trajectory_distances) // 2] |
|
|
|
if 0.0001 < current_dist < line_skip: |
|
line_cuboid = cuboid_from_line(line_start=self.trajectory_previous, |
|
line_end=current_pos, |
|
color=self.trajectory_color) |
|
|
|
self.trajectory.append(line_cuboid) |
|
else: |
|
if current_dist > line_skip: |
|
_logger.info(f"Detected jump: camera dist={current_dist:.3f}, threshold={line_skip:.3f}, " |
|
f"threshold estimated from {len(self.trajectory_distances)} estimates.") |
|
|
|
|
|
self.trajectory_previous = current_pos |
|
|
|
def add_position_marker(self, marker_pose, marker_color, marker_extent=0.015, frustum_maker=False): |
|
""" |
|
Adds a cube to the trajectory mesh to signify a singular camera position. |
|
|
|
@param marker_pose: 4x4 camera pose, OpenGL convention |
|
@param marker_color: RGB color of the marker |
|
@param marker_extent: size of the marker, marker is a cube of this side length |
|
""" |
|
|
|
if frustum_maker: |
|
current_pos_marker = generate_frustum_marker(marker_pose, marker_color, marker_extent) |
|
else: |
|
current_pos_marker = trimesh.primitives.Box( |
|
extents=(marker_extent, marker_extent, marker_extent), |
|
transform=marker_pose) |
|
for c in (0, 1, 2): |
|
|
|
current_pos_marker.visual.vertex_colors[:, c] = marker_color[c] |
|
|
|
self.trajectory.append(current_pos_marker) |
|
|
|
def _get_closest_frustum_distance(self, new_camera): |
|
""" |
|
Calculate distance to the closest, previously placed frustum in the trajectory so far. |
|
|
|
@param new_camera: 4x4 camera, OpenGL convention |
|
@return: distance to the closest frustum in the trajectory |
|
""" |
|
if len(self.frustum_positions) == 0: |
|
return self.frustum_skip + 1 |
|
else: |
|
distances = [np.linalg.norm(pos - new_camera[:3, 3]) for pos in self.frustum_positions] |
|
return min(distances) |
|
|
|
def add_camera_frustum(self, camera, image_file=None, sparse=True, frustum_color=None): |
|
""" |
|
Add a camera frustum object to the trajectory, minding distance to existing frustums. |
|
|
|
@param camera: 4x4 camera pose, OpenGL convention |
|
@param image_file: path to image to be displayed in frustum |
|
@param sparse: flag, if true a frustum is not placed if too close to existing frustums |
|
@param frustum_color: RGB color, if none default color is used |
|
""" |
|
new_camera = camera.copy() |
|
|
|
if frustum_color is None: |
|
frustum_color = self.trajectory_color |
|
|
|
|
|
if (sparse == False) or (self._get_closest_frustum_distance(new_camera) > self.frustum_skip): |
|
|
|
if image_file is not None: |
|
image_mesh, self.aspect_ratio_buffer = get_image_box(image_path=image_file, |
|
frustum_pose=new_camera, |
|
flip=True, |
|
cam_marker_size=self.frustum_scale) |
|
|
|
image_mesh = pyrender.Mesh.from_trimesh(image_mesh) |
|
self.frustum_images.append(image_mesh) |
|
|
|
frustum = generate_frustum_at_position(rotation=new_camera[:3, :3], |
|
translation=new_camera[:3, 3], |
|
color=frustum_color, |
|
size=self.frustum_scale, |
|
aspect_ratio=self.aspect_ratio_buffer) |
|
|
|
self.frustums.append(frustum) |
|
self.frustum_positions.append(new_camera[:3, 3]) |
|
|
|
def clear_frustums(self): |
|
""" |
|
Clear all existing frustums in the trajectory. |
|
""" |
|
self.frustums.clear() |
|
self.frustum_images.clear() |
|
self.frustum_previous = None |
|
|
|
def get_mesh(self): |
|
""" |
|
Turn trajectory into pyrender mesh. |
|
|
|
Frustum images are returned separately since merging textured and non-textured objects creates artifacts. |
|
|
|
@return: tuple, trajectory mesh + list of frustum image objects |
|
""" |
|
|
|
trajectory_mesh = self.trajectory + self.frustums |
|
trajectory_mesh = trimesh.util.concatenate(trajectory_mesh) |
|
trajectory_mesh = pyrender.Mesh.from_trimesh(trajectory_mesh) |
|
|
|
return trajectory_mesh, self.frustum_images |
|
|