blob: a71f14de13b34ef1c57e2775238c2e82cb6f2711 [file] [log] [blame]
Austin Schuhdcb6b362022-02-25 18:06:21 -08001#include "frc971/vision/extrinsics_calibration.h"
2
3#include "aos/time/time.h"
4#include "ceres/ceres.h"
5#include "frc971/analysis/in_process_plotter.h"
6#include "frc971/control_loops/runge_kutta.h"
7#include "frc971/vision/calibration_accumulator.h"
8#include "frc971/vision/charuco_lib.h"
9
10namespace frc971 {
11namespace vision {
12
13namespace chrono = std::chrono;
14using aos::distributed_clock;
15using aos::monotonic_clock;
16
17constexpr double kGravity = 9.8;
18
19// The basic ideas here are taken from Kalibr.
20// (https://github.com/ethz-asl/kalibr), but adapted to work with AOS, and to be
21// simpler.
22//
23// Camera readings and IMU readings come in at different times, on different
24// time scales. Our first problem is to align them in time so we can actually
25// compute an error. This is done in the calibration accumulator code. The
26// kalibr paper uses splines, while this uses kalman filters to solve the same
27// interpolation problem so we can get the expected vs actual pose at the time
28// each image arrives.
29//
30// The cost function is then fed the computed angular and positional error for
31// each camera sample before the kalman filter update. Intuitively, the smaller
32// the corrections to the kalman filter each step, the better the estimate
33// should be.
34//
35// We don't actually implement the angular kalman filter because the IMU is so
36// good. We give the solver an initial position and bias, and let it solve from
37// there. This lets us represent drift that is linear in time, which should be
38// good enough for ~1 minute calibration.
39//
40// TODO(austin): Kalman smoother ala
41// https://stanford.edu/~boyd/papers/pdf/auto_ks.pdf should allow for better
42// parallelism, and since we aren't causal, will take that into account a lot
43// better.
44
45// This class takes the initial parameters and biases, and computes the error
46// between the measured and expected camera readings. When optimized, this
47// gives us a cost function to minimize.
48template <typename Scalar>
49class CeresPoseFilter : public CalibrationDataObserver {
50 public:
51 typedef Eigen::Transform<Scalar, 3, Eigen::Affine> Affine3s;
52
53 CeresPoseFilter(Eigen::Quaternion<Scalar> initial_orientation,
Austin Schuh2895f4c2022-02-26 16:38:46 -080054 Eigen::Quaternion<Scalar> pivot_to_camera,
55 Eigen::Quaternion<Scalar> pivot_to_imu,
Austin Schuhdcb6b362022-02-25 18:06:21 -080056 Eigen::Matrix<Scalar, 3, 1> gyro_bias,
57 Eigen::Matrix<Scalar, 6, 1> initial_state,
58 Eigen::Quaternion<Scalar> board_to_world,
Austin Schuh2895f4c2022-02-26 16:38:46 -080059 Eigen::Matrix<Scalar, 3, 1> pivot_to_camera_translation,
60 Eigen::Matrix<Scalar, 3, 1> pivot_to_imu_translation,
Austin Schuhdcb6b362022-02-25 18:06:21 -080061 Scalar gravity_scalar,
62 Eigen::Matrix<Scalar, 3, 1> accelerometer_bias)
63 : accel_(Eigen::Matrix<double, 3, 1>::Zero()),
64 omega_(Eigen::Matrix<double, 3, 1>::Zero()),
65 imu_bias_(gyro_bias),
66 orientation_(initial_orientation),
67 x_hat_(initial_state),
68 p_(Eigen::Matrix<Scalar, 6, 6>::Zero()),
Austin Schuh2895f4c2022-02-26 16:38:46 -080069 pivot_to_camera_rotation_(pivot_to_camera),
70 pivot_to_camera_translation_(pivot_to_camera_translation),
71 pivot_to_imu_rotation_(pivot_to_imu),
72 pivot_to_imu_translation_(pivot_to_imu_translation),
Austin Schuhdcb6b362022-02-25 18:06:21 -080073 board_to_world_(board_to_world),
74 gravity_scalar_(gravity_scalar),
75 accelerometer_bias_(accelerometer_bias) {}
76
77 Scalar gravity_scalar() { return gravity_scalar_; }
78
79 virtual void ObserveCameraUpdate(
80 distributed_clock::time_point /*t*/,
81 Eigen::Vector3d /*board_to_camera_rotation*/,
82 Eigen::Quaternion<Scalar> /*imu_to_world_rotation*/,
83 Affine3s /*imu_to_world*/) {}
84
Austin Schuh2895f4c2022-02-26 16:38:46 -080085 void UpdateTurret(distributed_clock::time_point t,
86 Eigen::Vector2d state) override {
87 state_ = state;
88 state_time_ = t;
89 }
90
91 Eigen::Vector2d state_ = Eigen::Vector2d::Zero();
92 distributed_clock::time_point state_time_ = distributed_clock::min_time;
93
Austin Schuhdcb6b362022-02-25 18:06:21 -080094 // Observes a camera measurement by applying a kalman filter correction and
95 // accumulating up the error associated with the step.
96 void UpdateCamera(distributed_clock::time_point t,
97 std::pair<Eigen::Vector3d, Eigen::Vector3d> rt) override {
98 Integrate(t);
99
Austin Schuh2895f4c2022-02-26 16:38:46 -0800100 const double pivot_angle =
101 state_time_ == distributed_clock::min_time
102 ? 0.0
103 : state_(0) +
104 state_(1) * chrono::duration<double>(t - state_time_).count();
105
Austin Schuhdcb6b362022-02-25 18:06:21 -0800106 const Eigen::Quaternion<Scalar> board_to_camera_rotation(
107 frc971::controls::ToQuaternionFromRotationVector(rt.first)
108 .cast<Scalar>());
109 const Affine3s board_to_camera =
110 Eigen::Translation3d(rt.second).cast<Scalar>() *
111 board_to_camera_rotation;
112
Austin Schuh2895f4c2022-02-26 16:38:46 -0800113 const Affine3s pivot_to_camera =
114 pivot_to_camera_translation_ * pivot_to_camera_rotation_;
115 const Affine3s pivot_to_imu =
116 pivot_to_imu_translation_ * pivot_to_imu_rotation_;
Austin Schuhdcb6b362022-02-25 18:06:21 -0800117
118 // This converts us from (facing the board),
119 // x right, y up, z towards us -> x right, y away, z up.
120 // Confirmed to be right.
121
122 // Want world -> imu rotation.
123 // world <- board <- camera <- imu.
124 const Eigen::Quaternion<Scalar> imu_to_world_rotation =
125 board_to_world_ * board_to_camera_rotation.inverse() *
Austin Schuh2895f4c2022-02-26 16:38:46 -0800126 pivot_to_camera_rotation_ *
127 Eigen::AngleAxis<Scalar>(static_cast<Scalar>(-pivot_angle),
128 Eigen::Vector3d::UnitZ().cast<Scalar>()) *
129 pivot_to_imu_rotation_.inverse();
Austin Schuhdcb6b362022-02-25 18:06:21 -0800130
131 const Affine3s imu_to_world =
Austin Schuh2895f4c2022-02-26 16:38:46 -0800132 board_to_world_ * board_to_camera.inverse() * pivot_to_camera *
133 Eigen::AngleAxis<Scalar>(static_cast<Scalar>(-pivot_angle),
134 Eigen::Vector3d::UnitZ().cast<Scalar>()) *
135 pivot_to_imu.inverse();
Austin Schuhdcb6b362022-02-25 18:06:21 -0800136
137 const Eigen::Matrix<Scalar, 3, 1> z =
138 imu_to_world * Eigen::Matrix<Scalar, 3, 1>::Zero();
139
140 Eigen::Matrix<Scalar, 3, 6> H = Eigen::Matrix<Scalar, 3, 6>::Zero();
141 H(0, 0) = static_cast<Scalar>(1.0);
142 H(1, 1) = static_cast<Scalar>(1.0);
143 H(2, 2) = static_cast<Scalar>(1.0);
144 const Eigen::Matrix<Scalar, 3, 1> y = z - H * x_hat_;
145
146 const Eigen::Matrix<double, 3, 3> R =
147 (::Eigen::DiagonalMatrix<double, 3>().diagonal() << ::std::pow(0.01, 2),
148 ::std::pow(0.01, 2), ::std::pow(0.01, 2))
149 .finished()
150 .asDiagonal();
151
152 const Eigen::Matrix<Scalar, 3, 3> S =
153 H * p_ * H.transpose() + R.cast<Scalar>();
154 const Eigen::Matrix<Scalar, 6, 3> K = p_ * H.transpose() * S.inverse();
155
156 x_hat_ += K * y;
157 p_ = (Eigen::Matrix<Scalar, 6, 6>::Identity() - K * H) * p_;
158
159 const Eigen::Quaternion<Scalar> error(imu_to_world_rotation.inverse() *
160 orientation());
161
162 errors_.emplace_back(
163 Eigen::Matrix<Scalar, 3, 1>(error.x(), error.y(), error.z()));
164 position_errors_.emplace_back(y);
165
166 ObserveCameraUpdate(t, rt.first, imu_to_world_rotation, imu_to_world);
167 }
168
169 virtual void ObserveIMUUpdate(
170 distributed_clock::time_point /*t*/,
171 std::pair<Eigen::Vector3d, Eigen::Vector3d> /*wa*/) {}
172
173 void UpdateIMU(distributed_clock::time_point t,
174 std::pair<Eigen::Vector3d, Eigen::Vector3d> wa) override {
175 Integrate(t);
176 omega_ = wa.first;
177 accel_ = wa.second;
178
179 ObserveIMUUpdate(t, wa);
180 }
181
182 const Eigen::Quaternion<Scalar> &orientation() const { return orientation_; }
183
184 size_t num_errors() const { return errors_.size(); }
185 Scalar errorx(size_t i) const { return errors_[i].x(); }
186 Scalar errory(size_t i) const { return errors_[i].y(); }
187 Scalar errorz(size_t i) const { return errors_[i].z(); }
188
189 size_t num_perrors() const { return position_errors_.size(); }
190 Scalar errorpx(size_t i) const { return position_errors_[i].x(); }
191 Scalar errorpy(size_t i) const { return position_errors_[i].y(); }
192 Scalar errorpz(size_t i) const { return position_errors_[i].z(); }
193
194 private:
195 Eigen::Matrix<Scalar, 46, 1> Pack(Eigen::Quaternion<Scalar> q,
196 Eigen::Matrix<Scalar, 6, 1> x_hat,
197 Eigen::Matrix<Scalar, 6, 6> p) {
198 Eigen::Matrix<Scalar, 46, 1> result = Eigen::Matrix<Scalar, 46, 1>::Zero();
199 result.template block<4, 1>(0, 0) = q.coeffs();
200 result.template block<6, 1>(4, 0) = x_hat;
201 result.template block<36, 1>(10, 0) =
202 Eigen::Map<Eigen::Matrix<Scalar, 36, 1>>(p.data(), p.size());
203
204 return result;
205 }
206
207 std::tuple<Eigen::Quaternion<Scalar>, Eigen::Matrix<Scalar, 6, 1>,
208 Eigen::Matrix<Scalar, 6, 6>>
209 UnPack(Eigen::Matrix<Scalar, 46, 1> input) {
210 Eigen::Quaternion<Scalar> q(input.template block<4, 1>(0, 0));
211 Eigen::Matrix<Scalar, 6, 1> x_hat(input.template block<6, 1>(4, 0));
212 Eigen::Matrix<Scalar, 6, 6> p =
213 Eigen::Map<Eigen::Matrix<Scalar, 6, 6>>(input.data() + 10, 6, 6);
214 return std::make_tuple(q, x_hat, p);
215 }
216
217 Eigen::Matrix<Scalar, 46, 1> Derivative(
218 const Eigen::Matrix<Scalar, 46, 1> &input) {
219 auto [q, x_hat, p] = UnPack(input);
220
221 Eigen::Quaternion<Scalar> omega_q;
222 omega_q.w() = Scalar(0.0);
223 omega_q.vec() = 0.5 * (omega_.cast<Scalar>() - imu_bias_);
224 Eigen::Matrix<Scalar, 4, 1> q_dot = (q * omega_q).coeffs();
225
226 Eigen::Matrix<double, 6, 6> A = Eigen::Matrix<double, 6, 6>::Zero();
227 A(0, 3) = 1.0;
228 A(1, 4) = 1.0;
229 A(2, 5) = 1.0;
230
231 Eigen::Matrix<Scalar, 6, 1> x_hat_dot = A * x_hat;
232 x_hat_dot.template block<3, 1>(3, 0) =
233 orientation() * (accel_.cast<Scalar>() - accelerometer_bias_) -
234 Eigen::Vector3d(0, 0, kGravity).cast<Scalar>() * gravity_scalar_;
235
236 // Initialize the position noise to 0. If the solver is going to back-solve
237 // for the most likely starting position, let's just say that the noise is
238 // small.
239 constexpr double kPositionNoise = 0.0;
240 constexpr double kAccelerometerNoise = 2.3e-6 * 9.8;
241 constexpr double kIMUdt = 5.0e-4;
242 Eigen::Matrix<double, 6, 6> Q_dot(
243 (::Eigen::DiagonalMatrix<double, 6>().diagonal()
244 << ::std::pow(kPositionNoise, 2) / kIMUdt,
245 ::std::pow(kPositionNoise, 2) / kIMUdt,
246 ::std::pow(kPositionNoise, 2) / kIMUdt,
247 ::std::pow(kAccelerometerNoise, 2) / kIMUdt,
248 ::std::pow(kAccelerometerNoise, 2) / kIMUdt,
249 ::std::pow(kAccelerometerNoise, 2) / kIMUdt)
250 .finished()
251 .asDiagonal());
252 Eigen::Matrix<Scalar, 6, 6> p_dot = A.cast<Scalar>() * p +
253 p * A.transpose().cast<Scalar>() +
254 Q_dot.cast<Scalar>();
255
256 return Pack(Eigen::Quaternion<Scalar>(q_dot), x_hat_dot, p_dot);
257 }
258
259 virtual void ObserveIntegrated(distributed_clock::time_point /*t*/,
260 Eigen::Matrix<Scalar, 6, 1> /*x_hat*/,
261 Eigen::Quaternion<Scalar> /*orientation*/,
262 Eigen::Matrix<Scalar, 6, 6> /*p*/) {}
263
264 void Integrate(distributed_clock::time_point t) {
265 if (last_time_ != distributed_clock::min_time) {
266 Eigen::Matrix<Scalar, 46, 1> next = control_loops::RungeKutta(
267 [this](auto r) { return Derivative(r); },
268 Pack(orientation_, x_hat_, p_),
269 aos::time::DurationInSeconds(t - last_time_));
270
271 std::tie(orientation_, x_hat_, p_) = UnPack(next);
272
273 // Normalize q so it doesn't drift.
274 orientation_.normalize();
275 }
276
277 last_time_ = t;
278 ObserveIntegrated(t, x_hat_, orientation_, p_);
279 }
280
281 Eigen::Matrix<double, 3, 1> accel_;
282 Eigen::Matrix<double, 3, 1> omega_;
283 Eigen::Matrix<Scalar, 3, 1> imu_bias_;
284
285 // IMU -> world quaternion
286 Eigen::Quaternion<Scalar> orientation_;
287 Eigen::Matrix<Scalar, 6, 1> x_hat_;
288 Eigen::Matrix<Scalar, 6, 6> p_;
289 distributed_clock::time_point last_time_ = distributed_clock::min_time;
290
Austin Schuh2895f4c2022-02-26 16:38:46 -0800291 Eigen::Quaternion<Scalar> pivot_to_camera_rotation_;
292 Eigen::Translation<Scalar, 3> pivot_to_camera_translation_ =
293 Eigen::Translation3d(0, 0, 0).cast<Scalar>();
294
295 Eigen::Quaternion<Scalar> pivot_to_imu_rotation_;
296 Eigen::Translation<Scalar, 3> pivot_to_imu_translation_ =
Austin Schuhdcb6b362022-02-25 18:06:21 -0800297 Eigen::Translation3d(0, 0, 0).cast<Scalar>();
298
299 Eigen::Quaternion<Scalar> board_to_world_;
300 Scalar gravity_scalar_;
301 Eigen::Matrix<Scalar, 3, 1> accelerometer_bias_;
302 // States:
303 // xyz position
304 // xyz velocity
305 //
306 // Inputs
307 // xyz accel
308 //
309 // Measurement:
310 // xyz position from camera.
311 //
312 // Since the gyro is so good, we can just solve for the bias and initial
313 // position with the solver and see what it learns.
314
315 // Returns the angular errors for each camera sample.
316 std::vector<Eigen::Matrix<Scalar, 3, 1>> errors_;
317 std::vector<Eigen::Matrix<Scalar, 3, 1>> position_errors_;
318};
319
Austin Schuh2895f4c2022-02-26 16:38:46 -0800320// Drives the Z coordinate of the quaternion to 0.
321struct PenalizeQuaternionZ {
322 template <typename S>
323 bool operator()(const S *const pivot_to_imu_ptr, S *residual) const {
324 Eigen::Quaternion<S> pivot_to_imu(pivot_to_imu_ptr[3], pivot_to_imu_ptr[0],
325 pivot_to_imu_ptr[1], pivot_to_imu_ptr[2]);
326 residual[0] = pivot_to_imu.z();
327 return true;
328 }
329};
330
Austin Schuhdcb6b362022-02-25 18:06:21 -0800331// Subclass of the filter above which has plotting. This keeps debug code and
332// actual code separate.
333class PoseFilter : public CeresPoseFilter<double> {
334 public:
335 PoseFilter(Eigen::Quaternion<double> initial_orientation,
Austin Schuh2895f4c2022-02-26 16:38:46 -0800336 Eigen::Quaternion<double> pivot_to_camera,
337 Eigen::Quaternion<double> pivot_to_imu,
Austin Schuhdcb6b362022-02-25 18:06:21 -0800338 Eigen::Matrix<double, 3, 1> gyro_bias,
339 Eigen::Matrix<double, 6, 1> initial_state,
340 Eigen::Quaternion<double> board_to_world,
Austin Schuh2895f4c2022-02-26 16:38:46 -0800341 Eigen::Matrix<double, 3, 1> pivot_to_camera_translation,
342 Eigen::Matrix<double, 3, 1> pivot_to_imu_translation,
Austin Schuhdcb6b362022-02-25 18:06:21 -0800343 double gravity_scalar,
344 Eigen::Matrix<double, 3, 1> accelerometer_bias)
Austin Schuh2895f4c2022-02-26 16:38:46 -0800345 : CeresPoseFilter<double>(
346 initial_orientation, pivot_to_camera, pivot_to_imu, gyro_bias,
347 initial_state, board_to_world, pivot_to_camera_translation,
348 pivot_to_imu_translation, gravity_scalar, accelerometer_bias) {}
Austin Schuhdcb6b362022-02-25 18:06:21 -0800349
350 void Plot() {
351 std::vector<double> rx;
352 std::vector<double> ry;
353 std::vector<double> rz;
354 std::vector<double> x;
355 std::vector<double> y;
356 std::vector<double> z;
357 std::vector<double> vx;
358 std::vector<double> vy;
359 std::vector<double> vz;
360 for (const Eigen::Quaternion<double> &q : orientations_) {
361 Eigen::Matrix<double, 3, 1> rotation_vector =
362 frc971::controls::ToRotationVectorFromQuaternion(q);
363 rx.emplace_back(rotation_vector(0, 0));
364 ry.emplace_back(rotation_vector(1, 0));
365 rz.emplace_back(rotation_vector(2, 0));
366 }
367 for (const Eigen::Matrix<double, 6, 1> &x_hat : x_hats_) {
368 x.emplace_back(x_hat(0));
369 y.emplace_back(x_hat(1));
370 z.emplace_back(x_hat(2));
371 vx.emplace_back(x_hat(3));
372 vy.emplace_back(x_hat(4));
373 vz.emplace_back(x_hat(5));
374 }
375
376 frc971::analysis::Plotter plotter;
377 plotter.AddFigure("position");
378 plotter.AddLine(times_, rx, "x_hat(0)");
379 plotter.AddLine(times_, ry, "x_hat(1)");
380 plotter.AddLine(times_, rz, "x_hat(2)");
Austin Schuhcf848fc2022-02-25 21:34:18 -0800381 plotter.AddLine(camera_times_, camera_x_, "Camera x");
382 plotter.AddLine(camera_times_, camera_y_, "Camera y");
383 plotter.AddLine(camera_times_, camera_z_, "Camera z");
384 plotter.AddLine(camera_times_, camera_error_x_, "Camera error x");
385 plotter.AddLine(camera_times_, camera_error_y_, "Camera error y");
386 plotter.AddLine(camera_times_, camera_error_z_, "Camera error z");
Austin Schuhdcb6b362022-02-25 18:06:21 -0800387 plotter.Publish();
388
389 plotter.AddFigure("error");
390 plotter.AddLine(times_, rx, "x_hat(0)");
391 plotter.AddLine(times_, ry, "x_hat(1)");
392 plotter.AddLine(times_, rz, "x_hat(2)");
Austin Schuhcf848fc2022-02-25 21:34:18 -0800393 plotter.AddLine(camera_times_, camera_error_x_, "Camera error x");
394 plotter.AddLine(camera_times_, camera_error_y_, "Camera error y");
395 plotter.AddLine(camera_times_, camera_error_z_, "Camera error z");
Austin Schuhdcb6b362022-02-25 18:06:21 -0800396 plotter.Publish();
397
398 plotter.AddFigure("imu");
Austin Schuhcf848fc2022-02-25 21:34:18 -0800399 plotter.AddLine(camera_times_, world_gravity_x_, "world_gravity(0)");
400 plotter.AddLine(camera_times_, world_gravity_y_, "world_gravity(1)");
401 plotter.AddLine(camera_times_, world_gravity_z_, "world_gravity(2)");
402 plotter.AddLine(imu_times_, imu_x_, "imu x");
403 plotter.AddLine(imu_times_, imu_y_, "imu y");
404 plotter.AddLine(imu_times_, imu_z_, "imu z");
Austin Schuhdcb6b362022-02-25 18:06:21 -0800405 plotter.AddLine(times_, rx, "rotation x");
406 plotter.AddLine(times_, ry, "rotation y");
407 plotter.AddLine(times_, rz, "rotation z");
408 plotter.Publish();
409
410 plotter.AddFigure("raw");
Austin Schuhcf848fc2022-02-25 21:34:18 -0800411 plotter.AddLine(imu_times_, imu_x_, "imu x");
412 plotter.AddLine(imu_times_, imu_y_, "imu y");
413 plotter.AddLine(imu_times_, imu_z_, "imu z");
414 plotter.AddLine(imu_times_, imu_ratex_, "omega x");
415 plotter.AddLine(imu_times_, imu_ratey_, "omega y");
416 plotter.AddLine(imu_times_, imu_ratez_, "omega z");
417 plotter.AddLine(camera_times_, raw_camera_x_, "Camera x");
418 plotter.AddLine(camera_times_, raw_camera_y_, "Camera y");
419 plotter.AddLine(camera_times_, raw_camera_z_, "Camera z");
Austin Schuhdcb6b362022-02-25 18:06:21 -0800420 plotter.Publish();
421
422 plotter.AddFigure("xyz vel");
423 plotter.AddLine(times_, x, "x");
424 plotter.AddLine(times_, y, "y");
425 plotter.AddLine(times_, z, "z");
426 plotter.AddLine(times_, vx, "vx");
427 plotter.AddLine(times_, vy, "vy");
428 plotter.AddLine(times_, vz, "vz");
Austin Schuhcf848fc2022-02-25 21:34:18 -0800429 plotter.AddLine(camera_times_, camera_position_x_, "Camera x");
430 plotter.AddLine(camera_times_, camera_position_y_, "Camera y");
431 plotter.AddLine(camera_times_, camera_position_z_, "Camera z");
Austin Schuhdcb6b362022-02-25 18:06:21 -0800432 plotter.Publish();
433
434 plotter.Spin();
435 }
436
437 void ObserveIntegrated(distributed_clock::time_point t,
438 Eigen::Matrix<double, 6, 1> x_hat,
439 Eigen::Quaternion<double> orientation,
440 Eigen::Matrix<double, 6, 6> p) override {
441 VLOG(1) << t << " -> " << p;
442 VLOG(1) << t << " xhat -> " << x_hat.transpose();
443 times_.emplace_back(chrono::duration<double>(t.time_since_epoch()).count());
444 x_hats_.emplace_back(x_hat);
445 orientations_.emplace_back(orientation);
446 }
447
448 void ObserveIMUUpdate(
449 distributed_clock::time_point t,
450 std::pair<Eigen::Vector3d, Eigen::Vector3d> wa) override {
Austin Schuhcf848fc2022-02-25 21:34:18 -0800451 imu_times_.emplace_back(chrono::duration<double>(t.time_since_epoch()).count());
452 imu_ratex_.emplace_back(wa.first.x());
453 imu_ratey_.emplace_back(wa.first.y());
454 imu_ratez_.emplace_back(wa.first.z());
455 imu_x_.emplace_back(wa.second.x());
456 imu_y_.emplace_back(wa.second.y());
457 imu_z_.emplace_back(wa.second.z());
Austin Schuhdcb6b362022-02-25 18:06:21 -0800458
459 last_accel_ = wa.second;
460 }
461
462 void ObserveCameraUpdate(distributed_clock::time_point t,
463 Eigen::Vector3d board_to_camera_rotation,
464 Eigen::Quaternion<double> imu_to_world_rotation,
465 Eigen::Affine3d imu_to_world) override {
Austin Schuhcf848fc2022-02-25 21:34:18 -0800466 raw_camera_x_.emplace_back(board_to_camera_rotation(0, 0));
467 raw_camera_y_.emplace_back(board_to_camera_rotation(1, 0));
468 raw_camera_z_.emplace_back(board_to_camera_rotation(2, 0));
Austin Schuhdcb6b362022-02-25 18:06:21 -0800469
470 Eigen::Matrix<double, 3, 1> rotation_vector =
471 frc971::controls::ToRotationVectorFromQuaternion(imu_to_world_rotation);
Austin Schuhcf848fc2022-02-25 21:34:18 -0800472 camera_times_.emplace_back(
473 chrono::duration<double>(t.time_since_epoch()).count());
Austin Schuhdcb6b362022-02-25 18:06:21 -0800474
Austin Schuhcf848fc2022-02-25 21:34:18 -0800475 Eigen::Matrix<double, 3, 1> camera_error =
Austin Schuhdcb6b362022-02-25 18:06:21 -0800476 frc971::controls::ToRotationVectorFromQuaternion(
477 imu_to_world_rotation.inverse() * orientation());
478
Austin Schuhcf848fc2022-02-25 21:34:18 -0800479 camera_x_.emplace_back(rotation_vector(0, 0));
480 camera_y_.emplace_back(rotation_vector(1, 0));
481 camera_z_.emplace_back(rotation_vector(2, 0));
Austin Schuhdcb6b362022-02-25 18:06:21 -0800482
Austin Schuhcf848fc2022-02-25 21:34:18 -0800483 camera_error_x_.emplace_back(camera_error(0, 0));
484 camera_error_y_.emplace_back(camera_error(1, 0));
485 camera_error_z_.emplace_back(camera_error(2, 0));
Austin Schuhdcb6b362022-02-25 18:06:21 -0800486
487 const Eigen::Vector3d world_gravity =
488 imu_to_world_rotation * last_accel_ -
489 Eigen::Vector3d(0, 0, kGravity) * gravity_scalar();
490
491 const Eigen::Vector3d camera_position =
492 imu_to_world * Eigen::Vector3d::Zero();
493
Austin Schuhcf848fc2022-02-25 21:34:18 -0800494 world_gravity_x_.emplace_back(world_gravity.x());
495 world_gravity_y_.emplace_back(world_gravity.y());
496 world_gravity_z_.emplace_back(world_gravity.z());
Austin Schuhdcb6b362022-02-25 18:06:21 -0800497
Austin Schuhcf848fc2022-02-25 21:34:18 -0800498 camera_position_x_.emplace_back(camera_position.x());
499 camera_position_y_.emplace_back(camera_position.y());
500 camera_position_z_.emplace_back(camera_position.z());
Austin Schuhdcb6b362022-02-25 18:06:21 -0800501 }
502
Austin Schuhcf848fc2022-02-25 21:34:18 -0800503 std::vector<double> camera_times_;
504 std::vector<double> camera_x_;
505 std::vector<double> camera_y_;
506 std::vector<double> camera_z_;
507 std::vector<double> raw_camera_x_;
508 std::vector<double> raw_camera_y_;
509 std::vector<double> raw_camera_z_;
510 std::vector<double> camera_error_x_;
511 std::vector<double> camera_error_y_;
512 std::vector<double> camera_error_z_;
Austin Schuhdcb6b362022-02-25 18:06:21 -0800513
Austin Schuhcf848fc2022-02-25 21:34:18 -0800514 std::vector<double> world_gravity_x_;
515 std::vector<double> world_gravity_y_;
516 std::vector<double> world_gravity_z_;
517 std::vector<double> imu_x_;
518 std::vector<double> imu_y_;
519 std::vector<double> imu_z_;
520 std::vector<double> camera_position_x_;
521 std::vector<double> camera_position_y_;
522 std::vector<double> camera_position_z_;
Austin Schuhdcb6b362022-02-25 18:06:21 -0800523
Austin Schuhcf848fc2022-02-25 21:34:18 -0800524 std::vector<double> imu_times_;
525 std::vector<double> imu_ratex_;
526 std::vector<double> imu_ratey_;
527 std::vector<double> imu_ratez_;
Austin Schuhdcb6b362022-02-25 18:06:21 -0800528
529 std::vector<double> times_;
530 std::vector<Eigen::Matrix<double, 6, 1>> x_hats_;
531 std::vector<Eigen::Quaternion<double>> orientations_;
532
533 Eigen::Matrix<double, 3, 1> last_accel_ = Eigen::Matrix<double, 3, 1>::Zero();
534};
535
536// Adapter class from the KF above to a Ceres cost function.
537struct CostFunctor {
538 CostFunctor(const CalibrationData *d) : data(d) {}
539
540 const CalibrationData *data;
541
542 template <typename S>
Austin Schuh2895f4c2022-02-26 16:38:46 -0800543 bool operator()(const S *const initial_orientation_ptr,
544 const S *const pivot_to_camera_ptr,
545 const S *const pivot_to_imu_ptr, const S *const gyro_bias_ptr,
546 const S *const initial_state_ptr,
547 const S *const board_to_world_ptr,
548 const S *const pivot_to_camera_translation_ptr,
549 const S *const pivot_to_imu_translation_ptr,
Austin Schuhdcb6b362022-02-25 18:06:21 -0800550 const S *const gravity_scalar_ptr,
551 const S *const accelerometer_bias_ptr, S *residual) const {
Austin Schuh2895f4c2022-02-26 16:38:46 -0800552 Eigen::Quaternion<S> initial_orientation(
553 initial_orientation_ptr[3], initial_orientation_ptr[0],
554 initial_orientation_ptr[1], initial_orientation_ptr[2]);
555 Eigen::Quaternion<S> pivot_to_camera(
556 pivot_to_camera_ptr[3], pivot_to_camera_ptr[0], pivot_to_camera_ptr[1],
557 pivot_to_camera_ptr[2]);
558 Eigen::Quaternion<S> pivot_to_imu(pivot_to_imu_ptr[3], pivot_to_imu_ptr[0],
559 pivot_to_imu_ptr[1], pivot_to_imu_ptr[2]);
560 Eigen::Quaternion<S> board_to_world(
561 board_to_world_ptr[3], board_to_world_ptr[0], board_to_world_ptr[1],
562 board_to_world_ptr[2]);
563 Eigen::Matrix<S, 3, 1> gyro_bias(gyro_bias_ptr[0], gyro_bias_ptr[1],
564 gyro_bias_ptr[2]);
Austin Schuhdcb6b362022-02-25 18:06:21 -0800565 Eigen::Matrix<S, 6, 1> initial_state;
Austin Schuh2895f4c2022-02-26 16:38:46 -0800566 initial_state(0) = initial_state_ptr[0];
567 initial_state(1) = initial_state_ptr[1];
568 initial_state(2) = initial_state_ptr[2];
569 initial_state(3) = initial_state_ptr[3];
570 initial_state(4) = initial_state_ptr[4];
571 initial_state(5) = initial_state_ptr[5];
572 Eigen::Matrix<S, 3, 1> pivot_to_camera_translation(
573 pivot_to_camera_translation_ptr[0], pivot_to_camera_translation_ptr[1],
574 pivot_to_camera_translation_ptr[2]);
575 Eigen::Matrix<S, 3, 1> pivot_to_imu_translation(
576 pivot_to_imu_translation_ptr[0], pivot_to_imu_translation_ptr[1],
577 pivot_to_imu_translation_ptr[2]);
Austin Schuhdcb6b362022-02-25 18:06:21 -0800578 Eigen::Matrix<S, 3, 1> accelerometer_bias(accelerometer_bias_ptr[0],
579 accelerometer_bias_ptr[1],
580 accelerometer_bias_ptr[2]);
581
Austin Schuh2895f4c2022-02-26 16:38:46 -0800582 CeresPoseFilter<S> filter(
583 initial_orientation, pivot_to_camera, pivot_to_imu, gyro_bias,
584 initial_state, board_to_world, pivot_to_camera_translation,
585 pivot_to_imu_translation, *gravity_scalar_ptr, accelerometer_bias);
Austin Schuhdcb6b362022-02-25 18:06:21 -0800586 data->ReviewData(&filter);
587
588 for (size_t i = 0; i < filter.num_errors(); ++i) {
589 residual[3 * i + 0] = filter.errorx(i);
590 residual[3 * i + 1] = filter.errory(i);
591 residual[3 * i + 2] = filter.errorz(i);
592 }
593
594 for (size_t i = 0; i < filter.num_perrors(); ++i) {
595 residual[3 * filter.num_errors() + 3 * i + 0] = filter.errorpx(i);
596 residual[3 * filter.num_errors() + 3 * i + 1] = filter.errorpy(i);
597 residual[3 * filter.num_errors() + 3 * i + 2] = filter.errorpz(i);
598 }
599
600 return true;
601 }
602};
603
604void Solve(const CalibrationData &data,
605 CalibrationParameters *calibration_parameters) {
606 ceres::Problem problem;
607
608 ceres::EigenQuaternionParameterization *quaternion_local_parameterization =
609 new ceres::EigenQuaternionParameterization();
610 // Set up the only cost function (also known as residual). This uses
611 // auto-differentiation to obtain the derivative (jacobian).
612
Austin Schuh2895f4c2022-02-26 16:38:46 -0800613 {
614 ceres::CostFunction *cost_function =
615 new ceres::AutoDiffCostFunction<CostFunctor, ceres::DYNAMIC, 4, 4, 4, 3,
616 6, 4, 3, 3, 1, 3>(
617 new CostFunctor(&data), data.camera_samples_size() * 6);
618 problem.AddResidualBlock(
619 cost_function, new ceres::HuberLoss(1.0),
620 calibration_parameters->initial_orientation.coeffs().data(),
621 calibration_parameters->pivot_to_camera.coeffs().data(),
622 calibration_parameters->pivot_to_imu.coeffs().data(),
623 calibration_parameters->gyro_bias.data(),
624 calibration_parameters->initial_state.data(),
625 calibration_parameters->board_to_world.coeffs().data(),
626 calibration_parameters->pivot_to_camera_translation.data(),
627 calibration_parameters->pivot_to_imu_translation.data(),
628 &calibration_parameters->gravity_scalar,
629 calibration_parameters->accelerometer_bias.data());
630 }
631
632 {
633 ceres::CostFunction *turret_z_cost_function =
634 new ceres::AutoDiffCostFunction<PenalizeQuaternionZ, 1, 4>(
635 new PenalizeQuaternionZ());
636 problem.AddResidualBlock(
637 turret_z_cost_function, nullptr,
638 calibration_parameters->pivot_to_imu.coeffs().data());
639 }
640
641 if (calibration_parameters->has_pivot) {
642 // Constrain Z.
643 problem.SetParameterization(
644 calibration_parameters->pivot_to_imu_translation.data(),
645 new ceres::SubsetParameterization(3, {2}));
646 } else {
647 problem.SetParameterBlockConstant(
648 calibration_parameters->pivot_to_imu.coeffs().data());
649 problem.SetParameterBlockConstant(
650 calibration_parameters->pivot_to_imu_translation.data());
651 }
652
Austin Schuhdcb6b362022-02-25 18:06:21 -0800653 problem.SetParameterization(
654 calibration_parameters->initial_orientation.coeffs().data(),
655 quaternion_local_parameterization);
656 problem.SetParameterization(
Austin Schuh2895f4c2022-02-26 16:38:46 -0800657 calibration_parameters->pivot_to_camera.coeffs().data(),
Austin Schuhdcb6b362022-02-25 18:06:21 -0800658 quaternion_local_parameterization);
659 problem.SetParameterization(
660 calibration_parameters->board_to_world.coeffs().data(),
661 quaternion_local_parameterization);
662 for (int i = 0; i < 3; ++i) {
663 problem.SetParameterLowerBound(calibration_parameters->gyro_bias.data(), i,
664 -0.05);
665 problem.SetParameterUpperBound(calibration_parameters->gyro_bias.data(), i,
666 0.05);
667 problem.SetParameterLowerBound(
668 calibration_parameters->accelerometer_bias.data(), i, -0.05);
669 problem.SetParameterUpperBound(
670 calibration_parameters->accelerometer_bias.data(), i, 0.05);
671 }
672 problem.SetParameterLowerBound(&calibration_parameters->gravity_scalar, 0,
673 0.95);
674 problem.SetParameterUpperBound(&calibration_parameters->gravity_scalar, 0,
675 1.05);
676
677 // Run the solver!
678 ceres::Solver::Options options;
679 options.minimizer_progress_to_stdout = true;
680 options.gradient_tolerance = 1e-12;
681 options.function_tolerance = 1e-16;
682 options.parameter_tolerance = 1e-12;
683 ceres::Solver::Summary summary;
684 Solve(options, &problem, &summary);
685 LOG(INFO) << summary.FullReport();
686
687 LOG(INFO) << "initial_orientation "
688 << calibration_parameters->initial_orientation.coeffs().transpose();
Austin Schuh2895f4c2022-02-26 16:38:46 -0800689 LOG(INFO) << "pivot_to_imu "
690 << calibration_parameters->pivot_to_imu.coeffs().transpose();
691 LOG(INFO) << "pivot_to_imu(rotation) "
Austin Schuhdcb6b362022-02-25 18:06:21 -0800692 << frc971::controls::ToRotationVectorFromQuaternion(
Austin Schuh2895f4c2022-02-26 16:38:46 -0800693 calibration_parameters->pivot_to_imu)
694 .transpose();
695 LOG(INFO) << "pivot_to_camera "
696 << calibration_parameters->pivot_to_camera.coeffs().transpose();
697 LOG(INFO) << "pivot_to_camera(rotation) "
698 << frc971::controls::ToRotationVectorFromQuaternion(
699 calibration_parameters->pivot_to_camera)
Austin Schuhdcb6b362022-02-25 18:06:21 -0800700 .transpose();
701 LOG(INFO) << "gyro_bias " << calibration_parameters->gyro_bias.transpose();
702 LOG(INFO) << "board_to_world "
703 << calibration_parameters->board_to_world.coeffs().transpose();
704 LOG(INFO) << "board_to_world(rotation) "
705 << frc971::controls::ToRotationVectorFromQuaternion(
706 calibration_parameters->board_to_world)
707 .transpose();
Austin Schuh2895f4c2022-02-26 16:38:46 -0800708 LOG(INFO) << "pivot_to_imu_translation "
709 << calibration_parameters->pivot_to_imu_translation.transpose();
710 LOG(INFO) << "pivot_to_camera_translation "
711 << calibration_parameters->pivot_to_camera_translation.transpose();
Austin Schuhdcb6b362022-02-25 18:06:21 -0800712 LOG(INFO) << "gravity " << kGravity * calibration_parameters->gravity_scalar;
713 LOG(INFO) << "accelerometer bias "
714 << calibration_parameters->accelerometer_bias.transpose();
715}
716
717void Plot(const CalibrationData &data,
718 const CalibrationParameters &calibration_parameters) {
719 PoseFilter filter(calibration_parameters.initial_orientation,
Austin Schuh2895f4c2022-02-26 16:38:46 -0800720 calibration_parameters.pivot_to_camera,
721 calibration_parameters.pivot_to_imu,
Austin Schuhdcb6b362022-02-25 18:06:21 -0800722 calibration_parameters.gyro_bias,
723 calibration_parameters.initial_state,
724 calibration_parameters.board_to_world,
Austin Schuh2895f4c2022-02-26 16:38:46 -0800725 calibration_parameters.pivot_to_camera_translation,
726 calibration_parameters.pivot_to_imu_translation,
Austin Schuhdcb6b362022-02-25 18:06:21 -0800727 calibration_parameters.gravity_scalar,
728 calibration_parameters.accelerometer_bias);
729 data.ReviewData(&filter);
730 filter.Plot();
731}
732
733} // namespace vision
734} // namespace frc971