| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | #include "mvs/fusion.h" |
| |
|
| | #include "util/misc.h" |
| |
|
| | namespace colmap { |
| | namespace mvs { |
| | namespace internal { |
| |
|
| | template <typename T> |
| | float Median(std::vector<T>* elems) { |
| | CHECK(!elems->empty()); |
| | const size_t mid_idx = elems->size() / 2; |
| | std::nth_element(elems->begin(), elems->begin() + mid_idx, elems->end()); |
| | if (elems->size() % 2 == 0) { |
| | const float mid_element1 = static_cast<float>((*elems)[mid_idx]); |
| | const float mid_element2 = static_cast<float>( |
| | *std::max_element(elems->begin(), elems->begin() + mid_idx)); |
| | return (mid_element1 + mid_element2) / 2.0f; |
| | } else { |
| | return static_cast<float>((*elems)[mid_idx]); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | int FindNextImage(const std::vector<std::vector<int>>& overlapping_images, |
| | const std::vector<char>& used_images, |
| | const std::vector<char>& fused_images, |
| | const int prev_image_idx) { |
| | CHECK_EQ(used_images.size(), fused_images.size()); |
| |
|
| | for (const auto image_idx : overlapping_images.at(prev_image_idx)) { |
| | if (used_images.at(image_idx) && !fused_images.at(image_idx)) { |
| | return image_idx; |
| | } |
| | } |
| |
|
| | |
| | |
| | for (size_t image_idx = 0; image_idx < fused_images.size(); ++image_idx) { |
| | if (used_images[image_idx] && !fused_images[image_idx]) { |
| | return image_idx; |
| | } |
| | } |
| |
|
| | return -1; |
| | } |
| |
|
| | } |
| |
|
| | void StereoFusionOptions::Print() const { |
| | #define PrintOption(option) std::cout << #option ": " << option << std::endl |
| | PrintHeading2("StereoFusion::Options"); |
| | PrintOption(mask_path); |
| | PrintOption(max_image_size); |
| | PrintOption(min_num_pixels); |
| | PrintOption(max_num_pixels); |
| | PrintOption(max_traversal_depth); |
| | PrintOption(max_reproj_error); |
| | PrintOption(max_depth_error); |
| | PrintOption(max_normal_error); |
| | PrintOption(check_num_images); |
| | PrintOption(use_cache); |
| | PrintOption(cache_size); |
| | const auto& bbox_min = bounding_box.first.transpose().eval(); |
| | const auto& bbox_max = bounding_box.second.transpose().eval(); |
| | PrintOption(bbox_min); |
| | PrintOption(bbox_max); |
| | #undef PrintOption |
| | } |
| |
|
| | bool StereoFusionOptions::Check() const { |
| | CHECK_OPTION_GE(min_num_pixels, 0); |
| | CHECK_OPTION_LE(min_num_pixels, max_num_pixels); |
| | CHECK_OPTION_GT(max_traversal_depth, 0); |
| | CHECK_OPTION_GE(max_reproj_error, 0); |
| | CHECK_OPTION_GE(max_depth_error, 0); |
| | CHECK_OPTION_GE(max_normal_error, 0); |
| | CHECK_OPTION_GT(check_num_images, 0); |
| | CHECK_OPTION_GT(cache_size, 0); |
| | return true; |
| | } |
| |
|
| | StereoFusion::StereoFusion(const StereoFusionOptions& options, |
| | const std::string& workspace_path, |
| | const std::string& workspace_format, |
| | const std::string& pmvs_option_name, |
| | const std::string& input_type) |
| | : options_(options), |
| | workspace_path_(workspace_path), |
| | workspace_format_(workspace_format), |
| | pmvs_option_name_(pmvs_option_name), |
| | input_type_(input_type), |
| | max_squared_reproj_error_(options_.max_reproj_error * |
| | options_.max_reproj_error), |
| | min_cos_normal_error_(std::cos(DegToRad(options_.max_normal_error))) { |
| | CHECK(options_.Check()); |
| | } |
| |
|
| | const std::vector<PlyPoint>& StereoFusion::GetFusedPoints() const { |
| | return fused_points_; |
| | } |
| |
|
| | const std::vector<std::vector<int>>& StereoFusion::GetFusedPointsVisibility() |
| | const { |
| | return fused_points_visibility_; |
| | } |
| |
|
| | void StereoFusion::Run() { |
| | fused_points_.clear(); |
| | fused_points_visibility_.clear(); |
| |
|
| | options_.Print(); |
| | std::cout << std::endl; |
| |
|
| | std::cout << "Reading workspace..." << std::endl; |
| |
|
| | Workspace::Options workspace_options; |
| |
|
| | auto workspace_format_lower_case = workspace_format_; |
| | StringToLower(&workspace_format_lower_case); |
| | if (workspace_format_lower_case == "pmvs") { |
| | workspace_options.stereo_folder = |
| | StringPrintf("stereo-%s", pmvs_option_name_.c_str()); |
| | } |
| |
|
| | workspace_options.num_threads = options_.num_threads; |
| | workspace_options.max_image_size = options_.max_image_size; |
| | workspace_options.image_as_rgb = true; |
| | workspace_options.cache_size = options_.cache_size; |
| | workspace_options.workspace_path = workspace_path_; |
| | workspace_options.workspace_format = workspace_format_; |
| | workspace_options.input_type = input_type_; |
| |
|
| | const auto image_names = ReadTextFileLines(JoinPaths( |
| | workspace_path_, workspace_options.stereo_folder, "fusion.cfg")); |
| | int num_threads = 1; |
| | if (options_.use_cache) { |
| | workspace_ = std::make_unique<CachedWorkspace>(workspace_options); |
| | } else { |
| | workspace_ = std::make_unique<Workspace>(workspace_options); |
| | workspace_->Load(image_names); |
| | num_threads = GetEffectiveNumThreads(options_.num_threads); |
| | } |
| |
|
| | if (IsStopped()) { |
| | GetTimer().PrintMinutes(); |
| | return; |
| | } |
| |
|
| | std::cout << "Reading configuration..." << std::endl; |
| |
|
| | const auto& model = workspace_->GetModel(); |
| |
|
| | const double kMinTriangulationAngle = 0; |
| | if (model.GetMaxOverlappingImagesFromPMVS().empty()) { |
| | overlapping_images_ = model.GetMaxOverlappingImages( |
| | options_.check_num_images, kMinTriangulationAngle); |
| | } else { |
| | overlapping_images_ = model.GetMaxOverlappingImagesFromPMVS(); |
| | } |
| |
|
| | task_fused_points_.resize(num_threads); |
| | task_fused_points_visibility_.resize(num_threads); |
| |
|
| | used_images_.resize(model.images.size(), false); |
| | fused_images_.resize(model.images.size(), false); |
| | fused_pixel_masks_.resize(model.images.size()); |
| | depth_map_sizes_.resize(model.images.size()); |
| | bitmap_scales_.resize(model.images.size()); |
| | P_.resize(model.images.size()); |
| | inv_P_.resize(model.images.size()); |
| | inv_R_.resize(model.images.size()); |
| |
|
| | for (const auto& image_name : image_names) { |
| | const int image_idx = model.GetImageIdx(image_name); |
| |
|
| | if (!workspace_->HasBitmap(image_idx) || |
| | !workspace_->HasDepthMap(image_idx) || |
| | !workspace_->HasNormalMap(image_idx)) { |
| | std::cout |
| | << StringPrintf( |
| | "WARNING: Ignoring image %s, because input does not exist.", |
| | image_name.c_str()) |
| | << std::endl; |
| | continue; |
| | } |
| |
|
| | const auto& image = model.images.at(image_idx); |
| | const auto& depth_map = workspace_->GetDepthMap(image_idx); |
| |
|
| | used_images_.at(image_idx) = true; |
| |
|
| | InitFusedPixelMask(image_idx, depth_map.GetWidth(), depth_map.GetHeight()); |
| |
|
| | depth_map_sizes_.at(image_idx) = |
| | std::make_pair(depth_map.GetWidth(), depth_map.GetHeight()); |
| |
|
| | bitmap_scales_.at(image_idx) = std::make_pair( |
| | static_cast<float>(depth_map.GetWidth()) / image.GetWidth(), |
| | static_cast<float>(depth_map.GetHeight()) / image.GetHeight()); |
| |
|
| | Eigen::Matrix<float, 3, 3, Eigen::RowMajor> K = |
| | Eigen::Map<const Eigen::Matrix<float, 3, 3, Eigen::RowMajor>>( |
| | image.GetK()); |
| | K(0, 0) *= bitmap_scales_.at(image_idx).first; |
| | K(0, 2) *= bitmap_scales_.at(image_idx).first; |
| | K(1, 1) *= bitmap_scales_.at(image_idx).second; |
| | K(1, 2) *= bitmap_scales_.at(image_idx).second; |
| |
|
| | ComposeProjectionMatrix(K.data(), image.GetR(), image.GetT(), |
| | P_.at(image_idx).data()); |
| | ComposeInverseProjectionMatrix(K.data(), image.GetR(), image.GetT(), |
| | inv_P_.at(image_idx).data()); |
| | inv_R_.at(image_idx) = |
| | Eigen::Map<const Eigen::Matrix<float, 3, 3, Eigen::RowMajor>>( |
| | image.GetR()) |
| | .transpose(); |
| | } |
| |
|
| | std::cout << StringPrintf("Starting fusion with %d threads", num_threads) |
| | << std::endl; |
| | ThreadPool thread_pool(num_threads); |
| |
|
| | |
| | |
| | |
| | const int kRowStride = 10; |
| | auto ProcessImageRows = [&, this](const int row_start, const int height, |
| | const int width, const int image_idx, |
| | const Mat<char>& fused_pixel_mask) { |
| | const int row_end = std::min(height, row_start + kRowStride); |
| | for (int row = row_start; row < row_end; ++row) { |
| | for (int col = 0; col < width; ++col) { |
| | if (fused_pixel_mask.Get(row, col) > 0) { |
| | continue; |
| | } |
| | const int thread_id = thread_pool.GetThreadIndex(); |
| | Fuse(thread_id, image_idx, row, col); |
| | } |
| | } |
| | }; |
| |
|
| | size_t num_fused_images = 0; |
| | size_t total_fused_points = 0; |
| | for (int image_idx = 0; image_idx >= 0; |
| | image_idx = internal::FindNextImage(overlapping_images_, used_images_, |
| | fused_images_, image_idx)) { |
| | if (IsStopped()) { |
| | break; |
| | } |
| |
|
| | Timer timer; |
| | timer.Start(); |
| |
|
| | std::cout << StringPrintf("Fusing image [%d/%d] with index %d", |
| | num_fused_images + 1, model.images.size(), |
| | image_idx) |
| | << std::flush; |
| |
|
| | const int width = depth_map_sizes_.at(image_idx).first; |
| | const int height = depth_map_sizes_.at(image_idx).second; |
| | const auto& fused_pixel_mask = fused_pixel_masks_.at(image_idx); |
| |
|
| | for (int row_start = 0; row_start < height; row_start += kRowStride) { |
| | thread_pool.AddTask(ProcessImageRows, row_start, height, width, image_idx, |
| | fused_pixel_mask); |
| | } |
| | thread_pool.Wait(); |
| |
|
| | num_fused_images += 1; |
| | fused_images_.at(image_idx) = true; |
| |
|
| | total_fused_points = 0; |
| | for (const auto& task_fused_points : task_fused_points_) { |
| | total_fused_points += task_fused_points.size(); |
| | } |
| | std::cout << StringPrintf(" in %.3fs (%d points)", timer.ElapsedSeconds(), |
| | total_fused_points) |
| | << std::endl; |
| | } |
| |
|
| | fused_points_.reserve(total_fused_points); |
| | fused_points_visibility_.reserve(total_fused_points); |
| | for (size_t thread_id = 0; thread_id < task_fused_points_.size(); |
| | ++thread_id) { |
| | fused_points_.insert(fused_points_.end(), |
| | task_fused_points_[thread_id].begin(), |
| | task_fused_points_[thread_id].end()); |
| | task_fused_points_[thread_id].clear(); |
| |
|
| | fused_points_visibility_.insert( |
| | fused_points_visibility_.end(), |
| | task_fused_points_visibility_[thread_id].begin(), |
| | task_fused_points_visibility_[thread_id].end()); |
| | task_fused_points_visibility_[thread_id].clear(); |
| | } |
| |
|
| | if (fused_points_.empty()) { |
| | std::cout << "WARNING: Could not fuse any points. This is likely caused by " |
| | "incorrect settings - filtering must be enabled for the last " |
| | "call to patch match stereo." |
| | << std::endl; |
| | } |
| |
|
| | std::cout << "Number of fused points: " << fused_points_.size() << std::endl; |
| | GetTimer().PrintMinutes(); |
| | } |
| |
|
| | void StereoFusion::InitFusedPixelMask(int image_idx, size_t width, |
| | size_t height) { |
| | Bitmap mask; |
| | Mat<char>& fused_pixel_mask = fused_pixel_masks_.at(image_idx); |
| | const std::string mask_path = |
| | JoinPaths(options_.mask_path, |
| | workspace_->GetModel().GetImageName(image_idx) + ".png"); |
| | fused_pixel_mask = Mat<char>(width, height, 1); |
| | if (!options_.mask_path.empty() && ExistsFile(mask_path) && |
| | mask.Read(mask_path, false)) { |
| | BitmapColor<uint8_t> color; |
| | mask.Rescale(static_cast<int>(width), static_cast<int>(height), FILTER_BOX); |
| | for (size_t row = 0; row < height; ++row) { |
| | for (size_t col = 0; col < width; ++col) { |
| | mask.GetPixel(col, row, &color); |
| | fused_pixel_mask.Set(row, col, color.r == 0 ? 1 : 0); |
| | } |
| | } |
| | } else { |
| | fused_pixel_mask.Fill(0); |
| | } |
| | } |
| |
|
| | void StereoFusion::Fuse(const int thread_id, const int image_idx, const int row, |
| | const int col) { |
| | |
| | std::vector<FusionData> fusion_queue; |
| | fusion_queue.emplace_back(image_idx, row, col, 0); |
| |
|
| | Eigen::Vector4f fused_ref_point = Eigen::Vector4f::Zero(); |
| | Eigen::Vector3f fused_ref_normal = Eigen::Vector3f::Zero(); |
| |
|
| | |
| | std::vector<float> fused_point_x; |
| | std::vector<float> fused_point_y; |
| | std::vector<float> fused_point_z; |
| | std::vector<float> fused_point_nx; |
| | std::vector<float> fused_point_ny; |
| | std::vector<float> fused_point_nz; |
| | std::vector<uint8_t> fused_point_r; |
| | std::vector<uint8_t> fused_point_g; |
| | std::vector<uint8_t> fused_point_b; |
| | std::unordered_set<int> fused_point_visibility; |
| |
|
| | while (!fusion_queue.empty()) { |
| | const auto data = fusion_queue.back(); |
| | const int image_idx = data.image_idx; |
| | const int row = data.row; |
| | const int col = data.col; |
| | const int traversal_depth = data.traversal_depth; |
| |
|
| | fusion_queue.pop_back(); |
| |
|
| | |
| | auto& fused_pixel_mask = fused_pixel_masks_.at(image_idx); |
| | if (fused_pixel_mask.Get(row, col) > 0) { |
| | continue; |
| | } |
| |
|
| | const auto& depth_map = workspace_->GetDepthMap(image_idx); |
| | const float depth = depth_map.Get(row, col); |
| |
|
| | |
| | if (depth <= 0.0f) { |
| | continue; |
| | } |
| |
|
| | |
| | |
| | if (traversal_depth > 0) { |
| | |
| | const Eigen::Vector3f proj = P_.at(image_idx) * fused_ref_point; |
| |
|
| | |
| | const float depth_error = std::abs((proj(2) - depth) / depth); |
| | if (depth_error > options_.max_depth_error) { |
| | continue; |
| | } |
| |
|
| | |
| | const float col_diff = proj(0) / proj(2) - col; |
| | const float row_diff = proj(1) / proj(2) - row; |
| | const float squared_reproj_error = |
| | col_diff * col_diff + row_diff * row_diff; |
| | if (squared_reproj_error > max_squared_reproj_error_) { |
| | continue; |
| | } |
| | } |
| |
|
| | |
| | const auto& normal_map = workspace_->GetNormalMap(image_idx); |
| | const Eigen::Vector3f normal = |
| | inv_R_.at(image_idx) * Eigen::Vector3f(normal_map.Get(row, col, 0), |
| | normal_map.Get(row, col, 1), |
| | normal_map.Get(row, col, 2)); |
| |
|
| | |
| | if (traversal_depth > 0) { |
| | const float cos_normal_error = fused_ref_normal.dot(normal); |
| | if (cos_normal_error < min_cos_normal_error_) { |
| | continue; |
| | } |
| | } |
| |
|
| | |
| | const Eigen::Vector3f xyz = |
| | inv_P_.at(image_idx) * |
| | Eigen::Vector4f(col * depth, row * depth, depth, 1.0f); |
| |
|
| | |
| | BitmapColor<uint8_t> color; |
| | const auto& bitmap_scale = bitmap_scales_.at(image_idx); |
| | workspace_->GetBitmap(image_idx).InterpolateNearestNeighbor( |
| | col / bitmap_scale.first, row / bitmap_scale.second, &color); |
| |
|
| | |
| | fused_pixel_mask.Set(row, col, 1); |
| |
|
| | |
| | if (xyz(0) < options_.bounding_box.first(0) || |
| | xyz(1) < options_.bounding_box.first(1) || |
| | xyz(2) < options_.bounding_box.first(2) || |
| | xyz(0) > options_.bounding_box.second(0) || |
| | xyz(1) > options_.bounding_box.second(1) || |
| | xyz(2) > options_.bounding_box.second(2)) { |
| | continue; |
| | } |
| |
|
| | |
| | fused_point_x.push_back(xyz(0)); |
| | fused_point_y.push_back(xyz(1)); |
| | fused_point_z.push_back(xyz(2)); |
| | fused_point_nx.push_back(normal(0)); |
| | fused_point_ny.push_back(normal(1)); |
| | fused_point_nz.push_back(normal(2)); |
| | fused_point_r.push_back(color.r); |
| | fused_point_g.push_back(color.g); |
| | fused_point_b.push_back(color.b); |
| | fused_point_visibility.insert(image_idx); |
| |
|
| | |
| | if (traversal_depth == 0) { |
| | fused_ref_point = Eigen::Vector4f(xyz(0), xyz(1), xyz(2), 1.0f); |
| | fused_ref_normal = normal; |
| | } |
| |
|
| | if (fused_point_x.size() >= static_cast<size_t>(options_.max_num_pixels)) { |
| | break; |
| | } |
| |
|
| | if (traversal_depth >= options_.max_traversal_depth - 1) { |
| | continue; |
| | } |
| |
|
| | for (const auto next_image_idx : overlapping_images_.at(image_idx)) { |
| | if (!used_images_.at(next_image_idx) || |
| | fused_images_.at(next_image_idx)) { |
| | continue; |
| | } |
| |
|
| | const Eigen::Vector3f next_proj = |
| | P_.at(next_image_idx) * xyz.homogeneous(); |
| | const int next_col = |
| | static_cast<int>(std::round(next_proj(0) / next_proj(2))); |
| | const int next_row = |
| | static_cast<int>(std::round(next_proj(1) / next_proj(2))); |
| |
|
| | const auto& depth_map_size = depth_map_sizes_.at(next_image_idx); |
| | if (next_col < 0 || next_row < 0 || next_col >= depth_map_size.first || |
| | next_row >= depth_map_size.second) { |
| | continue; |
| | } |
| | fusion_queue.emplace_back(next_image_idx, next_row, next_col, |
| | traversal_depth + 1); |
| | } |
| | } |
| |
|
| | const size_t num_pixels = fused_point_x.size(); |
| | if (num_pixels >= static_cast<size_t>(options_.min_num_pixels)) { |
| | PlyPoint fused_point; |
| |
|
| | Eigen::Vector3f fused_normal; |
| | fused_normal.x() = internal::Median(&fused_point_nx); |
| | fused_normal.y() = internal::Median(&fused_point_ny); |
| | fused_normal.z() = internal::Median(&fused_point_nz); |
| | const float fused_normal_norm = fused_normal.norm(); |
| | if (fused_normal_norm < std::numeric_limits<float>::epsilon()) { |
| | return; |
| | } |
| |
|
| | fused_point.x = internal::Median(&fused_point_x); |
| | fused_point.y = internal::Median(&fused_point_y); |
| | fused_point.z = internal::Median(&fused_point_z); |
| |
|
| | fused_point.nx = fused_normal.x() / fused_normal_norm; |
| | fused_point.ny = fused_normal.y() / fused_normal_norm; |
| | fused_point.nz = fused_normal.z() / fused_normal_norm; |
| |
|
| | fused_point.r = TruncateCast<float, uint8_t>( |
| | std::round(internal::Median(&fused_point_r))); |
| | fused_point.g = TruncateCast<float, uint8_t>( |
| | std::round(internal::Median(&fused_point_g))); |
| | fused_point.b = TruncateCast<float, uint8_t>( |
| | std::round(internal::Median(&fused_point_b))); |
| |
|
| | task_fused_points_[thread_id].push_back(fused_point); |
| | task_fused_points_visibility_[thread_id].emplace_back( |
| | fused_point_visibility.begin(), fused_point_visibility.end()); |
| | } |
| | } |
| |
|
| | void WritePointsVisibility( |
| | const std::string& path, |
| | const std::vector<std::vector<int>>& points_visibility) { |
| | std::fstream file(path, std::ios::out | std::ios::binary); |
| | CHECK(file.is_open()) << path; |
| |
|
| | WriteBinaryLittleEndian<uint64_t>(&file, points_visibility.size()); |
| |
|
| | for (const auto& visibility : points_visibility) { |
| | WriteBinaryLittleEndian<uint32_t>(&file, visibility.size()); |
| | for (const auto& image_idx : visibility) { |
| | WriteBinaryLittleEndian<uint32_t>(&file, image_idx); |
| | } |
| | } |
| | } |
| |
|
| | } |
| | } |
| |
|