diff --git a/vision/src/main/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimator.java b/vision/src/main/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimator.java new file mode 100644 index 00000000..250d9a2a --- /dev/null +++ b/vision/src/main/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimator.java @@ -0,0 +1,392 @@ +package com.team2813.lib2813.vision; + +import static com.team2813.lib2813.vision.VisionNetworkTables.CAMERA_POSE_TOPIC; +import static com.team2813.lib2813.vision.VisionNetworkTables.getTableForCamera; +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Collectors.toMap; + +import edu.wpi.first.apriltag.AprilTagFieldLayout; +import edu.wpi.first.math.geometry.Pose2d; +import edu.wpi.first.math.geometry.Pose3d; +import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Rotation3d; +import edu.wpi.first.math.geometry.Transform3d; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; +import edu.wpi.first.networktables.StructPublisher; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import org.photonvision.EstimatedRobotPose; +import org.photonvision.PhotonCamera; +import org.photonvision.PhotonPoseEstimator; +import org.photonvision.simulation.PhotonCameraSim; +import org.photonvision.simulation.SimCameraProperties; +import org.photonvision.simulation.VisionSystemSim; + +/** + * Provides estimated robot positions, in field pose, from multiple PhotonVision cameras. + * + *
This class manages one or more PhotonVision cameras, and provides an API ({@link + * #processAllUnreadResults(PoseEstimateConsumer)}) to provide an updated estimated robot pose by + * combining readings from AprilTags visible from the cameras. It also supports adding the cameras + * to PhotonVision's simulated vision system. + * + *
Note that, when we are dealing with 2D and 3D poses, we follow the transformation conventions established by WPILib and PhotonVision. + * + *
Furthermore note that the global robot pose or any of the camera global poses are also
+ * referred to as "field-centric pose". In our libraries, field-centric poses are always specified relative to the blue origin.
+ */
+public class MultiPhotonPoseEstimator implements AutoCloseable {
+ private final List The returned value is used to get pose estimates from the camera.
+ */
+ private static PhotonCameraWrapper createCameraWrapperFromConfig(
+ Builder builder, String cameraName, CameraConfig cameraConfig) {
+ PhotonCamera camera = new PhotonCamera(builder.ntInstance, cameraName);
+ PhotonPoseEstimator estimator =
+ new PhotonPoseEstimator(
+ builder.aprilTagFieldLayout, builder.poseEstimatorStrategy, cameraConfig.robotToCamera);
+
+ // Create NetworkTables publishers for 1) the position of the camera relative to the robot and
+ // 2) the estimated position provided by the camera.
+ NetworkTable parentTable = getTableForCamera(camera);
+ StructPublisher Callers will typically pass a field-centric drive train pose.
+ *
+ * @param pose 2D field-centric (relative to blue origin) pose.
+ */
+ public void publishCameraPosesRelativeTo(Pose2d pose) {
+ Pose3d pose3d = new Pose3d(pose);
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ cameraWrapper.publishCameraPose(pose3d);
+ }
+ }
+
+ /**
+ * Add robot heading data to buffer. Must be called periodically for the
+ * PNP_DISTANCE_TRIG_SOLVE strategy.
+ *
+ * @param timestampSeconds timestamp of the robot heading data.
+ * @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
+ * coordinates.
+ */
+ public void addHeadingData(double timestampSeconds, Rotation2d heading) {
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ cameraWrapper.estimator.addHeadingData(timestampSeconds, heading);
+ }
+ }
+
+ /**
+ * Add robot heading data to buffer. Must be called periodically for the
+ * PNP_DISTANCE_TRIG_SOLVE strategy.
+ *
+ * @param timestampSeconds timestamp of the robot heading data.
+ * @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
+ * coordinates.
+ */
+ public void addHeadingData(double timestampSeconds, Rotation3d heading) {
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ cameraWrapper.estimator.addHeadingData(timestampSeconds, heading);
+ }
+ }
+
+ /**
+ * Clears all heading data in the buffer, and adds a new seed. Useful for preventing estimates
+ * from utilizing heading data provided prior to a pose or rotation reset.
+ *
+ * @param timestampSeconds timestamp of the robot heading data.
+ * @param heading Field-relative robot heading at given timestamp. Standard WPILIB field
+ * coordinates.
+ */
+ public void resetHeadingData(double timestampSeconds, Rotation2d heading) {
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ cameraWrapper.estimator.resetHeadingData(timestampSeconds, heading);
+ }
+ }
+
+ public void resetHeadingData(double timestampSeconds, Rotation3d heading) {
+ // TODO: Use PhotonPoseEstimator.resetHeadingData(double, Rotation2d) once we use a version of
+ // PhotonVision that includes it (see https://github.com/PhotonVision/photonvision/pull/2013).
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ cameraWrapper.estimator.resetHeadingData(timestampSeconds, heading.toRotation2d());
+ cameraWrapper.estimator.addHeadingData(timestampSeconds, heading);
+ }
+ }
+
+ /**
+ * Sends all unread robot-pose estimations from all cameras to the provided consumer.
+ *
+ * This method is supposed to be called from a routine updating drive-train pose with pose
+ * estimates from the photon vision cameras.
+ *
+ * @param poseEstimateConsumer Functional interface for consuming computed pose estimates.
+ */
+ public void processAllUnreadResults(PoseEstimateConsumer poseEstimateConsumer) {
+ for (PhotonCameraWrapper cameraWrapper : cameraWrappers) {
+ List
+ .toList();
+
+ poses.forEach(poseEstimateConsumer::addEstimatedRobotPose);
+ cameraWrapper.robotPosePublisher.publish(poses);
+ }
+ }
+
+ @Override
+ public void close() {
+ cameraWrappers.forEach(PhotonCameraWrapper::close);
+ cameraWrappers.clear();
+ }
+}
diff --git a/vision/src/main/java/com/team2813/lib2813/vision/PhotonVisionPosePublisher.java b/vision/src/main/java/com/team2813/lib2813/vision/PhotonVisionPosePublisher.java
index 2fab4c58..4c1a77e4 100644
--- a/vision/src/main/java/com/team2813/lib2813/vision/PhotonVisionPosePublisher.java
+++ b/vision/src/main/java/com/team2813/lib2813/vision/PhotonVisionPosePublisher.java
@@ -2,7 +2,6 @@
import static com.team2813.lib2813.vision.VisionNetworkTables.APRIL_TAG_POSE_TOPIC;
import static com.team2813.lib2813.vision.VisionNetworkTables.POSE_ESTIMATE_TOPIC;
-import static com.team2813.lib2813.vision.VisionNetworkTables.getTableForCamera;
import edu.wpi.first.apriltag.AprilTagFieldLayout;
import edu.wpi.first.math.geometry.Pose3d;
@@ -14,7 +13,6 @@
import java.util.Optional;
import java.util.function.Supplier;
import org.photonvision.EstimatedRobotPose;
-import org.photonvision.PhotonCamera;
import org.photonvision.targeting.PhotonTrackedTarget;
/**
@@ -38,28 +36,28 @@ public final class PhotonVisionPosePublisher {
private final AprilTagFieldLayout aprilTagFieldLayout;
/**
- * Creates a publisher for the provided camera and field layout.
+ * Creates a publisher that publishes values under the given table.
*
- * @param camera Camera to use to get the Network Tables name to publish to.
- * @param aprilTagFieldLayout Layout of AprilTags on a field.
+ * @param parentTable Parent table for all topics published by this publisher instance.
+ * @param aprilTagFieldLayout Layout of AprilTags on the field.
*/
- public PhotonVisionPosePublisher(PhotonCamera camera, AprilTagFieldLayout aprilTagFieldLayout) {
- this(camera, aprilTagFieldLayout, Timer::getFPGATimestamp);
+ public PhotonVisionPosePublisher(
+ NetworkTable parentTable, AprilTagFieldLayout aprilTagFieldLayout) {
+ this(parentTable, aprilTagFieldLayout, Timer::getFPGATimestamp);
}
/** Package-scoped constructor (for unit testing). */
PhotonVisionPosePublisher(
- PhotonCamera camera,
+ NetworkTable parentTable,
AprilTagFieldLayout aprilTagFieldLayout,
Supplier The key of the network table will be `Vision/[cameraName]`.
+ * The key of the network table will be `photonvision/[cameraName]/LatestPose`.
*/
public static NetworkTable getTableForCamera(PhotonCamera camera) {
- NetworkTableInstance ntInstance = camera.getCameraTable().getInstance();
- return ntInstance.getTable(TABLE_NAME).getSubTable(camera.getName());
+ return camera.getCameraTable().getSubTable(SUBTABLE_NAME);
}
private VisionNetworkTables() {
diff --git a/vision/src/test/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimatorTest.java b/vision/src/test/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimatorTest.java
new file mode 100644
index 00000000..c4346cb7
--- /dev/null
+++ b/vision/src/test/java/com/team2813/lib2813/vision/MultiPhotonPoseEstimatorTest.java
@@ -0,0 +1,53 @@
+package com.team2813.lib2813.vision;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.team2813.lib2813.testing.junit.jupiter.IsolatedNetworkTablesExtension;
+import edu.wpi.first.apriltag.AprilTag;
+import edu.wpi.first.apriltag.AprilTagFieldLayout;
+import edu.wpi.first.math.geometry.Pose3d;
+import edu.wpi.first.math.geometry.Quaternion;
+import edu.wpi.first.math.geometry.Rotation3d;
+import edu.wpi.first.math.geometry.Transform3d;
+import edu.wpi.first.math.geometry.Translation3d;
+import edu.wpi.first.networktables.NetworkTableInstance;
+import java.util.List;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.photonvision.PhotonPoseEstimator.PoseStrategy;
+
+/** Tests for {@link MultiPhotonPoseEstimator}. */
+@ExtendWith(IsolatedNetworkTablesExtension.class)
+class MultiPhotonPoseEstimatorTest {
+ private static final double FIELD_LENGTH = 17.548;
+ private static final double FIELD_WIDTH = 8.052;
+ private static final int REEFSCAPE_APRIL_TAG_ID = 7;
+ private static final Pose3d REEFSCAPE_APRIL_TAG_POSE =
+ new Pose3d(
+ new Translation3d(13.890498, 4.0259, 0.308102),
+ new Rotation3d(new Quaternion(1.0, 0.0, 0.0, 0.0)));
+ private static final Transform3d FRONT_CAMERA_TRANSFORM =
+ new Transform3d(
+ 0.1688157406,
+ 0.2939800826,
+ 0.1708140348,
+ new Rotation3d(0, -0.1745329252, -0.5235987756));
+
+ @ParameterizedTest
+ @EnumSource(value = PoseStrategy.class)
+ void getPrimaryStrategy(PoseStrategy poseStrategy, NetworkTableInstance ntInstance) {
+ try (var estimator =
+ new MultiPhotonPoseEstimator.Builder(ntInstance, createFieldLayout(), poseStrategy)
+ .addCamera("front", FRONT_CAMERA_TRANSFORM)
+ .build()) {
+ assertThat(estimator.getPrimaryStrategy()).isEqualTo(poseStrategy);
+ }
+ }
+
+ private static AprilTagFieldLayout createFieldLayout() {
+ List