source/calibration/FeatureMatcher.cpp (288 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/FeatureMatcher.h"
#include <future>
#include <queue>
#include <boost/timer/timer.hpp>
#include <folly/Format.h>
#include "source/calibration/FeatureDetector.h"
DEFINE_bool(custom_zncc, false, "uses custom ZNCC formula for patch matching");
DEFINE_double(depth_max, 100.0, "max depth in m");
DEFINE_double(depth_min, 1.0, "min depth in m");
DEFINE_double(depth_samples, 1000, "number of depths to sample");
DEFINE_double(max_depth_for_remap, 50, "max depth to reproject features");
DEFINE_double(overlap_threshold, 0, "minimum overlap between matched images");
DEFINE_double(reprojected_corner_drift_tolerance, 0.5, "in pixels");
DEFINE_double(
search_overlap,
0.25,
"overlap fraction between search windows at different disparities");
DEFINE_int32(search_radius, 100, "search radius in pixels");
DEFINE_double(
zncc_delta_threshold,
0.05,
"minimum zncc score difference betwen best and second best potential matches for a corner");
using PixelType = uint8_t;
using Image = cv::Mat_<PixelType>;
using ImageId = std::string;
namespace fb360_dep {
namespace calibration {
struct BestMatch {
int bestIdx;
double bestScore;
int secondBestIdx;
double secondBestScore;
BestMatch() {
// Initialize scores to -1. zncc scores are between -1 and 1, higher is better
// Initialize index to -1 indicating there is no match.
bestIdx = -1;
bestScore = -1;
secondBestIdx = -1;
secondBestScore = -1;
}
void updateCornerScore(const double newScore, const int newIdx) {
if (newScore > bestScore) {
// newScore is a better score for this corner
if (bestIdx == newIdx) {
// If this is the same index as the previous best don't update the second best
bestScore = newScore;
} else {
secondBestIdx = bestIdx;
secondBestScore = bestScore;
bestIdx = newIdx;
bestScore = newScore;
}
} else if (newScore > secondBestScore && bestIdx != newIdx) {
secondBestScore = newScore;
secondBestIdx = newIdx;
}
}
// A corner is weak if its best match score falls below the score threshold
// or if 2 (or more) highest match scores are close together (within
// zncc_delta_threshold). Weak corners are rejected since the potiential matches
// are nearly indistiguishable.
bool isWeakCorner() const {
return bestScore < FLAGS_match_score_threshold ||
bestScore - secondBestScore < FLAGS_zncc_delta_threshold;
}
};
double computeZncc(const Keypoint& corner0, const Keypoint& corner1) {
CHECK_EQ(corner0.patch.size, corner1.patch.size);
double zncc = 0;
for (int x = 0; x < corner0.patch.cols; ++x) {
for (int y = 0; y < corner0.patch.rows; ++y) {
zncc += (corner0.patch(y, x) - corner0.avg) * (corner1.patch(y, x) - corner1.avg);
}
}
if (FLAGS_custom_zncc) {
/* Custom ZNCC formula:
mean((p0 - p0.avg) * (p1 - p1.avg)) / p0.avg / p1.avg
---------------------------------------------
max(p0.stddev / p0.avg, p1.stddev / p1.avg)^2
*/
zncc /= corner0.patch.total();
zncc /= (corner0.avg * corner1.avg); // finishes numerator calculation
float denominator = std::max(corner0.std / corner0.avg, corner1.std / corner1.avg);
zncc /= (denominator * denominator);
} else {
zncc /= (corner0.std * corner1.std * corner0.patch.total());
}
return zncc;
}
// Compute a box around the pixel in camera 1 corresponding to the specified point in camera 0
cv::Rect2f computeBox(
const Camera& camera1,
const Camera& camera0,
const Camera::Vector2& pixel0,
const double depth) {
const Camera::Vector3 world = camera0.rig(pixel0, depth);
const Camera::Vector2 pixel1 = camera1.pixel(world);
return cv::Rect2f(
pixel1.x() - FLAGS_search_radius,
pixel1.y() - FLAGS_search_radius,
2 * FLAGS_search_radius,
2 * FLAGS_search_radius);
}
bool tooMuchOverlap(const cv::Rect2f& box, const cv::Rect2f& lastBox) {
return (box & lastBox).area() > FLAGS_search_overlap * box.area();
}
// Compute what a corner in camera 0 looks like from camera 1. I.e.
// - find the point in camera 1 corresponding to the specified point in camera 0
// - then, for each pixel in a square around that point, read the corresponding pixel from camera 0
// Return false if camera 1 doesn't see the specified point
bool projectCorner(
Image& projection1,
const Camera& camera1,
const Image& img0,
const Camera& camera0,
const Keypoint& corner0,
const double depth0) {
CHECK_EQ(img0.cols, camera0.resolution.x());
CHECK_EQ(img0.rows, camera0.resolution.y());
const Camera::Vector3 corner = camera0.rig(corner0.coords, depth0);
Camera::Vector2 corner1;
if (!camera1.sees(corner, corner1)) {
return false;
}
const double depth1 = (corner - camera1.position).norm();
CHECK_EQ(corner0.patch.cols, corner0.patch.rows);
const int radius = corner0.patch.cols / 2;
projection1.create(2 * radius + 1, 2 * radius + 1);
for (int xOffset = -radius; xOffset <= radius; xOffset++) {
for (int yOffset = -radius; yOffset <= radius; yOffset++) {
const Camera::Vector2 pixel1 = {corner1.x() + xOffset, corner1.y() + yOffset};
const Camera::Vector3 world = camera1.rig(pixel1, depth1);
Camera::Vector2 pixel0;
if (!camera0.sees(world, pixel0)) {
return false;
}
projection1(yOffset + radius, xOffset + radius) = FLAGS_use_nearest
? img0(int(pixel0.y()), int(pixel0.x()))
: cv_util::getPixelBilinear(img0, pixel0.x(), pixel0.y());
}
}
return true;
}
bool hasCornerNearCenter(const Image& image) {
const Camera::Vector2 center = 0.5 * Camera::Vector2(image.cols, image.rows);
Camera::Vector2 best = center;
for (const Camera::Vector2& corner : findScaledCorners(1, image, cv::Mat_<uint8_t>())) {
Camera::Vector2 offset = corner - center;
if (offset.dot(offset) < best.dot(best)) {
best = offset;
}
}
return best.dot(best) <= math_util::square(FLAGS_reprojected_corner_drift_tolerance);
}
bool getNextDepthSample(
int& currentDepthSample,
double& currentDisparity,
cv::Rect2f& currentBox,
const Camera& camera0,
const Camera::Vector2& corner0Coords,
const Camera& camera1) {
for (int nextDepthSample = currentDepthSample + 1; nextDepthSample < FLAGS_depth_samples;
++nextDepthSample) {
// We don't bother testing a disparity unless the search box is substantially different
const double nextDisparity = math_util::lerp(
1 / FLAGS_depth_max, 1 / FLAGS_depth_min, nextDepthSample / (FLAGS_depth_samples - 1.0));
const cv::Rect2f nextBox = computeBox(camera1, camera0, corner0Coords, 1 / nextDisparity);
if (!tooMuchOverlap(nextBox, currentBox)) {
currentDepthSample = nextDepthSample;
currentDisparity = nextDisparity;
currentBox = nextBox;
return true;
}
}
return false;
}
Overlap findMatches(
const Image& img0,
const std::vector<Keypoint>& corners0,
const Camera& camera0,
const Image& img1,
const std::vector<Keypoint>& corners1,
const Camera& camera1) {
boost::timer::cpu_timer timer;
boost::timer::cpu_timer znccTimer;
znccTimer.stop();
boost::timer::cpu_timer projectCornerTimer;
projectCornerTimer.stop();
Image image1; // optimization: avoid reallocation by keeping this outside loop
// For each corner in corners0, compute its best and second best match in corners1. and vice versa
std::vector<BestMatch> bestMatches0(corners0.size());
std::vector<BestMatch> bestMatches1(corners1.size());
int callsToZncc = 0;
int callsToProjectCorners = 0;
for (ssize_t index0 = 0; index0 < ssize(corners0); index0++) {
LOG_IF(INFO, (FLAGS_threads == 0 || FLAGS_threads == 1) && (index0 % 1000) == 0)
<< "Processing feature " << index0 << " of " << corners0.size() << " from pair "
<< camera0.id << " " << camera1.id;
const Keypoint& corner0 = corners0[index0];
int depthSample = -1;
double disparity = 0;
cv::Rect2f box1(0, 0, 0, 0);
bool firstProjection = true;
while (getNextDepthSample(depthSample, disparity, box1, camera0, corner0.coords, camera1)) {
// only remap corner for sufficiently large disparities
if (firstProjection || disparity > 1 / FLAGS_max_depth_for_remap) {
// compute what the area around corner 0 would look like from camera 1
projectCornerTimer.resume();
callsToProjectCorners++;
if (!projectCorner(image1, camera1, img0, camera0, corner0, 1 / disparity)) {
continue;
}
projectCornerTimer.stop();
// don't match if we can't rediscover the corner after it has been reprojected
if (!hasCornerNearCenter(image1)) {
continue;
}
firstProjection = false;
}
Keypoint projection1(image1);
// look for a corner in c1 that is in the box and looks similar
znccTimer.resume();
for (ssize_t index1 = 0; index1 < ssize(corners1); ++index1) {
cv::Point2f cvCoords(corners1[index1].coords.x(), corners1[index1].coords.y());
if (!box1.contains(cvCoords)) {
continue;
}
double score = computeZncc(projection1, corners1[index1]);
bestMatches0[index0].updateCornerScore(score, index1);
bestMatches1[index1].updateCornerScore(score, index0);
callsToZncc++;
}
znccTimer.stop();
}
}
// Take match if both ends are strong and each other's best match
Overlap overlap(camera0.id, camera1.id);
for (const BestMatch& bestMatch0 : bestMatches0) {
if (bestMatch0.isWeakCorner()) {
continue;
}
const BestMatch& bestMatch1 = bestMatches1[bestMatch0.bestIdx];
if (bestMatch1.isWeakCorner()) {
continue;
}
if (&bestMatch0 != &bestMatches0[bestMatch1.bestIdx]) {
continue;
}
overlap.matches.emplace_back(bestMatch0.bestScore, bestMatch1.bestIdx, bestMatch0.bestIdx);
}
// Only report timing in single threaded mode
// In multithreaded mode these will clocks include time from other threads
// running simultaneously
if (FLAGS_enable_timing && FLAGS_threads == 1) {
LOG(INFO) << folly::sformat(
"{} and {} matching complete. Overlap fraction: {}. Matches: {}. Timing: {} "
"Calls to ZNCC: {}. ZNCC Time: {} "
"Calls to ProjectCorners: {}. Project Corner Time: {} ",
camera0.id,
camera1.id,
camera0.overlap(camera1),
overlap.matches.size(),
timer.format(),
callsToZncc,
znccTimer.format(),
callsToProjectCorners,
projectCornerTimer.format());
} else {
LOG(INFO) << folly::sformat(
"{} and {} matching complete. Overlap fraction: {}. Matches: {}",
camera0.id,
camera1.id,
camera0.overlap(camera1),
overlap.matches.size());
}
return overlap;
}
std::vector<Overlap> findAllMatches(
const Camera::Rig& rig,
const std::vector<Image>& images,
const std::map<ImageId, std::vector<Keypoint>>& allCorners) {
std::vector<Overlap> overlaps;
boost::timer::cpu_timer matchTimer;
// Find matches across all pairings
const int threadCount = ThreadPool::getThreadCountFromFlag(FLAGS_threads);
std::queue<std::future<Overlap>> overlapFutures;
for (ssize_t c1 = 0; c1 < ssize(rig); c1++) {
for (ssize_t c2 = c1 + 1; c2 < ssize(rig); c2++) {
if (rig[c1].overlap(rig[c2]) < FLAGS_overlap_threshold) {
continue;
}
overlapFutures.push(std::async(
threadCount == 0 ? std::launch::deferred : std::launch::async,
&findMatches,
std::cref(images[c1]),
std::cref(allCorners.at(rig[c1].id)),
std::cref(rig[c1]),
std::cref(images[c2]),
std::cref(allCorners.at(rig[c2].id)),
std::cref(rig[c2])));
if (ssize(overlapFutures) >= threadCount && !overlapFutures.empty()) {
overlaps.emplace_back(overlapFutures.front().get());
overlapFutures.pop();
}
}
}
while (!overlapFutures.empty()) {
overlaps.emplace_back(overlapFutures.front().get());
overlapFutures.pop();
}
if (FLAGS_enable_timing) {
LOG(INFO) << folly::sformat("Matching stage time: {}", matchTimer.format());
}
return overlaps;
}
} // namespace calibration
} // namespace fb360_dep