blob: 9b1fa975a57e7ac40c51f8922899f5bdeacf0424 [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
Jim Ostrowskib3cab972022-12-03 15:47:00 -08003#include <algorithm>
Stephan Pleines6191f1d2024-05-30 20:44:45 -07004#include <iomanip>
Jim Ostrowskib3cab972022-12-03 15:47:00 -08005#include <limits>
Jim Ostrowskib3cab972022-12-03 15:47:00 -08006
milind-u8c72d532021-12-11 15:02:42 -08007#include "Eigen/Dense"
Austin Schuh99f7c6a2024-06-25 22:07:44 -07008#include "absl/flags/flag.h"
Philipp Schrader790cb542023-07-05 21:06:52 -07009#include "external/com_github_foxglove_schemas/CompressedImage_schema.h"
10#include "external/com_github_foxglove_schemas/ImageAnnotations_schema.h"
11#include <opencv2/highgui/highgui.hpp>
12
milind-u8c72d532021-12-11 15:02:42 -080013#include "aos/events/simulated_event_loop.h"
Jim Ostrowskib3cab972022-12-03 15:47:00 -080014#include "aos/network/team_number.h"
milind-u8c72d532021-12-11 15:02:42 -080015#include "aos/time/time.h"
16#include "frc971/control_loops/quaternion_utils.h"
Austin Schuhdcb6b362022-02-25 18:06:21 -080017#include "frc971/vision/charuco_lib.h"
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080018#include "frc971/wpilib/imu_batch_generated.h"
19
Austin Schuh99f7c6a2024-06-25 22:07:44 -070020ABSL_FLAG(bool, display_undistorted, false,
21 "If true, display the undistorted image.");
22ABSL_FLAG(std::string, save_path, "", "Where to store annotated images");
23ABSL_FLAG(bool, save_valid_only, false,
24 "If true, only save images with valid pose estimates");
milind-u8c72d532021-12-11 15:02:42 -080025
Stephan Pleinesf63bde82024-01-13 15:59:33 -080026namespace frc971::vision {
milind-u8c72d532021-12-11 15:02:42 -080027using aos::distributed_clock;
28using aos::monotonic_clock;
29namespace chrono = std::chrono;
30
Austin Schuh5b379072021-12-26 16:01:04 -080031constexpr double kG = 9.807;
32
milind-u8c72d532021-12-11 15:02:42 -080033void CalibrationData::AddCameraPose(
34 distributed_clock::time_point distributed_now, Eigen::Vector3d rvec,
35 Eigen::Vector3d tvec) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080036 // Always start with IMU (or turret) reading...
37 // Note, we may not have a turret, so need to handle that case
38 // If we later get a turret point, then we handle removal of camera points in
39 // AddTurret
40 if ((!imu_points_.empty() && imu_points_[0].first < distributed_now) &&
41 (turret_points_.empty() || turret_points_[0].first < distributed_now)) {
Austin Schuh5b379072021-12-26 16:01:04 -080042 rot_trans_points_.emplace_back(distributed_now, std::make_pair(rvec, tvec));
43 }
milind-u8c72d532021-12-11 15:02:42 -080044}
45
46void CalibrationData::AddImu(distributed_clock::time_point distributed_now,
47 Eigen::Vector3d gyro, Eigen::Vector3d accel) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080048 double zero_threshold = 1e-12;
49 // We seem to be getting 0 readings on IMU, so ignore for now
50 // TODO<Jim>: I think this has been resolved in HandleIMU, but want to leave
51 // this here just in case there are other ways this could happen
52 if ((fabs(accel(0)) < zero_threshold) && (fabs(accel(1)) < zero_threshold) &&
53 (fabs(accel(2)) < zero_threshold)) {
54 LOG(FATAL) << "Ignoring zero value from IMU accelerometer: " << accel
55 << " (gyro is " << gyro << ")";
56 } else {
57 imu_points_.emplace_back(distributed_now, std::make_pair(gyro, accel));
58 }
milind-u8c72d532021-12-11 15:02:42 -080059}
60
Austin Schuh2895f4c2022-02-26 16:38:46 -080061void CalibrationData::AddTurret(
62 aos::distributed_clock::time_point distributed_now, Eigen::Vector2d state) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080063 // We want the turret to be known too when solving. But, we don't know if
64 // we are going to have a turret until we get the first reading. In that
65 // case, blow away any camera readings from before.
66 // NOTE: Since the IMU motion is independent of the turret position, we don't
67 // need to remove the IMU readings before the turret
68 if (turret_points_.empty()) {
69 while (!rot_trans_points_.empty() &&
70 rot_trans_points_[0].first < distributed_now) {
71 LOG(INFO) << "Erasing, distributed " << distributed_now;
72 rot_trans_points_.erase(rot_trans_points_.begin());
73 }
Austin Schuh2895f4c2022-02-26 16:38:46 -080074 }
75 turret_points_.emplace_back(distributed_now, state);
76}
77
Austin Schuhdcb6b362022-02-25 18:06:21 -080078void CalibrationData::ReviewData(CalibrationDataObserver *observer) const {
milind-u8c72d532021-12-11 15:02:42 -080079 size_t next_camera_point = 0;
Jim Ostrowskiba2edd12022-12-03 15:44:37 -080080 size_t next_imu_point = 0;
81 size_t next_turret_point = 0;
82
83 // Just go until one of the data streams runs out. We lose a few points, but
84 // it makes the logic much easier
85 while (
86 next_camera_point != rot_trans_points_.size() &&
87 next_imu_point != imu_points_.size() &&
88 (turret_points_.empty() || next_turret_point != turret_points_.size())) {
89 // If camera_point is next, update it
90 if ((rot_trans_points_[next_camera_point].first <=
91 imu_points_[next_imu_point].first) &&
92 (turret_points_.empty() ||
93 (rot_trans_points_[next_camera_point].first <=
94 turret_points_[next_turret_point].first))) {
95 // Camera!
96 observer->UpdateCamera(rot_trans_points_[next_camera_point].first,
97 rot_trans_points_[next_camera_point].second);
98 ++next_camera_point;
99 } else {
100 // If it's not the camera, check if IMU is next
101 if (turret_points_.empty() || (imu_points_[next_imu_point].first <=
102 turret_points_[next_turret_point].first)) {
103 // IMU!
104 observer->UpdateIMU(imu_points_[next_imu_point].first,
105 imu_points_[next_imu_point].second);
106 ++next_imu_point;
milind-u8c72d532021-12-11 15:02:42 -0800107 } else {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800108 // If it's not IMU or camera, and turret_points is not empty, it must be
109 // the turret!
110 observer->UpdateTurret(turret_points_[next_turret_point].first,
111 turret_points_[next_turret_point].second);
112 ++next_turret_point;
milind-u8c72d532021-12-11 15:02:42 -0800113 }
114 }
115 }
116}
117
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800118CalibrationFoxgloveVisualizer::CalibrationFoxgloveVisualizer(
Maxwell Hendersonecc8a7c2024-02-29 20:19:45 -0800119 aos::EventLoop *event_loop, std::string_view camera_channel)
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800120 : event_loop_(event_loop),
Maxwell Hendersonecc8a7c2024-02-29 20:19:45 -0800121 image_converter_(event_loop_, camera_channel, camera_channel,
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800122 ImageCompression::kJpeg),
123 annotations_sender_(
Maxwell Hendersonecc8a7c2024-02-29 20:19:45 -0800124 event_loop_->MakeSender<foxglove::ImageAnnotations>(camera_channel)) {
125}
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800126
127aos::FlatbufferDetachedBuffer<aos::Configuration>
128CalibrationFoxgloveVisualizer::AddVisualizationChannels(
129 const aos::Configuration *config, const aos::Node *node) {
130 constexpr std::string_view channel_name = "/visualization";
131 aos::ChannelT channel_overrides;
132 channel_overrides.max_size = 10000000;
133 aos::FlatbufferDetachedBuffer<aos::Configuration> result =
134 aos::configuration::AddChannelToConfiguration(
135 config, channel_name,
136 aos::FlatbufferSpan<reflection::Schema>(
137 foxglove::ImageAnnotationsSchema()),
138 node, channel_overrides);
139 return aos::configuration::AddChannelToConfiguration(
140 &result.message(), channel_name,
141 aos::FlatbufferSpan<reflection::Schema>(
142 foxglove::CompressedImageSchema()),
143 node, channel_overrides);
144}
145
James Kuszmaul7e958812023-02-11 15:34:31 -0800146Calibration::Calibration(
147 aos::SimulatedEventLoopFactory *event_loop_factory,
148 aos::EventLoop *image_event_loop, aos::EventLoop *imu_event_loop,
Jim Ostrowski3dc21642024-01-22 16:08:40 -0800149 std::string_view hostname,
James Kuszmaul7e958812023-02-11 15:34:31 -0800150 const calibration::CameraCalibration *intrinsics_calibration,
151 TargetType target_type, std::string_view image_channel,
152 CalibrationData *data)
milind-u8c72d532021-12-11 15:02:42 -0800153 : image_event_loop_(image_event_loop),
154 image_factory_(event_loop_factory->GetNodeEventLoopFactory(
155 image_event_loop_->node())),
156 imu_event_loop_(imu_event_loop),
157 imu_factory_(
158 event_loop_factory->GetNodeEventLoopFactory(imu_event_loop_->node())),
159 charuco_extractor_(
James Kuszmaul7e958812023-02-11 15:34:31 -0800160 image_event_loop_, intrinsics_calibration, target_type, image_channel,
milind-u8c72d532021-12-11 15:02:42 -0800161 [this](cv::Mat rgb_image, monotonic_clock::time_point eof,
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800162 std::vector<cv::Vec4i> charuco_ids,
163 std::vector<std::vector<cv::Point2f>> charuco_corners,
164 bool valid, std::vector<Eigen::Vector3d> rvecs_eigen,
165 std::vector<Eigen::Vector3d> tvecs_eigen) {
milind-u8c72d532021-12-11 15:02:42 -0800166 HandleCharuco(rgb_image, eof, charuco_ids, charuco_corners, valid,
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800167 rvecs_eigen, tvecs_eigen);
168 }),
Jim Ostrowskicb8b4082024-01-21 02:23:46 -0800169 // TODO: Need to make this work for pi or orin
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800170 image_callback_(
171 image_event_loop_,
Jim Ostrowski3dc21642024-01-22 16:08:40 -0800172 absl::StrCat("/", aos::network::ParsePiOrOrin(hostname).value(),
173 std::to_string(
174 aos::network::ParsePiOrOrinNumber(hostname).value()),
175 image_channel),
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800176 [this](cv::Mat rgb_image, const monotonic_clock::time_point eof) {
177 charuco_extractor_.HandleImage(rgb_image, eof);
milind-u8c72d532021-12-11 15:02:42 -0800178 }),
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800179 data_(data),
180 visualizer_event_loop_(image_factory_->MakeEventLoop("visualization")),
Maxwell Hendersonecc8a7c2024-02-29 20:19:45 -0800181 visualizer_(visualizer_event_loop_.get(), image_channel) {
milind-u8c72d532021-12-11 15:02:42 -0800182 imu_factory_->OnShutdown([]() { cv::destroyAllWindows(); });
183
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800184 // Check for IMUValuesBatch topic on both /localizer and /drivetrain channels,
185 // since both are valid/possible
186 std::string imu_channel;
187 if (imu_event_loop->HasChannel<frc971::IMUValuesBatch>("/localizer")) {
188 imu_channel = "/localizer";
189 } else if (imu_event_loop->HasChannel<frc971::IMUValuesBatch>(
190 "/drivetrain")) {
191 imu_channel = "/drivetrain";
192 } else {
193 LOG(FATAL) << "Couldn't find channel with IMU data for either localizer or "
194 "drivtrain";
195 }
196
197 VLOG(2) << "Listening for " << frc971::IMUValuesBatch::GetFullyQualifiedName()
198 << " on channel: " << imu_channel;
199
milind-u8c72d532021-12-11 15:02:42 -0800200 imu_event_loop_->MakeWatcher(
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800201 imu_channel, [this](const frc971::IMUValuesBatch &imu) {
milind-u8c72d532021-12-11 15:02:42 -0800202 if (!imu.has_readings()) {
203 return;
204 }
205 for (const frc971::IMUValues *value : *imu.readings()) {
206 HandleIMU(value);
207 }
208 });
209}
210
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800211void Calibration::HandleCharuco(
212 cv::Mat rgb_image, const monotonic_clock::time_point eof,
213 std::vector<cv::Vec4i> /*charuco_ids*/,
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800214 std::vector<std::vector<cv::Point2f>> charuco_corners, bool valid,
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800215 std::vector<Eigen::Vector3d> rvecs_eigen,
216 std::vector<Eigen::Vector3d> tvecs_eigen) {
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800217 visualizer_.HandleCharuco(eof, charuco_corners);
milind-u8c72d532021-12-11 15:02:42 -0800218 if (valid) {
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800219 CHECK(rvecs_eigen.size() > 0) << "Require at least one target detected";
220 // We only use one (the first) target detected for calibration
221 data_->AddCameraPose(image_factory_->ToDistributedClock(eof),
222 rvecs_eigen[0], tvecs_eigen[0]);
milind-u8c72d532021-12-11 15:02:42 -0800223
milind-u8c72d532021-12-11 15:02:42 -0800224 Eigen::IOFormat HeavyFmt(Eigen::FullPrecision, 0, ", ", ",\n", "[", "]",
225 "[", "]");
226
227 const double age_double =
228 std::chrono::duration_cast<std::chrono::duration<double>>(
229 image_event_loop_->monotonic_now() - eof)
230 .count();
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800231 VLOG(1) << std::fixed << std::setprecision(6) << "Age: " << age_double
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800232 << ", Pose is R:" << rvecs_eigen[0].transpose().format(HeavyFmt)
233 << "\nT:" << tvecs_eigen[0].transpose().format(HeavyFmt);
milind-u8c72d532021-12-11 15:02:42 -0800234 }
235
Austin Schuh99f7c6a2024-06-25 22:07:44 -0700236 if (absl::GetFlag(FLAGS_visualize)) {
237 if (absl::GetFlag(FLAGS_display_undistorted)) {
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800238 const cv::Size image_size(rgb_image.cols, rgb_image.rows);
239 cv::Mat undistorted_rgb_image(image_size, CV_8UC3);
240 cv::undistort(rgb_image, undistorted_rgb_image,
241 charuco_extractor_.camera_matrix(),
242 charuco_extractor_.dist_coeffs());
milind-u8c72d532021-12-11 15:02:42 -0800243
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800244 cv::imshow("Display undist", undistorted_rgb_image);
245 }
milind-u8c72d532021-12-11 15:02:42 -0800246
Jim Ostrowskib3cab972022-12-03 15:47:00 -0800247 cv::imshow("Display", rgb_image);
248 cv::waitKey(1);
milind-u8c72d532021-12-11 15:02:42 -0800249 }
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800250
Austin Schuh99f7c6a2024-06-25 22:07:44 -0700251 if (absl::GetFlag(FLAGS_save_path) != "") {
252 if (!absl::GetFlag(FLAGS_save_valid_only) || valid) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800253 static int img_count = 0;
254 std::string image_name = absl::StrFormat("/img_%06d.png", img_count);
Austin Schuh99f7c6a2024-06-25 22:07:44 -0700255 std::string path = absl::GetFlag(FLAGS_save_path) + image_name;
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800256 VLOG(2) << "Saving image to " << path;
257 cv::imwrite(path, rgb_image);
258 img_count++;
259 }
260 }
milind-u8c72d532021-12-11 15:02:42 -0800261}
262
263void Calibration::HandleIMU(const frc971::IMUValues *imu) {
Jim Ostrowskiba2edd12022-12-03 15:44:37 -0800264 // Need to check for valid values, since we sometimes don't get them
265 if (!imu->has_gyro_x() || !imu->has_gyro_y() || !imu->has_gyro_z() ||
266 !imu->has_accelerometer_x() || !imu->has_accelerometer_y() ||
267 !imu->has_accelerometer_z()) {
268 return;
269 }
270
271 VLOG(2) << "IMU " << imu;
milind-u8c72d532021-12-11 15:02:42 -0800272 imu->UnPackTo(&last_value_);
273 Eigen::Vector3d gyro(last_value_.gyro_x, last_value_.gyro_y,
274 last_value_.gyro_z);
275 Eigen::Vector3d accel(last_value_.accelerometer_x,
276 last_value_.accelerometer_y,
277 last_value_.accelerometer_z);
278
James Kuszmaul969e4ab2023-01-28 16:09:19 -0800279 // TODO: ToDistributedClock may be too noisy.
milind-u8c72d532021-12-11 15:02:42 -0800280 data_->AddImu(imu_factory_->ToDistributedClock(monotonic_clock::time_point(
281 chrono::nanoseconds(imu->monotonic_timestamp_ns()))),
Austin Schuh5b379072021-12-26 16:01:04 -0800282 gyro, accel * kG);
milind-u8c72d532021-12-11 15:02:42 -0800283}
284
Stephan Pleinesf63bde82024-01-13 15:59:33 -0800285} // namespace frc971::vision