blob: 3e20a1e3a9e383a682f680facef1bfb90bc8134a [file] [log] [blame]
James Kuszmaul1057ce82019-02-09 17:58:24 -08001#ifndef Y2019_CONTROL_LOOPS_DRIVETRAIN_LOCALIZATER_H_
2#define Y2019_CONTROL_LOOPS_DRIVETRAIN_LOCALIZATER_H_
3
4#include <cmath>
5#include <memory>
6
7#include "frc971/control_loops/pose.h"
8#include "y2019/control_loops/drivetrain/camera.h"
9#include "frc971/control_loops/drivetrain/hybrid_ekf.h"
10
11namespace y2019 {
12namespace control_loops {
13
14template <int num_cameras, int num_targets, int num_obstacles,
15 int max_targets_per_frame, typename Scalar = double>
16class TypedLocalizer
17 : public ::frc971::control_loops::drivetrain::HybridEkf<Scalar> {
18 public:
19 typedef TypedCamera<num_targets, num_obstacles, Scalar> Camera;
20 typedef typename Camera::TargetView TargetView;
21 typedef typename Camera::Pose Pose;
22 typedef ::frc971::control_loops::drivetrain::HybridEkf<Scalar> HybridEkf;
23 typedef typename HybridEkf::State State;
24 typedef typename HybridEkf::StateSquare StateSquare;
25 typedef typename HybridEkf::Input Input;
26 typedef typename HybridEkf::Output Output;
27 using HybridEkf::kNInputs;
28 using HybridEkf::kNOutputs;
29 using HybridEkf::kNStates;
30
31 // robot_pose should be the object that is used by the cameras, such that when
32 // we update robot_pose, the cameras will change what they report the relative
33 // position of the targets as.
34 // Note that the parameters for the cameras should be set to allow slightly
35 // larger fields of view and slightly longer range than the true cameras so
36 // that we can identify potential matches for targets even when we have slight
37 // modelling errors.
38 TypedLocalizer(
39 const ::frc971::control_loops::drivetrain::DrivetrainConfig<Scalar>
40 &dt_config,
41 Pose *robot_pose)
42 : ::frc971::control_loops::drivetrain::HybridEkf<Scalar>(dt_config),
43 robot_pose_(robot_pose) {}
44
45 // Performs a kalman filter correction with a single camera frame, consisting
46 // of up to max_targets_per_frame targets and taken at time t.
47 // camera is the Camera used to take the images.
48 void UpdateTargets(
49 const Camera &camera,
50 const ::aos::SizedArray<TargetView, max_targets_per_frame> &targets,
51 ::aos::monotonic_clock::time_point t) {
52 if (targets.empty()) {
53 return;
54 }
55
56 if (t > HybridEkf::latest_t()) {
57 LOG(ERROR,
58 "target observations must be older than most recent encoder/gyro "
James Kuszmaul85ffeb82019-03-03 19:41:44 -080059 "update.\n");
James Kuszmaul1057ce82019-02-09 17:58:24 -080060 return;
61 }
62
63 Output z;
64 Eigen::Matrix<Scalar, kNOutputs, kNOutputs> R;
65 TargetViewToMatrices(targets[0], &z, &R);
66
67 // In order to perform the correction steps for the targets, we will
68 // separately perform a Correct step for each following target.
69 // This way, we can have the first correction figure out the mappings
70 // between targets in the image and targets on the field, and then re-use
71 // those mappings for all the remaining corrections.
72 // As such, we need to store the EKF functions that the remaining targets
73 // will need in arrays:
74 ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
75 max_targets_per_frame> h_functions;
76 ::aos::SizedArray<::std::function<Eigen::Matrix<Scalar, kNOutputs,
77 kNStates>(const State &)>,
78 max_targets_per_frame> dhdx_functions;
79 HybridEkf::Correct(
80 z, nullptr,
81 ::std::bind(&TypedLocalizer::MakeH, this, camera, targets, &h_functions,
82 &dhdx_functions, ::std::placeholders::_1,
83 ::std::placeholders::_2, ::std::placeholders::_3,
84 ::std::placeholders::_4),
85 {}, {}, R, t);
86 // Fetch cache:
87 for (size_t ii = 1; ii < targets.size(); ++ii) {
88 TargetViewToMatrices(targets[ii], &z, &R);
89 HybridEkf::Correct(z, nullptr, {}, h_functions[ii], dhdx_functions[ii], R,
90 t);
91 }
92 }
93
94 private:
95 // The threshold to use for completely rejecting potentially bad target
96 // matches.
97 // TODO(james): Tune
98 static constexpr Scalar kRejectionScore = 1.0;
99
100 // Computes the measurement (z) and noise covariance (R) matrices for a given
101 // TargetView.
102 void TargetViewToMatrices(const TargetView &view, Output *z,
103 Eigen::Matrix<Scalar, kNOutputs, kNOutputs> *R) {
104 *z << view.reading.heading, view.reading.distance, view.reading.skew;
105 // TODO(james): R should account as well for our confidence in the target
106 // matching. However, handling that properly requires thing a lot more about
107 // the probabilities.
108 R->setZero();
109 R->diagonal() << ::std::pow(view.noise.heading, 2),
110 ::std::pow(view.noise.distance, 2), ::std::pow(view.noise.skew, 2);
111 }
112
113 // This is the function that will be called once the Ekf has inserted the
114 // measurement into the right spot in the measurement queue and needs the
115 // output functions to actually perform the corrections.
116 // Specifically, this will take the estimate of the state at that time and
117 // figure out how the targets seen by the camera best map onto the actual
118 // targets on the field.
119 // It then fills in the h and dhdx functions that are called by the Ekf.
120 void MakeH(
121 const Camera &camera,
122 const ::aos::SizedArray<TargetView, max_targets_per_frame> &target_views,
123 ::aos::SizedArray<::std::function<Output(const State &, const Input &)>,
124 max_targets_per_frame> *h_functions,
125 ::aos::SizedArray<::std::function<Eigen::Matrix<Scalar, kNOutputs,
126 kNStates>(const State &)>,
127 max_targets_per_frame> *dhdx_functions,
128 const State &X_hat, const StateSquare &P,
129 ::std::function<Output(const State &, const Input &)> *h,
130 ::std::function<
131 Eigen::Matrix<Scalar, kNOutputs, kNStates>(const State &)> *dhdx) {
132 // Because we need to match camera targets ("views") to actual field
133 // targets, and because we want to take advantage of the correlations
134 // between the targets (i.e., if we see two targets in the image, they
135 // probably correspond to different on-field targets), the matching problem
136 // here is somewhat non-trivial. Some of the methods we use only work
137 // because we are dealing with very small N (e.g., handling the correlations
138 // between multiple views has combinatoric complexity, but since N = 3,
139 // it's not an issue).
140 //
141 // High-level steps:
142 // 1) Set the base robot pose for the cameras to the Pose implied by X_hat.
143 // 2) Fetch all the expected target views from the camera.
144 // 3) Determine the "magnitude" of the Kalman correction from each potential
145 // view/target pair.
146 // 4) Match based on the combination of targets with the smallest
147 // corrections.
148 // 5) Calculate h and dhdx for each pair of targets.
149 //
150 // For the "magnitude" of the correction, we do not directly use the
151 // standard Kalman correction formula. Instead, we calculate the correction
152 // we would get from each component of the measurement and take the L2 norm
153 // of those. This prevents situations where a target matches very poorly but
154 // produces an overall correction of near-zero.
155 // TODO(james): I do not know if this is strictly the correct method to
156 // minimize likely error, but should be reasonable.
157 //
158 // For the matching, we do the following (see MatchFrames):
159 // 1. Compute the best max_targets_per_frame matches for each view.
160 // 2. Exhaust every possible combination of view/target pairs and
161 // choose the best one.
162 // When we don't think the camera should be able to see as many targets as
163 // we actually got in the frame, then we do permit doubling/tripling/etc.
164 // up on potential targets once we've exhausted all the targets we think
165 // we can see.
166
167 // Set the current robot pose so that the cameras know where they are
168 // (all the cameras have robot_pose_ as their base):
169 *robot_pose_->mutable_pos() << X_hat(0, 0), X_hat(1, 0), 0.0;
170 robot_pose_->set_theta(X_hat(2, 0));
171
172 // Compute the things we *think* the camera should be seeing.
173 // Note: Because we will not try to match to any targets that are not
174 // returned by this function, we generally want the modelled camera to have
175 // a slightly larger field of view than the real camera, and be able to see
176 // slightly smaller targets.
177 const ::aos::SizedArray<TargetView, num_targets> camera_views =
178 camera.target_views();
179
180 // Each row contains the scores for each pair of target view and camera
181 // target view. Values in each row will not be populated past
182 // camera.target_views().size(); of the rows, only the first
183 // target_views.size() shall be populated.
184 // Higher scores imply a worse match. Zero implies a perfect match.
185 Eigen::Matrix<Scalar, max_targets_per_frame, num_targets> scores;
186 scores.setConstant(::std::numeric_limits<Scalar>::infinity());
187 // Each row contains the indices of the best matches per view, where
188 // index 0 is the best, 1 the second best, and 2 the third, etc.
189 // -1 indicates an unfilled field.
190 Eigen::Matrix<int, max_targets_per_frame, max_targets_per_frame>
191 best_matches;
192 best_matches.setConstant(-1);
193 // The H matrices for each potential matching. This has the same structure
194 // as the scores matrix.
195 ::std::array<::std::array<Eigen::Matrix<Scalar, kNOutputs, kNStates>,
196 max_targets_per_frame>,
197 num_targets> all_H_matrices;
198
199 // Iterate through and fill out the scores for each potential pairing:
200 for (size_t ii = 0; ii < target_views.size(); ++ii) {
201 const TargetView &target_view = target_views[ii];
202 Output z;
203 Eigen::Matrix<Scalar, kNOutputs, kNOutputs> R;
204 TargetViewToMatrices(target_view, &z, &R);
205
206 for (size_t jj = 0; jj < camera_views.size(); ++jj) {
207 // Compute the ckalman update for this step:
208 const TargetView &view = camera_views[jj];
209 const Eigen::Matrix<Scalar, kNOutputs, kNStates> H =
210 HMatrix(*view.target, target_view);
211 const Eigen::Matrix<Scalar, kNStates, kNOutputs> PH = P * H.transpose();
212 const Eigen::Matrix<Scalar, kNOutputs, kNOutputs> S = H * PH + R;
213 // Note: The inverse here should be very cheap so long as kNOutputs = 3.
214 const Eigen::Matrix<Scalar, kNStates, kNOutputs> K = PH * S.inverse();
215 const Output err = z - Output(view.reading.heading,
216 view.reading.distance, view.reading.skew);
217 // In order to compute the actual score, we want to consider each
218 // component of the error separately, as well as considering the impacts
219 // on the each of the states separately. As such, we calculate what
220 // the separate updates from each error component would be, and sum
221 // the impacts on the states.
222 Output scorer;
223 for (size_t kk = 0; kk < kNOutputs; ++kk) {
224 // TODO(james): squaredNorm or norm or L1-norm? Do we care about the
225 // square root? Do we prefer a quadratic or linear response?
226 scorer(kk, 0) = (K.col(kk) * err(kk, 0)).squaredNorm();
227 }
228 // Compute the overall score--note that we add in a term for the height,
229 // scaled by a manual fudge-factor. The height is not accounted for
230 // in the Kalman update because we are not trying to estimate the height
231 // of the robot directly.
232 Scalar score =
233 scorer.squaredNorm() +
234 ::std::pow((view.reading.height - target_view.reading.height) /
235 target_view.noise.height / 20.0,
236 2);
237 scores(ii, jj) = score;
238 all_H_matrices[ii][jj] = H;
239
240 // Update the best_matches matrix:
241 int insert_target = jj;
242 for (size_t kk = 0; kk < max_targets_per_frame; ++kk) {
243 int idx = best_matches(ii, kk);
244 // Note that -1 indicates an unfilled value.
245 if (idx == -1 || scores(ii, idx) > scores(ii, insert_target)) {
246 best_matches(ii, kk) = insert_target;
247 insert_target = idx;
248 if (idx == -1) {
249 break;
250 }
251 }
252 }
253 }
254 }
255
256 if (camera_views.size() == 0) {
257 // If we can't get a match, provide H = zero, which will make this
258 // correction step a nop.
259 *h = [](const State &, const Input &) { return Output::Zero(); };
260 *dhdx = [](const State &) {
261 return Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
262 };
263 for (size_t ii = 0; ii < target_views.size(); ++ii) {
264 h_functions->push_back(*h);
265 dhdx_functions->push_back(*dhdx);
266 }
267 } else {
268 // Go through and brute force the issue of what the best combination of
269 // target matches are. The worst case for this algorithm will be
270 // max_targets_per_frame!, which is awful for any N > ~4, but since
271 // max_targets_per_frame = 3, I'm not really worried.
272 ::std::array<int, max_targets_per_frame> best_frames =
273 MatchFrames(scores, best_matches, target_views.size());
274 for (size_t ii = 0; ii < target_views.size(); ++ii) {
275 int view_idx = best_frames[ii];
276 const Eigen::Matrix<Scalar, kNOutputs, kNStates> best_H =
277 all_H_matrices[ii][view_idx];
278 const TargetView best_view = camera_views[view_idx];
279 const TargetView target_view = target_views[ii];
280 const Scalar match_score = scores(ii, view_idx);
281 if (match_score > kRejectionScore) {
282 h_functions->push_back(
283 [](const State &, const Input &) { return Output::Zero(); });
284 dhdx_functions->push_back([](const State &) {
285 return Eigen::Matrix<Scalar, kNOutputs, kNStates>::Zero();
286 });
287 } else {
288 h_functions->push_back([this, &camera, best_view, target_view](
289 const State &X, const Input &) {
290 // This function actually handles determining what the Output should
291 // be at a given state, now that we have chosen the target that
292 // we want to match to.
293 *robot_pose_->mutable_pos() << X(0, 0), X(1, 0), 0.0;
294 robot_pose_->set_theta(X(2, 0));
295 const Pose relative_pose =
296 best_view.target->pose().Rebase(&camera.pose());
297 const Scalar heading = relative_pose.heading();
298 const Scalar distance = relative_pose.xy_norm();
299 const Scalar skew =
300 ::aos::math::NormalizeAngle(relative_pose.rel_theta());
301 return Output(heading, distance, skew);
302 });
303
304 // TODO(james): Experiment to better understand whether we want to
305 // recalculate H or not.
306 dhdx_functions->push_back(
307 [best_H](const Eigen::Matrix<Scalar, kNStates, 1> &) {
308 return best_H;
309 });
310 }
311 }
312 *h = h_functions->at(0);
313 *dhdx = dhdx_functions->at(0);
314 }
315 }
316
317 Eigen::Matrix<Scalar, kNOutputs, kNStates> HMatrix(
318 const Target &target, const TargetView &target_view) {
319 // To calculate dheading/d{x,y,theta}:
320 // heading = arctan2(target_pos - camera_pos) - camera_theta
321 Eigen::Matrix<Scalar, 3, 1> target_pos = target.pose().abs_pos();
322 Eigen::Matrix<Scalar, 3, 1> camera_pos = target_view.camera_pose.abs_pos();
323 Scalar diffx = target_pos.x() - camera_pos.x();
324 Scalar diffy = target_pos.y() - camera_pos.y();
325 Scalar norm2 = diffx * diffx + diffy * diffy;
326 Scalar dheadingdx = diffy / norm2;
327 Scalar dheadingdy = -diffx / norm2;
328 Scalar dheadingdtheta = -1.0;
329
330 // To calculate ddistance/d{x,y}:
331 // distance = sqrt(diffx^2 + diffy^2)
332 Scalar distance = ::std::sqrt(norm2);
333 Scalar ddistdx = -diffx / distance;
334 Scalar ddistdy = -diffy / distance;
335
336 // Skew = target.theta - camera.theta:
337 Scalar dskewdtheta = -1.0;
338 Eigen::Matrix<Scalar, kNOutputs, kNStates> H;
339 H.setZero();
340 H(0, 0) = dheadingdx;
341 H(0, 1) = dheadingdy;
342 H(0, 2) = dheadingdtheta;
343 H(1, 0) = ddistdx;
344 H(1, 1) = ddistdy;
345 H(2, 2) = dskewdtheta;
346 return H;
347 }
348
349 // A helper function for the fuller version of MatchFrames; this just
350 // removes some of the arguments that are only needed during the recursion.
351 // n_views is the number of targets actually seen in the camera image (i.e.,
352 // the number of rows in scores/best_matches that are actually populated).
353 ::std::array<int, max_targets_per_frame> MatchFrames(
354 const Eigen::Matrix<Scalar, max_targets_per_frame, num_targets> &scores,
355 const Eigen::Matrix<int, max_targets_per_frame, max_targets_per_frame> &
356 best_matches,
357 int n_views) {
358 ::std::array<int, max_targets_per_frame> best_set;
359 Scalar best_score;
360 // We start out without having "used" any views/targets:
361 ::aos::SizedArray<bool, max_targets_per_frame> used_views;
362 for (int ii = 0; ii < n_views; ++ii) {
363 used_views.push_back(false);
364 }
365 MatchFrames(scores, best_matches, used_views, {{false}}, &best_set,
366 &best_score);
367 return best_set;
368 }
369
370 // Recursively iterates over every plausible combination of targets/views
371 // that there is and determines the lowest-scoring combination.
372 // used_views and used_targets indicate which rows/columns of the
373 // scores/best_matches matrices should be ignored. When used_views is all
374 // true, that means that we are done recursing.
375 void MatchFrames(
376 const Eigen::Matrix<Scalar, max_targets_per_frame, num_targets> &scores,
377 const Eigen::Matrix<int, max_targets_per_frame, max_targets_per_frame> &
378 best_matches,
379 const ::aos::SizedArray<bool, max_targets_per_frame> &used_views,
380 const ::std::array<bool, num_targets> &used_targets,
381 ::std::array<int, max_targets_per_frame> *best_set, Scalar *best_score) {
382 *best_score = ::std::numeric_limits<Scalar>::infinity();
383 // Iterate by letting each target in the camera frame (that isn't in
384 // used_views) choose it's best match that isn't already taken. We then set
385 // the appropriate flags in used_views and used_targets and call MatchFrames
386 // to let all the other views sort themselves out.
387 for (size_t ii = 0; ii < used_views.size(); ++ii) {
388 if (used_views[ii]) {
389 continue;
390 }
391 int best_match = -1;
392 for (size_t jj = 0; jj < max_targets_per_frame; ++jj) {
393 if (best_matches(ii, jj) == -1) {
394 // If we run out of potential targets from the camera, then there
395 // are more targets in the frame than we think there should be.
396 // In this case, we are going to be doubling/tripling/etc. up
397 // anyhow. So we just give everyone their top choice:
398 // TODO(james): If we ever are dealing with larger numbers of
399 // targets per frame, do something to minimize doubling-up.
400 best_match = best_matches(ii, 0);
401 break;
402 }
403 best_match = best_matches(ii, jj);
404 if (!used_targets[best_match]) {
405 break;
406 }
407 }
408 // If we reach here and best_match = -1, that means that no potential
409 // targets were generated by the camera, and we should never have gotten
410 // here.
411 CHECK(best_match != -1);
412 ::aos::SizedArray<bool, max_targets_per_frame> sub_views = used_views;
413 sub_views[ii] = true;
414 ::std::array<bool, num_targets> sub_targets = used_targets;
415 sub_targets[best_match] = true;
416 ::std::array<int, max_targets_per_frame> sub_best_set;
417 Scalar score;
418 MatchFrames(scores, best_matches, sub_views, sub_targets, &sub_best_set,
419 &score);
420 score += scores(ii, best_match);
421 sub_best_set[ii] = best_match;
422 if (score < *best_score) {
423 *best_score = score;
424 *best_set = sub_best_set;
425 }
426 }
427 // best_score will be infinite if we did not find a result due to there
428 // being no targets that weren't set in used_vies; this is the
429 // base case of the recursion and so we set best_score to zero:
430 if (!::std::isfinite(*best_score)) {
431 *best_score = 0.0;
432 }
433 }
434
435 // The pose that is used by the cameras to determine the location of the robot
436 // and thus the expected view of the targets.
437 Pose *robot_pose_;
438}; // class TypedLocalizer
439
440} // namespace control_loops
441} // namespace y2019
442
443#endif // Y2019_CONTROL_LOOPS_DRIVETRAIN_LOCALIZATER_H_