AprilTag detection
Read FTC AprilTag detections safely, including SDK 12 clusters
An AprilTag is a high-contrast printed marker with a machine-readable pattern. The SDK can identify tags and estimate their pose relative to a camera. That estimate can help localization, but it is still a measurement: a blurry tag, a wrong tag size, poor camera calibration, or confusing the camera pose with the pose can lead to a bad decision.

SDK version
The code that reads detections changed in FTC SDK 12.0. This matters especially for the current BIOBUZZ : the official FTC documentation says BIOBUZZ has four AprilTag clusters and no standalone tags.
In SDK 12.0 and later, getDetections() can return either:
AprilTagSingleDetection, which has anidfield; orAprilTagClusterDetection, which has cluster metadata and a cluster name, but no.idfield.
The common parent type is AprilTagDetection. Pose fields such as ftcPose are on that parent and can be read for either type. Do not copy older code that reads detection.id directly from every result.
Processor setup
Make the processor before building VisionPortal. This convenience method uses the FTC SDK defaults:
AprilTagProcessor aprilTag = AprilTagProcessor.easyCreateWithDefaults();val aprilTag = AprilTagProcessor.easyCreateWithDefaults()The default game library is supplied by the FTC SDK. If you need custom tags or processor settings, use the current FTC sample and Builder documentation rather than adding old-season IDs by hand.
Camera setup
VisionPortal portal = new VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
.addProcessor(aprilTag)
.build();val portal = VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName::class.java, "Webcam 1"))
.addProcessor(aprilTag)
.build()As with any VisionPortal setup, Webcam 1 must match the configured hardware name. Keep the preview open during so you can see whether a marker is being detected before the robot moves.
Detection types
The Driver Hub preview below shows the processor finding BIOBUZZ AprilTag clusters in a real camera image. The colored boxes and labels are the detections overlaid on the frame. In the next step, you will read each detection from code.

