2023-01-14 09:06:15 -06:00
|
|
|
/*
|
|
|
|
|
* MIT License
|
|
|
|
|
*
|
2023-04-18 18:49:40 -04:00
|
|
|
* Copyright (c) PhotonVision
|
2023-01-14 09:06:15 -06:00
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
* SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package org.photonvision;
|
|
|
|
|
|
|
|
|
|
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
2023-04-18 12:50:23 -05:00
|
|
|
import edu.wpi.first.math.Matrix;
|
2023-01-14 09:06:15 -06:00
|
|
|
import edu.wpi.first.math.Pair;
|
|
|
|
|
import edu.wpi.first.math.geometry.Pose2d;
|
|
|
|
|
import edu.wpi.first.math.geometry.Pose3d;
|
|
|
|
|
import edu.wpi.first.math.geometry.Rotation3d;
|
|
|
|
|
import edu.wpi.first.math.geometry.Transform3d;
|
|
|
|
|
import edu.wpi.first.math.geometry.Translation3d;
|
2023-04-18 12:50:23 -05:00
|
|
|
import edu.wpi.first.math.numbers.N1;
|
|
|
|
|
import edu.wpi.first.math.numbers.N3;
|
|
|
|
|
import edu.wpi.first.math.numbers.N5;
|
2023-01-14 09:06:15 -06:00
|
|
|
import edu.wpi.first.wpilibj.DriverStation;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.HashSet;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Optional;
|
|
|
|
|
import java.util.Set;
|
2023-10-29 23:02:16 -04:00
|
|
|
import org.photonvision.estimation.TargetModel;
|
2023-02-13 17:57:01 -05:00
|
|
|
import org.photonvision.estimation.VisionEstimation;
|
2023-01-14 09:06:15 -06:00
|
|
|
import org.photonvision.targeting.PhotonPipelineResult;
|
|
|
|
|
import org.photonvision.targeting.PhotonTrackedTarget;
|
|
|
|
|
|
|
|
|
|
/**
|
2023-01-17 20:34:21 -05:00
|
|
|
* The PhotonPoseEstimator class filters or combines readings from all the AprilTags visible at a
|
2023-01-14 09:06:15 -06:00
|
|
|
* given timestamp on the field to produce a single robot in field pose, using the strategy set
|
|
|
|
|
* below. Example usage can be found in our apriltagExample example project.
|
|
|
|
|
*/
|
|
|
|
|
public class PhotonPoseEstimator {
|
|
|
|
|
/** Position estimation strategies that can be used by the {@link PhotonPoseEstimator} class. */
|
|
|
|
|
public enum PoseStrategy {
|
|
|
|
|
/** Choose the Pose with the lowest ambiguity. */
|
|
|
|
|
LOWEST_AMBIGUITY,
|
|
|
|
|
|
|
|
|
|
/** Choose the Pose which is closest to the camera height. */
|
|
|
|
|
CLOSEST_TO_CAMERA_HEIGHT,
|
|
|
|
|
|
|
|
|
|
/** Choose the Pose which is closest to a set Reference position. */
|
|
|
|
|
CLOSEST_TO_REFERENCE_POSE,
|
|
|
|
|
|
|
|
|
|
/** Choose the Pose which is closest to the last pose calculated */
|
|
|
|
|
CLOSEST_TO_LAST_POSE,
|
|
|
|
|
|
2023-02-14 12:49:08 -06:00
|
|
|
/** Return the average of the best target poses using ambiguity as weight. */
|
2023-02-13 17:57:01 -05:00
|
|
|
AVERAGE_BEST_TARGETS,
|
|
|
|
|
|
2023-10-17 10:20:00 -04:00
|
|
|
/**
|
|
|
|
|
* Use all visible tags to compute a single pose estimate on coprocessor. This option needs to
|
|
|
|
|
* be enabled on the PhotonVision web UI as well.
|
|
|
|
|
*/
|
|
|
|
|
MULTI_TAG_PNP_ON_COPROCESSOR,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Use all visible tags to compute a single pose estimate. This runs on the RoboRIO, and can
|
|
|
|
|
* take a lot of time.
|
|
|
|
|
*/
|
|
|
|
|
MULTI_TAG_PNP_ON_RIO
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AprilTagFieldLayout fieldTags;
|
2023-10-29 23:02:16 -04:00
|
|
|
private TargetModel tagModel = TargetModel.kAprilTag16h5;
|
2023-02-13 17:57:01 -05:00
|
|
|
private PoseStrategy primaryStrategy;
|
|
|
|
|
private PoseStrategy multiTagFallbackStrategy = PoseStrategy.LOWEST_AMBIGUITY;
|
2023-01-14 09:06:15 -06:00
|
|
|
private final PhotonCamera camera;
|
2023-02-06 09:51:35 -05:00
|
|
|
private Transform3d robotToCamera;
|
2023-01-14 09:06:15 -06:00
|
|
|
|
|
|
|
|
private Pose3d lastPose;
|
|
|
|
|
private Pose3d referencePose;
|
2023-02-13 18:22:22 -08:00
|
|
|
protected double poseCacheTimestampSeconds = -1;
|
2023-01-14 09:06:15 -06:00
|
|
|
private final Set<Integer> reportedErrors = new HashSet<>();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new PhotonPoseEstimator.
|
|
|
|
|
*
|
2023-01-17 20:34:21 -05:00
|
|
|
* @param fieldTags A WPILib {@link AprilTagFieldLayout} linking AprilTag IDs to Pose3d objects
|
2023-04-18 12:50:23 -05:00
|
|
|
* with respect to the FIRST field using the <a href=
|
|
|
|
|
* "https://docs.wpilib.org/en/stable/docs/software/advanced-controls/geometry/coordinate-systems.html#field-coordinate-system">Field
|
2023-11-04 09:25:49 -07:00
|
|
|
* Coordinate System</a>. Note that setting the origin of this layout object will affect the
|
|
|
|
|
* results from this class.
|
2023-01-14 09:06:15 -06:00
|
|
|
* @param strategy The strategy it should use to determine the best pose.
|
2023-02-14 12:49:08 -06:00
|
|
|
* @param camera PhotonCamera
|
|
|
|
|
* @param robotToCamera Transform3d from the center of the robot to the camera mount position (ie,
|
2023-04-18 12:50:23 -05:00
|
|
|
* robot ➔ camera) in the <a href=
|
|
|
|
|
* "https://docs.wpilib.org/en/stable/docs/software/advanced-controls/geometry/coordinate-systems.html#robot-coordinate-system">Robot
|
2023-01-17 20:34:21 -05:00
|
|
|
* Coordinate System</a>.
|
2023-01-14 09:06:15 -06:00
|
|
|
*/
|
|
|
|
|
public PhotonPoseEstimator(
|
|
|
|
|
AprilTagFieldLayout fieldTags,
|
|
|
|
|
PoseStrategy strategy,
|
|
|
|
|
PhotonCamera camera,
|
|
|
|
|
Transform3d robotToCamera) {
|
|
|
|
|
this.fieldTags = fieldTags;
|
2023-02-13 17:57:01 -05:00
|
|
|
this.primaryStrategy = strategy;
|
2023-01-14 09:06:15 -06:00
|
|
|
this.camera = camera;
|
|
|
|
|
this.robotToCamera = robotToCamera;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-18 12:50:23 -05:00
|
|
|
public PhotonPoseEstimator(
|
|
|
|
|
AprilTagFieldLayout fieldTags, PoseStrategy strategy, Transform3d robotToCamera) {
|
|
|
|
|
this.fieldTags = fieldTags;
|
|
|
|
|
this.primaryStrategy = strategy;
|
|
|
|
|
this.camera = null;
|
|
|
|
|
this.robotToCamera = robotToCamera;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-13 18:22:22 -08:00
|
|
|
/** Invalidates the pose cache. */
|
|
|
|
|
private void invalidatePoseCache() {
|
|
|
|
|
poseCacheTimestampSeconds = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkUpdate(Object oldObj, Object newObj) {
|
|
|
|
|
if (oldObj != newObj && oldObj != null && !oldObj.equals(newObj)) {
|
|
|
|
|
invalidatePoseCache();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 09:06:15 -06:00
|
|
|
/**
|
|
|
|
|
* Get the AprilTagFieldLayout being used by the PositionEstimator.
|
|
|
|
|
*
|
2023-11-04 09:25:49 -07:00
|
|
|
* <p>Note: Setting the origin of this layout will affect the results from this class.
|
|
|
|
|
*
|
2023-01-14 09:06:15 -06:00
|
|
|
* @return the AprilTagFieldLayout
|
|
|
|
|
*/
|
|
|
|
|
public AprilTagFieldLayout getFieldTags() {
|
|
|
|
|
return fieldTags;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the AprilTagFieldLayout being used by the PositionEstimator.
|
|
|
|
|
*
|
2023-11-04 09:25:49 -07:00
|
|
|
* <p>Note: Setting the origin of this layout will affect the results from this class.
|
|
|
|
|
*
|
2023-01-14 09:06:15 -06:00
|
|
|
* @param fieldTags the AprilTagFieldLayout
|
|
|
|
|
*/
|
|
|
|
|
public void setFieldTags(AprilTagFieldLayout fieldTags) {
|
2023-02-13 18:22:22 -08:00
|
|
|
checkUpdate(this.fieldTags, fieldTags);
|
2023-01-14 09:06:15 -06:00
|
|
|
this.fieldTags = fieldTags;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-29 23:02:16 -04:00
|
|
|
/**
|
|
|
|
|
* Get the TargetModel representing the tags being detected. This is used for on-rio multitag.
|
|
|
|
|
*
|
|
|
|
|
* <p>By default, this is {@link TargetModel#kAprilTag16h5}.
|
|
|
|
|
*/
|
|
|
|
|
public TargetModel getTagModel() {
|
|
|
|
|
return tagModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the TargetModel representing the tags being detected. This is used for on-rio multitag.
|
|
|
|
|
*
|
|
|
|
|
* @param tagModel E.g. {@link TargetModel#kAprilTag16h5}.
|
|
|
|
|
*/
|
|
|
|
|
public void setTagModel(TargetModel tagModel) {
|
|
|
|
|
this.tagModel = tagModel;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 09:06:15 -06:00
|
|
|
/**
|
|
|
|
|
* Get the Position Estimation Strategy being used by the Position Estimator.
|
|
|
|
|
*
|
|
|
|
|
* @return the strategy
|
|
|
|
|
*/
|
2023-02-13 17:57:01 -05:00
|
|
|
public PoseStrategy getPrimaryStrategy() {
|
|
|
|
|
return primaryStrategy;
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the Position Estimation Strategy used by the Position Estimator.
|
|
|
|
|
*
|
|
|
|
|
* @param strategy the strategy to set
|
|
|
|
|
*/
|
2023-02-13 17:57:01 -05:00
|
|
|
public void setPrimaryStrategy(PoseStrategy strategy) {
|
2023-02-13 18:22:22 -08:00
|
|
|
checkUpdate(this.primaryStrategy, strategy);
|
2023-02-13 17:57:01 -05:00
|
|
|
this.primaryStrategy = strategy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set the Position Estimation Strategy used in multi-tag mode when only one tag can be seen. Must
|
|
|
|
|
* NOT be MULTI_TAG_PNP
|
|
|
|
|
*
|
|
|
|
|
* @param strategy the strategy to set
|
|
|
|
|
*/
|
|
|
|
|
public void setMultiTagFallbackStrategy(PoseStrategy strategy) {
|
2023-02-13 18:22:22 -08:00
|
|
|
checkUpdate(this.multiTagFallbackStrategy, strategy);
|
2023-10-17 10:20:00 -04:00
|
|
|
if (strategy == PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR
|
|
|
|
|
|| strategy == PoseStrategy.MULTI_TAG_PNP_ON_RIO) {
|
2023-02-13 17:57:01 -05:00
|
|
|
DriverStation.reportWarning(
|
2023-06-18 07:38:43 -04:00
|
|
|
"Fallback cannot be set to MULTI_TAG_PNP! Setting to lowest ambiguity", false);
|
2023-02-13 17:57:01 -05:00
|
|
|
strategy = PoseStrategy.LOWEST_AMBIGUITY;
|
|
|
|
|
}
|
|
|
|
|
this.multiTagFallbackStrategy = strategy;
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the reference position that is being used by the estimator.
|
|
|
|
|
*
|
|
|
|
|
* @return the referencePose
|
|
|
|
|
*/
|
|
|
|
|
public Pose3d getReferencePose() {
|
|
|
|
|
return referencePose;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the stored reference pose for use when using the <b>CLOSEST_TO_REFERENCE_POSE</b>
|
|
|
|
|
* strategy.
|
|
|
|
|
*
|
|
|
|
|
* @param referencePose the referencePose to set
|
|
|
|
|
*/
|
|
|
|
|
public void setReferencePose(Pose3d referencePose) {
|
2023-02-13 18:22:22 -08:00
|
|
|
checkUpdate(this.referencePose, referencePose);
|
2023-01-14 09:06:15 -06:00
|
|
|
this.referencePose = referencePose;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the stored reference pose for use when using the <b>CLOSEST_TO_REFERENCE_POSE</b>
|
|
|
|
|
* strategy.
|
|
|
|
|
*
|
|
|
|
|
* @param referencePose the referencePose to set
|
|
|
|
|
*/
|
|
|
|
|
public void setReferencePose(Pose2d referencePose) {
|
2023-02-13 18:22:22 -08:00
|
|
|
setReferencePose(new Pose3d(referencePose));
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the stored last pose. Useful for setting the initial estimate when using the
|
|
|
|
|
* <b>CLOSEST_TO_LAST_POSE</b> strategy.
|
|
|
|
|
*
|
|
|
|
|
* @param lastPose the lastPose to set
|
|
|
|
|
*/
|
|
|
|
|
public void setLastPose(Pose3d lastPose) {
|
|
|
|
|
this.lastPose = lastPose;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the stored last pose. Useful for setting the initial estimate when using the
|
|
|
|
|
* <b>CLOSEST_TO_LAST_POSE</b> strategy.
|
|
|
|
|
*
|
|
|
|
|
* @param lastPose the lastPose to set
|
|
|
|
|
*/
|
|
|
|
|
public void setLastPose(Pose2d lastPose) {
|
2023-01-17 20:34:21 -05:00
|
|
|
setLastPose(new Pose3d(lastPose));
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
2023-06-03 21:04:04 -04:00
|
|
|
/**
|
|
|
|
|
* @return The current transform from the center of the robot to the camera mount position
|
|
|
|
|
*/
|
2023-02-06 09:51:35 -05:00
|
|
|
public Transform3d getRobotToCameraTransform() {
|
|
|
|
|
return robotToCamera;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Useful for pan and tilt mechanisms and such.
|
|
|
|
|
*
|
|
|
|
|
* @param robotToCamera The current transform from the center of the robot to the camera mount
|
|
|
|
|
* position
|
|
|
|
|
*/
|
|
|
|
|
public void setRobotToCameraTransform(Transform3d robotToCamera) {
|
|
|
|
|
this.robotToCamera = robotToCamera;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-14 09:06:15 -06:00
|
|
|
/**
|
|
|
|
|
* Poll data from the configured cameras and update the estimated position of the robot. Returns
|
2023-08-30 18:49:26 -04:00
|
|
|
* empty if:
|
2023-01-14 09:06:15 -06:00
|
|
|
*
|
2023-08-30 18:49:26 -04:00
|
|
|
* <ul>
|
|
|
|
|
* <li>New data has not been received since the last call to {@code update()}.
|
|
|
|
|
* <li>No targets were found from the camera
|
|
|
|
|
* <li>There is no camera set
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* @return an {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
|
|
|
|
|
* create the estimate.
|
2023-01-14 09:06:15 -06:00
|
|
|
*/
|
|
|
|
|
public Optional<EstimatedRobotPose> update() {
|
|
|
|
|
if (camera == null) {
|
|
|
|
|
DriverStation.reportError("[PhotonPoseEstimator] Missing camera!", false);
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PhotonPipelineResult cameraResult = camera.getLatestResult();
|
2023-02-09 14:43:52 -05:00
|
|
|
|
2023-04-18 12:50:23 -05:00
|
|
|
return update(cameraResult, camera.getCameraMatrix(), camera.getDistCoeffs());
|
2023-02-09 14:43:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-30 18:49:26 -04:00
|
|
|
* Updates the estimated position of the robot. Returns empty if:
|
|
|
|
|
*
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>The timestamp of the provided pipeline result is the same as in the previous call to
|
|
|
|
|
* {@code update()}.
|
|
|
|
|
* <li>No targets were found in the pipeline results.
|
|
|
|
|
* </ul>
|
2023-02-09 14:43:52 -05:00
|
|
|
*
|
|
|
|
|
* @param cameraResult The latest pipeline result from the camera
|
2023-08-30 18:49:26 -04:00
|
|
|
* @return an {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
|
|
|
|
|
* create the estimate.
|
2023-02-09 14:43:52 -05:00
|
|
|
*/
|
|
|
|
|
public Optional<EstimatedRobotPose> update(PhotonPipelineResult cameraResult) {
|
2023-04-18 12:50:23 -05:00
|
|
|
if (camera == null) return update(cameraResult, Optional.empty(), Optional.empty());
|
|
|
|
|
return update(cameraResult, camera.getCameraMatrix(), camera.getDistCoeffs());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2023-08-30 18:49:26 -04:00
|
|
|
* Updates the estimated position of the robot. Returns empty if:
|
2023-04-18 12:50:23 -05:00
|
|
|
*
|
2023-08-30 18:49:26 -04:00
|
|
|
* <ul>
|
|
|
|
|
* <li>The timestamp of the provided pipeline result is the same as in the previous call to
|
|
|
|
|
* {@code update()}.
|
|
|
|
|
* <li>No targets were found in the pipeline results.
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* @param cameraMatrix Camera calibration data that can be used in the case of no assigned
|
2023-04-18 12:50:23 -05:00
|
|
|
* PhotonCamera.
|
2023-08-30 18:49:26 -04:00
|
|
|
* @param distCoeffs Camera calibration data that can be used in the case of no assigned
|
2023-04-18 12:50:23 -05:00
|
|
|
* PhotonCamera
|
2023-08-30 18:49:26 -04:00
|
|
|
* @return an {@link EstimatedRobotPose} with an estimated pose, timestamp, and targets used to
|
|
|
|
|
* create the estimate.
|
2023-04-18 12:50:23 -05:00
|
|
|
*/
|
|
|
|
|
public Optional<EstimatedRobotPose> update(
|
|
|
|
|
PhotonPipelineResult cameraResult,
|
2023-08-30 18:49:26 -04:00
|
|
|
Optional<Matrix<N3, N3>> cameraMatrix,
|
|
|
|
|
Optional<Matrix<N5, N1>> distCoeffs) {
|
2023-02-13 18:22:22 -08:00
|
|
|
// Time in the past -- give up, since the following if expects times > 0
|
|
|
|
|
if (cameraResult.getTimestampSeconds() < 0) {
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-18 12:50:23 -05:00
|
|
|
// If the pose cache timestamp was set, and the result is from the same
|
|
|
|
|
// timestamp, return an
|
2023-02-13 18:22:22 -08:00
|
|
|
// empty result
|
|
|
|
|
if (poseCacheTimestampSeconds > 0
|
|
|
|
|
&& Math.abs(poseCacheTimestampSeconds - cameraResult.getTimestampSeconds()) < 1e-6) {
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remember the timestamp of the current result used
|
|
|
|
|
poseCacheTimestampSeconds = cameraResult.getTimestampSeconds();
|
|
|
|
|
|
|
|
|
|
// If no targets seen, trivial case -- return empty result
|
2023-01-14 09:06:15 -06:00
|
|
|
if (!cameraResult.hasTargets()) {
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-30 18:49:26 -04:00
|
|
|
return update(cameraResult, cameraMatrix, distCoeffs, this.primaryStrategy);
|
2023-02-13 17:57:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<EstimatedRobotPose> update(
|
2023-04-18 12:50:23 -05:00
|
|
|
PhotonPipelineResult cameraResult,
|
2023-08-30 18:49:26 -04:00
|
|
|
Optional<Matrix<N3, N3>> cameraMatrix,
|
|
|
|
|
Optional<Matrix<N5, N1>> distCoeffs,
|
2023-04-18 12:50:23 -05:00
|
|
|
PoseStrategy strat) {
|
2023-01-14 09:06:15 -06:00
|
|
|
Optional<EstimatedRobotPose> estimatedPose;
|
2023-02-13 17:57:01 -05:00
|
|
|
switch (strat) {
|
2023-01-14 09:06:15 -06:00
|
|
|
case LOWEST_AMBIGUITY:
|
|
|
|
|
estimatedPose = lowestAmbiguityStrategy(cameraResult);
|
|
|
|
|
break;
|
|
|
|
|
case CLOSEST_TO_CAMERA_HEIGHT:
|
|
|
|
|
estimatedPose = closestToCameraHeightStrategy(cameraResult);
|
|
|
|
|
break;
|
2023-02-13 18:22:22 -08:00
|
|
|
case CLOSEST_TO_REFERENCE_POSE:
|
|
|
|
|
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
|
|
|
|
break;
|
2023-01-14 09:06:15 -06:00
|
|
|
case CLOSEST_TO_LAST_POSE:
|
|
|
|
|
setReferencePose(lastPose);
|
|
|
|
|
estimatedPose = closestToReferencePoseStrategy(cameraResult, referencePose);
|
|
|
|
|
break;
|
|
|
|
|
case AVERAGE_BEST_TARGETS:
|
|
|
|
|
estimatedPose = averageBestTargetsStrategy(cameraResult);
|
|
|
|
|
break;
|
2023-10-17 10:20:00 -04:00
|
|
|
case MULTI_TAG_PNP_ON_RIO:
|
|
|
|
|
estimatedPose = multiTagOnRioStrategy(cameraResult, cameraMatrix, distCoeffs);
|
|
|
|
|
break;
|
|
|
|
|
case MULTI_TAG_PNP_ON_COPROCESSOR:
|
|
|
|
|
estimatedPose = multiTagOnCoprocStrategy(cameraResult, cameraMatrix, distCoeffs);
|
2023-02-13 17:57:01 -05:00
|
|
|
break;
|
2023-01-14 09:06:15 -06:00
|
|
|
default:
|
|
|
|
|
DriverStation.reportError(
|
|
|
|
|
"[PhotonPoseEstimator] Unknown Position Estimation Strategy!", false);
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (estimatedPose.isEmpty()) {
|
|
|
|
|
lastPose = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return estimatedPose;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-17 10:20:00 -04:00
|
|
|
private Optional<EstimatedRobotPose> multiTagOnCoprocStrategy(
|
|
|
|
|
PhotonPipelineResult result,
|
|
|
|
|
Optional<Matrix<N3, N3>> cameraMatrixOpt,
|
|
|
|
|
Optional<Matrix<N5, N1>> distCoeffsOpt) {
|
|
|
|
|
if (result.getMultiTagResult().estimatedPose.isPresent) {
|
|
|
|
|
var best_tf = result.getMultiTagResult().estimatedPose.best;
|
|
|
|
|
var best =
|
|
|
|
|
new Pose3d()
|
|
|
|
|
.plus(best_tf) // field-to-camera
|
2023-11-04 09:25:49 -07:00
|
|
|
.relativeTo(fieldTags.getOrigin())
|
2023-10-17 10:20:00 -04:00
|
|
|
.plus(robotToCamera.inverse()); // field-to-robot
|
|
|
|
|
return Optional.of(
|
|
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
best,
|
|
|
|
|
result.getTimestampSeconds(),
|
|
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR));
|
|
|
|
|
} else {
|
|
|
|
|
return update(result, cameraMatrixOpt, distCoeffsOpt, this.multiTagFallbackStrategy);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Optional<EstimatedRobotPose> multiTagOnRioStrategy(
|
2023-04-18 12:50:23 -05:00
|
|
|
PhotonPipelineResult result,
|
|
|
|
|
Optional<Matrix<N3, N3>> cameraMatrixOpt,
|
|
|
|
|
Optional<Matrix<N5, N1>> distCoeffsOpt) {
|
2023-06-18 00:00:30 -04:00
|
|
|
boolean hasCalibData = cameraMatrixOpt.isPresent() && distCoeffsOpt.isPresent();
|
2023-07-23 18:32:36 -07:00
|
|
|
// cannot run multitagPNP, use fallback strategy
|
|
|
|
|
if (!hasCalibData || result.getTargets().size() < 2) {
|
2023-06-17 22:16:10 -07:00
|
|
|
return update(result, cameraMatrixOpt, distCoeffsOpt, this.multiTagFallbackStrategy);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-18 00:00:30 -04:00
|
|
|
var pnpResults =
|
2023-07-23 18:32:36 -07:00
|
|
|
VisionEstimation.estimateCamPosePNP(
|
2023-10-29 23:02:16 -04:00
|
|
|
cameraMatrixOpt.get(), distCoeffsOpt.get(), result.getTargets(), fieldTags, tagModel);
|
2023-07-23 18:32:36 -07:00
|
|
|
// try fallback strategy if solvePNP fails for some reason
|
|
|
|
|
if (!pnpResults.isPresent)
|
|
|
|
|
return update(result, cameraMatrixOpt, distCoeffsOpt, this.multiTagFallbackStrategy);
|
2023-06-18 00:00:30 -04:00
|
|
|
var best =
|
|
|
|
|
new Pose3d()
|
|
|
|
|
.plus(pnpResults.best) // field-to-camera
|
|
|
|
|
.plus(robotToCamera.inverse()); // field-to-robot
|
2023-02-13 17:57:01 -05:00
|
|
|
|
2023-06-18 00:00:30 -04:00
|
|
|
return Optional.of(
|
2023-10-17 10:20:00 -04:00
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
best,
|
|
|
|
|
result.getTimestampSeconds(),
|
|
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.MULTI_TAG_PNP_ON_RIO));
|
2023-02-13 17:57:01 -05:00
|
|
|
}
|
|
|
|
|
|
2023-01-14 09:06:15 -06:00
|
|
|
/**
|
|
|
|
|
* Return the estimated position of the robot with the lowest position ambiguity from a List of
|
|
|
|
|
* pipeline results.
|
|
|
|
|
*
|
|
|
|
|
* @param result pipeline result
|
|
|
|
|
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
|
|
|
|
|
* estimation.
|
|
|
|
|
*/
|
|
|
|
|
private Optional<EstimatedRobotPose> lowestAmbiguityStrategy(PhotonPipelineResult result) {
|
|
|
|
|
PhotonTrackedTarget lowestAmbiguityTarget = null;
|
|
|
|
|
|
|
|
|
|
double lowestAmbiguityScore = 10;
|
|
|
|
|
|
|
|
|
|
for (PhotonTrackedTarget target : result.targets) {
|
|
|
|
|
double targetPoseAmbiguity = target.getPoseAmbiguity();
|
|
|
|
|
// Make sure the target is a Fiducial target.
|
|
|
|
|
if (targetPoseAmbiguity != -1 && targetPoseAmbiguity < lowestAmbiguityScore) {
|
|
|
|
|
lowestAmbiguityScore = targetPoseAmbiguity;
|
|
|
|
|
lowestAmbiguityTarget = target;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Although there are confirmed to be targets, none of them may be fiducial
|
|
|
|
|
// targets.
|
|
|
|
|
if (lowestAmbiguityTarget == null) return Optional.empty();
|
|
|
|
|
|
|
|
|
|
int targetFiducialId = lowestAmbiguityTarget.getFiducialId();
|
|
|
|
|
|
|
|
|
|
Optional<Pose3d> targetPosition = fieldTags.getTagPose(targetFiducialId);
|
|
|
|
|
|
|
|
|
|
if (targetPosition.isEmpty()) {
|
|
|
|
|
reportFiducialPoseError(targetFiducialId);
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Optional.of(
|
|
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(lowestAmbiguityTarget.getBestCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse()),
|
2023-02-13 17:57:01 -05:00
|
|
|
result.getTimestampSeconds(),
|
2023-10-17 10:20:00 -04:00
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.LOWEST_AMBIGUITY));
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the estimated position of the robot using the target with the lowest delta height
|
|
|
|
|
* difference between the estimated and actual height of the camera.
|
|
|
|
|
*
|
|
|
|
|
* @param result pipeline result
|
|
|
|
|
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
|
|
|
|
|
* estimation.
|
|
|
|
|
*/
|
|
|
|
|
private Optional<EstimatedRobotPose> closestToCameraHeightStrategy(PhotonPipelineResult result) {
|
|
|
|
|
double smallestHeightDifference = 10e9;
|
|
|
|
|
EstimatedRobotPose closestHeightTarget = null;
|
|
|
|
|
|
|
|
|
|
for (PhotonTrackedTarget target : result.targets) {
|
|
|
|
|
int targetFiducialId = target.getFiducialId();
|
|
|
|
|
|
|
|
|
|
// Don't report errors for non-fiducial targets. This could also be resolved by
|
|
|
|
|
// adding -1 to
|
|
|
|
|
// the initial HashSet.
|
|
|
|
|
if (targetFiducialId == -1) continue;
|
|
|
|
|
|
|
|
|
|
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
|
|
|
|
|
|
|
|
|
|
if (targetPosition.isEmpty()) {
|
|
|
|
|
reportFiducialPoseError(target.getFiducialId());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double alternateTransformDelta =
|
|
|
|
|
Math.abs(
|
|
|
|
|
robotToCamera.getZ()
|
|
|
|
|
- targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getAlternateCameraToTarget().inverse())
|
|
|
|
|
.getZ());
|
|
|
|
|
double bestTransformDelta =
|
|
|
|
|
Math.abs(
|
|
|
|
|
robotToCamera.getZ()
|
|
|
|
|
- targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getBestCameraToTarget().inverse())
|
|
|
|
|
.getZ());
|
|
|
|
|
|
|
|
|
|
if (alternateTransformDelta < smallestHeightDifference) {
|
|
|
|
|
smallestHeightDifference = alternateTransformDelta;
|
|
|
|
|
closestHeightTarget =
|
|
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getAlternateCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse()),
|
2023-02-13 17:57:01 -05:00
|
|
|
result.getTimestampSeconds(),
|
2023-10-17 10:20:00 -04:00
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.CLOSEST_TO_CAMERA_HEIGHT);
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bestTransformDelta < smallestHeightDifference) {
|
|
|
|
|
smallestHeightDifference = bestTransformDelta;
|
|
|
|
|
closestHeightTarget =
|
|
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getBestCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse()),
|
2023-02-13 17:57:01 -05:00
|
|
|
result.getTimestampSeconds(),
|
2023-10-17 10:20:00 -04:00
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.CLOSEST_TO_CAMERA_HEIGHT);
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Need to null check here in case none of the provided targets are fiducial.
|
|
|
|
|
return Optional.ofNullable(closestHeightTarget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the estimated position of the robot using the target with the lowest delta in the vector
|
|
|
|
|
* magnitude between it and the reference pose.
|
|
|
|
|
*
|
|
|
|
|
* @param result pipeline result
|
|
|
|
|
* @param referencePose reference pose to check vector magnitude difference against.
|
|
|
|
|
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
|
|
|
|
|
* estimation.
|
|
|
|
|
*/
|
|
|
|
|
private Optional<EstimatedRobotPose> closestToReferencePoseStrategy(
|
|
|
|
|
PhotonPipelineResult result, Pose3d referencePose) {
|
|
|
|
|
if (referencePose == null) {
|
|
|
|
|
DriverStation.reportError(
|
|
|
|
|
"[PhotonPoseEstimator] Tried to use reference pose strategy without setting the reference!",
|
|
|
|
|
false);
|
|
|
|
|
return Optional.empty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double smallestPoseDelta = 10e9;
|
|
|
|
|
EstimatedRobotPose lowestDeltaPose = null;
|
|
|
|
|
|
|
|
|
|
for (PhotonTrackedTarget target : result.targets) {
|
|
|
|
|
int targetFiducialId = target.getFiducialId();
|
|
|
|
|
|
|
|
|
|
// Don't report errors for non-fiducial targets. This could also be resolved by
|
|
|
|
|
// adding -1 to
|
|
|
|
|
// the initial HashSet.
|
|
|
|
|
if (targetFiducialId == -1) continue;
|
|
|
|
|
|
|
|
|
|
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
|
|
|
|
|
|
|
|
|
|
if (targetPosition.isEmpty()) {
|
|
|
|
|
reportFiducialPoseError(targetFiducialId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Pose3d altTransformPosition =
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getAlternateCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse());
|
|
|
|
|
Pose3d bestTransformPosition =
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getBestCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse());
|
|
|
|
|
|
|
|
|
|
double altDifference = Math.abs(calculateDifference(referencePose, altTransformPosition));
|
|
|
|
|
double bestDifference = Math.abs(calculateDifference(referencePose, bestTransformPosition));
|
|
|
|
|
|
|
|
|
|
if (altDifference < smallestPoseDelta) {
|
|
|
|
|
smallestPoseDelta = altDifference;
|
|
|
|
|
lowestDeltaPose =
|
2023-02-13 17:57:01 -05:00
|
|
|
new EstimatedRobotPose(
|
2023-10-17 10:20:00 -04:00
|
|
|
altTransformPosition,
|
|
|
|
|
result.getTimestampSeconds(),
|
|
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.CLOSEST_TO_REFERENCE_POSE);
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
if (bestDifference < smallestPoseDelta) {
|
|
|
|
|
smallestPoseDelta = bestDifference;
|
|
|
|
|
lowestDeltaPose =
|
2023-02-13 17:57:01 -05:00
|
|
|
new EstimatedRobotPose(
|
2023-10-17 10:20:00 -04:00
|
|
|
bestTransformPosition,
|
|
|
|
|
result.getTimestampSeconds(),
|
|
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.CLOSEST_TO_REFERENCE_POSE);
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return Optional.ofNullable(lowestDeltaPose);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the average of the best target poses using ambiguity as weight.
|
|
|
|
|
*
|
|
|
|
|
* @param result pipeline result
|
|
|
|
|
* @return the estimated position of the robot in the FCS and the estimated timestamp of this
|
|
|
|
|
* estimation.
|
|
|
|
|
*/
|
|
|
|
|
private Optional<EstimatedRobotPose> averageBestTargetsStrategy(PhotonPipelineResult result) {
|
|
|
|
|
List<Pair<PhotonTrackedTarget, Pose3d>> estimatedRobotPoses = new ArrayList<>();
|
|
|
|
|
double totalAmbiguity = 0;
|
|
|
|
|
|
|
|
|
|
for (PhotonTrackedTarget target : result.targets) {
|
|
|
|
|
int targetFiducialId = target.getFiducialId();
|
|
|
|
|
|
|
|
|
|
// Don't report errors for non-fiducial targets. This could also be resolved by
|
|
|
|
|
// adding -1 to
|
|
|
|
|
// the initial HashSet.
|
|
|
|
|
if (targetFiducialId == -1) continue;
|
|
|
|
|
|
|
|
|
|
Optional<Pose3d> targetPosition = fieldTags.getTagPose(target.getFiducialId());
|
|
|
|
|
|
|
|
|
|
if (targetPosition.isEmpty()) {
|
|
|
|
|
reportFiducialPoseError(targetFiducialId);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
double targetPoseAmbiguity = target.getPoseAmbiguity();
|
|
|
|
|
|
|
|
|
|
// Pose ambiguity is 0, use that pose
|
|
|
|
|
if (targetPoseAmbiguity == 0) {
|
|
|
|
|
return Optional.of(
|
|
|
|
|
new EstimatedRobotPose(
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getBestCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse()),
|
2023-02-13 17:57:01 -05:00
|
|
|
result.getTimestampSeconds(),
|
2023-10-17 10:20:00 -04:00
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.AVERAGE_BEST_TARGETS));
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalAmbiguity += 1.0 / target.getPoseAmbiguity();
|
|
|
|
|
|
|
|
|
|
estimatedRobotPoses.add(
|
|
|
|
|
new Pair<>(
|
|
|
|
|
target,
|
|
|
|
|
targetPosition
|
|
|
|
|
.get()
|
|
|
|
|
.transformBy(target.getBestCameraToTarget().inverse())
|
|
|
|
|
.transformBy(robotToCamera.inverse())));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Take the average
|
|
|
|
|
|
|
|
|
|
Translation3d transform = new Translation3d();
|
|
|
|
|
Rotation3d rotation = new Rotation3d();
|
|
|
|
|
|
|
|
|
|
if (estimatedRobotPoses.isEmpty()) return Optional.empty();
|
|
|
|
|
|
|
|
|
|
for (Pair<PhotonTrackedTarget, Pose3d> pair : estimatedRobotPoses) {
|
|
|
|
|
// Total ambiguity is non-zero confirmed because if it was zero, that pose was
|
|
|
|
|
// returned.
|
|
|
|
|
double weight = (1.0 / pair.getFirst().getPoseAmbiguity()) / totalAmbiguity;
|
|
|
|
|
Pose3d estimatedPose = pair.getSecond();
|
|
|
|
|
transform = transform.plus(estimatedPose.getTranslation().times(weight));
|
|
|
|
|
rotation = rotation.plus(estimatedPose.getRotation().times(weight));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Optional.of(
|
2023-02-13 17:57:01 -05:00
|
|
|
new EstimatedRobotPose(
|
2023-10-17 10:20:00 -04:00
|
|
|
new Pose3d(transform, rotation),
|
|
|
|
|
result.getTimestampSeconds(),
|
|
|
|
|
result.getTargets(),
|
|
|
|
|
PoseStrategy.AVERAGE_BEST_TARGETS));
|
2023-01-14 09:06:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Difference is defined as the vector magnitude between the two poses
|
|
|
|
|
*
|
|
|
|
|
* @return The absolute "difference" (>=0) between two Pose3ds.
|
|
|
|
|
*/
|
|
|
|
|
private double calculateDifference(Pose3d x, Pose3d y) {
|
|
|
|
|
return x.getTranslation().getDistance(y.getTranslation());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void reportFiducialPoseError(int fiducialId) {
|
|
|
|
|
if (!reportedErrors.contains(fiducialId)) {
|
|
|
|
|
DriverStation.reportError(
|
2023-01-17 20:34:21 -05:00
|
|
|
"[PhotonPoseEstimator] Tried to get pose of unknown AprilTag: " + fiducialId, false);
|
2023-01-14 09:06:15 -06:00
|
|
|
reportedErrors.add(fiducialId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|