source/calibration/FeatureDetector.cpp (160 lines of code) (raw):

/** * Copyright 2004-present Facebook. All Rights Reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ #include "source/calibration/FeatureDetector.h" #include <boost/timer/timer.hpp> #include <gflags/gflags.h> #include <glog/logging.h> #include <folly/Format.h> #include "source/util/Camera.h" #include "source/util/ThreadPool.h" DEFINE_int32(deduplicate_radius, 3, "remove duplicate corners found at different octaves"); DEFINE_double(harris_parameter, 0.04, "harris parameter"); DEFINE_double(harris_window_radius, 5, "harris corner detector window radius"); DEFINE_int32(max_corners, 10000, "maximum number of corners to detect at each level"); DEFINE_int32(min_feature_distance, 10, "minimum distance between features in pixels"); DEFINE_double(min_feature_quality, 0.00001, "minimum feature quality"); DEFINE_double( refine_corners_epsilon, 0.000001, "epsilon termiation value for refining corners to subpixel precision"); DEFINE_int32(refine_corners_radius, 5, "window radius for refining corners to subpixel precision"); DEFINE_int32(zncc_window_radius, 16, "zncc window radius in pixels"); using PixelType = uint8_t; using Image = cv::Mat_<PixelType>; using ImageId = std::string; namespace fb360_dep { namespace calibration { bool isUniqueCorner( const std::vector<Keypoint>& corners, const int previousCornerCount, const Camera::Vector2& corner) { if (FLAGS_deduplicate_radius <= 0) { return true; } for (int previousCorner = 0; previousCorner < previousCornerCount; previousCorner++) { if ((corners[previousCorner].coords - corner).norm() < FLAGS_deduplicate_radius) { return false; } } return true; } std::vector<Camera::Vector2> findScaledCorners( const double scale, const cv::Mat_<uint8_t>& imageFull, const cv::Mat_<uint8_t>& maskFull, const std::string& cameraId) { std::vector<Camera::Vector2> cameraCorners; std::vector<cv::Point2f> cvCorners; cv::Mat_<uint8_t> gray; cv::resize(imageFull, gray, cv::Size(), scale, scale, cv::INTER_AREA); cv::Mat_<uint8_t> mask; if (!maskFull.empty()) { cv::resize(maskFull, mask, cv::Size(), scale, scale, cv::INTER_AREA); } // Find corners using the cv harris detector cv::goodFeaturesToTrack( gray, cvCorners, FLAGS_max_corners, FLAGS_min_feature_quality, FLAGS_min_feature_distance * (FLAGS_same_scale ? scale : 1), mask, FLAGS_harris_window_radius, true, FLAGS_harris_parameter); if (cvCorners.empty()) { return cameraCorners; } // Refine corners to subpixel precision const cv::Size windowRadius(FLAGS_refine_corners_radius, FLAGS_refine_corners_radius); const cv::Size zeroZone = cv::Size(-1, -1); // means "no zeroZone" cv::TermCriteria criteria = cv::TermCriteria(cv::TermCriteria::EPS, 0, FLAGS_refine_corners_epsilon); // If refinement fails, opencv silently leaves the inputs untouched. So use unlikely inputs const cv::Point2f kOffset(0.0017, 0.0013); // just some unlikely offset std::vector<cv::Point2f> cvRefined(cvCorners); for (cv::Point2f& p : cvRefined) { p += kOffset; } cv::cornerSubPix(gray, cvRefined, windowRadius, zeroZone, criteria); // Only keep refined points, convert out of opencv coordinate convention, scale for (ssize_t i = 0; i < ssize(cvRefined); ++i) { if (cvRefined[i] != cvCorners[i] + kOffset) { cameraCorners.emplace_back((cvRefined[i].x + 0.5f) / scale, (cvRefined[i].y + 0.5f) / scale); } } return cameraCorners; } static bool isCloseToEdge(const Camera::Vector2& point, const Image& image, const int margin) { if (0 <= point.x() - margin && point.x() + margin < image.cols) { if (0 <= point.y() - margin && point.y() + margin < image.rows) { return false; } } return true; } cv::Mat_<uint8_t> generateImageCircleMask(const Camera& camera) { double width = camera.resolution.x(); double height = camera.resolution.y(); cv::Mat_<uint8_t> mask(height, width); for (double y = 0; y < height; ++y) { for (double x = 0; x < width; ++x) { Camera::Vector2 pixel = {x + 0.5, y + 0.5}; mask(y, x) = camera.isOutsideImageCircle(pixel) ? 0 : 255; } } return mask; } std::vector<Keypoint> findCorners(const Camera& camera, const Image& image, const bool useNearest) { LOG(INFO) << folly::sformat("Processing camera {}... ", camera.id); // Search for features at multiple scales int rejectedCorners = 0; int deduplicatedCorners = 0; std::vector<Keypoint> corners; // if we're comparing across a single scale, we don't rescale while finding corners int octaveCount = FLAGS_same_scale ? 1 : FLAGS_octave_count; const cv::Mat_<uint8_t>& mask = generateImageCircleMask(camera); for (int octave = 0; octave < octaveCount; ++octave) { double scale = std::pow(0.5, octave); std::vector<Camera::Vector2> octaveCorners = findScaledCorners(scale, image, mask, camera.id); if (FLAGS_log_verbose) { LOG(INFO) << folly::sformat( "{} found {} corners at scale {}", camera.id, octaveCorners.size(), scale); } int cornerCountBeforeOctave = corners.size(); for (const Camera::Vector2& octaveCorner : octaveCorners) { if (isCloseToEdge(octaveCorner, image, FLAGS_zncc_window_radius)) { rejectedCorners++; } else if (!isUniqueCorner(corners, cornerCountBeforeOctave, octaveCorner)) { deduplicatedCorners++; } else { corners.emplace_back(octaveCorner, image, FLAGS_zncc_window_radius, useNearest); } } } if (FLAGS_deduplicate_radius != 0) { LOG(INFO) << folly::sformat( "{} accepted corners: {} deduplicated corners: {} rejected corners {}", camera.id, corners.size(), deduplicatedCorners, rejectedCorners); } else { LOG(INFO) << folly::sformat( "{} accepted corners: {} rejected corners {}", camera.id, corners.size(), rejectedCorners); } return corners; } std::map<ImageId, std::vector<Keypoint>> findAllCorners(const Camera::Rig& rig, const std::vector<Image>& images, const bool useNearest) { std::map<ImageId, std::vector<Keypoint>> allCorners; boost::timer::cpu_timer featureTimer; ThreadPool threadPool(FLAGS_threads); for (int currentCamera = 0; currentCamera < ssize(rig); currentCamera++) { const Camera& camera = rig[currentCamera]; std::vector<Keypoint>& keypoints = allCorners[camera.id]; // modify allCorners in main thread const Image& image = images[currentCamera]; threadPool.spawn([&keypoints, &camera, &image, &useNearest] { keypoints = findCorners(camera, image, useNearest); }); } threadPool.join(); if (FLAGS_enable_timing) { LOG(INFO) << folly::sformat("Find corners stage time: {}", featureTimer.format()); } return allCorners; } } // namespace calibration } // namespace fb360_dep