Skip to content

Commit 199f0fa

Browse files
JuliusZhou124samdev-7
authored andcommitted
Add 6328's implementation of PNP distance for Trig Solving to PhotonPoseEstimator (PhotonVision#1767)
https://discord.com/channels/725836368059826228/725846784131203222/1334309604946874460 https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/85 Helps with ambiguous single tag estimates and produces more stability.
1 parent 7d3db77 commit 199f0fa

2 files changed

Lines changed: 178 additions & 1 deletion

File tree

photon-lib/src/main/java/org/photonvision/PhotonPoseEstimator.java

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@
3131
import edu.wpi.first.math.Pair;
3232
import edu.wpi.first.math.geometry.Pose2d;
3333
import edu.wpi.first.math.geometry.Pose3d;
34+
import edu.wpi.first.math.geometry.Rotation2d;
3435
import edu.wpi.first.math.geometry.Rotation3d;
36+
import edu.wpi.first.math.geometry.Transform2d;
3537
import edu.wpi.first.math.geometry.Transform3d;
38+
import edu.wpi.first.math.geometry.Translation2d;
3639
import edu.wpi.first.math.geometry.Translation3d;
40+
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
3741
import edu.wpi.first.math.numbers.N1;
3842
import edu.wpi.first.math.numbers.N3;
3943
import edu.wpi.first.math.numbers.N8;
@@ -83,7 +87,18 @@ public enum PoseStrategy {
8387
* Use all visible tags to compute a single pose estimate. This runs on the RoboRIO, and can
8488
* take a lot of time.
8589
*/
86-
MULTI_TAG_PNP_ON_RIO
90+
MULTI_TAG_PNP_ON_RIO,
91+
92+
/**
93+
* Use distance data from best visible tag to compute a Pose. This runs on the RoboRIO in order
94+
* to access the robot's yaw heading, and MUST have addHeadingData called every frame so heading
95+
* data is up-to-date.
96+
*
97+
* <p>Yields a Pose2d in estimatedRobotPose (0 for z, roll, pitch)
98+
*
99+
* <p>https://www.chiefdelphi.com/t/frc-6328-mechanical-advantage-2025-build-thread/477314/98
100+
*/
101+
PNP_DISTANCE_TRIG_SOLVE
87102
}
88103

