| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| | 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) |
| |
|
| |
|