[Bug] 4.2.0: images.txt poses from a rig reconstruction do not match image.CamFromWorld() (stale per-image poses)
Description
When reconstructing a multi-camera rig (frames with multiple sensors, constant per-sensor sensor_from_rig poses), the per-image poses written to the exported images.txt do not match image.CamFromWorld() of the in-memory reconstruction. frames.txt + rigs.txt, however, are self-consistent: composing cam_from_world = cam_from_rig ∘ rig_from_world from them reproduces image.CamFromWorld() exactly (0.0000° for all images and all sensors).
In other words, after a rig-constrained reconstruction the model in memory is perfectly rigid (per-timestamp baseline norms between all sensors are constant, as expected for a physical rig), but the exported TXT carries per-image poses that violate this rigidity (baseline norms varying by ~±50% over time; per-image rotation errors up to 63° relative to the composition of the exported frames.txt/rigs.txt).
This makes any downstream consumer of the standard TXT format (undistortion, training pipelines, further SfM tools) see non-rigid cameras even though the rig-constrained solution itself is rigid.
Steps to reproduce
- Build a rig dataset per doc/rigs.html (two-pass workflow, unknown sensor poses):
- Pass 1: plain
colmap mapperwithout rigs (per-camera trivial rigs), partial model is fine. colmap rig_configurator --database_path db --input_path <pass1 model> --rig_config_path rig.json(JSON omitscam_from_rig_*so poses are averaged from the pass-1 model; 448 multi-sensor frames written).- Pass 2:
colmap mapper --Mapper.ba_refine_sensor_from_rig 0(also reproduced with refinement enabled and withbundle_adjuster --BundleAdjustment.refine_sensor_from_rig 1).
- Pass 1: plain
- Load the final model and compare, for every image,
image.CamFromWorld()against the exportedimages.txt:
import pycolmap, numpy as np
rec = pycolmap.Reconstruction("sparse/0") # final rig model (binary)
Rig = rec.rigs[next(iter(rec.rigs))]
def R_of(r3d):
return np.asarray(r3d.rotation.matrix())
def center(r3d):
R = R_of(r3d); t = np.asarray(r3d.translation)
return -R.T @ t
max_ang, n_mismatch, n = 0.0, 0, 0
for img_id, img in rec.images.items():
cfw = img.cam_from_world()
R_i = R_of(cfw)
c_i = center(cfw)
frame = rec.frames[img.frame_id()]
R_f, t_f = (lambda r: (np.asarray(r.rotation.matrix()), np.asarray(r.translation)))(frame.rig_from_world())
cam_id = img.camera_id
if cam_id != Rig.ref_sensor_id.id:
R_s, t_s = (lambda r: (np.asarray(r.rotation.matrix()), np.asarray(r.translation)))(Rig.sensor_from_rig(pycolmap.sensor_t(pycolmap.SensorType.CAMERA, cam_id)))
R_c, t_c = R_s @ R_f, R_s @ t_f + t_s
else:
R_c, t_c = R_f, t_f
# images.txt as exported by model_converter / write_text
row = txt_rows[img.name()] # standard COLMAP TXT parsing
R_t = qvec_to_R(row["qvec"]); t_t = row["tvec"]
c_t = -R_t.T @ t_t
ang = np.degrees(np.arccos(np.clip((np.trace(R_c @ R_t.T) - 1) / 2, -1, 1)))
dt = np.linalg.norm(c_t - c_i) # camera-center mismatch
n += 1
if ang > 0.01:
n_mismatch += 1
max_ang = max(max_ang, ang)
print(f"{n_mismatch}/{n} images mismatch, max rotation error {max_ang:.2f} deg")Actual result
images=1705, frames=434, rigs=1(4 sensors, constantsensor_from_rigfor the 3 non-reference sensors).- Composing
frames.txt+rigs.txtreproducesimage.CamFromWorld()exactly: max rotation error 0.0000°. - 1166 / 1705 exported
images.txtentries do not matchimage.CamFromWorld(): rotation error up to 63°, camera-center distance up to 0.59 in model units. - Present right after
mapper, and unchanged after runningbundle_adjuster(refine_sensor_from_rig0 or 1). - The exported
frames.txt/rigs.txtare correct and rigid; onlyimages.txtis inconsistent with them.
Expected result
images.txt should contain exactly image.CamFromWorld() (i.e., sensor_from_rig ∘ rig_from_world), so that the standard TXT round-trip preserves the rigid rig solution.
Impact
Any consumer of the exported TXT receives cameras that are inconsistent with the rig-constrained solution — in our case the poses look like an unconstrained per-image solution, which reintroduces the cross-camera inconsistency that the rig constraint was supposed to remove.
Notes
- Verified with the official 4.2.0 Windows CUDA binary and the pycolmap 4.2.0 wheel (both
model_converterTXT export andpycolmapwrite_textshow the mismatch;frames.txt/rigs.txtare correct in both). - The rig dataset follows doc/rigs.html exactly (foldered images with identical basenames per frame,
single_camera_per_folder 1,rig_configuratorbefore sequential matching, two-pass pose derivation). - Happy to share the database/models for debugging (the raw images are from a proprietary scan but can be excluded — the ETH3D terrains rig example likely reproduces this as well).
Source: colmap/colmap