diff --git a/applications/reconstruct/include/ftl/registration.hpp b/applications/reconstruct/include/ftl/registration.hpp index 4c37d63821f3e81df1e841e2e5ad16862d5489b1..d75d96efae5b168dd5a4065acc54d19d8d87f1ed 100644 --- a/applications/reconstruct/include/ftl/registration.hpp +++ b/applications/reconstruct/include/ftl/registration.hpp @@ -2,6 +2,7 @@ #define _FTL_RECONSTRUCT_REGISTRATION_HPP_ #include <ftl/config.h> +#include <ftl/configurable.hpp> #include <ftl/rgbd.hpp> #include <opencv2/opencv.hpp> @@ -13,23 +14,193 @@ namespace ftl { namespace registration { -/* Find transformation matrix for transforming clouds_source to clouds_target - * Assumes that corresponding points in clouds_source[i] and clouds_target[i] have same indices +void to_json(nlohmann::json &json, std::map<std::string, Eigen::Matrix4f> &transformations); +void from_json(nlohmann::json &json, std::map<std::string, Eigen::Matrix4f> &transformations); + +bool loadTransformations(const std::string &path, std::map<std::string, Eigen::Matrix4f> &data); +bool saveTransformations(const std::string &path, std::map<std::string, Eigen::Matrix4f> &data); + +/** @brief Find transformation matrix for transforming clouds_source to clouds_target. + * Assumes that corresponding points in clouds_source[i] and clouds_target[i] have same indices. */ Eigen::Matrix4f findTransformation( std::vector<pcl::PointCloud<pcl::PointXYZ>::Ptr> clouds_source, std::vector<pcl::PointCloud<pcl::PointXYZ>::Ptr> clouds_target); -/* Convert chessboard corners found with OpenCV's findChessboardCorners to PCL point cloud. - */ +/** @brief Convert chessboard corners found with OpenCV's findChessboardCorners to PCL point cloud. */ pcl::PointCloud<pcl::PointXYZ>::Ptr cornersToPointCloud(const std::vector<cv::Point2f> &corners, const cv::Mat &disp, const ftl::rgbd::CameraParameters &p); -/* Find chessboard corners from image. +/** @brief Find chessboard corners from image and store them in PCL PointCloud. + * @note Corners will be drawn in rgb. + */ +bool findChessboardCorners(cv::Mat &rgb, const cv::Mat &depth, const ftl::rgbd::CameraParameters &p, const cv::Size pattern_size, pcl::PointCloud<pcl::PointXYZ>::Ptr &out, float error_threshold); + +/** + * @brief Abstract class for registration + * + * Registration procedure + * + * @todo Support for multiple features (possibly necessary for other algorithms). + */ +class Registration : public ftl::Configurable { +public: + Registration(nlohmann::json &config); + void addSource(ftl::rgbd::RGBDSource* source); + size_t getSourcesCount() { return sources_.size(); } + + /** + * @brief Run registration loop. + * + * Loop terminates when processData() returns false. + */ + virtual void run(); + + /** + * @brief Find registration transformations. run() must be called before + * findTransformations(). Transformations are in same order as + * sources were added with addSource(). + * + * Transformations are calculated to targetsource if configured. If + * targetsource is not configured or is not found in inputs, which target + * coordinate system is used depends on sub-classes' implementation. + * + * @param Output parameter for transformations. + * @return True if transformations found, otherwise false. + */ + virtual bool findTransformations(std::vector<Eigen::Matrix4f> &data)=0; + + /** + * @brief Overload of findTransformations(). Map keys are source URIs. + */ + virtual bool findTransformations(std::map<std::string, Eigen::Matrix4f> &data); + +protected: + ftl::rgbd::RGBDSource* getSource(size_t idx); + + bool isTargetSourceSet(); + bool isTargetSourceFound(); + bool isTargetSource(ftl::rgbd::RGBDSource* source); + bool isTargetSource(size_t idx); + + /** + * @brief Get index for target source. If target source not defined + * returns 0. If target source is not found, returns 0. + */ + size_t getTargetSourceIdx(); + + /** + * @brief Resets visibility matrix. + */ + void resetVisibility(); + + /** + * @brief Check if there is enough data to cover all the cameras; + * that is visibility is a connected graph. Implemented with BFS. + * Implementations of processData() in sub-classes may use this + * method (but it is not required). + * + * @todo Add support for checking visibility for each different features + * found in all images. Also see findFeatures(). + */ + bool connectedVisibility(); + + /** + * @brief Check if there are enough data for all the sources to calculate + * transformations. Method may also implement additional processing + * for the data. Called once after iteration round. run() stops + * when processData() returns false. + */ + virtual bool processData()=0; + + /** + * @brief Find features in source, return + * + * Called iteratively n times for every input (n * inputs.size()). Exact + * details depend on implementation of processData(). Implementations may + * choose not to set/use visibility information. + * + * @param Input source + * @param Unique index for source provided by run(). Same source will + * always have same idx. Implementations may choose to ignore it. + * @return True/false if feature was found. Used to build adjacency matrix. + */ + virtual bool findFeatures(ftl::rgbd::RGBDSource* source, size_t idx)=0; + + std::vector<std::vector<bool>> visibility_; /*< Adjacency matrix for sources (feature visibility). */ + +private: + std::optional<std::string> target_source_; /*< Reference coordinate system for transformations. */ + std::vector<ftl::rgbd::RGBDSource*> sources_; +}; + +/** + * @brief Registration using chessboard calibration pattern + * + * Parameters from configuration: + * + * patternsize: required + * Chessboard pattern size, inner corners. + * + * maxerror: default +inf + * Maximum allowed error value for pattern detection. MSE error between + * estimated plane and points captured from input. + * + * delay: default 500 + * Milliseconds between captured images. + * + * chain: default false + * Enabling allows camera chaining. In chain mode, pattern is not + * required to be visible in every source. In default (chain: false) + * mode, pattern visibility is required for every source. + * + * iter: default 10 + * Number of iterations for capturing calibration samples. In + * non-chaining mode, each iteration consists of images where patterns + * were detected on every input. In chaining mode each iteration only + * requires camera visibility to be connected. */ -bool findChessboardCorners(cv::Mat &rgb, const cv::Mat &disp, const ftl::rgbd::CameraParameters &p, const cv::Size pattern_size, pcl::PointCloud<pcl::PointXYZ>::Ptr &out, float error_threshold); +class ChessboardRegistration : public Registration { +public: + ChessboardRegistration(nlohmann::json &config); + /** + * @brief Creates new ChessboardRegistration or ChessboardRegistrationChain + * object depending on chain option in config. User of the method + * needs to free the memory. + */ + static ChessboardRegistration* create(nlohmann::json &config); + void run() override; + bool findTransformations(std::vector<Eigen::Matrix4f> &data) override; + +protected: + bool findFeatures(ftl::rgbd::RGBDSource* source, size_t idx) override; + bool processData() override; + cv::Size pattern_size_; + std::vector<std::vector<std::optional<pcl::PointCloud<pcl::PointXYZ>::Ptr>>> data_; + std::vector<Eigen::Matrix4f> T_; + float error_threshold_; + uint delay_; + uint iter_; + uint iter_remaining_; }; + +/** + * @brief Chain registration. Finds visibility and then runs registration. + */ +class ChessboardRegistrationChain : public ChessboardRegistration { +public: + ChessboardRegistrationChain(nlohmann::json &config); + + bool findTransformations(std::vector<Eigen::Matrix4f> &data) override; + +protected: + bool processData() override; + std::vector<Eigen::Matrix4f> T_; + std::vector<std::vector<std::pair<size_t, size_t>>> edges_; }; +} +} + #endif // HAVE_PCL -#endif // _FTL_RECONSTRUCT_REGISTRATION_HPP_ +#endif // _FTL_RECONSTRUCT_REGISTRATION_HPP_ \ No newline at end of file diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp index 3c561795f0598f002f9d3743e02e54c2d26932ed..4ebc6fbd37fc8fe9e68a632f03bac386b940eb8d 100644 --- a/applications/reconstruct/src/main.cpp +++ b/applications/reconstruct/src/main.cpp @@ -60,168 +60,10 @@ using std::unique_lock; using std::vector; -#ifdef HAVE_PCL -using pcl::PointCloud; -using pcl::PointXYZ; -using pcl::PointXYZRGB; -#endif - using cv::Mat; -namespace ftl { -namespace rgbd { -PointCloud<PointXYZRGB>::Ptr createPointCloud(RGBDSource *src); -PointCloud<PointXYZRGB>::Ptr createPointCloud(RGBDSource *src, const cv::Point3_<uchar> &col); -} -} - -PointCloud<PointXYZRGB>::Ptr ftl::rgbd::createPointCloud(RGBDSource *src) { - const double CX = src->getParameters().cx; - const double CY = src->getParameters().cy; - const double FX = src->getParameters().fx; - const double FY = src->getParameters().fy; - - cv::Mat rgb; - cv::Mat depth; - src->getRGBD(rgb, depth); - - pcl::PointCloud<pcl::PointXYZRGB>::Ptr point_cloud_ptr(new pcl::PointCloud<pcl::PointXYZRGB>); - point_cloud_ptr->width = rgb.cols * rgb.rows; - point_cloud_ptr->height = 1; - - for(int i=0;i<rgb.rows;i++) { - const float *sptr = depth.ptr<float>(i); - for(int j=0;j<rgb.cols;j++) { - float d = sptr[j]; // * 1000.0f; - - pcl::PointXYZRGB point; - point.x = (((double)j + CX) / FX) * d; - point.y = (((double)i + CY) / FY) * d; - point.z = d; - - if (point.x == INFINITY || point.y == INFINITY || point.z > 20.0f || point.z < 0.04f) { - point.x = 0.0f; point.y = 0.0f; point.z = 0.0f; - } - - cv::Point3_<uchar> prgb = rgb.at<cv::Point3_<uchar>>(i, j); - uint32_t rgb = (static_cast<uint32_t>(prgb.z) << 16 | static_cast<uint32_t>(prgb.y) << 8 | static_cast<uint32_t>(prgb.x)); - point.rgb = *reinterpret_cast<float*>(&rgb); - - point_cloud_ptr -> points.push_back(point); - } - } - - return point_cloud_ptr; -} - -PointCloud<PointXYZRGB>::Ptr ftl::rgbd::createPointCloud(RGBDSource *src, const cv::Point3_<uchar> &prgb) { - const double CX = src->getParameters().cx; - const double CY = src->getParameters().cy; - const double FX = src->getParameters().fx; - const double FY = src->getParameters().fy; - - cv::Mat rgb; - cv::Mat depth; - src->getRGBD(rgb, depth); - - pcl::PointCloud<pcl::PointXYZRGB>::Ptr point_cloud_ptr(new pcl::PointCloud<pcl::PointXYZRGB>); - point_cloud_ptr->width = rgb.cols * rgb.rows; - point_cloud_ptr->height = 1; - - for(int i=0;i<rgb.rows;i++) { - const float *sptr = depth.ptr<float>(i); - for(int j=0;j<rgb.cols;j++) { - float d = sptr[j]; // * 1000.0f; - - pcl::PointXYZRGB point; - point.x = (((double)j + CX) / FX) * d; - point.y = (((double)i + CY) / FY) * d; - point.z = d; - - if (point.x == INFINITY || point.y == INFINITY || point.z > 20.0f || point.z < 0.04f) { - point.x = 0.0f; point.y = 0.0f; point.z = 0.0f; - } - - uint32_t rgb = (static_cast<uint32_t>(prgb.z) << 16 | static_cast<uint32_t>(prgb.y) << 8 | static_cast<uint32_t>(prgb.x)); - point.rgb = *reinterpret_cast<float*>(&rgb); - - point_cloud_ptr -> points.push_back(point); - } - } - - return point_cloud_ptr; -} - - -#ifdef HAVE_PCL -#include <pcl/filters/uniform_sampling.h> - -/* -class InputMerged { -private: - // todo: Abstract input systems, can also use other 3D inputs (like InputMerged) - vector<InputStereo> inputs_; - vector<Eigen::Matrix4f> T_; - -public: - PointCloud<PointXYZRGB>::Ptr getPointCloud() { - PointCloud<PointXYZRGB>::Ptr result(new PointCloud<PointXYZRGB>); - for (size_t i = 0; i < T_.size(); i++) { - inputs_[i].lock(); - PointCloud<PointXYZRGB>::Ptr cloud = inputs_[i].getPointCloud(); - // Documentation: Can be used with cloud_in equal to cloud_out - pcl::transformPointCloud(*cloud, *cloud, T_[i]); - *result += *cloud; - } - - PointCloud<PointXYZRGB>::Ptr result_sampled(new PointCloud<PointXYZRGB>); - pcl::UniformSampling<PointXYZRGB> uniform_sampling; - uniform_sampling.setInputCloud(result); - uniform_sampling.setRadiusSearch(0.1f); // todo parametrize - uniform_sampling.filter(*result_sampled); - - return result_sampled; - } -}; -*/ -std::map<string, Eigen::Matrix4f> loadRegistration() { - std::map<string, Eigen::Matrix4f> registration; - std::ifstream file(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json"); - - // Use identity transform if no registration - if (!file.is_open()) { - LOG(ERROR) << "Error loading registration from file!"; - return registration; - } - - nlohmann::json load; - file >> load; - - for (auto it = load.begin(); it != load.end(); ++it) { - Eigen::Matrix4f m; - auto data = m.data(); - - for(size_t i = 0; i < 16; i++) {; - data[i] = it.value()[i]; - } - - registration[it.key()] = m; - - } - return registration; -} - -void saveRegistration(std::map<string, Eigen::Matrix4f> registration) { - nlohmann::json save; - for (auto &item : registration) { - auto val = nlohmann::json::array(); - for(size_t j = 0; j < 16; j++) { val.push_back((float) item.second.data()[j]); } - save[item.first] = val; - } - - std::ofstream file(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json"); - file << save; -} +using ftl::registration::loadTransformations; +using ftl::registration::saveTransformations; struct Cameras { RGBDSource *source; @@ -229,96 +71,12 @@ struct Cameras { DepthCameraParams params; }; -template <template<class> class Container> -std::map<string, Eigen::Matrix4f> runRegistration(ftl::Configurable *root, ftl::net::Universe &net, Container<Cameras> &inputs) { - std::map<string, Eigen::Matrix4f> registration; - - // NOTE: uses config["registration"] - - if (!root->getConfig()["registration"].is_object()) { - LOG(FATAL) << "Configuration missing \"registration\" entry!"; - return registration; - } - - int iter = (int) root->getConfig()["registration"]["calibration"]["iterations"]; - int delay = (int) root->getConfig()["registration"]["calibration"]["delay"]; - float max_error = (int) root->getConfig()["registration"]["calibration"]["max_error"]; - string ref_uri = (string) root->getConfig()["registration"]["reference-source"]; - cv::Size pattern_size( (int) root->getConfig()["registration"]["calibration"]["patternsize"][0], - (int) root->getConfig()["registration"]["calibration"]["patternsize"][1]); - - // config["registration"] done - - size_t ref_i; - bool found = false; - for (size_t i = 0; i < inputs.size(); i++) { - if (inputs[i].source->getConfig()["uri"] == ref_uri) { - ref_i = i; - found = true; - break; - } - } - - if (!found) { LOG(ERROR) << "Reference input not found!"; return registration; } - - for (auto &input : inputs) { - cv::namedWindow("Registration: " + (string)input.source->getConfig()["uri"], cv::WINDOW_KEEPRATIO|cv::WINDOW_NORMAL); - } - - // vector for every input: vector of point clouds of patterns - vector<vector<PointCloud<PointXYZ>::Ptr>> patterns(inputs.size()); - - while (iter > 0) { - net.broadcast("grab"); - bool retval = true; // set to false if pattern not found in one of the sources - vector<PointCloud<PointXYZ>::Ptr> patterns_iter(inputs.size()); - - for (size_t i = 0; i < inputs.size(); i++) { - RGBDSource *input = inputs[i].source; - Mat rgb, depth; - - input->getRGBD(rgb, depth); - - if ((rgb.cols == 0) || (depth.cols == 0)) { retval = false; break; } - - retval &= ftl::registration::findChessboardCorners(rgb, depth, input->getParameters(), pattern_size, patterns_iter[i], max_error); - - cv::imshow("Registration: " + (string)input->getConfig()["uri"], rgb); - } - cv::waitKey(delay); - - // every camera found the pattern - if (retval) { - for (size_t i = 0; i < patterns_iter.size(); i++) { - patterns[i].push_back(patterns_iter[i]); - } - iter--; - } - else { LOG(WARNING) << "Pattern not detected on all inputs";} - } - - for (auto &input : inputs) { cv::destroyWindow("Registration: " + (string)input.source->getConfig()["uri"]); } - - for (size_t i = 0; i < inputs.size(); i++) { - Eigen::Matrix4f T; - if (i == ref_i) { - T.setIdentity(); - } - else { - T = ftl::registration::findTransformation(patterns[i], patterns[ref_i]); - } - registration[(string)inputs[i].source->getConfig()["uri"]] = T; - } - saveRegistration(registration); - return registration; -} -#endif - static void run(ftl::Configurable *root) { Universe *net = ftl::create<Universe>(root, "net"); // Make sure connections are complete - //sleep_for(milliseconds(500)); + // sleep_for(milliseconds(500)); + // TODO: possible to do more reliably? net->waitConnections(); std::vector<Cameras> inputs; @@ -328,13 +86,12 @@ static void run(ftl::Configurable *root) { LOG(ERROR) << "No sources configured!"; return; } - //std::vector<Display> displays; // TODO Allow for non-net source types - for (auto &src : *sources) { - RGBDSource *in = ftl::rgbd::RGBDSource::create(src, net); //new ftl::rgbd::NetSource(src, &net); + for (auto &src : *sources) { + RGBDSource *in = ftl::rgbd::RGBDSource::create(src, net); if (!in) { - LOG(ERROR) << "Unrecognised source: " << src; + LOG(ERROR) << "Unrecognized source: " << src; } else { auto &cam = inputs.emplace_back(); cam.source = in; @@ -348,85 +105,78 @@ static void run(ftl::Configurable *root) { cam.params.m_sensorDepthWorldMin = in->getParameters().minDepth; cam.gpu.alloc(cam.params); - //displays.emplace_back(config["display"], src["uri"]); - LOG(INFO) << (string)src["uri"] << " loaded " << cam.params.fx; - } - } - - // Displays and Inputs configured - - // load point cloud transformations - - std::map<string, Eigen::Matrix4f> registration; - if (root->getConfig()["registration"]["calibration"]["run"]) { - registration = runRegistration(root, *net, inputs); - } - else { - LOG(INFO) << "LOAD REG"; - registration = loadRegistration(); - } - - // verify that registration and configuration is valid - // (registration includes every camera) - - bool valid_registration = true; - string ref_input = root->getConfig()["registration"]["reference-source"]; - - // check every camera is included in registration - for (auto &input : inputs) { - string uri = input.source->getConfig()["uri"]; - if (registration.find(uri) == registration.end()) { - valid_registration = false; - LOG(ERROR) << "Camera pose for " + uri + " not found in registration!"; + LOG(INFO) << (string) src["uri"] << " loaded " << cam.params.fx; } } + + // TODO move this block in its own method and add automated tests + // for error cases - if (registration.find(ref_input) == registration.end()) { - LOG(WARNING) << "reference input " + ref_input + " not found in registration"; + std::optional<json_t> merge = root->get<json_t>("merge"); + if (!merge) { + LOG(WARNING) << "Input merging not configured, using only first input in configuration"; + inputs = { inputs[0] }; } - - // if registration not valid, use reference input or first input - if (!valid_registration) { - vector<Cameras> inputs_; - - for (auto &input : inputs) { - if ((string) input.source->getConfig()["uri"] == ref_input) { - inputs_.push_back(input); - break; + + if (inputs.size() > 1) { + std::map<std::string, Eigen::Matrix4f> transformations; + + if ((*merge)["register"]) { + LOG(INFO) << "Registration requested"; + + ftl::registration::Registration *reg = ftl::registration::ChessboardRegistration::create(*merge); + for (auto &input : inputs) { + while(!input.source->isReady()) { std::this_thread::sleep_for(std::chrono::milliseconds(50)); } + reg->addSource(input.source); + } + + reg->run(); + if (reg->findTransformations(transformations)) { + if (!saveTransformations(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json", transformations)) { + LOG(ERROR) << "Error saving new registration"; + }; } + else { + LOG(ERROR) << "Registration failed"; + } + + free(reg); } - - if (inputs_.size() == 0) { - LOG(ERROR) << "Reference input not found in configured inputs, using first input: " + (string) inputs[0].source->getConfig()["uri"]; - inputs_.push_back(inputs[0]); + else { + if (loadTransformations(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json", transformations)) { + LOG(INFO) << "Loaded camera trasformations from file"; + } + else { + LOG(ERROR) << "Error loading camera transformations from file"; + } } - - inputs = inputs_; - inputs[0].source->setPose(Eigen::Matrix4f::Identity()); - } - else { - LOG(INFO) << "Registration valid, assigning poses"; - vector<Eigen::Matrix4f> T; + for (auto &input : inputs) { - LOG(INFO) << (unsigned long long)input.source; - Eigen::Matrix4f RT = (registration.count(input.source->getConfig()["uri"].get<string>()) > 0) ? - registration[(string)input.source->getConfig()["uri"]] : Eigen::Matrix4f::Identity(); - T.push_back(RT); - input.source->setPose(RT); + string uri = input.source->getURI(); + auto T = transformations.find(uri); + if (T == transformations.end()) { + LOG(ERROR) << "Camera pose for " + uri + " not found in transformations"; + LOG(WARNING) << "Using only first configured source"; + // TODO: use target source if configured and found + inputs = { inputs[0] }; + inputs[0].source->setPose(Eigen::Matrix4f::Identity()); + break; + } + input.source->setPose(T->second); } } + + // Displays and Inputs configured LOG(INFO) << "Using sources:"; - for (auto &input : inputs) { LOG(INFO) << " " + (string) input.source->getConfig()["uri"]; } + for (auto &input : inputs) { LOG(INFO) << " " + (string) input.source->getURI(); } - //vector<PointCloud<PointXYZRGB>::Ptr> clouds(inputs.size()); ftl::rgbd::Display *display = ftl::create<ftl::rgbd::Display>(root, "display"); ftl::rgbd::VirtualSource *virt = ftl::create<ftl::rgbd::VirtualSource>(root, "virtual", net); ftl::voxhash::SceneRep *scene = ftl::create<ftl::voxhash::SceneRep>(root, "voxelhash"); virt->setScene(scene); display->setSource(virt); - unsigned char frameCount = 0; bool paused = false; @@ -470,7 +220,6 @@ static void run(ftl::Configurable *root) { } frameCount++; - display->update(); } } diff --git a/applications/reconstruct/src/registration.cpp b/applications/reconstruct/src/registration.cpp index 37be030b9cdca8aa22bb008f75e3e54cc428660b..370fcf59dbc3838b5c0de66624763ab26d994df9 100644 --- a/applications/reconstruct/src/registration.cpp +++ b/applications/reconstruct/src/registration.cpp @@ -26,12 +26,64 @@ namespace ftl { namespace registration { using ftl::rgbd::CameraParameters; +using ftl::rgbd::RGBDSource; + +using std::string; using std::vector; +using std::pair; +using std::map; +using std::optional; + using pcl::PointCloud; using pcl::PointXYZ; using pcl::PointXYZRGB; using cv::Mat; +using Eigen::Matrix4f; + +void from_json(nlohmann::json &json, map<string, Matrix4f> &transformations) { + for (auto it = json.begin(); it != json.end(); ++it) { + Eigen::Matrix4f m; + auto data = m.data(); + for(size_t i = 0; i < 16; i++) { data[i] = it.value()[i]; } + transformations[it.key()] = m; + } +} + +void to_json(nlohmann::json &json, map<string, Matrix4f> &transformations) { + for (auto &item : transformations) { + auto val = nlohmann::json::array(); + for(size_t i = 0; i < 16; i++) { val.push_back((float) item.second.data()[i]); } + json[item.first] = val; + } +} + +bool loadTransformations(const string &path, map<string, Matrix4f> &data) { + std::ifstream file(path); + if (!file.is_open()) { + LOG(ERROR) << "Error loading transformations from file " << path; + return false; + } + + nlohmann::json json_registration; + file >> json_registration; + from_json(json_registration, data); + return true; +} + +bool saveTransformations(const string &path, map<string, Matrix4f> &data) { + nlohmann::json data_json; + to_json(data_json, data); + std::ofstream file(path); + + if (!file.is_open()) { + LOG(ERROR) << "Error writing transformations to file " << path; + return false; + } + + file << std::setw(4) << data_json; + return true; +} // todo template: fitPlane<typename T>(PointCloud<T> cloud_in, PointCloud<T> cloud_out) // @@ -76,11 +128,11 @@ float fitPlaneError(PointCloud<PointXYZ>::Ptr cloud_in, float distance_threshold score += d * d; } - return score / cloud_proj.size(); + return (score / cloud_proj.size()) * 10000000.0f; } //template<typename T = PointXYZ> typename -PointCloud<PointXYZ>::Ptr cornersToPointCloud(const vector<cv::Point2f> &corners, const Mat &disp, const CameraParameters &p) { +PointCloud<PointXYZ>::Ptr cornersToPointCloud(const vector<cv::Point2f> &corners, const Mat &depth, const CameraParameters &p) { int corners_len = corners.size(); vector<cv::Vec3f> points(corners_len); @@ -102,7 +154,7 @@ PointCloud<PointXYZ>::Ptr cornersToPointCloud(const vector<cv::Point2f> &corners for (int i = 0; i < corners_len; i++) { double x = corners[i].x; double y = corners[i].y; - double d = disp.at<float>((int) y, (int) x); // * 1000.0f; // todo: better estimation + double d = depth.at<float>((int) y, (int) x); // * 1000.0f; // todo: better estimation //cv::Vec4d homg_pt = Q_ * cv::Vec4d(x, y, d, 1.0); //cv::Vec3d p = cv::Vec3d(homg_pt.val) / homg_pt[3]; @@ -112,16 +164,13 @@ PointCloud<PointXYZ>::Ptr cornersToPointCloud(const vector<cv::Point2f> &corners point.y = (((double)y + CY) / FY) * d; // / 1000.0f; point.z = d; - // no check since disparities assumed to be good in the calibration pattern - // if (fabs(d-minDisparity) <= FLT_EPSILON) - cloud->push_back(point); } return cloud; } -bool findChessboardCorners(Mat &rgb, const Mat &disp, const CameraParameters &p, const cv::Size pattern_size, PointCloud<PointXYZ>::Ptr &out, float error_threshold) { +bool findChessboardCorners(Mat &rgb, const Mat &depth, const CameraParameters &p, const cv::Size pattern_size, PointCloud<PointXYZ>::Ptr &out, float error_threshold) { vector<cv::Point2f> corners(pattern_size.width * pattern_size.height); #if CV_VERSION_MAJOR >= 4 @@ -133,7 +182,7 @@ bool findChessboardCorners(Mat &rgb, const Mat &disp, const CameraParameters &p, cv::drawChessboardCorners(rgb, pattern_size, Mat(corners), retval); if (!retval) { return false; } - auto corners_cloud = cornersToPointCloud(corners, disp, p); + auto corners_cloud = cornersToPointCloud(corners, depth, p); // simple check that the values make some sense float error = fitPlaneError(corners_cloud, error_threshold); // should use different parameter? LOG(INFO) << "MSE against estimated plane: " << error; @@ -200,6 +249,323 @@ Eigen::Matrix4f findTransformation(vector<PointCloud<PointXYZ>::Ptr> clouds_sour return T; } +Registration::Registration(nlohmann::json &config) : + ftl::Configurable(config) { + target_source_ = get<string>("targetsource"); + if (!target_source_) { + LOG(WARNING) << "targetsource not set"; + } +} + +RGBDSource* Registration::getSource(size_t idx) { + return sources_[idx]; +} + +bool Registration::isTargetSourceSet() { + return (bool) target_source_; +} + +bool Registration::isTargetSourceFound() { + for (RGBDSource* source : sources_ ) { + if (isTargetSource(source)) return true; + } + return false; +} + +bool Registration::isTargetSource(RGBDSource *source) { + if (target_source_) { return source->getURI() == *target_source_; } + return false; +} + +bool Registration::isTargetSource(size_t idx) { + if (idx >= sources_.size()) return false; // assert + return isTargetSource(sources_[idx]); +} + +size_t Registration::getTargetSourceIdx() { + if (!target_source_) return 0; + for (size_t idx = 0; idx < sources_.size(); ++idx) { + if (isTargetSource(sources_[idx])) return idx; + } + + return 0; +} + +void Registration::addSource(RGBDSource *source) { + // TODO: check that source is not already included + sources_.push_back(source); +} + +/** + * @param adjacency matrix + * @param index of starting vertex + * @param (out) edges connecting each level + * @returns true if graph connected (all vertices visited), otherwise false + * + * Breadth First Search + */ +bool isConnected(vector<vector<bool>> matrix, size_t start_idx, vector<vector<pair<size_t, size_t>>> &edges) { +vector<bool> visited(matrix.size(), false); + DCHECK(start_idx < matrix.size()); + + edges.clear(); + vector<size_t> level { start_idx }; + + visited[start_idx] = true; + size_t visited_count = 1; + + while(level.size() != 0) { + vector<size_t> level_prev = level; + level = {}; + + vector<pair<size_t, size_t>> new_edges; + + for (size_t current : level_prev) { + for (size_t i = 0; i < matrix.size(); ++i) { + if (matrix[current][i] && !visited[i]) { + visited[i] = true; + visited_count += 1; + level.push_back(i); + // could also save each level's vertices + + new_edges.push_back(pair(current, i)); + } + }} + if (new_edges.size() > 0) edges.push_back(new_edges); + } + + return visited_count == matrix.size(); +} + +bool isConnected(vector<vector<bool>> matrix, size_t start_idx = 0) { + vector<vector<pair<size_t, size_t>>> edges; + return isConnected(matrix, start_idx, edges); +} + +/** + * @param Adjacency matrix + * @returns Vector containing degree of each vertex +*/ +vector<uint> verticleDegrees(vector<vector<bool>> matrix) { + vector<uint> res(matrix.size(), 0); + for (size_t i = 0; i < matrix.size(); ++i) { + for (size_t j = 0; j < matrix.size(); ++j) { + if (matrix[i][j]) res[i] = res[i] + 1; + }} + return res; +} + +bool Registration::connectedVisibility() { + return isConnected(visibility_, getTargetSourceIdx()); +} + +void Registration::resetVisibility() { + visibility_ = vector(sources_.size(), vector<bool>(sources_.size(), false)); +} + +void Registration::run() { + resetVisibility(); + + do { + vector<bool> visible(sources_.size(), false); + + for (size_t i = 0; i < sources_.size(); ++i) { + bool retval = findFeatures(sources_[i], i); + visible[i] = retval; + } + + for (size_t i = 0; i < visible.size(); ++i) { + for (size_t j = 0; j < visible.size(); ++j) { + bool val = visible[i] && visible[j]; + visibility_[i][j] = visibility_[i][j] || val; + visibility_[j][i] = visibility_[j][i] || val; + }} + } + while(processData()); +} + +bool Registration::findTransformations(map<string, Matrix4f> &data) { + vector<Matrix4f> T; + data.clear(); + + if (!findTransformations(T)) return false; + for (size_t i = 0; i < sources_.size(); ++i) { + data[sources_[i]->getURI()] = T[i]; + } + return true; +} + +ChessboardRegistration* ChessboardRegistration::create(nlohmann::json &config) { + if (config.value<bool>("chain", false)) { + return new ChessboardRegistrationChain(config); + } + else { + return new ChessboardRegistration(config); + } +} + +ChessboardRegistration::ChessboardRegistration(nlohmann::json &config) : + Registration(config) { + + auto patternsize = get<vector<int>>("patternsize"); + if (!patternsize) { LOG(FATAL) << "Registration run enabled but pattern size not set"; } + pattern_size_ = cv::Size((*patternsize)[0], (*patternsize)[1]); + + auto maxerror = get<float>("maxerror"); + if (!maxerror) { LOG(WARNING) << "maxerror not set"; } + auto delay = get<int>("delay"); + if (!delay) { LOG(INFO) << "delay not set in configuration"; } + auto iter = get<int>("iterations"); + if (!iter) { LOG(INFO) << "iterations not set in configuration"; } + auto chain = get<bool>("chain"); + if (!chain) { LOG(INFO) << "input chaining disabled"; } + else { LOG(INFO) << "Input chaining enabled"; } + + error_threshold_ = maxerror ? *maxerror : std::numeric_limits<float>::infinity(); + iter_ = iter ? *iter : 10; + delay_ = delay ? *delay : 50; +} + +void ChessboardRegistration::run() { + if (!isTargetSourceFound()) { + LOG(WARNING) << "targetsource not found in sources"; + } + + if (data_.size() != getSourcesCount()) { + data_ = vector<vector<optional<PointCloud<PointXYZ>::Ptr>>>(getSourcesCount()); + } + iter_remaining_ = iter_; + + // TODO: Move GUI elsewhere. Also applies to processData() and findFeatures() + for (size_t i = 0; i < getSourcesCount(); ++i) { + cv::namedWindow("Registration: " + getSource(i)->getURI(), + cv::WINDOW_KEEPRATIO|cv::WINDOW_NORMAL); + } + + Registration::run(); + + for (size_t i = 0; i < getSourcesCount(); ++i) { + cv::destroyWindow("Registration: " + getSource(i)->getURI()); + } +} + +bool ChessboardRegistration::findFeatures(RGBDSource *source, size_t idx) { + optional<PointCloud<PointXYZ>::Ptr> result; + PointCloud<PointXYZ>::Ptr cloud(new PointCloud<PointXYZ>); + + Mat rgb, depth; + source->getRGBD(rgb, depth); + + bool retval = findChessboardCorners(rgb, depth, source->getParameters(), pattern_size_, cloud, error_threshold_); + if (retval) { + result.emplace(cloud); + } + data_[idx].push_back(result); + + cv::imshow("Registration: " + source->getURI(), rgb); + + return retval; +} + +bool ChessboardRegistration::processData() { + bool retval = connectedVisibility(); + resetVisibility(); + + if (retval) { + iter_remaining_--; + } + else{ + LOG(INFO) << "Pattern not visible in all inputs"; + for (auto &sample : data_) { sample.pop_back(); } + } + + //std::this_thread::sleep_for(std::chrono::milliseconds(delay_)); + cv::waitKey(delay_); // OpenCV GUI doesn't show otherwise + + return iter_remaining_ > 0; +} + +bool ChessboardRegistration::findTransformations(vector<Matrix4f> &data) { + data.clear(); + vector<bool> status(getSourcesCount(), false); + size_t idx_target = getTargetSourceIdx(); + + for (size_t idx = 0; idx < getSourcesCount(); ++idx) { + Matrix4f T; + if (idx == idx_target) { + T.setIdentity(); + } + else { + vector<PointCloud<PointXYZ>::Ptr> d; + vector<PointCloud<PointXYZ>::Ptr> d_target; + d.reserve(iter_); + d_target.reserve(iter_); + + for (size_t i = 0; i < iter_; ++i) { + auto val = data_[idx][i]; + auto val_target = data_[idx_target][i]; + if (val && val_target) { + d.push_back(*val); + d_target.push_back(*val_target); + } + } + T = findTransformation(d, d_target); + } + data.push_back(T); + } + return true; +} + +ChessboardRegistrationChain::ChessboardRegistrationChain(nlohmann::json &config) : + ChessboardRegistration(config) { + error_threshold_ = std::numeric_limits<float>::infinity(); +} + +bool ChessboardRegistrationChain::processData() { + for (auto &sample : data_ ) { sample.clear(); } + bool retval = isConnected(visibility_, getTargetSourceIdx(), edges_); + + if (retval) { + LOG(INFO) << "Chain complete, depth: " << edges_.size(); + return false; + } + else{ + LOG(5) << "Chain not complete "; + } + + return true; +} + +bool ChessboardRegistrationChain::findTransformations(vector<Matrix4f> &data) { + // TODO Change to group registration: register all sources which have visibility + // to the target source in chain. + + LOG(INFO) << "Running pairwise registration"; + data = vector<Matrix4f>(getSourcesCount(), Matrix4f::Identity()); + + for (vector<pair<size_t, size_t>> level : edges_) { + for (pair<size_t, size_t> edge : level) { + LOG(INFO) << "Registering source " + << getSource(edge.second)->getURI() << " to source" + << getSource(edge.first)->getURI(); + + nlohmann::json conf(config_); + conf["targetsource"] = getSource(edge.first)->getURI(); + conf["chain"] = false; + + vector<Matrix4f> result; + ChessboardRegistration reg(conf); + reg.addSource(getSource(edge.first)); + reg.addSource(getSource(edge.second)); + reg.run(); + if (!reg.findTransformations(result)) { return false; } + data[edge.second] = data[edge.first] * result[1]; + } + } + + return true; +} + } // namespace registration } // namespace ftl diff --git a/config/config.jsonc b/config/config.jsonc index 8f1f8f8e4ae7acceffa9705a9c0273434b6d7086..95fc7fde61d4bfe83c386b1f442ee5f310824b84 100644 --- a/config/config.jsonc +++ b/config/config.jsonc @@ -149,15 +149,13 @@ "display": { "$ref": "#displays/left" }, "virtual": { "$ref": "#virtual_cams/default" }, "voxelhash": { "$ref": "#hash_conf/default" }, - "registration": { - "reference-source" : "ftl://utu.fi#vision_default/source", - "calibration" : { - "max_error": 25, - "run": false, - "iterations" : 10, - "delay" : 500, - "patternsize" : [9, 6] - } + "merge": { + "register": false, + "targetsource" : "ftl://utu.fi#vision_default/source", + "maxerror": 25, + "iterations" : 10, + "delay" : 500, + "patternsize" : [9, 6] } }, @@ -172,15 +170,14 @@ "display": { "$ref": "#displays/left" }, "virtual": { "$ref": "#virtual_cams/default" }, "voxelhash": { "$ref": "#hash_conf/default" }, - "registration": { - "reference-source" : "ftl://utu.fi/node4#vision_default/source", - "calibration" : { - "max_error": 25, - "run": false, - "iterations" : 10, - "delay" : 500, - "patternsize" : [9, 6] - } + "merge": { + "targetsource" : "ftl://utu.fi/node4#vision_default/source", + "register": false, + "chain": false, + "maxerror": 25, + "iterations" : 10, + "delay" : 500, + "patternsize" : [9, 6] }, "stream": {} },