source/conversion/ExportPointCloud.cpp (163 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. */ const char* kUsage = R"( - Reads a set of color and disparity images and produces an ascii file with a single point per line Each line contains "x y z 1 r g b", where - x y z is the position (in meters) - r g b is the color (0..255) The format can be imported as a .txt into meshlab with File -> Import Mesh set Separator to "SPACE" and set Point format to "X Y Z Reflectance R G B" - Example: ./ExportPointCloud \ --output=/path/to/video/output \ --color=/path/to/video/color \ --disparity=/path/to/output/disparity \ --rig=/path/to/rigs/rig.json \ --frame=000000 )"; #include <gflags/gflags.h> #include <glog/logging.h> #include "source/util/Camera.h" #include "source/util/ImageUtil.h" #include "source/util/SystemUtil.h" #include "source/util/ThreadPool.h" using namespace fb360_dep; using namespace fb360_dep::image_util; DEFINE_string(cameras, "", "comma-separated cameras to render (empty for all)"); DEFINE_bool(clip, false, "points beyond max_depth are clipped, not clamped"); DEFINE_string(color, "", "path to input color images (required)"); DEFINE_string(disparity, "", "path to disparity files (.pfm) (required)"); DEFINE_string(frame, "000000", "frame to process (lexical)"); DEFINE_bool(header_count, true, "add point count to the start of the file"); DEFINE_double(max_depth, INFINITY, "depth is clamped to this value (m). Use e.g. 20 to visualize"); DEFINE_string(output, "", "output filename (required)"); DEFINE_string(rig, "", "path to camera rig .json (required)"); DEFINE_int32(subsample, 1, "how often we sample (>= 1)"); DEFINE_int32(threads, -1, "number of threads (-1 = auto, 0 = none)"); // World coordinate (xyz) and corresponding color (rgb) using WorldColor = Eigen::Array<float, 6, 1>; void verifyInputs(const Camera::Rig& rig) { CHECK_NE(FLAGS_threads, 0); CHECK_NE(FLAGS_color, ""); CHECK_NE(FLAGS_disparity, ""); CHECK_NE(FLAGS_output, ""); verifyImagePaths(FLAGS_color, rig, FLAGS_frame, FLAGS_frame); verifyImagePaths(FLAGS_disparity, rig, FLAGS_frame, FLAGS_frame, ".pfm"); } std::vector<WorldColor> getPoints(const Camera& cam) { const std::string& camId = cam.id; LOG(INFO) << folly::sformat("Processing camera {}...", camId); // Load disparity and color, and resize color to size of disparity const cv::Mat_<float> disparity = loadImage<float>(FLAGS_disparity, camId, FLAGS_frame); // Load color (resize it to match disparity) const cv::Mat_<cv::Vec3f> color = loadResizedImage<cv::Vec3f>(FLAGS_color, camId, FLAGS_frame, disparity.size()); // Rescale camera as disparity may not be full resolution const int w = disparity.cols; const int h = disparity.rows; const Camera camRescale = cam.rescale({w, h}); // Transform each pixel to world coordinates and append to result std::vector<WorldColor> points(w * h); std::vector<bool> idxToDelete(w * h, false); ThreadPool threadPool(FLAGS_threads); const int edgeX = w; const int edgeY = 1; for (int yBegin = 0; yBegin < h; yBegin += edgeY) { for (int xBegin = 0; xBegin < w; xBegin += edgeX) { const int xEnd = std::min(xBegin + edgeX, w); const int yEnd = std::min(yBegin + edgeY, h); threadPool.spawn([&, xBegin, yBegin, xEnd, yEnd] { for (int y = yBegin; y < yEnd; ++y) { for (int x = xBegin; x < xEnd; ++x) { const int idx = w * y + x; if (FLAGS_subsample > 1 && rand() % FLAGS_subsample != 0) { idxToDelete[idx] = true; continue; // only retain 1 in subsample points } Camera::Vector2 pixel = {x + 0.5, y + 0.5}; if (camRescale.isOutsideImageCircle(pixel)) { idxToDelete[idx] = true; continue; } WorldColor worldColor; const double m = 1 / disparity(y, x); Camera::Vector3 world = camRescale.rig(pixel, m); const Camera::Real depth = world.norm(); if (depth > FLAGS_max_depth) { if (FLAGS_clip) { idxToDelete[idx] = true; continue; } world *= FLAGS_max_depth / depth; } worldColor.head<3>() = world.cast<float>(); const cv::Vec3f c = color(y, x); worldColor.tail<3>() = Eigen::Array3f(c[2], c[1], c[0]); points[y * w + x] = worldColor; } } }); } } threadPool.join(); std::vector<WorldColor> pointsFiltered; for (ssize_t i = 0; i < ssize(points); ++i) { if (!idxToDelete[i]) { pointsFiltered.push_back(points[i]); } } return pointsFiltered; } void writePoints( std::ofstream& file, const std::vector<std::vector<WorldColor>>& pointClouds, const int lines) { LOG(INFO) << folly::format("Writing {} points to file...", lines); // Merge all points std::vector<WorldColor> pointsAll; pointsAll.reserve(lines); // preallocate memory for (const std::vector<WorldColor>& points : pointClouds) { pointsAll.insert(pointsAll.end(), points.begin(), points.end()); } ThreadPool threadPool(FLAGS_threads); const int threads = threadPool.getMaxThreads(); const int pointsPerThread = std::ceil((float)pointsAll.size() / threads); std::vector<std::stringstream> filestreams(threads); for (int i = 0; i < threads; ++i) { const int start = i * pointsPerThread; const int end = std::min((i + 1) * pointsPerThread - 1, int(pointsAll.size()) - 1); threadPool.spawn([&, i, start, end] { std::stringstream fileI; for (int j = start; j <= end; ++j) { // A line in a pts file represents x y z "intensity" r g b // x y z are in meters, we arbitrarily set "intensity" to 1, and rgb is between 0 and 255 const WorldColor& point = pointsAll[j]; fileI << folly::format("{} {} {} 1 ", point[0], point[1], point[2]); fileI << folly::format( "{:.0f} {:.0f} {:.0f}\n", 255 * point[3], 255 * point[4], 255 * point[5]); } filestreams[i] = std::move(fileI); }); } threadPool.join(); LOG(INFO) << "Merging files..."; for (const auto& filestream : filestreams) { file << filestream.rdbuf(); } } int main(int argc, char** argv) { gflags::SetUsageMessage(kUsage); system_util::initDep(argc, argv); CHECK_NE(FLAGS_rig, ""); const Camera::Rig rig = filterDestinations(Camera::loadRig(FLAGS_rig), FLAGS_cameras); verifyInputs(rig); std::vector<std::vector<WorldColor>> pointClouds; for (const Camera& cam : rig) { pointClouds.push_back(getPoints(cam)); } const filesystem::path fnOut = filesystem::path(FLAGS_output); filesystem::create_directories(fnOut.parent_path()); std::ofstream file(fnOut.string()); CHECK(file.is_open()) << folly::sformat("Cannot open file for writing: {}", fnOut.string()); int lines = 0; for (const std::vector<WorldColor>& pointCloud : pointClouds) { lines += pointCloud.size(); } if (FLAGS_header_count) { file << folly::format("{}\n", lines); } writePoints(file, pointClouds, lines); LOG(INFO) << folly::format("{} lines written", lines); return EXIT_SUCCESS; }