blob: b4b1a87de418f6315cfd29c4774e944cc975eef5 [file] [log] [blame]
Milind Upadhyay7c205222022-11-16 18:20:58 -08001#include "frc971/vision/target_mapper.h"
2
3#include "frc971/control_loops/control_loop.h"
4#include "frc971/vision/ceres/angle_local_parameterization.h"
5#include "frc971/vision/ceres/normalize_angle.h"
6#include "frc971/vision/ceres/pose_graph_2d_error_term.h"
7#include "frc971/vision/geometry.h"
8
9DEFINE_uint64(max_num_iterations, 100,
10 "Maximum number of iterations for the ceres solver");
11
12namespace frc971::vision {
13
14Eigen::Affine3d PoseUtils::Pose2dToAffine3d(ceres::examples::Pose2d pose2d) {
15 Eigen::Affine3d H_world_pose =
16 Eigen::Translation3d(pose2d.x, pose2d.y, 0.0) *
17 Eigen::AngleAxisd(pose2d.yaw_radians, Eigen::Vector3d::UnitZ());
18 return H_world_pose;
19}
20
21ceres::examples::Pose2d PoseUtils::Affine3dToPose2d(Eigen::Affine3d H) {
22 Eigen::Vector3d T = H.translation();
23 double heading = std::atan2(H.rotation()(1, 0), H.rotation()(0, 0));
24 return ceres::examples::Pose2d{T[0], T[1],
25 ceres::examples::NormalizeAngle(heading)};
26}
27
28ceres::examples::Pose2d PoseUtils::ComputeRelativePose(
29 ceres::examples::Pose2d pose_1, ceres::examples::Pose2d pose_2) {
30 Eigen::Affine3d H_world_1 = Pose2dToAffine3d(pose_1);
31 Eigen::Affine3d H_world_2 = Pose2dToAffine3d(pose_2);
32
33 // Get the location of 2 in the 1 frame
34 Eigen::Affine3d H_1_2 = H_world_1.inverse() * H_world_2;
35 return Affine3dToPose2d(H_1_2);
36}
37
38ceres::examples::Pose2d PoseUtils::ComputeOffsetPose(
39 ceres::examples::Pose2d pose_1, ceres::examples::Pose2d pose_2_relative) {
40 auto H_world_1 = Pose2dToAffine3d(pose_1);
41 auto H_1_2 = Pose2dToAffine3d(pose_2_relative);
42 auto H_world_2 = H_world_1 * H_1_2;
43
44 return Affine3dToPose2d(H_world_2);
45}
46
47namespace {
48double ExponentiatedSinTerm(double theta) {
49 return (theta == 0.0 ? 1.0 : std::sin(theta) / theta);
50}
51
52double ExponentiatedCosTerm(double theta) {
53 return (theta == 0.0 ? 0.0 : (1 - std::cos(theta)) / theta);
54}
55} // namespace
56
57ceres::examples::Pose2d DataAdapter::InterpolatePose(
58 const TimestampedPose &pose_start, const TimestampedPose &pose_end,
59 aos::distributed_clock::time_point time) {
60 auto delta_pose =
61 PoseUtils::ComputeRelativePose(pose_start.pose, pose_end.pose);
62 // Time from start of period, on the scale 0-1 where 1 is the end.
63 double interpolation_scalar =
64 static_cast<double>((time - pose_start.time).count()) /
65 static_cast<double>((pose_end.time - pose_start.time).count());
66
67 double theta = delta_pose.yaw_radians;
68 // Take the log of the transformation matrix:
69 // https://mathoverflow.net/questions/118533/how-to-compute-se2-group-exponential-and-logarithm
70 StdFormLine dx_line = {.a = ExponentiatedSinTerm(theta),
71 .b = -ExponentiatedCosTerm(theta),
72 .c = delta_pose.x};
73 StdFormLine dy_line = {.a = ExponentiatedCosTerm(theta),
74 .b = ExponentiatedSinTerm(theta),
75 .c = delta_pose.y};
76
77 std::optional<cv::Point2d> solution = dx_line.Intersection(dy_line);
78 CHECK(solution.has_value());
79
80 // Re-exponentiate with the new values scaled by the interpolation scalar to
81 // get an interpolated tranformation matrix
82 double a = solution->x * interpolation_scalar;
83 double b = solution->y * interpolation_scalar;
84 double alpha = theta * interpolation_scalar;
85
86 ceres::examples::Pose2d interpolated_pose = {
87 .x = a * ExponentiatedSinTerm(theta) - b * ExponentiatedCosTerm(theta),
88 .y = a * ExponentiatedCosTerm(theta) + b * ExponentiatedSinTerm(theta),
89 .yaw_radians = alpha};
90
91 return PoseUtils::ComputeOffsetPose(pose_start.pose, interpolated_pose);
92} // namespace frc971::vision
93
94std::pair<std::vector<ceres::examples::Constraint2d>,
95 std::vector<ceres::examples::Pose2d>>
96DataAdapter::MatchTargetDetections(
97 const std::vector<TimestampedPose> &timestamped_robot_poses,
98 const std::vector<TimestampedDetection> &timestamped_target_detections) {
99 // Interpolate robot poses
100 std::map<aos::distributed_clock::time_point, ceres::examples::Pose2d>
101 interpolated_poses;
102
103 CHECK_GT(timestamped_robot_poses.size(), 1ul)
104 << "Need more than 1 robot pose";
105 auto robot_pose_it = timestamped_robot_poses.begin();
106 for (const auto &timestamped_detection : timestamped_target_detections) {
107 aos::distributed_clock::time_point target_time = timestamped_detection.time;
Milind Upadhyay81420a22022-12-14 21:13:06 -0800108
109 // Skip this target detection if we have no robot poses before it
110 if (robot_pose_it->time > target_time) {
111 continue;
112 }
113
Milind Upadhyay7c205222022-11-16 18:20:58 -0800114 // Find the robot point right before this localization
115 while (robot_pose_it->time > target_time ||
116 (robot_pose_it + 1)->time <= target_time) {
117 robot_pose_it++;
118 CHECK(robot_pose_it < timestamped_robot_poses.end() - 1)
119 << "Need a robot pose before and after every target detection";
120 }
121
122 auto start = robot_pose_it;
123 auto end = robot_pose_it + 1;
124 interpolated_poses.emplace(target_time,
125 InterpolatePose(*start, *end, target_time));
126 }
127
Milind Upadhyay81420a22022-12-14 21:13:06 -0800128 // In the case that all target detections were before the first robot
129 // detection, we would have no interpolated poses at this point
130 CHECK_GT(interpolated_poses.size(), 0ul)
131 << "Need a robot pose before and after every target detection";
132
Milind Upadhyay7c205222022-11-16 18:20:58 -0800133 // Match consecutive detections
134 std::vector<ceres::examples::Constraint2d> target_constraints;
135 std::vector<ceres::examples::Pose2d> robot_delta_poses;
136
137 auto last_detection = timestamped_target_detections[0];
138 auto last_robot_pose =
139 interpolated_poses[timestamped_target_detections[0].time];
140
141 for (auto it = timestamped_target_detections.begin() + 1;
142 it < timestamped_target_detections.end(); it++) {
143 // Skip two consecutive detections of the same target, because the solver
144 // doesn't allow this
145 if (it->id == last_detection.id) {
146 continue;
147 }
148
149 auto robot_pose = interpolated_poses[it->time];
150 auto robot_delta_pose =
151 PoseUtils::ComputeRelativePose(last_robot_pose, robot_pose);
152 auto confidence = ComputeConfidence(last_detection.time, it->time);
153
154 target_constraints.emplace_back(ComputeTargetConstraint(
155 last_detection, PoseUtils::Pose2dToAffine3d(robot_delta_pose), *it,
156 confidence));
157 robot_delta_poses.emplace_back(robot_delta_pose);
158
159 last_detection = *it;
160 last_robot_pose = robot_pose;
161 }
162
163 return {target_constraints, robot_delta_poses};
164}
165
166Eigen::Matrix3d DataAdapter::ComputeConfidence(
167 aos::distributed_clock::time_point start,
168 aos::distributed_clock::time_point end) {
169 constexpr size_t kX = 0;
170 constexpr size_t kY = 1;
171 constexpr size_t kTheta = 2;
172
173 // Uncertainty matrix between start and end
174 Eigen::Matrix3d P = Eigen::Matrix3d::Zero();
175
176 {
177 // Noise for odometry-based robot position measurements
178 Eigen::Matrix3d Q_odometry = Eigen::Matrix3d::Zero();
179 Q_odometry(kX, kX) = std::pow(0.045, 2);
180 Q_odometry(kY, kY) = std::pow(0.045, 2);
181 Q_odometry(kTheta, kTheta) = std::pow(0.01, 2);
182
183 // Add uncertainty for robot position measurements from start to end
184 int iterations = (end - start) / frc971::controls::kLoopFrequency;
185 P += static_cast<double>(iterations) * Q_odometry;
186 }
187
188 {
189 // Noise for vision-based target localizations
190 Eigen::Matrix3d Q_vision = Eigen::Matrix3d::Zero();
191 Q_vision(kX, kX) = std::pow(0.09, 2);
192 Q_vision(kY, kY) = std::pow(0.09, 2);
193 Q_vision(kTheta, kTheta) = std::pow(0.02, 2);
194
195 // Add uncertainty for the 2 vision measurements (1 at start and 1 at end)
196 P += 2.0 * Q_vision;
197 }
198
199 return P.inverse();
200}
201
202ceres::examples::Constraint2d DataAdapter::ComputeTargetConstraint(
203 const TimestampedDetection &target_detection_start,
204 const Eigen::Affine3d &H_robotstart_robotend,
205 const TimestampedDetection &target_detection_end,
206 const Eigen::Matrix3d &confidence) {
207 // Compute the relative pose (constraint) between the two targets
208 Eigen::Affine3d H_targetstart_targetend =
209 target_detection_start.H_robot_target.inverse() * H_robotstart_robotend *
210 target_detection_end.H_robot_target;
211 ceres::examples::Pose2d target_constraint =
212 PoseUtils::Affine3dToPose2d(H_targetstart_targetend);
213
214 return ceres::examples::Constraint2d{
215 target_detection_start.id, target_detection_end.id,
216 target_constraint.x, target_constraint.y,
217 target_constraint.yaw_radians, confidence};
218}
219
220TargetMapper::TargetMapper(
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800221 std::string_view target_poses_path,
222 std::vector<ceres::examples::Constraint2d> target_constraints)
223 : target_constraints_(target_constraints) {
224 aos::FlatbufferDetachedBuffer<TargetMap> target_map =
225 aos::JsonFileToFlatbuffer<TargetMap>(target_poses_path);
226 for (const auto *target_pose_fbs : *target_map.message().target_poses()) {
227 target_poses_[target_pose_fbs->id()] = ceres::examples::Pose2d{
228 target_pose_fbs->x(), target_pose_fbs->y(), target_pose_fbs->yaw()};
229 }
230}
231
232TargetMapper::TargetMapper(
Milind Upadhyay7c205222022-11-16 18:20:58 -0800233 std::map<TargetId, ceres::examples::Pose2d> target_poses,
234 std::vector<ceres::examples::Constraint2d> target_constraints)
235 : target_poses_(target_poses), target_constraints_(target_constraints) {}
236
237std::optional<TargetMapper::TargetPose> TargetMapper::GetTargetPoseById(
238 std::vector<TargetMapper::TargetPose> target_poses, TargetId target_id) {
239 for (auto target_pose : target_poses) {
240 if (target_pose.id == target_id) {
241 return target_pose;
242 }
243 }
244
245 return std::nullopt;
246}
247
248// Taken from ceres/examples/slam/pose_graph_2d/pose_graph_2d.cc
Milind Upadhyay7c205222022-11-16 18:20:58 -0800249void TargetMapper::BuildOptimizationProblem(
250 std::map<int, ceres::examples::Pose2d> *poses,
251 const std::vector<ceres::examples::Constraint2d> &constraints,
252 ceres::Problem *problem) {
253 CHECK_NOTNULL(poses);
254 CHECK_NOTNULL(problem);
255 if (constraints.empty()) {
256 LOG(WARNING) << "No constraints, no problem to optimize.";
257 return;
258 }
259
260 ceres::LossFunction *loss_function = new ceres::HuberLoss(2.0);
261 ceres::LocalParameterization *angle_local_parameterization =
262 ceres::examples::AngleLocalParameterization::Create();
263
264 for (std::vector<ceres::examples::Constraint2d>::const_iterator
265 constraints_iter = constraints.begin();
266 constraints_iter != constraints.end(); ++constraints_iter) {
267 const ceres::examples::Constraint2d &constraint = *constraints_iter;
268
269 std::map<int, ceres::examples::Pose2d>::iterator pose_begin_iter =
270 poses->find(constraint.id_begin);
271 CHECK(pose_begin_iter != poses->end())
272 << "Pose with ID: " << constraint.id_begin << " not found.";
273 std::map<int, ceres::examples::Pose2d>::iterator pose_end_iter =
274 poses->find(constraint.id_end);
275 CHECK(pose_end_iter != poses->end())
276 << "Pose with ID: " << constraint.id_end << " not found.";
277
278 const Eigen::Matrix3d sqrt_information =
279 constraint.information.llt().matrixL();
280 // Ceres will take ownership of the pointer.
281 ceres::CostFunction *cost_function =
282 ceres::examples::PoseGraph2dErrorTerm::Create(
283 constraint.x, constraint.y, constraint.yaw_radians,
284 sqrt_information);
285 problem->AddResidualBlock(
286 cost_function, loss_function, &pose_begin_iter->second.x,
287 &pose_begin_iter->second.y, &pose_begin_iter->second.yaw_radians,
288 &pose_end_iter->second.x, &pose_end_iter->second.y,
289 &pose_end_iter->second.yaw_radians);
290
291 problem->SetParameterization(&pose_begin_iter->second.yaw_radians,
292 angle_local_parameterization);
293 problem->SetParameterization(&pose_end_iter->second.yaw_radians,
294 angle_local_parameterization);
295 }
296
297 // The pose graph optimization problem has three DOFs that are not fully
298 // constrained. This is typically referred to as gauge freedom. You can apply
299 // a rigid body transformation to all the nodes and the optimization problem
300 // will still have the exact same cost. The Levenberg-Marquardt algorithm has
301 // internal damping which mitigates this issue, but it is better to properly
302 // constrain the gauge freedom. This can be done by setting one of the poses
303 // as constant so the optimizer cannot change it.
304 std::map<int, ceres::examples::Pose2d>::iterator pose_start_iter =
305 poses->begin();
306 CHECK(pose_start_iter != poses->end()) << "There are no poses.";
307 problem->SetParameterBlockConstant(&pose_start_iter->second.x);
308 problem->SetParameterBlockConstant(&pose_start_iter->second.y);
309 problem->SetParameterBlockConstant(&pose_start_iter->second.yaw_radians);
310}
311
312// Taken from ceres/examples/slam/pose_graph_2d/pose_graph_2d.cc
313bool TargetMapper::SolveOptimizationProblem(ceres::Problem *problem) {
314 CHECK_NOTNULL(problem);
315
316 ceres::Solver::Options options;
317 options.max_num_iterations = FLAGS_max_num_iterations;
318 options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
319
320 ceres::Solver::Summary summary;
321 ceres::Solve(options, problem, &summary);
322
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800323 LOG(INFO) << summary.FullReport() << '\n';
Milind Upadhyay7c205222022-11-16 18:20:58 -0800324
325 return summary.IsSolutionUsable();
326}
327
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800328void TargetMapper::Solve(std::string_view field_name,
329 std::optional<std::string_view> output_dir) {
Milind Upadhyay7c205222022-11-16 18:20:58 -0800330 ceres::Problem problem;
331 BuildOptimizationProblem(&target_poses_, target_constraints_, &problem);
332
333 CHECK(SolveOptimizationProblem(&problem))
334 << "The solve was not successful, exiting.";
335
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800336 // TODO(milind): add origin to first target offset to all poses
337
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800338 auto map_json = MapToJson(field_name);
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800339 VLOG(1) << "Solved target poses: " << map_json;
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800340
341 if (output_dir.has_value()) {
342 std::string output_path =
343 absl::StrCat(output_dir.value(), "/", field_name, ".json");
344 LOG(INFO) << "Writing map to file: " << output_path;
345 aos::util::WriteStringToFileOrDie(output_path, map_json);
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800346 }
347}
348
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800349std::string TargetMapper::MapToJson(std::string_view field_name) const {
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800350 flatbuffers::FlatBufferBuilder fbb;
351
352 // Convert poses to flatbuffers
353 std::vector<flatbuffers::Offset<TargetPoseFbs>> target_poses_fbs;
354 for (const auto &[id, pose] : target_poses_) {
355 TargetPoseFbs::Builder target_pose_builder(fbb);
356 target_pose_builder.add_id(id);
Milind Upadhyaycf757772022-12-14 20:57:36 -0800357 target_pose_builder.add_x(pose.x);
358 target_pose_builder.add_y(pose.y);
359 target_pose_builder.add_yaw(pose.yaw_radians);
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800360
361 target_poses_fbs.emplace_back(target_pose_builder.Finish());
362 }
363
Milind Upadhyay05652cb2022-12-07 20:51:51 -0800364 const auto field_name_offset = fbb.CreateString(field_name);
365 flatbuffers::Offset<TargetMap> target_map_offset = CreateTargetMap(
366 fbb, fbb.CreateVector(target_poses_fbs), field_name_offset);
Milind Upadhyaycd677a32022-12-04 13:06:43 -0800367
368 return aos::FlatbufferToJson(
369 flatbuffers::GetMutableTemporaryPointer(fbb, target_map_offset),
370 {.multi_line = true});
Milind Upadhyay7c205222022-11-16 18:20:58 -0800371}
372
373} // namespace frc971::vision