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