source/calibration/MatchCorners.cpp (222 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/MatchCorners.h"
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <folly/Format.h>
#include "source/calibration/Calibration.h"
#include "source/calibration/FeatureDetector.h"
#include "source/calibration/FeatureMatcher.h"
#include "source/util/FilesystemUtil.h"
#include "source/util/ImageUtil.h"
#include "source/util/ThreadPool.h"
using namespace fb360_dep;
using namespace fb360_dep::calibration;
using namespace fb360_dep::image_util;
DEFINE_int32(
camera_count,
0,
"Total number of cameras to match. Default value of 0 will match all the cameras in the json");
DEFINE_string(
color_channel,
"grayscale",
"color channel. supported channels: grayscale, red, green, blue");
DEFINE_int32(min_features, 1500, "minimum number of features to consider calibration valid");
DEFINE_int32(octave_count, 4, "number of resolutions to use when looking for features");
DEFINE_bool(same_scale, false, "match at same scale where feature was found");
DEFINE_double(scale, 1, "scale at which to perform matching");
DEFINE_bool(use_nearest, false, "use nearest neighbor during corner matching, default is bilinear");
using Image = cv::Mat_<uint8_t>;
using ImageId = std::string;
namespace fb360_dep {
namespace calibration {
Image extractSingleChannelImage(const cv::Mat_<cv::Vec3b>& image) {
std::vector<Image> splitImage;
cv::split(image, splitImage);
const int channels = 3;
CHECK_EQ(splitImage.size(), channels) << "error loading separate color channels";
LOG(INFO) << folly::sformat("Loading single channge images {}", FLAGS_color_channel);
Image singleChannelImage;
if (FLAGS_color_channel == "grayscale") {
cv::cvtColor(image, singleChannelImage, cv::COLOR_BGR2GRAY);
} else if (FLAGS_color_channel == "blue") {
singleChannelImage = splitImage[0];
} else if (FLAGS_color_channel == "green") {
singleChannelImage = splitImage[1];
} else if (FLAGS_color_channel == "red") {
singleChannelImage = splitImage[2];
} else {
LOG(FATAL) << folly::sformat("Unknown color channel selected: {}", FLAGS_color_channel);
}
return singleChannelImage;
}
std::vector<Image> loadSingleChannelImages(const filesystem::path& dir, const Camera::Rig& rig) {
// Load color images without alpha in bgr format
std::vector<cv::Mat_<cv::Vec3b>> images =
loadImages<cv::Vec3b>(dir, rig, FLAGS_frame, FLAGS_threads);
ThreadPool threadPool(FLAGS_threads);
std::vector<Image> singleChannelImages;
for (auto& image : images) {
singleChannelImages.emplace_back(extractSingleChannelImage(image));
}
return singleChannelImages;
}
static void saveMatches(
const filesystem::path& filename,
const std::map<ImageId, std::vector<Keypoint>>& allCorners,
const std::vector<Overlap>& overlaps) {
const filesystem::path colorDir = FLAGS_color;
CHECK(!allCorners.empty());
const std::string imageExt = filesystem::getFirstExtension(colorDir / allCorners.begin()->first);
folly::dynamic allCornersData = Keypoint::serializeRig(allCorners, FLAGS_frame, imageExt);
folly::dynamic allMatches = folly::dynamic::array;
for (const auto& overlap : overlaps) {
allMatches.push_back(overlap.serialize(FLAGS_frame, imageExt));
}
folly::dynamic matchesData =
folly::dynamic::object("all_matches", allMatches)("images", allCornersData);
LOG(INFO) << folly::sformat("Saving matches to file: {}", filename.string());
if (filename.has_parent_path()) {
filesystem::create_directories(filename.parent_path());
}
CHECK(folly::writeFile(folly::toPrettyJson(matchesData), filename.string().data()));
}
template <typename T>
static void downscale(cv::Mat_<T>& image) {
if (FLAGS_scale != 1.0) {
cv::resize(image, image, {}, FLAGS_scale, FLAGS_scale, cv::INTER_AREA);
}
}
std::vector<Image> loadChannels(const Camera::Rig& rig) {
std::vector<Image> images;
LOG(INFO) << "Loading images... ";
const filesystem::path colorDir = FLAGS_color;
if (FLAGS_color_channel == "grayscale") {
images = loadImages<uint8_t>(colorDir, rig, FLAGS_frame, FLAGS_threads);
} else if (
FLAGS_color_channel == "red" || FLAGS_color_channel == "green" ||
FLAGS_color_channel == "blue") {
images = loadSingleChannelImages(colorDir, rig);
} else {
LOG(FATAL) << folly::sformat("Unknown color channel selected: {}", FLAGS_color_channel);
}
LOG(INFO) << "Images loaded";
// scale according to FLAGS_scale and verify that resolutions match
for (Image& image : images) {
downscale(image);
}
// check that camera and image aspect ratios match within 1%
CHECK_EQ(ssize(rig), ssize(images));
for (ssize_t i = 0; i < ssize(rig); ++i) {
const Camera::Vector2 res = rig[i].resolution;
const double ratio = images[i].cols / double(images[i].rows);
CHECK_LT(ratio - 0.01, res.x() / res.y()) << rig[i].id << " image and camera shape mismatch";
CHECK_LT(res.x() / res.y(), ratio + 0.01) << rig[i].id << " image and camera shape mismatch";
}
return images;
}
static Camera::Rig loadRig() {
Camera::Rig rig = Camera::loadRig(FLAGS_rig_in);
while (0 < FLAGS_camera_count && FLAGS_camera_count < ssize(rig)) {
rig.pop_back(); // we will only match a subset of the cameras in the rig
}
CHECK_GT(ssize(rig), 0);
return rig;
}
// rescale rig to match image resolution
static Camera::Rig rescale(const Camera::Rig& rigFull, const std::vector<Image>& images) {
Camera::Rig rig;
CHECK_EQ(ssize(rigFull), ssize(images));
for (ssize_t i = 0; i < ssize(rigFull); ++i) {
rig.push_back(rigFull[i].rescale({images[i].cols, images[i].rows}));
}
CHECK_EQ(ssize(rig), ssize(rigFull));
return rig;
}
// upscale corners from image to rig resolution
static void upscale(
std::map<ImageId, std::vector<Keypoint>>& corners,
const Camera::Rig& rig,
const std::vector<Image>& images) {
for (auto& pair : corners) {
// compute index of camera id in pair.first
ssize_t index = -1;
for (ssize_t i = 0; i < ssize(rig); ++i) {
if (rig[i].id == pair.first) {
index = i;
}
}
CHECK_NE(index, -1) << "no camera named " << pair.first << " found in rig";
// go through keypoints scaling from image to camera resolution
for (Keypoint& keypoint : pair.second) {
keypoint.coords.x() *= rig[index].resolution.x() / images[index].cols;
keypoint.coords.y() *= rig[index].resolution.y() / images[index].rows;
}
}
}
void processScale(
const float scale,
const Camera::Rig& rigFull,
const std::vector<Image>& images,
std::map<ImageId, std::vector<Keypoint>>& allCorners,
std::vector<Overlap>& overlaps) {
LOG(INFO) << folly::sformat("Processing scale: {}", scale);
std::vector<Image> scaledImages;
for (const Image& image : images) {
Image scaledImage;
cv::resize(image, scaledImage, {}, scale, scale, cv::INTER_AREA);
scaledImages.push_back(scaledImage);
}
// rescale rig to match images
CHECK(!scaledImages.empty());
const Camera::Rig rig = rescale(rigFull, scaledImages);
std::map<ImageId, std::vector<Keypoint>> newCorners =
findAllCorners(rig, scaledImages, FLAGS_use_nearest);
std::vector<Overlap> newOverlaps = findAllMatches(rig, scaledImages, newCorners);
upscale(newCorners, rigFull, scaledImages);
// matches refer to corners by index, so we need to offset these by the total number
for (Overlap& newOverlap : newOverlaps) {
const ImageId& image0 = newOverlap.images[0];
const ImageId& image1 = newOverlap.images[1];
for (auto& match : newOverlap.matches) {
match.corners[0] += allCorners[image0].size();
match.corners[1] += allCorners[image1].size();
}
}
// add newOverlaps to overlaps
if (overlaps.empty()) {
overlaps = newOverlaps;
} else {
CHECK_EQ(ssize(overlaps), ssize(newOverlaps));
for (ssize_t i = 0; i < ssize(overlaps); ++i) {
overlaps[i].matches.insert(
overlaps[i].matches.end(), newOverlaps[i].matches.begin(), newOverlaps[i].matches.end());
}
}
// add newCorners to corners
for (const auto& entry : newCorners) {
const ImageId& image = entry.first;
allCorners[image].insert(
allCorners[image].end(), newCorners[image].begin(), newCorners[image].end());
}
}
void processOctaves(
const Camera::Rig& rigFull,
const std::vector<Image>& images,
std::map<ImageId, std::vector<Keypoint>>& allCorners,
std::vector<Overlap>& overlaps) {
int octaveCount = FLAGS_same_scale ? FLAGS_octave_count : 1;
for (int octave = 0; octave < octaveCount; octave++) {
float scale = std::pow(0.5, octave);
processScale(scale, rigFull, images, allCorners, overlaps);
}
}
} // namespace calibration
} // namespace fb360_dep
int matchCorners() {
CHECK_NE(FLAGS_color, "");
CHECK_NE(FLAGS_rig_in, "");
CHECK_NE(FLAGS_matches, "");
// Load camera rig
const Camera::Rig& rigFull = loadRig();
const int validFrame = getSingleFrame(FLAGS_color, rigFull, FLAGS_frame);
FLAGS_frame = image_util::intToStringZeroPad(validFrame);
// Load input grayscale images
const std::vector<Image>& images = loadChannels(rigFull);
std::map<ImageId, std::vector<Keypoint>> allCorners;
std::vector<Overlap> overlaps;
processOctaves(rigFull, images, allCorners, overlaps);
for (const auto& entry : allCorners) {
if (ssize(entry.second) < FLAGS_min_features) {
throw std::runtime_error(folly::sformat(
"Too few features found in camera {}: {}", entry.first, ssize(entry.second)));
}
}
saveMatches(FLAGS_matches, allCorners, overlaps);
return EXIT_SUCCESS;
}