2025-01-09 12:57:04 -07:00
|
|
|
package frc4388.robot.subsystems;
|
|
|
|
|
|
|
|
|
|
import edu.wpi.first.math.Matrix;
|
|
|
|
|
import edu.wpi.first.math.VecBuilder;
|
|
|
|
|
import edu.wpi.first.math.geometry.Pose2d;
|
2025-01-15 16:32:13 -07:00
|
|
|
import edu.wpi.first.math.geometry.Rotation2d;
|
2025-01-10 21:36:30 -07:00
|
|
|
import edu.wpi.first.math.geometry.Transform3d;
|
|
|
|
|
import edu.wpi.first.math.geometry.Translation2d;
|
2025-01-09 12:57:04 -07:00
|
|
|
import edu.wpi.first.math.numbers.N1;
|
|
|
|
|
import edu.wpi.first.math.numbers.N3;
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Optional;
|
|
|
|
|
|
|
|
|
|
import org.photonvision.EstimatedRobotPose;
|
|
|
|
|
import org.photonvision.PhotonCamera;
|
|
|
|
|
import org.photonvision.PhotonPoseEstimator;
|
|
|
|
|
import org.photonvision.PhotonPoseEstimator.PoseStrategy;
|
|
|
|
|
import org.photonvision.simulation.PhotonCameraSim;
|
|
|
|
|
import org.photonvision.simulation.SimCameraProperties;
|
|
|
|
|
import org.photonvision.simulation.VisionSystemSim;
|
2025-01-10 21:36:30 -07:00
|
|
|
import org.photonvision.targeting.MultiTargetPNPResult;
|
2025-01-17 19:34:08 -07:00
|
|
|
import org.photonvision.targeting.PhotonPipelineResult;
|
2025-01-09 12:57:04 -07:00
|
|
|
import org.photonvision.targeting.PhotonTrackedTarget;
|
|
|
|
|
|
|
|
|
|
import com.ctre.phoenix6.Utils;
|
|
|
|
|
|
|
|
|
|
import edu.wpi.first.networktables.GenericEntry;
|
|
|
|
|
import edu.wpi.first.wpilibj.shuffleboard.BuiltInLayouts;
|
|
|
|
|
import edu.wpi.first.wpilibj.shuffleboard.BuiltInWidgets;
|
|
|
|
|
import edu.wpi.first.wpilibj.shuffleboard.Shuffleboard;
|
|
|
|
|
import edu.wpi.first.wpilibj.shuffleboard.ShuffleboardLayout;
|
|
|
|
|
import edu.wpi.first.wpilibj.smartdashboard.Field2d;
|
|
|
|
|
import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard;
|
2025-01-16 16:51:09 -07:00
|
|
|
import frc4388.robot.Constants.FieldConstants;
|
2025-01-09 12:57:04 -07:00
|
|
|
import frc4388.robot.Constants.VisionConstants;
|
|
|
|
|
import frc4388.utility.Status;
|
|
|
|
|
import frc4388.utility.Subsystem;
|
2025-02-28 14:21:32 -07:00
|
|
|
import frc4388.utility.Status.ReportLevel;
|
2025-01-09 12:57:04 -07:00
|
|
|
|
|
|
|
|
public class Vision extends Subsystem {
|
|
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
// private PhotonCamera leftCamera;
|
|
|
|
|
// private PhotonCamera rightCamera;
|
|
|
|
|
|
|
|
|
|
private PhotonCamera[] cameras;
|
|
|
|
|
private PhotonPoseEstimator[] estimators;
|
2025-01-09 12:57:04 -07:00
|
|
|
|
2025-01-16 19:41:05 -07:00
|
|
|
private boolean isTagDetected = false;
|
|
|
|
|
private boolean isTagProcessed = false;
|
2025-01-09 12:57:04 -07:00
|
|
|
private Pose2d lastVisionPose = new Pose2d();
|
|
|
|
|
private Pose2d lastPhysOdomPose = new Pose2d();
|
|
|
|
|
|
|
|
|
|
private Matrix<N3, N1> curStdDevs;
|
|
|
|
|
|
|
|
|
|
private Field2d field = new Field2d();
|
2025-01-10 21:36:30 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
ShuffleboardLayout subsystemLayout = Shuffleboard.getTab("Subsystems")
|
|
|
|
|
.getLayout(getSubsystemName(), BuiltInLayouts.kList)
|
|
|
|
|
.withSize(2, 2);
|
|
|
|
|
|
2025-01-16 19:41:05 -07:00
|
|
|
GenericEntry sbTagDetected = subsystemLayout
|
2025-01-10 21:36:30 -07:00
|
|
|
.add("Tag Detected", false)
|
|
|
|
|
.withWidget(BuiltInWidgets.kBooleanBox)
|
|
|
|
|
.getEntry();
|
2025-01-16 19:41:05 -07:00
|
|
|
|
|
|
|
|
GenericEntry sbTagProcessed = subsystemLayout
|
|
|
|
|
.add("Tag Processed", false)
|
|
|
|
|
.withWidget(BuiltInWidgets.kBooleanBox)
|
|
|
|
|
.getEntry();
|
2025-01-10 21:36:30 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
GenericEntry sbLeftCamConnected = subsystemLayout
|
|
|
|
|
.add("Left Camera Connnected", false)
|
|
|
|
|
.withWidget(BuiltInWidgets.kBooleanBox)
|
|
|
|
|
.getEntry();
|
|
|
|
|
|
|
|
|
|
GenericEntry sbRightCamConnected = subsystemLayout
|
|
|
|
|
.add("Right Camera Connnected", false)
|
2025-01-10 21:36:30 -07:00
|
|
|
.withWidget(BuiltInWidgets.kBooleanBox)
|
|
|
|
|
.getEntry();
|
2025-01-09 12:57:04 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
public Vision(PhotonCamera leftCamera, PhotonCamera rightCamera){
|
2025-01-09 12:57:04 -07:00
|
|
|
SmartDashboard.putData(field);
|
|
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
this.cameras = new PhotonCamera[]{leftCamera, rightCamera};
|
|
|
|
|
|
2025-02-06 13:23:16 -07:00
|
|
|
PhotonPoseEstimator photonEstimatorLeft = new PhotonPoseEstimator(FieldConstants.kTagLayout, PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, VisionConstants.LEFT_CAMERA_POS);
|
|
|
|
|
PhotonPoseEstimator photonEstimatorRight = new PhotonPoseEstimator(FieldConstants.kTagLayout, PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, VisionConstants.RIGHT_CAMERA_POS);
|
2025-02-06 13:02:39 -07:00
|
|
|
|
|
|
|
|
photonEstimatorLeft.setMultiTagFallbackStrategy(PoseStrategy.LOWEST_AMBIGUITY);
|
|
|
|
|
photonEstimatorRight.setMultiTagFallbackStrategy(PoseStrategy.LOWEST_AMBIGUITY);
|
|
|
|
|
|
|
|
|
|
this.estimators = new PhotonPoseEstimator[]{photonEstimatorLeft, photonEstimatorRight};
|
2025-01-09 12:57:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void periodic() {
|
2025-02-06 13:02:39 -07:00
|
|
|
update();
|
|
|
|
|
field.setRobotPose(getPose2d());
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-17 19:34:08 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
private void update() {
|
2025-01-16 19:41:05 -07:00
|
|
|
isTagProcessed = false;
|
2025-02-06 13:02:39 -07:00
|
|
|
isTagDetected = false;
|
2025-01-09 12:57:04 -07:00
|
|
|
|
|
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
int cams = 0;
|
2025-01-09 12:57:04 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
double X = 0;
|
|
|
|
|
double Y = 0;
|
|
|
|
|
double Yaw = 0;
|
2025-01-10 21:36:30 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
for(int i = 0; i < cameras.length; i++){
|
|
|
|
|
PhotonCamera camera = cameras[i];
|
|
|
|
|
PhotonPoseEstimator estimator = estimators[i];
|
2025-01-16 19:41:05 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
var results = camera.getAllUnreadResults();
|
2025-01-10 21:36:30 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
// If there are no more updates from the camera
|
|
|
|
|
if (results.size() == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var result = results.get(results.size()-1);
|
|
|
|
|
|
|
|
|
|
isTagDetected = isTagDetected | result.hasTargets();
|
|
|
|
|
|
|
|
|
|
// If there are no tags
|
|
|
|
|
if(!result.hasTargets())
|
|
|
|
|
continue;
|
2025-01-09 12:57:04 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
Optional<EstimatedRobotPose> estimatedRobotPose = getEstimatedGlobalPose(result, estimator);
|
2025-01-09 12:57:04 -07:00
|
|
|
|
2025-02-06 13:02:39 -07:00
|
|
|
// If the tag was failed to be processed
|
|
|
|
|
if(estimatedRobotPose.isEmpty())
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
Pose2d pose = estimatedRobotPose.get().estimatedPose.toPose2d();
|
|
|
|
|
X += pose.getX();
|
|
|
|
|
Y += pose.getY();
|
|
|
|
|
Yaw += pose.getRotation().getDegrees();
|
|
|
|
|
cams++;
|
|
|
|
|
isTagProcessed = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(isTagProcessed){
|
|
|
|
|
lastVisionPose = new Pose2d(X/cams, Y/cams, Rotation2d.fromDegrees(Yaw/cams));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-09 12:57:04 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The latest estimated robot pose on the field from vision data. This may be empty. This should
|
|
|
|
|
* only be called once per loop.
|
|
|
|
|
*
|
|
|
|
|
* <p>Also includes updates for the standard deviations, which can (optionally) be retrieved with
|
|
|
|
|
* {@link getEstimationStdDevs}
|
|
|
|
|
*
|
|
|
|
|
* @return An {@link EstimatedRobotPose} with an estimated pose, estimate timestamp, and targets
|
|
|
|
|
* used for estimation.
|
|
|
|
|
*/
|
2025-02-06 13:02:39 -07:00
|
|
|
public Optional<EstimatedRobotPose> getEstimatedGlobalPose(PhotonPipelineResult change, PhotonPoseEstimator estimator) {
|
2025-01-09 12:57:04 -07:00
|
|
|
Optional<EstimatedRobotPose> visionEst = Optional.empty();
|
2025-02-06 13:02:39 -07:00
|
|
|
|
|
|
|
|
var targets = change.getTargets();
|
|
|
|
|
for(int i = targets.size()-1; i >= 0; i--){
|
|
|
|
|
Transform3d pos = targets.get(i).getBestCameraToTarget();
|
|
|
|
|
double distance = Math.sqrt(Math.pow(pos.getX(),2) + Math.pow(pos.getY(),2) + Math.pow(pos.getZ(),2));
|
|
|
|
|
if (distance > VisionConstants.MIN_ESTIMATION_DISTANCE) {
|
|
|
|
|
change.targets.remove(i);
|
2025-01-17 19:34:08 -07:00
|
|
|
}
|
2025-02-06 13:02:39 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(targets.size() <= 0)
|
|
|
|
|
return visionEst; // Will be empty
|
|
|
|
|
|
|
|
|
|
visionEst = estimator.update(change);
|
|
|
|
|
updateEstimationStdDevs(visionEst, change.getTargets(), estimator);
|
2025-01-17 19:34:08 -07:00
|
|
|
|
2025-01-09 12:57:04 -07:00
|
|
|
return visionEst;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculates new standard deviations This algorithm is a heuristic that creates dynamic standard
|
|
|
|
|
* deviations based on number of tags, estimation strategy, and distance from the tags.
|
|
|
|
|
*
|
|
|
|
|
* @param estimatedPose The estimated pose to guess standard deviations for.
|
|
|
|
|
* @param targets All targets in this camera frame
|
|
|
|
|
*/
|
|
|
|
|
private void updateEstimationStdDevs(
|
2025-02-06 13:02:39 -07:00
|
|
|
Optional<EstimatedRobotPose> estimatedPose,
|
|
|
|
|
List<PhotonTrackedTarget> targets,
|
|
|
|
|
PhotonPoseEstimator estimator) {
|
2025-01-09 12:57:04 -07:00
|
|
|
if (estimatedPose.isEmpty()) {
|
|
|
|
|
// No pose input. Default to single-tag std devs
|
|
|
|
|
curStdDevs = VisionConstants.kSingleTagStdDevs;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// Pose present. Start running Heuristic
|
|
|
|
|
var estStdDevs = VisionConstants.kSingleTagStdDevs;
|
|
|
|
|
int numTags = 0;
|
|
|
|
|
double avgDist = 0;
|
|
|
|
|
|
|
|
|
|
// Precalculation - see how many tags we found, and calculate an average-distance metric
|
|
|
|
|
for (var tgt : targets) {
|
2025-02-06 13:02:39 -07:00
|
|
|
var tagPose = estimator.getFieldTags().getTagPose(tgt.getFiducialId());
|
2025-01-09 12:57:04 -07:00
|
|
|
if (tagPose.isEmpty()) continue;
|
2025-01-17 19:34:08 -07:00
|
|
|
|
|
|
|
|
double distance = tagPose
|
|
|
|
|
.get()
|
|
|
|
|
.toPose2d()
|
|
|
|
|
.getTranslation()
|
|
|
|
|
.getDistance(estimatedPose.get().estimatedPose.toPose2d().getTranslation());
|
|
|
|
|
|
|
|
|
|
if (distance < VisionConstants.MIN_ESTIMATION_DISTANCE) {
|
|
|
|
|
numTags++;
|
|
|
|
|
avgDist += distance;
|
|
|
|
|
}
|
2025-01-09 12:57:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (numTags == 0) {
|
|
|
|
|
// No tags visible. Default to single-tag std devs
|
|
|
|
|
curStdDevs = VisionConstants.kSingleTagStdDevs;
|
|
|
|
|
} else {
|
|
|
|
|
// One or more tags visible, run the full heuristic.
|
|
|
|
|
avgDist /= numTags;
|
|
|
|
|
// Decrease std devs if multiple targets are visible
|
|
|
|
|
if (numTags > 1) estStdDevs = VisionConstants.kMultiTagStdDevs;
|
|
|
|
|
// Increase std devs based on (average) distance
|
|
|
|
|
if (numTags == 1 && avgDist > 4)
|
|
|
|
|
estStdDevs = VecBuilder.fill(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE);
|
|
|
|
|
else estStdDevs = estStdDevs.times(1 + (avgDist * avgDist / 30));
|
|
|
|
|
curStdDevs = estStdDevs;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the latest standard deviations of the estimated pose from {@link
|
|
|
|
|
* #getEstimatedGlobalPose()}, for use with {@link
|
|
|
|
|
* edu.wpi.first.math.estimator.SwerveDrivePoseEstimator SwerveDrivePoseEstimator}. This should
|
|
|
|
|
* only be used when there are targets visible.
|
|
|
|
|
*/
|
|
|
|
|
public Matrix<N3, N1> getEstimationStdDevs() {
|
|
|
|
|
return curStdDevs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void setLastOdomPose(Optional<Pose2d> pose){
|
|
|
|
|
if(pose.isPresent())
|
|
|
|
|
lastPhysOdomPose = pose.get();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Pose2d getPose2d() {
|
2025-01-16 19:41:05 -07:00
|
|
|
if(isTagDetected && isTagProcessed)
|
2025-01-09 12:57:04 -07:00
|
|
|
return lastVisionPose;
|
|
|
|
|
else
|
|
|
|
|
return lastPhysOdomPose;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static double getTime() {
|
|
|
|
|
return Utils.getCurrentTimeSeconds();
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 19:40:37 -07:00
|
|
|
public boolean isTag(){
|
2025-01-16 19:41:05 -07:00
|
|
|
return isTagDetected && isTagProcessed;
|
2025-01-09 19:40:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-01-09 12:57:04 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String getSubsystemName() {
|
|
|
|
|
return "Vision";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenericEntry sbShiftState = subsystemLayout
|
|
|
|
|
// .add("Shift State", 0)
|
|
|
|
|
// .withWidget(BuiltInWidgets.kNumberBar)
|
|
|
|
|
// .getEntry();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void queryStatus() {
|
2025-01-16 19:41:05 -07:00
|
|
|
sbTagDetected.setBoolean(isTagDetected);
|
|
|
|
|
sbTagProcessed.setBoolean(isTagProcessed);
|
2025-02-06 13:02:39 -07:00
|
|
|
sbLeftCamConnected.setBoolean(cameras[0].isConnected());
|
|
|
|
|
sbRightCamConnected.setBoolean(cameras[1].isConnected());
|
2025-01-10 21:36:30 -07:00
|
|
|
// field.setRobotPose(getPose2d());
|
2025-01-09 12:57:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Status diagnosticStatus() {
|
|
|
|
|
Status status = new Status();
|
|
|
|
|
|
2025-02-28 14:21:32 -07:00
|
|
|
if(cameras[0].isConnected())
|
|
|
|
|
status.addReport(ReportLevel.INFO, "Left Camera Connected");
|
|
|
|
|
else
|
|
|
|
|
status.addReport(ReportLevel.ERROR, "Left Camera DISCONNECTED");
|
|
|
|
|
|
|
|
|
|
if(cameras[1].isConnected())
|
|
|
|
|
status.addReport(ReportLevel.INFO, "Right Camera Connected");
|
|
|
|
|
else
|
|
|
|
|
status.addReport(ReportLevel.ERROR, "Right Camera DISCONNECTED");
|
|
|
|
|
|
|
|
|
|
|
2025-01-09 12:57:04 -07:00
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|