lardon3d/tests/test_dense_mvs.cpp

1307 lines
53 KiB
C++

#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <fstream>
#include <iterator>
#include <locale>
#include <lardon3d/dense_mvs.h>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/calib3d.hpp>
#include <openssl/evp.h>
#include <signal.h>
#include <sstream>
#include <string>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <vector>
#define CHECK(x) \
do { \
if (!(x)) { \
std::fprintf(stderr, "line %d: %s\n", __LINE__, #x); \
return false; \
} \
} while (0)
namespace {
const char *good_ply =
"ply\nformat ascii 1.0\nelement vertex 2\nproperty float x\nproperty float "
"y\nproperty float z\nend_header\n1 2 3\n4 5 6\n";
bool put(const std::string &p, const std::string &s, bool x = true) {
FILE *f = fopen(p.c_str(), "wb");
if (!f)
return false;
bool ok = fwrite(s.data(), 1, s.size(), f) == s.size() && !fclose(f);
return ok && (!x || !chmod(p.c_str(), 0700));
}
std::string shell_octal(const std::string &data) {
static const char digits[] = "01234567";
std::string escaped;
escaped.reserve(data.size() * 4);
for (unsigned char value : data) {
escaped.push_back('\\');
escaped.push_back(digits[(value >> 6U) & 7U]);
escaped.push_back(digits[(value >> 3U) & 7U]);
escaped.push_back(digits[value & 7U]);
}
return escaped;
}
std::string script(const std::string &v, const std::string &body) {
const bool densify = v.find("Densify") != std::string::npos || v == "D";
const std::string options = densify
? "--max-threads --resolution-level "
"--min-resolution --number-views --fusion-mode" +
std::string(v.find("CUDA") != std::string::npos
? " --cuda-device"
: "")
: "--max-threads --image-folder";
return "#!/bin/sh\nif [ \"$1\" = \"--help\" ]; then printf '%s\\n' "
"'OpenMVS x64 v2.4.0 " + options + "'; exit 1; fi\n" + body;
}
struct F {
std::string dir, ic, dp, log, image;
Lardon3DSparseIncrementalCamera cam{};
Lardon3DSparseIncrementalLandmark landmark{};
Lardon3DSparseIncrementalLandmarkObservation observation{};
Lardon3DSparseIncrementalObservation source_observation{};
Lardon3DSparseIncrementalResult snap{};
Lardon3DDenseMvsSourceImage src{};
Lardon3DDenseMvsInput in{};
F() {
char p[] = "/tmp/l3d-mvs-XXXXXX";
char *d = mkdtemp(p);
if (!d)
return;
dir = d;
ic = dir + "/InterfaceCOLMAP";
dp = dir + "/DensifyPointCloud";
log = dir + "/argv";
image = dir + "/image.jpg";
cam.image_id = 17;
cam.component_key = 1;
double r[] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
memcpy(cam.pose_cw.rotation_cw, r, sizeof r);
snap.cameras = &cam;
snap.camera_count = 1;
landmark.landmark_id = 23;
landmark.track_id = 29;
landmark.component_key = 1;
landmark.point = {1, 2, 3};
landmark.reprojection_rmse_px = 0.25;
landmark.observation_count = 1;
snap.landmarks = &landmark;
snap.landmark_count = 1;
observation = {23, 29, 17, 31, 37, 0};
snap.observations = &observation;
snap.observation_count = 1;
source_observation = {29, 17, 31, 37, 40, 20.25, 10.5};
src.image_id = 17;
src.source_path = image.c_str();
src.calibration = {64, 48, 50, 51, 32, 24, 0, 0, 0, 0};
for (size_t i = 0; i < 32; i++) {
in.base_reconstruction_identity[i] = static_cast<unsigned char>(i + 1U);
in.calibration_scope_identity[i] = static_cast<unsigned char>(0x40U + i);
}
in.snapshot = &snap;
in.source_observations = &source_observation;
in.source_observation_count = 1;
in.source_images = &src;
in.source_image_count = 1;
in.parameters = {1, 320, 4, 0};
in.execution_thread_count = 3;
cv::Mat pixels(48, 64, CV_8UC3, cv::Scalar(20, 40, 60));
if (!cv::imwrite(image, pixels)) return;
FILE *image_file = fopen(image.c_str(), "rb");
EVP_MD_CTX *context = EVP_MD_CTX_new();
unsigned int digest_size = 0;
unsigned char buffer[4096];
bool hashed = image_file && context &&
EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1;
while (hashed) {
const size_t size = fread(buffer, 1, sizeof buffer, image_file);
if (size && EVP_DigestUpdate(context, buffer, size) != 1) hashed = false;
if (size < sizeof buffer) {
hashed = hashed && !ferror(image_file);
break;
}
}
hashed = hashed &&
EVP_DigestFinal_ex(context, src.immutable_sha256, &digest_size) == 1 &&
digest_size == 32;
EVP_MD_CTX_free(context);
if (image_file) fclose(image_file);
if (!hashed)
return;
sync();
}
void sync() {
src.source_path = image.c_str();
in.staging_directory = dir.c_str();
in.interface_colmap_executable = ic.c_str();
in.densify_point_cloud_executable = dp.c_str();
}
};
bool install(F &f, const std::string &iv = "Interface 1",
const std::string &dv = "Densify 1",
const std::string &ply = good_ply) {
std::string b =
"if [ -n \"$DENSE_ARGV_LOG\" ]; then : > \"$DENSE_ARGV_LOG\"; for a in "
"\"$@\"; do printf '%s\\n' \"$a\" >> \"$DENSE_ARGV_LOG\"; done; "
"fi\no=\nwhile [ $# -gt 0 ]; do if [ \"$1\" = -o ]; then shift; o=$1; "
"fi; shift; done\nprintf '%b' '" +
shell_octal(ply) + "' > \"$o.ply\"\n";
return put(f.ic, script(iv, "exit 0\n")) && put(f.dp, script(dv, b));
}
Lardon3DDenseMvsStatus run(F &f, Lardon3DDenseMvsResult &r) {
f.sync();
return lardon3d_dense_mvs_run(&f.in, &r);
}
bool zero(const unsigned char *p, size_t n) {
for (size_t i = 0; i < n; i++)
if (p[i])
return false;
return true;
}
std::string result_workspace(const Lardon3DDenseMvsResult &result) {
const std::string path(result.point_cloud_path);
const size_t slash = path.rfind('/');
return slash == std::string::npos ? std::string() : path.substr(0, slash);
}
std::vector<std::string> private_workspaces(const std::string &parent) {
std::vector<std::string> paths;
DIR *directory = opendir(parent.c_str());
if (!directory) return paths;
while (dirent *entry = readdir(directory)) {
if (std::strncmp(entry->d_name, "lardon3d-mvs-", 13) == 0)
paths.push_back(parent + "/" + entry->d_name);
}
closedir(directory);
std::sort(paths.begin(), paths.end());
return paths;
}
bool digest_file(const std::string &path, unsigned char digest[32]) {
FILE *file = fopen(path.c_str(), "rb");
EVP_MD_CTX *context = EVP_MD_CTX_new();
bool ok = file && context &&
EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1;
unsigned char buffer[4096];
while (ok) {
const size_t size = fread(buffer, 1, sizeof buffer, file);
if (size && EVP_DigestUpdate(context, buffer, size) != 1) ok = false;
if (size < sizeof buffer) { ok = ok && !ferror(file); break; }
}
unsigned int length = 0;
ok = ok && EVP_DigestFinal_ex(context, digest, &length) == 1 && length == 32;
EVP_MD_CTX_free(context);
if (file && fclose(file) != 0) ok = false;
return ok;
}
class CommaNumpunct : public std::numpunct<char> {
protected:
char do_decimal_point() const override { return ','; }
};
class GlobalLocaleGuard {
public:
GlobalLocaleGuard()
: previous_(std::locale::global(
std::locale(std::locale::classic(), new CommaNumpunct))) {}
~GlobalLocaleGuard() { std::locale::global(previous_); }
GlobalLocaleGuard(const GlobalLocaleGuard &) = delete;
GlobalLocaleGuard &operator=(const GlobalLocaleGuard &) = delete;
private:
std::locale previous_;
};
bool goldens() {
static const unsigned char pr[40] = {
0x4c, 0x33, 0x44, 0x4d, 0x50, 0x52, 0x4d, 0x31, 1, 0, 0, 0, 1, 0,
0, 0, 1, 0, 0, 0, 0x40, 1, 0, 0, 4, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0};
static const unsigned char pd[32] = {
0x03, 0x82, 0x06, 0xe5, 0xe8, 0x72, 0x9e, 0x13, 0x87, 0x14, 0xc6,
0xb4, 0xcc, 0x8b, 0xe4, 0x8c, 0x78, 0xa9, 0x94, 0xcf, 0x83, 0x3d,
0xc0, 0x2a, 0xca, 0x89, 0xd6, 0xc1, 0x49, 0xd5, 0xdd, 0xe4};
static const unsigned char br[148] = {
0x4c, 0x33, 0x44, 0x4d, 0x42, 0x4b, 0x44, 0x31, 1, 0, 0, 0, 1,
0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 2, 0, 0, 0, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97,
98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123,
124, 125, 126, 127, 128};
static const unsigned char bd[32] = {
0x7d, 0xdb, 0xe5, 0xc5, 0x13, 0x7f, 0x71, 0x5d, 0x4a, 0xe7, 7,
0xc8, 0xa4, 0xc9, 0x84, 0xf6, 0xa2, 0xe9, 0x76, 0x60, 0x52, 0xa7,
0x60, 0x78, 0x1d, 0x5e, 0x24, 0xdd, 0xf3, 0x31, 0xae, 0x18};
static const unsigned char ir[220] = {
0x4c, 0x33, 0x44, 0x4d, 0x44, 0x49, 0x44, 0x32, 2, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5};
static const unsigned char id[32] = {
0x61, 0x1c, 0x54, 0x2c, 0x4f, 0xe6, 0xf9, 0x1d, 0x8d, 0xf2, 0xea,
0x77, 0x8e, 0xe8, 0xbe, 0x70, 0x8e, 0xb4, 0x81, 0x36, 0xf9, 0x7b,
0xae, 0xaf, 0xe8, 0x45, 0xc0, 0x17, 0x93, 0x2d, 0x04, 0x90};
unsigned char r[220]{}, d[32]{};
Lardon3DDenseMvsParameters p{1, 320, 4, 1};
CHECK(lardon3d_dense_mvs_parameter_fingerprint_record(&p, r) &&
!memcmp(r, pr, 40));
CHECK(lardon3d_dense_mvs_parameter_fingerprint(&p, d) && !memcmp(d, pd, 32));
Lardon3DDenseMvsBackendManifest m{};
for (size_t i = 0; i < 32; i++) {
m.interface_colmap_version_identity[i] = static_cast<unsigned char>(i + 1U);
m.interface_colmap_binary_sha256[i] = static_cast<unsigned char>(i + 33U);
m.densify_point_cloud_version_identity[i] =
static_cast<unsigned char>(i + 65U);
m.densify_point_cloud_binary_sha256[i] =
static_cast<unsigned char>(i + 97U);
}
CHECK(lardon3d_dense_mvs_backend_manifest_record(&m, r) &&
!memcmp(r, br, 148));
CHECK(lardon3d_dense_mvs_backend_manifest_digest(&m, d) &&
!memcmp(d, bd, 32));
Lardon3DDenseMvsIdentity x{};
memset(x.base_reconstruction_identity, 1, 32);
memset(x.source_image_set_identity, 2, 32);
memset(x.calibration_scope_identity, 3, 32);
memset(x.calibration_binding_identity, 6, 32);
memset(x.backend_binary_sha256, 4, 32);
memset(x.parameter_fingerprint, 5, 32);
x.dense_kind = x.dense_version = x.backend_kind = x.backend_version = 1;
CHECK(lardon3d_dense_mvs_identity_record(&x, r));
for (size_t i = 0; i < 220; ++i)
if (r[i] != ir[i]) {
std::fprintf(stderr, "identity mismatch %zu: %u != %u\n", i,
static_cast<unsigned int>(r[i]),
static_cast<unsigned int>(ir[i]));
return false;
}
CHECK(lardon3d_dense_mvs_identity_digest(&x, d) && !memcmp(d, id, 32));
return true;
}
bool calibration_identity_contract() {
F f;
static const unsigned char expected[32] = {
0xba, 0x29, 0x68, 0x86, 0x8b, 0x24, 0x5b, 0x84, 0x1d, 0xad, 0xfe,
0x69, 0x84, 0xa1, 0x2d, 0x79, 0xe3, 0x18, 0x82, 0x9a, 0xde, 0x0e,
0x9f, 0x94, 0x0f, 0xb7, 0x59, 0x4c, 0x81, 0x5c, 0xb5, 0x76};
unsigned char base_binding[32], base_source[32];
CHECK(lardon3d_dense_mvs_calibration_binding_identity(&f.src, 1,
base_binding) &&
!memcmp(base_binding, expected, 32));
CHECK(lardon3d_dense_mvs_source_image_set_identity(&f.src, 1, base_source));
auto dense_digest = [&](const Lardon3DDenseMvsSourceImage &image,
unsigned char digest[32]) {
Lardon3DDenseMvsIdentity identity{};
memset(identity.base_reconstruction_identity, 1, 32);
memset(identity.source_image_set_identity, 2, 32);
memset(identity.calibration_scope_identity, 3, 32);
memset(identity.backend_binary_sha256, 4, 32);
memset(identity.parameter_fingerprint, 5, 32);
identity.dense_kind = identity.dense_version = 1;
identity.backend_kind = identity.backend_version = 1;
return lardon3d_dense_mvs_calibration_binding_identity(
&image, 1, identity.calibration_binding_identity) &&
lardon3d_dense_mvs_identity_digest(&identity, digest);
};
unsigned char base_dense[32];
CHECK(dense_digest(f.src, base_dense));
auto changed = [&](auto mutate) {
Lardon3DDenseMvsSourceImage image = f.src;
mutate(image.calibration);
unsigned char binding[32], dense[32], source[32];
CHECK(lardon3d_dense_mvs_calibration_binding_identity(&image, 1, binding));
CHECK(dense_digest(image, dense));
CHECK(lardon3d_dense_mvs_source_image_set_identity(&image, 1, source));
CHECK(memcmp(binding, base_binding, 32) && memcmp(dense, base_dense, 32));
CHECK(!memcmp(source, base_source, 32));
return true;
};
CHECK(changed([](auto &c) { ++c.width; }));
CHECK(changed([](auto &c) { ++c.height; }));
CHECK(changed([](auto &c) { c.fx += 1.0; }));
CHECK(changed([](auto &c) { c.fy += 1.0; }));
CHECK(changed([](auto &c) { c.cx += 1.0; }));
CHECK(changed([](auto &c) { c.cy += 1.0; }));
CHECK(changed([](auto &c) { c.k1 += 1.0; }));
CHECK(changed([](auto &c) { c.k2 += 1.0; }));
CHECK(changed([](auto &c) { c.p1 += 1.0; }));
CHECK(changed([](auto &c) { c.p2 += 1.0; }));
Lardon3DDenseMvsSourceImage signed_zero = f.src;
signed_zero.calibration.k1 = -0.0;
unsigned char zero_binding[32];
CHECK(lardon3d_dense_mvs_calibration_binding_identity(&signed_zero, 1,
zero_binding));
CHECK(!memcmp(zero_binding, base_binding, 32));
Lardon3DDenseMvsSourceImage images[2] = {f.src, f.src};
images[1].image_id = 18;
images[1].calibration.fx = 75.0;
unsigned char ordered[32], reordered[32];
CHECK(lardon3d_dense_mvs_calibration_binding_identity(images, 2, ordered));
std::swap(images[0], images[1]);
CHECK(lardon3d_dense_mvs_calibration_binding_identity(images, 2, reordered));
CHECK(!memcmp(ordered, reordered, 32));
images[1].image_id = images[0].image_id;
CHECK(!lardon3d_dense_mvs_calibration_binding_identity(images, 2, reordered));
Lardon3DDenseMvsSourceImage invalid = f.src;
invalid.calibration.fx = NAN;
CHECK(!lardon3d_dense_mvs_calibration_binding_identity(&invalid, 1,
reordered));
invalid = f.src;
invalid.calibration.p2 = INFINITY;
CHECK(!lardon3d_dense_mvs_calibration_binding_identity(&invalid, 1,
reordered));
return true;
}
bool execution_configuration_not_scientific_identity() {
F f;
CHECK(install(f));
Lardon3DDenseMvsResult first{}, second{};
CHECK(run(f, first) == LARDON3D_DENSE_MVS_OK);
const std::string other_staging = f.dir + "/other-staging";
f.in.execution_thread_count = 1;
f.in.staging_directory = other_staging.c_str();
CHECK(lardon3d_dense_mvs_run(&f.in, &second) == LARDON3D_DENSE_MVS_OK);
CHECK(!memcmp(first.dense_identity, second.dense_identity, 32));
return true;
}
bool backend_argv() {
F f;
CHECK(install(f));
setenv("DENSE_ARGV_LOG", f.log.c_str(), 1);
Lardon3DDenseMvsResult base{};
CHECK(run(f, base) == LARDON3D_DENSE_MVS_OK);
std::ifstream z(f.log);
std::vector<std::string> a;
std::string s;
while (getline(z, s))
a.push_back(s);
std::vector<std::pair<std::string, std::string>> e = {
{"--resolution-level", "1"}, {"--min-resolution", "320"},
{"--number-views", "4"}, {"--fusion-mode", "0"},
{"--max-threads", "3"}};
for (auto &o : e) {
int n = 0;
for (size_t i = 0; i < a.size(); i++)
if (a[i] == o.first) {
n++;
CHECK(i + 1 < a.size() && a[i + 1] == o.second);
}
CHECK(n == 1);
}
CHECK(std::find(a.begin(), a.end(), "--cuda-device") == a.end());
{
F cuda;
CHECK(install(cuda, "Interface 1", "Densify CUDA"));
setenv("DENSE_ARGV_LOG", cuda.log.c_str(), 1);
Lardon3DDenseMvsResult cuda_result{};
CHECK(run(cuda, cuda_result) == LARDON3D_DENSE_MVS_OK);
std::ifstream arguments(cuda.log);
std::vector<std::string> values((std::istream_iterator<std::string>(arguments)), {});
const auto option = std::find(values.begin(), values.end(), "--cuda-device");
CHECK(option != values.end() && option + 1 != values.end() && *(option + 1) == "-2");
}
f.in.execution_thread_count = 9;
Lardon3DDenseMvsResult t{};
CHECK(run(f, t) == 0 && !memcmp(base.dense_identity, t.dense_identity, 32));
auto changed = [&](const std::string &is, const std::string &ds) {
CHECK(put(f.ic, is) && put(f.dp, ds));
Lardon3DDenseMvsResult q{};
CHECK(run(f, q) == 0);
CHECK(memcmp(base.backend_implementation_sha256,
q.backend_implementation_sha256, 32));
return true;
};
std::string gi = script("Interface 1", "exit 0\n"),
gd = script("Densify 1",
"o=\nwhile [ $# -gt 0 ]; do if [ \"$1\" = -o ]; then "
"shift; o=$1; fi; shift; done\nprintf '%s' '" +
std::string(good_ply) + "' > \"$o.ply\"\n");
CHECK(changed(script("Interface 1", "# B1\nexit 0\n"), gd));
CHECK(
changed(gi, script("Densify 1", "# B2\n" + gd.substr(gd.find("o=\n")))));
CHECK(changed(script("Interface 2", "exit 0\n"), gd));
CHECK(changed(gi, script("Densify 2", gd.substr(gd.find("o=\n")))));
F other;
CHECK(install(other));
Lardon3DDenseMvsResult q{};
CHECK(run(other, q) == 0 && !memcmp(base.backend_implementation_sha256,
q.backend_implementation_sha256, 32));
unsetenv("DENSE_ARGV_LOG");
return true;
}
bool rotations() {
F f;
CHECK(install(f));
double a = 3.14159265358979323846 - 1e-6, c = cos(a), s = sin(a);
struct R {
double m[9], q[4];
} rs[] = {{{1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 0, 0, 0}},
{{1, 0, 0, 0, -1, 0, 0, 0, -1}, {0, 1, 0, 0}},
{{-1, 0, 0, 0, 1, 0, 0, 0, -1}, {0, 0, 1, 0}},
{{-1, 0, 0, 0, -1, 0, 0, 0, 1}, {0, 0, 0, 1}},
{{1, 0, 0, 0, c, -s, 0, s, c}, {cos(a / 2), sin(a / 2), 0, 0}}};
for (size_t k = 0; k < 5; k++) {
memcpy(f.cam.pose_cw.rotation_cw, rs[k].m, sizeof rs[k].m);
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == 0);
std::ifstream in(result_workspace(r) + "/colmap/sparse/images.txt");
uint64_t n;
double q[4];
CHECK(bool(in >> n >> q[0] >> q[1] >> q[2] >> q[3]));
double dot = 0;
for (int j = 0; j < 4; j++) {
CHECK(std::isfinite(q[j]));
dot += q[j] * rs[k].q[j];
}
CHECK(q[0] > 0 || (q[0] == 0 &&
(q[1] > 0 || (q[1] == 0 &&
(q[2] > 0 || (q[2] == 0 && q[3] >= 0))))));
CHECK(fabs(fabs(dot) - 1) < 1e-5);
if (!k) {
std::ifstream x(result_workspace(r) + "/colmap/sparse/images.txt");
std::string one((std::istreambuf_iterator<char>(x)), {});
Lardon3DDenseMvsResult rr{};
CHECK(run(f, rr) == 0);
std::ifstream y(result_workspace(rr) + "/colmap/sparse/images.txt");
std::string two((std::istreambuf_iterator<char>(y)), {});
CHECK(one == two);
}
}
const double invalid[][9] = {
{2, 0, 0, 0, 1, 0, 0, 0, 1},
{1, 0.01, 0, 0, 1, 0, 0, 0, 1},
{-1, 0, 0, 0, 1, 0, 0, 0, 1},
{NAN, 0, 0, 0, 1, 0, 0, 0, 1},
{1, 0, 0, 0, 1, 0, 0, 0, INFINITY}};
for (const auto &matrix : invalid) {
memcpy(f.cam.pose_cw.rotation_cw, matrix, sizeof matrix);
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT);
}
double boundary[] = {1, 1e-6, 0, 0, 1, 0, 0, 0, 1};
memcpy(f.cam.pose_cw.rotation_cw, boundary, sizeof boundary);
Lardon3DDenseMvsResult accepted{};
CHECK(run(f, accepted) == LARDON3D_DENSE_MVS_OK);
boundary[1] = 1.000001e-6;
memcpy(f.cam.pose_cw.rotation_cw, boundary, sizeof boundary);
Lardon3DDenseMvsResult rejected{};
CHECK(run(f, rejected) == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT);
double row_major_boundary[] = {1, 0, 0, 1e-6, 1, 0, 0, 0, 1};
memcpy(f.cam.pose_cw.rotation_cw, row_major_boundary,
sizeof row_major_boundary);
Lardon3DDenseMvsResult row_major_accepted{};
CHECK(run(f, row_major_accepted) == LARDON3D_DENSE_MVS_OK);
row_major_boundary[3] = 1.000001e-6;
memcpy(f.cam.pose_cw.rotation_cw, row_major_boundary,
sizeof row_major_boundary);
Lardon3DDenseMvsResult row_major_rejected{};
CHECK(run(f, row_major_rejected) == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT);
return true;
}
bool fail(const std::string &i, const std::string &d,
Lardon3DDenseMvsStatus want) {
F f;
CHECK(put(f.ic, i) && put(f.dp, d));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == want);
CHECK(r.status == want && r.status != LARDON3D_DENSE_MVS_OK &&
!r.point_count && !r.point_cloud_path[0]);
return true;
}
bool processes() {
std::string gi = script("I", "exit 0\n"), bad = script("D", "exit 9\n");
CHECK(fail(script("I", "exit 8\n"), bad, LARDON3D_DENSE_MVS_BACKEND_ERROR));
CHECK(fail(gi, bad, LARDON3D_DENSE_MVS_BACKEND_ERROR));
{
F f;
CHECK(install(f));
f.ic += ".missing";
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == LARDON3D_DENSE_MVS_BACKEND_ERROR &&
zero(r.dense_identity, 32));
}
{
F f;
CHECK(install(f));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == 0);
struct stat info {};
const std::string workspace = result_workspace(r);
CHECK(stat((workspace + "/interface-colmap.log").c_str(), &info) == 0 &&
(info.st_mode & 0777) == 0600);
CHECK(stat((workspace + "/densify-point-cloud.log").c_str(), &info) == 0 &&
(info.st_mode & 0777) == 0600);
}
{
F f;
std::string marker = f.dir + "/marker";
std::string descendant = f.dir + "/descendant";
std::string body = "printf dense-stdout; printf dense-stderr >&2; "
"(trap '' TERM; sleep 30; printf survived > '" + descendant +
"') & printf launched > '" + marker + "'; exit 7\n";
CHECK(put(f.ic, script("I", "exit 0\n")) &&
put(f.dp, script("D", body)));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == LARDON3D_DENSE_MVS_BACKEND_ERROR);
CHECK(access(marker.c_str(), F_OK) == 0);
CHECK(access(descendant.c_str(), F_OK) != 0);
const auto workspaces = private_workspaces(f.dir);
CHECK(workspaces.size() == 1);
std::ifstream log(workspaces[0] + "/densify-point-cloud.log");
std::string output((std::istreambuf_iterator<char>(log)), {});
CHECK(output.find("dense-stdout") != std::string::npos &&
output.find("dense-stderr") != std::string::npos);
}
{
F f;
std::string delayed =
"sleep 6\no=\nwhile [ $# -gt 0 ]; do if [ \"$1\" = -o ]; then "
"shift; o=$1; fi; shift; done\nprintf '%s' '" +
std::string(good_ply) + "' > \"$o.ply\"\n";
CHECK(put(f.ic, script("I", "exit 0\n")) && put(f.dp, script("D", delayed)));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == LARDON3D_DENSE_MVS_OK);
}
{
F f;
std::string verbose =
"dd if=/dev/zero bs=1100000 count=2 2>/dev/null\n"
"o=\nwhile [ $# -gt 0 ]; do if [ \"$1\" = -o ]; then shift; "
"o=$1; fi; shift; done\nprintf '%s' '" +
std::string(good_ply) + "' > \"$o.ply\"\n";
CHECK(put(f.ic, script("I", "exit 0\n")) &&
put(f.dp, script("D", verbose)));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == LARDON3D_DENSE_MVS_OK);
struct stat info {};
CHECK(stat((result_workspace(r) + "/densify-point-cloud.log").c_str(), &info) == 0 &&
info.st_size == 1024 * 1024);
}
F probe_fixture;
const std::string probe_marker = probe_fixture.dir + "/help-entered";
CHECK(setenv("PROBE_MARKER", probe_marker.c_str(), 1) == 0);
std::string over =
"#!/bin/sh\nif [ \"$1\" = --help ]; then printf entered > \"$PROBE_MARKER\"; "
"i=0; while [ $i -lt 17000 ]; "
"do printf x; i=$((i+1)); done; exit 0; fi\n";
CHECK(fail(over, bad, LARDON3D_DENSE_MVS_BACKEND_ERROR));
CHECK(access(probe_marker.c_str(), F_OK) == 0 && unlink(probe_marker.c_str()) == 0);
CHECK(fail("#!/bin/sh\nif [ \"$1\" = --help ]; then printf entered > \"$PROBE_MARKER\"; "
"printf v; exec 1>&- "
"2>&-; sleep 20; fi\n",
bad, LARDON3D_DENSE_MVS_BACKEND_ERROR));
CHECK(access(probe_marker.c_str(), F_OK) == 0 && unlink(probe_marker.c_str()) == 0);
CHECK(fail("#!/bin/sh\nif [ \"$1\" = --help ]; then printf entered > \"$PROBE_MARKER\"; "
"sleep 20; fi\n", bad,
LARDON3D_DENSE_MVS_BACKEND_ERROR));
CHECK(access(probe_marker.c_str(), F_OK) == 0 && unlink(probe_marker.c_str()) == 0);
CHECK(fail("#!/bin/sh\nif [ \"$1\" = --help ]; then printf entered > \"$PROBE_MARKER\"; "
"trap '' TERM; while "
":; do sleep 1; done; fi\n",
bad, LARDON3D_DENSE_MVS_BACKEND_ERROR));
CHECK(access(probe_marker.c_str(), F_OK) == 0);
unsetenv("PROBE_MARKER");
return true;
}
std::string ply(const std::string &props, const std::string &rows = "1 2 3\n",
int n = 1, const std::string &fmt = "ascii") {
return "ply\nformat " + fmt + " 1.0\nelement vertex " + std::to_string(n) +
"\n" + props + "end_header\n" + rows;
}
void append_u32_le(std::string &data, uint32_t value) {
for (unsigned int shift = 0; shift < 32; shift += 8)
data.push_back(static_cast<char>((value >> shift) & 0xffU));
}
void append_f32_le(std::string &data, float value) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof bits);
append_u32_le(data, bits);
}
std::string binary_ply(float x, float y, float z, bool trailing = false) {
std::string data =
"ply\nformat binary_little_endian 1.0\nelement vertex 1\n"
"property float x\nproperty float y\nproperty float z\n"
"property uchar red\nend_header\n";
append_f32_le(data, x);
append_f32_le(data, y);
append_f32_le(data, z);
data.push_back(static_cast<char>(127));
if (trailing) data.push_back('x');
return data;
}
std::string openmvs_binary_ply(bool truncate_views = false) {
std::string data =
"ply\nformat binary_little_endian 1.0\nelement vertex 1\n"
"property float x\nproperty float y\nproperty float z\n"
"property list uchar uint view_indices\n"
"property list uchar float view_weights\nend_header\n";
append_f32_le(data, 1.25F);
append_f32_le(data, -2.5F);
append_f32_le(data, 3.75F);
data.push_back(2);
append_u32_le(data, 7);
if (!truncate_views) append_u32_le(data, 11);
data.push_back(2);
append_f32_le(data, 0.75F);
append_f32_le(data, 0.25F);
return data;
}
std::string oversized_binary_list_ply() {
std::string data =
"ply\nformat binary_little_endian 1.0\nelement vertex 1\n"
"property float x\nproperty float y\nproperty float z\n"
"property list uint uint view_indices\nend_header\n";
append_f32_le(data, 1.0F);
append_f32_le(data, 2.0F);
append_f32_le(data, 3.0F);
append_u32_le(data, UINT32_MAX);
return data;
}
bool plys() {
std::string xyz = "property float x\nproperty float y\nproperty float z\n";
std::vector<std::pair<std::string, bool>> v = {
{good_ply, true},
{"ply\r\nformat ascii 1.0\r\nelement vertex 1\r\nproperty float x\r\n"
"property float y\r\nproperty float z\r\nend_header\r\n1 2 3\r\n",
true},
{"ply\rformat ascii 1.0\nelement vertex 1\n" + xyz +
"end_header\n1 2 3\n",
false},
{ply(xyz, "1 2\r 3\n"), false},
{ply("property float y\nproperty float z\n", "2 3\n"), false},
{ply("property float x\nproperty float z\n", "1 3\n"), false},
{ply("property float x\nproperty float y\n", "1 2\n"), false},
{ply("property float x\nproperty float x\nproperty float y\nproperty "
"float z\n",
"1 1 2 3\n"),
false},
{ply("property float x\nproperty float y\nproperty float y\nproperty "
"float z\n",
"1 2 2 3\n"),
false},
{ply("property float x\nproperty float y\nproperty float z\nproperty "
"float z\n",
"1 2 3 3\n"),
false},
{ply("property float\n", "1\n"), false},
{ply(xyz, "1 2 3\n", 2), false},
{ply(xyz, "nan 2 3\n"), false},
{ply(xyz, "inf 2 3\n"), false},
{ply(xyz + "property uchar red\n", "1 2 3 255\n"), true},
{ply(xyz, "1 2 3\n \t\r\n"), true},
{ply(xyz, "1 2 3\n \t\r"), false},
{ply(xyz, "1 2 3\ngarbage\n"), false},
{"ply\nformat ascii 1.0\nelement vertex 1\n" + xyz +
"element face 0\nend_header\n1 2 3\n", false},
{ply(xyz, "", 0), false},
{ply(xyz, "", 1, "binary_little_endian"), false},
{binary_ply(1.25F, -2.5F, 3.75F), true},
{binary_ply(NAN, 2.0F, 3.0F), false},
{binary_ply(1.0F, 2.0F, 3.0F, true), false},
{openmvs_binary_ply(), true},
{openmvs_binary_ply(true), false},
{oversized_binary_list_ply(), false},
{ply(xyz + "property list uchar uint view_indices\n", "1 2 3 0\n"), false}};
std::string too_many = xyz;
for (int i = 3; i < 257; ++i)
too_many += "property float p" + std::to_string(i) + "\n";
v.push_back({ply(too_many, ""), false});
v.push_back({"ply\nformat ascii 1.0\ncomment " +
std::string(64 * 1024 + 1, 'x') + "\nend_header\n", false});
v.push_back({ply(xyz, std::string(64 * 1024 + 1, '1') + "\n"), false});
std::string huge_header = "ply\nformat ascii 1.0\n";
while (huge_header.size() <= 1024 * 1024)
huge_header += "comment " + std::string(1000, 'x') + "\n";
v.push_back({huge_header + "end_header\n", false});
std::string crlf_header = "ply\r\nformat ascii 1.0\r\n";
size_t normalized_size = std::string("ply\nformat ascii 1.0\n").size();
for (size_t i = 0; i < 1000; ++i) {
crlf_header += "comment " + std::string(1000, 'x') + "\r\n";
normalized_size += 1009;
}
const size_t end_header_size = std::string("end_header\n").size();
CHECK(normalized_size + end_header_size < 1024 * 1024);
const size_t filler_size = 1024 * 1024 - normalized_size - end_header_size;
CHECK(filler_size >= 9 && filler_size <= 64 * 1024 + 1);
crlf_header += "comment " + std::string(filler_size - 9, 'x') + "\r\n";
crlf_header += "end_header\r\n";
v.push_back({crlf_header, false});
for (size_t case_index = 0; case_index < v.size(); ++case_index) {
auto &c = v[case_index];
F f;
CHECK(install(f, "I", "D", c.first));
Lardon3DDenseMvsResult r{};
auto st = run(f, r);
if ((st == 0) != c.second)
std::fprintf(stderr, "PLY case %zu returned %d\n", case_index,
static_cast<int>(st));
CHECK((st == 0) == c.second);
if (!c.second)
CHECK(r.status == LARDON3D_DENSE_MVS_INVALID_OUTPUT);
}
return true;
}
bool private_workspace_isolation() {
F f;
CHECK(install(f));
Lardon3DDenseMvsResult first{};
CHECK(run(f, first) == LARDON3D_DENSE_MVS_OK);
const std::string first_workspace = result_workspace(first);
CHECK(!first_workspace.empty());
CHECK(put(f.dp, script("D", "exit 0\n")));
Lardon3DDenseMvsResult second{};
CHECK(run(f, second) == LARDON3D_DENSE_MVS_INVALID_OUTPUT);
CHECK(second.status == LARDON3D_DENSE_MVS_INVALID_OUTPUT &&
!second.point_cloud_path[0] && second.point_count == 0);
const auto workspaces = private_workspaces(f.dir);
CHECK(workspaces.size() == 2 && workspaces[0] != workspaces[1]);
CHECK((workspaces[0] == first_workspace || workspaces[1] == first_workspace));
return true;
}
bool provenance_path() {
F f;
CHECK(install(f));
Lardon3DDenseMvsResult r{};
CHECK(run(f, r) == 0);
unsigned char p[32], s[32], d[32];
CHECK(lardon3d_dense_mvs_parameter_fingerprint(&f.in.parameters, p));
CHECK(lardon3d_dense_mvs_source_image_set_identity(&f.src, 1, s));
CHECK(!memcmp(r.parameter_fingerprint, p, 32) &&
!memcmp(r.base_reconstruction_identity,
f.in.base_reconstruction_identity, 32) &&
!memcmp(r.source_image_set_identity, s, 32));
CHECK(memcmp(r.base_reconstruction_identity, r.dense_identity, 32) &&
r.point_count == 2 &&
result_workspace(r).find(f.dir + "/lardon3d-mvs-") == 0 &&
strlen(r.point_cloud_path) < sizeof r.point_cloud_path);
Lardon3DDenseMvsIdentity x{};
memcpy(x.base_reconstruction_identity, f.in.base_reconstruction_identity, 32);
memcpy(x.source_image_set_identity, s, 32);
memcpy(x.calibration_scope_identity, f.in.calibration_scope_identity, 32);
CHECK(lardon3d_dense_mvs_calibration_binding_identity(
&f.src, 1, x.calibration_binding_identity));
memcpy(x.backend_binary_sha256, r.backend_implementation_sha256, 32);
memcpy(x.parameter_fingerprint, p, 32);
x.dense_kind = x.dense_version = x.backend_kind = x.backend_version = 1;
CHECK(lardon3d_dense_mvs_identity_digest(&x, d) &&
!memcmp(d, r.dense_identity, 32) &&
!memcmp(x.backend_binary_sha256, r.backend_implementation_sha256, 32));
std::string huge(LARDON3D_DENSE_MVS_PATH_CAPACITY, 'x');
f.in.staging_directory = huge.c_str();
Lardon3DDenseMvsResult bad{};
CHECK(lardon3d_dense_mvs_run(&f.in, &bad) ==
LARDON3D_DENSE_MVS_INVALID_ARGUMENT);
CHECK(bad.status == LARDON3D_DENSE_MVS_INVALID_ARGUMENT &&
bad.status != LARDON3D_DENSE_MVS_OK && !bad.point_cloud_path[0] &&
!bad.point_count);
return true;
}
bool failure_atomic_and_mapping() {
F f;
CHECK(install(f));
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_OK);
result.point_count = 99;
std::memset(result.dense_identity, 0xa5, sizeof result.dense_identity);
f.cam.pose_cw.rotation_cw[0] = 2.0;
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT);
CHECK(result.status == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT &&
result.point_count == 0 && result.point_cloud_path[0] == '\0' &&
zero(result.dense_identity, sizeof result.dense_identity));
f.cam.pose_cw.rotation_cw[0] = 1.0;
CHECK(install(f));
f.src.calibration.fx = std::numeric_limits<double>::denorm_min();
f.src.calibration.fy = std::numeric_limits<double>::denorm_min();
f.src.calibration.k1 = 0.1;
result.point_count = 99;
std::memset(result.dense_identity, 0xa5, sizeof result.dense_identity);
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(result.status == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE &&
result.point_count == 0 && result.point_cloud_path[0] == '\0' &&
zero(result.dense_identity, sizeof result.dense_identity));
f.src.calibration = {64, 48, 50, 51, 32, 24, 0, 0, 0, 0};
const std::string marker = f.dir + "/capability-called";
const std::string backend_marker = f.dir + "/backend-called";
CHECK(put(f.ic,
"#!/bin/sh\nif [ \"$1\" = --help ]; then printf '%s\\n' "
"'OpenMVS x64 v2.4.0 --max-threads --image-folder'; "
"printf called > '" + marker + "'; exit 1; fi\nprintf backend > '" +
backend_marker + "'\n"));
f.src.immutable_sha256[0] ^= 1U;
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(access(marker.c_str(), F_OK) != 0 &&
access(backend_marker.c_str(), F_OK) != 0);
f.src.immutable_sha256[0] ^= 1U;
f.src.source_path = f.dir.c_str();
CHECK(lardon3d_dense_mvs_run(&f.in, &result) ==
LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
f.src.source_path = f.image.c_str();
F io;
CHECK(install(io));
const std::string blocked = io.dir + "/blocked";
CHECK(put(blocked, "not-a-directory", false));
io.in.staging_directory = blocked.c_str();
CHECK(lardon3d_dense_mvs_run(&io.in, &result) == LARDON3D_DENSE_MVS_IO_ERROR);
CHECK(result.status == LARDON3D_DENSE_MVS_IO_ERROR && result.point_count == 0);
F pointers;
CHECK(install(pointers));
pointers.snap.observations = nullptr;
CHECK(run(pointers, result) == LARDON3D_DENSE_MVS_INVALID_SNAPSHOT);
pointers.snap.observations = &pointers.observation;
pointers.in.source_observations = nullptr;
CHECK(run(pointers, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
return true;
}
size_t open_fd_count() {
DIR *directory = opendir("/proc/self/fd");
if (!directory) return SIZE_MAX;
size_t count = 0;
while (const dirent *entry = readdir(directory))
if (std::strcmp(entry->d_name, ".") && std::strcmp(entry->d_name, ".."))
++count;
closedir(directory);
return count;
}
bool source_file_types_and_dimensions() {
for (const std::string extension : {".jpg", ".png", ".bmp"}) {
F f;
CHECK(install(f));
const std::string encoded = f.dir + "/source" + extension;
cv::Mat pixels(48, 64, CV_8UC3, cv::Scalar(20, 40, 60));
CHECK(cv::imwrite(encoded, pixels));
f.image = encoded;
f.src.source_path = f.image.c_str();
CHECK(digest_file(f.image, f.src.immutable_sha256));
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_OK);
f.src.calibration.width++;
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
}
F f;
CHECK(install(f));
const std::string fifo = f.dir + "/source-fifo";
CHECK(mkfifo(fifo.c_str(), 0600) == 0);
f.image = fifo;
const size_t descriptors_before = open_fd_count();
CHECK(descriptors_before != SIZE_MAX);
for (size_t iteration = 0; iteration < 32; ++iteration) {
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
}
CHECK(open_fd_count() == descriptors_before);
const pid_t child = fork();
CHECK(child >= 0);
if (child == 0) {
Lardon3DDenseMvsResult result{};
const auto status = run(f, result);
_exit(status == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE ? 0 : 1);
}
int child_status = 0;
bool exited = false;
for (size_t iteration = 0; iteration < 200; ++iteration) {
const pid_t waited = waitpid(child, &child_status, WNOHANG);
CHECK(waited >= 0);
if (waited == child) {
exited = true;
break;
}
timespec delay{0, 10 * 1000 * 1000};
(void)nanosleep(&delay, nullptr);
}
if (!exited) {
(void)kill(child, SIGKILL);
(void)waitpid(child, &child_status, 0);
}
CHECK(exited && WIFEXITED(child_status) && WEXITSTATUS(child_status) == 0);
f.image = f.dir;
Lardon3DDenseMvsResult directory_result{};
CHECK(run(f, directory_result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(open_fd_count() == descriptors_before);
return true;
}
bool source_preflight_and_backend_bound() {
auto rejected_before_probe = [](F &f) {
const std::string marker = f.dir + "/probe-called";
CHECK(put(f.ic, "#!/bin/sh\nprintf called > '" + marker + "'\n") &&
put(f.dp, "#!/bin/sh\nprintf called > '" + marker + "'\n"));
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(access(marker.c_str(), F_OK) != 0);
return true;
};
{
F f;
f.src.calibration.fx = NAN;
CHECK(rejected_before_probe(f));
}
{
F f;
f.source_observation.image_id++;
CHECK(rejected_before_probe(f));
}
{
F f;
f.src.image_id++;
CHECK(rejected_before_probe(f));
}
{
F f;
Lardon3DSparseIncrementalLandmarkObservation snapshot_observations[2] = {
f.observation, f.observation};
Lardon3DSparseIncrementalObservation source_observations[2] = {
f.source_observation, f.source_observation};
f.snap.observations = snapshot_observations;
f.snap.observation_count = 2;
f.landmark.observation_count = 2;
f.in.source_observations = source_observations;
f.in.source_observation_count = 2;
CHECK(rejected_before_probe(f));
}
{
F f;
const unsigned char png_header[] = {
0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 0, 0, 0, 13,
'I', 'H', 'D', 'R', 0, 0, 0x4e, 0x20, 0, 0, 0x4e, 0x20};
CHECK(put(f.image, std::string(reinterpret_cast<const char *>(png_header),
sizeof png_header), false));
CHECK(digest_file(f.image, f.src.immutable_sha256));
CHECK(rejected_before_probe(f));
}
{
F f;
CHECK(install(f));
const std::string marker = f.dir + "/help-entered";
CHECK(put(f.ic,
"#!/bin/sh\nif [ \"$1\" = --help ]; then printf '%s\\n' "
"'OpenMVS x64 v2.4.0 --max-threads --image-folder'; "
"printf called > '" + marker + "'; exit 1; fi\nexit 0\n"));
CHECK(truncate(f.ic.c_str(),
static_cast<off_t>(UINT64_C(1024) * 1024 * 1024 + 1)) == 0);
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_BACKEND_ERROR);
CHECK(access(marker.c_str(), F_OK) == 0);
}
{
F f;
CHECK(install(f));
CHECK(truncate(f.dp.c_str(),
static_cast<off_t>(UINT64_C(1024) * 1024 * 1024)) == 0);
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_BACKEND_ERROR);
}
return true;
}
bool complete_colmap_contract() {
F f;
CHECK(install(f));
f.src.calibration.k1 = 0.12;
f.src.calibration.k2 = -0.03;
f.src.calibration.p1 = 0.002;
f.src.calibration.p2 = -0.001;
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_OK);
const std::string workspace = result_workspace(result);
cv::Mat exported = cv::imread(workspace + "/colmap/images/image_1.png",
cv::IMREAD_UNCHANGED);
CHECK(!exported.empty() && exported.cols == 64 && exported.rows == 48);
std::ifstream images(workspace + "/colmap/sparse/images.txt");
std::string pose_line, observation_line;
CHECK(bool(std::getline(images, pose_line)) &&
bool(std::getline(images, observation_line)));
std::istringstream observation_tokens(observation_line);
observation_tokens.imbue(std::locale::classic());
double exported_x = 0, exported_y = 0;
uint32_t point_id = 0;
CHECK(bool(observation_tokens >> exported_x >> exported_y >> point_id) &&
point_id == 1);
const auto &k = f.src.calibration;
const cv::Mat camera = cv::Mat(cv::Matx33d(
k.fx, 0, k.cx,
0, k.fy, k.cy,
0, 0, 1));
const cv::Mat distortion = cv::Mat(cv::Matx<double, 1, 4>(
k.k1, k.k2, k.p1, k.p2));
std::vector<cv::Point2d> source = {
{f.source_observation.x, f.source_observation.y}};
std::vector<cv::Point2d> expected;
cv::undistortPoints(source, expected, camera, distortion, cv::noArray(), camera);
CHECK(exported_x == expected[0].x && exported_y == expected[0].y);
std::ifstream points(workspace + "/colmap/sparse/points3D.txt");
uint32_t exported_point_id = 0, red = 0, green = 0, blue = 0;
double x = 0, y = 0, z = 0, error = 0;
uint32_t image_id = 0;
size_t point2d_index = SIZE_MAX;
CHECK(bool(points >> exported_point_id >> x >> y >> z >> red >> green >> blue >>
error >> image_id >> point2d_index));
CHECK(exported_point_id == 1 && x == 1 && y == 2 && z == 3 &&
red == 0 && green == 0 && blue == 0 && error == 0.25 &&
image_id == 1 && point2d_index == 0);
return true;
}
bool multi_track_membership() {
F f;
CHECK(install(f));
Lardon3DSparseIncrementalCamera cameras[2] = {f.cam, f.cam};
cameras[1].image_id = 18;
Lardon3DSparseIncrementalLandmark landmarks[2] = {f.landmark, f.landmark};
landmarks[0].observation_count = 2;
landmarks[1].landmark_id = 24;
landmarks[1].track_id = 30;
landmarks[1].point = {4, 5, 6};
landmarks[1].observation_count = 2;
Lardon3DSparseIncrementalLandmarkObservation observations[4] = {
{24, 30, 18, 34, 40, 0},
{23, 29, 17, 31, 37, 0},
{24, 30, 17, 33, 39, 1},
{23, 29, 18, 32, 38, 1}};
Lardon3DSparseIncrementalObservation source_observations[4] = {
{30, 18, 34, 40, 41, 14, 15},
{29, 17, 31, 37, 40, 10, 11},
{30, 17, 33, 39, 42, 12, 13},
{29, 18, 32, 38, 43, 16, 17}};
Lardon3DDenseMvsSourceImage images[2] = {f.src, f.src};
images[1].image_id = 18;
f.snap.cameras = cameras;
f.snap.camera_count = 2;
f.snap.landmarks = landmarks;
f.snap.landmark_count = 2;
f.snap.observations = observations;
f.snap.observation_count = 4;
f.in.source_observations = source_observations;
f.in.source_observation_count = 4;
f.in.source_images = images;
f.in.source_image_count = 2;
Lardon3DDenseMvsResult result{};
const auto status = run(f, result);
if (status != LARDON3D_DENSE_MVS_OK)
std::fprintf(stderr, "multi-track returned %d\n", static_cast<int>(status));
CHECK(status == LARDON3D_DENSE_MVS_OK);
std::ifstream points(result_workspace(result) + "/colmap/sparse/points3D.txt");
std::string first, second;
CHECK(bool(std::getline(points, first)) && bool(std::getline(points, second)));
CHECK(first == "1 1.0000000000000000e+00 2.0000000000000000e+00 "
"3.0000000000000000e+00 0 0 0 2.5000000000000000e-01 1 0 2 0");
CHECK(second == "2 4.0000000000000000e+00 5.0000000000000000e+00 "
"6.0000000000000000e+00 0 0 0 2.5000000000000000e-01 1 1 2 1");
const std::string test_path(__FILE__);
const size_t tests_component = test_path.rfind("tests/test_dense_mvs.cpp");
CHECK(tests_component != std::string::npos);
std::ifstream source(test_path.substr(0, tests_component) + "src/dense_mvs.cpp");
const std::string implementation((std::istreambuf_iterator<char>(source)), {});
CHECK(implementation.find("for (const auto &entry : image_observations)") ==
std::string::npos);
CHECK(implementation.find("before.st_dev == after.st_dev") !=
std::string::npos &&
implementation.find("before.st_ino == after.st_ino") !=
std::string::npos &&
implementation.find("before.st_size == after.st_size") !=
std::string::npos &&
implementation.find("before.st_mtim.tv_nsec == after.st_mtim.tv_nsec") !=
std::string::npos &&
implementation.find("before.st_ctim.tv_nsec == after.st_ctim.tv_nsec") !=
std::string::npos);
return true;
}
bool bounded_inputs_and_empty_camera_observations() {
{
F f;
const std::string backend_marker = f.dir + "/backend-called";
CHECK(put(f.ic, "#!/bin/sh\nprintf called > '" + backend_marker + "'\n") &&
put(f.dp, "#!/bin/sh\nprintf called > '" + backend_marker + "'\n"));
CHECK(truncate(f.image.c_str(),
static_cast<off_t>(UINT64_C(1024) * 1024 * 1024 + 1)) == 0);
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(result.status == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE &&
access(backend_marker.c_str(), F_OK) != 0);
}
{
F f;
CHECK(install(f));
f.in.parameters.fusion_mode = 1;
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_ARGUMENT);
CHECK(result.status == LARDON3D_DENSE_MVS_INVALID_ARGUMENT &&
access(f.log.c_str(), F_OK) != 0);
}
{
F f;
CHECK(install(f));
f.src.calibration.width = UINT32_MAX;
f.src.calibration.height = UINT32_MAX;
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
CHECK(result.status == LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE);
}
{
F f;
CHECK(install(f));
Lardon3DSparseIncrementalCamera cameras[2] = {f.cam, f.cam};
cameras[1].image_id = 18;
Lardon3DDenseMvsSourceImage images[2] = {f.src, f.src};
images[1].image_id = 18;
f.snap.cameras = cameras;
f.snap.camera_count = 2;
f.in.source_images = images;
f.in.source_image_count = 2;
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_OK);
std::ifstream exported(result_workspace(result) + "/colmap/sparse/images.txt");
std::string first_pose, first_points, second_pose, second_points;
CHECK(bool(std::getline(exported, first_pose)) &&
bool(std::getline(exported, first_points)) &&
bool(std::getline(exported, second_pose)) &&
bool(std::getline(exported, second_points)));
CHECK(!first_points.empty() && !second_pose.empty() && second_points.empty());
}
return true;
}
bool deterministic_colmap_numbers() {
F f;
CHECK(install(f));
const double calibration[] = {
501.2345678901234, 502.3456789012345, 319.4567890123456,
239.5678901234567, 0.1234567890123456, -0.2345678901234567,
0.0034567890123456, -0.0045678901234567};
f.src.calibration.fx = calibration[0];
f.src.calibration.fy = calibration[1];
f.src.calibration.cx = calibration[2];
f.src.calibration.cy = calibration[3];
f.src.calibration.k1 = calibration[4];
f.src.calibration.k2 = calibration[5];
f.src.calibration.p1 = calibration[6];
f.src.calibration.p2 = calibration[7];
const double translation[] = {
9.876543210987654, -8.765432109876543, 0.0076543210987654};
std::memcpy(f.cam.pose_cw.translation_cw, translation, sizeof translation);
GlobalLocaleGuard locale_guard;
Lardon3DDenseMvsResult result{};
CHECK(run(f, result) == LARDON3D_DENSE_MVS_OK);
const std::string workspace = result_workspace(result);
std::ifstream cameras(workspace + "/colmap/sparse/cameras.txt");
std::ifstream images(workspace + "/colmap/sparse/images.txt");
const std::string camera_text((std::istreambuf_iterator<char>(cameras)), {});
const std::string image_text((std::istreambuf_iterator<char>(images)), {});
std::istringstream camera_tokens(camera_text);
std::istringstream image_tokens(image_text);
camera_tokens.imbue(std::locale::classic());
image_tokens.imbue(std::locale::classic());
uint64_t camera_id = 0, image_id = 0, image_camera_id = 0;
unsigned int width = 0, height = 0;
std::string model, image_name;
std::string camera_numbers[4], quaternion_numbers[4], translation_numbers[3];
CHECK(bool(camera_tokens >> camera_id >> model >> width >> height));
for (std::string &token : camera_numbers)
CHECK(bool(camera_tokens >> token));
CHECK(bool(image_tokens >> image_id));
for (std::string &token : quaternion_numbers)
CHECK(bool(image_tokens >> token));
for (std::string &token : translation_numbers)
CHECK(bool(image_tokens >> token));
CHECK(bool(image_tokens >> image_camera_id >> image_name));
CHECK(camera_id == 1 && image_id == 1 && image_camera_id == 1 &&
model == "PINHOLE" && width == 64 && height == 48 &&
image_name == "image_1.png");
auto scientific_round_trip = [](const std::string &token, double expected) {
CHECK(token.find('.') != std::string::npos);
CHECK(token.find_first_of("eE") != std::string::npos);
std::istringstream parser(token);
parser.imbue(std::locale::classic());
double parsed = 0;
CHECK(bool(parser >> parsed));
CHECK(parser.peek() == std::char_traits<char>::eof());
CHECK(parsed == expected);
return true;
};
const double exported_calibration[] = {
calibration[0], calibration[1], calibration[2] + 0.5,
calibration[3] + 0.5};
for (size_t i = 0; i < 4; ++i)
CHECK(scientific_round_trip(camera_numbers[i], exported_calibration[i]));
for (const std::string &token : quaternion_numbers) {
CHECK(token.find('.') != std::string::npos);
CHECK(token.find_first_of("eE") != std::string::npos);
}
for (size_t i = 0; i < 3; ++i)
CHECK(scientific_round_trip(translation_numbers[i], translation[i]));
CHECK(camera_text.find(',') == std::string::npos &&
image_text.find(',') == std::string::npos);
return true;
}
} // namespace
int main() {
return goldens() && calibration_identity_contract() &&
execution_configuration_not_scientific_identity() &&
backend_argv() && rotations() && processes() && plys() &&
private_workspace_isolation() && provenance_path() &&
failure_atomic_and_mapping() && complete_colmap_contract() &&
source_file_types_and_dimensions() &&
multi_track_membership() &&
source_preflight_and_backend_bound() &&
bounded_inputs_and_empty_camera_observations() &&
deterministic_colmap_numbers()
? 0
: 1;
}