Splat.Report

Coordinate systems: COLMAP vs the engine

Working5 min readUpdated 2026-07-09Verified 2026-07-09

A splat that trains cleanly and still comes out mirrored, or subtly inside-out, once it’s loaded into a viewer is almost never a training bug. It’s a coordinate convention mismatch between COLMAP, where the camera poses came from, and whatever engine is rendering the result. Both sides use a perfectly sensible, internally consistent right-handed system; they just don’t agree with each other, and the fix depends on converting correctly rather than guessing at axis signs until something looks right.

COLMAP’s convention

COLMAP follows the standard computer-vision (OpenCV) camera convention. In a camera’s own local frame, the X axis points right, the Y axis points down, and the Z axis points forward, into the scene, away from the camera. This is a right-handed coordinate system. COLMAP’s images output (QW QX QY QZ TX TY TZ per image) stores this as a world-to-camera transform: a 3D world point is rotated by the quaternion and then translated by T to arrive at that point’s coordinates in the camera’s own frame, not the camera’s position in the world. The camera’s actual position in world space has to be recovered from the inverse of that transform; reading T directly as “where the camera is” is a common and separate mistake from the axis-convention issue this article is mainly about.

COLMAP’s world frame itself has no fixed orientation. Nothing about running SfM guarantees the reconstruction ends up gravity-aligned or centred anywhere in particular; the whole model can come out at an arbitrary rotation and offset relative to true up, which matters separately from the axis-convention question and is why some pipelines run an orientation-alignment step before handing the model onward.

The engine’s convention

PlayCanvas and three.js both use a right-handed coordinate system with Y pointing up and Z pointing back, meaning the camera looks down the negative Z axis by convention. This is the same broad family as OpenGL’s convention and is common across real-time engines and web 3D frameworks. So both COLMAP and a typical splat-viewing engine are right-handed systems; they differ in which way is “up” and which way the camera looks, not in handedness itself.

Converting between them without mirroring anything

Going from COLMAP’s local camera frame (X right, Y down, Z forward) to the engine’s (X right, Y up, Z back) only requires flipping the sign of the Y and Z components, on both the translation and every basis vector of the rotation. Negating two axes out of three is equivalent to a 180-degree rotation about the axis you left alone, X in this case, which is a proper rotation: it preserves handedness, so nothing about the scene’s chirality changes. Applied consistently to every camera’s position, orientation, and to the sparse point cloud that seeds the initial Gaussians, this produces a scene that sits correctly, right-way round, in the engine’s frame.

The mirrored-splat bug happens when only one of those two axes gets flipped instead of both, most often Y alone, because negating Y does correctly turn “down” into “up” and a first look at the result can seem plausible; the scene occupies roughly the right region of space and individual objects seem to be roughly the right size. But negating a single axis out of three flips handedness: a right-handed system becomes a left-handed one, and the geometry comes out as a mirror reflection of the real scene rather than a rotation of it. The tell is that gross layout looks right while chirality is wrong: text reads backwards, asymmetric objects appear reflected left to right, and a known handed detail, a door that should swing one way, a staircase that should turn a particular direction, comes out the wrong way round. It’s also possible to flip position correctly and forget to apply the same sign change to the rotation component, which produces a scene that sits in the right place but with every Gaussian’s internal orientation and view-dependent shading pointing the wrong way, a subtler variant of the same root cause.

Where this actually bites in a working pipeline

Most reference training implementations built around COLMAP’s output, and most established splat viewers, already carry this conversion internally and get it right without any manual intervention. The risk shows up at the seams: a custom import path, a hand-rolled camera export from a CG tool feeding COLMAP-format poses into a trainer, or a conversion script written to bridge two tools that don’t share a convention out of the box. Any point in a pipeline where camera poses or point positions are read, transformed, and written back out by code you wrote yourself is a point where a single-axis flip can slip in unnoticed, because the failure mode doesn’t crash anything; it just quietly produces a mirrored scene that still looks, from a distance, like a plausible reconstruction.

In practice

When a splat looks mirrored rather than merely misoriented, gross shape right, handedness wrong, check whether the conversion code negates one axis or two between the SfM convention and the engine convention, and whether that negation is applied to rotations as well as positions. Two-axis negation (a proper rotation) is correct; one-axis negation (a reflection) is the bug, every time. And if the scene sits in a plausible location but at some unexpected global tilt rather than mirrored, that’s usually the separate, unrelated issue of COLMAP’s ungrounded world orientation, not a handedness bug at all, and calls for an orientation-alignment step rather than a sign-flip fix.

Related papers

Structure-from-Motion Revisited

Schönberger, Frahm · 2016

Introduces the incremental Structure-from-Motion pipeline, robust initial-pair and next-image selection, and bundle-adjustment scheduling, later released as the open-source COLMAP toolkit that most 3DGS capture pipelines use to produce camera poses and a sparse point cloud for training.