MoGe / utils3d /io /colmap.py
Ruicheng's picture
first commit
ec0c8fa
raw
history blame
6.47 kB
from typing import *
from pathlib import Path
import numpy as np
from scipy.spatial.transform import Rotation
__all__ = ['read_extrinsics_from_colmap', 'read_intrinsics_from_colmap', 'write_extrinsics_as_colmap', 'write_intrinsics_as_colmap']
def write_extrinsics_as_colmap(file: Union[str, Path], extrinsics: np.ndarray, image_names: Union[str, List[str]] = 'image_{i:04d}.png', camera_ids: List[int] = None):
"""
Write extrinsics to colmap `images.txt` file.
Args:
file: Path to `images.txt` file.
extrinsics: (N, 4, 4) array of extrinsics.
image_names: str or List of str, image names. Length is N.
If str, it should be a format string with `i` as the index. (i starts from 1, in correspondence with IMAGE_ID in colmap)
camera_ids: List of int, camera ids. Length is N.
If None, it will be set to [1, 2, ..., N].
"""
assert extrinsics.shape[1:] == (4, 4) and extrinsics.ndim == 3 or extrinsics.shape == (4, 4)
if extrinsics.ndim == 2:
extrinsics = extrinsics[np.newaxis, ...]
quats = Rotation.from_matrix(extrinsics[:, :3, :3]).as_quat()
trans = extrinsics[:, :3, 3]
if camera_ids is None:
camera_ids = list(range(1, len(extrinsics) + 1))
if isinstance(image_names, str):
image_names = [image_names.format(i=i) for i in range(1, len(extrinsics) + 1)]
assert len(extrinsics) == len(image_names) == len(camera_ids), \
f'Number of extrinsics ({len(extrinsics)}), image_names ({len(image_names)}), and camera_ids ({len(camera_ids)}) must be the same'
with open(file, 'w') as fp:
print("# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME", file=fp)
for i, (quat, t, name, camera_id) in enumerate(zip(quats.tolist(), trans.tolist(), image_names, camera_ids)):
# Colmap has wxyz order while scipy.spatial.transform.Rotation has xyzw order. Haha, wcnm.
qx, qy, qz, qw = quat
tx, ty, tz = t
print(f'{i + 1} {qw:f} {qx:f} {qy:f} {qz:f} {tx:f} {ty:f} {tz:f} {camera_id:d} {name}', file=fp)
print()
def write_intrinsics_as_colmap(file: Union[str, Path], intrinsics: np.ndarray, width: int, height: int, normalized: bool = False):
"""
Write intrinsics to colmap `cameras.txt` file. Currently only support PINHOLE model (no distortion)
Args:
file: Path to `cameras.txt` file.
intrinsics: (N, 3, 3) array of intrinsics.
width: Image width.
height: Image height.
normalized: Whether the intrinsics are normalized. If True, the intrinsics will unnormalized for writing.
"""
assert intrinsics.shape[1:] == (3, 3) and intrinsics.ndim == 3 or intrinsics.shape == (3, 3)
if intrinsics.ndim == 2:
intrinsics = intrinsics[np.newaxis, ...]
if normalized:
intrinsics = intrinsics * np.array([width, height, 1])[:, None]
with open(file, 'w') as fp:
print("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]", file=fp)
for i, intr in enumerate(intrinsics):
fx, fy, cx, cy = intr[0, 0], intr[1, 1], intr[0, 2], intr[1, 2]
print(f'{i + 1} PINHOLE {width:d} {height:d} {fx:f} {fy:f} {cx:f} {cy:f}', file=fp)
def read_extrinsics_from_colmap(file: Union[str, Path]) -> Union[np.ndarray, List[int], List[str]]:
"""
Read extrinsics from colmap `images.txt` file.
Args:
file: Path to `images.txt` file.
Returns:
extrinsics: (N, 4, 4) array of extrinsics.
camera_ids: List of int, camera ids. Length is N. Note that camera ids in colmap typically starts from 1.
image_names: List of str, image names. Length is N.
"""
with open(file) as fp:
lines = fp.readlines()
image_names, quats, trans, camera_ids = [], [], [], []
i_line = 0
for line in lines:
line = line.strip()
if line.startswith('#'):
continue
i_line += 1
if i_line % 2 == 0:
continue
image_id, qw, qx, qy, qz, tx, ty, tz, camera_id, name = line.split()
quats.append([float(qx), float(qy), float(qz), float(qw)])
trans.append([float(tx), float(ty), float(tz)])
camera_ids.append(int(camera_id))
image_names.append(name)
quats = np.array(quats, dtype=np.float32)
trans = np.array(trans, dtype=np.float32)
rotation = Rotation.from_quat(quats).as_matrix()
extrinsics = np.concatenate([
np.concatenate([rotation, trans[..., None]], axis=-1),
np.array([0, 0, 0, 1], dtype=np.float32)[None, None, :].repeat(len(quats), axis=0)
], axis=-2)
return extrinsics, camera_ids, image_names
def read_intrinsics_from_colmap(file: Union[str, Path], normalize: bool = False) -> Tuple[List[int], np.ndarray, np.ndarray]:
"""
Read intrinsics from colmap `cameras.txt` file.
Args:
file: Path to `cameras.txt` file.
normalize: Whether to normalize the intrinsics. If True, the intrinsics will be normalized. (mapping coordinates to [0, 1] range)
Returns:
camera_ids: List of int, camera ids. Length is N. Note that camera ids in colmap typically starts from 1.
intrinsics: (N, 3, 3) array of intrinsics.
distortions: (N, 5) array of distortions.
"""
with open(file) as fp:
lines = fp.readlines()
intrinsics, distortions, camera_ids = [], [], []
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
camera_id, model, width, height, *params = line.split()
camera_id, width, height = int(camera_id), int(width), int(height)
if model == 'PINHOLE':
fx, fy, cx, cy = map(float, params[:4])
k1 = k2 = k3 = p1 = p2 = 0.0
elif model == 'OPENCV':
fx, fy, cx, cy, k1, k2, p1, p2, k3 = *map(float, params[:8]), 0.0
elif model == 'SIMPLE_RADIAL':
f, cx, cy, k = map(float, params[:4])
fx = fy = f
k1, k2, p1, p2, k3 = k, 0.0, 0.0, 0.0, 0.0
camera_ids.append(camera_id)
if normalize:
fx, fy, cx, cy = fx / width, fy / height, cx / width, cy / height
intrinsics.append([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
distortions.append([k1, k2, p1, p2, k3])
intrinsics = np.array(intrinsics, dtype=np.float32)
distortions = np.array(distortions, dtype=np.float32)
return camera_ids, intrinsics, distortions