source/render/GenerateEquirect.cpp (219 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 <math.h> #include <algorithm> #include <gflags/gflags.h> #include <glog/logging.h> #include <opencv2/opencv.hpp> #include <folly/Format.h> #include "source/rig/RigTransform.h" #include "source/util/Camera.h" #include "source/util/ImageUtil.h" #include "source/util/ThreadPool.h" using namespace fb360_dep; using namespace fb360_dep::image_util; const std::string kUsageMessage = R"( - Generates an equirect from a set of color images at a uniformly spaced range of depths. - Example: ./GenerateEquirect \ --color=/path/to/video/color \ --output=/path/to/output \ --rig=/path/to/rigs/rig.json \ --frame=000000 \ --depth_min=1.0 \ --depth_max=1000.0 \ --num_depths=50 )"; DEFINE_bool(black_bg, false, "set the background to be optionally black (red by default)"); DEFINE_string(camera_id, "", "id of camera selected to be centered"); DEFINE_string(cameras, "", "cameras to render (comma-separated)"); DEFINE_string(color, "", "path to input color images (required)"); DEFINE_bool(crop_equirect, false, "crop the equirect to only include visible images"); DEFINE_double(depth_max, 10.0, "max depth in m"); DEFINE_double(depth_min, 1.0, "min depth in m"); DEFINE_string(frame, "000000", "frame to process (lexical)"); DEFINE_uint64(height, 512, "equirect height in pixels"); DEFINE_uint64(num_depths, 50, "num depths"); DEFINE_string(output, "", "path to output directory (required)"); DEFINE_string(rig, "", "path to camera rig .json (required)"); DEFINE_double(scale, 1, "image scale factor"); DEFINE_int32(threads, -1, "number of threads (-1 = max allowed, 0 = no threading)"); using Image = cv::Mat_<cv::Vec4f>; void saveImage(const cv::Mat_<cv::Vec4f> eqr, const double depth) { size_t height = eqr.rows; size_t width = eqr.cols; // Add text to image showing current depth const std::string depthStr = folly::sformat("{:.2f}", depth); const cv::Point2f textPos((85.0f / 100.0f) * width, (6.0f / 100.0f) * height); const int textFont = cv::FONT_HERSHEY_PLAIN; const double textScale = 2; const cv::Scalar textColor(0, 1, 0, 1); // green cv::putText(eqr, depthStr + " m", textPos, textFont, textScale, textColor); const filesystem::path equirectDir = filesystem::path(FLAGS_output) / "equirect"; filesystem::create_directories(equirectDir); // Pad filename with zeros so they are saved in lexicographical order const std::string filename = folly::sformat("{}/{:05}_cm.png", equirectDir.string(), int(depth * 100)); cv_util::imwriteExceptionOnFail(filename, 255.0f * eqr); } cv::Vec4f getPixelColor( const Camera::Vector3 currentPoint, const Camera::Rig& rig, const std::vector<Image>& images) { cv::Vec4f accColor(0, 0, 0, 0); int accImages = 0; for (int currentCamera = 0; currentCamera < int(rig.size()); ++currentCamera) { // If pixel in bounds Camera::Vector2 currentPixel; if (rig[currentCamera].sees(currentPoint, currentPixel)) { accColor += images[currentCamera](currentPixel.y(), currentPixel.x()); accImages++; } } cv::Vec4f pixelColor; if (accImages > 0) { pixelColor = accColor / accImages; } else { pixelColor = FLAGS_black_bg ? cv::Vec4f(0, 0, 0, 1) : cv::Vec4f(0, 0, 1, 1); } return pixelColor; } Camera::Vector3 getEquirectPoint( const double x, const double y, const double depth, const double width, const double height) { double theta = -1 * ((x + 0.5) / width * 2 * M_PI); double phi = (y + 0.5) / height * M_PI; double cartX = depth * sin(phi) * cos(theta); double cartY = depth * sin(phi) * sin(theta); double cartZ = depth * cos(phi); return Camera::Vector3(cartX, cartY, cartZ); } Image createEquirect( const Camera::Rig& rig, const std::vector<Image>& images, const size_t height, const size_t width, const float depth) { Image equirectImage(height, width); for (double x = 0; x < width; x++) { for (double y = 0; y < height; y++) { Camera::Vector3 currentPoint = getEquirectPoint(x, y, depth, width, height); equirectImage(y, x) = getPixelColor(currentPoint, rig, images); } } return equirectImage; } Image createCroppedEquirect( const Camera::Rig& rig, const std::vector<Image>& images, const size_t height, const size_t width, const float depth) { double minX = width; double maxX = 0; double minY = height; double maxY = 0; for (double x = 0; x < width; x++) { for (double y = 0; y < height; y++) { Camera::Vector3 currentPoint = getEquirectPoint(x, y, depth, width, height); for (int currentCamera = 0; currentCamera < int(rig.size()); currentCamera++) { Camera::Vector2 currentPixel; if (rig[currentCamera].sees(currentPoint, currentPixel)) { minX = floor(std::min(minX, x)); maxX = ceil(std::max(maxX, x)); minY = floor(std::min(minY, y)); maxY = ceil(std::max(maxY, y)); } } } } const size_t newHeight = FLAGS_height; const size_t newWidth = FLAGS_height / (maxY - minY) * (maxX - minX); Image equirectImage(newHeight, newWidth); for (double x = 0; x < newWidth; x++) { for (double y = 0; y < newHeight; y++) { Camera::Vector3 currentPoint = getEquirectPoint( x * (maxX - minX) / newWidth + minX, y * (maxY - minY) / newHeight + minY, depth, width, height); equirectImage(y, x) = getPixelColor(currentPoint, rig, images); } } return equirectImage; } // Returns angle between two 3-vectors double getRotationAngle(Camera::Vector3 v1, Camera::Vector3 v2, int signFactor) { double dotprod = v1.transpose() * v2; double magnitude = v1.norm() * v2.norm(); return magnitude == 0 ? 0 : signFactor * acos(dotprod / magnitude); } // Rotate rig so that selected camera is facing towards centered in the equirect void centerRig(Camera::Rig& rig, std::string camera_id) { const Camera::Vector3 centerOfEquirect(-1, 0, 0); const Camera::Vector3 upwards(0, 0, 1); // desired final upward orientation for camera const Camera::Vector3 translation(0, 0, 0); const Eigen::UniformScaling<double> scale(1); Camera::Real theta = 0; // angle around x axis Camera::Real psi = 0; // angle around y axis Camera::Real phi = 0; // angle around z axis // Yaw (rotation around z axis) const Camera& selectedCamera = Camera::findCameraById(camera_id, rig); Camera::Vector3 projectedOnXY = Camera::Vector3(selectedCamera.forward().x(), selectedCamera.forward().y(), 0); int phiFactor = selectedCamera.forward().y() > 0 ? 1 : -1; phi = getRotationAngle(centerOfEquirect, projectedOnXY, phiFactor); const Camera::Vector3 rotation1(0, 0, phi); rig = transformRig(rig, rotation1, translation, scale); // Pitch (rotation around y axis) const Camera& selectedCamera2 = Camera::findCameraById(camera_id, rig); Camera::Vector3 projectedOnXZ = Camera::Vector3(selectedCamera2.forward().x(), 0, selectedCamera2.forward().z()); int psiFactor = selectedCamera2.forward().z() > 0 ? -1 : 1; psi = getRotationAngle(centerOfEquirect, projectedOnXZ, psiFactor); const Camera::Vector3 rotation2(0, psi, 0); rig = transformRig(rig, rotation2, translation, scale); // Roll (rotation around x axis) const Camera& selectedCamera3 = Camera::findCameraById(camera_id, rig); Camera::Vector3 projectedOnYZ = Camera::Vector3(0, selectedCamera3.up().y(), selectedCamera3.up().z()); int thetaFactor = selectedCamera3.up().y() > 0 ? 1 : -1; theta = getRotationAngle(upwards, projectedOnYZ, thetaFactor); const Camera::Vector3 rotation3(theta, 0, 0); rig = transformRig(rig, rotation3, translation, scale); } int main(int argc, char* argv[]) { system_util::initDep(argc, argv, kUsageMessage); CHECK_NE(FLAGS_color, ""); CHECK_NE(FLAGS_rig, ""); CHECK_NE(FLAGS_output, ""); // Load camera rig. This allows us to use all the goodies in // calibration/Camera.h and util/ImageUtil.h Camera::Rig rig = filterDestinations(Camera::loadRig(FLAGS_rig), FLAGS_cameras); if (!FLAGS_camera_id.empty()) { centerRig(rig, FLAGS_camera_id); } LOG(INFO) << "Loading images..."; // Load input color images const std::vector<Image> images = loadScaledImages<cv::Vec4f>(FLAGS_color, rig, FLAGS_frame, FLAGS_scale); CHECK_GT(images.size(), 0) << "no images loaded!"; CHECK_EQ(images.size(), rig.size()); // Rescale cameras for (Camera& src : rig) { src = src.rescale(src.resolution * FLAGS_scale); } size_t height = FLAGS_height; size_t width = 2 * height; const float dispMin = 1.0f / FLAGS_depth_max; const float dispMax = 1.0f / FLAGS_depth_min; ThreadPool threadPool(FLAGS_threads); for (int i = FLAGS_num_depths - 1; i >= 0; --i) { threadPool.spawn([&, i] { const float fraction = float(i) / float(FLAGS_num_depths - 1); const float disp = FLAGS_num_depths == 1 ? dispMin : fraction * dispMin + (1 - fraction) * dispMax; const float depth = 1.0f / disp; LOG(INFO) << folly::sformat("Depth {} of {}...", (FLAGS_num_depths - i), FLAGS_num_depths); Image equirectImage; if (FLAGS_crop_equirect) { equirectImage = createCroppedEquirect(rig, images, height, width, depth); } else { equirectImage = createEquirect(rig, images, height, width, depth); } saveImage(equirectImage, depth); }); } threadPool.join(); return EXIT_SUCCESS; }