89104
private AprilTagFieldLayout fieldTags;
@@ -97,6 +112,9 @@ public enum PoseStrategy {
97112
protected double poseCacheTimestampSeconds = -1;
98113
private final Set<Integer> reportedErrors = new HashSet<>();
99114

115+
private final TimeInterpolatableBuffer<Rotation2d> headingBuffer =
116+
TimeInterpolatableBuffer.createBuffer(1.0);
117+
100118
/**
101119
* Create a new PhotonPoseEstimator.
102120
*
@@ -259,6 +277,30 @@ public void setLastPose(Pose2d lastPose) {
259277
setLastPose(new Pose3d(lastPose));
260278
}
261279

280+
/**
281+
* Add robot heading data to buffer. Must be called periodically for the
282+
* <b>PNP_DISTANCE_TRIG_SOLVE</b> strategy.
283+
*
284+
* @param timestampSeconds timestamp of the robot heading data.
285+
* @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
286+
* coordinates.
287+
*/
288+
public void addHeadingData(double timestampSeconds, Rotation3d heading) {
289+
addHeadingData(timestampSeconds, heading.toRotation2d());
290+
}
291+
292+
/**
293+
* Add robot heading data to buffer. Must be called periodically for the
294+
* <b>PNP_DISTANCE_TRIG_SOLVE</b> strategy.
295+
*
296+
* @param timestampSeconds timestamp of the robot heading data.
297+
* @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
298+
* coordinates.
299+
*/
300+
public void addHeadingData(double timestampSeconds, Rotation2d heading) {
301+
headingBuffer.addSample(timestampSeconds, heading);
302+
}
303+
262304
/**
263305
* @return The current transform from the center of the robot to the camera mount position
264306
*/
@@ -374,6 +416,7 @@ private Optional<EstimatedRobotPose> update(
374416
case MULTI_TAG_PNP_ON_RIO ->
375417
multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
376418
case MULTI_TAG_PNP_ON_COPROCESSOR -> multiTagOnCoprocStrategy(cameraResult);
419+
case PNP_DISTANCE_TRIG_SOLVE -> pnpDistanceTrigSolveStrategy(cameraResult);
377420
};
378421

379422
if (estimatedPose.isPresent()) {
@@ -383,6 +426,74 @@ private Optional<EstimatedRobotPose> update(
383426
return estimatedPose;
384427
}
385428

429+
private Optional<EstimatedRobotPose> pnpDistanceTrigSolveStrategy(PhotonPipelineResult result) {
430+
PhotonTrackedTarget bestTarget = result.getBestTarget();
431+
432+
if (bestTarget == null) return Optional.empty();
433+
434+
Translation2d camToTagTranslation =
435+
new Pose3d(
436+
Translation3d.kZero,
437+
new Rotation3d(
438+
0,
439+
-Math.toRadians(bestTarget.getPitch()),
440+
-Math.toRadians(bestTarget.getYaw())))
441+
.transformBy(
442+
new Transform3d(
443+
new Translation3d(
444+
bestTarget.getBestCameraToTarget().getTranslation().getNorm(), 0, 0),
445+
Rotation3d.kZero))
446+
.getTranslation()
447+
.rotateBy(
448+
new Rotation3d(
449+
getRobotToCameraTransform().getRotation().getX(),
450+
getRobotToCameraTransform().getRotation().getY(),
451+
0))
452+
.toTranslation2d();
453+
454+
if (headingBuffer.getSample(result.getTimestampSeconds()).isEmpty()) {
455+
return Optional.empty();
456+
}
457+
458+
Rotation2d headingSample = headingBuffer.getSample(result.getTimestampSeconds()).get();
459+
460+
Rotation2d camToTagRotation =
461+
headingSample.plus(
462+
getRobotToCameraTransform()
463+
.getRotation()
464+
.toRotation2d()
465+
.plus(camToTagTranslation.getAngle()));
466+
467+
if (fieldTags.getTagPose(bestTarget.getFiducialId()).isEmpty()) return Optional.empty();
468+
var tagPose2d = fieldTags.getTagPose(bestTarget.getFiducialId()).get().toPose2d();
469+
470+
Translation2d fieldToCameraTranslation =
471+
new Pose2d(tagPose2d.getTranslation(), camToTagRotation.plus(Rotation2d.kPi))
472+
.transformBy(new Transform2d(camToTagTranslation.getNorm(), 0, Rotation2d.kZero))
473+
.getTranslation();
474+
475+
Pose2d robotPose =
476+
new Pose2d(
477+
fieldToCameraTranslation,
478+
headingSample.plus(getRobotToCameraTransform().getRotation().toRotation2d()))
479+
.transformBy(
480+
new Transform2d(
481+
new Pose3d(
482+
getRobotToCameraTransform().getTranslation(),
483+
getRobotToCameraTransform().getRotation())
484+
.toPose2d(),
485+
Pose2d.kZero));
486+
487+
robotPose = new Pose2d(robotPose.getTranslation(), headingSample);
488+
489+
return Optional.of(
490+
new EstimatedRobotPose(
491+
new Pose3d(robotPose),
492+
result.getTimestampSeconds(),
493+
result.getTargets(),
494+
PoseStrategy.PNP_DISTANCE_TRIG_SOLVE));
495+
}
496+
386497
private Optional<EstimatedRobotPose> multiTagOnCoprocStrategy(PhotonPipelineResult result) {
387498
if (result.getMultiTagResult().isEmpty()) {
388499
return update(result, this.multiTagFallbackStrategy);

photon-lib/src/test/java/org/photonvision/PhotonPoseEstimatorTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
import org.junit.jupiter.api.BeforeAll;
4343
import org.junit.jupiter.api.Test;
4444
import org.photonvision.PhotonPoseEstimator.PoseStrategy;
45+
import org.photonvision.estimation.TargetModel;
46+
import org.photonvision.simulation.PhotonCameraSim;
47+
import org.photonvision.simulation.SimCameraProperties;
48+
import org.photonvision.simulation.VisionTargetSim;
4549
import org.photonvision.targeting.PhotonPipelineResult;
4650
import org.photonvision.targeting.PhotonTrackedTarget;
4751
import org.photonvision.targeting.TargetCorner;
@@ -489,6 +493,68 @@ void closestToLastPose() {
489493
assertEquals(1, pose.getZ(), .01);
490494
}
491495

496+
@Test
497+
void pnpDistanceTrigSolve() {
498+
PhotonCameraInjector cameraOne = new PhotonCameraInjector();
499+
PhotonCameraSim cameraOneSim =
500+
new PhotonCameraSim(cameraOne, SimCameraProperties.PERFECT_90DEG());
501+
502+
List<VisionTargetSim> simTargets =
503+
aprilTags.getTags().stream()
504+
.map((AprilTag x) -> new VisionTargetSim(x.pose, TargetModel.kAprilTag36h11, x.ID))
505+
.toList();
506+
507+
/* Compound Rolled + Pitched + Yaw */
508+
509+
Transform3d compoundTestTransform =
510+
new Transform3d(
511+
-Units.inchesToMeters(12),
512+
-Units.inchesToMeters(11),
513+
3,
514+
new Rotation3d(
515+
Units.degreesToRadians(37), Units.degreesToRadians(6), Units.degreesToRadians(60)));
516+
517+
var estimator =
518+
new PhotonPoseEstimator(
519+
aprilTags, PoseStrategy.PNP_DISTANCE_TRIG_SOLVE, compoundTestTransform);
520+
521+
/* this is the real pose of the robot base we test against */
522+
var realPose = new Pose3d(7.3, 4.42, 0, new Rotation3d(0, 0, 2.197));
523+
PhotonPipelineResult result =
524+
cameraOneSim.process(
525+
1, realPose.transformBy(estimator.getRobotToCameraTransform()), simTargets);
526+
527+
estimator.addHeadingData(result.getTimestampSeconds(), realPose.getRotation().toRotation2d());
528+
529+
var estimatedPose = estimator.update(result);
530+
var pose = estimatedPose.get().estimatedPose;
531+
532+
assertEquals(realPose.getX(), pose.getX(), .01);
533+
assertEquals(realPose.getY(), pose.getY(), .01);
534+
assertEquals(0.0, pose.getZ(), .01);
535+
536+
/* Straight on */
537+
538+
Transform3d straightOnTestTransform = new Transform3d(0, 0, 3, new Rotation3d(0, 0, 0));
539+
540+
estimator.setRobotToCameraTransform(straightOnTestTransform);
541+
542+
/* Pose to compare with */
543+
realPose = new Pose3d(4.81, 2.38, 0, new Rotation3d(0, 0, 2.818));
544+
result =
545+
cameraOneSim.process(
546+
1, realPose.transformBy(estimator.getRobotToCameraTransform()), simTargets);
547+
548+
estimator.addHeadingData(result.getTimestampSeconds(), realPose.getRotation().toRotation2d());
549+
550+
estimatedPose = estimator.update(result);
551+
pose = estimatedPose.get().estimatedPose;
552+
553+
assertEquals(realPose.getX(), pose.getX(), .01);
554+
assertEquals(realPose.getY(), pose.getY(), .01);
555+
assertEquals(0.0, pose.getZ(), .01);
556+
}
557+
492558
@Test
493559
void cacheIsInvalidated() {
494560
PhotonCameraInjector cameraOne = new PhotonCameraInjector();

0 commit comments

Comments
 (0)