Fiducial markers are the cheapest way to give a vision system a trustworthy reference frame. We use them for AGV docking, robot-cell registration, fixture checks and as ground truth when validating a learned pipeline. They are also easy to get subtly wrong: a pose that looks fine in the overlay can be centimetres off, or flip between two mirror solutions from frame to frame. This tutorial builds a marker pose pipeline that reports an uncertainty alongside every pose.
1. Pick the dictionary and print size on purpose
- Dictionary: use the smallest dictionary that covers the IDs you need (for example
DICT_4X4_50). Fewer IDs means more bit distance between codes and fewer false identifications.DICT_APRILTAG_36h11is a good choice when you want AprilTag-family robustness against false positives. - Physical size: work backwards from your camera. You want the marker's inner bit cells to be at least about 4 to 5 pixels wide at the furthest working distance. Compute
pixels_per_cell = focal_px * marker_side_m / (distance_m * cells_across). - Print quality: mount on a rigid, matte, flat substrate. A curled paper print adds more error than any algorithm choice. Keep a white border of at least one cell.
2. Detect with the ArucoDetector API
import cv2, numpy as np
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_APRILTAG_36h11)
params = cv2.aruco.DetectorParameters()
params.cornerRefinementMethod = cv2.aruco.CORNER_REFINE_SUBPIX
params.cornerRefinementWinSize = 5
detector = cv2.aruco.ArucoDetector(dictionary, params)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
corners, ids, rejected = detector.detectMarkers(gray)
Sub-pixel corner refinement is the single most valuable parameter change. Always detect on the undistorted or calibrated geometry you will use for pose, and use the intrinsics from a proper calibration (see our ChArUco calibration tutorial), not a datasheet focal length.
3. Estimate pose with solvePnP, not the legacy helper
The old single-marker pose helper has been deprecated in recent releases. Call solvePnP yourself with the planar-square solver, which also lets you inspect the ambiguity:
s = marker_side_m / 2.0
obj = np.array([[-s, s, 0], [ s, s, 0], [ s, -s, 0], [-s, -s, 0]], np.float32)
def pose(c, K, dist):
n, rvecs, tvecs, errs = cv2.solvePnPGeneric(
obj, c.reshape(4, 2), K, dist, flags=cv2.SOLVEPNP_IPPE_SQUARE)
return rvecs, tvecs, errs # two candidate solutions, sorted by error
IPPE_SQUARE returns up to two solutions. When the marker is viewed nearly face-on, the two have similar reprojection error and the pose can flip between them. This is the classic "axis jumps" bug.
4. Reject ambiguous poses
Compare the reprojection error of the best and second-best solution. If errs[1] / errs[0] is below a threshold (we start at about 2.0 and tune on recorded data), mark the pose as ambiguous. Then either skip the frame, or choose the candidate closest to the previous accepted pose, or prefer the one whose marker normal points toward the camera. Also reject detections where:
- reprojection error is above roughly 1 px for a refined detection;
- the marker is viewed at a grazing angle (normal more than about 70 degrees from the optical axis);
- the marker touches the image border.
5. Fuse several markers and filter over time
One marker gives a noisy pose, mainly in depth and rotation about axes in the image plane. If the layout is known (a board or several wall markers with measured offsets), stack all their corners in one solvePnP call to get a much better pose. For a moving camera, smooth with a simple constant-velocity Kalman filter or an exponential filter on translation and a quaternion slerp on rotation. Gate the filter with the ambiguity check above so a flipped pose never enters it.
6. Quote an uncertainty
Do not report a bare pose. Validate on a rig where you know the truth, such as a linear stage or a printed grid with measured spacing:
- Move the marker through known positions at your working distances and angles.
- Record the pose error per axis against truth.
- Report the 95th percentile error per distance band, e.g. "lateral 0.8 mm, depth 3 mm at 1.5 m".
Depth error normally grows with the square of distance, and lateral error roughly linearly, so band your results.
7. Production checklist
- Intrinsics calibrated at the working focus and resolution; recalibrate if the lens is refocused.
- Fixed exposure or an exposure gate: a saturated white border shrinks the apparent marker.
- Ambiguity and reprojection gates logged with each pose.
- A recorded regression clip with expected poses in CI, so a dictionary or parameter change cannot silently shift results.
If you need marker-based registration for a robot cell, docking or inspection fixture, with an error budget you can put in a contract, get in touch and one of our senior OpenCV developers will help.