Skip to content
FRC Article 20 min read

PhotonVision Setup for FRC: Install, Pipelines & AprilTag Pose

PhotonVision setup for FRC: install it on a Raspberry Pi or Orange Pi coprocessor, build an AprilTag pipeline, run multi-tag pose, and feed swerve odometry.

to read
20 min

to read

words
2,838

words

sections
8

sections

PhotonVision is a free, open-source computer-vision program that runs on a coprocessor next to your roboRIO, watches the field through one or more USB cameras, and streams tracking data back over NetworkTables. For most FRC teams it exists to do one job well: look at the AprilTags bolted around the field and tell your robot where it is. Get that working and your swerve drive stops drifting over a match, autos line up on the first try, and "drive to position" becomes a real button instead of a prayer.

This guide walks the whole path end to end: flashing PhotonVision onto a Raspberry Pi or Orange Pi, reaching the web dashboard, calibrating the camera, building an AprilTag pipeline, turning on multi-tag pose estimation, and finally fusing that pose into a WPILib SwerveDrivePoseEstimator. Every version-sensitive spec and API name here was checked against the current PhotonVision and WPILib docs, but FRC software moves fast — when a number matters, open the docs for the exact release you installed and confirm.

What PhotonVision is and what you need

PhotonVision does the heavy image processing off-board so the roboRIO stays free for driving. You need a coprocessor (a small Linux computer), at least one camera, a way to power the coprocessor at 5V on the robot, and a network path from the coprocessor to the roboRIO through the robot radio. If you are choosing between PhotonVision and Limelight's own software, or wondering whether a Limelight can run PhotonVision, see Limelight vs PhotonVision and the Limelight setup guide — a Limelight is just a coprocessor with a camera attached, and PhotonVision images exist for several Limelight models.

PhotonVision ships pre-built system images for these boards:

CoprocessorNotes
Raspberry Pi 3, 4, 5Pi 4/5 recommended; Pi 5 has the most CPU headroom
Orange Pi 5, 5B, 5 ProStrong price-to-performance, popular for multi-camera setups
Limelight 2, 2+, 3, 3G, 4Run PhotonVision on Limelight hardware if you prefer its tooling
Rubik Pi 3Newer supported board

A Raspberry Pi 4 or 5, or an Orange Pi 5, is the sweet spot for 2026. AprilTag detection is CPU-bound, so more cores and higher clocks translate directly into more frames per second and lower latency. Whatever you pick, plan on an 8 GB or larger microSD card.

For the camera, a global-shutter USB camera is strongly preferred over a rolling-shutter webcam: global shutter freezes the whole frame at once, keeping tags sharp while the robot moves. Whatever camera you use, you'll calibrate it at the exact resolution you intend to run.

Installing PhotonVision on your coprocessor

Installation is "flash an image, boot it, open a web page." You are not installing packages by hand — the pre-built image already contains the OS and PhotonVision configured to start on boot.

Flashing a pre-built image

  1. Go to the PhotonVision releases page and download the image that matches your board. Files are named like photonvision-<version>-linuxarm64_RaspberryPi.img.xz for a Pi, photonvision-<version>-linuxarm64_orangepi5.img.xz for an Orange Pi 5, and similar _orangepi5b, _orangepi5pro, or _limelight3 suffixes for the other boards. Download the .img.xz directly — do not unzip it first.
  2. Flash it to your microSD card with Raspberry Pi Imager. Choose "Use custom" for the OS, select the .img.xz, pick your card, and write.
  3. Put the card in the coprocessor and power it on.

Version trap that bites teams every season: avoid Raspberry Pi Imager 2.0.2 or later — those releases fail to write the image to an SD card. Versions 2.0.0 and earlier write successfully. Balena Etcher was recommended in the past but is no longer, due to instability and lack of ongoing support. If your freshly flashed card refuses to boot, the flashing tool is the first thing to suspect.

First boot and the dashboard

Give the coprocessor a minute on first boot to expand its filesystem. Put your laptop on the same network, then browse to:

http://photonvision.local:5800

