diff --git a/README.md b/README.md index f4ac5ce..2c68672 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ persistante, enrichissable et versionnable. Adjustment final par composante, tous PASS / FROZEN - **Sparse SfM Gate F** : orchestration durable, runtime gouverné et publication atomique, PASS / FROZEN +- **MVS-M1** : frontière externe OpenMVS v2.4.0 + `InterfaceCOLMAP`/`DensifyPointCloud`, export COLMAP déterministe (OpenCV + undistortion, observations transformées et tracks réels), texte exporté en + flux et tracks indexés sans rescanner quadratiquement les observations ; espace + de travail privé neuf par invocation sous le staging appelant, sans réemploi ; + identité dense liée à la reconstruction de base, au jeu source, au + `calibration_scope_identity` historique, au binding numérique MVS, au backend + et aux paramètres ; `L3DMDID2` v2 (220 octets) et binding `L3DMCAL1` v1 ; PLY + OpenMVS binaire little-endian validé (en-tête <= 1 MiB en octets bruts, + LF/CRLF acceptés, CR seul malformé rejeté, ligne <= 64 KiB), fusionné en + mode 0 — IMPLEMENTED / + VALIDATION PENDING - **Geometric Verification Model v1** : identité, masque d'inliers et modèle 3×3 persistants - **Geometric Verifier v1** : Fundamental USAC/MAGSAC, reprise et lots resource-aware - **Task Kind Registry** : identité métier durable et reconstruction runtime explicite @@ -171,9 +183,19 @@ fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; ces deux ma restent sur OpenCV L2. Track Model/Builder, les primitives géométriques Gate C, le noyau Sparse SfM incrémental Gate D et le Bundle Adjustment final Gate E sont implémentés et validés. L'orchestration Sparse SfM Gate F est PASS / FROZEN ; -l'intégration Governor Gate G est **PASS / FROZEN** ; le DAG, -le viewer et les étapes denses restent des tickets séparés planifiés. Le Resource Governor ne -constitue pas un Resource System générique : voir la décision d’architecture. +l'intégration Governor Gate G est **PASS / FROZEN**. MVS-M1 est **IMPLEMENTED / +VALIDATION PENDING** : une frontière OpenMVS v2.4.0 externe et bornée, sans +publication dense durable ni MVS complet. Les sources sont liées par SHA-256 +complet, borné à 1 GiB par fichier régulier (sans budget agrégé de dataset) ; les +octets source restent un binding distinct de l'identité dense. Celle-ci lie la +reconstruction de base, le jeu d'images source, le `calibration_scope_identity` +historique, le binding numérique de calibration MVS `L3DMCAL1` v1, le backend et +les paramètres dans `L3DMDID2` v2 (220 octets). Chaque appel utilise un espace de +travail privé neuf sous le staging appelant, sans réemploi d'une scène, +profondeur, cache ou sortie antérieure. Le DAG, le viewer et les autres étapes +denses restent des tickets séparés planifiés. +Le Resource Governor ne constitue pas un Resource System générique : voir la décision +d’architecture. ## Licence diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index 24d82e6..e1d1c2f 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -253,6 +253,103 @@ durables, scratch/SSD, GPU SfM/BA et nouvelles composantes déconnectées. --- +### MVS-M1. Dense externe borné + +MVS-M1 est **IMPLEMENTED / VALIDATION PENDING**. Son identité scientifique lie +l'identité de reconstruction de base, l'identité du jeu d'images source, le +`calibration_scope_identity` historique, le binding numérique de calibration MVS, +le backend et les paramètres. Les octets des sources restent liés séparément par +leur SHA-256 ; ils ne sont pas remplacés par le binding de calibration. Le record +d'identité dense est `L3DMDID2`, version 2, de taille fixe 220 octets. Sa frontière +externe est OpenMVS v2.4.0 : Sparse/H est exporté vers COLMAP, puis traité par +`InterfaceCOLMAP` et `DensifyPointCloud`. + +Le binding numérique de calibration est le record `L3DMCAL1`, version 1. Il est +formé après tri strict des `image_id` et rejet des identifiants dupliqués ; tous +les entiers et binary64 y sont encodés explicitement en little-endian de largeur +fixe. Les NaN et infinis sont rejetés et toute valeur `-0` est canonisée en `+0` +avant l'encodage. Il lie, pour chaque image, son identifiant, ses dimensions et +les paramètres numériques de calibration utilisés par l'undistortion. + +L'export COLMAP est déterministe et écrit en flux : locale classique, notation +scientifique et précision binary64 fidèle au round-trip (`max_digits10 - 1` +chiffres après le point). Les images et les observations source sont undistordues +de façon déterministe avec OpenCV ; les observations exportées sont les +coordonnées transformées. `points3D.txt` contient les tracks COLMAP réels, +produits dans l'ordre déterministe à partir d'un index de landmarks et de leurs +observations : l'export ne rescane pas quadratiquement les observations pour +chaque landmark. + +Chaque invocation crée sous le staging fourni par l'appelant un espace de travail +privé neuf. Aucune scène, profondeur, cache ou sortie d'une invocation antérieure +n'est recherchée ni réemployée. Les images undistordues et tous les intermédiaires +COLMAP/OpenMVS restent dans cet espace de travail. + +Le résultat accepté est le PLY fusionné ; `fusion_mode` doit être 0. Le lecteur +valide le format de sortie par défaut d'OpenMVS v2.4.0 : PLY binaire +little-endian. Le plafond d'en-tête est de 1 MiB et compte les octets bruts : +une fin de ligne CRLF consomme donc deux octets, contrairement à LF. Les règles +de lignes acceptent LF et CRLF ; un CR seul malformé est rejeté. Chaque ligne +est bornée à 64 KiB. Le lecteur borne les listes de propriétés `view_indices` et +`view_weights` avant +allocation ou lecture de leur contenu, et valide les valeurs numériques sans +dépendre de la locale du processus. + +La capacité CPU-only est conditionnelle : le probe borné interroge chaque +exécutable backend, vérifie qu'il s'identifie comme OpenMVS v2.4.0 et relève les +options effectivement exposées. Si `DensifyPointCloud` expose `--cuda-device`, +l'adaptateur demande explicitement le périphérique CPU ; sinon il n'ajoute aucune +option CUDA. Les options requises, dont `--max-threads`, doivent être exposées ; +celle-ci reçoit le nombre de threads d'exécution. Ce nombre est opérationnel et +exclu de l'identité scientifique. + +Les contrats B1–B6 du jalon sont les suivants : + +- **B1 — result failure atomicity.** Un échec ne retourne aucun + résultat MVS-M1 partiel. +- **B2 — source-image status classification.** Les erreurs source, + snapshot, I/O, backend, output et OOM restent distinctes. +- **B3 — source byte identity binding.** Chaque source acceptée est + un fichier régulier dont la taille du descripteur est contrôlée avant le hash ; + son SHA-256 complet est calculé avec une mémoire de streaming fixe, jusqu'à + 1 GiB au maximum par fichier source, puis la stabilité du fichier est vérifiée + après lecture. Il n'existe volontairement aucun budget agrégé de 1 GiB pour le + jeu de sources : la vérification totale croît avec les octets d'entrée acceptés + et son admission/comptage global relève d'un futur Task/Governor. +- **B4 — deterministic COLMAP numeric export.** Les images, observations et + tracks sont ceux décrits ci-dessus ; aucune observation distordue n'est exportée + à la place de sa coordonnée transformée, et l'export des tracks ne réalise pas + de rescan quadratique landmark-observation. Une coordonnée source undistordue + non finie est `INVALID_SOURCE_IMAGE`, tandis qu'une pose du snapshot invalide + reste `INVALID_SNAPSHOT`. +- **B5 — write/close FD cleanup.** Les descripteurs possédés sont nettoyés + explicitement lors de l'écriture et de la fermeture, + y compris sur erreur. +- **B6 — C ABI / `std::bad_alloc` containment.** L'API C ne laisse pas + traverser `std::bad_alloc`. + +Indépendamment de la borne de ligne PLY, l'inspection de l'en-tête d'image +encodée avant décodage est bornée à 64 KiB. Les formats acceptés implémentés sont +PNG, JPEG et BMP ; les dimensions, le nombre de pixels et l'estimation de working +set sont validés avant décodage (16 384 pixels par dimension, 40 Mpx et 240 MB). +Les intermédiaires d'undistortion déterministe +et les intermédiaires COLMAP/OpenMVS sont privés à l'espace de travail neuf créé +par invocation sous le staging appelant ; aucune invocation ne réemploie des +artefacts antérieurs. + +Les deux exécutables backend sont hashés sous un budget borné partagé, distinct +du plafond par fichier des sources. Les processus possédés et leurs groupes sont +nettoyés ; stdout et stderr sont intégralement drainés pour chaque backend, avec +au plus 1 MiB retenu par log. L'excédent est consommé puis écarté, sans empêcher +le backend de poursuivre et de réussir. + +Restent différés : publication dense dans la Project DB, Task Runtime, +Queue/Governor, `ResourceEstimate`, annulation, publication dense durable, +mesh, texturing, viewer, scratch/SSD et infrastructure backend généralisée. +Ce jalon ne marque ni MVS complet ni PASS / FROZEN. + +--- + ### I. Reconstruction Layers | Aspect | Description | @@ -360,7 +457,26 @@ and Sparse SfM Gate C geometry are **IMPLEMENTED**. The synchronous in-memory incremental Sparse SfM Gate D core is **IMPLEMENTED / PASS**, and final per-component Gate E BA is **PASS / FROZEN**. Gate F orchestration and Gate G Governor integration are **PASS / FROZEN**. Phase H v1 is **PASS / FROZEN**. -MVS, mesh, texturing and viewer remain **PLANNED**. +MVS-M1 is **IMPLEMENTED / VALIDATION PENDING**: `L3DMDID2` v2 (220-byte) dense +identity binds the base reconstruction, source-image-set, +historical `calibration_scope_identity`, exact `L3DMCAL1` v1 numeric calibration +binding, backend and parameters, while source bytes remain separately bound; +deterministic OpenCV-undistorted COLMAP images, transformed observations and real +tracks exported from an index without a quadratic landmark-observation rescan; +and the external OpenMVS v2.4.0 +`InterfaceCOLMAP`/`DensifyPointCloud` boundary. Every invocation has a fresh +private workspace beneath caller staging and reuses no prior scene, depth, cache +or output. Its numeric calibration binding sorts image IDs, uses explicit +fixed-width little-endian fields, rejects NaN/Inf and canonicalizes `-0`; PLY +headers are capped at 1 MiB of raw bytes (so CRLF costs two), accept LF/CRLF, +reject malformed bare CR, and cap each line at 64 KiB. +Non-finite undistorted source coordinates are `INVALID_SOURCE_IMAGE`; invalid +snapshot poses remain `INVALID_SNAPSHOT`. It validates OpenMVS's default binary +little-endian PLY, including bounded `view_indices`/`view_weights` lists with +locale-independent numeric validation, requires fusion mode 0 for the fused PLY, +and has conditional CPU-only capability plus supported `--max-threads`. It does +not include durable dense publication, runtime/governor integration, mesh, +texturing or viewer; those remain **PLANNED**. Ce document décrit la vision architecturale cible du pipeline de reconstruction. Les modules listés ici ne sont pas tous implémentés. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 35a8206..b338939 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -58,7 +58,31 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la ### Phase 5 : Reconstruction - ✅ Orchestration de reconstruction incrémentale H v1 — **PASS / FROZEN** -- 📋 MVS / dense +- 🔄 MVS-M1 — **IMPLEMENTED / VALIDATION PENDING** : identité dense `L3DMDID2` + v2 (220 octets) liant reconstruction de base, jeu d'images source, + `calibration_scope_identity` historique, binding numérique exact `L3DMCAL1` v1, + backend et paramètres ; les octets source restent liés séparément ; frontière + OpenMVS v2.4.0 externe + `InterfaceCOLMAP`/`DensifyPointCloud` ; export COLMAP déterministe avec + undistortion OpenCV, observations transformées et tracks réels ; texte COLMAP + exporté en flux et tracks déterministes indexés sans rescan quadratique des + observations. Le binding de calibration trie les `image_id`, encode des champs + little-endian explicites à largeur fixe, rejette NaN/Inf et canonise `-0`. Chaque + invocation crée sous le staging appelant un espace de travail privé neuf, sans + réemployer scène, profondeur, cache ou sortie antérieure. Le PLY par défaut + OpenMVS v2.4.0, binaire little-endian, est validé avec listes + `view_indices`/`view_weights` bornées, nombres indépendants de la locale, + en-tête <= 1 MiB en octets bruts (CRLF = deux octets), lignes <= 64 KiB, + LF/CRLF acceptés et CR seul malformé rejeté ; le PLY fusionné est requis en mode + de fusion 0. Une coordonnée source undistordue + non finie est `INVALID_SOURCE_IMAGE`, une pose snapshot invalide reste + `INVALID_SNAPSHOT`. Capacité CPU-only conditionnelle et `--max-threads` + supporté ; bornes locales. Le SHA-256 source est complet et borné à 1 GiB par + fichier régulier, sans budget agrégé du jeu de sources ; le hashage des deux + binaires backend relève d'un budget partagé distinct. Sans nouveau sous-système. + Restent différés : publication Project DB/durable, Task Runtime, + Queue/Governor, `ResourceEstimate`, annulation, mesh, texturing, viewer, + scratch/SSD et infrastructure backend généralisée. - 📋 Mesh - 📋 Contraintes externes - 📋 Consolidation diff --git a/include/lardon3d/dense_mvs.h b/include/lardon3d/dense_mvs.h new file mode 100644 index 0000000..de607df --- /dev/null +++ b/include/lardon3d/dense_mvs.h @@ -0,0 +1,131 @@ +#ifndef LARDON3D_DENSE_MVS_H +#define LARDON3D_DENSE_MVS_H + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_DENSE_MVS_SHA256_SIZE = 32, + LARDON3D_DENSE_MVS_PARAMETER_RECORD_SIZE = 40, + LARDON3D_DENSE_MVS_IDENTITY_RECORD_SIZE = 220, + LARDON3D_DENSE_MVS_BACKEND_MANIFEST_RECORD_SIZE = 148, + LARDON3D_DENSE_MVS_VERSION_IDENTITY_SIZE = 32, + LARDON3D_DENSE_MVS_PATH_CAPACITY = 4096, + LARDON3D_DENSE_MVS_KIND_OPENMVS = 1, + LARDON3D_DENSE_MVS_VERSION = 1, + LARDON3D_DENSE_MVS_OPENMVS_VERSION = 0x00020400, +}; + +typedef enum { + LARDON3D_DENSE_MVS_OK = 0, + LARDON3D_DENSE_MVS_INVALID_ARGUMENT, + LARDON3D_DENSE_MVS_INVALID_SNAPSHOT, + LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE, + LARDON3D_DENSE_MVS_IO_ERROR, + LARDON3D_DENSE_MVS_BACKEND_ERROR, + LARDON3D_DENSE_MVS_INVALID_OUTPUT, + LARDON3D_DENSE_MVS_OUT_OF_MEMORY, +} Lardon3DDenseMvsStatus; + +typedef struct { + uint32_t resolution_level; + uint32_t minimum_resolution; + uint32_t number_views; + uint32_t fusion_mode; +} Lardon3DDenseMvsParameters; + +/* The version fields identify the pinned version established by bounded --help probes. */ +typedef struct { + unsigned char interface_colmap_version_identity[32]; + unsigned char interface_colmap_binary_sha256[32]; + unsigned char densify_point_cloud_version_identity[32]; + unsigned char densify_point_cloud_binary_sha256[32]; +} Lardon3DDenseMvsBackendManifest; + +typedef struct { + unsigned char base_reconstruction_identity[32]; + unsigned char source_image_set_identity[32]; + unsigned char calibration_scope_identity[32]; + unsigned char calibration_binding_identity[32]; + uint32_t dense_kind; + uint32_t dense_version; + uint32_t backend_kind; + uint32_t backend_version; + /* SHA-256 of the fixed-order OpenMVS v1 backend manifest. */ + unsigned char backend_binary_sha256[32]; + unsigned char parameter_fingerprint[32]; +} Lardon3DDenseMvsIdentity; + +typedef struct { + uint64_t image_id; + unsigned char immutable_sha256[32]; + const char *source_path; + Lardon3DSparseGeometryCalibration calibration; +} Lardon3DDenseMvsSourceImage; + +typedef struct { + const Lardon3DSparseIncrementalResult *snapshot; + /* Source-pixel coordinates keyed by the snapshot observation identity. */ + const Lardon3DSparseIncrementalObservation *source_observations; + size_t source_observation_count; + const Lardon3DDenseMvsSourceImage *source_images; + size_t source_image_count; + unsigned char base_reconstruction_identity[32]; + unsigned char calibration_scope_identity[32]; + Lardon3DDenseMvsParameters parameters; + uint32_t execution_thread_count; + const char *staging_directory; + const char *interface_colmap_executable; + const char *densify_point_cloud_executable; +} Lardon3DDenseMvsInput; + +typedef struct { + Lardon3DDenseMvsStatus status; + unsigned char dense_identity[32]; + unsigned char parameter_fingerprint[32]; + unsigned char base_reconstruction_identity[32]; + unsigned char source_image_set_identity[32]; + unsigned char backend_implementation_sha256[32]; + char point_cloud_path[LARDON3D_DENSE_MVS_PATH_CAPACITY]; + uint64_t point_count; +} Lardon3DDenseMvsResult; + +bool lardon3d_dense_mvs_parameter_fingerprint_record( + const Lardon3DDenseMvsParameters *parameters, + unsigned char record[LARDON3D_DENSE_MVS_PARAMETER_RECORD_SIZE]); +bool lardon3d_dense_mvs_parameter_fingerprint( + const Lardon3DDenseMvsParameters *parameters, unsigned char digest[32]); +bool lardon3d_dense_mvs_backend_manifest_record( + const Lardon3DDenseMvsBackendManifest *manifest, + unsigned char record[LARDON3D_DENSE_MVS_BACKEND_MANIFEST_RECORD_SIZE]); +bool lardon3d_dense_mvs_backend_manifest_digest( + const Lardon3DDenseMvsBackendManifest *manifest, unsigned char digest[32]); +bool lardon3d_dense_mvs_identity_record( + const Lardon3DDenseMvsIdentity *identity, + unsigned char record[LARDON3D_DENSE_MVS_IDENTITY_RECORD_SIZE]); +bool lardon3d_dense_mvs_identity_digest( + const Lardon3DDenseMvsIdentity *identity, unsigned char digest[32]); +bool lardon3d_dense_mvs_source_image_set_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]); +bool lardon3d_dense_mvs_calibration_binding_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]); + +/* run() replaces result on success and resets it to failure-atomic state on error. */ +Lardon3DDenseMvsStatus lardon3d_dense_mvs_run( + const Lardon3DDenseMvsInput *input, Lardon3DDenseMvsResult *result); +void lardon3d_dense_mvs_result_destroy(Lardon3DDenseMvsResult *result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/meson.build b/meson.build index 8743722..8196a9c 100644 --- a/meson.build +++ b/meson.build @@ -43,6 +43,13 @@ opencv_benchmark = dependency( modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_imgproc'], ) +opencv_dense_mvs = dependency( + 'opencv5', + required: true, + include_type: 'system', + modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_imgproc', 'opencv_calib3d'], +) + opencv_geometry = dependency( 'opencv5', version: '>=5.0.0', @@ -158,6 +165,8 @@ executable( 'src/incremental_reconstruction_identity.cpp', 'src/incremental_reconstruction.cpp', 'src/incremental_reconstruction_task.cpp', + 'src/dense_mvs_identity.cpp', + 'src/dense_mvs.cpp', 'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c', @@ -179,7 +188,16 @@ executable( '-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE', '-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE', ], - dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry, ceres] + dependencies: [ + ncursesw, + threads, + sqlite3, + openssl, + opencv, + opencv_geometry, + opencv_dense_mvs, + ceres, + ] + matcher_backend_dependencies, ) @@ -980,6 +998,14 @@ incremental_reconstruction_test = executable( ) test('incremental-reconstruction', incremental_reconstruction_test, timeout: 120) +dense_mvs_test = executable( + 'test-dense-mvs', + sources: ['tests/test_dense_mvs.cpp', 'src/dense_mvs_identity.cpp', 'src/dense_mvs.cpp'], + include_directories: include_directories('include'), + dependencies: [openssl, opencv_dense_mvs], +) +test('dense-mvs', dense_mvs_test, timeout: 30) + sparse_sfm_payload_test = executable( 'test-sparse-sfm-payload', sources: [ diff --git a/src/dense_mvs.cpp b/src/dense_mvs.cpp new file mode 100644 index 0000000..84f89e0 --- /dev/null +++ b/src/dense_mvs.cpp @@ -0,0 +1,1288 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lardon3d::dense_mvs_detail { +bool source_image_set_identity(const Lardon3DDenseMvsSourceImage *images, + size_t count, unsigned char digest[32]); +bool calibration_binding_identity(const Lardon3DDenseMvsSourceImage *images, + size_t count, unsigned char digest[32]); +} // namespace lardon3d::dense_mvs_detail + +namespace { +constexpr size_t kMaxVersion = 16384; +constexpr int kVersionTimeoutMs = 5000; +constexpr uint64_t kMaxSourceFileBytes = UINT64_C(1024) * 1024 * 1024; +constexpr uint64_t kMaxBackendFileBytes = UINT64_C(1024) * 1024 * 1024; +constexpr size_t kMaxBackendLogBytes = 1024 * 1024; +constexpr size_t kMaxPlyHeader = 1024 * 1024; +constexpr size_t kMaxPlyLine = 64 * 1024; +constexpr size_t kMaxPlyProperties = 256; +bool sha(const unsigned char *p, size_t n, unsigned char d[32]) { + unsigned int length = 0; + return p && d && + EVP_Digest(p, n, d, &length, EVP_sha256(), nullptr) == 1 && + length == 32; +} +bool executable(const char *path) { + struct stat status {}; + return path && stat(path, &status) == 0 && S_ISREG(status.st_mode) && + access(path, X_OK) == 0; +} +bool same_file_snapshot(const struct stat &before, const struct stat &after) { + return before.st_dev == after.st_dev && before.st_ino == after.st_ino && + before.st_size == after.st_size && + before.st_mtim.tv_sec == after.st_mtim.tv_sec && + before.st_mtim.tv_nsec == after.st_mtim.tv_nsec && + before.st_ctim.tv_sec == after.st_ctim.tv_sec && + before.st_ctim.tv_nsec == after.st_ctim.tv_nsec; +} +bool sha_regular_file(const char *path, unsigned char digest[32], + uint64_t max_bytes = UINT64_MAX, + uint64_t *remaining_bytes = nullptr) { + if (!path || !digest) return false; + const int descriptor = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (descriptor < 0) return false; + struct stat status {}; + const uint64_t effective_max = remaining_bytes + ? std::min(max_bytes, *remaining_bytes) + : max_bytes; + bool ok = fstat(descriptor, &status) == 0 && S_ISREG(status.st_mode) && + status.st_size >= 0 && + static_cast(status.st_size) <= effective_max; + if (!ok) { + (void)close(descriptor); + return false; + } + FILE *file = fdopen(descriptor, "rb"); + if (!file) { + (void)close(descriptor); + return false; + } + const uint64_t expected_size = ok ? static_cast(status.st_size) : 0; + EVP_MD_CTX *context = EVP_MD_CTX_new(); + ok = ok && context && EVP_DigestInit_ex(context, EVP_sha256(), nullptr) == 1; + unsigned char buffer[8192]; + uint64_t total = 0; + while (ok && total < expected_size) { + const uint64_t remaining = expected_size - total; + const size_t request = static_cast( + std::min(remaining, sizeof buffer)); + const size_t size = std::fread(buffer, 1, request, file); + if (size != 0) { + total += size; + ok = EVP_DigestUpdate(context, buffer, size) == 1; + } + if (size < request) ok = false; + } + struct stat final_status {}; + ok = ok && !std::ferror(file) && total == expected_size && + fstat(fileno(file), &final_status) == 0 && + same_file_snapshot(status, final_status); + unsigned int length = 0; + if (ok) ok = EVP_DigestFinal_ex(context, digest, &length) == 1 && length == 32; + EVP_MD_CTX_free(context); + const int close_result = std::fclose(file); + ok = ok && close_result == 0; + if (ok && remaining_bytes) *remaining_bytes -= expected_size; + return ok; +} +bool ms(uint64_t *value) { + timespec time {}; + if (!value || clock_gettime(CLOCK_MONOTONIC, &time) || time.tv_sec < 0) + return false; + *value = static_cast(time.tv_sec) * 1000 + + static_cast(time.tv_nsec) / 1000000; + return true; +} +bool wait_nonblocking(pid_t child, int *status, bool *exited) { + for (;;) { + const pid_t result = waitpid(child, status, WNOHANG); + if (result == child) { *exited = true; return true; } + if (result == 0) return true; + if (errno == ECHILD) { *exited = true; return true; } + if (errno != EINTR) return false; + } +} + +/* Probe children have a deadline; backend execution intentionally does not. */ +bool signal_group(pid_t group, int signal) { + return kill(-group, signal) == 0 || errno == ESRCH; +} + +bool owns_process_group(pid_t child) { + const pid_t group = getpgid(child); + return group == child || (group < 0 && errno == ESRCH); +} + +bool terminate_and_reap(pid_t child, pid_t group) { + int status = 0; + bool exited = false; + if (!wait_nonblocking(child, &status, &exited)) return false; + if (!signal_group(group, SIGTERM)) return false; + uint64_t start = 0; + if (!ms(&start)) return false; + while (true) { + uint64_t now = 0; + if (!ms(&now) || now - start >= 250) break; + if (!wait_nonblocking(child, &status, &exited)) return false; + if (exited && kill(-group, 0) != 0 && errno == ESRCH) return true; + (void)poll(nullptr, 0, 10); + } + if (!signal_group(group, SIGKILL)) return false; + if (!ms(&start)) return false; + while (true) { + uint64_t now = 0; + if (!ms(&now) || now - start >= 250) return false; + if (!wait_nonblocking(child, &status, &exited)) return false; + if (exited) return true; + (void)poll(nullptr, 0, 10); + } +} + +bool run(const std::vector& a, const std::string &directory, + const char *log_name) { + if(a.empty())return false; + std::vector v; + for(const auto&x:a)v.push_back(const_cast(x.c_str())); + v.push_back(nullptr); + const std::string log_path = directory + "/" + log_name; + int log_fd = open(log_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (log_fd < 0) return false; + int output[2]; + if (pipe(output) != 0) { close(log_fd); return false; } + int launch[2]; + if (pipe(launch) != 0) { + close(output[0]); close(output[1]); close(log_fd); return false; + } + if (fcntl(launch[1], F_SETFD, FD_CLOEXEC) != 0) { + close(launch[0]); close(launch[1]); close(output[0]); close(output[1]); + close(log_fd); return false; + } + pid_t c=fork(); + if(c<0){close(launch[0]);close(launch[1]);close(output[0]);close(output[1]);close(log_fd);return false;} + if(!c){ + close(launch[0]); + close(output[0]); + const int input_fd = open("/dev/null", O_RDONLY); + if (setpgid(0, 0) != 0 || input_fd < 0 || dup2(input_fd, STDIN_FILENO) < 0 || + dup2(output[1], STDOUT_FILENO) < 0 || + dup2(output[1], STDERR_FILENO) < 0) { + const int error = errno; + (void)!write(launch[1], &error, sizeof error); + _exit(127); + } + close(input_fd); + close(output[1]); + close(log_fd); + execv(v[0],v.data()); + const int error = errno; + (void)!write(launch[1], &error, sizeof error); + _exit(127); + } + close(launch[1]); + close(output[1]); + if (setpgid(c, c) != 0 && errno != EACCES && errno != ESRCH) { + close(launch[0]); close(output[0]); close(log_fd); + (void)terminate_and_reap(c, c); + return false; + } + if (!owns_process_group(c)) { + close(launch[0]); close(output[0]); close(log_fd); + (void)terminate_and_reap(c, c); + return false; + } + int launch_error = 0; + ssize_t launch_size; + do { launch_size = read(launch[0], &launch_error, sizeof launch_error); } + while (launch_size < 0 && errno == EINTR); + close(launch[0]); + if (launch_size != 0) { + close(output[0]); close(log_fd); + (void)terminate_and_reap(c, c); + return false; + } + int flags = fcntl(output[0], F_GETFL); + if (flags < 0 || fcntl(output[0], F_SETFL, flags | O_NONBLOCK) != 0) { + close(output[0]); close(log_fd); + (void)terminate_and_reap(c, c); + return false; + } + int s = 0; + bool exited = false, eof = false, io_ok = true; + size_t retained = 0; + while (!exited || !eof) { + if (!exited && !wait_nonblocking(c, &s, &exited)) { io_ok = false; break; } + for (;;) { + unsigned char buffer[8192]; + const ssize_t size = read(output[0], buffer, sizeof buffer); + if (size > 0) { + const size_t keep = std::min(static_cast(size), + kMaxBackendLogBytes - retained); + if (keep && write(log_fd, buffer, keep) != static_cast(keep)) + io_ok = false; + retained += keep; + continue; + } + if (size == 0) eof = true; + else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) + io_ok = false; + break; + } + if (!io_ok || (exited && eof)) break; + pollfd descriptor{output[0], POLLIN | POLLHUP, 0}; + if (poll(&descriptor, 1, 50) < 0 && errno != EINTR) { io_ok = false; break; } + if (exited && !eof && (kill(-c, 0) == 0 || errno != ESRCH)) { + if (!terminate_and_reap(c, c)) io_ok = false; + } + } + close(output[0]); + if (close(log_fd) != 0) io_ok = false; + if (!io_ok) { + (void)terminate_and_reap(c, c); + return false; + } + const bool ok = WIFEXITED(s)&&WEXITSTATUS(s)==0; + /* A successful direct child may still have left helpers in its owned group. */ + if (kill(-c, 0) == 0 || errno != ESRCH) { + if (!terminate_and_reap(c, c)) return false; + } + return ok; +} + +struct BackendCapabilities { + bool cuda_option; + unsigned char version_identity[32]; +}; + +bool probe(const char *path, bool interface_colmap, + BackendCapabilities *capabilities) { + int fd[2]; + if (!path || !capabilities || pipe(fd)) return false; + pid_t c=fork(); + if(c<0){close(fd[0]);close(fd[1]);return false;} + if(!c){ + if (setpgid(0, 0) != 0) _exit(127); + const char *arguments[] = {path, "--help", nullptr}; + (void)dup2(fd[1],STDOUT_FILENO); + (void)dup2(fd[1],STDERR_FILENO); + close(fd[0]); + close(fd[1]); + execv(path, const_cast(arguments)); + _exit(127); + } + if (setpgid(c, c) != 0 && errno != EACCES && errno != ESRCH) { + close(fd[0]); + close(fd[1]); + (void)terminate_and_reap(c, c); + return false; + } + if (!owns_process_group(c)) { + close(fd[0]); + close(fd[1]); + (void)terminate_and_reap(c, c); + return false; + } + close(fd[1]); + const int flags=fcntl(fd[0],F_GETFL); + if(flags<0||fcntl(fd[0],F_SETFL,flags|O_NONBLOCK)!=0){close(fd[0]);(void)terminate_and_reap(c,c);return false;} + std::vector out; + uint64_t start=0; + bool ok=ms(&start), eof=false, exited=false; + const uint64_t deadline=start+kVersionTimeoutMs; + int s=0; + while (ok && (!exited || !eof)) { + if (!exited && !wait_nonblocking(c, &s, &exited)) { + ok = false; + break; + } + for (;;) { + unsigned char b[512]; + const ssize_t n=read(fd[0],b,sizeof b); + if(n>0){if((size_t)n>kMaxVersion-out.size()){ok=false;break;}out.insert(out.end(),b,b+n);continue;} + if(n==0)eof=true; + else if(errno!=EAGAIN&&errno!=EWOULDBLOCK&&errno!=EINTR)ok=false; + break; + } + if (!ok || (exited && eof)) break; + uint64_t now=0; + if(!ms(&now)||now>=deadline){ok=false;break;} + pollfd x{fd[0],POLLIN|POLLHUP,0}; + const int r=poll(&x,1,(int)std::min(deadline-now,50)); + if(r<0&&errno!=EINTR){ok=false;break;} + } + close(fd[0]); + if(!ok||!exited){(void)terminate_and_reap(c,c);return false;} + const bool process_ok = eof && WIFEXITED(s) && !out.empty(); + if (kill(-c, 0) == 0 || errno != ESRCH) { + if (!terminate_and_reap(c, c)) return false; + } + if (!process_ok) return false; + const std::string output(out.begin(), out.end()); + const bool pinned = output.find("OpenMVS") != std::string::npos && + output.find("v2.4.0") != std::string::npos; + const bool common = output.find("--max-threads") != std::string::npos; + const bool specific = interface_colmap + ? output.find("--image-folder") != std::string::npos + : output.find("--resolution-level") != std::string::npos && + output.find("--min-resolution") != std::string::npos && + output.find("--number-views") != std::string::npos && + output.find("--fusion-mode") != std::string::npos; + static const unsigned char version_record[] = "OpenMVS v2.4.0"; + capabilities->cuda_option = output.find("--cuda-device") != std::string::npos; + return pinned && common && specific && + sha(version_record, sizeof version_record - 1, + capabilities->version_identity); +} +bool finite_cal(const Lardon3DSparseGeometryCalibration &calibration) { + return calibration.width && calibration.height && + std::isfinite(calibration.fx) && std::isfinite(calibration.fy) && + std::isfinite(calibration.cx) && std::isfinite(calibration.cy) && + std::isfinite(calibration.k1) && std::isfinite(calibration.k2) && + std::isfinite(calibration.p1) && std::isfinite(calibration.p2) && + calibration.fx > 0 && calibration.fy > 0; +} +struct ObservationKey { + uint64_t feature_set_id; + uint32_t feature_index; + + bool operator<(const ObservationKey &other) const { + return feature_set_id < other.feature_set_id || + (feature_set_id == other.feature_set_id && + feature_index < other.feature_index); + } +}; + +struct ExportObservation { + const Lardon3DSparseIncrementalLandmarkObservation *snapshot; + const Lardon3DSparseIncrementalObservation *source; + uint32_t point_id; + double x; + double y; +}; + +bool finalize(std::ofstream &file) { + file.flush(); + const bool written = file.good(); + file.close(); + return written && !file.fail(); +} + +bool valid_rotation(const double *r) { + if (!r) return false; + for (int i = 0; i < 9; ++i) if (!std::isfinite(r[i])) return false; + double residual = 0; + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 3; ++col) { + double dot = 0; + for (int k = 0; k < 3; ++k) dot += r[row * 3 + k] * r[col * 3 + k]; + residual = std::max(residual, std::fabs(dot - (row == col ? 1.0 : 0.0))); + } + } + const double det = r[0] * (r[4] * r[8] - r[5] * r[7]) - + r[1] * (r[3] * r[8] - r[5] * r[6]) + + r[2] * (r[3] * r[7] - r[4] * r[6]); + return residual <= 1e-6 && std::fabs(det - 1.0) <= 1e-6; +} +bool quaternion(const double *rotation, double q[4]) { + if (!rotation || !q || !valid_rotation(rotation)) return false; + const double trace = rotation[0] + rotation[4] + rotation[8]; + double scale = 0; + if (trace > 0) { + scale = std::sqrt(trace + 1) * 2; + q[0] = 0.25 * scale; + q[1] = (rotation[7] - rotation[5]) / scale; + q[2] = (rotation[2] - rotation[6]) / scale; + q[3] = (rotation[3] - rotation[1]) / scale; + } else if (rotation[0] > rotation[4] && rotation[0] > rotation[8]) { + scale = std::sqrt(std::max(0.0, 1 + rotation[0] - rotation[4] - rotation[8])) * 2; + q[0] = (rotation[7] - rotation[5]) / scale; + q[1] = 0.25 * scale; + q[2] = (rotation[1] + rotation[3]) / scale; + q[3] = (rotation[2] + rotation[6]) / scale; + } else if (rotation[4] > rotation[8]) { + scale = std::sqrt(std::max(0.0, 1 + rotation[4] - rotation[0] - rotation[8])) * 2; + q[0] = (rotation[2] - rotation[6]) / scale; + q[1] = (rotation[1] + rotation[3]) / scale; + q[2] = 0.25 * scale; + q[3] = (rotation[5] + rotation[7]) / scale; + } else { + scale = std::sqrt(std::max(0.0, 1 + rotation[8] - rotation[0] - rotation[4])) * 2; + q[0] = (rotation[3] - rotation[1]) / scale; + q[1] = (rotation[2] + rotation[6]) / scale; + q[2] = (rotation[5] + rotation[7]) / scale; + q[3] = 0.25 * scale; + } + const double norm = std::sqrt(q[0] * q[0] + q[1] * q[1] + + q[2] * q[2] + q[3] * q[3]); + if (!(norm > 0) || !std::isfinite(norm)) return false; + for (size_t index = 0; index < 4; ++index) q[index] /= norm; + const bool negative = q[0] < 0 || + (q[0] == 0 && + (q[1] < 0 || + (q[1] == 0 && (q[2] < 0 || (q[2] == 0 && q[3] < 0))))); + if (negative) + for (size_t index = 0; index < 4; ++index) q[index] = -q[index]; + return std::all_of(q, q + 4, [](double value) { return std::isfinite(value); }); +} + +bool finite_point(const Lardon3DSparseGeometryPoint3 &point) { + return std::isfinite(point.x) && std::isfinite(point.y) && + std::isfinite(point.z); +} + +/* Two fixed-size CV_8UC3 image buffers cover decode plus undistortion. */ +constexpr uint64_t kMaxImageDimension = 16384; +constexpr uint64_t kMaxImagePixels = 40000000; +constexpr uint64_t kMaxImageWorkingSetBytes = 240000000; + +bool bounded_image_allocation(uint32_t width, uint32_t height) { + if (!width || !height || width > kMaxImageDimension || + height > kMaxImageDimension || width > UINT64_MAX / height) + return false; + const uint64_t pixels = static_cast(width) * height; + constexpr uint64_t bytes_per_pixel_for_two_images = 2U * 3U; + return pixels <= kMaxImagePixels && + pixels <= kMaxImageWorkingSetBytes / bytes_per_pixel_for_two_images; +} + +uint16_t be16(const unsigned char *p) { + return static_cast((static_cast(p[0]) << 8) | + static_cast(p[1])); +} +uint32_t be32(const unsigned char *p) { + return uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | + uint32_t(p[2]) << 8 | p[3]; +} +uint32_t le32(const unsigned char *p) { + return uint32_t(p[0]) | uint32_t(p[1]) << 8 | + uint32_t(p[2]) << 16 | uint32_t(p[3]) << 24; +} + +bool encoded_image_dimensions(const char *path, uint32_t *width, + uint32_t *height) { + if (!path || !width || !height) return false; + const int descriptor = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (descriptor < 0) return false; + struct stat status {}; + if (fstat(descriptor, &status) != 0 || !S_ISREG(status.st_mode)) { + (void)close(descriptor); + return false; + } + unsigned char data[64 * 1024]; + size_t size = 0; + bool read_ok = true; + while (size < sizeof data) { + ssize_t amount; + do { + amount = read(descriptor, data + size, sizeof data - size); + } while (amount < 0 && errno == EINTR); + if (amount < 0) { + read_ok = false; + break; + } + if (amount == 0) break; + size += static_cast(amount); + } + read_ok = close(descriptor) == 0 && read_ok; + if (!read_ok) return false; + if (size >= 24 && !std::memcmp(data, "\x89PNG\r\n\x1a\n", 8) && + !std::memcmp(data + 12, "IHDR", 4)) { + *width = be32(data + 16); *height = be32(data + 20); return true; + } + if (size >= 26 && data[0] == 'B' && data[1] == 'M') { + *width = le32(data + 18); + const int32_t signed_height = static_cast(le32(data + 22)); + if (signed_height == INT32_MIN) return false; + *height = static_cast(std::abs(signed_height)); + return true; + } + if (size >= 4 && data[0] == 0xff && data[1] == 0xd8) { + size_t offset = 2; + while (offset + 4 <= size) { + if (data[offset++] != 0xff) return false; + while (offset < size && data[offset] == 0xff) ++offset; + if (offset >= size) return false; + const unsigned char marker = data[offset++]; + if (marker == 0xd8 || marker == 0xd9) continue; + if (offset + 2 > size) return false; + const uint16_t length = be16(data + offset); + if (length < 2 || offset + length > size) return false; + if ((marker >= 0xc0 && marker <= 0xc3) || + (marker >= 0xc5 && marker <= 0xc7) || + (marker >= 0xc9 && marker <= 0xcb) || + (marker >= 0xcd && marker <= 0xcf)) { + if (length < 7) return false; + *height = be16(data + offset + 3); *width = be16(data + offset + 5); + return true; + } + offset += length; + } + } + return false; +} + +Lardon3DDenseMvsStatus validate_snapshot(const Lardon3DDenseMvsInput &input) { + const auto &snapshot = *input.snapshot; + if (!input.source_observations || + input.source_observation_count != snapshot.observation_count) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + if ((snapshot.camera_count && !snapshot.cameras) || + (snapshot.landmark_count && !snapshot.landmarks) || + (snapshot.observation_count && !snapshot.observations) || + snapshot.camera_count == 0 || snapshot.landmark_count == 0 || + snapshot.observation_count == 0 || + snapshot.camera_count > UINT32_MAX || + snapshot.landmark_count > UINT32_MAX) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + + std::map camera_components; + std::map landmarks; + std::map sources; + std::map observed; + std::set> positions; + for (size_t index = 0; index < snapshot.camera_count; ++index) { + const auto &camera = snapshot.cameras[index]; + if (!camera.image_id || !camera.component_key || + !camera_components.emplace(camera.image_id, camera.component_key).second) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + for (double value : camera.pose_cw.translation_cw) + if (!std::isfinite(value)) return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + if (!valid_rotation(camera.pose_cw.rotation_cw)) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + } + for (size_t index = 0; index < snapshot.landmark_count; ++index) { + const auto &landmark = snapshot.landmarks[index]; + if (!landmark.landmark_id || !landmark.track_id || + landmark.observation_count == 0 || !finite_point(landmark.point) || + !std::isfinite(landmark.reprojection_rmse_px) || + !landmarks.emplace(landmark.landmark_id, &landmark).second) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + } + for (size_t index = 0; index < input.source_observation_count; ++index) { + const auto &observation = input.source_observations[index]; + if (!observation.image_id || !observation.track_id || + !observation.feature_set_id || !std::isfinite(observation.x) || + !std::isfinite(observation.y) || + !sources.emplace(ObservationKey{observation.feature_set_id, + observation.feature_index}, + &observation) + .second) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + for (size_t index = 0; index < snapshot.observation_count; ++index) { + const auto &observation = snapshot.observations[index]; + const auto landmark = landmarks.find(observation.landmark_id); + const auto camera = camera_components.find(observation.image_id); + const auto source = sources.find( + {observation.feature_set_id, observation.feature_index}); + if (landmark == landmarks.end() || camera == camera_components.end() || + observation.track_id != landmark->second->track_id || + camera->second != landmark->second->component_key || + observation.position_in_track >= landmark->second->observation_count || + !positions.emplace(observation.track_id, + observation.position_in_track) + .second) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + if (source == sources.end() || + source->second->image_id != observation.image_id || + source->second->track_id != observation.track_id) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + ++observed[observation.landmark_id]; + } + for (const auto &entry : landmarks) + if (observed[entry.first] != entry.second->observation_count) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + return LARDON3D_DENSE_MVS_OK; +} + +Lardon3DDenseMvsStatus validate_sources(const Lardon3DDenseMvsInput &input) { + const auto &snapshot = *input.snapshot; + if (!input.source_observations || + input.source_observation_count != snapshot.observation_count || + input.source_image_count != snapshot.camera_count) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + std::set camera_ids; + for (size_t index = 0; index < snapshot.camera_count; ++index) + camera_ids.insert(snapshot.cameras[index].image_id); + std::set source_ids; + for (size_t index = 0; index < input.source_image_count; ++index) { + const auto &image = input.source_images[index]; + if (!image.image_id || !image.source_path || !*image.source_path || + !finite_cal(image.calibration) || + !bounded_image_allocation(image.calibration.width, + image.calibration.height) || + !source_ids.insert(image.image_id).second) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + uint32_t encoded_width = 0, encoded_height = 0; + if (!encoded_image_dimensions(image.source_path, &encoded_width, + &encoded_height) || + !bounded_image_allocation(encoded_width, encoded_height) || + encoded_width != image.calibration.width || + encoded_height != image.calibration.height) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + if (source_ids != camera_ids) return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + return LARDON3D_DENSE_MVS_OK; +} + +Lardon3DDenseMvsStatus export_colmap(const Lardon3DDenseMvsInput *in, + const std::string &directory) { + std::vector cameras; + for (size_t i = 0; i < in->snapshot->camera_count; ++i) + cameras.push_back(in->snapshot->cameras + i); + std::sort(cameras.begin(), cameras.end(), [](const auto *a, const auto *b) { + return a->image_id < b->image_id; + }); + if (cameras.size() != in->source_image_count) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + std::vector landmarks; + for (size_t index = 0; index < in->snapshot->landmark_count; ++index) + landmarks.push_back(in->snapshot->landmarks + index); + std::sort(landmarks.begin(), landmarks.end(), [](const auto *a, const auto *b) { + return a->landmark_id < b->landmark_id; + }); + std::map point_ids; + std::map source_coordinates; + for (size_t index = 0; index < in->source_observation_count; ++index) { + const auto &observation = in->source_observations[index]; + source_coordinates.emplace( + ObservationKey{observation.feature_set_id, observation.feature_index}, + &observation); + } + for (size_t index = 0; index < landmarks.size(); ++index) + point_ids.emplace(landmarks[index]->landmark_id, static_cast(index + 1)); + + std::map> image_observations; + for (size_t index = 0; index < in->snapshot->observation_count; ++index) { + const auto *observation = in->snapshot->observations + index; + const auto coordinate = source_coordinates.find( + {observation->feature_set_id, observation->feature_index}); + image_observations[observation->image_id].push_back( + {observation, coordinate->second, point_ids.at(observation->landmark_id), 0, 0}); + } + for (auto &entry : image_observations) + std::sort(entry.second.begin(), entry.second.end(), [](const auto &a, const auto &b) { + if (a.point_id != b.point_id) return a.point_id < b.point_id; + if (a.snapshot->feature_set_id != b.snapshot->feature_set_id) + return a.snapshot->feature_set_id < b.snapshot->feature_set_id; + return a.snapshot->feature_index < b.snapshot->feature_index; + }); + + const std::string sparse_directory = directory + "/sparse"; + const std::string image_directory = directory + "/images"; + if ((mkdir(sparse_directory.c_str(), 0700) != 0 && errno != EEXIST) || + (mkdir(image_directory.c_str(), 0700) != 0 && errno != EEXIST)) + return LARDON3D_DENSE_MVS_IO_ERROR; + + std::ofstream camera_file(sparse_directory + "/cameras.txt", + std::ios::out | std::ios::trunc); + std::ofstream image_file(sparse_directory + "/images.txt", + std::ios::out | std::ios::trunc); + std::ofstream point_file(sparse_directory + "/points3D.txt", + std::ios::out | std::ios::trunc); + if (!camera_file || !image_file || !point_file) + return LARDON3D_DENSE_MVS_IO_ERROR; + for (std::ofstream *file : {&camera_file, &image_file, &point_file}) { + file->imbue(std::locale::classic()); + *file << std::scientific + << std::setprecision(std::numeric_limits::max_digits10 - 1); + } + std::map>> tracks; + uint64_t id = 1; + for (const auto *camera : cameras) { + if (id > 1 && cameras[id - 2]->image_id == camera->image_id) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + const Lardon3DDenseMvsSourceImage *image = nullptr; + for (size_t index = 0; index < in->source_image_count; ++index) + if (in->source_images[index].image_id == camera->image_id) + image = in->source_images + index; + if (!image || !finite_cal(image->calibration)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + double q[4]; + if (!quaternion(camera->pose_cw.rotation_cw, q)) + return LARDON3D_DENSE_MVS_INVALID_SNAPSHOT; + const auto &k = image->calibration; + if (!bounded_image_allocation(k.width, k.height)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + uint32_t encoded_width = 0, encoded_height = 0; + if (!encoded_image_dimensions(image->source_path, &encoded_width, + &encoded_height) || + !bounded_image_allocation(encoded_width, encoded_height) || + encoded_width != k.width || encoded_height != k.height) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + cv::Mat source_image; + try { + source_image = cv::imread(image->source_path, cv::IMREAD_COLOR); + } catch (const cv::Exception &error) { + if (error.code == cv::Error::StsNoMem) throw std::bad_alloc(); + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + if (source_image.empty() || source_image.cols != static_cast(k.width) || + source_image.rows != static_cast(k.height)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + const cv::Mat camera_matrix = 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( + k.k1, k.k2, k.p1, k.p2)); + cv::Mat undistorted; + try { + cv::undistort(source_image, undistorted, camera_matrix, distortion, + camera_matrix); + } catch (const cv::Exception &error) { + if (error.code == cv::Error::StsNoMem) throw std::bad_alloc(); + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + const std::string image_name = "image_" + std::to_string(id) + ".png"; + try { + if (!cv::imwrite(image_directory + "/" + image_name, undistorted)) + return LARDON3D_DENSE_MVS_IO_ERROR; + } catch (const cv::Exception &error) { + if (error.code == cv::Error::StsNoMem) throw std::bad_alloc(); + return LARDON3D_DENSE_MVS_IO_ERROR; + } + + auto &observations = image_observations[camera->image_id]; + std::vector distorted_points; + distorted_points.reserve(observations.size()); + for (const auto &observation : observations) + distorted_points.emplace_back(observation.source->x, observation.source->y); + std::vector undistorted_points; + if (!distorted_points.empty()) { + try { + cv::undistortPoints(distorted_points, undistorted_points, camera_matrix, + distortion, cv::noArray(), camera_matrix); + } catch (const cv::Exception &error) { + if (error.code == cv::Error::StsNoMem) throw std::bad_alloc(); + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + } + for (size_t index = 0; index < observations.size(); ++index) { + observations[index].x = undistorted_points[index].x; + observations[index].y = undistorted_points[index].y; + if (!std::isfinite(observations[index].x) || + !std::isfinite(observations[index].y)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + + /* InterfaceCOLMAP subtracts 0.5 from COLMAP's principal point. */ + camera_file << id << " PINHOLE " << k.width << ' ' << k.height << ' ' + << k.fx << ' ' << k.fy << ' ' << k.cx + 0.5 << ' ' + << k.cy + 0.5 << '\n'; + image_file << id << ' ' << q[0] << ' ' << q[1] << ' ' << q[2] << ' ' << q[3] + << ' ' << camera->pose_cw.translation_cw[0] << ' ' + << camera->pose_cw.translation_cw[1] << ' ' + << camera->pose_cw.translation_cw[2] << ' ' << id << ' ' + << image_name << '\n'; + for (size_t index = 0; index < observations.size(); ++index) { + const auto &observation = observations[index]; + image_file << observation.x << ' ' << observation.y << ' ' + << observation.point_id << ' '; + tracks[observation.point_id].emplace_back(static_cast(id), index); + } + image_file << '\n'; + if (!camera_file || !image_file) return LARDON3D_DENSE_MVS_IO_ERROR; + ++id; + } + + for (const auto *landmark : landmarks) { + const uint32_t point_id = point_ids.at(landmark->landmark_id); + point_file << point_id << ' ' << landmark->point.x << ' ' << landmark->point.y + << ' ' << landmark->point.z << " 0 0 0 " + << landmark->reprojection_rmse_px; + const auto track = tracks.find(point_id); + if (track != tracks.end()) + for (const auto &member : track->second) + point_file << ' ' << member.first << ' ' << member.second; + point_file << '\n'; + if (!point_file) return LARDON3D_DENSE_MVS_IO_ERROR; + } + if (!finalize(camera_file) || !finalize(image_file) || !finalize(point_file)) + return LARDON3D_DENSE_MVS_IO_ERROR; + return LARDON3D_DENSE_MVS_OK; +} + +bool bounded_line(std::ifstream &file, std::string &line, bool *had_newline, + size_t *raw_size) { + line.clear(); + *raw_size = 0; + char value = 0; + while (file.get(value)) { + if (*raw_size == SIZE_MAX) return false; + ++*raw_size; + if (value == '\n') { + *had_newline = true; + if (!line.empty() && line.back() == '\r') line.pop_back(); + return line.find('\r') == std::string::npos; + } + if (line.size() == kMaxPlyLine) return false; + line.push_back(value); + } + *had_newline = false; + return file.eof() && !line.empty() && line.find('\r') == std::string::npos; +} + +bool bounded_header_line(std::ifstream &file, std::string &line, size_t &total) { + bool had_newline = false; + size_t raw_size = 0; + if (!bounded_line(file, line, &had_newline, &raw_size)) return false; + if (total > kMaxPlyHeader || raw_size > kMaxPlyHeader - total) return false; + total += raw_size; + return true; +} +bool ply_data_line(std::ifstream &file, std::string &line) { + bool had_newline = false; + size_t raw_size = 0; + return bounded_line(file, line, &had_newline, &raw_size); +} + +enum class PlyScalar { + I8, U8, I16, U16, I32, U32, F32, F64 +}; + +struct PlyProperty { + std::string name; + PlyScalar scalar{}; + PlyScalar count_scalar{}; + size_t scalar_size = 0; + size_t count_size = 0; + bool list = false; +}; + +bool ply_integer(PlyScalar scalar) { + return scalar != PlyScalar::F32 && scalar != PlyScalar::F64; +} + +bool ply_scalar(const std::string &name, PlyScalar *scalar, size_t *size) { + if (name == "char" || name == "int8") { + *scalar = PlyScalar::I8; + *size = 1; + } else if (name == "uchar" || name == "uint8") { + *scalar = PlyScalar::U8; + *size = 1; + } else if (name == "short" || name == "int16") { + *scalar = PlyScalar::I16; + *size = 2; + } else if (name == "ushort" || name == "uint16") { + *scalar = PlyScalar::U16; + *size = 2; + } else if (name == "int" || name == "int32") { + *scalar = PlyScalar::I32; + *size = 4; + } else if (name == "uint" || name == "uint32") { + *scalar = PlyScalar::U32; + *size = 4; + } else if (name == "float" || name == "float32") { + *scalar = PlyScalar::F32; + *size = 4; + } else if (name == "double" || name == "float64") { + *scalar = PlyScalar::F64; + *size = 8; + } else { + return false; + } + return true; +} + +bool read_little_endian(std::ifstream &file, PlyScalar scalar, double *value) { + size_t size = 0; + PlyScalar ignored{}; + const char *name = scalar == PlyScalar::I8 ? "int8" : + scalar == PlyScalar::U8 ? "uint8" : + scalar == PlyScalar::I16 ? "int16" : + scalar == PlyScalar::U16 ? "uint16" : + scalar == PlyScalar::I32 ? "int32" : + scalar == PlyScalar::U32 ? "uint32" : + scalar == PlyScalar::F32 ? "float32" : "float64"; + if (!ply_scalar(name, &ignored, &size)) return false; + unsigned char bytes[8]{}; + if (!file.read(reinterpret_cast(bytes), static_cast(size))) + return false; + uint64_t bits = 0; + for (size_t index = 0; index < size; ++index) + bits |= static_cast(bytes[index]) << (index * 8U); + switch (scalar) { + case PlyScalar::I8: + *value = static_cast(bytes[0]); + break; + case PlyScalar::U8: + *value = bytes[0]; + break; + case PlyScalar::I16: + *value = static_cast(static_cast(bits)); + break; + case PlyScalar::U16: + *value = static_cast(bits); + break; + case PlyScalar::I32: + *value = static_cast(static_cast(bits)); + break; + case PlyScalar::U32: + *value = static_cast(bits); + break; + case PlyScalar::F32: { + const uint32_t raw = static_cast(bits); + float decoded = 0; + std::memcpy(&decoded, &raw, sizeof decoded); + *value = decoded; + break; + } + case PlyScalar::F64: { + double decoded = 0; + std::memcpy(&decoded, &bits, sizeof decoded); + *value = decoded; + break; + } + } + return true; +} + +bool ply(const std::string &path, uint64_t *point_count) { + struct stat status {}; + if (!point_count || stat(path.c_str(), &status) || !S_ISREG(status.st_mode) || + status.st_size <= 0) return false; + std::ifstream file(path, std::ios::in | std::ios::binary); + std::string line; + size_t header_size = 0; + if (!bounded_header_line(file, line, header_size) || line != "ply" || + !bounded_header_line(file, line, header_size)) return false; + const bool ascii = line == "format ascii 1.0"; + const bool binary_little_endian = line == "format binary_little_endian 1.0"; + if (!ascii && !binary_little_endian) return false; + bool vertex_element = false; + bool header_complete = false; + uint64_t vertices = 0; + std::vector> properties; + while (bounded_header_line(file, line, header_size)) { + if (line == "end_header") { header_complete = true; break; } + std::istringstream fields(line); + fields.imbue(std::locale::classic()); + std::string kind, type, name; + if (!(fields >> kind)) return false; + if (kind == "comment" || kind == "obj_info") continue; + if (kind == "element") { + if (vertex_element || !(fields >> type >> name) || type != "vertex") return false; + char *end = nullptr; + errno = 0; + const unsigned long long parsed = strtoull(name.c_str(), &end, 10); + if (errno || !end || *end || parsed == 0) return false; + vertices = parsed; + vertex_element = true; + continue; + } + if (kind == "property") { + if (!vertex_element || !(fields >> type) || properties.size() >= kMaxPlyProperties) + return false; + if (type == "list") { + std::string count_type, item_type; + if (ascii || !(fields >> count_type >> item_type >> name)) return false; + properties.push_back({type, count_type, item_type, name}); + } else { + if (!(fields >> name)) return false; + properties.push_back({type, name}); + } + continue; + } + return false; + } + int xi = -1, yi = -1, zi = -1; + std::vector decoded_properties; + for (size_t i = 0; i < properties.size(); ++i) { + PlyProperty property; + property.list = properties[i][0] == "list"; + property.name = property.list ? properties[i][3] : properties[i][1]; + const std::string &item_type = property.list ? properties[i][2] : properties[i][0]; + if (!ply_scalar(item_type, &property.scalar, &property.scalar_size)) return false; + if (property.list && + (!ply_scalar(properties[i][1], &property.count_scalar, &property.count_size) || + !ply_integer(property.count_scalar))) + return false; + decoded_properties.push_back(property); + int *index = property.name == "x" ? &xi : property.name == "y" ? &yi : + property.name == "z" ? &zi : nullptr; + if (index) { + if (*index >= 0 || property.list || + (item_type != "float" && item_type != "float32" && item_type != "double" && + item_type != "float64")) + return false; + *index = static_cast(i); + } + } + if (!header_complete || !vertex_element || xi < 0 || yi < 0 || zi < 0 || + properties.empty()) return false; + uint64_t remaining = 0; + if (binary_little_endian) { + const std::streampos body_offset = file.tellg(); + if (body_offset < 0 || static_cast(body_offset) > + static_cast(status.st_size)) + return false; + remaining = static_cast(status.st_size) - static_cast(body_offset); + uint64_t minimum_vertex_size = 0; + for (const PlyProperty &property : decoded_properties) { + const size_t property_minimum = property.list ? property.count_size : property.scalar_size; + if (property_minimum > UINT64_MAX - minimum_vertex_size) return false; + minimum_vertex_size += property_minimum; + } + if (minimum_vertex_size == 0 || vertices > remaining / minimum_vertex_size) return false; + } + for (uint64_t vertex = 0; vertex < vertices; ++vertex) { + if (ascii) { + if (!ply_data_line(file, line)) return false; + std::istringstream fields(line); + fields.imbue(std::locale::classic()); + for (size_t index = 0; index < decoded_properties.size(); ++index) { + double value = 0; + if (!(fields >> value) || !std::isfinite(value)) + return false; + } + std::string extra; + if (fields >> extra) return false; + } else { + for (size_t index = 0; index < decoded_properties.size(); ++index) { + const PlyProperty &property = decoded_properties[index]; + if (property.list) { + double count_value = 0; + if (property.count_size > remaining || + !read_little_endian(file, property.count_scalar, &count_value) || + count_value < 0) + return false; + remaining -= property.count_size; + const uint64_t count = static_cast(count_value); + if (count > remaining / property.scalar_size) return false; + const uint64_t bytes = count * property.scalar_size; + if (bytes > static_cast(std::numeric_limits::max()) || + !file.seekg(static_cast(bytes), std::ios::cur)) + return false; + remaining -= bytes; + continue; + } + double value = 0; + if (property.scalar_size > remaining || + !read_little_endian(file, property.scalar, &value) || + ((static_cast(index) == xi || static_cast(index) == yi || + static_cast(index) == zi) && !std::isfinite(value))) + return false; + remaining -= property.scalar_size; + } + } + } + if (binary_little_endian) + return remaining == 0 && file.peek() == std::char_traits::eof() && + (*point_count = vertices, true); + char trailing; + bool pending_cr = false; + while (file.get(trailing)) { + if (pending_cr) { + if (trailing != '\n') return false; + pending_cr = false; + } else if (trailing == '\r') { + pending_cr = true; + } else if (!std::isspace(static_cast(trailing))) { + return false; + } + } + if (pending_cr) return false; + *point_count = vertices; + return true; +} +Lardon3DDenseMvsStatus run_impl(const Lardon3DDenseMvsInput *input, + Lardon3DDenseMvsResult *result) { + if (!input || !result || !input->snapshot || !input->source_images || + !input->source_image_count || !input->staging_directory || + !input->execution_thread_count || !input->interface_colmap_executable || + !input->densify_point_cloud_executable) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + if (input->parameters.fusion_mode != 0) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + + unsigned char source_identity[32], calibration_binding_identity[32]; + unsigned char parameter_fingerprint[32], backend_identity[32]; + if (!lardon3d_dense_mvs_parameter_fingerprint( + &input->parameters, parameter_fingerprint)) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + if (!lardon3d::dense_mvs_detail::source_image_set_identity( + input->source_images, input->source_image_count, source_identity)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + if (!lardon3d::dense_mvs_detail::calibration_binding_identity( + input->source_images, input->source_image_count, + calibration_binding_identity)) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + + const Lardon3DDenseMvsStatus snapshot_status = validate_snapshot(*input); + if (snapshot_status != LARDON3D_DENSE_MVS_OK) return snapshot_status; + const Lardon3DDenseMvsStatus source_status = validate_sources(*input); + if (source_status != LARDON3D_DENSE_MVS_OK) return source_status; + for (size_t index = 0; index < input->source_image_count; ++index) { + unsigned char actual[32]; + const auto &image = input->source_images[index]; + if (!sha_regular_file(image.source_path, actual, kMaxSourceFileBytes) || + std::memcmp(actual, image.immutable_sha256, sizeof actual) != 0) + return LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } + + if (!executable(input->interface_colmap_executable) || + !executable(input->densify_point_cloud_executable)) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + + BackendCapabilities interface_capabilities{}; + BackendCapabilities densify_capabilities{}; + if (!probe(input->interface_colmap_executable, true, &interface_capabilities) || + !probe(input->densify_point_cloud_executable, false, &densify_capabilities)) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + + Lardon3DDenseMvsBackendManifest manifest{}; + uint64_t remaining_backend_hash_bytes = kMaxBackendFileBytes; + if (!sha_regular_file(input->interface_colmap_executable, + manifest.interface_colmap_binary_sha256, + kMaxBackendFileBytes, &remaining_backend_hash_bytes) || + !sha_regular_file(input->densify_point_cloud_executable, + manifest.densify_point_cloud_binary_sha256, + kMaxBackendFileBytes, &remaining_backend_hash_bytes)) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + std::memcpy(manifest.interface_colmap_version_identity, + interface_capabilities.version_identity, 32); + std::memcpy(manifest.densify_point_cloud_version_identity, + densify_capabilities.version_identity, 32); + if (!lardon3d_dense_mvs_backend_manifest_digest(&manifest, backend_identity)) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + + Lardon3DDenseMvsIdentity identity{}; + std::memcpy(identity.base_reconstruction_identity, + input->base_reconstruction_identity, 32); + std::memcpy(identity.source_image_set_identity, source_identity, 32); + std::memcpy(identity.calibration_scope_identity, + input->calibration_scope_identity, 32); + std::memcpy(identity.calibration_binding_identity, + calibration_binding_identity, 32); + identity.dense_kind = identity.backend_kind = LARDON3D_DENSE_MVS_KIND_OPENMVS; + identity.dense_version = identity.backend_version = LARDON3D_DENSE_MVS_VERSION; + std::memcpy(identity.backend_binary_sha256, backend_identity, 32); + std::memcpy(identity.parameter_fingerprint, parameter_fingerprint, 32); + unsigned char dense_identity[32]; + if (!lardon3d_dense_mvs_identity_digest(&identity, dense_identity)) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + + const std::string staging(input->staging_directory); + constexpr char workspace_suffix[] = "/lardon3d-mvs-XXXXXX"; + constexpr char cloud_suffix[] = "/dense.ply"; + if (staging.size() > SIZE_MAX - sizeof workspace_suffix || + staging.size() + sizeof workspace_suffix - 1 > + SIZE_MAX - sizeof cloud_suffix || + staging.size() + sizeof workspace_suffix - 1 + sizeof cloud_suffix - 1 >= + sizeof result->point_cloud_path) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + if (mkdir(staging.c_str(), 0700) != 0 && errno != EEXIST) + return LARDON3D_DENSE_MVS_IO_ERROR; + std::vector workspace_template(staging.begin(), staging.end()); + workspace_template.insert(workspace_template.end(), workspace_suffix, + workspace_suffix + sizeof workspace_suffix); + char *created = mkdtemp(workspace_template.data()); + if (!created) return LARDON3D_DENSE_MVS_IO_ERROR; + const std::string root(created); + const std::string workspace = root + "/colmap"; + const std::string scene = root + "/scene.mvs"; + const std::string cloud_stem = root + "/dense"; + const std::string cloud = cloud_stem + ".ply"; + if (mkdir(workspace.c_str(), 0700) != 0) + return LARDON3D_DENSE_MVS_IO_ERROR; + const Lardon3DDenseMvsStatus export_status = export_colmap(input, workspace); + if (export_status != LARDON3D_DENSE_MVS_OK) return export_status; + if (!run({input->interface_colmap_executable, "-i", workspace, "-o", scene, + "--max-threads", std::to_string(input->execution_thread_count)}, + root, "interface-colmap.log")) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + std::vector densify_arguments = { + input->densify_point_cloud_executable, + scene, + "-o", + cloud_stem, + "--resolution-level", + std::to_string(input->parameters.resolution_level), + "--min-resolution", + std::to_string(input->parameters.minimum_resolution), + "--number-views", + std::to_string(input->parameters.number_views), + "--fusion-mode", + std::to_string(input->parameters.fusion_mode), + "--max-threads", + std::to_string(input->execution_thread_count), + }; + if (densify_capabilities.cuda_option) { + densify_arguments.emplace_back("--cuda-device"); + densify_arguments.emplace_back("-2"); + } + if (!run(densify_arguments, root, "densify-point-cloud.log")) + return LARDON3D_DENSE_MVS_BACKEND_ERROR; + + if (!ply(cloud, &result->point_count)) return LARDON3D_DENSE_MVS_INVALID_OUTPUT; + std::memcpy(result->dense_identity, dense_identity, 32); + std::memcpy(result->parameter_fingerprint, parameter_fingerprint, 32); + std::memcpy(result->base_reconstruction_identity, + input->base_reconstruction_identity, 32); + std::memcpy(result->source_image_set_identity, source_identity, 32); + std::memcpy(result->backend_implementation_sha256, backend_identity, 32); + const int copied = std::snprintf(result->point_cloud_path, + sizeof result->point_cloud_path, "%s", cloud.c_str()); + if (copied < 0 || static_cast(copied) >= sizeof result->point_cloud_path) + return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + return LARDON3D_DENSE_MVS_OK; +} +} + +extern "C" Lardon3DDenseMvsStatus lardon3d_dense_mvs_run( + const Lardon3DDenseMvsInput *input, Lardon3DDenseMvsResult *result) { + if (!result) return LARDON3D_DENSE_MVS_INVALID_ARGUMENT; + Lardon3DDenseMvsResult candidate{}; + Lardon3DDenseMvsStatus status; + try { + status = run_impl(input, &candidate); + } catch (const std::bad_alloc &) { + status = LARDON3D_DENSE_MVS_OUT_OF_MEMORY; + } catch (const cv::Exception &error) { + status = error.code == cv::Error::StsNoMem + ? LARDON3D_DENSE_MVS_OUT_OF_MEMORY + : LARDON3D_DENSE_MVS_INVALID_SOURCE_IMAGE; + } catch (...) { + status = LARDON3D_DENSE_MVS_IO_ERROR; + } + if (status == LARDON3D_DENSE_MVS_OK) { + candidate.status = status; + *result = candidate; + } else { + std::memset(result, 0, sizeof *result); + result->status = status; + } + return status; +} + +extern "C" void lardon3d_dense_mvs_result_destroy(Lardon3DDenseMvsResult *result) { + if (result) std::memset(result, 0, sizeof *result); +} diff --git a/src/dense_mvs_identity.cpp b/src/dense_mvs_identity.cpp new file mode 100644 index 0000000..92b6af8 --- /dev/null +++ b/src/dense_mvs_identity.cpp @@ -0,0 +1,214 @@ +#include + +#include +#include +#include +#include +#include +#include + +namespace lardon3d::dense_mvs_detail { +bool source_image_set_identity(const Lardon3DDenseMvsSourceImage *images, + size_t count, unsigned char digest[32]); +bool calibration_binding_identity(const Lardon3DDenseMvsSourceImage *images, + size_t count, unsigned char digest[32]); +} // namespace lardon3d::dense_mvs_detail + +namespace { +bool put_u32(unsigned char *out, size_t cap, size_t *at, uint32_t value) { + if (!out || !at || *at > cap - 4) return false; + for (unsigned int i = 0; i < 4; ++i) out[(*at)++] = (unsigned char)(value >> (8U * i)); + return true; +} +bool put_u64(unsigned char *out, size_t cap, size_t *at, uint64_t value) { + if (!out || !at || *at > cap - 8) return false; + for (unsigned int i = 0; i < 8; ++i) out[(*at)++] = (unsigned char)(value >> (8U * i)); + return true; +} +bool put_f64(unsigned char *out, size_t cap, size_t *at, double value) { + if (!std::isfinite(value)) return false; + if (value == 0.0) value = 0.0; + uint64_t bits = 0; + static_assert(sizeof bits == sizeof value); + std::memcpy(&bits, &value, sizeof bits); + return put_u64(out, cap, at, bits); +} +bool sha(const unsigned char *data, size_t size, unsigned char out[32]) { + unsigned int length = 0; + return data && out && EVP_Digest(data, size, out, &length, EVP_sha256(), nullptr) == 1 && length == 32; +} +bool nonzero(const unsigned char value[32]) { + for (size_t i = 0; i < 32; ++i) if (value[i] != 0) return true; + return false; +} +bool manifest_valid(const Lardon3DDenseMvsBackendManifest *manifest) { + return manifest && nonzero(manifest->interface_colmap_version_identity) && + nonzero(manifest->interface_colmap_binary_sha256) && + nonzero(manifest->densify_point_cloud_version_identity) && + nonzero(manifest->densify_point_cloud_binary_sha256); +} +} + +extern "C" bool lardon3d_dense_mvs_parameter_fingerprint_record( + const Lardon3DDenseMvsParameters *parameters, unsigned char record[40]) { + if (!parameters || !record || parameters->number_views == 0) return false; + std::memset(record, 0, 40); std::memcpy(record, "L3DMPRM1", 8); + size_t at = 8; + return put_u32(record, 40, &at, 1) && put_u32(record, 40, &at, 1) && + put_u32(record, 40, &at, parameters->resolution_level) && + put_u32(record, 40, &at, parameters->minimum_resolution) && + put_u32(record, 40, &at, parameters->number_views) && + put_u32(record, 40, &at, parameters->fusion_mode) && + put_u32(record, 40, &at, 1) && /* FIXED_K_OPENCV_UNDISTORT_V1 */ + put_u32(record, 40, &at, 1) && /* OPENMVS_2_4_CPU_ONLY_V1 */ + at == 40; +} +extern "C" bool lardon3d_dense_mvs_parameter_fingerprint( + const Lardon3DDenseMvsParameters *parameters, unsigned char digest[32]) { + unsigned char record[40]; + return digest && + lardon3d_dense_mvs_parameter_fingerprint_record(parameters, record) && + sha(record, sizeof(record), digest); +} +extern "C" bool lardon3d_dense_mvs_backend_manifest_record( + const Lardon3DDenseMvsBackendManifest *manifest, unsigned char record[148]) { + if (!manifest_valid(manifest) || !record) return false; + std::memset(record, 0, 148); + std::memcpy(record, "L3DMBKD1", 8); + size_t at = 8; + if (!put_u32(record, 148, &at, 1) || !put_u32(record, 148, &at, 1)) return false; + std::memcpy(record + at, manifest->interface_colmap_version_identity, 32); at += 32; + std::memcpy(record + at, manifest->interface_colmap_binary_sha256, 32); at += 32; + if (!put_u32(record, 148, &at, 2)) return false; + std::memcpy(record + at, manifest->densify_point_cloud_version_identity, 32); at += 32; + std::memcpy(record + at, manifest->densify_point_cloud_binary_sha256, 32); at += 32; + return at == 148; +} +extern "C" bool lardon3d_dense_mvs_backend_manifest_digest( + const Lardon3DDenseMvsBackendManifest *manifest, unsigned char digest[32]) { + unsigned char record[148]; + return digest && lardon3d_dense_mvs_backend_manifest_record(manifest, record) && + sha(record, sizeof(record), digest); +} +extern "C" bool lardon3d_dense_mvs_identity_record( + const Lardon3DDenseMvsIdentity *id, unsigned char record[220]) { + if (!id || !record || id->dense_kind != 1 || id->dense_version != 1 || + id->backend_kind != 1 || id->backend_version == 0 || !nonzero(id->base_reconstruction_identity) || + !nonzero(id->source_image_set_identity) || !nonzero(id->calibration_scope_identity) || + !nonzero(id->calibration_binding_identity) || + !nonzero(id->backend_binary_sha256) || !nonzero(id->parameter_fingerprint)) return false; + std::memset(record, 0, 220); std::memcpy(record, "L3DMDID2", 8); size_t at = 8; + if (!put_u32(record, 220, &at, 2) || !put_u32(record, 220, &at, id->dense_kind) || + !put_u32(record, 220, &at, id->dense_version) || !put_u32(record, 220, &at, id->backend_kind) || + !put_u32(record, 220, &at, id->backend_version)) return false; + for (const auto *field : {id->base_reconstruction_identity, id->source_image_set_identity, + id->calibration_scope_identity, id->calibration_binding_identity, + id->backend_binary_sha256, + id->parameter_fingerprint}) { std::memcpy(record + at, field, 32); at += 32; } + return at == 220; +} +extern "C" bool lardon3d_dense_mvs_identity_digest( + const Lardon3DDenseMvsIdentity *id, unsigned char digest[32]) { + unsigned char record[220]; + return digest && lardon3d_dense_mvs_identity_record(id, record) && + sha(record, sizeof(record), digest); +} +bool lardon3d::dense_mvs_detail::calibration_binding_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]) { + constexpr size_t kRecordSize = 80; + if (!images || !digest || count == 0 || + count > (SIZE_MAX - 20) / kRecordSize) + return false; + std::vector ordered; + ordered.reserve(count); + for (size_t i = 0; i < count; ++i) { + if (images[i].image_id == 0) return false; + ordered.push_back(&images[i]); + } + std::sort(ordered.begin(), ordered.end(), [](auto a, auto b) { + return a->image_id < b->image_id; + }); + for (size_t i = 1; i < count; ++i) + if (ordered[i - 1]->image_id == ordered[i]->image_id) return false; + std::vector record(20 + count * kRecordSize, 0); + std::memcpy(record.data(), "L3DMCAL1", 8); + size_t at = 8; + if (!put_u32(record.data(), record.size(), &at, 1) || + !put_u64(record.data(), record.size(), &at, count)) + return false; + for (const auto *image : ordered) { + const auto &cal = image->calibration; + if (!put_u64(record.data(), record.size(), &at, image->image_id) || + !put_u32(record.data(), record.size(), &at, cal.width) || + !put_u32(record.data(), record.size(), &at, cal.height) || + !put_f64(record.data(), record.size(), &at, cal.fx) || + !put_f64(record.data(), record.size(), &at, cal.fy) || + !put_f64(record.data(), record.size(), &at, cal.cx) || + !put_f64(record.data(), record.size(), &at, cal.cy) || + !put_f64(record.data(), record.size(), &at, cal.k1) || + !put_f64(record.data(), record.size(), &at, cal.k2) || + !put_f64(record.data(), record.size(), &at, cal.p1) || + !put_f64(record.data(), record.size(), &at, cal.p2)) + return false; + } + return at == record.size() && sha(record.data(), record.size(), digest); +} + +extern "C" bool lardon3d_dense_mvs_calibration_binding_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]) { + try { + return lardon3d::dense_mvs_detail::calibration_binding_identity( + images, count, digest); + } catch (const std::bad_alloc &) { + return false; + } catch (...) { + return false; + } +} +bool lardon3d::dense_mvs_detail::source_image_set_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]) { + if (!images || !digest || count == 0 || count > (SIZE_MAX - 20) / 40) + return false; + std::vector ordered; + ordered.reserve(count); + for (size_t i = 0; i < count; ++i) { + if (images[i].image_id == 0 || !images[i].source_path || + !nonzero(images[i].immutable_sha256)) + return false; + ordered.push_back(&images[i]); + } + std::sort(ordered.begin(), ordered.end(), [](auto a, auto b) { + return a->image_id < b->image_id; + }); + for (size_t i = 1; i < count; ++i) + if (ordered[i - 1]->image_id == ordered[i]->image_id) return false; + std::vector record(20 + count * 40, 0); + std::memcpy(record.data(), "L3DMSRC1", 8); + size_t at = 8; + if (!put_u32(record.data(), record.size(), &at, 1) || + !put_u64(record.data(), record.size(), &at, count)) + return false; + for (const auto *image : ordered) { + if (!put_u64(record.data(), record.size(), &at, image->image_id)) + return false; + std::memcpy(record.data() + at, image->immutable_sha256, 32); + at += 32; + } + return at == record.size() && sha(record.data(), record.size(), digest); +} + +extern "C" bool lardon3d_dense_mvs_source_image_set_identity( + const Lardon3DDenseMvsSourceImage *images, size_t count, + unsigned char digest[32]) { + try { + return lardon3d::dense_mvs_detail::source_image_set_identity( + images, count, digest); + } catch (const std::bad_alloc &) { + return false; + } catch (...) { + return false; + } +} diff --git a/tests/test_dense_mvs.cpp b/tests/test_dense_mvs.cpp new file mode 100644 index 0000000..947396a --- /dev/null +++ b/tests/test_dense_mvs.cpp @@ -0,0 +1,1307 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(i + 1U); + in.calibration_scope_identity[i] = static_cast(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 private_workspaces(const std::string &parent) { + std::vector 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 { +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(i + 1U); + m.interface_colmap_binary_sha256[i] = static_cast(i + 33U); + m.densify_point_cloud_version_identity[i] = + static_cast(i + 65U); + m.densify_point_cloud_binary_sha256[i] = + static_cast(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(r[i]), + static_cast(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 a; + std::string s; + while (getline(z, s)) + a.push_back(s); + std::vector> 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 values((std::istream_iterator(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(x)), {}); + Lardon3DDenseMvsResult rr{}; + CHECK(run(f, rr) == 0); + std::ifstream y(result_workspace(rr) + "/colmap/sparse/images.txt"); + std::string two((std::istreambuf_iterator(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(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((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(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> 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(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::denorm_min(); + f.src.calibration.fy = std::numeric_limits::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(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(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(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( + k.k1, k.k2, k.p1, k.p2)); + std::vector source = { + {f.source_observation.x, f.source_observation.y}}; + std::vector 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(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(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(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(cameras)), {}); + const std::string image_text((std::istreambuf_iterator(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::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; +}