blob: 5584ed7e0cb42cb41841897b238debf534c096c2 [file] [log] [blame]
James Kuszmaul7e958812023-02-11 15:34:31 -08001#include "frc971/vision/intrinsics_calibration_lib.h"
2
Jim Ostrowskib974cca2024-01-28 15:07:50 -08003DECLARE_bool(visualize);
4
Stephan Pleinesf63bde82024-01-13 15:59:33 -08005namespace frc971::vision {
James Kuszmaul7e958812023-02-11 15:34:31 -08006
Jim Ostrowskib974cca2024-01-28 15:07:50 -08007// Found that under 50 ms would fail image too often on intrinsics with
8// visualize on
9constexpr aos::monotonic_clock::duration kMaxImageAge =
10 aos::monotonic_clock::duration(std::chrono::milliseconds(50));
11
James Kuszmaul7e958812023-02-11 15:34:31 -080012IntrinsicsCalibration::IntrinsicsCalibration(
Jim Ostrowski3dc21642024-01-22 16:08:40 -080013 aos::EventLoop *event_loop, std::string_view hostname,
Jim Ostrowskib974cca2024-01-28 15:07:50 -080014 std::string_view camera_channel, std::string_view camera_id,
15 std::string_view base_intrinsics_file, bool display_undistorted,
16 std::string_view calibration_folder, aos::ExitHandle *exit_handle)
Jim Ostrowski3dc21642024-01-22 16:08:40 -080017 : hostname_(hostname),
18 cpu_type_(aos::network::ParsePiOrOrin(hostname_)),
19 cpu_number_(aos::network::ParsePiOrOrinNumber(hostname_)),
Jim Ostrowskib974cca2024-01-28 15:07:50 -080020 camera_channel_(camera_channel),
James Kuszmaul7e958812023-02-11 15:34:31 -080021 camera_id_(camera_id),
22 prev_H_camera_board_(Eigen::Affine3d()),
23 prev_image_H_camera_board_(Eigen::Affine3d()),
24 base_intrinsics_(
25 aos::JsonFileToFlatbuffer<calibration::CameraCalibration>(
26 base_intrinsics_file)),
27 charuco_extractor_(
28 event_loop, &base_intrinsics_.message(), TargetType::kCharuco,
Jim Ostrowskib974cca2024-01-28 15:07:50 -080029 camera_channel_,
James Kuszmaul7e958812023-02-11 15:34:31 -080030 [this](cv::Mat rgb_image, const aos::monotonic_clock::time_point eof,
31 std::vector<cv::Vec4i> charuco_ids,
32 std::vector<std::vector<cv::Point2f>> charuco_corners,
33 bool valid, std::vector<Eigen::Vector3d> rvecs_eigen,
34 std::vector<Eigen::Vector3d> tvecs_eigen) {
35 HandleCharuco(rgb_image, eof, charuco_ids, charuco_corners, valid,
36 rvecs_eigen, tvecs_eigen);
37 }),
38 image_callback_(
Jim Ostrowski144ab392024-03-17 18:41:49 -070039 event_loop, camera_channel_,
James Kuszmaul7e958812023-02-11 15:34:31 -080040 [this](cv::Mat rgb_image,
41 const aos::monotonic_clock::time_point eof) {
Jim Ostrowski144ab392024-03-17 18:41:49 -070042 if (exit_collection_) {
43 return;
44 }
James Kuszmaul7e958812023-02-11 15:34:31 -080045 charuco_extractor_.HandleImage(rgb_image, eof);
46 },
Jim Ostrowskib974cca2024-01-28 15:07:50 -080047 kMaxImageAge),
James Kuszmaul7e958812023-02-11 15:34:31 -080048 display_undistorted_(display_undistorted),
49 calibration_folder_(calibration_folder),
Jim Ostrowski144ab392024-03-17 18:41:49 -070050 exit_handle_(exit_handle),
51 exit_collection_(false) {
52 if (!FLAGS_visualize) {
53 // The only way to exit into the calibration routines is by hitting "q"
54 // while visualization is running. The event_loop doesn't pause enough
55 // to handle ctrl-c exit requests
56 LOG(INFO) << "Setting visualize to true, since currently the intrinsics "
57 "only works this way";
58 FLAGS_visualize = true;
59 }
Jim Ostrowskib974cca2024-01-28 15:07:50 -080060 LOG(INFO) << "Hostname is: " << hostname_ << " and camera channel is "
61 << camera_channel_;
62
Jim Ostrowski3dc21642024-01-22 16:08:40 -080063 CHECK(cpu_number_) << ": Invalid cpu number " << hostname_
64 << ", failed to parse cpu number";
James Kuszmaul7e958812023-02-11 15:34:31 -080065 std::regex re{"^[0-9][0-9]-[0-9][0-9]"};
66 CHECK(std::regex_match(camera_id_, re))
67 << ": Invalid camera_id '" << camera_id_ << "', should be of form YY-NN";
68}
69
70void IntrinsicsCalibration::HandleCharuco(
71 cv::Mat rgb_image, const aos::monotonic_clock::time_point /*eof*/,
72 std::vector<cv::Vec4i> charuco_ids,
73 std::vector<std::vector<cv::Point2f>> charuco_corners, bool valid,
74 std::vector<Eigen::Vector3d> rvecs_eigen,
75 std::vector<Eigen::Vector3d> tvecs_eigen) {
Jim Ostrowskib974cca2024-01-28 15:07:50 -080076 if (FLAGS_visualize) {
77 // Reduce resolution displayed on remote viewer to prevent lag
78 cv::resize(rgb_image, rgb_image,
79 cv::Size(rgb_image.cols / 2, rgb_image.rows / 2));
80 cv::imshow("Display", rgb_image);
James Kuszmaul7e958812023-02-11 15:34:31 -080081
Jim Ostrowskib974cca2024-01-28 15:07:50 -080082 if (display_undistorted_) {
83 const cv::Size image_size(rgb_image.cols, rgb_image.rows);
84 cv::Mat undistorted_rgb_image(image_size, CV_8UC3);
85 cv::undistort(rgb_image, undistorted_rgb_image,
86 charuco_extractor_.camera_matrix(),
87 charuco_extractor_.dist_coeffs());
James Kuszmaul7e958812023-02-11 15:34:31 -080088
Jim Ostrowskib974cca2024-01-28 15:07:50 -080089 cv::imshow("Display undist", undistorted_rgb_image);
90 }
James Kuszmaul7e958812023-02-11 15:34:31 -080091 }
92
93 int keystroke = cv::waitKey(1);
Jim Ostrowski144ab392024-03-17 18:41:49 -070094 if ((keystroke & 0xFF) == static_cast<int>('q')) {
95 LOG(INFO) << "Going to exit";
96 exit_collection_ = true;
97 exit_handle_->Exit();
98 }
James Kuszmaul7e958812023-02-11 15:34:31 -080099 // If we haven't got a valid pose estimate, don't use these points
100 if (!valid) {
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800101 LOG(INFO) << "Skipping because pose is not valid";
James Kuszmaul7e958812023-02-11 15:34:31 -0800102 return;
103 }
104 CHECK(tvecs_eigen.size() == 1)
105 << "Charuco board should only return one translational pose";
106 CHECK(rvecs_eigen.size() == 1)
107 << "Charuco board should only return one rotational pose";
108 // Calibration calculates rotation and translation delta from last image
109 // stored to automatically capture next image
110
111 Eigen::Affine3d H_board_camera =
112 Eigen::Translation3d(tvecs_eigen[0]) *
113 Eigen::AngleAxisd(rvecs_eigen[0].norm(),
114 rvecs_eigen[0] / rvecs_eigen[0].norm());
115 Eigen::Affine3d H_camera_board_ = H_board_camera.inverse();
116 Eigen::Affine3d H_delta = H_board_camera * prev_H_camera_board_;
117
118 Eigen::AngleAxisd delta_r = Eigen::AngleAxisd(H_delta.rotation());
119
120 Eigen::Vector3d delta_t = H_delta.translation();
121
122 double r_norm = std::abs(delta_r.angle());
123 double t_norm = delta_t.norm();
124
125 bool store_image = false;
126 double percent_motion =
127 std::max<double>(r_norm / kDeltaRThreshold, t_norm / kDeltaTThreshold);
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800128 LOG(INFO) << "Captured: " << all_charuco_ids_.size() << " points; \nMoved "
129 << static_cast<int>(percent_motion * 100) << "% of what's needed";
James Kuszmaul7e958812023-02-11 15:34:31 -0800130 // Verify that camera has moved enough from last stored image
131 if (r_norm > kDeltaRThreshold || t_norm > kDeltaTThreshold) {
132 // frame_ refers to deltas between current and last captured image
133 Eigen::Affine3d frame_H_delta = H_board_camera * prev_image_H_camera_board_;
134
135 Eigen::AngleAxisd frame_delta_r =
136 Eigen::AngleAxisd(frame_H_delta.rotation());
137
138 Eigen::Vector3d frame_delta_t = frame_H_delta.translation();
139
140 double frame_r_norm = std::abs(frame_delta_r.angle());
141 double frame_t_norm = frame_delta_t.norm();
142
143 // Make sure camera has stopped moving before storing image
144 store_image =
145 frame_r_norm < kFrameDeltaRLimit && frame_t_norm < kFrameDeltaTLimit;
146 double percent_stop = std::max<double>(frame_r_norm / kFrameDeltaRLimit,
147 frame_t_norm / kFrameDeltaTLimit);
148 LOG(INFO) << "Captured: " << all_charuco_ids_.size()
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800149 << "points; \nMoved enough ("
150 << static_cast<int>(percent_motion * 100)
151 << "%); Need to stop (last motion was "
152 << static_cast<int>(percent_stop * 100)
153 << "% of limit; needs to be < 1% to capture)";
James Kuszmaul7e958812023-02-11 15:34:31 -0800154 }
155 prev_image_H_camera_board_ = H_camera_board_;
156
157 if (store_image) {
158 if (valid) {
159 prev_H_camera_board_ = H_camera_board_;
160
161 // Unpack the Charuco ids from Vec4i
162 std::vector<int> charuco_ids_int;
163 for (cv::Vec4i charuco_id : charuco_ids) {
164 charuco_ids_int.emplace_back(charuco_id[0]);
165 }
166 all_charuco_ids_.emplace_back(std::move(charuco_ids_int));
167 all_charuco_corners_.emplace_back(std::move(charuco_corners[0]));
168
169 if (r_norm > kDeltaRThreshold) {
170 LOG(INFO) << "Triggered by rotation delta = " << r_norm << " > "
171 << kDeltaRThreshold;
172 }
173 if (t_norm > kDeltaTThreshold) {
174 LOG(INFO) << "Triggered by translation delta = " << t_norm << " > "
175 << kDeltaTThreshold;
176 }
177 }
James Kuszmaul7e958812023-02-11 15:34:31 -0800178 }
179}
180
181aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
182IntrinsicsCalibration::BuildCalibration(
183 cv::Mat camera_matrix, cv::Mat dist_coeffs,
Jim Ostrowski3dc21642024-01-22 16:08:40 -0800184 aos::realtime_clock::time_point realtime_now, std::string_view cpu_type,
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800185 uint16_t cpu_number, std::string_view camera_channel,
Jim Ostrowskifa717862024-02-11 21:16:02 -0800186 std::string_view camera_id, uint16_t team_number,
187 double reprojection_error) {
James Kuszmaul7e958812023-02-11 15:34:31 -0800188 flatbuffers::FlatBufferBuilder fbb;
Jim Ostrowski144ab392024-03-17 18:41:49 -0700189 // THIS IS A HACK FOR 2024, since we call Orin2 "Imu"
190 std::string cpu_name = absl::StrFormat("%s%d", cpu_type, cpu_number);
191 if (cpu_type == "orin" && cpu_number == 2) {
192 LOG(INFO) << "Renaming orin2 to imu";
193 cpu_name = "imu";
194 }
James Kuszmaul7e958812023-02-11 15:34:31 -0800195 flatbuffers::Offset<flatbuffers::String> name_offset =
Jim Ostrowski144ab392024-03-17 18:41:49 -0700196 fbb.CreateString(cpu_name.c_str());
James Kuszmaul7e958812023-02-11 15:34:31 -0800197 flatbuffers::Offset<flatbuffers::String> camera_id_offset =
198 fbb.CreateString(camera_id);
199 flatbuffers::Offset<flatbuffers::Vector<float>> intrinsics_offset =
200 fbb.CreateVector<float>(9u, [&camera_matrix](size_t i) {
201 return static_cast<float>(
202 reinterpret_cast<double *>(camera_matrix.data)[i]);
203 });
204
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800205 std::optional<uint16_t> camera_number =
206 frc971::vision::CameraNumberFromChannel(std::string(camera_channel));
207
James Kuszmaul7e958812023-02-11 15:34:31 -0800208 flatbuffers::Offset<flatbuffers::Vector<float>>
209 distortion_coefficients_offset =
210 fbb.CreateVector<float>(5u, [&dist_coeffs](size_t i) {
211 return static_cast<float>(
212 reinterpret_cast<double *>(dist_coeffs.data)[i]);
213 });
214
215 calibration::CameraCalibration::Builder camera_calibration_builder(fbb);
216
217 camera_calibration_builder.add_node_name(name_offset);
218 camera_calibration_builder.add_team_number(team_number);
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800219 camera_calibration_builder.add_camera_number(camera_number.value());
James Kuszmaul7e958812023-02-11 15:34:31 -0800220 camera_calibration_builder.add_camera_id(camera_id_offset);
Jim Ostrowskifa717862024-02-11 21:16:02 -0800221 camera_calibration_builder.add_reprojection_error(
222 static_cast<float>(reprojection_error));
James Kuszmaul7e958812023-02-11 15:34:31 -0800223 camera_calibration_builder.add_calibration_timestamp(
224 realtime_now.time_since_epoch().count());
225 camera_calibration_builder.add_intrinsics(intrinsics_offset);
226 camera_calibration_builder.add_dist_coeffs(distortion_coefficients_offset);
227 fbb.Finish(camera_calibration_builder.Finish());
228
229 return fbb.Release();
230}
231
232void IntrinsicsCalibration::MaybeCalibrate() {
233 // TODO: This number should depend on coarse vs. fine pattern
234 // Maybe just on total # of ids found, not just images
235 if (all_charuco_ids_.size() >= 50) {
236 LOG(INFO) << "Beginning calibration on " << all_charuco_ids_.size()
237 << " images";
238 cv::Mat camera_matrix, dist_coeffs;
239 std::vector<cv::Mat> rvecs, tvecs;
240 cv::Mat std_deviations_intrinsics, std_deviations_extrinsics,
241 per_view_errors;
242
243 // Set calibration flags (same as in calibrateCamera() function)
244 int calibration_flags = 0;
245 cv::Size img_size(640, 480);
246 const double reprojection_error = cv::aruco::calibrateCameraCharuco(
247 all_charuco_corners_, all_charuco_ids_, charuco_extractor_.board(),
248 img_size, camera_matrix, dist_coeffs, rvecs, tvecs,
249 std_deviations_intrinsics, std_deviations_extrinsics, per_view_errors,
250 calibration_flags);
Jim Ostrowskifa717862024-02-11 21:16:02 -0800251 CHECK_LE(reprojection_error, 2.0)
James Kuszmaul7e958812023-02-11 15:34:31 -0800252 << ": Reproduction error is bad-- greater than 1 pixel.";
253 LOG(INFO) << "Reprojection Error is " << reprojection_error;
254
255 const aos::realtime_clock::time_point realtime_now =
256 aos::realtime_clock::now();
257 std::optional<uint16_t> team_number =
Jim Ostrowski3dc21642024-01-22 16:08:40 -0800258 aos::network::team_number_internal::ParsePiOrOrinTeamNumber(hostname_);
259 CHECK(team_number) << ": Invalid hostname " << hostname_
James Kuszmaul7e958812023-02-11 15:34:31 -0800260 << ", failed to parse team number";
261 aos::FlatbufferDetachedBuffer<calibration::CameraCalibration>
Jim Ostrowskifa717862024-02-11 21:16:02 -0800262 camera_calibration = BuildCalibration(
263 camera_matrix, dist_coeffs, realtime_now, cpu_type_.value(),
264 cpu_number_.value(), camera_channel_, camera_id_,
265 team_number.value(), reprojection_error);
James Kuszmaul7e958812023-02-11 15:34:31 -0800266 std::stringstream time_ss;
267 time_ss << realtime_now;
268
Jim Ostrowskib974cca2024-01-28 15:07:50 -0800269 std::optional<uint16_t> camera_number =
270 frc971::vision::CameraNumberFromChannel(camera_channel_);
Jim Ostrowski33208982024-03-02 15:01:45 -0800271 CHECK(camera_number.has_value());
272 std::string calibration_filename =
273 CalibrationFilename(calibration_folder_, hostname_, team_number.value(),
274 camera_number.value(), camera_id_, time_ss.str());
James Kuszmaul7e958812023-02-11 15:34:31 -0800275
276 LOG(INFO) << calibration_filename << " -> "
277 << aos::FlatbufferToJson(camera_calibration,
278 {.multi_line = true});
279
280 aos::util::WriteStringToFileOrDie(
281 calibration_filename,
282 aos::FlatbufferToJson(camera_calibration, {.multi_line = true}));
283 } else {
284 LOG(INFO) << "Skipping calibration due to not enough images.";
285 }
286}
Stephan Pleinesf63bde82024-01-13 15:59:33 -0800287} // namespace frc971::vision