photonvision.local is the coprocessor's default hostname and 5800 is the dashboard port. If .local name resolution doesn't work (mDNS is flaky on some Windows setups), browse to the coprocessor's IP on the same port instead — for example http://10.TE.AM.11:5800, substituting your team number. As a last resort, scan the network for a device with port 5800 open.

The dashboard is where you do everything from here: pick a camera, choose a pipeline, tune settings, calibrate, and watch a live stream. All configuration lives on the coprocessor and persists across reboots — robot code only ever reads the results.

Networking it onto the robot

On the bench, DHCP through a switch or the robot radio is fine. On the field, PhotonVision strongly recommends a static IP, because it makes the connection deterministic and faster to come up.

  • In the dashboard's Settings tab, set your team number, switch the connection from DHCP to Static, and assign an address in the 10.TE.AM.xx form. For team 4488 that's 10.44.88.xx. Pick a host number that doesn't collide with the roboRIO (.2) or the radio; something in the teens (.11, .12) per camera is conventional.
  • Only set a static IP when the coprocessor is connected through the robot radio. Don't set one while testing at home on your house network unless you know exactly what you're doing — you'll lock yourself out.
  • Run the coprocessor through the radio or a network switch. A direct laptop-to-coprocessor Ethernet cable relies on link-local addressing and is unreliable; keep a radio or switch in the loop. On pre-2025 radios, use a switch rather than the radio's second port, which has known issues. The FRC radio is the only router FIRST officially supports.

If you run more than one coprocessor, give each a unique hostname (letters, numbers, and hyphens only) so they don't both answer to photonvision.local.

Calibrating the camera (required for 3D)

Here's the step teams skip and then wonder why their pose is 30 cm off: AprilTag 3D pose requires camera calibration. Calibration measures your camera's focal length, optical center, and lens distortion so PhotonVision can turn pixels into real-world geometry. An uncalibrated camera produces confidently wrong poses.

Key rules:

  • Calibration is specific to each physical camera and each resolution. Calibrate at the exact resolution you'll run the pipeline. Recalibrate if you change cameras or resolution.
  • The minimum supported resolution is 640×480. Higher resolutions see tags farther away but cost CPU; the Pi/Orange Pi may not keep up at large resolutions.
  • Capture at least 12 images of the calibration target from varied distances and angles — 12 is the bare minimum the tool accepts, and the docs recommend capturing many more (on the order of 50) for a result you can trust. Tilt the board and cover the whole frame across the set. Move the target, not the camera.
  • PhotonVision supports both ChArUco boards and plain chessboards, and defaults to ChArUco, which tends to produce more reliable calibrations because it still resolves when the board is partially out of frame.
  • After calibrating, check the reported mean reprojection error. It should be under about 1 pixel. If it's higher, recalibrate with more varied, sharper images before trusting the pose.

Print your calibration target on rigid, flat stock with a clean white border — a wavy paper target ruins the calibration no matter how many images you take.

Building an AprilTag pipeline

FRC uses the 36h11 AprilTag family, and PhotonVision defaults to it — the "36h11" name means each tag is a 36-bit code with a minimum Hamming distance of 11 between valid codes. That large margin is what lets the detector reject false positives and still decode a tag when a few bits are corrupted by occlusion or motion blur. If you want the conceptual background on how these markers encode an ID and a pose, read AprilTags explained.

To build one: add a camera, create a new AprilTag pipeline, confirm the tag family is 36h11, and enable 3D mode (which needs the calibration you just did). Point it at a tag and you should see the ID and a 3D axis overlay on the stream.

Tuning the pipeline

The AprilTag pipeline exposes several knobs. The defaults are sane; reach for these only when you're trading detection range against frame rate or false positives:

SettingWhat it doesPractical guidance
DecimateDownsamples the image before detectionHigher = more FPS (higher detection rate) but shorter detection distance — weaker on small/far tags
BlurGaussian blur before detectionUsually 0; a little can help very noisy images
ThreadsCPU threads used for detectionMore threads = more FPS; the docs suggest staying about one below the coprocessor's total CPU threads
Refine EdgesSharpens detected tag edgesLeave on; cheap accuracy improvement
Pose IterationsIterations the pose solver runsA smaller value (roughly 0–100) is recommended; too few is noisy head-on, too many locks onto a possibly-wrong solution
Max Error BitsBit-error tolerance when decodingLower rejects marginal detections; raise only if you're missing valid tags
Decision Margin CutoffRejects low-confidence detectionsRaise to cut false positives, lower to catch faint tags

