blob: 944081317f85a83bf91109937baa4f15cd7cc527 [file] [log] [blame]
Milind Upadhyay7c205222022-11-16 18:20:58 -08001#include "frc971/vision/target_mapper.h"
2
3#include <random>
4
5#include "aos/events/simulated_event_loop.h"
Milind Upadhyayc5beba12022-12-17 17:41:20 -08006#include "aos/testing/path.h"
Milind Upadhyay7c205222022-11-16 18:20:58 -08007#include "aos/testing/random_seed.h"
Milind Upadhyayc5beba12022-12-17 17:41:20 -08008#include "aos/util/math.h"
Milind Upadhyay7c205222022-11-16 18:20:58 -08009#include "glog/logging.h"
10#include "gtest/gtest.h"
11
12namespace frc971::vision {
13
14namespace {
15constexpr double kToleranceMeters = 0.05;
16constexpr double kToleranceRadians = 0.05;
Milind Upadhyayc5beba12022-12-17 17:41:20 -080017// Conversions between euler angles and quaternion result in slightly-off
18// doubles
19constexpr double kOrientationEqTolerance = 1e-10;
20constexpr std::chrono::milliseconds kMaxDt = std::chrono::milliseconds(3);
Milind Upadhyay05652cb2022-12-07 20:51:51 -080021constexpr std::string_view kFieldName = "test";
Milind Upadhyay7c205222022-11-16 18:20:58 -080022} // namespace
23
Milind Upadhyayc5beba12022-12-17 17:41:20 -080024// Angles normalized by aos::math::NormalizeAngle()
25#define EXPECT_NORMALIZED_ANGLES_NEAR(theta1, theta2, tolerance) \
26 { \
27 double delta = std::abs(aos::math::DiffAngle(theta1, theta2)); \
28 /* Have to check delta - 2pi for the case that one angle is very */ \
29 /* close to -pi, and the other is very close to +pi */ \
30 EXPECT_TRUE(delta < tolerance || std::abs(aos::math::DiffAngle( \
31 delta, 2.0 * M_PI)) < tolerance); \
Milind Upadhyay7c205222022-11-16 18:20:58 -080032 }
33
Milind Upadhyayc5beba12022-12-17 17:41:20 -080034#define EXPECT_POSE_NEAR(pose1, pose2) \
35 { \
36 for (size_t i = 0; i < 3; i++) { \
37 EXPECT_NEAR(pose1.p(i), pose2.p(i), kToleranceMeters); \
38 } \
39 auto rpy_1 = PoseUtils::QuaternionToEulerAngles(pose1.q); \
40 auto rpy_2 = PoseUtils::QuaternionToEulerAngles(pose2.q); \
41 for (size_t i = 0; i < 3; i++) { \
42 SCOPED_TRACE(absl::StrFormat("rpy_1(%d) = %f, rpy_2(%d) = %f", i, \
43 rpy_1(i), i, rpy_2(i))); \
44 EXPECT_NORMALIZED_ANGLES_NEAR(rpy_1(i), rpy_2(i), kToleranceRadians) \
45 } \
46 }
Milind Upadhyay7c205222022-11-16 18:20:58 -080047
Milind Upadhyayc5beba12022-12-17 17:41:20 -080048#define EXPECT_POSE_EQ(pose1, pose2) \
49 EXPECT_EQ(pose1.p, pose2.p); \
50 EXPECT_EQ(pose1.q, pose2.q);
Milind Upadhyay7c205222022-11-16 18:20:58 -080051
Milind Upadhyayc5beba12022-12-17 17:41:20 -080052#define EXPECT_QUATERNION_NEAR(q1, q2) \
53 EXPECT_NEAR(q1.x(), q2.x(), kOrientationEqTolerance) << q1 << " != " << q2; \
54 EXPECT_NEAR(q1.y(), q2.y(), kOrientationEqTolerance) << q1 << " != " << q2; \
55 EXPECT_NEAR(q1.z(), q2.z(), kOrientationEqTolerance) << q1 << " != " << q2; \
56 EXPECT_NEAR(q1.w(), q2.w(), kOrientationEqTolerance) << q1 << " != " << q2;
57
58// Expects same roll, pitch, and yaw values (not equivalent rotation)
59#define EXPECT_RPY_EQ(rpy_1, rpy_2) \
60 { \
61 for (size_t i = 0; i < 3; i++) { \
62 SCOPED_TRACE(absl::StrFormat("rpy_1(%d) = %f, rpy_2(%d) = %f", i, \
63 rpy_1(i), i, rpy_2(i))); \
64 EXPECT_NORMALIZED_ANGLES_NEAR(rpy_1(i), rpy_2(i), \
65 kOrientationEqTolerance) \
66 } \
67 }
68
69#define EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(roll, pitch, yaw) \
70 { \
71 auto rpy = Eigen::Vector3d(roll, pitch, yaw); \
72 auto converted_rpy = PoseUtils::QuaternionToEulerAngles( \
73 PoseUtils::EulerAnglesToQuaternion(rpy)); \
74 EXPECT_RPY_EQ(converted_rpy, rpy); \
75 }
Milind Upadhyay7c205222022-11-16 18:20:58 -080076
77// Both confidence matrixes should have the same dimensions and be square
78#define EXPECT_CONFIDENCE_GT(confidence1, confidence2) \
79 { \
80 ASSERT_EQ(confidence1.rows(), confidence2.rows()); \
81 ASSERT_EQ(confidence1.rows(), confidence1.cols()); \
82 ASSERT_EQ(confidence2.rows(), confidence2.cols()); \
83 for (size_t i = 0; i < confidence1.rows(); i++) { \
84 EXPECT_GT(confidence1(i, i), confidence2(i, i)); \
85 } \
86 }
87
88namespace {
Milind Upadhyayc5beba12022-12-17 17:41:20 -080089ceres::examples::Pose3d Make2dPose(double x, double y, double yaw_radians) {
90 return ceres::examples::Pose3d{Eigen::Vector3d(x, y, 0.0),
91 PoseUtils::EulerAnglesToQuaternion(
92 Eigen::Vector3d(0.0, 0.0, yaw_radians))};
Milind Upadhyay7c205222022-11-16 18:20:58 -080093}
94
Milind Upadhyayebf93ee2023-01-05 14:12:58 -080095// Assumes camera and robot origin are the same
96DataAdapter::TimestampedDetection MakeTimestampedDetection(
97 aos::distributed_clock::time_point time, Eigen::Affine3d H_robot_target,
98 TargetMapper::TargetId id) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -080099 auto target_pose = PoseUtils::Affine3dToPose3d(H_robot_target);
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800100 return DataAdapter::TimestampedDetection{
101 time, H_robot_target,
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800102 std::sqrt(std::pow(target_pose.p(0), 2) + std::pow(target_pose.p(1), 2)),
103 id};
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800104}
105
Milind Upadhyay7c205222022-11-16 18:20:58 -0800106bool TargetIsInView(TargetMapper::TargetPose target_detection) {
107 // And check if it is within the fov of the robot /
108 // camera, assuming camera is pointing in the
109 // positive x-direction of the robot
110 double angle_to_target =
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800111 atan2(target_detection.pose.p(1), target_detection.pose.p(0));
Milind Upadhyay7c205222022-11-16 18:20:58 -0800112
113 // Simulated camera field of view, in radians
114 constexpr double kCameraFov = M_PI_2;
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800115 if (std::abs(angle_to_target) <= kCameraFov / 2.0) {
116 VLOG(2) << "Found target in view, based on T = "
117 << target_detection.pose.p(0) << ", " << target_detection.pose.p(1)
118 << " with angle " << angle_to_target;
Milind Upadhyay7c205222022-11-16 18:20:58 -0800119 return true;
120 } else {
121 return false;
122 }
123}
124
125aos::distributed_clock::time_point TimeInMs(size_t ms) {
126 return aos::distributed_clock::time_point(std::chrono::milliseconds(ms));
127}
128
129} // namespace
130
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800131TEST(PoseUtilsTest, EulerAnglesAndQuaternionConversions) {
132 // Make sure that the conversions are consistent back and forth.
133 // These angles shouldn't get changed to a different, equivalent roll pitch
134 // yaw.
135 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, 0.0, M_PI);
136 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, 0.0, -M_PI);
137 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, 0.0, M_PI_2);
138 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, 0.0, -M_PI_2);
139 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, 0.0, 0.0);
140 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, M_PI_4, 0.0);
141 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, -M_PI_4, 0.0);
142 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(0.0, -M_PI_4, M_PI_4);
143 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(M_PI_4, -M_PI_4, M_PI_4);
144 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(-M_PI_2, -M_PI_4, M_PI_4);
145
146 // Now, do a sweep of roll, pitch, and yaws in the normalized
147 // range.
148 // - roll: (-pi/2, pi/2)
149 // - pitch: (-pi/2, pi/2)
150 // - yaw: [-pi, pi)
151 constexpr double kThetaMaxRoll = M_PI_2 - kToleranceRadians;
152 constexpr double kThetaMaxPitch = M_PI_2 - kToleranceRadians;
153 constexpr double kThetaMaxYaw = M_PI;
154 constexpr double kDeltaTheta = M_PI / 16;
155
156 for (double roll = -kThetaMaxRoll; roll < kThetaMaxRoll;
157 roll += kDeltaTheta) {
158 for (double pitch = -kThetaMaxPitch; pitch < kThetaMaxPitch;
159 pitch += kDeltaTheta) {
160 for (double yaw = -kThetaMaxYaw; yaw < kThetaMaxYaw; yaw += kDeltaTheta) {
161 SCOPED_TRACE(
162 absl::StrFormat("roll: %f, pitch: %f, yaw: %f", roll, pitch, yaw));
163 EXPECT_EULER_ANGLES_QUATERNION_BACK_AND_FORTH_EQ(roll, pitch, yaw);
164 }
165 }
166 }
167}
168
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800169TEST(DataAdapterTest, ComputeConfidence) {
170 // Check the confidence matrices. Don't check the actual values
171 // in case the constants change, just check that the confidence of contraints
172 // decreases as time period or distances from camera increase.
173 {
174 // Vary time period
175 constexpr double kDistanceStart = 0.5;
176 constexpr double kDistanceEnd = 2.0;
177
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800178 TargetMapper::ConfidenceMatrix last_confidence =
179 TargetMapper::ConfidenceMatrix::Zero();
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800180 for (size_t dt = 0; dt < 15; dt++) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800181 TargetMapper::ConfidenceMatrix confidence =
182 DataAdapter::ComputeConfidence(TimeInMs(0), TimeInMs(dt),
183 kDistanceStart, kDistanceEnd);
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800184
185 if (dt != 0) {
186 // Confidence only decreases every 5ms (control loop period)
187 if (dt % 5 == 0) {
188 EXPECT_CONFIDENCE_GT(last_confidence, confidence);
189 } else {
190 EXPECT_EQ(last_confidence, confidence);
191 }
192 }
193 last_confidence = confidence;
194 }
195 }
196
197 {
198 // Vary distance at start
199 constexpr int kDt = 3;
200 constexpr double kDistanceEnd = 1.5;
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800201 TargetMapper::ConfidenceMatrix last_confidence =
202 TargetMapper::ConfidenceMatrix::Zero();
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800203 for (double distance_start = 0.0; distance_start < 3.0;
204 distance_start += 0.5) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800205 TargetMapper::ConfidenceMatrix confidence =
206 DataAdapter::ComputeConfidence(TimeInMs(0), TimeInMs(kDt),
207 distance_start, kDistanceEnd);
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800208 if (distance_start != 0.0) {
209 EXPECT_CONFIDENCE_GT(last_confidence, confidence);
210 }
211 last_confidence = confidence;
212 }
213 }
214
215 {
216 // Vary distance at end
217 constexpr int kDt = 2;
218 constexpr double kDistanceStart = 2.5;
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800219 TargetMapper::ConfidenceMatrix last_confidence =
220 TargetMapper::ConfidenceMatrix::Zero();
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800221 for (double distance_end = 0.0; distance_end < 3.0; distance_end += 0.5) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800222 TargetMapper::ConfidenceMatrix confidence =
223 DataAdapter::ComputeConfidence(TimeInMs(0), TimeInMs(kDt),
224 kDistanceStart, distance_end);
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800225 if (distance_end != 0.0) {
226 EXPECT_CONFIDENCE_GT(last_confidence, confidence);
227 }
228 last_confidence = confidence;
229 }
230 }
231}
232
Milind Upadhyayec493912022-12-18 21:33:15 -0800233TEST(DataAdapterTest, MatchTargetDetections) {
Milind Upadhyayec493912022-12-18 21:33:15 -0800234 std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections =
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800235 {MakeTimestampedDetection(
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800236 TimeInMs(5), PoseUtils::Pose3dToAffine3d(Make2dPose(5.0, -5.0, 0.0)),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800237 2),
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800238 MakeTimestampedDetection(
239 TimeInMs(6),
240 PoseUtils::Pose3dToAffine3d(Make2dPose(5.0, -4.0, M_PI)), 0),
241 MakeTimestampedDetection(
242 TimeInMs(10),
243 PoseUtils::Pose3dToAffine3d(Make2dPose(3.0, -3.0, M_PI)), 1),
244 MakeTimestampedDetection(
245 TimeInMs(13),
246 PoseUtils::Pose3dToAffine3d(Make2dPose(4.0, -7.0, M_PI_2)), 2),
247 MakeTimestampedDetection(
248 TimeInMs(14),
249 PoseUtils::Pose3dToAffine3d(Make2dPose(4.0, -4.0, M_PI_2)), 2)};
Milind Upadhyayec493912022-12-18 21:33:15 -0800250
Milind Upadhyayec493912022-12-18 21:33:15 -0800251 auto target_constraints =
252 DataAdapter::MatchTargetDetections(timestamped_target_detections, kMaxDt);
253
254 // The constraint between the detection at 6ms and the one at 10 ms is skipped
255 // because dt > kMaxDt.
256 // Also, the constraint between the last two detections is skipped because
257 // they are the same target
258 EXPECT_EQ(target_constraints.size(),
259 timestamped_target_detections.size() - 3);
260
261 // Between 5ms and 6ms detections
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800262 EXPECT_DOUBLE_EQ(target_constraints[0].t_be.p(0), 0.0);
263 EXPECT_DOUBLE_EQ(target_constraints[0].t_be.p(1), 1.0);
264 EXPECT_QUATERNION_NEAR(
265 target_constraints[0].t_be.q,
266 PoseUtils::EulerAnglesToQuaternion(Eigen::Vector3d(0.0, 0.0, M_PI)));
Milind Upadhyayec493912022-12-18 21:33:15 -0800267 EXPECT_EQ(target_constraints[0].id_begin, 2);
268 EXPECT_EQ(target_constraints[0].id_end, 0);
269
270 // Between 10ms and 13ms detections
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800271 EXPECT_DOUBLE_EQ(target_constraints[1].t_be.p(0), -1.0);
272 EXPECT_DOUBLE_EQ(target_constraints[1].t_be.p(1), 4.0);
273 EXPECT_QUATERNION_NEAR(
274 target_constraints[1].t_be.q,
275 PoseUtils::EulerAnglesToQuaternion(Eigen::Vector3d(0.0, 0.0, -M_PI_2)));
Milind Upadhyayec493912022-12-18 21:33:15 -0800276 EXPECT_EQ(target_constraints[1].id_begin, 1);
277 EXPECT_EQ(target_constraints[1].id_end, 2);
278}
279
Milind Upadhyay7c205222022-11-16 18:20:58 -0800280TEST(TargetMapperTest, TwoTargetsOneConstraint) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800281 ceres::examples::MapOfPoses target_poses;
282 target_poses[0] = Make2dPose(5.0, 0.0, M_PI);
283 target_poses[1] = Make2dPose(-5.0, 0.0, 0.0);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800284
Milind Upadhyay7c205222022-11-16 18:20:58 -0800285 std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections =
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800286 {MakeTimestampedDetection(
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800287 TimeInMs(5), PoseUtils::Pose3dToAffine3d(Make2dPose(3.0, 0.0, M_PI)),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800288 0),
289 MakeTimestampedDetection(
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800290 TimeInMs(6), PoseUtils::Pose3dToAffine3d(Make2dPose(-7.0, 0.0, 0.0)),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800291 1)};
Milind Upadhyay7c205222022-11-16 18:20:58 -0800292 auto target_constraints =
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800293 DataAdapter::MatchTargetDetections(timestamped_target_detections, kMaxDt);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800294
295 frc971::vision::TargetMapper mapper(target_poses, target_constraints);
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800296 mapper.Solve(kFieldName);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800297
298 ASSERT_EQ(mapper.target_poses().size(), 2);
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800299 EXPECT_POSE_NEAR(mapper.target_poses()[0], Make2dPose(5.0, 0.0, M_PI));
300 EXPECT_POSE_NEAR(mapper.target_poses()[1], Make2dPose(-5.0, 0.0, 0.0));
Milind Upadhyay7c205222022-11-16 18:20:58 -0800301}
302
303TEST(TargetMapperTest, TwoTargetsTwoConstraints) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800304 ceres::examples::MapOfPoses target_poses;
305 target_poses[0] = Make2dPose(5.0, 0.0, M_PI);
306 target_poses[1] = Make2dPose(-5.0, 0.0, -M_PI_2);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800307
Milind Upadhyay7c205222022-11-16 18:20:58 -0800308 std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections =
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800309 {MakeTimestampedDetection(
310 TimeInMs(5),
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800311 PoseUtils::Pose3dToAffine3d(Make2dPose(6.0, 0.0, M_PI_2)), 0),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800312 MakeTimestampedDetection(
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800313 TimeInMs(7),
314 PoseUtils::Pose3dToAffine3d(Make2dPose(6.0, 10.0, -M_PI)), 1),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800315 MakeTimestampedDetection(
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800316 TimeInMs(12),
317 PoseUtils::Pose3dToAffine3d(Make2dPose(1.0, 0.0, M_PI)), 0),
318 MakeTimestampedDetection(
319 TimeInMs(13),
320 PoseUtils::Pose3dToAffine3d(Make2dPose(-9.0, 0.0, -M_PI_2)), 1)};
Milind Upadhyay7c205222022-11-16 18:20:58 -0800321 auto target_constraints =
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800322 DataAdapter::MatchTargetDetections(timestamped_target_detections, kMaxDt);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800323
324 frc971::vision::TargetMapper mapper(target_poses, target_constraints);
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800325 mapper.Solve(kFieldName);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800326
327 ASSERT_EQ(mapper.target_poses().size(), 2);
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800328 EXPECT_POSE_NEAR(mapper.target_poses()[0], Make2dPose(5.0, 0.0, M_PI));
329 EXPECT_POSE_NEAR(mapper.target_poses()[1], Make2dPose(-5.0, 0.0, -M_PI_2));
Milind Upadhyay7c205222022-11-16 18:20:58 -0800330}
331
332TEST(TargetMapperTest, TwoTargetsOneNoisyConstraint) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800333 ceres::examples::MapOfPoses target_poses;
334 target_poses[0] = Make2dPose(5.0, 0.0, M_PI);
335 target_poses[1] = Make2dPose(-5.0, 0.0, 0.0);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800336
Milind Upadhyay7c205222022-11-16 18:20:58 -0800337 std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections =
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800338 {MakeTimestampedDetection(
339 TimeInMs(5),
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800340 PoseUtils::Pose3dToAffine3d(Make2dPose(3.01, 0.001, M_PI - 0.001)),
Milind Upadhyayebf93ee2023-01-05 14:12:58 -0800341 0),
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800342 MakeTimestampedDetection(
343 TimeInMs(7),
344 PoseUtils::Pose3dToAffine3d(Make2dPose(-7.01, 0.0, 0.0)), 1)};
Milind Upadhyay7c205222022-11-16 18:20:58 -0800345
346 auto target_constraints =
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800347 DataAdapter::MatchTargetDetections(timestamped_target_detections, kMaxDt);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800348
349 frc971::vision::TargetMapper mapper(target_poses, target_constraints);
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800350 mapper.Solve(kFieldName);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800351
352 ASSERT_EQ(mapper.target_poses().size(), 2);
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800353 EXPECT_POSE_NEAR(mapper.target_poses()[0], Make2dPose(5.0, 0.0, M_PI));
354 EXPECT_POSE_NEAR(mapper.target_poses()[1], Make2dPose(-5.0, 0.0, 0.0));
Milind Upadhyay7c205222022-11-16 18:20:58 -0800355}
356
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800357class ChargedUpTargetMapperTest : public ::testing::Test {
358 public:
359 ChargedUpTargetMapperTest() : generator_(aos::testing::RandomSeed()) {}
360
361 // Generates noisy target detection if target in camera FOV
362 std::optional<DataAdapter::TimestampedDetection> MaybeGenerateNoisyDetection(
363 const ceres::examples::Pose3d &robot_pose,
364 const TargetMapper::TargetPose &target_pose, size_t t,
365 bool force_in_view = false) {
366 TargetMapper::TargetPose target_detection = {
367 .id = target_pose.id,
368 .pose = PoseUtils::ComputeRelativePose(robot_pose, target_pose.pose)};
369 if (force_in_view || TargetIsInView(target_detection)) {
370 // Define random generator with Gaussian
371 // distribution
372 constexpr double kMean = 0.0;
373 constexpr double kStdDev = 1.0;
374 // Can play with this to see how it impacts
375 // randomness
376 constexpr double kNoiseScalePosition = 0.01;
377 constexpr double kNoiseScaleOrientation = 0.0005;
378 std::normal_distribution<double> dist(kMean, kStdDev);
379
380 target_detection.pose.p(0) += dist(generator_) * kNoiseScalePosition;
381 target_detection.pose.p(1) += dist(generator_) * kNoiseScalePosition;
382 target_detection.pose.p(2) += dist(generator_) * kNoiseScalePosition;
383
384 target_detection.pose.q.w() += dist(generator_) * kNoiseScaleOrientation;
385 target_detection.pose.q.x() += dist(generator_) * kNoiseScaleOrientation;
386 target_detection.pose.q.y() += dist(generator_) * kNoiseScaleOrientation;
387 target_detection.pose.q.z() += dist(generator_) * kNoiseScaleOrientation;
388
389 auto time_point = TimeInMs(t);
390 return MakeTimestampedDetection(
391 time_point, PoseUtils::Pose3dToAffine3d(target_detection.pose),
392 target_detection.id);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800393 }
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800394
395 return std::nullopt;
Milind Upadhyay7c205222022-11-16 18:20:58 -0800396 }
397
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800398 private:
399 std::default_random_engine generator_;
400};
401
402// Drive in a circle around the 2023 field, and add a bit of randomness to 3d
403// pose detections
404TEST_F(ChargedUpTargetMapperTest, FieldCircleMotion) {
405 // Read target map
406 auto target_map_fbs = aos::JsonFileToFlatbuffer<TargetMap>(
407 aos::testing::ArtifactPath("frc971/vision/target_map.json"));
408
409 std::vector<TargetMapper::TargetPose> actual_target_poses;
410 ceres::examples::MapOfPoses target_poses;
411 for (auto *target_pose_fbs : *target_map_fbs.message().target_poses()) {
412 auto target_pose = TargetMapper::TargetPose{
413 static_cast<int>(target_pose_fbs->id()),
414 ceres::examples::Pose3d{
415 Eigen::Vector3d(target_pose_fbs->x(), target_pose_fbs->y(),
416 target_pose_fbs->z()),
417 PoseUtils::EulerAnglesToQuaternion(Eigen::Vector3d(
418 target_pose_fbs->roll(), target_pose_fbs->pitch(),
419 target_pose_fbs->yaw()))}};
420 actual_target_poses.emplace_back(target_pose);
421 target_poses[target_pose.id] = target_pose.pose;
422 }
423
424 double kFieldHalfLength = 16.54 / 2.0; // half length of the field
425 double kFieldHalfWidth = 8.02 / 2.0; // half width of the field
426
Milind Upadhyay7c205222022-11-16 18:20:58 -0800427 // Now, create a bunch of robot poses and target
428 // observations
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800429 constexpr size_t kDt = 5;
430 constexpr double kRobotZ = 1.0;
Milind Upadhyay7c205222022-11-16 18:20:58 -0800431
Milind Upadhyay7c205222022-11-16 18:20:58 -0800432 std::vector<DataAdapter::TimestampedDetection> timestamped_target_detections;
433
434 constexpr size_t kTotalSteps = 100;
435 for (size_t step_count = 0; step_count < kTotalSteps; step_count++) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800436 size_t t = kDt * step_count;
437
Milind Upadhyay7c205222022-11-16 18:20:58 -0800438 // Circle clockwise around the center of the field
439 double robot_theta = t;
Milind Upadhyay7c205222022-11-16 18:20:58 -0800440
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800441 constexpr double kRadiusScalar = 0.5;
442 double robot_x = (kRadiusScalar * kFieldHalfLength) * cos(robot_theta);
443 double robot_y = (kRadiusScalar * -kFieldHalfWidth) * sin(robot_theta);
444
445 auto robot_pose =
446 ceres::examples::Pose3d{.p = Eigen::Vector3d(robot_x, robot_y, kRobotZ),
447 .q = PoseUtils::EulerAnglesToQuaternion(
448 Eigen::Vector3d(robot_theta, 0.0, 0.0))};
449
Milind Upadhyay7c205222022-11-16 18:20:58 -0800450 for (TargetMapper::TargetPose target_pose : actual_target_poses) {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800451 auto optional_detection =
452 MaybeGenerateNoisyDetection(robot_pose, target_pose, t);
453 if (optional_detection.has_value()) {
454 timestamped_target_detections.emplace_back(*optional_detection);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800455 }
456 }
457 }
458
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800459 // The above circular motion only captures targets 1-4, so add another
460 // detection with the camera looking at targets 5-8, and 4 at the same time to
461 // have a connection to the rest of the targets
Milind Upadhyay7c205222022-11-16 18:20:58 -0800462 {
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800463 auto last_robot_pose =
464 ceres::examples::Pose3d{.p = Eigen::Vector3d(0.0, 0.0, kRobotZ),
465 .q = PoseUtils::EulerAnglesToQuaternion(
466 Eigen::Vector3d(0.0, 0.0, M_PI))};
467 for (size_t id = 4; id <= 8; id++) {
468 auto target_pose =
469 TargetMapper::GetTargetPoseById(actual_target_poses, id).value();
470 auto optional_detection = MaybeGenerateNoisyDetection(
471 last_robot_pose, target_pose, kTotalSteps * kDt, true);
472
473 ASSERT_TRUE(optional_detection.has_value())
474 << "Didn't detect target " << target_pose.id;
475 timestamped_target_detections.emplace_back(*optional_detection);
476 }
Milind Upadhyay7c205222022-11-16 18:20:58 -0800477 }
478
479 auto target_constraints =
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800480 DataAdapter::MatchTargetDetections(timestamped_target_detections, kMaxDt);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800481 frc971::vision::TargetMapper mapper(target_poses, target_constraints);
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800482 mapper.Solve(kFieldName);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800483
484 for (auto [target_pose_id, mapper_target_pose] : mapper.target_poses()) {
485 TargetMapper::TargetPose actual_target_pose =
486 TargetMapper::GetTargetPoseById(actual_target_poses, target_pose_id)
487 .value();
488 EXPECT_POSE_NEAR(mapper_target_pose, actual_target_pose.pose);
489 }
490
491 //
492 // See what happens when we don't start with the
493 // correct values
494 //
495 for (auto [target_id, target_pose] : target_poses) {
496 // Skip first pose, since that needs to be correct
497 // and is fixed in the solver
Milind Upadhyayc5beba12022-12-17 17:41:20 -0800498 if (target_id != 1) {
499 ceres::examples::Pose3d bad_pose{
500 Eigen::Vector3d::Zero(),
501 PoseUtils::EulerAnglesToQuaternion(Eigen::Vector3d::Zero())};
Milind Upadhyay7c205222022-11-16 18:20:58 -0800502 target_poses[target_id] = bad_pose;
503 }
504 }
505
506 frc971::vision::TargetMapper mapper_bad_poses(target_poses,
507 target_constraints);
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800508 mapper_bad_poses.Solve(kFieldName);
Milind Upadhyay7c205222022-11-16 18:20:58 -0800509
510 for (auto [target_pose_id, mapper_target_pose] :
511 mapper_bad_poses.target_poses()) {
512 TargetMapper::TargetPose actual_target_pose =
513 TargetMapper::GetTargetPoseById(actual_target_poses, target_pose_id)
514 .value();
515 EXPECT_POSE_NEAR(mapper_target_pose, actual_target_pose.pose);
516 }
517}
518
519} // namespace frc971::vision