This is a real FTC camera preview with the detected BIOBUZZ clusters labeled. The labels show recognition; the telemetry example below shows how your reads the measurements. Use your own live preview to verify detections before the robot acts.
Reading detections
The SDK gives you a list because the camera can see more than one detection at a time. In SDK 12, check whether each result is a single tag or a cluster before reading type-specific information:
List<AprilTagDetection> detections = aprilTag.getDetections();
for (AprilTagDetection detection : detections) {
if (detection instanceof AprilTagSingleDetection) {
AprilTagSingleDetection single = (AprilTagSingleDetection) detection;
telemetry.addData("Single tag ID", single.id);
if (single.metadata != null) {
telemetry.addData("Tag name", single.metadata.name);
}
} else if (detection instanceof AprilTagClusterDetection) {
AprilTagClusterDetection cluster = (AprilTagClusterDetection) detection;
telemetry.addData("Cluster", cluster.metadata.name);
telemetry.addData("Visible cluster", "%d%%", cluster.percentClusterFound);
}
if (detection.ftcPose != null) {
telemetry.addData("XYZ (in)", "%.1f, %.1f, %.1f",
detection.ftcPose.x, detection.ftcPose.y, detection.ftcPose.z);
telemetry.addData("Pitch / Roll / Yaw (deg)", "%.1f / %.1f / %.1f",
detection.ftcPose.pitch, detection.ftcPose.roll, detection.ftcPose.yaw);
telemetry.addData("Range (in)", "%.1f", detection.ftcPose.range);
telemetry.addData("Bearing (deg)", "%.1f", detection.ftcPose.bearing);
telemetry.addData("Elevation (deg)", "%.1f", detection.ftcPose.elevation);
}
}val detections: List<AprilTagDetection> = aprilTag.detections
for (detection in detections) {
when (detection) {
is AprilTagSingleDetection -> {
telemetry.addData("Single tag ID", detection.id)
detection.metadata?.let { telemetry.addData("Tag name", it.name) }
}
is AprilTagClusterDetection -> {
telemetry.addData("Cluster", detection.metadata.name)
telemetry.addData("Visible cluster", "%d%%", detection.percentClusterFound)
}
}
detection.ftcPose?.let { pose ->
telemetry.addData("XYZ (in)", "%.1f, %.1f, %.1f", pose.x, pose.y, pose.z)
telemetry.addData("Pitch / Roll / Yaw (deg)", "%.1f / %.1f / %.1f",
pose.pitch, pose.roll, pose.yaw)
telemetry.addData("Range (in)", "%.1f", pose.range)
telemetry.addData("Bearing (deg)", "%.1f", pose.bearing)
telemetry.addData("Elevation (deg)", "%.1f", pose.elevation)
}
}Add these imports when using the example in a OpMode:
import java.util.List;
import org.firstinspires.ftc.vision.apriltag.AprilTagDetection;
import org.firstinspires.ftc.vision.apriltag.AprilTagSingleDetection;
import org.firstinspires.ftc.vision.apriltag.AprilTagClusterDetection;import org.firstinspires.ftc.vision.apriltag.AprilTagDetection
import org.firstinspires.ftc.vision.apriltag.AprilTagSingleDetection
import org.firstinspires.ftc.vision.apriltag.AprilTagClusterDetectionThe code has a reason for each check:
getDetections()obtains the processor's current list.instanceoftells Java which kind of detection it is handling.- A single-tag ID is read only after casting to
AprilTagSingleDetection. - Cluster name and visibility are read only after casting to
AprilTagClusterDetection. - Pose is read from the parent result type because both kinds of detection can have
ftcPose.
If your project uses SDK 11 or earlier, this code will not compile because the cluster classes do not exist in that version. Use the samples and API docs that match your project's FTC SDK version.
Pose coordinates
ftcPose describes the detected target relative to the camera lens. In the FTC frame, Y points forward out of the lens, X points right, and Z points up. If a tag is left of the camera centerline, its X value is negative. Those translations and range use the processor's configured output distance unit (inches by default), independently of the tag metadata's units. AprilTagProcessor.Builder.setOutputUnits() configures distance and angle units; the default angle unit is degrees (normally inches for FTC field tags). The rotations are in degrees with the processor's default settings; their signs follow the FTC axes and right-hand rule.
The names are easy to mix up, so connect each value to a question:
| Telemetry value | Meaning in the FTC camera frame | Beginner check |
|---|---|---|
x | Side-to-side offset; positive is to camera-right. | Move the tag left: X should become negative. |
y | Forward distance from the lens to the tag . | Move the tag farther away: Y should grow. |
z | Vertical offset; positive is above the lens. | Put the tag below the lens: Z should be negative. |
pitch | Tag rotation about the FTC X axis. | Tilt the tag up/down and observe this value. |
roll | Tag rotation about the FTC Y axis. | Rotate the tag clockwise/counterclockwise in its image plane. |
yaw | Tag rotation about the FTC Z axis (vertical). | Turn the tag left/right; this is not the same as . |
range | Horizontal distance from camera to tag center, derived from X and Y. | A sideways tag can have a large range even when Y is unchanged. |
bearing | Horizontal angle from the camera's forward direction to the tag. | Use this to aim toward the tag, after checking the sign. |
elevation | Vertical angle from forward to the tag center. | A tag above or below the lens changes this angle. |
Pitch, roll, and yaw names are the FTC SDK's mapping to its camera axes; do not assume another robotics library uses the same frame or angle naming. Bearing answers “which way should I turn to point at the center?” Yaw answers “how is the tag face rotated?” A tag can have near-zero bearing but nonzero yaw when it is centered in the image but turned sideways.
A cluster can report a target point placed somewhere useful in its group layout, rather than at the center of any printed marker. The official BIOBUZZ cluster guidance explains that the HIVE CELL cluster pose is designed to point toward the CELL opening. Do not substitute the center of one visible tag for the cluster pose.
Pose quality depends on the tag size and camera calibration. Check the current field tag layout and game manual before assuming a location or meaning. Do not use a stale detection after the camera no longer sees the target.
Detection testing
- Put the robot on or leave the disabled.
- Run the SDK's
ConceptAprilTagsample that matches your SDK version. - Keep the OpMode in INIT and open Camera Stream.
- Move a printed tag or cluster through the image and watch the detection name, range, and bearing.
- Hide the target. Confirm your program handles an empty detection list.
- Test the actual robot camera placement and real field lighting before using measurements for localization.
Common mistakes
| Mistake | Why it fails | Better check |
|---|---|---|
Reading .id on every AprilTagDetection | Cluster detections do not have a single-tag ID. | Check instanceof AprilTagSingleDetection first. |
| Assuming every season uses single tags | SDK 12 added cluster detections; BIOBUZZ uses clusters. | Inspect the current game library and matching sample. |
| Treating camera pose as robot pose | The camera can be offset and rotated on the robot. | Apply the camera-to-robot transform or use the SDK's robot-pose support when configured. |
| Trusting a pose with unknown tag size or calibration | Scale and orientation estimates can be wrong. | Verify tag metadata and camera calibration for the selected resolution. |
| Acting on an empty or stale result | The target may be outside the view or no longer visible. | Require a fresh detection and define a safe fallback. |