MultiTag pose estimation

A single AprilTag gives a usable pose, but it carries ambiguity — from one tag the solver can't always tell which of two orientations you're really in, especially close to head-on. MultiTag fixes this. When two or more tags are visible in the same frame, PhotonVision combines all their corners with the known field layout and solves for one camera pose against the whole constellation. The result is far more stable, and it's computed on the coprocessor, so it adds no load to the roboRIO.

To enable it:

  1. Make sure the camera is calibrated and the pipeline is in 3D mode — MultiTag needs both.
  2. Confirm the correct field layout JSON is loaded on the coprocessor. Each season ships separate welded and AndyMark variants because the two field constructions place tags slightly differently; use the one matching your field. Any mismatch between the loaded layout and the tags' true positions becomes direct pose error.
  3. On the Output tab, enable "Do Multi-Target Estimation."

To load or update the layout file, go to Settings → Device Control, use Import Settings, choose the AprilTag Layout type, and select your JSON.

The MultiTag result is a transform from the fixed field origin to the camera, expressed in WPILib's blue-alliance coordinate frame — the same frame your odometry lives in, which is exactly what makes it drop-in for pose estimation. For a refresher on odometry, pose, and how vision corrects drift, see odometry and pose estimation.

Pulling pose into WPILib swerve odometry

The robot code side uses PhotonLib, PhotonVision's vendor library. The flow is: read results from the camera, run them through a PhotonPoseEstimator that knows the field layout and where the camera sits on the robot, then feed the resulting field pose into your SwerveDrivePoseEstimator as a vision measurement.

Install PhotonLib

