Spaces:
Running on Zero
Running on Zero
File size: 11,207 Bytes
dc09dcc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Convert existing calibration data into SplatAnything's camera JSON convention."""
from pathlib import Path
import os
import struct
import numpy as np
from PIL import Image
from src.utils.camera_io import validate_pose, validate_intrinsics
from src.utils.geometry import colmap_to_opencv_intrinsics
def to_opencv_c2w(matrix, pose_convention, camera_axes):
"""Invert w2c first, then convert camera axes while preserving the world frame."""
matrix = np.asarray(matrix, dtype=np.float64)
if matrix.shape == (3, 4):
matrix = np.vstack([matrix, [0, 0, 0, 1]])
validate_pose(matrix, 'source')
if pose_convention not in {'c2w', 'w2c'} or camera_axes not in {'opencv', 'opengl'}:
raise ValueError('Specify c2w/w2c and opencv/opengl explicitly.')
result = np.linalg.inv(matrix) if pose_convention == 'w2c' else matrix.copy()
if camera_axes == 'opengl':
result = result @ np.diag([1., -1., -1., 1.])
return result
def _store(records, name, pose, intrinsic, image_size=None):
# Inference takes a flat image directory. Never collapse ambiguous source names silently.
basename = Path(name).name
stem = Path(basename).stem
if basename in records or any(Path(existing).stem == stem for existing in records):
raise ValueError(f'Ambiguous or duplicate source image name: {name!r}. '
'Each image needs a unique filename and stem.')
records[basename] = (pose, intrinsic, image_size)
def _match_records(images, records):
missing = [p.name for p in images if p.name not in records]
if missing:
raise ValueError(f'{len(missing)} input images have no calibration: '
f'{", ".join(missing[:5])}. Keep only calibrated/registered images '
'in the input folder and preserve their filenames.')
poses, intrinsics = {}, {}
for path in images:
pose, intrinsic, expected_size = records[path.name]
if expected_size is not None:
with Image.open(path) as image:
actual_size = image.size
if actual_size != expected_size:
raise ValueError(f'{path.name}: image size {actual_size} differs from calibration '
f'size {expected_size}. Use the matching undistorted images/model; '
'do not mix original and resized calibration data.')
poses[path.stem] = pose
intrinsics[path.stem] = intrinsic
return poses, intrinsics
def cameras_from_arrays(images, poses_path, intrinsics_path, image_names_path,
pose_convention, camera_axes, intrinsics_pixel_center='opencv'):
"""Match matrix rows to explicit image names; a single K may be shared by all views."""
names = [line.strip() for line in Path(image_names_path).read_text(encoding='utf-8').splitlines()
if line.strip()]
poses = np.load(poses_path, allow_pickle=False)
intrinsics = np.load(intrinsics_path, allow_pickle=False)
if not isinstance(poses, np.ndarray) or not isinstance(intrinsics, np.ndarray):
raise ValueError('Use numeric .npy arrays, not .npz archives or object arrays.')
if poses.ndim == 2:
poses = poses[None]
if poses.ndim != 3 or poses.shape[1:] not in {(4, 4), (3, 4)}:
raise ValueError('Poses must have shape [N, 4, 4] or [N, 3, 4].')
if len(names) != len(poses):
raise ValueError(f'image_names.txt has {len(names)} names but poses has {len(poses)} rows.')
if intrinsics.shape == (3, 3):
intrinsics = np.broadcast_to(intrinsics, (len(poses), 3, 3))
if intrinsics.shape != (len(poses), 3, 3):
raise ValueError('Intrinsics must have shape [3, 3] or [N, 3, 3], in original-image pixels.')
if intrinsics_pixel_center not in {'opencv', 'colmap'}:
raise ValueError('Intrinsics pixel center must be opencv or colmap.')
records = {}
for name, pose, intrinsic in zip(names, poses, intrinsics):
intrinsic = np.asarray(intrinsic, dtype=np.float64).copy()
if intrinsics_pixel_center == 'colmap':
intrinsic = colmap_to_opencv_intrinsics(intrinsic)
validate_intrinsics(intrinsic, name)
_store(records, name, to_opencv_c2w(pose, pose_convention, camera_axes), intrinsic)
return _match_records(images, records)
def _pinhole_camera(model, width, height, params):
if model == 'SIMPLE_PINHOLE' and len(params) == 3:
fx, cx, cy = params
fy = fx
elif model == 'PINHOLE' and len(params) == 4:
fx, fy, cx, cy = params
else:
raise ValueError(f'Unsupported COLMAP camera model/parameters: {model}. '
'Run COLMAP image_undistorter and use its PINHOLE or SIMPLE_PINHOLE '
'model together with the undistorted images.')
if width <= 0 or height <= 0:
raise ValueError('COLMAP camera image dimensions must be positive.')
matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]], dtype=np.float64)
# COLMAP locates the first pixel center at (0.5, 0.5); inference uses (0, 0).
matrix = colmap_to_opencv_intrinsics(matrix)
validate_intrinsics(matrix, 'COLMAP')
return matrix, (width, height)
def _pose_from_colmap(qvec, tvec):
"""Convert COLMAP's Hamilton WXYZ quaternion and w2c translation to c2w."""
qvec = np.asarray(qvec, dtype=np.float64)
norm = np.linalg.norm(qvec)
if not np.isfinite(norm) or norm < 1e-12:
raise ValueError('COLMAP pose has an invalid quaternion.')
w, x, y, z = qvec / norm
rotation = np.array([
[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
[2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
[2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)],
])
matrix = np.eye(4)
matrix[:3, :3] = rotation.T
matrix[:3, 3] = -rotation.T @ np.asarray(tvec, dtype=np.float64)
validate_pose(matrix, 'COLMAP')
return matrix
def _read_text_model(model_dir):
cameras, records, image_ids = {}, {}, set()
for line in (model_dir / 'cameras.txt').read_text(encoding='utf-8').splitlines():
if not line.strip() or line.lstrip().startswith('#'):
continue
parts = line.split()
if len(parts) < 7:
raise ValueError('Malformed cameras.txt camera line.')
camera_id = int(parts[0])
if camera_id in cameras:
raise ValueError(f'Duplicate COLMAP camera ID: {camera_id}')
cameras[camera_id] = _pinhole_camera(parts[1], int(parts[2]), int(parts[3]),
[float(value) for value in parts[4:]])
with (model_dir / 'images.txt').open(encoding='utf-8') as handle:
for line in handle:
if not line.strip() or line.lstrip().startswith('#'):
continue
parts = line.strip().split(maxsplit=9)
if len(parts) != 10:
raise ValueError('Malformed images.txt image header.')
image_id = int(parts[0])
if image_id in image_ids:
raise ValueError(f'Duplicate COLMAP image ID: {image_id}')
image_ids.add(image_id)
camera_id = int(parts[8])
if camera_id not in cameras:
raise ValueError(f'COLMAP image {parts[9]} refers to missing camera {camera_id}.')
matrix, size = cameras[camera_id]
pose = _pose_from_colmap([float(v) for v in parts[1:5]], [float(v) for v in parts[5:8]])
_store(records, parts[9], pose, matrix, size)
points_line = handle.readline()
if not points_line or len(points_line.split()) % 3:
raise ValueError('Each images.txt image header must be followed by a POINTS2D line '
'(an empty line is valid for zero observations).')
return records
def _unpack(handle, layout):
size = struct.calcsize('<' + layout)
data = handle.read(size)
if len(data) != size:
raise ValueError(f'Truncated COLMAP binary file: {handle.name}')
return struct.unpack('<' + layout, data)
def _read_binary_model(model_dir):
cameras, records, image_ids = {}, {}, set()
with (model_dir / 'cameras.bin').open('rb') as handle:
count, = _unpack(handle, 'Q')
for _ in range(count):
camera_id, model_id, width, height = _unpack(handle, 'iiQQ')
if model_id not in {0, 1}:
raise ValueError(f'Unsupported COLMAP binary camera model ID {model_id}. '
'Use image_undistorter to export a pinhole model and matching images.')
if camera_id in cameras:
raise ValueError(f'Duplicate COLMAP camera ID: {camera_id}')
model, num_params = ('SIMPLE_PINHOLE', 3) if model_id == 0 else ('PINHOLE', 4)
cameras[camera_id] = _pinhole_camera(model, width, height, _unpack(handle, 'd'*num_params))
with (model_dir / 'images.bin').open('rb') as handle:
count, = _unpack(handle, 'Q')
for _ in range(count):
values = _unpack(handle, 'i7di')
image_id, camera_id = values[0], values[-1]
if image_id in image_ids:
raise ValueError(f'Duplicate COLMAP image ID: {image_id}')
image_ids.add(image_id)
name = bytearray()
while True:
char = handle.read(1)
if not char:
raise ValueError('Truncated COLMAP image name.')
if char == b'\0':
break
name.extend(char)
name = name.decode('utf-8')
if camera_id not in cameras:
raise ValueError(f'COLMAP image {name} refers to missing camera {camera_id}.')
matrix, size = cameras[camera_id]
_store(records, name, _pose_from_colmap(values[1:5], values[5:8]), matrix, size)
points, = _unpack(handle, 'Q')
end = handle.tell() + points * 24
if end > os.fstat(handle.fileno()).st_size:
raise ValueError(f'Truncated COLMAP point observations for image {name}.')
handle.seek(end)
return records
def cameras_from_colmap(images, model_dir, model_format='auto'):
"""Read the cameras/images pair only; points3D, pycolmap, and GPU libraries are not needed."""
model_dir = Path(model_dir)
if model_format not in {'auto', 'bin', 'txt'}:
raise ValueError('COLMAP model format must be auto, bin, or txt.')
if model_format == 'auto':
model_format = next((ext for ext in ('bin', 'txt') if all(
(model_dir / f'{name}.{ext}').is_file() for name in ('cameras', 'images'))), None)
if model_format is None or not all((model_dir / f'{name}.{model_format}').is_file()
for name in ('cameras', 'images')):
raise ValueError(f'No matching cameras/images .bin or .txt pair found in {model_dir}. '
'Point --model at one sparse reconstruction, e.g. sparse/0.')
records = _read_binary_model(model_dir) if model_format == 'bin' else _read_text_model(model_dir)
return _match_records(images, records)
|