Skip to content

Commit 5f1139f

Browse files
committed
moved assm Grid.cpp out of algorithms, added main.cpp usage example.
1 parent 52178fd commit 5f1139f

File tree

5 files changed

+261
-24
lines changed

5 files changed

+261
-24
lines changed
File renamed without changes.

external/assm/algorithms/PhotometricRemeshing.h

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
#pragma once
22

33
#include <functional>
4-
#include <assm/algorithms/Triangulation.h>
4+
#include "Triangulation.h"
55

6-
#include <assm/algorithms/ScreenMeshing.h>
7-
#include <assm/algorithms/ScreenRemeshing.h>
8-
#include <assm/algorithms/Grid.h>
6+
#include "ScreenMeshing.h"
7+
#include "ScreenRemeshing.h"
8+
#include "../Grid.h"
99

1010
template <typename Projection = pmp::Orthographic>
1111
class PhotometricRemeshing

external/assm/algorithms/ScreenDifferentialGeometry.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <Eigen/Core>
55
#include <Eigen/Eigenvalues>
66
#include "DifferentialGeometry.h"
7-
#include "Grid.h"
7+
#include "../Grid.h"
88
#include "Rasterizer.h"
99

1010
namespace pmp

external/assm/main.cpp

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
}

relightlab/normalsframe.cpp

+23-19
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,43 @@ NormalsFrame::NormalsFrame(QWidget *parent): QFrame(parent) {
2323

2424
content->addWidget(jpg = new QRadioButton("JPEG: normalmap"));
2525
content->addWidget(png = new QRadioButton("PNG: normalmap"));
26-
QButtonGroup *group = new QButtonGroup(this);
27-
group->addButton(jpg);
28-
group->addButton(png);
29-
jpg->setChecked(true);
26+
{
27+
QButtonGroup *group = new QButtonGroup(this);
28+
group->addButton(jpg);
29+
group->addButton(png);
30+
jpg->setChecked(true);
31+
}
3032

3133
content->addWidget(new QLabel("<h2>Flatten normals</h2>"));
3234
content->addWidget(radial = new QCheckBox("Radial"));
3335
content->addWidget(fourier = new QCheckBox("Fourier"));
34-
QHBoxLayout *fourier_layout = new QHBoxLayout;
35-
content->addLayout(fourier_layout);
36+
{
37+
QHBoxLayout *fourier_layout = new QHBoxLayout;
3638

37-
fourier_layout->addWidget(new HelpLabel("Fourier image percent: ", "normals/flatten"));
38-
fourier_layout->addWidget(fourier_radius = new QSpinBox);
39-
fourier_radius->setRange(0, 100);
39+
fourier_layout->addWidget(new HelpLabel("Fourier image percent: ", "normals/flatten"));
40+
fourier_layout->addWidget(fourier_radius = new QSpinBox);
41+
fourier_radius->setRange(0, 100);
4042

43+
content->addLayout(fourier_layout);
44+
}
4145

4246
content->addSpacing(30);
4347
content->addWidget(new QLabel("<h2>Export 3D surface</h2>"));
44-
4548
content->addSpacing(30);
46-
QHBoxLayout *discontinuity_layout = new QHBoxLayout;
47-
content->addLayout(discontinuity_layout);
48-
49-
discontinuity_layout->addWidget(new HelpLabel("Disconitnuity parameter:", "normals/disconituity"));
50-
discontinuity_layout->addWidget(discontinuity = new QDoubleSpinBox);
51-
discontinuity->setRange(0.0, 4.0);
52-
discontinuity->setValue(2.0);
49+
{
50+
QHBoxLayout *discontinuity_layout = new QHBoxLayout;
51+
52+
discontinuity_layout->addWidget(new HelpLabel("Disconitnuity parameter:", "normals/disconituity"));
53+
discontinuity_layout->addWidget(discontinuity = new QDoubleSpinBox);
54+
discontinuity->setRange(0.0, 4.0);
55+
discontinuity->setValue(2.0);
56+
content->addLayout(discontinuity_layout);
57+
}
5358

5459
content->addWidget(tif = new QCheckBox("TIF: depthmap"));
5560
content->addWidget(ply = new QCheckBox("PLY: mesh"));
5661

5762

58-
5963
QPushButton *save = new QPushButton("Export");
6064
content->addWidget(save);
6165

@@ -67,7 +71,7 @@ NormalsFrame::NormalsFrame(QWidget *parent): QFrame(parent) {
6771
void NormalsFrame::save() {
6872
if(qRelightApp->project().dome.directions.size() == 0) {
6973
QMessageBox::warning(this, "Missing light directions.", "You need light directions for this dataset to build a normalmap.\n"
70-
"You can either load a dome or .lp file or mark a reflective sphere in the 'Lights' tab.");
74+
"You can either load a dome or .lp file or mark a reflective sphere in the 'Lights' tab.");
7175
return;
7276
}
7377
QString filter = jpg->isChecked() ? "JPEG Images (*.jpg)" : "PNG Images (*.png)";

0 commit comments

Comments
 (0)