blob: 8125e5ef3cf7e907365894bb850c0def61378e02 [file] [log] [blame]
Austin Schuhdcb6b362022-02-25 18:06:21 -08001#include "frc971/vision/calibration_accumulator.h"
milind-u8c72d532021-12-11 15:02:42 -08002
milind-u8c72d532021-12-11 15:02:42 -08003#include "Eigen/Dense"
4#include "aos/events/simulated_event_loop.h"
5#include "aos/time/time.h"
6#include "frc971/control_loops/quaternion_utils.h"
Austin Schuhdcb6b362022-02-25 18:06:21 -08007#include "frc971/vision/charuco_lib.h"
Jim Ostrowskiba2edd12022-12-03 15:44:37 -08008#include "frc971/wpilib/imu_batch_generated.h"
9
10#include <algorithm>
11#include <limits>
12#include <opencv2/highgui/highgui.hpp>
milind-u8c72d532021-12-11 15:02:42 -080013
14DEFINE_bool(display_undistorted, false,
15 "If true, display the undistorted image.");
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080016DEFINE_string(save_path, "", "Where to store annotated images");
17DEFINE_bool(save_valid_only, false,
18 "If true, only save images with valid pose estimates");
milind-u8c72d532021-12-11 15:02:42 -080019
20namespace frc971 {
21namespace vision {
22using aos::distributed_clock;
23using aos::monotonic_clock;
24namespace chrono = std::chrono;
25
Austin Schuh5b379072021-12-26 16:01:04 -080026constexpr double kG = 9.807;
27
milind-u8c72d532021-12-11 15:02:42 -080028void CalibrationData::AddCameraPose(
29 distributed_clock::time_point distributed_now, Eigen::Vector3d rvec,
30 Eigen::Vector3d tvec) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080031 // Always start with IMU (or turret) reading...
32 // Note, we may not have a turret, so need to handle that case
33 // If we later get a turret point, then we handle removal of camera points in
34 // AddTurret
35 if ((!imu_points_.empty() && imu_points_[0].first < distributed_now) &&
36 (turret_points_.empty() || turret_points_[0].first < distributed_now)) {
Austin Schuh5b379072021-12-26 16:01:04 -080037 rot_trans_points_.emplace_back(distributed_now, std::make_pair(rvec, tvec));
38 }
milind-u8c72d532021-12-11 15:02:42 -080039}
40
41void CalibrationData::AddImu(distributed_clock::time_point distributed_now,
42 Eigen::Vector3d gyro, Eigen::Vector3d accel) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080043 double zero_threshold = 1e-12;
44 // We seem to be getting 0 readings on IMU, so ignore for now
45 // TODO<Jim>: I think this has been resolved in HandleIMU, but want to leave
46 // this here just in case there are other ways this could happen
47 if ((fabs(accel(0)) < zero_threshold) && (fabs(accel(1)) < zero_threshold) &&
48 (fabs(accel(2)) < zero_threshold)) {
49 LOG(FATAL) << "Ignoring zero value from IMU accelerometer: " << accel
50 << " (gyro is " << gyro << ")";
51 } else {
52 imu_points_.emplace_back(distributed_now, std::make_pair(gyro, accel));
53 }
milind-u8c72d532021-12-11 15:02:42 -080054}
55
Austin Schuh2895f4c2022-02-26 16:38:46 -080056void CalibrationData::AddTurret(
57 aos::distributed_clock::time_point distributed_now, Eigen::Vector2d state) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080058 // We want the turret to be known too when solving. But, we don't know if
59 // we are going to have a turret until we get the first reading. In that
60 // case, blow away any camera readings from before.
61 // NOTE: Since the IMU motion is independent of the turret position, we don't
62 // need to remove the IMU readings before the turret
63 if (turret_points_.empty()) {
64 while (!rot_trans_points_.empty() &&
65 rot_trans_points_[0].first < distributed_now) {
66 LOG(INFO) << "Erasing, distributed " << distributed_now;
67 rot_trans_points_.erase(rot_trans_points_.begin());
68 }
Austin Schuh2895f4c2022-02-26 16:38:46 -080069 }
70 turret_points_.emplace_back(distributed_now, state);
71}
72
Austin Schuhdcb6b362022-02-25 18:06:21 -080073void CalibrationData::ReviewData(CalibrationDataObserver *observer) const {
milind-u8c72d532021-12-11 15:02:42 -080074 size_t next_camera_point = 0;
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080075 size_t next_imu_point = 0;
76 size_t next_turret_point = 0;
77
78 // Just go until one of the data streams runs out. We lose a few points, but
79 // it makes the logic much easier
80 while (
81 next_camera_point != rot_trans_points_.size() &&
82 next_imu_point != imu_points_.size() &&
83 (turret_points_.empty() || next_turret_point != turret_points_.size())) {
84 // If camera_point is next, update it
85 if ((rot_trans_points_[next_camera_point].first <=
86 imu_points_[next_imu_point].first) &&
87 (turret_points_.empty() ||
88 (rot_trans_points_[next_camera_point].first <=
89 turret_points_[next_turret_point].first))) {
90 // Camera!
91 observer->UpdateCamera(rot_trans_points_[next_camera_point].first,
92 rot_trans_points_[next_camera_point].second);
93 ++next_camera_point;
94 } else {
95 // If it's not the camera, check if IMU is next
96 if (turret_points_.empty() || (imu_points_[next_imu_point].first <=
97 turret_points_[next_turret_point].first)) {
98 // IMU!
99 observer->UpdateIMU(imu_points_[next_imu_point].first,
100 imu_points_[next_imu_point].second);
101 ++next_imu_point;
milind-u8c72d532021-12-11 15:02:42 -0800102 } else {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800103 // If it's not IMU or camera, and turret_points is not empty, it must be
104 // the turret!
105 observer->UpdateTurret(turret_points_[next_turret_point].first,
106 turret_points_[next_turret_point].second);
107 ++next_turret_point;
milind-u8c72d532021-12-11 15:02:42 -0800108 }
109 }
110 }
111}
112
113Calibration::Calibration(aos::SimulatedEventLoopFactory *event_loop_factory,
114 aos::EventLoop *image_event_loop,
115 aos::EventLoop *imu_event_loop, std::string_view pi,
116 CalibrationData *data)
117 : image_event_loop_(image_event_loop),
118 image_factory_(event_loop_factory->GetNodeEventLoopFactory(
119 image_event_loop_->node())),
120 imu_event_loop_(imu_event_loop),
121 imu_factory_(
122 event_loop_factory->GetNodeEventLoopFactory(imu_event_loop_->node())),
123 charuco_extractor_(
124 image_event_loop_, pi,
125 [this](cv::Mat rgb_image, monotonic_clock::time_point eof,
126 std::vector<int> charuco_ids,
127 std::vector<cv::Point2f> charuco_corners, bool valid,
128 Eigen::Vector3d rvec_eigen, Eigen::Vector3d tvec_eigen) {
129 HandleCharuco(rgb_image, eof, charuco_ids, charuco_corners, valid,
130 rvec_eigen, tvec_eigen);
131 }),
132 data_(data) {
133 imu_factory_->OnShutdown([]() { cv::destroyAllWindows(); });
134
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800135 // Check for IMUValuesBatch topic on both /localizer and /drivetrain channels,
136 // since both are valid/possible
137 std::string imu_channel;
138 if (imu_event_loop->HasChannel<frc971::IMUValuesBatch>("/localizer")) {
139 imu_channel = "/localizer";
140 } else if (imu_event_loop->HasChannel<frc971::IMUValuesBatch>(
141 "/drivetrain")) {
142 imu_channel = "/drivetrain";
143 } else {
144 LOG(FATAL) << "Couldn't find channel with IMU data for either localizer or "
145 "drivtrain";
146 }
147
148 VLOG(2) << "Listening for " << frc971::IMUValuesBatch::GetFullyQualifiedName()
149 << " on channel: " << imu_channel;
150
milind-u8c72d532021-12-11 15:02:42 -0800151 imu_event_loop_->MakeWatcher(
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800152 imu_channel, [this](const frc971::IMUValuesBatch &imu) {
milind-u8c72d532021-12-11 15:02:42 -0800153 if (!imu.has_readings()) {
154 return;
155 }
156 for (const frc971::IMUValues *value : *imu.readings()) {
157 HandleIMU(value);
158 }
159 });
160}
161
162void Calibration::HandleCharuco(cv::Mat rgb_image,
163 const monotonic_clock::time_point eof,
164 std::vector<int> /*charuco_ids*/,
165 std::vector<cv::Point2f> /*charuco_corners*/,
166 bool valid, Eigen::Vector3d rvec_eigen,
167 Eigen::Vector3d tvec_eigen) {
168 if (valid) {
169 data_->AddCameraPose(image_factory_->ToDistributedClock(eof), rvec_eigen,
170 tvec_eigen);
171
milind-u8c72d532021-12-11 15:02:42 -0800172 Eigen::IOFormat HeavyFmt(Eigen::FullPrecision, 0, ", ", ",\n", "[", "]",
173 "[", "]");
174
175 const double age_double =
176 std::chrono::duration_cast<std::chrono::duration<double>>(
177 image_event_loop_->monotonic_now() - eof)
178 .count();
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800179 VLOG(1) << std::fixed << std::setprecision(6) << "Age: " << age_double
180 << ", Pose is R:" << rvec_eigen.transpose().format(HeavyFmt)
181 << "\nT:" << tvec_eigen.transpose().format(HeavyFmt);
milind-u8c72d532021-12-11 15:02:42 -0800182 }
183
184 cv::imshow("Display", rgb_image);
185
186 if (FLAGS_display_undistorted) {
187 const cv::Size image_size(rgb_image.cols, rgb_image.rows);
188 cv::Mat undistorted_rgb_image(image_size, CV_8UC3);
189 cv::undistort(rgb_image, undistorted_rgb_image,
190 charuco_extractor_.camera_matrix(),
191 charuco_extractor_.dist_coeffs());
192
193 cv::imshow("Display undist", undistorted_rgb_image);
194 }
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800195
196 if (FLAGS_save_path != "") {
197 if (!FLAGS_save_valid_only || valid) {
198 static int img_count = 0;
199 std::string image_name = absl::StrFormat("/img_%06d.png", img_count);
200 std::string path = FLAGS_save_path + image_name;
201 VLOG(2) << "Saving image to " << path;
202 cv::imwrite(path, rgb_image);
203 img_count++;
204 }
205 }
milind-u8c72d532021-12-11 15:02:42 -0800206}
207
208void Calibration::HandleIMU(const frc971::IMUValues *imu) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800209 // Need to check for valid values, since we sometimes don't get them
210 if (!imu->has_gyro_x() || !imu->has_gyro_y() || !imu->has_gyro_z() ||
211 !imu->has_accelerometer_x() || !imu->has_accelerometer_y() ||
212 !imu->has_accelerometer_z()) {
213 return;
214 }
215
216 VLOG(2) << "IMU " << imu;
milind-u8c72d532021-12-11 15:02:42 -0800217 imu->UnPackTo(&last_value_);
218 Eigen::Vector3d gyro(last_value_.gyro_x, last_value_.gyro_y,
219 last_value_.gyro_z);
220 Eigen::Vector3d accel(last_value_.accelerometer_x,
221 last_value_.accelerometer_y,
222 last_value_.accelerometer_z);
223
224 data_->AddImu(imu_factory_->ToDistributedClock(monotonic_clock::time_point(
225 chrono::nanoseconds(imu->monotonic_timestamp_ns()))),
Austin Schuh5b379072021-12-26 16:01:04 -0800226 gyro, accel * kG);
milind-u8c72d532021-12-11 15:02:42 -0800227}
228
229} // namespace vision
230} // namespace frc971