Austin Schuh | bb4aae7 | 2021-10-08 22:12:25 -0700 | [diff] [blame^] | 1 | #include <opencv2/aruco/charuco.hpp> |
| 2 | #include <opencv2/calib3d.hpp> |
| 3 | #include <opencv2/features2d.hpp> |
| 4 | #include <opencv2/highgui/highgui.hpp> |
| 5 | #include <opencv2/imgproc.hpp> |
| 6 | #include "Eigen/Dense" |
| 7 | #include "Eigen/Geometry" |
| 8 | |
| 9 | #include "frc971/wpilib/imu_batch_generated.h" |
| 10 | #include "absl/strings/str_format.h" |
| 11 | #include "frc971/control_loops/quaternion_utils.h" |
| 12 | #include "y2020/vision/charuco_lib.h" |
| 13 | #include "aos/events/logging/log_reader.h" |
| 14 | #include "aos/events/shm_event_loop.h" |
| 15 | #include "aos/init.h" |
| 16 | #include "aos/network/team_number.h" |
| 17 | #include "aos/time/time.h" |
| 18 | #include "aos/util/file.h" |
| 19 | #include "frc971/control_loops/drivetrain/improved_down_estimator.h" |
| 20 | #include "y2020/vision/sift/sift_generated.h" |
| 21 | #include "y2020/vision/sift/sift_training_generated.h" |
| 22 | #include "y2020/vision/tools/python_code/sift_training_data.h" |
| 23 | #include "y2020/vision/vision_generated.h" |
| 24 | #include "y2020/vision/charuco_lib.h" |
| 25 | |
| 26 | DEFINE_string(config, "config.json", "Path to the config file to use."); |
| 27 | DEFINE_string(pi, "pi-7971-2", "Pi name to calibrate."); |
| 28 | DEFINE_bool(display_undistorted, false, |
| 29 | "If true, display the undistorted image."); |
| 30 | |
| 31 | namespace frc971 { |
| 32 | namespace vision { |
| 33 | namespace chrono = std::chrono; |
| 34 | using aos::distributed_clock; |
| 35 | using aos::monotonic_clock; |
| 36 | |
| 37 | // Class to both accumulate and replay camera and IMU data in time order. |
| 38 | class CalibrationData { |
| 39 | public: |
| 40 | CalibrationData() |
| 41 | : x_hat_(Eigen::Matrix<double, 9, 1>::Zero()), |
| 42 | q_(Eigen::Matrix<double, 9, 9>::Zero()) {} |
| 43 | |
| 44 | // Adds an IMU point to the list at the provided time. |
| 45 | void AddImu(distributed_clock::time_point distributed_now, |
| 46 | Eigen::Vector3d gyro, Eigen::Vector3d accel) { |
| 47 | imu_points_.emplace_back(distributed_now, std::make_pair(gyro, accel)); |
| 48 | } |
| 49 | |
| 50 | // Adds a camera/charuco detection to the list at the provided time. |
| 51 | void AddCharuco(distributed_clock::time_point distributed_now, |
| 52 | Eigen::Vector3d rvec, Eigen::Vector3d tvec) { |
| 53 | rot_trans_points_.emplace_back(distributed_now, std::make_pair(rvec, tvec)); |
| 54 | } |
| 55 | |
| 56 | // Processes the data points by calling UpdateCamera and UpdateIMU in time |
| 57 | // order. |
| 58 | void ReviewData() { |
| 59 | size_t next_imu_point = 0; |
| 60 | size_t next_camera_point = 0; |
| 61 | while (true) { |
| 62 | if (next_imu_point != imu_points_.size()) { |
| 63 | // There aren't that many combinations, so just brute force them all |
| 64 | // rather than being too clever. |
| 65 | if (next_camera_point != rot_trans_points_.size()) { |
| 66 | if (imu_points_[next_imu_point].first > |
| 67 | rot_trans_points_[next_camera_point].first) { |
| 68 | // Camera! |
| 69 | UpdateCamera(rot_trans_points_[next_camera_point].first, |
| 70 | rot_trans_points_[next_camera_point].second); |
| 71 | ++next_camera_point; |
| 72 | } else { |
| 73 | // IMU! |
| 74 | UpdateIMU(imu_points_[next_imu_point].first, |
| 75 | imu_points_[next_imu_point].second); |
| 76 | ++next_imu_point; |
| 77 | } |
| 78 | } else { |
| 79 | if (next_camera_point != rot_trans_points_.size()) { |
| 80 | // Camera! |
| 81 | UpdateCamera(rot_trans_points_[next_camera_point].first, |
| 82 | rot_trans_points_[next_camera_point].second); |
| 83 | ++next_camera_point; |
| 84 | } else { |
| 85 | // Nothing left for either list of points, so we are done. |
| 86 | break; |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | void UpdateCamera(distributed_clock::time_point t, |
| 94 | std::pair<Eigen::Vector3d, Eigen::Vector3d> rt) { |
| 95 | LOG(INFO) << t << " Camera " << rt.second.transpose(); |
| 96 | } |
| 97 | |
| 98 | void UpdateIMU(distributed_clock::time_point t, |
| 99 | std::pair<Eigen::Vector3d, Eigen::Vector3d> wa) { |
| 100 | LOG(INFO) << t << " IMU " << wa.first.transpose(); |
| 101 | } |
| 102 | |
| 103 | private: |
| 104 | // TODO(austin): Actually use these. Or make a new "callback" object which has these. |
| 105 | Eigen::Matrix<double, 9, 1> x_hat_; |
| 106 | Eigen::Matrix<double, 9, 9> q_; |
| 107 | |
| 108 | // Proposed filter states: |
| 109 | // States: |
| 110 | // xyz position |
| 111 | // xyz velocity |
| 112 | // orientation rotation vector |
| 113 | // |
| 114 | // Inputs |
| 115 | // xyz accel |
| 116 | // angular rates |
| 117 | // |
| 118 | // Measurement: |
| 119 | // xyz position |
| 120 | // orientation rotation vector |
| 121 | |
| 122 | std::vector<std::pair<distributed_clock::time_point, |
| 123 | std::pair<Eigen::Vector3d, Eigen::Vector3d>>> |
| 124 | imu_points_; |
| 125 | |
| 126 | // Store pose samples as timestamp, along with |
| 127 | // pair of rotation, translation vectors |
| 128 | std::vector<std::pair<distributed_clock::time_point, |
| 129 | std::pair<Eigen::Vector3d, Eigen::Vector3d>>> |
| 130 | rot_trans_points_; |
| 131 | }; |
| 132 | |
| 133 | // Class to register image and IMU callbacks in AOS and route them to the |
| 134 | // corresponding CalibrationData class. |
| 135 | class Calibration { |
| 136 | public: |
| 137 | Calibration(aos::SimulatedEventLoopFactory *event_loop_factory, |
| 138 | aos::EventLoop *image_event_loop, aos::EventLoop *imu_event_loop, |
| 139 | std::string_view pi, CalibrationData *data) |
| 140 | : image_event_loop_(image_event_loop), |
| 141 | image_factory_(event_loop_factory->GetNodeEventLoopFactory( |
| 142 | image_event_loop_->node())), |
| 143 | imu_event_loop_(imu_event_loop), |
| 144 | imu_factory_(event_loop_factory->GetNodeEventLoopFactory( |
| 145 | imu_event_loop_->node())), |
| 146 | charuco_extractor_( |
| 147 | image_event_loop_, pi, |
| 148 | [this](cv::Mat rgb_image, monotonic_clock::time_point eof, |
| 149 | std::vector<int> charuco_ids, |
| 150 | std::vector<cv::Point2f> charuco_corners, bool valid, |
| 151 | Eigen::Vector3d rvec_eigen, Eigen::Vector3d tvec_eigen) { |
| 152 | HandleCharuco(rgb_image, eof, charuco_ids, charuco_corners, valid, |
| 153 | rvec_eigen, tvec_eigen); |
| 154 | }), |
| 155 | data_(data) { |
| 156 | imu_event_loop_->MakeWatcher( |
| 157 | "/drivetrain", [this](const frc971::IMUValuesBatch &imu) { |
| 158 | if (!imu.has_readings()) { |
| 159 | return; |
| 160 | } |
| 161 | for (const frc971::IMUValues *value : *imu.readings()) { |
| 162 | HandleIMU(value); |
| 163 | } |
| 164 | }); |
| 165 | } |
| 166 | |
| 167 | // Processes a charuco detection. |
| 168 | void HandleCharuco(cv::Mat rgb_image, const monotonic_clock::time_point eof, |
| 169 | std::vector<int> /*charuco_ids*/, |
| 170 | std::vector<cv::Point2f> /*charuco_corners*/, bool valid, |
| 171 | Eigen::Vector3d rvec_eigen, Eigen::Vector3d tvec_eigen) { |
| 172 | if (valid) { |
| 173 | Eigen::Quaternion<double> rotation( |
| 174 | frc971::controls::ToQuaternionFromRotationVector(rvec_eigen)); |
| 175 | Eigen::Translation3d translation(tvec_eigen); |
| 176 | |
| 177 | const Eigen::Affine3d board_to_camera = translation * rotation; |
| 178 | (void)board_to_camera; |
| 179 | |
| 180 | // TODO(austin): Need a gravity vector input. |
| 181 | // |
| 182 | // TODO(austin): Need a state, covariance, and model. |
| 183 | // |
| 184 | // TODO(austin): Need to record all the values out of a log and run it |
| 185 | // as a batch run so we can feed it into ceres. |
| 186 | |
| 187 | // LOG(INFO) << "rotation " << rotation.matrix(); |
| 188 | // LOG(INFO) << "translation " << translation.matrix(); |
| 189 | // Z -> up |
| 190 | // Y -> away from cameras 2 and 3 |
| 191 | // X -> left |
| 192 | Eigen::Vector3d imu(last_value_.accelerometer_x, |
| 193 | last_value_.accelerometer_y, |
| 194 | last_value_.accelerometer_z); |
| 195 | |
| 196 | // For cameras 2 and 3... |
| 197 | Eigen::Quaternion<double> imu_to_camera( |
| 198 | Eigen::AngleAxisd(-0.5 * M_PI, Eigen::Vector3d::UnitX())); |
| 199 | |
| 200 | Eigen::Quaternion<double> board_to_world( |
| 201 | Eigen::AngleAxisd(0.5 * M_PI, Eigen::Vector3d::UnitX())); |
| 202 | |
| 203 | Eigen::IOFormat HeavyFmt(Eigen::FullPrecision, 0, ", ", ",\n", "[", "]", |
| 204 | "[", "]"); |
| 205 | |
| 206 | LOG(INFO); |
| 207 | LOG(INFO) << "World Gravity " |
| 208 | << (board_to_world * rotation.inverse() * imu_to_camera * imu) |
| 209 | .transpose() |
| 210 | .format(HeavyFmt); |
| 211 | LOG(INFO) << "Board Gravity " |
| 212 | << (rotation.inverse() * imu_to_camera * imu) |
| 213 | .transpose() |
| 214 | .format(HeavyFmt); |
| 215 | LOG(INFO) << "Camera Gravity " |
| 216 | << (imu_to_camera * imu).transpose().format(HeavyFmt); |
| 217 | LOG(INFO) << "IMU Gravity " << imu.transpose().format(HeavyFmt); |
| 218 | |
| 219 | const double age_double = |
| 220 | std::chrono::duration_cast<std::chrono::duration<double>>( |
| 221 | image_event_loop_->monotonic_now() - eof) |
| 222 | .count(); |
| 223 | LOG(INFO) << std::fixed << std::setprecision(6) << "Age: " << age_double |
| 224 | << ", Pose is R:" << rvec_eigen.transpose().format(HeavyFmt) |
| 225 | << " T:" << tvec_eigen.transpose().format(HeavyFmt); |
| 226 | |
| 227 | data_->AddCharuco(image_factory_->ToDistributedClock(eof), rvec_eigen, |
| 228 | tvec_eigen); |
| 229 | } |
| 230 | |
| 231 | cv::imshow("Display", rgb_image); |
| 232 | |
| 233 | if (FLAGS_display_undistorted) { |
| 234 | const cv::Size image_size(rgb_image.cols, rgb_image.rows); |
| 235 | cv::Mat undistorted_rgb_image(image_size, CV_8UC3); |
| 236 | cv::undistort(rgb_image, undistorted_rgb_image, |
| 237 | charuco_extractor_.camera_matrix(), |
| 238 | charuco_extractor_.dist_coeffs()); |
| 239 | |
| 240 | cv::imshow("Display undist", undistorted_rgb_image); |
| 241 | } |
| 242 | |
| 243 | int keystroke = cv::waitKey(1); |
| 244 | if ((keystroke & 0xFF) == static_cast<int>('q')) { |
| 245 | // image_event_loop_->Exit(); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | // Processes an IMU reading. |
| 250 | void HandleIMU(const frc971::IMUValues *imu) { |
| 251 | VLOG(1) << "IMU " << imu; |
| 252 | imu->UnPackTo(&last_value_); |
| 253 | Eigen::Vector3d gyro(last_value_.gyro_x, last_value_.gyro_y, |
| 254 | last_value_.gyro_z); |
| 255 | Eigen::Vector3d accel(last_value_.accelerometer_x, |
| 256 | last_value_.accelerometer_y, |
| 257 | last_value_.accelerometer_z); |
| 258 | |
| 259 | data_->AddImu(imu_factory_->ToDistributedClock(monotonic_clock::time_point( |
| 260 | chrono::nanoseconds(imu->monotonic_timestamp_ns()))), |
| 261 | gyro, accel); |
| 262 | } |
| 263 | |
| 264 | frc971::IMUValuesT last_value_; |
| 265 | |
| 266 | private: |
| 267 | aos::EventLoop *image_event_loop_; |
| 268 | aos::NodeEventLoopFactory *image_factory_; |
| 269 | aos::EventLoop *imu_event_loop_; |
| 270 | aos::NodeEventLoopFactory *imu_factory_; |
| 271 | |
| 272 | CharucoExtractor charuco_extractor_; |
| 273 | |
| 274 | CalibrationData *data_; |
| 275 | }; |
| 276 | |
| 277 | void Main(int argc, char **argv) { |
| 278 | CalibrationData data; |
| 279 | |
| 280 | { |
| 281 | // Now, accumulate all the data into the data object. |
| 282 | aos::logger::LogReader reader( |
| 283 | aos::logger::SortParts(aos::logger::FindLogs(argc, argv))); |
| 284 | |
| 285 | aos::SimulatedEventLoopFactory factory(reader.configuration()); |
| 286 | reader.Register(&factory); |
| 287 | |
| 288 | CHECK(aos::configuration::MultiNode(reader.configuration())); |
| 289 | |
| 290 | // Find the nodes we care about. |
| 291 | const aos::Node *const roborio_node = |
| 292 | aos::configuration::GetNode(factory.configuration(), "roborio"); |
| 293 | |
| 294 | std::optional<uint16_t> pi_number = aos::network::ParsePiNumber(FLAGS_pi); |
| 295 | CHECK(pi_number); |
| 296 | LOG(INFO) << "Pi " << *pi_number; |
| 297 | const aos::Node *const pi_node = aos::configuration::GetNode( |
| 298 | factory.configuration(), absl::StrCat("pi", *pi_number)); |
| 299 | |
| 300 | LOG(INFO) << "roboRIO " << aos::FlatbufferToJson(roborio_node); |
| 301 | LOG(INFO) << "Pi " << aos::FlatbufferToJson(pi_node); |
| 302 | |
| 303 | std::unique_ptr<aos::EventLoop> roborio_event_loop = |
| 304 | factory.MakeEventLoop("calibration", roborio_node); |
| 305 | std::unique_ptr<aos::EventLoop> pi_event_loop = |
| 306 | factory.MakeEventLoop("calibration", pi_node); |
| 307 | |
| 308 | // Now, hook Calibration up to everything. |
| 309 | Calibration extractor(&factory, pi_event_loop.get(), |
| 310 | roborio_event_loop.get(), FLAGS_pi, &data); |
| 311 | |
| 312 | factory.Run(); |
| 313 | |
| 314 | reader.Deregister(); |
| 315 | } |
| 316 | |
| 317 | LOG(INFO) << "Done with event_loop running"; |
| 318 | // And now we have it, we can start processing it. |
| 319 | data.ReviewData(); |
| 320 | } |
| 321 | |
| 322 | } // namespace vision |
| 323 | } // namespace frc971 |
| 324 | |
| 325 | int main(int argc, char **argv) { |
| 326 | aos::InitGoogle(&argc, &argv); |
| 327 | |
| 328 | frc971::vision::Main(argc, argv); |
| 329 | } |