|
| 1 | + |
| 2 | +#include "ArgumentParser.h" |
| 3 | +#include <assm/Grid.h> |
| 4 | +#include <assm/algorithms/PhotometricRemeshing.h> |
| 5 | +#include <assm/algorithms/Integration.h> |
| 6 | + |
| 7 | +#include <Eigen/Dense> |
| 8 | +#include <opencv2/opencv.hpp> |
| 9 | + |
| 10 | +#include <iostream> |
| 11 | +#include <chrono> |
| 12 | + |
| 13 | +using namespace std::chrono; |
| 14 | +using namespace std; |
| 15 | + |
| 16 | +void print_help_message() |
| 17 | +{ |
| 18 | + std::cout << "Options\n"; |
| 19 | + std::cout << " -n <path-to-normal-map> = Path to the normal maps (as .exr file)\n"; |
| 20 | + std::cout << " -e <approximation-error> = Desired approximation error (orthographic: pixels, perspective: mm)\n"; |
| 21 | + std::cout << " -t <path-to-triangle-mesh> = Path to write the output mesh to (we recommend using .obj files)\n"; |
| 22 | + std::cout << "\n"; |
| 23 | + std::cout << " -m <path-to-foreground-mask> = Path to foreground mask (as b/w .png file, optional) \n"; |
| 24 | + std::cout << " -l <lower-limit> = Minimal allowed edge length (optional, default: 1px)\n"; |
| 25 | + std::cout << " -h <higher-limit> = Maximal allowed edge length (optional, default: 100px)\n"; |
| 26 | + |
| 27 | + std::cout << "\nYou can switch from orthographic to perspective projection by supplying intrinsics\n"; |
| 28 | + std::cout << " | -x 0 -u |\n"; |
| 29 | + std::cout << " | 0 -y -v |\n"; |
| 30 | + std::cout << " | 0 0 1 |\n"; |
| 31 | + std::cout << "\nPlease Note: In orthographic mode, all lengths (approximation error, minimal and maximal lengths) are in pixels.\nIn perspective mode, we use millimeters instead.\n"; |
| 32 | +} |
| 33 | + |
| 34 | +bool saveObj(const char *filename, pmp::SurfaceMesh &mesh) { |
| 35 | + FILE *fp = fopen(filename, "wb"); |
| 36 | + if(!fp) { |
| 37 | + cerr << "Could not open file: " << filename << endl; |
| 38 | + return false; |
| 39 | + } |
| 40 | + for(auto vertex: mesh.vertices()) { |
| 41 | + auto p = mesh.position(vertex); |
| 42 | + fprintf(fp, "v %f %f %f\n", p[0], p[1], p[2]); |
| 43 | + } |
| 44 | + for (auto face : mesh.faces()) { |
| 45 | + int indexes[3]; |
| 46 | + int count =0 ; |
| 47 | + for (auto vertex : mesh.vertices(face)) { |
| 48 | + auto p = mesh.position(vertex); |
| 49 | + indexes[count++] = vertex.idx() + 1; |
| 50 | + } |
| 51 | + fprintf(fp, "f %d %d %d\n", indexes[0], indexes[1], indexes[2]); |
| 52 | + } |
| 53 | + fclose(fp); |
| 54 | + return true; |
| 55 | +} |
| 56 | + |
| 57 | +Grid<Eigen::Vector3f> loadNormalMap(const char *filename) { |
| 58 | + cv::Mat normals_char = cv::imread(filename, cv::IMREAD_UNCHANGED); |
| 59 | + cv::Mat normals_float; |
| 60 | + normals_char.convertTo(normals_float, CV_32FC3); |
| 61 | + |
| 62 | + // Discard alpha if neccessary |
| 63 | + if (normals_float.channels() == 4) { |
| 64 | + cv::cvtColor(normals_float, normals_float, cv::COLOR_BGRA2BGR); |
| 65 | + |
| 66 | + } else if (normals_float.channels() != 3) { |
| 67 | + std::cout << "Error: Normal Map must have three/four channels.\n"; |
| 68 | + exit(0); |
| 69 | + } |
| 70 | + |
| 71 | + |
| 72 | + Grid<Eigen::Vector3f> normals(normals_float.rows, normals_float.cols, Eigen::Vector3f::Zero()); |
| 73 | + |
| 74 | + for (int v = 0; v != normals_float.rows; ++v) { |
| 75 | + for (int u = 0; u != normals_float.cols; ++u) { |
| 76 | + cv::Vec3f n = normals_float.at<cv::Vec3f>(v, u); |
| 77 | + n[0] = -n[0] + 127.0f; |
| 78 | + n[1] = n[1] - 127.0f; |
| 79 | + n[2] = -n[2] + 127.0f; |
| 80 | + n /= sqrt(n[0]*n[0] + n[1]*n[1] + n[2]*n[2]); |
| 81 | + |
| 82 | + normals.at(v, u) = Eigen::Vector3f(n[2], n[1], n[0]); //z y x because of opencv loads images as bgr |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + return normals; |
| 87 | +} |
| 88 | + |
| 89 | +Grid<unsigned char> loadMask(const std::string &filename) { |
| 90 | + cv::Mat maskcv = cv::imread(filename, cv::IMREAD_GRAYSCALE); |
| 91 | + if (maskcv.empty()) { |
| 92 | + std::cout << "Error: Could not read provided foreground mask!\n"; |
| 93 | + std::cout << "The foreground mask must be a black-and-white .png image.\n"; |
| 94 | + exit(0); |
| 95 | + } |
| 96 | + |
| 97 | + Grid<unsigned char> mask(maskcv.rows, maskcv.cols, 0); |
| 98 | + for (int v = 0; v != maskcv.rows; ++v) { |
| 99 | + for (int u = 0; u != maskcv.cols; ++u) { |
| 100 | + mask.at(v, u) = maskcv.at<uchar>(v, u); |
| 101 | + } |
| 102 | + } |
| 103 | + return mask; |
| 104 | +} |
| 105 | + |
| 106 | + |
| 107 | +int main(int argc, char *argv[]) { |
| 108 | + ArgumentParser parser(argc, argv); |
| 109 | + |
| 110 | + if (!parser.has_arguments()) { |
| 111 | + print_help_message(); |
| 112 | + return 0; |
| 113 | + } |
| 114 | + |
| 115 | + if (!parser.has_argument('n')) { |
| 116 | + std::cout << "Error: No Normal Map provided!\n\n"; |
| 117 | + print_help_message(); |
| 118 | + return 1; |
| 119 | + } |
| 120 | + |
| 121 | + if (!parser.has_argument('t')) { |
| 122 | + std::cout << "Warning: No output Mesh provided!\n\n"; |
| 123 | + } |
| 124 | + |
| 125 | + |
| 126 | + Grid<Eigen::Vector3f> normals = loadNormalMap(parser.get_argument('n').c_str()); |
| 127 | + |
| 128 | + int height = normals.rows(); |
| 129 | + int width = normals.cols(); |
| 130 | + |
| 131 | + Grid<unsigned char> mask(0, 0, 0); //null mask. |
| 132 | + |
| 133 | + if (parser.has_argument('m')) |
| 134 | + mask = loadMask(parser.get_argument('m')); |
| 135 | + |
| 136 | + if(mask.rows() == 0) { |
| 137 | + mask = Grid<unsigned char>(normals.rows(), normals.cols(), 0); |
| 138 | + mask.fill(255); |
| 139 | + } |
| 140 | + |
| 141 | + // Perspective Mode |
| 142 | + if (parser.has_argument('x')) |
| 143 | + { |
| 144 | + float ax = std::stod(parser.get_argument('x')); |
| 145 | + float ay = parser.has_argument('y') ? std::stod(parser.get_argument('y')) : ax; |
| 146 | + |
| 147 | + float u0 = parser.has_argument('u') ? std::stod(parser.get_argument('u')) : static_cast<float>(width) / 2.; |
| 148 | + float v0 = parser.has_argument('v') ? std::stod(parser.get_argument('v')) : static_cast<float>(height) / 2.; |
| 149 | + |
| 150 | + float scale = 1. / std::sqrt(std::abs(ax * ay)); // Approximately the size of one pixel |
| 151 | + |
| 152 | + // Load remeshing values |
| 153 | + float l_min = parser.has_argument('l') ? std::stof(parser.get_argument('l')) : scale; |
| 154 | + float l_max = parser.has_argument('h') ? std::stof(parser.get_argument('h')) : 100. * scale; |
| 155 | + float approx_error = parser.has_argument('e') ? (std::stof(parser.get_argument('e')) / 1000.) : 1. * scale; |
| 156 | + |
| 157 | + // Run remeshing |
| 158 | + PhotometricRemeshing<pmp::Perspective> remesher(normals, mask, pmp::Perspective(ax, -ay, u0, v0)); |
| 159 | + remesher.run(l_min, l_max, approx_error); |
| 160 | + |
| 161 | + // Perform integration |
| 162 | + pmp::Integration<double, pmp::Perspective> integrator(remesher.mesh(), normals, mask, pmp::Perspective(ax, -ay, u0, v0)); |
| 163 | + integrator.run(); |
| 164 | + |
| 165 | + // Transform mesh to world-coordinates |
| 166 | + for (auto v : remesher.mesh().vertices()) |
| 167 | + { |
| 168 | + auto pos = remesher.mesh().position(v); |
| 169 | + |
| 170 | + pos[0] -= u0; |
| 171 | + pos[0] /= ax; |
| 172 | + |
| 173 | + pos[1] -= v0; |
| 174 | + pos[1] /= ay; |
| 175 | + |
| 176 | + remesher.mesh().position(v) = pos; |
| 177 | + } |
| 178 | + |
| 179 | + // Remove slivers |
| 180 | + int slivers = integrator.remove_slivers(); |
| 181 | + |
| 182 | + if (slivers > 0) |
| 183 | + { |
| 184 | + std::cout << "Removed " << slivers << " slivers\n"; |
| 185 | + } |
| 186 | + |
| 187 | + if (parser.has_argument('t')) |
| 188 | + { |
| 189 | + saveObj(parser.get_argument('t').c_str(), remesher.mesh()); |
| 190 | + } |
| 191 | + } |
| 192 | + // Orthographic Mode |
| 193 | + else |
| 194 | + { |
| 195 | + // Load remeshing values |
| 196 | + float l_min = parser.has_argument('l') ? std::stof(parser.get_argument('l')) : 1; |
| 197 | + float l_max = parser.has_argument('h') ? std::stof(parser.get_argument('h')) : 100.; |
| 198 | + float approx_error = parser.has_argument('e') ? std::stof(parser.get_argument('e')) : 1.; |
| 199 | + |
| 200 | + auto now = high_resolution_clock::now(); |
| 201 | + |
| 202 | + cout << "Remeshing" << endl; |
| 203 | + PhotometricRemeshing<pmp::Orthographic> remesher(normals, mask); |
| 204 | + remesher.run(l_min, l_max, approx_error); |
| 205 | + |
| 206 | + auto end = high_resolution_clock::now(); |
| 207 | + auto time = duration_cast<seconds>(end - now); |
| 208 | + cout << "Took: " << time.count() << endl; |
| 209 | + |
| 210 | + cout << "integrating" << endl; |
| 211 | + |
| 212 | + // Perform integration |
| 213 | + pmp::Integration<double, pmp::Orthographic> integrator(remesher.mesh(), normals, mask); |
| 214 | + integrator.run(); |
| 215 | + |
| 216 | + auto rend = high_resolution_clock::now(); |
| 217 | + auto rtime = duration_cast<seconds>(rend - end); |
| 218 | + cout << "Took: " << rtime.count() << endl; |
| 219 | + |
| 220 | + // Remove slivers |
| 221 | + int slivers = integrator.remove_slivers(); |
| 222 | + |
| 223 | + if (slivers > 0) |
| 224 | + { |
| 225 | + std::cout << "Removed " << slivers << " slivers\n"; |
| 226 | + } |
| 227 | + |
| 228 | + if (parser.has_argument('t')) |
| 229 | + { |
| 230 | + saveObj(parser.get_argument('t').c_str(), remesher.mesh()); |
| 231 | + } |
| 232 | + } |
| 233 | +} |
0 commit comments