You detected the marker, estimatePoseSingleMarkers handed you rvec and tvec, and the numbers look too large or too small to be meters, so the easy conclusion is that they must be pixels. They are not. The pose comes from a physical model of the marker, and one argument sets the unit of tvec: the marker length you passed in. This explainer settles the units question with a pinhole check you can run in one line, shows what rvec's radians mean, and walks the full chain from those two vectors to a goal pose a ROS 2 Nav2 robot will drive to. If your distances feel wrong by a factor of a thousand, start here.
Why isn't tvec in pixels when the camera matrix is?
Because the pose solver never works in pixels alone. It multiplies your focal length in pixels by the marker's true side length and divides by the marker's side in pixels, so the pixel units cancel and tvec comes out in the unit of the marker length.
Your camera matrix is right to be in pixels - fx = 1000 px describes the image on the sensor's pixel grid, and nothing else. What the matrix cannot do is tell the solver how big anything in the scene is. That fact enters through the marker length argument. The ArUco routine builds the marker's four corners in world units from it, passes them with the detected pixel corners to the pose solve underneath (solvePnP in OpenCV), and the recovered translation inherits exactly those units.
Pixels appear above and below the fraction and cancel; the meters you put in through L are the meters you get back in tvec. Pixels alone never measure a length - pixels plus one known length do, which is the same cancellation behind camera calibration for length measurement.
What unit does estimatePoseSingleMarkers return tvec in?
Exactly the unit you passed the marker length in: pass 0.100 and tvec is in meters, pass 100 and it is in millimeters, and nothing about the camera matrix changes that.
A floating-point number carries no unit, so the function follows one convention: tvec speaks the unit of the marker length argument. Choose meters once, at the call, and every consumer downstream - your code, the transform chain, Nav2 - receives meters.
| You pass as marker length | tvec comes back in | To get meters |
|---|---|---|
| 0.100 | meters | use it as it is |
| 100 | millimeters | divide by 1000 |
| 1.0 | marker sides | multiply by 0.100 m |
| 100000 | micrometers | divide by 1000000 |
What units does rvec have?
rvec is a rotation vector in radians: its direction is the rotation axis and its magnitude is the rotation angle, so it never carries meters or any other length unit, whatever you passed as marker length.
Three numbers pack axis and angle - the unit axis scaled by the angle in radians. A magnitude of 0.030 rad means the marker is turned 0.030 x 57.3, about 1.7 degrees, relative to the camera. If a value near 0.03 turns up interpreted as 0.03 m somewhere in your pipeline, rotation is being read as distance: only tvec takes a length unit.
ROS 2 wants orientation as a quaternion, and that conversion is closed-form - next section.
How do you prove tvec is in meters before wiring it into Nav2?
Rebuild the depth with the pinhole formula Z = fx x L / l_px and compare: with fx = 1000 px, a 0.100 m marker spanning 200 px must give tvec z of about 0.50 m, and it will.
Worked example. The calibration returns fx = fy = 1000 px with the principal point at (cx, cy) = (640, 512) px. The marker is a 0.100 m square measured with a ruler; the detector reports a 200 px side, centered 64 px right of and 51 px above the principal point.
- Z = 1000 x 0.100 / 200 = 0.50 m
- x = 0.50 x 64 / 1000 = 0.032 m
- y = 0.50 x (-51) / 1000 = -0.026 m
Call estimatePoseSingleMarkers with marker length 0.100 and tvec returns [0.032, -0.026, 0.500] - meters, because that is the unit you declared. Pass 100 instead and the same detection returns [32, -26, 500]: the identical pose in millimeters, and dividing by 1000 puts you back where you started.
# One calibration, one detection - example values
fx = 1000.0 # float, px, from your calibration
cx, cy = 640.0, 512.0 # floats, px, principal point
marker_length_m = 0.100 # float, m, ruler-measured marker side
side_px = 200.0 # float, px, mean of the four detected sides
z_expected = fx * marker_length_m / side_px # 1000 * 0.100 / 200 = 0.500 m
x_expected = z_expected * 64.0 / fx # 0.500 * 64 / 1000 = 0.032 m
y_expected = z_expected * -51.0 / fx # 0.500 * -51 / 1000 = -0.026 m
# tvec_z, tvec_x, tvec_y read from estimatePoseSingleMarkers
assert abs(tvec_z - z_expected) < 0.01 # 0.500 m, in meters
assert abs(tvec_x - x_expected) < 0.005
If the assert fails by a factor near 1000, a millimeter number met a meter number - most often in the marker length. If it fails by the same few percent at every distance, the marker printed small or fx is off. That scale error is separate from the unit question.
How do you turn rvec and tvec into a pose Nav2 accepts?
Convert rvec to a quaternion, rotate it from OpenCV's optical frame into the ROS convention (x forward, y left, z up) with the fixed quaternion (x, y, z, w) = (-0.5, 0.5, -0.5, 0.5), then let your robot's TF tree put it in the map frame.
The vectors come back in the camera's optical frame, which is not the frame robots drive in:
| Frame | x | y | z |
|---|---|---|---|
| Optical (tvec, rvec) | right | down | forward |
| ROS body | forward | left | up |
with the fixed rotation
Numbers, continuing the example: rvec = (0.013, -0.026, 0.007) rad, so theta = 0.030 rad and q_rvec is about (w 0.9999, x 0.0065, y -0.0130, z 0.0035). The translation rotates too: t_body = R · tvec = (0.500, -0.032, 0.026) m - half a meter ahead, 32 mm right of center, 26 mm up, in robot axes.
Two rules keep this correct. First, compose, don't eyeball: q_body is the quaternion product of the fixed optical-to-body rotation with q_rvec, and one wrong sign aims the robot backwards. The quick test: apply q_body to (0, 0, 1) and require (1, 0, 0), optical forward mapped to robot forward. Second, let the robot's transform chain finish the job. Publish the pose in the optical frame and let the tree that already carries the camera mount place it in the map frame Nav2 plans in.
The marker's own frame points z out of the tag's face. A wall tag's center is on the wall, so offset the goal along that z by your stand-off - 0.50 m in this example. The robot then stops in front of the marker instead of at it.
The same frame bookkeeping bites depth cameras; see depth value vs true distance. When one camera cannot see the marker from where the robot needs it, multi-camera marker triangulation and world coordinates extends this pose into a shared world frame.
How large must the marker be in the image for a pose you can navigate on?
Give the marker at least 100 pixels on its side: at fx = 1000 px one pixel subtends 1 mrad, which is 0.5 mm of lateral error at 0.5 m. At these sizes unit mistakes - not pixel noise - are what usually break a Nav2 goal.
The marker side in pixels is l = fx · L / Z, and fx is the lens focal length divided by the pixel pitch. The 0.100 m marker at 0.50 m with fx = 1000 px spans 1000 x 0.100 / 0.500 = 200 px. It falls to the 100 px floor at Z = 1000 x 0.100 / 100 = 1.0 m. A 16 mm lens on a 4 µm pixel gives fx = 16 / 0.004 = 4000 px, which holds 100 px on the same marker out to 4.0 m; beyond that, grow the marker, the focal length, or both. Fiducial marker pixel size: why tiny tags fail covers what breaks first when tags get small, and object-space pixel size from magnification shows the mm-per-pixel view of the same arithmetic.
On the camera side this is a corners problem, not a megapixel problem. A global shutter removes rolling skew while the robot rolls, and mono keeps the black-white edge sharp. The MV-CU020-90GC is a 2.5 MP GigE camera with a global shutter, a 1/1.7" sensor and a C mount. For one marker viewed from inside a couple of meters, that is plenty.
- Sensor format
- 1/1.7"
- Lens mount
- C-mount
- Interface
- GigE
- Resolution
- Up to 3 MP
Lens choice follows stand-off. The LS-FFL-1582 is an 8 mm C-mount FA lens for 2/3" sensors with a 57.6° field of view. A 2/3" lens's 8-11 mm image circle clears the camera's roughly 9 mm sensor diagonal. For the four-meter stand-off in the arithmetic above, step up a class: the MV-CH120-11UC is a 12 MP USB3 camera with a 1.1" sensor, a global shutter and a C-mount, and the LS-FFL-0567 is a 16 mm C-mount lens for 1.1" sensors rated at 20 MP with distortion under 0.3%.
- Sensor format
- 2/3"
- Mount
- C-mount
- Type
- Machine vision fixed
- Sensor format
- 1.1"
Both pairings can be rebuilt in machine vision lenses and area scan cameras, filtered by mount, image circle, shutter and sensor format, or sized end to end in the camera step of the configurator. The camera finder does the same filtering from the shop side.
Why does the pose jump between frames when the corners barely move?
Because a flat marker seen through a pinhole camera has two valid pose solutions, and pixel-level noise can flip the solver between them: corners agreeing to within a couple of pixels while rvec and tvec swing by tens of degrees is pose ambiguity, not a unit problem.
A planar target is the hard case for pose solvers - two mirror-image poses project to nearly the same four corners, so a one- or two-pixel corner change can select the other solution. Three fixes, in order of payoff:
- Gate on physics. A marker bolted to a wall cannot rotate 40 degrees between frames 40 ms apart; reject any pose that implies it, or filter the pose over a short window.
- Add markers. A board of several markers, or a ChArUco grid, fixes the plane and removes the flip - ChArUco board dimensions and pose covers the sizing traps.
- Treat constant offsets separately. A focal length off by 2% shifts every distance by 2% but never makes the pose jump: constant percent error is scale (marker printed small, or fx off); frame-to-frame explosions are ambiguity.
When the pose is stable but the distance is wrong, work through fiducial pose wrong distance: calibration and frames.
What should you check before shipping the marker-to-Nav2 chain?
Six checks catch nearly every failure in this chain: the marker length you pass, the pinhole depth check, rvec in radians, the quaternion sign test, the goal stand-off, and one tape-measure test of the finished system.
- Marker length from a ruler, in meters. Pass 0.100 for a 100 mm marker, measured on the print. This argument sets tvec's unit, and a marker that prints 3% small reads 3% short at every distance.
- Pinhole check at one known distance. Z = fx x L / l_px must match tvec z within 1%; one test covers units, marker size and focal length together.
- rvec read as radians. 0.030 rad is 1.7 degrees of tilt; the moment a rotation value is treated as a distance, frames are mixed.
- Quaternion sign test. q_body applied to (0, 0, 1) must return (1, 0, 0); one wrong sign sends the robot backwards.
- Goal offset along the tag's z axis. The marker center sits on the surface; offset the goal by the stand-off so the robot stops in front of it.
- Tape measure the closed loop. Marker at a measured 0.50 m, Nav2 goal at the same 0.50 m: end-to-end agreement proves units, frames and mount transform at once.
From here, browse the 3D vision topic for the neighboring guides, or bring your stand-off and marker size to the contact page and we will size the chain with you.
Questions
What number exactly should I pass as marker length for meters?
Pass the side length of the printed marker as a bare number in meters: 0.100 for a marker that measures 100 mm on a side. Measure the print with a ruler rather than trusting the file, because printers scale - a marker that prints 3% small makes every tvec distance read 3% short, the same symptom as an fx that is 3% off. Print scale and focal-length errors look identical from one distance; measuring the marker removes one of the two.
Is rvec in meters as well?
No. rvec is a rotation vector: its direction is the rotation axis and its magnitude is the rotation angle in radians, so 0.030 means the marker is tilted about 1.7 degrees. Length units never enter rvec - only tvec inherits the unit of the marker length argument. Any code that adds, thresholds or plots rvec alongside meters is mixing an angle with a distance.
Should I undistort the image before estimating the marker pose?
No. Feed the raw image to the detector and pass the distortion coefficients from the same calibration as the camera matrix to the pose call; it removes the distortion as part of the solve. Undistorting first also works, but then the pose call must receive zero distortion and intrinsics that match the undistorted image. Mixing the two paths bows straight marker edges and biases rvec, and the error grows toward the image corners.
How far away can a 100 mm marker still give a usable pose?
The marker spans l = fx x L / Z pixels, so with fx = 1000 px a 0.100 m marker drops to 100 px at Z = 1.0 m - the floor for a navigable pose. At 40 px one pixel of corner jitter is already 2.5% of the side, and pose noise follows it. Doubling the marker size or the focal length doubles the range: the same marker with fx = 4000 px holds 100 px out to 4.0 m.
How do I test the whole chain against a tape measure?
Tape a marker flat on a wall, measure from the camera to the marker with a tape, and compare tvec z: agreement within 1% says units, marker length and focal length are consistent. Rotate the marker 90 degrees and check the quaternion follows by the same 90 degrees. A constant few-percent offset points at print scale or fx; a pose that explodes between frames points at planar pose ambiguity instead.


