Hello,
I exported a 3D scan from Metashape as both FBX (with cameras) and COLMAP.
I then processed the FBX in Houdini and exported it back to COLMAP using a custom Python exporter. During my tests, I just imported the fbx and exported as COLMAP wihtout touching Scene.
When comparing the original Metashape COLMAP export with my Houdini-generated COLMAP, the results match almost perfectly (~99%), but I’m seeing small differences in the decimal values of the camera poses (rotation and translation). (Compare GIF below)
The discrepancies are minor, but I’d like to fix them to get the exact same results.
Are there documented conventions, common pitfalls, or known sources of small numerical differences when generating COLMAP camera poses (e.g. coordinate systems, transform direction, precision, etc.)?
From the Metashape side: is there any additional processing or convention applied during COLMAP export that might not be obvious from the format itself?
Would it be possible to look at the COLMAP exporter implementation used by Metashape, or is any reference implementation available for comparison?
Full disclosure: I don’t understand this level of math, and I can’t write code this complex. It’s "AI-slop coded".
Thanks
Felix
Relevant code snippets:
COLMAP pose export
def get_camera_pose(tsec):
M = cam.worldTransformAtTime(tsec)
# Extract rotation and translation (Houdini space)
R = mat3_to_np(M.extractRotationMatrix3())
t = np.array(M.extractTranslates())
# Axis flip (COLMAP / Metashape convention)
F = np.diag([1, -1, -1])
# Apply coordinate system conversion
t = F @ t
R_flip = F @ R @ F
# Convert rotation to quaternion
qw, qx, qy, qz = quat_from_R(R_flip)
# Convert to COLMAP-style translation
t = -R_flip @ t
return qw, qx, qy, qz, t[0], t[1], t[2]
Quaternion conversiondef quat_from_R(R):
trace = np.trace(R)
if trace > 0:
s = np.sqrt(trace + 1.0) * 2
qw = 0.25 * s
qx = (R[2,1] - R[1,2]) / s
qy = (R[0,2] - R[2,0]) / s
qz = (R[1,0] - R[0,1]) / s
else:
i = np.argmax(np.diag(R))
if i == 0:
s = np.sqrt(1 + R[0,0] - R[1,1] - R[2,2]) * 2
qw = (R[2,1] - R[1,2]) / s
qx = 0.25 * s
qy = (R[0,1] + R[1,0]) / s
qz = (R[0,2] + R[2,0]) / s
elif i == 1:
s = np.sqrt(1 + R[1,1] - R[0,0] - R[2,2]) * 2
qw = (R[0,2] - R[2,0]) / s
qx = (R[0,1] + R[1,0]) / s
qy = 0.25 * s
qz = (R[1,2] + R[2,1]) / s
else:
s = np.sqrt(1 + R[2,2] - R[0,0] - R[1,1]) * 2
qw = (R[1,0] - R[0,1]) / s
qx = (R[0,2] + R[2,0]) / s
qy = (R[1,2] + R[2,1]) / s
qz = 0.25 * s
q = np.array([qw, qx, qy, qz])
return q / np.linalg.norm(q)Point cloud axis flippts = np.array([p.attribValue("P") for p in points], dtype=np.float32)
# Axis conversion (same convention as cameras)
pts[:,1] *= -1
pts[:,2] *= -1Camera intrinsicsfx = (focal_length_mm / sensor_width_mm) * image_width
fy = fx
cx = image_width * 0.5
cy = image_height * 0.5Houdini COLMAP Camera:0 0.881718624894961 0.421885433732614 0.197111127422913 0.075710968582176 -0.101257561880197 1.409076302291411 10.873460085694473 1 IMG_5331.jpg
Metashape COLMAP Camera:0 0.8821619613006131 0.4213870373509246 0.1960171815014118 0.07616103559635141 -0.1290817466061904 1.418968976769138 10.87187852770778 0 IMG_5331.jpg
