diff --git a/applications/gui2/src/modules/calibration/calibration.hpp b/applications/gui2/src/modules/calibration/calibration.hpp index 00b33b3ec5288c7f49469f8c51002e2a33efa372..cff1b82759b58661d98324f6b767838ad0ce9c53 100644 --- a/applications/gui2/src/modules/calibration/calibration.hpp +++ b/applications/gui2/src/modules/calibration/calibration.hpp @@ -162,7 +162,7 @@ public: void run(); /** Save calibration */ - void save(); + void saveCalibration(); ftl::calibration::CalibrationData::Intrinsic calibration(); @@ -264,7 +264,7 @@ public: std::string cameraName(int camera); std::vector<std::string> cameraNames(); - ftl::calibration::ExtrinsicCalibration& calib(); + ftl::calibration::ExtrinsicCalibration& calib() { return state_.calib; } // should avoid /** hasFrame(int) must be true before calling getFrame() **/ bool hasFrame(int camera); @@ -412,7 +412,7 @@ public: void run(); /** Save calibration */ - void save(); + void saveCalibration(); /** check if calibration valid: baseline > 0 */ bool calibrated(); diff --git a/applications/gui2/src/modules/calibration/extrinsic.cpp b/applications/gui2/src/modules/calibration/extrinsic.cpp index c1e4c07e3764bcfe62f9eadf50952b81f0cc7c8d..f5b35eed4fd85100e89adf31d1555cfb894ec7b5 100644 --- a/applications/gui2/src/modules/calibration/extrinsic.cpp +++ b/applications/gui2/src/modules/calibration/extrinsic.cpp @@ -83,7 +83,7 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources calibr.intrinsic = CalibrationData::Intrinsic(calibr.intrinsic, sz); // Scale intrinsics - state_.calib.addStereoCamera(calibl.intrinsic, calibr.intrinsic); + state_.calib.addStereoCamera(calibl, calibr); // Update rectification unsigned int idx = state_.cameras.size() - 2; @@ -288,6 +288,7 @@ void ExtrinsicCalibration::updateCalibration() { auto& frame = fs->frames[c]; update[frame_id] = frame.get<CalibrationData>(Channel::CalibrationData); } + update[frame_id].origin = cv::Mat::eye(4, 4, CV_64FC1); update[frame_id].get(c.channel) = state_.calib.calibrationOptimized(i); } @@ -370,12 +371,14 @@ void ExtrinsicCalibration::run() { for (int c = 0; c < cameraCount(); c += 2) { auto l = state_.calib.calibrationOptimized(c); auto r = state_.calib.calibrationOptimized(c + 1); - LOG(INFO) << c << ": rvec " << l.extrinsic.rvec << "; tvec " << l.extrinsic.tvec; - LOG(INFO) << c + 1 << ": rvec " << r.extrinsic.rvec << "; tvec " << r.extrinsic.tvec; stereoRectify(c, c + 1, l, r); + /*LOG(INFO) << c << ": rvec " << l.extrinsic.rvec + << "; tvec " << l.extrinsic.tvec; + LOG(INFO) << c + 1 << ": rvec " << r.extrinsic.rvec + << "; tvec " << r.extrinsic.tvec;*/ LOG(INFO) << "baseline (" << c << ", " << c + 1 << "): " - << cv::norm(l.extrinsic.tvec, r.extrinsic.tvec); + << cv::norm(l.extrinsic.tvec - r.extrinsic.tvec); } } catch (ftl::exception &ex) { @@ -419,6 +422,9 @@ const std::vector<cv::Point2d>& ExtrinsicCalibration::previousPoints(int camera) return state_.points_prev[camera]; } + + + int ExtrinsicCalibration::getFrameCount(int camera) { return state_.calib.points().getCount(unsigned(camera)); } diff --git a/applications/gui2/src/modules/calibration/intrinsic.cpp b/applications/gui2/src/modules/calibration/intrinsic.cpp index 9a3dc06bdef71397f48626edf3a02e05a1a3875d..2fa2e9c54fcf996b95994d07a8b055fd9010473f 100644 --- a/applications/gui2/src/modules/calibration/intrinsic.cpp +++ b/applications/gui2/src/modules/calibration/intrinsic.cpp @@ -204,7 +204,7 @@ bool IntrinsicCalibration::onFrame_(const ftl::data::FrameSetPtr& fs) { } -void IntrinsicCalibration::save() { +void IntrinsicCalibration::saveCalibration() { auto& frame = fs_current_->frames[state_->id.source()]; CalibrationData calib_data = CalibrationData(frame.get<CalibrationData>(Channel::CalibrationData)); auto& calibration = calib_data.get(state_->channel); diff --git a/applications/gui2/src/modules/calibration/stereo.cpp b/applications/gui2/src/modules/calibration/stereo.cpp index 1ac7c9fc10cf39938abd266ab68e862050c971a3..052bf15572b0e97790cfbfed8ac7c4cf90f96230 100644 --- a/applications/gui2/src/modules/calibration/stereo.cpp +++ b/applications/gui2/src/modules/calibration/stereo.cpp @@ -182,7 +182,7 @@ bool StereoCalibration::onFrame_(const ftl::data::FrameSetPtr& fs) { } -void StereoCalibration::save() { +void StereoCalibration::saveCalibration() { auto fs = std::atomic_load(&(fs_current_)); setCalibration((*fs)[state_->id.source()], state_->calib); } diff --git a/applications/gui2/src/views/calibration/extrinsicview.cpp b/applications/gui2/src/views/calibration/extrinsicview.cpp index b59f2ffa46b31802a5fa7b60c757439575324f29..2fe214712e4ba993dc071276130696a052a2ee8b 100644 --- a/applications/gui2/src/views/calibration/extrinsicview.cpp +++ b/applications/gui2/src/views/calibration/extrinsicview.cpp @@ -368,6 +368,84 @@ void ExtrinsicCalibrationView::CalibrationWindow::build() { else { flags &= ~ExtrinsicCalibration::Flags::RATIONAL_MODEL; } }); + //////////////////////////////////////////////////////////////////////////// + + new nanogui::Label(wfreeze, "Use available (calibrated) extrinsics for cameras: "); + auto* use_extrinsics = new nanogui::Widget(wfreeze); + use_extrinsics->setLayout(new nanogui::BoxLayout + (nanogui::Orientation::Horizontal, nanogui::Alignment::Minimum)); + for (int n = 0; n < ctrl_->cameraCount(); n++) { + auto* b = new nanogui::Button(use_extrinsics, std::to_string(n)); + b->setFlags(nanogui::Button::Flags::ToggleButton); + b->setPushed(ctrl_->calib().useExtrinsic(n)); + b->setEnabled(ctrl_->calib().calibration(n).extrinsic.valid()); + b->setChangeCallback([this, n](bool v) { + ctrl_->calib().setUseExtrinsic(n, v); + }); + } + { + auto* b = new nanogui::Button(use_extrinsics, "All"); + b->setCallback([this, use_extrinsics](){ + for (int i = 0; i < use_extrinsics->childCount() - 2; i ++) { + auto* b = dynamic_cast<nanogui::Button*>(use_extrinsics->childAt(i)); + b->setPushed(true); + b->changeCallback()(true); + } + }); + } + { + auto* b = new nanogui::Button(use_extrinsics, "None"); + b->setCallback([this, use_extrinsics](){ + for (int i = 0; i < use_extrinsics->childCount() - 2; i ++) { + auto* b = dynamic_cast<nanogui::Button*>(use_extrinsics->childAt(i)); + b->setPushed(false); + b->changeCallback()(false); + } + }); + } + + //////////////////////////////////////////////////////////////////////////// + // TODO: selecting camera should also enable use existing above for same c + + new nanogui::Label(wfreeze, "Fix extrinsics for cameras: "); + auto* fix_extrinsics = new nanogui::Widget(wfreeze); + fix_extrinsics->setLayout(new nanogui::BoxLayout + (nanogui::Orientation::Horizontal, nanogui::Alignment::Minimum)); + for (int n = 0; n < ctrl_->cameraCount(); n++) { + auto* b = new nanogui::Button(fix_extrinsics, std::to_string(n)); + b->setFlags(nanogui::Button::Flags::ToggleButton); + b->setEnabled(ctrl_->calib().useExtrinsic(n)); + b->setPushed(ctrl_->calib().options().fix_camera_extrinsic.count(n)); + b->setChangeCallback([this, n](bool v){ + if (v) { + ctrl_->calib().options().fix_camera_extrinsic.insert(n); + } + else { + ctrl_->calib().options().fix_camera_extrinsic.erase(n); + } + }); + } + { + auto* b = new nanogui::Button(fix_extrinsics, "All"); + b->setCallback([this, fix_extrinsics](){ + for (int i = 0; i < fix_extrinsics->childCount() - 2; i ++) { + auto* b = dynamic_cast<nanogui::Button*>(fix_extrinsics->childAt(i)); + b->setPushed(true); + b->changeCallback()(true); + } + }); + } + { + auto* b = new nanogui::Button(fix_extrinsics, "None"); + b->setCallback([this, fix_extrinsics](){ + for (int i = 0; i < fix_extrinsics->childCount() - 2; i ++) { + auto* b = dynamic_cast<nanogui::Button*>(fix_extrinsics->childAt(i)); + b->setPushed(false); + b->changeCallback()(false); + } + }); + } + /* Needs thinking: visualize visibility graph? Use earlier alignment (if * some of the cameras already calibrated), do elsewhere? */ @@ -509,7 +587,7 @@ private: std::set<int> rows_; std::map<int, nanogui::Color> colors_; - int n_colors_ = 16; + int n_colors_ = 8; float alpha_threshold_ = 2.0f; public: @@ -568,14 +646,14 @@ void StereoCalibrationImageView::draw(NVGcontext* ctx) { nvgStrokeWidth(ctx, swidth); nvgStroke(ctx); - if (swidth*0.5f > alpha_threshold_) { + /*if (swidth*0.5f > alpha_threshold_) { nvgBeginPath(ctx); nvgMoveTo(ctx, x, p.y() - swidth*0.5f); nvgLineTo(ctx, x + w, p.y() - swidth*0.5f); nvgStrokeColor(ctx, nvgRGBA(0, 0, 0, 196)); nvgStrokeWidth(ctx, 1.0f); nvgStroke(ctx); - } + }*/ nvgResetScissor(ctx); y_im += h; } diff --git a/applications/gui2/src/views/calibration/intrinsicview.cpp b/applications/gui2/src/views/calibration/intrinsicview.cpp index 6129ce48db1e53f46880aae553d4a8cd0be46e2d..cfa9941adf3d88c07d0be8d4076a9259d925cc91 100644 --- a/applications/gui2/src/views/calibration/intrinsicview.cpp +++ b/applications/gui2/src/views/calibration/intrinsicview.cpp @@ -492,7 +492,7 @@ IntrinsicCalibrationView::ResultWindow::ResultWindow(nanogui::Widget* parent, In bsave_ = new nanogui::Button(this, "Save"); bsave_->setCallback([button = bsave_, ctrl = ctrl_](){ - ctrl->save(); + ctrl->saveCalibration(); button->setCaption("Saved"); button->setEnabled(false); }); diff --git a/applications/gui2/src/views/calibration/stereoview.cpp b/applications/gui2/src/views/calibration/stereoview.cpp index 3dccafd41b9108037fc9ac8b56ebac0cd5c5fd69..e67eb269c6d0a2bfae834fa5a18d68080c2c44ea 100644 --- a/applications/gui2/src/views/calibration/stereoview.cpp +++ b/applications/gui2/src/views/calibration/stereoview.cpp @@ -376,7 +376,7 @@ StereoCalibrationView::ResultWindow::ResultWindow(nanogui::Widget* parent, Stere bsave_ = new nanogui::Button(this, "Save"); bsave_->setCallback([button = bsave_, ctrl = ctrl_](){ - ctrl->save(); + ctrl->saveCalibration(); button->setCaption("Saved"); button->setEnabled(false); }); diff --git a/components/calibration/include/ftl/calibration/extrinsic.hpp b/components/calibration/include/ftl/calibration/extrinsic.hpp index a9f7208e39ff158cb830e0e8900b5d263defe2b6..645674ddba6208fcc5f35fa9e014e87452aee511 100644 --- a/components/calibration/include/ftl/calibration/extrinsic.hpp +++ b/components/calibration/include/ftl/calibration/extrinsic.hpp @@ -158,17 +158,10 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1, class ExtrinsicCalibration { public: - /** add a single (uncalibrated) camera. Returns index of camera. */ - unsigned int addCamera(const CalibrationData::Intrinsic &); - - /** add a single calibrated camera (if valid calibration). Returns index of camera. */ + /** add a single camera (if valid calibration). Returns index of camera. */ unsigned int addCamera(const CalibrationData::Calibration &); - /** Add a stereo camera pair. Pairs always use other cameras to estimate - * initial pose. Returns index of first camera. */ - unsigned int addStereoCamera(const CalibrationData::Intrinsic &, const CalibrationData::Intrinsic &); - - /** Add calibrated stereo camera (if contains valid calibration) */ + /** Add stereo camera */ unsigned int addStereoCamera(const CalibrationData::Calibration &, const CalibrationData::Calibration &); const CalibrationData::Intrinsic& intrinsic(unsigned int c); @@ -187,11 +180,16 @@ public: /* set bundle adjustment options */ void setOptions(ftl::calibration::BundleAdjustment::Options options) { options_ = options; } - ftl::calibration::BundleAdjustment::Options options() { return options_; } + ftl::calibration::BundleAdjustment::Options& options() { return options_; } /** Number of cameras added */ unsigned int camerasCount() { return calib_.size(); } + /** use existing extrinsic calibration for camera */ + void setUseExtrinsic(unsigned int c, bool v) { is_calibrated_.at(c) = v; } + /** is existing extrinsic parameters used for given camera */ + bool useExtrinsic(unsigned int c) { return is_calibrated_.at(c); }; + /** status message */ std::string status(); @@ -208,17 +206,29 @@ public: MSGPACK_DEFINE(points_, mask_, pairs_, calib_, is_calibrated_); protected: - /** Initial pairwise calibration and triangulation. */ + /** Calculate initial pose and triangulate points for two cameras **/ + void calculatePairPose(unsigned int c1, unsigned int c2); + + /** Only triangulate points using existing calibration */ + void triangulate(unsigned int c1, unsigned int c2); + + /** (1) Initial pairwise calibration and triangulation. */ void calculatePairPoses(); - /** Calculate initial poses from pairs */ + /** (2) Calculate initial poses from pairs for non calibrated cameras */ void calculateInitialPoses(); - /** Bundle adjustment on initial poses and triangulations. */ + /** (3) Bundle adjustment on initial poses and triangulations. */ double optimize(); + /** Select optimal camera for chains. Optimal camera has most visibility and + * is already calibrated (if any initial calibrations included). + */ + int selectOptimalCamera(); + private: void updateStatus_(std::string); + std::vector<CalibrationData::Calibration> calib_; std::vector<CalibrationData::Calibration> calib_optimized_; ftl::calibration::BundleAdjustment::Options options_; @@ -226,13 +236,17 @@ private: CalibrationPoints<double> points_; std::set<std::pair<unsigned int, unsigned int>> mask_; std::map<std::pair<unsigned int, unsigned int>, std::tuple<cv::Mat, cv::Mat, double>> pairs_; - unsigned int c_ref_; // true if camera already has valid calibration; initial pose estimation is // skipped (and points are directly triangulated) std::vector<bool> is_calibrated_; - int min_points_ = 64; // minimum number of points required for pair calibration + // prune points which have higher reprojection error than given threshold + // and re-run bundle adjustment. (might be able to remove some problematic + // observations from extreme angles etc.) + std::vector<double> prune_observations_ = {2.5, 2.0, 2.0, 1.0}; + + int min_obs_ = 64; // minimum number of observations required for pair calibration // TODO: add map {c1,c2}Â for existing calibration which is used if available. // std::shared_ptr<std::string> status_; diff --git a/components/calibration/include/ftl/calibration/optimize.hpp b/components/calibration/include/ftl/calibration/optimize.hpp index b05d9f9d3dc1cde858a706ed1404e8886162601d..a92f0354d471ae0df6192c8ecc92879af2b4b54a 100644 --- a/components/calibration/include/ftl/calibration/optimize.hpp +++ b/components/calibration/include/ftl/calibration/optimize.hpp @@ -67,7 +67,6 @@ struct Camera { const static int n_parameters = 18; const static int n_distortion_parameters = 8; - bool quaternion = false; double data[n_parameters] = {0.0}; enum Parameter { @@ -136,17 +135,17 @@ public: bool use_nonmonotonic_steps = false; // use quaternion rotation - bool use_quaternion = false; + bool use_quaternion = true; // fix_camera_extrinsic and fix_camera_intrinsic overlap with some of // the generic options. The more generic setting is always used, the // specific extrinsic/intrinsic options are applied on top of those. // fix extrinsic paramters for cameras - std::vector<int> fix_camera_extrinsic = {}; + std::set<int> fix_camera_extrinsic = {}; // fix intrinsic paramters for cameras - std::vector<int> fix_camera_intrinsic = {}; + std::set<int> fix_camera_intrinsic = {}; bool fix_focal = false; bool fix_principal_point = false; diff --git a/components/calibration/include/ftl/calibration/structures.hpp b/components/calibration/include/ftl/calibration/structures.hpp index fffbe503508e29b2fbe88fe4c72566bf97c63206..37a9f3d97b181f1450e23de5f0c186db26ff2305 100644 --- a/components/calibration/include/ftl/calibration/structures.hpp +++ b/components/calibration/include/ftl/calibration/structures.hpp @@ -116,6 +116,9 @@ struct CalibrationData { Intrinsic intrinsic; Extrinsic extrinsic; + /** 4x4 projection matrix */ + cv::Mat matrix(); + MSGPACK_DEFINE(intrinsic, extrinsic); }; diff --git a/components/calibration/src/extrinsic.cpp b/components/calibration/src/extrinsic.cpp index d32961040338efec217048c1017b84588b12d443..25c79e4ff7a7303a1986a96f87eed5f1c6c63613 100644 --- a/components/calibration/src/extrinsic.cpp +++ b/components/calibration/src/extrinsic.cpp @@ -290,6 +290,24 @@ int recoverPose(const cv::Mat &E, const std::vector<cv::Point2d> &_points1, distanceThresh, cv::noArray(), triangulatedPoints); } +static double scalePoints(const std::vector<cv::Point3d> &object_points, const cv::Mat& points_in, std::vector<cv::Point3d> &points_out) { + + points_out.clear(); + points_out.reserve(points_in.cols); + // convert from homogenous coordinates + for (int col = 0; col < points_in.cols; col++) { + CHECK_NE(points_in.at<double>(3, col), 0); + cv::Point3d p = cv::Point3d(points_in.at<double>(0, col), + points_in.at<double>(1, col), + points_in.at<double>(2, col)) + / points_in.at<double>(3, col); + points_out.push_back(p); + } + + double s = ftl::calibration::optimizeScale(object_points, points_out); + return s; +} + double calibratePair(const cv::Mat &K1, const cv::Mat &D1, const cv::Mat &K2, const cv::Mat &D2, const std::vector<cv::Point2d> &points1, @@ -297,27 +315,16 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1, const std::vector<cv::Point3d> &object_points, cv::Mat &R, cv::Mat &t, std::vector<cv::Point3d> &points_out, bool optimize) { + // FM_8POINT should be good enough if there are no outliers in points1 and + // points2 (all correspondences are correct) cv::Mat F = cv::findFundamentalMat(points1, points2, cv::noArray(), cv::FM_8POINT); cv::Mat E = K2.t() * F * K1; cv::Mat points3dh; - // distanceThresh unit? + // distanceThresh? recoverPose(E, points1, points2, K1, K2, R, t, 1000.0, points3dh); - points_out.clear(); - points_out.reserve(points3dh.cols); - - // convert from homogenous coordinates - for (int col = 0; col < points3dh.cols; col++) { - CHECK_NE(points3dh.at<double>(3, col), 0); - cv::Point3d p = cv::Point3d(points3dh.at<double>(0, col), - points3dh.at<double>(1, col), - points3dh.at<double>(2, col)) - / points3dh.at<double>(3, col); - points_out.push_back(p); - } - - double s = ftl::calibration::optimizeScale(object_points, points_out); + double s = scalePoints(object_points, points3dh, points_out); t = t * s; auto params1 = Camera(K1, D1, cv::Mat::eye(3, 3, CV_64FC1), cv::Mat::zeros(3, 1, CV_64FC1), {0, 0}); @@ -353,14 +360,6 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1, // ==== Extrinsic Calibration ================================================== -unsigned int ExtrinsicCalibration::addCamera(const CalibrationData::Intrinsic &c) { - unsigned int idx = calib_.size(); - calib_.push_back({c, {}}); - calib_optimized_.push_back(calib_.back()); - is_calibrated_.push_back(false); - return idx; -} - unsigned int ExtrinsicCalibration::addCamera(const CalibrationData::Calibration &c) { unsigned int idx = calib_.size(); calib_.push_back(c); @@ -369,18 +368,6 @@ unsigned int ExtrinsicCalibration::addCamera(const CalibrationData::Calibration return idx; } -unsigned int ExtrinsicCalibration::addStereoCamera(const CalibrationData::Intrinsic &c1, const CalibrationData::Intrinsic &c2) { - unsigned int idx = calib_.size(); - calib_.push_back({c1, {}}); - calib_optimized_.push_back(calib_.back()); - calib_.push_back({c2, {}}); - calib_optimized_.push_back(calib_.back()); - is_calibrated_.push_back(false); - is_calibrated_.push_back(false); - mask_.insert({idx, idx + 1}); - return idx; -} - unsigned int ExtrinsicCalibration::addStereoCamera(const CalibrationData::Calibration &c1, const CalibrationData::Calibration &c2) { unsigned int idx = calib_.size(); calib_.push_back({c1.intrinsic, c1.extrinsic}); @@ -403,12 +390,86 @@ void ExtrinsicCalibration::updateStatus_(std::string str) { std::atomic_store(&status_, std::make_shared<std::string>(str)); } +void ExtrinsicCalibration::calculatePairPose(unsigned int c1, unsigned int c2) { + + // calculate paramters and update triangulation + + cv::Mat K1 = calib_[c1].intrinsic.matrix(); + cv::Mat distCoeffs1 = calib_[c1].intrinsic.distCoeffs.Mat(); + cv::Mat K2 = calib_[c2].intrinsic.matrix(); + cv::Mat distCoeffs2 = calib_[c2].intrinsic.distCoeffs.Mat(); + auto pts = points().getPoints({c1, c2}, 0); + auto object = points().getObject(0); + cv::Mat R, t; + std::vector<cv::Point3d> points3d; + auto rmse = calibratePair(K1, distCoeffs1, K2, distCoeffs2, + pts[0], pts[1], object, R, t, points3d, true); + + // debug info + LOG(INFO) << "RMSE (cameras " << c1 << " & " << c2 << "): " << rmse; + + points().setTriangulatedPoints(c1, c2, points3d); + + pairs_[{c1, c2}] = {R, t, rmse}; + + cv::Mat R_i, t_i; + R.copyTo(R_i); + t.copyTo(t_i); + transform::inverse(R_i, t_i); + pairs_[{c2, c1}] = {R_i, t_i, rmse}; +} + +void ExtrinsicCalibration::triangulate(unsigned int c1, unsigned int c2) { + + cv::Mat T, R, t, R_i, t_i; + + T = calib_[c1].extrinsic.matrix() * + transform::inverse(calib_[c2].extrinsic.matrix()); + transform::getRotationAndTranslation(T, R_i, t_i); + + T = calib_[c2].extrinsic.matrix() * + transform::inverse(calib_[c1].extrinsic.matrix()); + transform::getRotationAndTranslation(T, R, t); + + pairs_[{c1, c1}] = {R, t, NAN}; + pairs_[{c2, c1}] = {R_i, t_i, NAN}; + + auto pts = points().getPoints({c1, c2}, 0); + std::vector<cv::Point3d> pointsw; + + const auto& calib1 = calib_[c1]; + const auto& calib2 = calib_[c2]; + + CHECK_EQ(pts[0].size(), pts[1].size()); + + cv::Mat pts1(1, pts[0].size(), CV_64FC2, pts[0].data()); + cv::Mat pts2(1, pts[1].size(), CV_64FC2, pts[1].data()); + cv::Mat out(1, pts[0].size(), CV_64FC2); + + // Undistort points first. Note: undistortPoints() returns points in + // normalized coordinates, therefore projection matrices for + // cv::triangulatePoints() only include extrinsic parameters. + std::vector<cv::Point2d> pts1u, pts2u; + cv::undistortPoints(pts1, pts1u, calib1.intrinsic.matrix(), calib1.intrinsic.distCoeffs.Mat()); + cv::undistortPoints(pts2, pts2u, calib2.intrinsic.matrix(), calib2.intrinsic.distCoeffs.Mat()); + + cv::Mat P1 = cv::Mat::eye(3, 4, CV_64FC1); + cv::Mat P2 = T(cv::Rect(0, 0, 4, 3)); + + // documentation claims cv::triangulatePoints() requires floats; however + // seems to only work with doubles (documentation outdated?). + // According to https://stackoverflow.com/a/16299909 cv::triangulatePoints() + // implements least squares method described in H&Z p312 + cv::triangulatePoints(P1, P2, pts1u, pts2u, out); + // scalePoints() converts to non-homogenous coordinates and estimates scale + scalePoints(points().getObject(0), out, pointsw); + points().setTriangulatedPoints(c1, c2, pointsw); +} + void ExtrinsicCalibration::calculatePairPoses() { - const auto& visibility = points_.visibility(); // Calibrate all pairs. TODO: might be expensive if number of cameras is high - // if not performed for all pairs, remaining non-triangulated poits have to - // be separately triangulated later. + // if not performed for all pairs. int i = 1; int i_max = (camerasCount() * camerasCount()) / 2 + 1; @@ -417,7 +478,9 @@ void ExtrinsicCalibration::calculatePairPoses() { for (unsigned int c2 = c1; c2 < camerasCount(); c2++) { updateStatus_( "Calculating pose for pair " + - std::to_string(i++) + " of " + std::to_string(i_max)); + std::to_string(i++) + " of " + std::to_string(i_max) + + " and triangulating points"); + if (c1 == c2) { pairs_[{c1, c2}] = { cv::Mat::eye(cv::Size(3, 3), CV_64FC1), @@ -427,51 +490,52 @@ void ExtrinsicCalibration::calculatePairPoses() { continue; } - if (mask_.count({c1, c2}) > 0 ) { continue; } - if (pairs_.find({c1, c2}) != pairs_.end()) { continue; } + if (pairs_.find({c1, c2}) != pairs_.end()) { + LOG(WARNING) << "pair already processed (this shold not happen)"; + continue; + } // require minimum number of visible points - if (visibility.count(c1, c2) < min_points_) { + if (points().visibility().count(c1, c2) < min_obs_) { LOG(WARNING) << "skipped pair (" << c1 << ", " << c2 << "), not enough points"; continue; } - // calculate paramters and update triangulation - - cv::Mat K1 = calib_[c1].intrinsic.matrix(); - cv::Mat distCoeffs1 = calib_[c1].intrinsic.distCoeffs.Mat(); - cv::Mat K2 = calib_[c2].intrinsic.matrix(); - cv::Mat distCoeffs2 = calib_[c2].intrinsic.distCoeffs.Mat(); - auto pts = points().getPoints({c1, c2}, 0); - auto object = points().getObject(0); - cv::Mat R, t; - std::vector<cv::Point3d> points3d; - auto rmse = calibratePair(K1, distCoeffs1, K2, distCoeffs2, - pts[0], pts[1], object, R, t, points3d, true); - - // debug info - LOG(INFO) << "RMSE (cameras " << c1 << " & " << c2 << "): " << rmse; - - points().setTriangulatedPoints(c1, c2, points3d); - - pairs_[{c1, c2}] = {R, t, rmse}; - - cv::Mat R_i, t_i; - R.copyTo(R_i); - t.copyTo(t_i); - transform::inverse(R_i, t_i); - pairs_[{c2, c1}] = {R_i, t_i, rmse}; + if (is_calibrated_[c1] && is_calibrated_[c2]) { + LOG(INFO) << "using existing pose for cameras " << c1 << " and " + << c2 << "(only triangulating points)"; + triangulate(c1, c2); + } + else { + if (mask_.count({c1, c2}) > 0 ) { continue; } + calculatePairPose(c1, c2); + } }} } +int ExtrinsicCalibration::selectOptimalCamera() { + // Pick optimal camera: most views of calibration pattern. If existing + // calibration is used, reference camera must already be calibrated. + int c = 0; + int calibrated_c_ref_max = 0; + for (unsigned int i = 0; i < is_calibrated_[i]; i++) { + if (is_calibrated_[i] && points().getCount(i) > calibrated_c_ref_max) { + calibrated_c_ref_max = points().getCount(i); + c = i; + } + } + if (!calibrated_c_ref_max == 0) { + c = points().visibility().argmax(); + } + return c; +} + void ExtrinsicCalibration::calculateInitialPoses() { updateStatus_("Initial poses from chained transformations"); // mask stereo cameras (do not pairwise calibrate a stereo pair; unreliable) auto visibility = points_.visibility(); - for (const auto& m: mask_) { - visibility.mask(m.first, m.second); - } + for (const auto& [c1, c2]: mask_) { visibility.mask(c1, c2); } // mask cameras which did not have enough points TODO: triangulation later // would still be useful (calculate initial poses, then triangulate) @@ -480,20 +544,21 @@ void ExtrinsicCalibration::calculateInitialPoses() { if (pairs_.count({c1, c2}) == 0) { visibility.mask(c1, c2); } - - // mask bad pairs (high rmse) - /*if (std::get<2>(pairs_.at({c1, c2})) > 16.0) { - visibility.mask(c1, c2); - }*/ }} - // pick optimal camera: most views of calibration pattern - c_ref_ = visibility.argmax(); + // select optimal camera to calculate chains to. TODO: if any of the + // cameras is already calibrated, use most visible calibrated camera as + // target camera. + auto c_ref = selectOptimalCamera(); - auto paths = visibility.shortestPath(c_ref_); + auto paths = visibility.shortestPath(c_ref); for (unsigned int c = 0; c < camerasCount(); c++) { - if (c == c_ref_) { continue; } + if (is_calibrated_[c]) { + // already calibrated. skip chain + continue; + } + if (c == unsigned(c_ref)) { continue; } cv::Mat R_chain = cv::Mat::eye(cv::Size(3, 3), CV_64FC1); cv::Mat t_chain = cv::Mat(cv::Size(1, 3), CV_64FC1, cv::Scalar(0.0)); @@ -517,7 +582,6 @@ void ExtrinsicCalibration::calculateInitialPoses() { while(path.size() > 1); // note: direction of chain in the loop (ref to target transformation) - calib_[c].extrinsic = CalibrationData::Extrinsic(R_chain, t_chain).inverse(); } @@ -533,6 +597,10 @@ static std::vector<bool> visibility(unsigned int ncameras, uint64_t visible) { /* absolute difference between min and max for each set of coordinates */ static cv::Point3d absdiff(const std::vector<double> &xs, const std::vector<double> &ys, const std::vector<double> &zs) { + if (xs.size() < 2) { + return {0.0, 0.0, 0.0}; + } + double minx = INFINITY; double maxx = -INFINITY; for (auto x : xs) { @@ -551,11 +619,24 @@ static cv::Point3d absdiff(const std::vector<double> &xs, const std::vector<doub minz = std::min(minz, z); maxz = std::max(maxz, z); } - return {abs(minx - maxx), abs(miny - maxy), abs(minz - maxz)}; + cv::Point3d diff = {abs(minx - maxx), abs(miny - maxy), abs(minz - maxz)}; + return diff; } double ExtrinsicCalibration::optimize() { + // triangulate points for stereo pairs (all points triangulated after this) + updateStatus_("Triangulating remaining points"); + for (const auto& [c1, c2]: mask_) { + if (points().visibility().count(c1, c2) >= min_obs_) { + triangulate(c1, c2); + } + else { + LOG(INFO) << "Skipping triangulation for pair " << c1 << ", " << c2; + } + } + + // Build BA BundleAdjustment ba; std::vector<Camera> cameras; std::vector<cv::Mat> T; // camera to world @@ -579,7 +660,6 @@ double ExtrinsicCalibration::optimize() { // triangulated multiple times: use median values. Note T[] contains // inverse transformations, as points are transformed from camera to world // (instead the other way around by parameters in cameras[]). - updateStatus_("Calculating points in world coordinates"); // NOTE: above CalibrationPoints datastructure not optimal regarding how @@ -626,11 +706,9 @@ double ExtrinsicCalibration::optimize() { unsigned int n = px.size(); unsigned int m = n / 2; - if (m == 0) { + if (n == 0) { n_points_missing++; break; - // not triangulated (see earlier steps) - // TODO: triangulate here } if (n % 2 == 0 && n > 1) { // mean of two points if number of points even @@ -657,19 +735,17 @@ double ExtrinsicCalibration::optimize() { // was very low quality (more than % bad points) LOG(ERROR) << "Large variation in "<< n_points_bad << " " "triangulated points. Are initial intrinsic parameters " - "good?"; + "good? If initial camera poses were used, try again " + "without using existing values."; } if (float(n_points_missing)/float(n_points - n_points_bad) > threhsold_warning_) { - // low number of points; most points only visible in pairs? + // this should not happen any more (all points should be triangulated). LOG(WARNING) << "Large number of points skipped. Are there enough " - "visible points between stereo camera pairs? (TODO: " - "implement necessary triangulation after pair " - "calibration)"; + "visible points between stereo camera pairs?"; } updateStatus_("Bundle adjustment"); - options_.fix_camera_extrinsic = {int(c_ref_)}; options_.verbose = true; options_.max_iter = 250; // should converge much earlier @@ -679,10 +755,19 @@ double ExtrinsicCalibration::optimize() { LOG(INFO) << "fix distortion: " << (options_.fix_distortion ? "yes" : "no"); ba.run(options_); - LOG(INFO) << "removed points: " << ba.removeObservations(2.0); - ba.run(options_); - LOG(INFO) << "removed points: " << ba.removeObservations(1.0); - ba.run(options_); + + int n_removed = 0; + for (const auto& t : prune_observations_) { + n_removed += ba.removeObservations(t); + if (float(n_removed)/float(n_points) > threhsold_warning_) { + LOG(WARNING) << "significant number of observations removed"; + break; + } + else { + LOG(INFO) << "removed observations: " << n_removed; + ba.run(options_); + } + } calib_optimized_.resize(calib_.size()); rmse_.resize(calib_.size()); diff --git a/components/calibration/src/optimize.cpp b/components/calibration/src/optimize.cpp index 26a28d5c99c6067c386fc33154f43c11311ca616..73788380cb14d09f0eaa50d4791a5e52b6b25a6e 100644 --- a/components/calibration/src/optimize.cpp +++ b/components/calibration/src/optimize.cpp @@ -157,8 +157,7 @@ Mat Camera::distortionCoefficients() const { Mat Camera::rvec() const { cv::Mat rvec(cv::Size(3, 1), CV_64FC1); CHECK_EQ(rvec.step1(), 3); - ceres::QuaternionToAngleAxis(data + Parameter::ROTATION, - (double*)(rvec.data)); + ceres::QuaternionToAngleAxis(data + Parameter::ROTATION, (double*)(rvec.data)); return rvec; } @@ -361,7 +360,7 @@ double ftl::calibration::optimizeScale(const vector<Point3d> &object_points, vec vector<double> d; ceres::Problem problem; - auto loss_function = new ceres::HuberLoss(1.0); + ceres::LossFunction* loss_function = nullptr; // use all pairwise distances for (size_t i = 0; i < object_points.size(); i++) { @@ -612,7 +611,7 @@ void BundleAdjustment::_buildBundleAdjustmentProblem(ceres::Problem &problem, co } void BundleAdjustment::_buildProblem(ceres::Problem &problem, const BundleAdjustment::Options &options) { - + CHECK(options.use_quaternion) << "Only quaternion rotations are supported"; _buildBundleAdjustmentProblem(problem, options); _buildLengthProblem(problem, options); } diff --git a/components/calibration/src/structures.cpp b/components/calibration/src/structures.cpp index 8c2c9ec5f5457d7c20fb92d8dc784fe5b276e81c..de8b34bee70e91fb67fc2098c5e51ce66773c783 100644 --- a/components/calibration/src/structures.cpp +++ b/components/calibration/src/structures.cpp @@ -87,7 +87,7 @@ cv::Mat CalibrationData::Intrinsic::matrix(cv::Size size) const { } bool CalibrationData::Intrinsic::valid() const { - return (resolution == cv::Size{0, 0}); + return (resolution != cv::Size{0, 0}); } //////////////////////////////////////////////////////////////////////////////// @@ -247,3 +247,16 @@ CalibrationData::Calibration& CalibrationData::get(ftl::codecs::Channel channel) bool CalibrationData::hasCalibration(ftl::codecs::Channel channel) const { return data.count(channel) != 0; } + +// ==== CalibrationData::Calibration =========================================== +#include <loguru.hpp> +cv::Mat CalibrationData::Calibration::matrix() { + if (!intrinsic.valid() || !extrinsic.valid()) { + throw ftl::exception("Invalid calibration"); + } + + cv::Mat P = extrinsic.matrix(); + cv::Mat R = P(cv::Rect(0, 0, 3, 3)); + R = intrinsic.matrix() * R; + return P; +}