feat: complete MVS-M1 dense reconstruction

This commit is contained in:
fy59 2026-08-26 17:31:59 +02:00
parent 4f8d1af002
commit e5e09ab6c6
8 changed files with 3134 additions and 6 deletions

View file

@ -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 darchitecture.
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
darchitecture.
## Licence

View file

@ -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 B1B6 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.

View file

@ -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

View file

@ -0,0 +1,131 @@
#ifndef LARDON3D_DENSE_MVS_H
#define LARDON3D_DENSE_MVS_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/sparse_sfm_incremental.h>
#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

View file

@ -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: [

1288
src/dense_mvs.cpp Normal file

File diff suppressed because it is too large Load diff

214
src/dense_mvs_identity.cpp Normal file
View file

@ -0,0 +1,214 @@
#include <lardon3d/dense_mvs.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <new>
#include <openssl/evp.h>
#include <vector>
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<const Lardon3DDenseMvsSourceImage *> 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<unsigned char> 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<const Lardon3DDenseMvsSourceImage *> 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<unsigned char> 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;
}
}

1307
tests/test_dense_mvs.cpp Normal file

File diff suppressed because it is too large Load diff