|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import torch |
|
import torch.nn as nn |
|
import torch.nn.functional as F |
|
|
|
import numpy as np |
|
import pycolmap |
|
|
|
|
|
def batch_matrix_to_pycolmap( |
|
points3d, |
|
extrinsics, |
|
intrinsics, |
|
tracks, |
|
image_size, |
|
masks=None, |
|
max_reproj_error=None, |
|
max_points3D_val=3000, |
|
shared_camera=False, |
|
camera_type="SIMPLE_PINHOLE", |
|
extra_params=None, |
|
): |
|
""" |
|
Convert Batched Pytorch Tensors to PyCOLMAP |
|
|
|
Check https://github.com/colmap/pycolmap for more details about its format |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
N, P, _ = tracks.shape |
|
assert len(extrinsics) == N |
|
assert len(intrinsics) == N |
|
assert len(points3d) == P |
|
assert image_size.shape[0] == 2 |
|
|
|
projected_points_2d, projected_points_cam = project_3D_points(points3d, extrinsics, intrinsics, return_points_cam=True) |
|
projected_diff = (projected_points_2d - tracks).norm(dim=-1) |
|
projected_points_2d[projected_points_cam[:, -1] <= 0] = 1e6 |
|
reproj_mask = projected_diff < max_reproj_error |
|
|
|
if masks is not None: |
|
masks = torch.logical_and(masks, reproj_mask) |
|
else: |
|
masks = reproj_mask |
|
|
|
extrinsics = extrinsics.cpu().numpy() |
|
intrinsics = intrinsics.cpu().numpy() |
|
|
|
if extra_params is not None: |
|
extra_params = extra_params.cpu().numpy() |
|
|
|
|
|
tracks = tracks.cpu().numpy() |
|
points3d = points3d.cpu().numpy() |
|
image_size = image_size.cpu().numpy() |
|
|
|
|
|
reconstruction = pycolmap.Reconstruction() |
|
|
|
masks = masks.cpu().numpy() |
|
|
|
inlier_num = masks.sum(0) |
|
valid_mask = inlier_num >= 2 |
|
valid_idx = np.nonzero(valid_mask)[0] |
|
|
|
|
|
for vidx in valid_idx: |
|
reconstruction.add_point3D( |
|
points3d[vidx], pycolmap.Track(), np.zeros(3) |
|
) |
|
|
|
num_points3D = len(valid_idx) |
|
|
|
camera = None |
|
|
|
for fidx in range(N): |
|
|
|
if camera is None or (not shared_camera): |
|
if camera_type == "SIMPLE_RADIAL": |
|
focal = (intrinsics[fidx][0, 0] + intrinsics[fidx][1, 1]) / 2 |
|
pycolmap_intri = np.array( |
|
[ |
|
focal, |
|
intrinsics[fidx][0, 2], |
|
intrinsics[fidx][1, 2], |
|
extra_params[fidx][0], |
|
] |
|
) |
|
elif camera_type == "SIMPLE_PINHOLE": |
|
focal = (intrinsics[fidx][0, 0] + intrinsics[fidx][1, 1]) / 2 |
|
pycolmap_intri = np.array( |
|
[ |
|
focal, |
|
intrinsics[fidx][0, 2], |
|
intrinsics[fidx][1, 2], |
|
] |
|
) |
|
else: |
|
raise ValueError( |
|
f"Camera type {camera_type} is not supported yet" |
|
) |
|
|
|
camera = pycolmap.Camera( |
|
model=camera_type, |
|
width=image_size[0], |
|
height=image_size[1], |
|
params=pycolmap_intri, |
|
camera_id=fidx, |
|
) |
|
|
|
|
|
reconstruction.add_camera(camera) |
|
|
|
|
|
cam_from_world = pycolmap.Rigid3d( |
|
pycolmap.Rotation3d(extrinsics[fidx][:3, :3]), |
|
extrinsics[fidx][:3, 3], |
|
) |
|
image = pycolmap.Image( |
|
id=fidx, |
|
name=f"image_{fidx}", |
|
camera_id=camera.camera_id, |
|
cam_from_world=cam_from_world, |
|
) |
|
|
|
points2D_list = [] |
|
|
|
point2D_idx = 0 |
|
|
|
for point3D_id in range(1, num_points3D + 1): |
|
original_track_idx = valid_idx[point3D_id - 1] |
|
|
|
if ( |
|
reconstruction.points3D[point3D_id].xyz < max_points3D_val |
|
).all(): |
|
if masks[fidx][original_track_idx]: |
|
|
|
point2D_xy = tracks[fidx][original_track_idx] |
|
|
|
|
|
points2D_list.append( |
|
pycolmap.Point2D(point2D_xy, point3D_id) |
|
) |
|
|
|
|
|
track = reconstruction.points3D[point3D_id].track |
|
track.add_element(fidx, point2D_idx) |
|
point2D_idx += 1 |
|
|
|
assert point2D_idx == len(points2D_list) |
|
|
|
try: |
|
image.points2D = pycolmap.ListPoint2D(points2D_list) |
|
image.registered = True |
|
except: |
|
print(f"frame {fidx} is out of BA") |
|
image.registered = False |
|
|
|
|
|
reconstruction.add_image(image) |
|
|
|
return reconstruction |
|
|
|
|
|
def pycolmap_to_batch_matrix( |
|
reconstruction, device="cuda", camera_type="SIMPLE_PINHOLE" |
|
): |
|
""" |
|
Convert a PyCOLMAP Reconstruction Object to batched PyTorch tensors. |
|
|
|
Args: |
|
reconstruction (pycolmap.Reconstruction): The reconstruction object from PyCOLMAP. |
|
device (str): The device to place the tensors on (default: "cuda"). |
|
camera_type (str): The type of camera model used (default: "SIMPLE_PINHOLE"). |
|
|
|
Returns: |
|
tuple: A tuple containing points3D, extrinsics, intrinsics, and optionally extra_params. |
|
""" |
|
|
|
num_images = len(reconstruction.images) |
|
max_points3D_id = max(reconstruction.point3D_ids()) |
|
points3D = np.zeros((max_points3D_id, 3)) |
|
|
|
for point3D_id in reconstruction.points3D: |
|
points3D[point3D_id - 1] = reconstruction.points3D[point3D_id].xyz |
|
points3D = torch.from_numpy(points3D).to(device) |
|
|
|
extrinsics = [] |
|
intrinsics = [] |
|
|
|
extra_params = [] if camera_type == "SIMPLE_RADIAL" else None |
|
|
|
for i in range(num_images): |
|
|
|
pyimg = reconstruction.images[i] |
|
pycam = reconstruction.cameras[pyimg.camera_id] |
|
matrix = pyimg.cam_from_world.matrix() |
|
extrinsics.append(matrix) |
|
|
|
|
|
calibration_matrix = pycam.calibration_matrix() |
|
intrinsics.append(calibration_matrix) |
|
|
|
if camera_type == "SIMPLE_RADIAL": |
|
extra_params.append(pycam.params[-1]) |
|
|
|
|
|
extrinsics = torch.from_numpy(np.stack(extrinsics)).to(device) |
|
|
|
intrinsics = torch.from_numpy(np.stack(intrinsics)).to(device) |
|
|
|
if camera_type == "SIMPLE_RADIAL": |
|
extra_params = torch.from_numpy(np.stack(extra_params)).to(device) |
|
extra_params = extra_params[:, None] |
|
|
|
return points3D, extrinsics, intrinsics, extra_params |
|
|
|
|
|
|
|
|
|
|
|
def project_3D_points( |
|
points3D, |
|
extrinsics, |
|
intrinsics=None, |
|
extra_params=None, |
|
return_points_cam=False, |
|
default=0, |
|
only_points_cam=False, |
|
): |
|
""" |
|
Transforms 3D points to 2D using extrinsic and intrinsic parameters. |
|
Args: |
|
points3D (torch.Tensor): 3D points of shape Px3. |
|
extrinsics (torch.Tensor): Extrinsic parameters of shape Bx3x4. |
|
intrinsics (torch.Tensor): Intrinsic parameters of shape Bx3x3. |
|
extra_params (torch.Tensor): Extra parameters of shape BxN, which is used for radial distortion. |
|
Returns: |
|
torch.Tensor: Transformed 2D points of shape BxNx2. |
|
""" |
|
with torch.cuda.amp.autocast(dtype=torch.double): |
|
N = points3D.shape[0] |
|
B = extrinsics.shape[0] |
|
points3D_homogeneous = torch.cat( |
|
[points3D, torch.ones_like(points3D[..., 0:1])], dim=1 |
|
) |
|
|
|
points3D_homogeneous = points3D_homogeneous.unsqueeze(0).expand( |
|
B, -1, -1 |
|
) |
|
|
|
|
|
|
|
points_cam = torch.bmm( |
|
extrinsics, points3D_homogeneous.transpose(-1, -2) |
|
) |
|
|
|
if only_points_cam: |
|
return points_cam |
|
|
|
|
|
points2D = img_from_cam(intrinsics, points_cam, extra_params) |
|
|
|
if return_points_cam: |
|
return points2D, points_cam |
|
return points2D |
|
|
|
|
|
def img_from_cam(intrinsics, points_cam, extra_params=None, default=0.0): |
|
""" |
|
Applies intrinsic parameters and optional distortion to the given 3D points. |
|
|
|
Args: |
|
intrinsics (torch.Tensor): Intrinsic camera parameters of shape Bx3x3. |
|
points_cam (torch.Tensor): 3D points in camera coordinates of shape Bx3xN. |
|
extra_params (torch.Tensor, optional): Distortion parameters of shape BxN, where N can be 1, 2, or 4. |
|
default (float, optional): Default value to replace NaNs in the output. |
|
|
|
Returns: |
|
points2D (torch.Tensor): 2D points in pixel coordinates of shape BxNx2. |
|
""" |
|
|
|
|
|
points_cam = points_cam / points_cam[:, 2:3, :] |
|
|
|
uv = points_cam[:, :2, :] |
|
|
|
|
|
if extra_params is not None: |
|
uu, vv = apply_distortion(extra_params, uv[:, 0], uv[:, 1]) |
|
uv = torch.stack([uu, vv], dim=1) |
|
|
|
|
|
points_cam_homo = torch.cat( |
|
(uv, torch.ones_like(uv[:, :1, :])), dim=1 |
|
) |
|
|
|
points2D_homo = torch.bmm(intrinsics, points_cam_homo) |
|
|
|
|
|
points2D = points2D_homo[:, :2, :] |
|
|
|
|
|
points2D = torch.nan_to_num(points2D, nan=default) |
|
|
|
return points2D.transpose(1, 2) |
|
|
|
|