In VS Code with the WPILib extension, click the WPILib logo in the activity bar, open Manage Vendor Libraries → Install new libraries (online), and select PhotonLib. (Offline, download photonlib-<version>.zip from the releases page into your project's vendordeps.) Match the PhotonLib version to the PhotonVision version on your coprocessor.

Read results the current way

PhotonVision streams a queue of results. The modern API drains that queue so you never miss or double-count a frame. getLatestResult() is deprecated (marked for removal since 2024) precisely because it can skip or repeat results; use getAllUnreadResults() and call it exactly once per robot loop:

PhotonCamera camera = new PhotonCamera("front_left"); // the nickname from the dashboard

for (PhotonPipelineResult result : camera.getAllUnreadResults()) {
    if (result.hasTargets()) {
        PhotonTrackedTarget target = result.getBestTarget();
        int id = target.getFiducialId();
        double yaw = target.getYaw();               // degrees, positive to the left
        double ambiguity = target.getPoseAmbiguity();
        Transform3d camToTag = target.getBestCameraToTarget();
    }
}

The internal result queue holds about 20 changes, so call getAllUnreadResults() every loop. If your code stalls and the queue overflows, the oldest results are dropped.

Set up the pose estimator

Load the field layout with WPILib's AprilTagFieldLayout.loadField(...) — the older loadAprilTagLayoutField() helper is deprecated. AprilTagFields.kDefaultField aliases the current season, with welded/AndyMark variants in that enum; pick the one matching your field and make sure it agrees with the coprocessor.

The robotToCamera transform describes where the camera is mounted, in the robot frame: X forward, Y left, Z up, in meters and radians. Measure it carefully — an error here shifts every pose you get back.

AprilTagFieldLayout tagLayout =
    AprilTagFieldLayout.loadField(AprilTagFields.kDefaultField);

// Camera 30 cm forward of robot center, 20 cm up, facing straight ahead.
Transform3d robotToCam = new Transform3d(
    new Translation3d(0.30, 0.0, 0.20),
    new Rotation3d(0.0, 0.0, 0.0));

PhotonPoseEstimator poseEstimator =
    new PhotonPoseEstimator(tagLayout, robotToCam);

Feed it into the SwerveDrivePoseEstimator

SwerveDrivePoseEstimator is a drop-in replacement for SwerveDriveOdometry that fuses your encoder/gyro odometry with latency-compensated vision measurements. You already call update() on it every loop with module states and gyro angle; the vision correction is one extra call, addVisionMeasurement(Pose2d, timestampSeconds, stdDevs):

for (PhotonPipelineResult result : camera.getAllUnreadResults()) {
    Optional<EstimatedRobotPose> est =
        poseEstimator.estimateCoprocMultiTagPose(result);
    // MultiTag needs 2+ tags; fall back to a single-tag strategy otherwise.
    if (est.isEmpty()) est = poseEstimator.estimateLowestAmbiguityPose(result);

    est.ifPresent(e ->
        swervePoseEstimator.addVisionMeasurement(
            e.estimatedPose.toPose2d(),   // field pose, Pose3d -> Pose2d
            e.timestampSeconds,           // when the frame was captured
            visionStdDevs));              // Matrix<N3, N1>: [x m, y m, theta rad]
}

EstimatedRobotPose carries the field pose (estimatedPose, a Pose3d) and the capture time (timestampSeconds) already latency-compensated — pass both straight through. PhotonLib's estimator API has shifted between seasons (older code set a PoseStrategy on the constructor and called update(result)), so if a method name here doesn't resolve, check the javadocs for the exact PhotonLib version you installed.

The standard deviations matrix is how you tell the filter how much to trust vision versus odometry. Bigger numbers mean "trust this less."

// Trust odometry more, vision less, when only one far tag is visible;
// tighten these when MultiTag gives a strong multi-tag fix.
Matrix<N3, N1> visionStdDevs = VecBuilder.fill(0.5, 0.5, 1.0); // x m, y m, theta rad

A good habit is to scale the standard deviations by confidence — trust a close, low-ambiguity MultiTag result tightly, and inflate the deviations (or reject the measurement) when only a single distant or high-ambiguity tag is visible. Reject anything that lands off the field or jumps implausibly far from your current estimate.

Frequently asked questions

Do I need to calibrate the camera before AprilTag pose works?

Yes. 3D AprilTag pose requires calibration at the resolution you run, and the pose is only as good as that calibration. Aim for a mean reprojection error under about 1 pixel, and recalibrate whenever you change cameras or resolution.

Why can't I reach photonvision.local:5800?

Most often it's mDNS name resolution failing on your laptop, not PhotonVision being down. Try the coprocessor's IP with :5800 instead (for example 10.TE.AM.11:5800), confirm you're on the same network through a switch or radio, and remember a direct laptop-to-Pi Ethernet cable is unreliable.

Should I use getLatestResult() or getAllUnreadResults()?

Use getAllUnreadResults(). getLatestResult() is deprecated because it can miss or repeat frames. Call getAllUnreadResults() once per robot loop and iterate over the list it returns.

What's the difference between single-tag and MultiTag pose?

A single tag can be ambiguous — the solver may not distinguish two mirror orientations, especially head-on. MultiTag combines two or more tags seen in the same frame with the field layout to produce one much more stable pose, and it runs on the coprocessor.

Which field layout do I load — welded or AndyMark?

Load whichever matches the physical field you're on; the two constructions place tags slightly differently each season. Use the same variant on the coprocessor and in your robot code, because any layout mismatch turns directly into pose error. Always confirm the correct tag family and layout in the current game manual and WPILib release.

How do I keep a bad vision reading from throwing off odometry?

Gate the measurements. Skip poses that land off the field or jump too far from your current estimate, and raise the standard deviations for distant or high-ambiguity results so the filter leans on odometry until a stronger fix arrives.

Once this loop is closed, your robot always knows where it is, and everything downstream — auto-align, path following, "drive to that position" — gets easier. Start simple: one calibrated camera, one AprilTag pipeline, MultiTag on, feeding your pose estimator. Get that solid before adding a second camera. For more FRC software walkthroughs, browse the guides, and check the current-season game manual and WPILib release notes for the details that change year to year.

Spot an error or something out of date?Create a free account to suggest an edit

Keep reading

More from the pit

Start learning FRC — free

Structured lessons and quizzes across every department. Create a free account to save your progress, track your team, and earn a certificate.

394lessons
11departments
100%free