source/render/MeshUtil.h (325 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.
*/
#pragma once
#include <fstream>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <Eigen/Geometry>
#include "source/util/Camera.h"
#include "source/util/CvUtil.h"
#include "source/util/FilesystemUtil.h"
#include "source/util/MathUtil.h"
namespace fb360_dep {
namespace mesh_util {
Eigen::Vector3d calcBarycentrics(const Eigen::Vector2d& point, const Eigen::Matrix3d& triangle) {
const Eigen::Vector2d base = triangle.row(2).head<2>();
Eigen::Matrix2d m = triangle.topLeftCorner<2, 2>();
m.row(0) -= base;
m.row(1) -= base;
Eigen::Vector3d result;
result.head<2>() = m.transpose().colPivHouseholderQr().solve(point - base);
result.z() = 1 - result.x() - result.y();
return result;
}
inline void writePfm(
const cv::Mat_<float>& original,
const Camera::Vector2& resolution,
const Eigen::MatrixXd& vertexes,
const Eigen::MatrixXi& faces,
const filesystem::path& filenamePfm) {
LOG(INFO) << "Writing PFM file...";
// rasterize each face into dst
cv::Mat_<float> dst(original.size(), -FLT_MAX);
for (int face = 0; face < faces.rows(); ++face) {
Eigen::Matrix3d triangle;
for (int i = 0; i < triangle.rows(); ++i) {
triangle.row(i) = vertexes.row(faces(face, i));
}
// rescale x,y to original depth map's size
triangle.col(0) *= original.cols / resolution.x();
triangle.col(1) *= original.rows / resolution.y();
// crude rasterizer
Eigen::Vector3d bboxMin = triangle.colwise().minCoeff();
Eigen::Vector3d bboxMax = triangle.colwise().maxCoeff();
// for each pixel center in bounding box
for (int y = floor(bboxMin.y()); y < ceil(bboxMax.y()); ++y) {
for (int x = floor(bboxMin.x()); x < ceil(bboxMax.x()); ++x) {
// ignore rasterization rules, just include all edges
Eigen::Vector3d bary = calcBarycentrics({x + 0.5, y + 0.5}, triangle);
if (bary.x() >= 0 && bary.y() >= 0 && bary.z() >= 0) {
CHECK(0 <= x && x < dst.cols) << x << dst.cols;
CHECK(0 <= y && y < dst.rows) << y << dst.rows;
dst(y, x) = float(triangle.col(2).dot(bary));
}
}
}
}
cv_util::writeCvMat32FC1ToPFM(filenamePfm, dst);
}
inline void writeDepth(
const Eigen::MatrixXd& vertexes,
const Eigen::MatrixXi& faces,
const filesystem::path& fnVtx,
const filesystem::path& fnIdx) {
{
std::ofstream file(fnVtx.string(), std::ios::binary);
Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> v =
vertexes.cast<float>();
file.write(reinterpret_cast<char*>(v.data()), v.size() * sizeof(float));
}
{
std::ofstream file(fnIdx.string(), std::ios::binary);
Eigen::Matrix<uint32_t, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> f =
faces.cast<uint32_t>();
file.write(reinterpret_cast<char*>(f.data()), f.size() * sizeof(uint32_t));
}
}
inline void writeObj(
const Eigen::MatrixXd& vertexes,
const Eigen::MatrixXi& faces,
const filesystem::path& filenameObj,
const filesystem::path& filenameMtl = "") {
const bool st = vertexes.cols() == 5;
CHECK(vertexes.cols() == 3 || st) << "expected xyz or xyzst";
CHECK_EQ(st, !filenameMtl.empty()) << "texture coordinates and material go together";
FILE* fp = fopen(filenameObj.c_str(), "w");
CHECK(fp) << "file open failed: " << filenameObj;
if (!filenameMtl.empty()) {
fprintf(fp, "mtllib %s\nusemtl material\n", filenameMtl.c_str());
}
for (int i = 0; i < vertexes.rows(); ++i) {
// Use the shortest representation: %e or %f
fprintf(fp, "v %g %g %g\n", vertexes(i, 0), vertexes(i, 1), vertexes(i, 2));
if (st) {
fprintf(fp, "vt %g %g\n", vertexes(i, 3), vertexes(i, 4));
}
}
for (int i = 0; i < faces.rows(); ++i) {
// obj indexes are 1-based
if (!st) {
fprintf(fp, "f %d %d %d\n", faces(i, 0) + 1, faces(i, 1) + 1, faces(i, 2) + 1);
} else {
fprintf(
fp,
"f %d/%d %d/%d %d/%d\n",
faces(i, 0) + 1,
faces(i, 0) + 1,
faces(i, 1) + 1,
faces(i, 1) + 1,
faces(i, 2) + 1,
faces(i, 2) + 1);
}
}
fclose(fp);
}
inline std::string writeMtl(const filesystem::path& pathObj, const filesystem::path& pathColor) {
const std::string pathRelColor = filesystem::relative(pathColor, pathObj.parent_path()).string();
filesystem::path pathMtl(pathObj);
pathMtl.replace_extension(".mtl");
std::ofstream f(pathMtl.string());
f << "newmtl material" << std::endl;
f << "illum 0" << std::endl;
f << "Kd 1 1 1" << std::endl;
f << "map_Kd " << pathRelColor << std::endl;
return pathMtl.filename().string(); // just filename (with extension)
}
inline Eigen::MatrixXd readVertexes(const filesystem::path& fnVtx) {
uint64_t size = filesystem::file_size(fnVtx);
const int width = 3;
uint64_t height = size / (width * sizeof(float));
Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> v(height, width);
std::ifstream file(fnVtx.string(), std::ios::binary);
file.read(reinterpret_cast<char*>(v.data()), v.size() * sizeof(float));
return v.cast<double>();
}
inline Eigen::MatrixXi readFaces(const filesystem::path& fnIdx) {
uint64_t size = filesystem::file_size(fnIdx);
const int width = 3;
uint64_t height = size / (width * sizeof(int));
Eigen::Matrix<int, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor> v(height, width);
std::ifstream file(fnIdx.string(), std::ios::binary);
file.read(reinterpret_cast<char*>(v.data()), v.size() * sizeof(int));
return v;
}
// return a mask representing which of the 4 possible triangles to output
inline unsigned getTriangleMask(
const Eigen::MatrixXd& verts,
const int base,
const int width,
const float tearRatio,
const bool isRigCoordinates) {
const int tli = base;
const int tri = base + 1;
const int bli = base + width;
const int bri = base + width + 1;
// If isRigCoordinates, distance from rig center is the norm of the vector
// If individual camera, z-coord is distance from rig center
const double tl = isRigCoordinates ? verts.row(tli).norm() : verts(tli, 2);
const double tr = isRigCoordinates ? verts.row(tri).norm() : verts(tri, 2);
const double bl = isRigCoordinates ? verts.row(bli).norm() : verts(bli, 2);
const double br = isRigCoordinates ? verts.row(bri).norm() : verts(bri, 2);
std::vector<std::tuple<double, int>> v = {
std::make_tuple(tl, 0),
std::make_tuple(tr, 1),
std::make_tuple(bl, 2),
std::make_tuple(br, 3)};
sort(v.begin(), v.end());
// are all 4 values pretty close?
if (std::get<0>(v.front()) / std::get<0>(v.back()) > tearRatio) {
// output both triangles, splitting along the shallowest diagonal
if (std::abs(tl - br) < std::abs(tr - bl)) {
return 1 << 1 | 1 << 2; // triangles 1 and 2
}
return 1 << 0 | 1 << 3; // triangles 0 and 3
}
double lo = std::get<0>(v.front()) / std::get<0>(v[2]);
double hi = std::get<0>(v[1]) / std::get<0>(v.back());
// are the 3 lowest values pretty close?
if (lo >= tearRatio && lo > hi) {
// output the triangle that does not include back
int index = std::get<1>(v.back()) ^ 0x3;
return 1 << index;
}
// are the 3 highest values pretty close?
if (hi >= tearRatio) {
// output the triangle that does not include front
int index = std::get<1>(v.front()) ^ 0x3;
return 1 << index;
}
// don't output anything
return 0;
}
template <typename T>
inline void addTriangle(T&& face, const int which, int base, int width) {
// Triangles are defines counterclock-wise
switch (which) {
case 0: // top-left
face(0) = base + width;
face(1) = base + 1;
face(2) = base;
break;
case 1: // top-right
face(0) = base;
face(1) = base + width + 1;
face(2) = base + 1;
break;
case 2: // bottom-left
face(0) = base + width + 1;
face(1) = base;
face(2) = base + width;
break;
case 3: // bottom-right
face(0) = base + 1;
face(1) = base + width;
face(2) = base + width + 1;
break;
}
}
// Gets a set of vertexes and creates corresponding faces
// wrapHorizontally and isRigCoordinates explain the semantics of vertexes
// - wrapHorizontally: true to fix (link) meridian ends on equirect
// - isRigCoordinates: true if vertexes hold plain rig coords,
// false if vertex z represents distance from rig center
// examples of (wrapHorizontally, isRigCoordinates):
// - rig coordinate equirect mesh = (true, true)
// - rig coordinate camera mesh = (false, true)
// - equierror camera mesh = (false, false)
//
// tearRatio causes slivery triangles to be discarded if
// min(depth) / max(depth) < tearRatio
//
// a reasonable value to try is ~0.95, which means it won't connect
// vertexes if one is at 10 m while the neighbor is at 9.5 m
inline Eigen::MatrixXi getFaces(
const Eigen::MatrixXd& vertexes,
const int width,
const int height,
const bool wrapHorizontally,
const bool isRigCoordinates,
const float tearRatio = 0.0f) {
Eigen::MatrixXi faces(width * (height - 1) * 2, 3);
int face = 0;
for (int y = 0; y < height - 1; ++y) {
for (int x = 0; x < width - 1; ++x) {
int base = y * width + x;
unsigned mask = getTriangleMask(vertexes, base, width, tearRatio, isRigCoordinates);
for (int triangle = 0; triangle < 4; ++triangle) {
if ((mask >> triangle) & 1) {
addTriangle(faces.row(face++), triangle, base, width);
}
}
}
}
if (wrapHorizontally) {
// Link last and first longitudes
// Note how triangles are always defined counterclock-wise
for (int y = 0; y < height - 1; ++y) {
int base = y * width;
faces.row(face++) = Eigen::Vector3i(base + width, base, base + width - 1);
faces.row(face++) = Eigen::Vector3i(base + width - 1, base + 2 * width - 1, base + width);
}
}
return faces.topRows(face);
}
inline Eigen::MatrixXd getVertexesEquirect(const cv::Mat_<float>& disparity, const float maxDepth) {
const int width = disparity.cols;
const int height = disparity.rows;
Eigen::MatrixXd vertexes(width * height, 3);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const float u = float(x + 0.5) / float(width);
const float v = float(y + 0.5) / float(height);
const float theta = u * 2.0f * M_PI;
const float phi = v * M_PI;
const float depth = std::fmin(maxDepth, 1.0f / disparity(y, x));
vertexes.row(y * width + x) =
depth * Eigen::Vector3d(sin(phi) * cos(theta), cos(phi), sin(phi) * sin(theta));
}
}
return vertexes;
}
// for equi error discussion, see cameraMeshVS in RigScene.cpp
inline Eigen::MatrixXd getVertexesEquiError(const cv::Mat_<float>& depth, const Camera& camera) {
int width = depth.cols;
int height = depth.rows;
const double kRadius = 1; // change this to 100 if rig is in cm
const double scale = camera.getScalarFocal() * kRadius;
Eigen::MatrixXd vertexes(width * height, 3);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// equi-error coordinates
Eigen::Vector3d equi(
camera.resolution.x() / width * (x + 0.5),
camera.resolution.y() / height * (y + 0.5),
scale / depth(y, x));
// actual rig coordinates would be
// Camera::Vector2 pixel(x + 0.5, y + 0.5);
// Camera::Vector3 rig = camera.rig(pixel).pointAt(depth(y, x));
int i = y * width + x;
for (int c = 0; c < equi.size(); ++c) {
vertexes(i, c) = equi[c];
}
}
}
return vertexes;
}
// Assumes vertexes have been generated in order from a depth map stored row-major so that
// mask(y, x) corresponds to vertexes(y * mask.cols + x)
inline void applyMaskToVertexesAndFaces(
Eigen::MatrixXd& vertexes,
Eigen::MatrixXi& faces,
const cv::Mat_<bool> mask) {
const int width = mask.cols;
const int height = mask.rows;
CHECK_EQ(width * height, vertexes.rows());
bool* originalVertexMask = (bool*)mask.data;
// Keep faces only if all vertexes have a non-zero mask
std::vector<int> outputFaceIndexes;
for (int i = 0; i < faces.rows(); ++i) {
const Eigen::Vector3i& face = faces.row(i);
bool allVertexesRetained = true;
for (int j = 0; j < face.size(); ++j) {
if (!originalVertexMask[face(j)]) {
allVertexesRetained = false;
break;
}
}
if (allVertexesRetained) {
outputFaceIndexes.push_back(i);
}
}
// Keep only vertexes of retained faces
std::vector<bool> vertexMask(vertexes.rows(), false);
for (int i : outputFaceIndexes) {
const Eigen::Vector3i& face = faces.row(i);
for (int j = 0; j < face.size(); ++j) {
vertexMask[face(j)] = true;
}
}
// Compute mapping between old and new vertex indices
std::map<int, int> inputToOutputVertexIndexes;
int numOutputVertexes = 0;
for (int i = 0; i < vertexes.rows(); ++i) {
if (vertexMask[i]) {
inputToOutputVertexIndexes[i] = numOutputVertexes++;
}
}
// create output vertex matrix
Eigen::MatrixXd inputVertexes = vertexes;
vertexes = Eigen::MatrixXd(numOutputVertexes, 3);
for (auto iter = inputToOutputVertexIndexes.begin(); iter != inputToOutputVertexIndexes.end();
++iter) {
vertexes.row(iter->second) = inputVertexes.row(iter->first);
}
// Create output face matrix and re-index vertexes
Eigen::MatrixXi inputFaces = faces;
faces = Eigen::MatrixXi(outputFaceIndexes.size(), 3);
for (ssize_t i = 0; i < ssize(outputFaceIndexes); ++i) {
const Eigen::Vector3i& face = inputFaces.row(outputFaceIndexes[i]);
for (int j = 0; j < face.size(); ++j) {
faces(i, j) = inputToOutputVertexIndexes[face(j)];
}
}
}
// Add texture coordinates to vertexes
inline void addTextureCoordinatesEquirect(Eigen::MatrixXd& vertexes) {
vertexes.conservativeResize(Eigen::NoChange, 5);
for (int v = 0; v < vertexes.rows(); ++v) {
// similar to createEquirectProgram() in ParallaxViewer.cpp:
// texture goes +x, +z, -x, -z, +x from left to right (0 to 1)
// and -y to +y from top to bottom (0 to 1)
Eigen::Vector3d pos = vertexes.row(v).head<3>();
const double xzNorm = Eigen::Vector2d(pos.x(), pos.z()).norm();
vertexes(v, 3) = std::atan2(-pos.z(), -pos.x()) * 0.5 / M_PI + 0.5;
vertexes(v, 4) = -std::atan2(-pos.y(), xzNorm) / M_PI + 0.5;
}
}
} // namespace mesh_util
} // namespace fb360_dep