article / 20 min
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.
/ 2,838 words / 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.
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:
| Coprocessor | Notes |
|---|---|
| Raspberry Pi 3, 4, 5 | Pi 4/5 recommended; Pi 5 has the most CPU headroom |
| Orange Pi 5, 5B, 5 Pro | Strong price-to-performance, popular for multi-camera setups |
| Limelight 2, 2+, 3, 3G, 4 | Run PhotonVision on Limelight hardware if you prefer its tooling |
| Rubik Pi 3 | Newer 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.
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.
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..img.xz, pick your card, and write.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.
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.
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.
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.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.
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:
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.
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.
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:
| Setting | What it does | Practical guidance |
|---|---|---|
| Decimate | Downsamples the image before detection | Higher = more FPS (higher detection rate) but shorter detection distance — weaker on small/far tags |
| Blur | Gaussian blur before detection | Usually 0; a little can help very noisy images |
| Threads | CPU threads used for detection | More threads = more FPS; the docs suggest staying about one below the coprocessor's total CPU threads |
| Refine Edges | Sharpens detected tag edges | Leave on; cheap accuracy improvement |
| Pose Iterations | Iterations the pose solver runs | A smaller value (roughly 0–100) is recommended; too few is noisy head-on, too many locks onto a possibly-wrong solution |
| Max Error Bits | Bit-error tolerance when decoding | Lower rejects marginal detections; raise only if you're missing valid tags |
| Decision Margin Cutoff | Rejects low-confidence detections | Raise to cut false positives, lower to catch faint tags |
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:
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.
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.
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.
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.
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);
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.
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.
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.
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.
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.
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.
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.
where this came from
This article is AI-assisted: drafted from primary sources, then reviewed and edited by hand. Errors still get through. When one is reported we fix it and write down what changed, in public, in the corrections log.
dept / programming-software
You came for one answer. These teach the same subject properly, in the order a team actually learns it. All 51 are free, and none of them need an account to read.
if you want to keep the ticks
LearnFRC has 394 free FRC lessons across every department. Create a free account to save your place, track your progress, and earn a certificate.
Create a free accountReading never needs one. This is only for saving where you got to.
these are the closest, not the newest
15 min read
How an FRC robot tracks its field position with WPILib: wheel odometry vs pose estimation, gyro heading, fusing AprilTag vision, and field-centric driving.
read it8 min read
AprilTags in FRC are the fiducial markers robots use to see the field and know their exact position. How the tags, cameras, and pose estimation actually work.
read it11 min read
Set up a Limelight on your FRC robot: wiring, power, static IP networking, the web UI, building pipelines, AprilTag botpose, and NetworkTables in Java.
read itend of sheet
Keep going with the other 102 articles, or work through all 394 lessons across 11 departments.