|
16 | 16 | package com.team2813.lib2813.vision; |
17 | 17 |
|
18 | 18 | import static com.google.common.truth.Truth.assertThat; |
| 19 | +import static com.team2813.lib2813.testing.truth.Pose3dSubject.assertThat; |
| 20 | +import static edu.wpi.first.units.Units.Meters; |
19 | 21 |
|
| 22 | +import com.team2813.lib2813.testing.junit.jupiter.InitWPILib; |
20 | 23 | import com.team2813.lib2813.testing.junit.jupiter.ProvideUniqueNetworkTableInstance; |
21 | | -import edu.wpi.first.apriltag.AprilTag; |
22 | 24 | import edu.wpi.first.apriltag.AprilTagFieldLayout; |
| 25 | +import edu.wpi.first.math.geometry.Pose2d; |
23 | 26 | import edu.wpi.first.math.geometry.Pose3d; |
24 | | -import edu.wpi.first.math.geometry.Quaternion; |
| 27 | +import edu.wpi.first.math.geometry.Rotation2d; |
25 | 28 | import edu.wpi.first.math.geometry.Rotation3d; |
26 | 29 | import edu.wpi.first.math.geometry.Transform3d; |
27 | | -import edu.wpi.first.math.geometry.Translation3d; |
| 30 | +import edu.wpi.first.math.geometry.Translation2d; |
28 | 31 | import edu.wpi.first.networktables.NetworkTableInstance; |
| 32 | +import edu.wpi.first.units.measure.Distance; |
| 33 | +import java.util.ArrayList; |
29 | 34 | import java.util.List; |
| 35 | +import java.util.function.Consumer; |
| 36 | +import java.util.stream.Stream; |
30 | 37 | import org.junit.jupiter.params.ParameterizedTest; |
| 38 | +import org.junit.jupiter.params.provider.Arguments; |
31 | 39 | import org.junit.jupiter.params.provider.EnumSource; |
| 40 | +import org.junit.jupiter.params.provider.MethodSource; |
| 41 | +import org.photonvision.EstimatedRobotPose; |
32 | 42 | import org.photonvision.PhotonPoseEstimator.PoseStrategy; |
| 43 | +import org.photonvision.simulation.SimCameraProperties; |
| 44 | +import org.photonvision.simulation.VisionSystemSim; |
| 45 | +import org.photonvision.targeting.PhotonTrackedTarget; |
33 | 46 |
|
34 | 47 | /** Tests for {@link MultiPhotonPoseEstimator}. */ |
35 | 48 | @ProvideUniqueNetworkTableInstance |
| 49 | +@InitWPILib |
36 | 50 | class MultiPhotonPoseEstimatorTest { |
37 | | - private static final double FIELD_LENGTH = 17.548; |
38 | | - private static final double FIELD_WIDTH = 8.052; |
39 | | - private static final int REEFSCAPE_APRIL_TAG_ID = 7; |
40 | | - private static final Pose3d REEFSCAPE_APRIL_TAG_POSE = |
41 | | - new Pose3d( |
42 | | - new Translation3d(13.890498, 4.0259, 0.308102), |
43 | | - new Rotation3d(new Quaternion(1.0, 0.0, 0.0, 0.0))); |
| 51 | + // Place the camera in the center of the robot, ~17.1cm up, facing forward and up. |
44 | 52 | private static final Transform3d FRONT_CAMERA_TRANSFORM = |
45 | | - new Transform3d( |
46 | | - 0.1688157406, |
47 | | - 0.2939800826, |
48 | | - 0.1708140348, |
49 | | - new Rotation3d(0, -0.1745329252, -0.5235987756)); |
| 53 | + new Transform3d(0, 0, 0.1708140348, new Rotation3d(0, -0.1745329252, 0)); |
50 | 54 |
|
51 | | - private static final Camera FRONT_CAMERA = new Camera("front", FRONT_CAMERA_TRANSFORM); |
| 55 | + private static final Camera FRONT_CAMERA = |
| 56 | + new Camera("front", FRONT_CAMERA_TRANSFORM, SimCameraProperties::PERFECT_90DEG); |
52 | 57 |
|
53 | 58 | @ParameterizedTest |
54 | 59 | @EnumSource(value = PoseStrategy.class) |
55 | 60 | void getPrimaryStrategy(PoseStrategy poseStrategy, NetworkTableInstance ntInstance) { |
56 | 61 | try (var estimator = |
57 | | - MultiPhotonPoseEstimator.builder(ntInstance, createFieldLayout(), poseStrategy) |
| 62 | + MultiPhotonPoseEstimator.builder( |
| 63 | + ntInstance, ReefscapeAprilTag.createFieldLayout(), poseStrategy) |
58 | 64 | .addCamera(FRONT_CAMERA) |
59 | 65 | .build()) { |
60 | 66 | assertThat(estimator.getPrimaryStrategy()).isEqualTo(poseStrategy); |
61 | 67 | } |
62 | 68 | } |
63 | 69 |
|
64 | | - private static AprilTagFieldLayout createFieldLayout() { |
65 | | - List<AprilTag> aprilTags = |
66 | | - List.of(new AprilTag(REEFSCAPE_APRIL_TAG_ID, REEFSCAPE_APRIL_TAG_POSE)); |
67 | | - return new AprilTagFieldLayout(aprilTags, FIELD_LENGTH, FIELD_WIDTH); |
| 70 | + private record PoseTestData(Pose3d robotPose, ReefscapeAprilTag aprilTag) {} |
| 71 | + |
| 72 | + private static Stream<Arguments> posesInField() { |
| 73 | + return Stream.of( |
| 74 | + facingAprilTag(ReefscapeAprilTag.RED_REEF_CENTER, Meters.of(1)), |
| 75 | + facingAprilTag(ReefscapeAprilTag.BLUE_REEF_CENTER, Meters.of(1)), |
| 76 | + facingAprilTag(ReefscapeAprilTag.RED_PROCESSOR, Meters.of(0.5)), |
| 77 | + facingAprilTag(ReefscapeAprilTag.BLUE_PROCESSOR, Meters.of(0.5))); |
| 78 | + } |
| 79 | + |
| 80 | + @ParameterizedTest(name = "{0}") |
| 81 | + @MethodSource("posesInField") |
| 82 | + void processAllUnreadResults_estimatedPoseInField( |
| 83 | + String testName, PoseTestData testData, NetworkTableInstance ntInstance) { |
| 84 | + AprilTagFieldLayout fieldLayout = ReefscapeAprilTag.createFieldLayout(testData.aprilTag); |
| 85 | + VisionSystemSim visionSystemSim = new VisionSystemSim("test"); |
| 86 | + visionSystemSim.addAprilTags(fieldLayout); |
| 87 | + |
| 88 | + double z = testData.aprilTag.toAprilTag().pose.getZ(); |
| 89 | + Camera camera = |
| 90 | + new Camera( |
| 91 | + "front", |
| 92 | + new Transform3d(0, 0, z, FRONT_CAMERA_TRANSFORM.getRotation()), |
| 93 | + SimCameraProperties::PERFECT_90DEG); |
| 94 | + |
| 95 | + try (var estimator = |
| 96 | + MultiPhotonPoseEstimator.builder(ntInstance, fieldLayout, PoseStrategy.LOWEST_AMBIGUITY) |
| 97 | + .addCamera(camera) |
| 98 | + .build()) { |
| 99 | + estimator.addCamerasToSimulator( |
| 100 | + visionSystemSim, |
| 101 | + (c, simCamera) -> { |
| 102 | + simCamera.enableRawStream(false); |
| 103 | + simCamera.enableProcessedStream(false); |
| 104 | + }); |
| 105 | + visionSystemSim.update(testData.robotPose); |
| 106 | + |
| 107 | + var estimateCollector = new EstimateCollector(); |
| 108 | + var rejectedPoseCollector = new RejectedPoseCollector(); |
| 109 | + |
| 110 | + // Call the method under test |
| 111 | + estimator.processAllUnreadResults(estimateCollector, rejectedPoseCollector); |
| 112 | + |
| 113 | + assertThat(estimateCollector.estimates).hasSize(1); |
| 114 | + assertThat(rejectedPoseCollector.rejectedPoses).isEmpty(); |
| 115 | + assertThat(estimateCollector.estimates.get(0).estimatedPose) |
| 116 | + .isWithin(0.01) |
| 117 | + .of(testData.robotPose); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + private static Stream<Arguments> posesOutOfField() { |
| 122 | + return Stream.of( |
| 123 | + facingAprilTag(ReefscapeAprilTag.RED_REEF_CENTER, Meters.of(4)), |
| 124 | + facingAprilTag(ReefscapeAprilTag.BLUE_REEF_CENTER, Meters.of(4)), |
| 125 | + facingAprilTag(ReefscapeAprilTag.RED_PROCESSOR, Meters.of(8.1)), |
| 126 | + facingAprilTag(ReefscapeAprilTag.BLUE_PROCESSOR, Meters.of(8.1))); |
| 127 | + } |
| 128 | + |
| 129 | + @ParameterizedTest(name = "{0}") |
| 130 | + @MethodSource("posesOutOfField") |
| 131 | + void processAllUnreadResults_estimatedPoseOutsideField( |
| 132 | + String testName, PoseTestData testData, NetworkTableInstance ntInstance) { |
| 133 | + AprilTagFieldLayout fieldLayout = ReefscapeAprilTag.createFieldLayout(testData.aprilTag); |
| 134 | + VisionSystemSim visionSystemSim = new VisionSystemSim("test"); |
| 135 | + visionSystemSim.addAprilTags(fieldLayout); |
| 136 | + |
| 137 | + double z = testData.aprilTag.toAprilTag().pose.getZ(); |
| 138 | + Camera camera = |
| 139 | + new Camera( |
| 140 | + "front", |
| 141 | + new Transform3d(0, 0, z, FRONT_CAMERA_TRANSFORM.getRotation()), |
| 142 | + SimCameraProperties::PERFECT_90DEG); |
| 143 | + |
| 144 | + try (var estimator = |
| 145 | + MultiPhotonPoseEstimator.builder(ntInstance, fieldLayout, PoseStrategy.LOWEST_AMBIGUITY) |
| 146 | + .addCamera(camera) |
| 147 | + .build()) { |
| 148 | + estimator.addCamerasToSimulator( |
| 149 | + visionSystemSim, |
| 150 | + (c, simCamera) -> { |
| 151 | + simCamera.enableRawStream(false); |
| 152 | + simCamera.enableProcessedStream(false); |
| 153 | + }); |
| 154 | + visionSystemSim.update(testData.robotPose); |
| 155 | + |
| 156 | + var estimateCollector = new EstimateCollector(); |
| 157 | + var rejectedPoseCollector = new RejectedPoseCollector(); |
| 158 | + |
| 159 | + // Call the method under test |
| 160 | + estimator.processAllUnreadResults(estimateCollector, rejectedPoseCollector); |
| 161 | + |
| 162 | + assertThat(estimateCollector.estimates).isEmpty(); |
| 163 | + assertThat(rejectedPoseCollector.rejectedPoses).hasSize(1); |
| 164 | + assertThat(targetsUsed(rejectedPoseCollector.rejectedPoses.get(0))) |
| 165 | + .containsExactly(testData.aprilTag.id()); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + private static List<Integer> targetsUsed(EstimatedRobotPose estimatedPose) { |
| 170 | + return estimatedPose.targetsUsed.stream().map(PhotonTrackedTarget::getFiducialId).toList(); |
| 171 | + } |
| 172 | + |
| 173 | + /** Creates test data with a position that is the given distance away from the given AprilTag. */ |
| 174 | + private static Arguments facingAprilTag(ReefscapeAprilTag tag, Distance distanceFromTag) { |
| 175 | + Pose2d closestTagPose = tag.toAprilTag().pose.toPose2d(); |
| 176 | + Rotation2d tagRotation = closestTagPose.getRotation(); |
| 177 | + double distance = distanceFromTag.in(Meters); |
| 178 | + Translation2d translation = |
| 179 | + new Translation2d(distance * tagRotation.getCos(), distance * tagRotation.getSin()); |
| 180 | + |
| 181 | + Pose2d robotPose = |
| 182 | + new Pose2d( |
| 183 | + closestTagPose.getTranslation().plus(translation), |
| 184 | + tagRotation.rotateBy(Rotation2d.k180deg)); |
| 185 | + |
| 186 | + var testName = String.format("%.1fmFrom%s", distance, toCamelCase(tag.name())); |
| 187 | + return Arguments.of(testName, new PoseTestData(new Pose3d(robotPose), tag)); |
| 188 | + } |
| 189 | + |
| 190 | + private static String toCamelCase(String s) { |
| 191 | + StringBuilder camelCaseString = new StringBuilder(); |
| 192 | + for (String part : s.split("_")) { |
| 193 | + camelCaseString.append(part.substring(0, 1).toUpperCase()); |
| 194 | + camelCaseString.append(part.substring(1).toLowerCase()); |
| 195 | + } |
| 196 | + return camelCaseString.toString(); |
| 197 | + } |
| 198 | + |
| 199 | + private static class EstimateCollector implements PoseEstimateConsumer<Camera> { |
| 200 | + final List<EstimatedRobotPose> estimates = new ArrayList<>(); |
| 201 | + |
| 202 | + @Override |
| 203 | + public void addEstimatedRobotPose(EstimatedRobotPose estimatedPose, Camera camera) { |
| 204 | + assertThat(camera.name()).isEqualTo(FRONT_CAMERA.name()); |
| 205 | + estimates.add(estimatedPose); |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + private static class RejectedPoseCollector implements Consumer<EstimatedRobotPose> { |
| 210 | + final List<EstimatedRobotPose> rejectedPoses = new ArrayList<>(); |
| 211 | + |
| 212 | + @Override |
| 213 | + public void accept(EstimatedRobotPose estimatedRobotPose) { |
| 214 | + rejectedPoses.add(estimatedRobotPose); |
| 215 | + } |
68 | 216 | } |
69 | 217 | } |
0 commit comments