feat(sparse-sfm): implement and freeze Gate F

This commit is contained in:
fy59 2026-08-25 11:56:41 +02:00
parent a67d903a8c
commit 2e0104162c
26 changed files with 2275 additions and 78 deletions

View file

@ -29,9 +29,11 @@ persistante, enrichissable et versionnable.
- **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v16** : résultats géométriques, Track Model v1 et persistance Sparse SfM
- **Project Database v17** : résultats v16 et payload durable typé Sparse SfM Gate F
- **Sparse SfM Gates C/D/E** : géométrie calibrée, noyau incrémental et Bundle
Adjustment final par composante, tous PASS / FROZEN
- **Sparse SfM Gate F** : orchestration durable, runtime gouverné et publication
atomique, PASS / FROZEN
- **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
@ -169,8 +171,8 @@ de petits lots, le Resource Governor interactif et un hot path Vulkan ORB exact
fallback CPU. La feasibility Vulkan SIFT/RootSIFT a été rejetée ; ces deux matchers
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, l'intégration
Governor Gate G, le DAG, le viewer et les étapes denses restent des tickets
implémentés et validés. L'orchestration Sparse SfM Gate F est PASS / FROZEN ;
l'intégration Governor Gate G, 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.

View file

@ -122,7 +122,7 @@ les Feature Sets persistés.
PASS**, the Sparse SfM v16 persistence model is **FROZEN** after Gate B;
Gate C geometry and the synchronous in-memory Gate D incremental core are
**IMPLEMENTED / PASS**. Final per-component Gate E BA is **PASS / FROZEN**;
Gate F project/task orchestration remains **PLANNED**.
Gate F project/task orchestration is **PASS / FROZEN**. Gate G remains planned.
## Extension v2 multi-descriptor

View file

@ -137,6 +137,11 @@ ni persistance, ni Task Runtime, ni Resource Governor.
**Statut :** GATES C/D/E — PASS / FROZEN
Gate F relie ces noyaux au Task Runtime durable, à la Queue/Governor/Reservation
et à la publication Project DB v17 atomique et idempotente.
**Statut :** GATE F — PASS / FROZEN. Gate G reste planifiée.
## Résultats et publication live
Les traitements fonctionnent par séquences adaptatives : lire un lot borné,

View file

@ -1,6 +1,45 @@
# Base de données projet Lardon3D
> Version courante : **v16**. La migration transactionnelle v15→v16 ajoute
## Gate F — décision Project DB v17
**PASS / FROZEN.** La migration historique v15→v16 et le
modèle de reconstruction Sparse SfM v16 restent inchangés. Gate F fait avancer
la tête de schéma à v17 par une migration strictement additive contenant une
seule table métier de tâche : `sparse_sfm_tasks`.
Cette table suit le modèle des autres Task Kinds durables : une ligne par
`task_id`, clé primaire et clé étrangère vers `tasks(task_id)` avec suppression
en cascade. Elle conserve les références immuables `track_set_id` et
`calibration_scope_id`, `sfm_kind`, `sfm_version`, puis les 27 valeurs effectives
de `Lardon3DSparseIncrementalParameters`, y compris les sous-structures
relative-pose, PnP et refinement. Les entiers suivent leurs largeurs C
canoniques ; les flottants finis sont stockés en SQLite `REAL` et doivent
retrouver exactement leurs bits binary64 après fermeture/réouverture.
Le Task Kind et sa version restent dans `tasks` et versionnent l'interprétation
du payload. Le fingerprint F0 est dérivé au rechargement et n'est pas dupliqué.
Le checkpoint générique v1 n'est pas modifié. L'écriture du résumé générique et
du payload typé est une transaction unique ; une ligne absente, incompatible
ou invalide interdit la reconstruction runtime sans appliquer de valeurs par
défaut.
Les quatre champs `uint64_t` de domaine complet — limites observations/Tracks
et seeds déterministes relative-pose/PnP — sont chacun un BLOB de huit octets
little-endian avec `CHECK(length(...)=8)`. L'API exige la classe BLOB et la
longueur exacte au rechargement. Les métriques globales persistées restent les
diagnostics Gate F calculés sur toutes les observations retenues du résultat
Gate E final ; elles ne participent pas à l'identité.
La publication Gate F est la projection durable du résultat scientifique
complet : une composante est persistée si et seulement si ses nombres d'images
enregistrées et de landmarks sont tous deux strictement positifs. Une
composante dont le BA est rejeté mais dont la géométrie Gate D reste valide est
persistée exactement ; seule une composante de graphe non reconstruite est
omise. Les comptes et métriques globaux décrivent exclusivement la géométrie
effectivement persistée. Aucun placeholder ni table diagnostique n'est ajouté.
> Version courante : **v17**. La migration transactionnelle v16→v17 ajoute le
> payload durable typé `sparse_sfm_tasks`. La migration transactionnelle v15→v16 ajoute
> le modèle persistant Sparse SfM (calibrations, scopes et reconstructions).
> La migration transactionnelle v14→v15 ajoute
> `track_builder_tasks` pour le payload durable explicite du Task Builder. La

View file

@ -127,7 +127,7 @@ implémenté dans Project DB v15 (`track_sets`, `tracks`, `track_observations` e
le payload de tâche). Les primitives de géométrie calibrée Gate C sont
implémentées et le noyau Sparse SfM incrémental Gate D est IMPLEMENTED / PASS ;
le Bundle Adjustment final Gate E est PASS / FROZEN ; l'orchestration projet
et tâche Gate F reste PLANNED.
et tâche Gate F est PASS / FROZEN.
Le modèle de persistance Sparse SfM v16 est gelé après Gate B.
**Sparse SfM Gate A : PASS.** Le contrat géométrique, la stratégie
@ -136,8 +136,9 @@ les limites BA et l'enveloppe matérielle sont documentés dans
`architecture/sparse_sfm.md`. Ses primitives pures calibrées Gate C sont
**IMPLEMENTED / PASS** et son noyau incrémental synchrone en mémoire Gate D est
**IMPLEMENTED / PASS**. Le Bundle Adjustment final par composante Gate E est
**PASS / FROZEN**. L'orchestration Gate F reste **PLANNED**, tandis que la
persistance v16 et ses lecteurs bornés restent ceux de B2.
**PASS / FROZEN**. L'orchestration Gate F est **PASS / FROZEN**. Le modèle de
reconstruction v16 et ses lecteurs bornés restent ceux de B2 ; le payload de
tâche Gate F est l'ajout v17.
---
@ -258,8 +259,9 @@ Import, Image Catalog, Feature Extraction, Feature Store, Visual Index,
Candidate Pair, Matching v1, Geometric Verification, Track Model/Builder v1
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,
Gate G Governor integration, MVS, mesh, texturing and viewer remain **PLANNED**.
per-component Gate E BA is **PASS / FROZEN**. Gate F orchestration is
**PASS / FROZEN**. Gate G Governor integration, MVS, mesh,
texturing and viewer 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

@ -4,8 +4,9 @@
**ACCEPTED** — architecture decision for the post-Gate C documentation freeze.
Current gates: Gate A **PASS**, Gate B **PASS / FROZEN**, Gate C **PASS**.
Project Database: **v16**.
Current Sparse SfM gates A through F are **PASS / FROZEN**. Gate G remains
open/planned.
Project Database: current schema **v17**; historical v16 remains frozen.
This record is normative for the current architecture. It does not introduce
an implementation, a public API, a persistence format or a roadmap commitment.
@ -94,15 +95,47 @@ Tasks create estimates, request reservations, execute bounded work and release
reservations. The Governor owns only policy, budgets, pressure, lots and
reservations.
## Project DB v16 boundary
### Task demand declaration versus admission policy
Project DB v16 remains unchanged and frozen. It already stores the current
logical IDs, content hashes, sizes, states, relations and artifact paths needed
by implemented stores. No resource table, handle table, cache table,
dependency table, schema field or migration is required by this decision.
**FROZEN.** A task producer owns the immutable declaration of the workload it
submits through the existing `Lardon3DResourceEstimate` contract. Constructing
that estimate from immutable payload, input shape and known implementation
characteristics describes what the task expects to require; it is not a
Governor policy decision.
No Project DB v17, bundle redesign, Track Model change, Track Builder change or
persistence API redesign is part of this scope.
The existing Task Runtime, queue and Governor remain responsible for deciding
whether and when the declared work is admitted and for creating the mandatory
reservation. Admission thresholds, current machine state, pressure, telemetry,
adaptive tuning, swap or scratch policy and future estimate calibration remain
outside the task producer. A producer never changes scientific inputs or
identity in response to resources.
Gate F may therefore construct the immutable Sparse SfM task estimate required
by `lardon3d_task_create_typed()` and submit the task through the normal queue.
It does not inspect resource snapshots, decide admission, create reservations,
change Governor or queue policy, or bypass the reservation invariant. The
estimate is operational metadata and is excluded from the Sparse SfM parameter
fingerprint, candidate identity and scientific determinism. Future Gate G owns
resource-management policy and operational refinement of that estimate.
Gate F v1 freezes its declarative Sparse SfM RAM request as the checked sum
`128 MiB + I*64 KiB + T*2048 + O*512`, rounded upward to one MiB, where `I`,
`T` and `O` are immutable participating-image, Track and observation counts.
The complete amount is fixed RAM for one atomic batch; per-item RAM and all GPU
fields are zero, batch bounds are one, and the task requests one CPU thread and
one IO slot in the CPU class. These coefficients are conservative operational
policy inputs, not measured dynamically or included in scientific identity.
## Project DB boundary
The historical Project DB v16 migration and Sparse SfM reconstruction model
remain unchanged and frozen. Gate F is authorized to advance the current schema
head to v17 only with the dedicated `sparse_sfm_tasks` typed-payload table; that
task persistence does not add a resource table, handle table, cache table,
dependency table or resource-policy field.
No further schema expansion, bundle redesign, Track Model change, Track Builder
change or persistence API redesign is part of this resource-boundary scope.
## Options considered
@ -164,7 +197,7 @@ resolver or Governor asset integration as part of the current architecture.
- Generic dependency graph or cycle management.
- Streaming or asynchronous resource loading.
- CPU/GPU dual-residency manager.
- Project DB v17 or schema migration.
- Project DB schema beyond the additive Gate F v17 typed-task migration.
- Bundle redesign.
- Governor integration with persistent asset identity.
- BA, new orchestration, Task Runtime redesign or renderer redesign.
@ -178,8 +211,9 @@ contracts remain authoritative in their own domains.
## Persistence impact
None. Project DB remains v16. Existing IDs, hashes, metadata and relative paths
are not renamed, generalized or duplicated.
The historical Project DB v16 migration remains unchanged. Gate F adds only the
v17 typed-task payload already described above. Existing IDs, hashes, metadata
and relative paths are not renamed, generalized or duplicated.
## Memory / resource impact
@ -239,7 +273,8 @@ These are trigger conditions, not current requirements or roadmap commitments.
- No Track Model or Track Builder contract is changed.
- No bundle or migration is introduced.
- Governor ownership remains limited to execution budgets.
- Project DB remains v16.
- At the Gate B freeze, Project DB remained v16; the later additive Gate F v17
migration does not alter that historical proof.
Therefore Gate B remains **PASS / FROZEN**.

View file

@ -198,7 +198,7 @@ as a whole, so Track identity and observation ownership remain simple.
Bundle Adjustment implementation, E01--E35 matrix, normal suite, targeted
ASan/UBSan with LeakSanitizer, full sequential ASan/UBSan suite and at least
20 fresh-process E27 comparisons are validated. Gate F project orchestration
and Gate G resource integration remain later gates.
is now **PASS / FROZEN**; Gate G resource integration remains a later gate.
**DECISION: Gate E v1 is a synchronous, independent final per-component Bundle
Adjustment applied as post-processing to a copy of the immutable final Gate D
@ -779,6 +779,185 @@ solver until measured. The v1 target is deterministic ordering and numerical
reproducibility within documented tolerances; single-threaded reductions are
the initial reference.
### Gate F publication policy
**FROZEN.** Gate F keeps the Gate E execution domain separate from the Gate E
scientific result domain. `EXECUTION_OK` is the necessary and sufficient Gate E
condition for publication eligibility. Under that execution status,
`COMPLETE`, `PARTIAL` and `FAILED` are all valid complete in-memory Gate E
results and are published atomically. A successful Project DB publication or
exact-identity reuse makes the Task Runtime execution successful for all three
scientific statuses.
| Gate E execution | Scientific status | Publish | Runtime after DB success |
|---|---|---|---|
| `EXECUTION_OK` | `COMPLETE` | exact Gate E result | success |
| `EXECUTION_OK` | `PARTIAL` | exact Gate E result | success |
| `EXECUTION_OK` | `FAILED` | exact Gate E result | success |
| `EXECUTION_INVALID_ARGUMENT` | none | no publication | failure |
| `EXECUTION_OUT_OF_MEMORY` | none | no publication | failure |
| `EXECUTION_INTERNAL_ERROR` | none | no publication | failure |
Scientific `PARTIAL` describes a complete result in which accepted components
contain validated optimized values and rejected components preserve their exact
Gate D values. It never permits partial database visibility. Scientific
`FAILED` means that no eligible component accepted Bundle Adjustment; under
`EXECUTION_OK` it remains a valid complete Gate E result whose components
preserve the Gate D values selected by Gate E.
Gate F never publishes Gate D as a fallback after a Gate E execution error. It
does not rerun Gate D or Gate E, retry automatically, reinterpret scientific
status as a Task Runtime state, or require scientific `COMPLETE` for runtime
success. A Project DB publication failure rolls back and fails the runtime
execution. The scientific status is output diagnostic metadata, not part of
the candidate identity or parameter fingerprint. Gate F uses an existing
status carrier when one exists; the absence of a dedicated Project DB v16
column does not require a migration or authorize reuse of an unrelated column.
### Gate F resource-demand boundary
**FROZEN.** Gate F materializes the immutable `Lardon3DResourceEstimate` needed
to describe its task to the existing Task Runtime. The estimate is a pure
function of durable task input shape and known implementation characteristics;
it never depends on current RAM, swap, PSI, load, queue depth, task attempt or
Governor state. Gate F submits through the existing queue and does not perform
admission or reservation itself.
The existing Governor owns admission and reservation policy. Future Gate G owns
telemetry, adaptive tuning, pressure and swap/scratch policy, scheduling policy
and estimate refinement. The estimate never enters the 372-byte parameter
record, candidate identity, `sfm_version` or scientific decisions, and cannot
change Gate D or Gate E parameters. This clarification preserves
`NO_NEW_SUBSYSTEM` and the mandatory reservation invariant.
### Gate F durable task payload
**FROZEN.** Gate F advances the current Project Database schema head from v16
to v17 with one strictly additive `sparse_sfm_tasks` table. The historical v16
migration and its immutable reconstruction model remain unchanged. The new
table follows the existing one-to-one typed-task pattern: its primary key is a
foreign key to `tasks(task_id)` with cascade cleanup, and creation records the
generic task snapshot and typed payload in one transaction.
The payload stores the immutable Track Set reference, calibration-scope
reference, Sparse SfM kind and version, and every one of the 27 effective
scalars in `Lardon3DSparseIncrementalParameters`, using their existing fixed
integer widths and exact finite SQLite binary64 values. The Task Kind version
selects the payload interpretation. Reload never reapplies defaults and rejects
a missing row, incompatible kind/version, invalid identity or invalid
parameter. The generic checkpoint codec remains v1 and unchanged.
The parameter fingerprint is not stored in `sparse_sfm_tasks`. Reconstruction
loads and validates the effective fields, rebuilds the unchanged 372-byte F0
record and recomputes SHA-256. Calibration values, Track observations,
ResourceEstimate and runtime metadata are not duplicated in the typed payload.
This is an additive Project DB/typed-Task extension, not a generic payload or
persistence subsystem.
### Gate F closure decisions
**FROZEN.** The v1 declarative estimate uses the immutable pre-admission counts
`I` (distinct participating images), `T` (Tracks) and `O` (Track observations):
```text
raw = 134217728 + I*65536 + T*2048 + O*512
memory_fixed_bytes = raw rounded upward to a whole MiB
```
Every operation is checked `uint64_t` arithmetic; overflow rejects task
creation before persistence. The remaining estimate is RAM-per-item 0, all GPU
fields 0, minimum and maximum batch 1, one CPU thread, one IO slot and CPU task
class. These conservative coefficients cover Gate D containers, adapters,
camera/landmark state, Gate E copies, ordering/residual storage and Ceres
working storage. They are operational, machine-state independent and excluded
from scientific identity. A restored task uses its persisted generic estimate.
The four full-domain `uint64_t` payload values (`maximum_observations`,
`maximum_tracks` and both deterministic seeds) are individual exact eight-byte
little-endian SQL BLOBs. Any other storage class or length is corrupt; there is
no signed cast, text, REAL conversion or domain restriction. Their F0 encoding
remains unchanged.
Gate D `COMPLETE` and usable `PARTIAL` results proceed to Gate E. Gate D
scientific `FAILED` fails the Task, invokes no Gate E and publishes nothing.
Gate D invalid input or allocation failure likewise fails execution. A claimed
usable result that violates frozen structure is an internal integration
failure. Once Gate E is legitimately reached, every `EXECUTION_OK` scientific
status remains publication-eligible.
Gate F computes publication diagnostics from every retained observation of the
exact final Gate E result. With source-pixel residual `dx,dy`, each observation
contributes `s=dx*dx+dy*dy` and `e=sqrt(s)`. Global RMSE is
`sqrt(sum(s)/N)`. Global median is the middle sorted `e`, or for even `N`,
`lower + (upper-lower)/2`. Projection is the frozen Gate E distorted pinhole
model, binary64, with strict minimum depth. Empty, non-finite or invalid
projection input fails publication; no observation is skipped or clamped.
Metrics are deterministic result diagnostics, never candidate identity.
### Gate F durable reconstruction projection
**FROZEN.** Gate F projects the complete Gate D/E scientific result onto the
existing Project DB reconstruction model. A component belongs to the durable
projection exactly when `registered_image_count > 0` and `landmark_count > 0`.
Every such component is persisted with its exact final Gate E geometry. This
includes a BA-rejected component whose valid Gate D geometry Gate E preserved.
A non-reconstructed graph component failing either predicate remains an
ephemeral scientific/orchestration diagnostic and has no Project DB component
row. Gate F fabricates no camera, landmark or placeholder and does not fail an
otherwise publishable reconstruction merely because such diagnostics exist.
The projected result must still satisfy every top-level Project DB invariant;
otherwise publication does not occur and the Task fails.
Persisted component and geometry counts describe only this durable projection.
Global reprojection metrics likewise include exactly the retained observations
belonging to persisted geometry. Omission neither renumbers scientific
component keys nor changes candidate identity. No persistent diagnostic table,
sidecar, metadata blob or schema beyond v17 `sparse_sfm_tasks` is introduced.
Gate F v1 is **PASS / FROZEN**. Gate D and Gate E remain **PASS / FROZEN**;
Gate G remains open and planned.
### Gate F validation closure
**PASS / FROZEN.** Gate F freezes the F0 372-byte parameter record v1 and its
SHA-256 digest, Project DB v17 typed payload, `sparse_sfm.run` version 1,
pre-admission declarative estimate, governed Task Runtime execution, durable
replay, deterministic D→E orchestration, exact candidate reuse and atomic
reconstruction publication. Canonical gate progression:
```text
Gate A — PASS / FROZEN
Gate B — PASS / FROZEN
Gate C — PASS / FROZEN
Gate D — PASS / FROZEN
Gate E — PASS / FROZEN
Gate F — PASS / FROZEN
Gate G — OPEN / PLANNED
```
The five candidate-identity dimensions remain separate:
```text
(
input_track_set_identity,
calibration_scope_identity,
sfm_kind,
sfm_version,
parameter_fingerprint
)
```
The F0 golden SHA-256 digest remains
`e1c83e5b2036e49254a9426ddbace42b7831373bc896f27abdd2f61e302f9e8c`.
Final validation completed with the normal suite at 41/41, targeted Gate F
ASan/UBSan/LeakSanitizer at 4/4 with leak detection enabled, and the full
sequential ASan/UBSan suite at 41/41. Fresh-process validation passed 20/20 for
the Gate F contract and 20/20 for the production `sparse_sfm.run` task. The C17
public-header probe and `git diff --check` passed. Final human diff review
passed; no Gate F implementation work, validation work or human decision
remains.
## Future persistence and API candidates
No Project DB v16 is created in Gate A. A later model gate may define immutable
@ -831,21 +1010,21 @@ reported separately. Thread probes are limited to 1/2/4/8 threads and stop if
MemAvailable, swap, PSI or desktop responsiveness becomes unhealthy. No
production Sparse SfM code is created by this gate.
## Future gate plan
## Gate decomposition
- **Gate B — Sparse Reconstruction Model:** immutable in-memory model, result
- **Gate B — PASS / FROZEN — Sparse Reconstruction Model:** immutable in-memory model, result
states, calibration ownership and candidate persistence contract; no DB v16
until this contract is reviewed.
- **Gate C — Geometry primitives:** normalized camera model, relative pose,
- **Gate C — PASS / FROZEN — Geometry primitives:** normalized camera model, relative pose,
deterministic seed, triangulation and PnP with synthetic ground truth.
- **Gate D — Incremental core:** registration ordering, components,
- **Gate D — PASS / FROZEN — Incremental core:** registration ordering, components,
unregistered-image policy and deterministic reconstruction output.
- **Gate E — Final Bundle Adjustment:** synchronous final per-component BA on a
- **Gate E — PASS / FROZEN — Final Bundle Adjustment:** synchronous final per-component BA on a
copy of the immutable Gate D result, with its scientific and numerical
contract frozen here; interleaved local BA is deferred.
- **Gate F — Project orchestration:** explicit Track Set/calibration input,
- **Gate F — PASS / FROZEN — Project orchestration:** explicit Track Set/calibration input,
atomic publication and durable runtime integration.
- **Gate G — Resource/freeze:** Governor admission, sustained hardware safety,
- **Gate G — OPEN / PLANNED — Resource/freeze:** Governor admission, sustained hardware safety,
recovery, full validation and final freeze.
## Algorithm comparison and Gate A evidence

View file

@ -79,3 +79,8 @@ générique v1 ne possède aucun payload propre au kind.
depuis son payload Project DB v15 et son asset little-endian validé. Le callback
réutilise l'orchestration Gate C et le reconstructeur refuse toute corruption,
version, fingerprint, checksum, tri, unicité ou L3DTSIS1 incohérents.
**PASS / FROZEN** — `sparse_sfm.run`, version 1, recharge le
payload scientifique explicite Project DB v17, restaure l'estimation générique
persistée et rejoue D puis E depuis les références Track Set/calibration. Le
fingerprint F0 est recalculé ; le checkpoint générique v1 reste inchangé.

View file

@ -53,6 +53,7 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
- ✅ Sparse SfM : primitives géométriques Gate C et noyau incrémental Gate D
implémentés
- ✅ Sparse SfM Gate E : Bundle Adjustment final par composante PASS / FROZEN
- ✅ Sparse SfM Gate F : orchestration durable et publication atomique PASS / FROZEN
### Phase 5 : Reconstruction (PLANNED)
- 📋 Orchestration de reconstruction incrémentale

View file

@ -7,9 +7,10 @@
#include <time.h>
#include <lardon3d/task.h>
#include <lardon3d/sparse_sfm_incremental.h>
enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 16,
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 17,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -391,6 +392,15 @@ typedef struct {
int64_t created_at;
} Lardon3DProjectDbTrackSet;
typedef struct {
uint64_t task_id;
uint64_t track_set_id;
uint64_t calibration_scope_id;
uint32_t sfm_kind;
uint32_t sfm_version;
Lardon3DSparseIncrementalParameters parameters;
} Lardon3DProjectDbSparseSfmTask;
void lardon3d_project_db_free_track(Lardon3DProjectDbTrack *track);
Lardon3DProjectDbResult lardon3d_project_db_create_track_set(
Lardon3DProjectDb *database, const Lardon3DProjectDbTrackSet *configuration,
@ -602,6 +612,14 @@ Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task(
Lardon3DProjectDbResult lardon3d_project_db_load_track_builder_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbTrackBuilderTask *parameters);
Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at);
Lardon3DProjectDbResult lardon3d_project_db_load_sparse_sfm_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbSparseSfmTask *parameters);
Lardon3DProjectDbResult lardon3d_project_db_create_match_result(
Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a,

View file

@ -0,0 +1,38 @@
#ifndef LARDON3D_SPARSE_SFM_TASK_H
#define LARDON3D_SPARSE_SFM_TASK_H
#include <stdbool.h>
#include <stdint.h>
#include <lardon3d/app_state.h>
#include <lardon3d/sparse_sfm_incremental.h>
#include <lardon3d/task_kind_registry.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LARDON3D_SPARSE_SFM_TASK_KIND "sparse_sfm.run"
enum { LARDON3D_SPARSE_SFM_TASK_KIND_VERSION = 1 };
typedef struct {
uint64_t track_set_id;
uint64_t calibration_scope_id;
Lardon3DSparseIncrementalParameters parameters;
} Lardon3DSparseSfmTaskConfiguration;
Lardon3DTask *lardon3d_project_create_sparse_sfm_task(
Lardon3DAppState *state, const Lardon3DSparseSfmTaskConfiguration *configuration,
uint64_t *task_id);
bool lardon3d_project_enqueue_sparse_sfm_task(
Lardon3DAppState *state, const Lardon3DSparseSfmTaskConfiguration *configuration,
uint64_t *task_id);
bool lardon3d_sparse_sfm_task_reconstruct(const Lardon3DTaskDurableSnapshot *snapshot,
void *context, Lardon3DTaskKindBinding *binding);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -153,6 +153,8 @@ executable(
'src/sparse_sfm_geometry.cpp',
'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_bundle_adjustment.cpp',
'src/sparse_sfm_gate_f.cpp',
'src/sparse_sfm_task.cpp',
'src/task.c',
'src/task_checkpoint.c',
'src/task_kind_registry.c',
@ -169,7 +171,10 @@ executable(
'src/track_builder_task.cpp',
] + matcher_backend_sources,
include_directories: include_directories('include'),
c_args: ['-DLARDON3D_TRACK_BUILDER_TASK_AVAILABLE'],
c_args: [
'-DLARDON3D_TRACK_BUILDER_TASK_AVAILABLE',
'-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE',
],
dependencies: [ncursesw, threads, sqlite3, openssl, opencv, opencv_geometry, ceres]
+ matcher_backend_dependencies,
)
@ -918,6 +923,59 @@ geometric_verifier_task_test = executable(
test('geometric-verifier-task', geometric_verifier_task_test, timeout: 60,
env: opencv_test_environment)
sparse_sfm_gate_f_test = executable(
'test-sparse-sfm-gate-f',
sources: [
'tests/test_sparse_sfm_gate_f.cpp',
'src/sparse_sfm_gate_f.cpp',
'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_geometry.cpp',
],
include_directories: include_directories('include'),
dependencies: [openssl, opencv_geometry],
)
test('sparse-sfm-gate-f', sparse_sfm_gate_f_test, timeout: 60)
sparse_sfm_task_test = executable(
'test-sparse-sfm-task',
sources: [
'tests/test_sparse_sfm_task.cpp',
'src/sparse_sfm_task.cpp',
'src/sparse_sfm_gate_f.cpp',
'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_geometry.cpp',
'src/sparse_sfm_bundle_adjustment.cpp',
'src/project_db.c', 'src/project_db_sparse_sfm.c',
'src/feature_store.c', 'src/feature_extractor_opencv.cpp',
'src/app_state.c',
'src/task.c', 'src/task_checkpoint.c', 'src/task_kind_registry.c',
'src/task_queue.c',
'src/resource_governor.c', 'src/resource_snapshot.c',
],
cpp_args: ['-DLARDON3D_SPARSE_SFM_TASK_TESTING'],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, opencv_geometry, ceres],
)
test('sparse-sfm-task', sparse_sfm_task_test, timeout: 120)
sparse_sfm_payload_test = executable(
'test-sparse-sfm-payload',
sources: [
'tests/test_sparse_sfm_payload.cpp',
'src/project_db.c',
'src/project_db_sparse_sfm.c',
'src/task.c',
'src/task_kind_registry.c',
'src/resource_governor.c',
'src/resource_snapshot.c',
'src/sparse_sfm_incremental.cpp',
'src/sparse_sfm_geometry.cpp',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv_geometry],
)
test('sparse-sfm-payload', sparse_sfm_payload_test, timeout: 60)
track_builder_test = executable(
'test-track-builder-core',
sources: ['tests/test_track_builder_core.cpp', 'src/track_builder.cpp'],

View file

@ -423,6 +423,30 @@ static const char schema_sparse_sfm_v16_b[] =
"CREATE INDEX sparse_landmark_observations_page_idx ON "
"sparse_landmark_observations(landmark_id,position_in_track);";
static const char schema_sparse_sfm_task_v17[] =
"CREATE TABLE sparse_sfm_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"track_set_id INTEGER NOT NULL REFERENCES track_sets(track_set_id),"
"calibration_scope_id INTEGER NOT NULL REFERENCES sparse_calibration_scopes(scope_id),"
"sfm_kind INTEGER NOT NULL CHECK(sfm_kind=1),"
"sfm_version INTEGER NOT NULL CHECK(sfm_version=1),"
"minimum_seed_tracks INTEGER NOT NULL,minimum_seed_landmarks INTEGER NOT NULL,"
"minimum_pnp_correspondences INTEGER NOT NULL,maximum_seed_candidates INTEGER NOT NULL,"
"maximum_registration_rounds INTEGER NOT NULL,maximum_landmarks_per_round INTEGER NOT NULL,"
"maximum_images INTEGER NOT NULL,maximum_observations BLOB NOT NULL "
"CHECK(length(maximum_observations)=8),maximum_tracks BLOB NOT NULL "
"CHECK(length(maximum_tracks)=8),reprojection_threshold_px REAL NOT NULL,"
"minimum_track_parallax_rad REAL NOT NULL,relative_robust_threshold_px REAL NOT NULL,"
"relative_confidence REAL NOT NULL,relative_max_iterations INTEGER NOT NULL,"
"relative_minimum_inliers INTEGER NOT NULL,relative_minimum_inlier_ratio REAL NOT NULL,"
"relative_minimum_parallax_rad REAL NOT NULL,relative_minimum_cheirality_ratio REAL NOT NULL,"
"relative_deterministic_seed BLOB NOT NULL CHECK(length(relative_deterministic_seed)=8),"
"pnp_reprojection_threshold_px REAL NOT NULL,pnp_confidence REAL NOT NULL,"
"pnp_max_iterations INTEGER NOT NULL,pnp_minimum_inliers INTEGER NOT NULL,"
"pnp_minimum_inlier_ratio REAL NOT NULL,pnp_deterministic_seed BLOB NOT NULL "
"CHECK(length(pnp_deterministic_seed)=8),refinement_max_iterations INTEGER NOT NULL,"
"refinement_convergence_tolerance REAL NOT NULL);";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
@ -839,6 +863,23 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v16 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK && from_version < 17) {
result = execute(database, schema_sparse_sfm_task_v17,
"migrate schema v16 to v17");
#ifdef LARDON3D_PROJECT_DB_TESTING
if (result == LARDON3D_PROJECT_DB_OK &&
getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V17")) {
result = execute(database, "INSERT INTO missing_v17_test_table VALUES(1)",
"forced migration v17 failure");
}
#endif
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(
database,
"UPDATE metadata SET value=17 WHERE key='schema_version' AND value=16",
"finish schema v17 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration");
}
@ -1150,6 +1191,36 @@ static bool valid_durable_task(const Lardon3DTaskDurableSnapshot *snapshot, int6
updated_at >= 0;
}
static bool valid_sparse_sfm_parameters(const Lardon3DSparseIncrementalParameters *p) {
return p && p->minimum_seed_tracks >= 2 && p->minimum_seed_landmarks > 0 &&
p->minimum_pnp_correspondences >= 4 && p->maximum_seed_candidates > 0 &&
p->maximum_registration_rounds > 0 && p->maximum_landmarks_per_round > 0 &&
p->maximum_images >= 2 && p->maximum_observations >= p->maximum_tracks &&
isfinite(p->reprojection_threshold_px) && p->reprojection_threshold_px > 0.0 &&
isfinite(p->minimum_track_parallax_rad) && p->minimum_track_parallax_rad >= 0.0 &&
isfinite(p->relative_pose.robust_threshold_px) &&
p->relative_pose.robust_threshold_px > 0.0 && isfinite(p->relative_pose.confidence) &&
p->relative_pose.confidence > 0.0 && p->relative_pose.confidence < 1.0 &&
p->relative_pose.max_iterations > 0 &&
p->relative_pose.minimum_inliers >= p->minimum_seed_tracks &&
isfinite(p->relative_pose.minimum_inlier_ratio) &&
p->relative_pose.minimum_inlier_ratio > 0.0 &&
p->relative_pose.minimum_inlier_ratio <= 1.0 &&
isfinite(p->relative_pose.minimum_parallax_rad) &&
p->relative_pose.minimum_parallax_rad >= 0.0 &&
isfinite(p->relative_pose.minimum_cheirality_ratio) &&
p->relative_pose.minimum_cheirality_ratio > 0.0 &&
p->relative_pose.minimum_cheirality_ratio <= 1.0 &&
isfinite(p->pnp.reprojection_threshold_px) && p->pnp.reprojection_threshold_px > 0.0 &&
isfinite(p->pnp.confidence) && p->pnp.confidence > 0.0 && p->pnp.confidence < 1.0 &&
p->pnp.max_iterations > 0 &&
p->pnp.minimum_inliers >= p->minimum_pnp_correspondences &&
isfinite(p->pnp.minimum_inlier_ratio) && p->pnp.minimum_inlier_ratio > 0.0 &&
p->pnp.minimum_inlier_ratio <= 1.0 && p->refinement.max_iterations > 0 &&
isfinite(p->refinement.convergence_tolerance) &&
p->refinement.convergence_tolerance > 0.0;
}
static Lardon3DProjectDbResult
record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
@ -1160,6 +1231,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_pair,
const Lardon3DProjectDbMatcherTask *matcher,
const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier,
const Lardon3DProjectDbSparseSfmTask *sparse_sfm,
int64_t updated_at) {
bool typed = task_kind != NULL;
if (!database || !valid_durable_task(snapshot, updated_at) ||
@ -1220,6 +1292,12 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
geometric_verifier->min_inlier_ratio > 1.0 ||
geometric_verifier->seed_policy_version == 0 ||
geometric_verifier->canonicalization_version == 0)) ||
(sparse_sfm &&
(!task_kind || strcmp(task_kind, "sparse_sfm.run") != 0 || task_kind_version != 1 ||
!valid_task_id(sparse_sfm->task_id) || sparse_sfm->task_id != snapshot->id ||
!valid_task_id(sparse_sfm->track_set_id) ||
!valid_task_id(sparse_sfm->calibration_scope_id) || sparse_sfm->sfm_kind != 1 ||
sparse_sfm->sfm_version != 1 || !valid_sparse_sfm_parameters(&sparse_sfm->parameters))) ||
(checkpoint && !valid_checkpoint(checkpoint))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
@ -1489,6 +1567,103 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && sparse_sfm) {
const Lardon3DSparseIncrementalParameters *p = &sparse_sfm->parameters;
unsigned char maximum_observations[8];
unsigned char maximum_tracks[8];
unsigned char relative_seed[8];
unsigned char pnp_seed[8];
for (unsigned int i = 0; i < 8; ++i) {
maximum_observations[i] = (unsigned char)(p->maximum_observations >> (8U * i));
maximum_tracks[i] = (unsigned char)(p->maximum_tracks >> (8U * i));
relative_seed[i] = (unsigned char)(p->relative_pose.deterministic_seed >> (8U * i));
pnp_seed[i] = (unsigned char)(p->pnp.deterministic_seed >> (8U * i));
}
result = prepare(
database,
"INSERT INTO sparse_sfm_tasks(task_id,track_set_id,calibration_scope_id,sfm_kind,"
"sfm_version,minimum_seed_tracks,minimum_seed_landmarks,minimum_pnp_correspondences,"
"maximum_seed_candidates,maximum_registration_rounds,maximum_landmarks_per_round,"
"maximum_images,maximum_observations,maximum_tracks,reprojection_threshold_px,"
"minimum_track_parallax_rad,relative_robust_threshold_px,relative_confidence,"
"relative_max_iterations,relative_minimum_inliers,relative_minimum_inlier_ratio,"
"relative_minimum_parallax_rad,relative_minimum_cheirality_ratio,"
"relative_deterministic_seed,pnp_reprojection_threshold_px,pnp_confidence,"
"pnp_max_iterations,pnp_minimum_inliers,pnp_minimum_inlier_ratio,"
"pnp_deterministic_seed,refinement_max_iterations,refinement_convergence_tolerance) "
"VALUES(?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) ON CONFLICT(task_id) DO "
"UPDATE SET track_set_id=excluded.track_set_id WHERE sparse_sfm_tasks.track_set_id="
"excluded.track_set_id AND sparse_sfm_tasks.calibration_scope_id="
"excluded.calibration_scope_id AND sparse_sfm_tasks.sfm_kind=excluded.sfm_kind AND "
"sparse_sfm_tasks.sfm_version=excluded.sfm_version AND sparse_sfm_tasks."
"minimum_seed_tracks=excluded.minimum_seed_tracks AND sparse_sfm_tasks."
"minimum_seed_landmarks=excluded.minimum_seed_landmarks AND sparse_sfm_tasks."
"minimum_pnp_correspondences=excluded.minimum_pnp_correspondences AND sparse_sfm_tasks."
"maximum_seed_candidates=excluded.maximum_seed_candidates AND sparse_sfm_tasks."
"maximum_registration_rounds=excluded.maximum_registration_rounds AND sparse_sfm_tasks."
"maximum_landmarks_per_round=excluded.maximum_landmarks_per_round AND sparse_sfm_tasks."
"maximum_images=excluded.maximum_images AND sparse_sfm_tasks.maximum_observations="
"excluded.maximum_observations AND sparse_sfm_tasks.maximum_tracks=excluded.maximum_tracks "
"AND sparse_sfm_tasks.reprojection_threshold_px=excluded.reprojection_threshold_px AND "
"sparse_sfm_tasks.minimum_track_parallax_rad=excluded.minimum_track_parallax_rad AND "
"sparse_sfm_tasks.relative_robust_threshold_px=excluded.relative_robust_threshold_px AND "
"sparse_sfm_tasks.relative_confidence=excluded.relative_confidence AND sparse_sfm_tasks."
"relative_max_iterations=excluded.relative_max_iterations AND sparse_sfm_tasks."
"relative_minimum_inliers=excluded.relative_minimum_inliers AND sparse_sfm_tasks."
"relative_minimum_inlier_ratio=excluded.relative_minimum_inlier_ratio AND sparse_sfm_tasks."
"relative_minimum_parallax_rad=excluded.relative_minimum_parallax_rad AND sparse_sfm_tasks."
"relative_minimum_cheirality_ratio=excluded.relative_minimum_cheirality_ratio AND "
"sparse_sfm_tasks.relative_deterministic_seed=excluded.relative_deterministic_seed AND "
"sparse_sfm_tasks.pnp_reprojection_threshold_px=excluded.pnp_reprojection_threshold_px AND "
"sparse_sfm_tasks.pnp_confidence=excluded.pnp_confidence AND sparse_sfm_tasks."
"pnp_max_iterations=excluded.pnp_max_iterations AND sparse_sfm_tasks."
"pnp_minimum_inliers=excluded.pnp_minimum_inliers AND sparse_sfm_tasks."
"pnp_minimum_inlier_ratio=excluded.pnp_minimum_inlier_ratio AND sparse_sfm_tasks."
"pnp_deterministic_seed=excluded.pnp_deterministic_seed AND sparse_sfm_tasks."
"refinement_max_iterations=excluded.refinement_max_iterations AND sparse_sfm_tasks."
"refinement_convergence_tolerance=excluded.refinement_convergence_tolerance",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)sparse_sfm->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)sparse_sfm->track_set_id);
sqlite3_bind_int64(statement, 3, (sqlite3_int64)sparse_sfm->calibration_scope_id);
sqlite3_bind_int(statement, 4, (int)sparse_sfm->sfm_kind);
sqlite3_bind_int(statement, 5, (int)sparse_sfm->sfm_version);
sqlite3_bind_int64(statement, 6, p->minimum_seed_tracks);
sqlite3_bind_int64(statement, 7, p->minimum_seed_landmarks);
sqlite3_bind_int64(statement, 8, p->minimum_pnp_correspondences);
sqlite3_bind_int64(statement, 9, p->maximum_seed_candidates);
sqlite3_bind_int64(statement, 10, p->maximum_registration_rounds);
sqlite3_bind_int64(statement, 11, p->maximum_landmarks_per_round);
sqlite3_bind_int64(statement, 12, p->maximum_images);
sqlite3_bind_blob(statement, 13, maximum_observations, 8, SQLITE_TRANSIENT);
sqlite3_bind_blob(statement, 14, maximum_tracks, 8, SQLITE_TRANSIENT);
sqlite3_bind_double(statement, 15, p->reprojection_threshold_px);
sqlite3_bind_double(statement, 16, p->minimum_track_parallax_rad);
sqlite3_bind_double(statement, 17, p->relative_pose.robust_threshold_px);
sqlite3_bind_double(statement, 18, p->relative_pose.confidence);
sqlite3_bind_int64(statement, 19, p->relative_pose.max_iterations);
sqlite3_bind_int64(statement, 20, p->relative_pose.minimum_inliers);
sqlite3_bind_double(statement, 21, p->relative_pose.minimum_inlier_ratio);
sqlite3_bind_double(statement, 22, p->relative_pose.minimum_parallax_rad);
sqlite3_bind_double(statement, 23, p->relative_pose.minimum_cheirality_ratio);
sqlite3_bind_blob(statement, 24, relative_seed, 8, SQLITE_TRANSIENT);
sqlite3_bind_double(statement, 25, p->pnp.reprojection_threshold_px);
sqlite3_bind_double(statement, 26, p->pnp.confidence);
sqlite3_bind_int64(statement, 27, p->pnp.max_iterations);
sqlite3_bind_int64(statement, 28, p->pnp.minimum_inliers);
sqlite3_bind_double(statement, 29, p->pnp.minimum_inlier_ratio);
sqlite3_bind_blob(statement, 30, pnp_seed, 8, SQLITE_TRANSIENT);
sqlite3_bind_int64(statement, 31, p->refinement.max_iterations);
sqlite3_bind_double(statement, 32, p->refinement.convergence_tolerance);
result = step_done(database, statement, "upsert sparse SfM task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Payload Sparse SfM immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
result =
prepare(database,
@ -1521,7 +1696,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) {
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
@ -1533,7 +1708,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
}
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
@ -1544,7 +1719,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
@ -1554,7 +1729,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
}
static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) {
@ -3536,7 +3711,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, parameters, NULL, NULL, NULL, updated_at);
NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task(
@ -3583,7 +3758,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, parameters, NULL, NULL, updated_at);
NULL, NULL, parameters, NULL, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
@ -3639,7 +3814,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, parameters, NULL, updated_at);
NULL, NULL, NULL, parameters, NULL, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
@ -3697,7 +3872,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, NULL, parameters, updated_at);
NULL, NULL, NULL, NULL, parameters, NULL, updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
@ -3769,7 +3944,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
Lardon3DProjectDbResult result = record_task_internal(
db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL,
db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
updated_at);
if (result != LARDON3D_PROJECT_DB_OK) return result;
(void)pthread_mutex_lock(&db->mutex);
@ -3875,6 +4050,123 @@ Lardon3DProjectDbResult lardon3d_project_db_load_track_builder_task(
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *kind, uint32_t version, const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL,
NULL, NULL, NULL, NULL, parameters, updated_at);
}
static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) {
if (sqlite3_column_type(statement, column) != SQLITE_BLOB ||
sqlite3_column_bytes(statement, column) != 8) {
return false;
}
const unsigned char *bytes = sqlite3_column_blob(statement, column);
uint64_t decoded = 0;
for (unsigned int i = 0; i < 8; ++i) decoded |= (uint64_t)bytes[i] << (8U * i);
*value = decoded;
return true;
}
Lardon3DProjectDbResult lardon3d_project_db_load_sparse_sfm_task(
Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DProjectDbSparseSfmTask *parameters) {
if (!database || !valid_task_id(task_id) || !parameters) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
memset(parameters, 0, sizeof(*parameters));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(
database,
"SELECT s.track_set_id,s.calibration_scope_id,s.sfm_kind,s.sfm_version,"
"s.minimum_seed_tracks,s.minimum_seed_landmarks,s.minimum_pnp_correspondences,"
"s.maximum_seed_candidates,s.maximum_registration_rounds,s.maximum_landmarks_per_round,"
"s.maximum_images,s.maximum_observations,s.maximum_tracks,s.reprojection_threshold_px,"
"s.minimum_track_parallax_rad,s.relative_robust_threshold_px,s.relative_confidence,"
"s.relative_max_iterations,s.relative_minimum_inliers,s.relative_minimum_inlier_ratio,"
"s.relative_minimum_parallax_rad,s.relative_minimum_cheirality_ratio,"
"s.relative_deterministic_seed,s.pnp_reprojection_threshold_px,s.pnp_confidence,"
"s.pnp_max_iterations,s.pnp_minimum_inliers,s.pnp_minimum_inlier_ratio,"
"s.pnp_deterministic_seed,s.refinement_max_iterations,s.refinement_convergence_tolerance,"
"t.task_kind,t.task_kind_version FROM sparse_sfm_tasks s JOIN tasks t ON t.task_id=s.task_id "
"WHERE s.task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
bool corrupt = code != SQLITE_ROW;
if (code == SQLITE_DONE) {
result = LARDON3D_PROJECT_DB_NOT_FOUND;
} else if (!corrupt) {
const char *kind = (const char *)sqlite3_column_text(statement, 31);
sqlite3_int64 ids[2] = {sqlite3_column_int64(statement, 0),
sqlite3_column_int64(statement, 1)};
sqlite3_int64 integers[15];
const int integer_columns[15] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 17, 18, 25, 26, 29, 32};
for (size_t i = 0; i < 15; ++i) integers[i] = sqlite3_column_int64(statement,
integer_columns[i]);
double doubles[11];
const int double_columns[11] = {13, 14, 15, 16, 19, 20, 21, 23, 24, 27, 30};
for (size_t i = 0; i < 11; ++i) doubles[i] = sqlite3_column_double(statement,
double_columns[i]);
corrupt = ids[0] <= 0 || ids[1] <= 0 || !kind || strcmp(kind, "sparse_sfm.run") != 0 ||
integers[0] != 1 || integers[1] != 1 || integers[14] != 1;
for (size_t i = 2; i < 14; ++i) {
if (integers[i] <= 0 || integers[i] > UINT32_MAX) corrupt = true;
}
for (size_t i = 0; i < 11; ++i) {
if (sqlite3_column_type(statement, double_columns[i]) != SQLITE_FLOAT ||
!isfinite(doubles[i])) {
corrupt = true;
}
}
parameters->task_id = task_id;
parameters->track_set_id = (uint64_t)ids[0];
parameters->calibration_scope_id = (uint64_t)ids[1];
parameters->sfm_kind = (uint32_t)integers[0];
parameters->sfm_version = (uint32_t)integers[1];
Lardon3DSparseIncrementalParameters *p = &parameters->parameters;
p->minimum_seed_tracks = (uint32_t)integers[2];
p->minimum_seed_landmarks = (uint32_t)integers[3];
p->minimum_pnp_correspondences = (uint32_t)integers[4];
p->maximum_seed_candidates = (uint32_t)integers[5];
p->maximum_registration_rounds = (uint32_t)integers[6];
p->maximum_landmarks_per_round = (uint32_t)integers[7];
p->maximum_images = (uint32_t)integers[8];
corrupt |= !read_u64_blob(statement, 11, &p->maximum_observations);
corrupt |= !read_u64_blob(statement, 12, &p->maximum_tracks);
p->reprojection_threshold_px = doubles[0];
p->minimum_track_parallax_rad = doubles[1];
p->relative_pose.robust_threshold_px = doubles[2];
p->relative_pose.confidence = doubles[3];
p->relative_pose.max_iterations = (uint32_t)integers[9];
p->relative_pose.minimum_inliers = (uint32_t)integers[10];
p->relative_pose.minimum_inlier_ratio = doubles[4];
p->relative_pose.minimum_parallax_rad = doubles[5];
p->relative_pose.minimum_cheirality_ratio = doubles[6];
corrupt |= !read_u64_blob(statement, 22, &p->relative_pose.deterministic_seed);
p->pnp.reprojection_threshold_px = doubles[7];
p->pnp.confidence = doubles[8];
p->pnp.max_iterations = (uint32_t)integers[11];
p->pnp.minimum_inliers = (uint32_t)integers[12];
p->pnp.minimum_inlier_ratio = doubles[9];
corrupt |= !read_u64_blob(statement, 28, &p->pnp.deterministic_seed);
p->refinement.max_iterations = (uint32_t)integers[13];
p->refinement.convergence_tolerance = doubles[10];
if (corrupt || !valid_sparse_sfm_parameters(p)) result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
result = sqlite_result(database, code, "read sparse SfM task");
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool read_match_result(sqlite3_stmt *statement,
Lardon3DProjectDbMatchResult *result) {
sqlite3_int64 id = sqlite3_column_int64(statement, 0);

152
src/sparse_sfm_gate_f.cpp Normal file
View file

@ -0,0 +1,152 @@
#include "sparse_sfm_gate_f_internal.h"
#include <cmath>
#include <cstring>
#include <limits>
#include <openssl/evp.h>
#include <vector>
#include <algorithm>
namespace {
bool put_u32(unsigned char *record, size_t *offset, uint32_t value) {
if (*offset > 372U - 4U) return false;
for (unsigned int byte = 0; byte < 4; ++byte)
record[(*offset)++] = static_cast<unsigned char>(value >> (byte * 8U));
return true;
}
bool put_u64(unsigned char *record, size_t *offset, uint64_t value) {
if (*offset > 372U - 8U) return false;
for (unsigned int byte = 0; byte < 8; ++byte)
record[(*offset)++] = static_cast<unsigned char>(value >> (byte * 8U));
return true;
}
bool put_f64(unsigned char *record, size_t *offset, 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(record, offset, bits);
}
bool checked_add(uint64_t left, uint64_t right, uint64_t *result) {
if (right > std::numeric_limits<uint64_t>::max() - left) return false;
*result = left + right;
return true;
}
bool checked_mul(uint64_t left, uint64_t right, uint64_t *result) {
if (left != 0 && right > std::numeric_limits<uint64_t>::max() / left) return false;
*result = left * right;
return true;
}
} // namespace
bool lardon3d_sparse_sfm_fingerprint_record(
const Lardon3DSparseIncrementalParameters *p, unsigned char record[372]) {
if (!p || !record) return false;
std::memset(record, 0, 372);
std::memcpy(record, "L3DSFMFP", 8);
size_t offset = 8;
bool ok = put_u32(record, &offset, 1) &&
put_u32(record, &offset, p->minimum_seed_tracks) &&
put_u32(record, &offset, p->minimum_seed_landmarks) &&
put_u32(record, &offset, p->minimum_pnp_correspondences) &&
put_u32(record, &offset, p->maximum_seed_candidates) &&
put_u32(record, &offset, p->maximum_registration_rounds) &&
put_u32(record, &offset, p->maximum_landmarks_per_round) &&
put_u32(record, &offset, p->maximum_images) &&
put_u64(record, &offset, p->maximum_observations) &&
put_u64(record, &offset, p->maximum_tracks) &&
put_f64(record, &offset, p->reprojection_threshold_px) &&
put_f64(record, &offset, p->minimum_track_parallax_rad) &&
put_f64(record, &offset, p->relative_pose.robust_threshold_px) &&
put_f64(record, &offset, p->relative_pose.confidence) &&
put_u32(record, &offset, p->relative_pose.max_iterations) &&
put_u32(record, &offset, p->relative_pose.minimum_inliers) &&
put_f64(record, &offset, p->relative_pose.minimum_inlier_ratio) &&
put_f64(record, &offset, p->relative_pose.minimum_parallax_rad) &&
put_f64(record, &offset, p->relative_pose.minimum_cheirality_ratio) &&
put_u64(record, &offset, p->relative_pose.deterministic_seed) &&
put_f64(record, &offset, p->pnp.reprojection_threshold_px) &&
put_f64(record, &offset, p->pnp.confidence) &&
put_u32(record, &offset, p->pnp.max_iterations) &&
put_u32(record, &offset, p->pnp.minimum_inliers) &&
put_f64(record, &offset, p->pnp.minimum_inlier_ratio) &&
put_u64(record, &offset, p->pnp.deterministic_seed) &&
put_u32(record, &offset, p->refinement.max_iterations) &&
put_f64(record, &offset, p->refinement.convergence_tolerance);
for (unsigned int policy = 0; ok && policy < 19; ++policy)
ok = put_u32(record, &offset, 1);
ok = ok && put_f64(record, &offset, 1e-9);
for (unsigned int policy = 0; ok && policy < 2; ++policy)
ok = put_u32(record, &offset, 1);
ok = ok && put_f64(record, &offset, 1e-9) && put_u32(record, &offset, 1) &&
put_f64(record, &offset, 2.0);
for (unsigned int policy = 0; ok && policy < 6; ++policy)
ok = put_u32(record, &offset, 1);
ok = ok && put_u32(record, &offset, 1) && put_u32(record, &offset, 50) &&
put_f64(record, &offset, 1e-6) && put_f64(record, &offset, 1e-10) &&
put_f64(record, &offset, 1e-8) && put_u32(record, &offset, 0) &&
put_u32(record, &offset, 1) && put_f64(record, &offset, 1e-12) &&
put_u32(record, &offset, 1) && put_u32(record, &offset, 1);
return ok && offset == 372;
}
bool lardon3d_sparse_sfm_parameter_fingerprint(
const Lardon3DSparseIncrementalParameters *parameters, unsigned char digest[32]) {
if (!digest) return false;
unsigned char record[372];
unsigned int length = 0;
return lardon3d_sparse_sfm_fingerprint_record(parameters, record) &&
EVP_Digest(record, sizeof(record), digest, &length, EVP_sha256(), nullptr) == 1 &&
length == 32;
}
bool lardon3d_sparse_sfm_resource_estimate(uint64_t image_count, uint64_t track_count,
uint64_t observation_count,
Lardon3DResourceEstimate *estimate) {
if (!estimate) return false;
uint64_t images = 0, tracks = 0, observations = 0, raw = 134217728ULL;
if (!checked_mul(image_count, 65536ULL, &images) ||
!checked_mul(track_count, 2048ULL, &tracks) ||
!checked_mul(observation_count, 512ULL, &observations) ||
!checked_add(raw, images, &raw) || !checked_add(raw, tracks, &raw) ||
!checked_add(raw, observations, &raw)) return false;
constexpr uint64_t mib = 1024ULL * 1024ULL;
uint64_t remainder = raw % mib;
if (remainder != 0 && !checked_add(raw, mib - remainder, &raw)) return false;
*estimate = {raw, 0, 0, 0, 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_CPU};
return true;
}
bool lardon3d_sparse_sfm_component_persistable(uint64_t registered_image_count,
uint64_t landmark_count) {
return registered_image_count > 0 && landmark_count > 0;
}
bool lardon3d_sparse_sfm_publication_metrics(const double *squared_errors, size_t count,
double *rmse, double *median) {
if (!squared_errors || count == 0 || !rmse || !median) return false;
try {
std::vector<double> errors;
errors.reserve(count);
double sum = 0.0;
for (size_t index = 0; index < count; ++index) {
double squared = squared_errors[index];
if (!std::isfinite(squared) || squared < 0.0 || !std::isfinite(sum + squared)) return false;
sum += squared;
errors.push_back(std::sqrt(squared));
}
std::sort(errors.begin(), errors.end());
*rmse = std::sqrt(sum / static_cast<double>(count));
size_t middle = count / 2;
*median = count % 2 ? errors[middle]
: errors[middle - 1] + (errors[middle] - errors[middle - 1]) / 2.0;
return std::isfinite(*rmse) && std::isfinite(*median);
} catch (...) {
return false;
}
}

View file

@ -0,0 +1,23 @@
#ifndef LARDON3D_SPARSE_SFM_GATE_F_INTERNAL_H
#define LARDON3D_SPARSE_SFM_GATE_F_INTERNAL_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/sparse_sfm_incremental.h>
bool lardon3d_sparse_sfm_fingerprint_record(
const Lardon3DSparseIncrementalParameters *parameters, unsigned char record[372]);
bool lardon3d_sparse_sfm_parameter_fingerprint(
const Lardon3DSparseIncrementalParameters *parameters, unsigned char digest[32]);
bool lardon3d_sparse_sfm_resource_estimate(uint64_t participating_image_count,
uint64_t track_count, uint64_t observation_count,
Lardon3DResourceEstimate *estimate);
bool lardon3d_sparse_sfm_component_persistable(uint64_t registered_image_count,
uint64_t landmark_count);
bool lardon3d_sparse_sfm_publication_metrics(const double *squared_errors, size_t count,
double *rmse, double *median);
#endif

667
src/sparse_sfm_task.cpp Normal file
View file

@ -0,0 +1,667 @@
#include "sparse_sfm_gate_f_internal.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <map>
#include <new>
#include <set>
#include <string>
#include <tuple>
#include <vector>
extern "C" {
#include <lardon3d/feature_store.h>
#include <lardon3d/project.h>
#include <lardon3d/sparse_sfm_bundle_adjustment.h>
#include <lardon3d/sparse_sfm_model.h>
#include <lardon3d/sparse_sfm_task.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
}
namespace {
struct Context {
std::string project_path;
Lardon3DProjectDb *database = nullptr;
Lardon3DProjectDbSparseSfmTask durable{};
struct WorkloadShape {
uint64_t image_count = 0;
uint64_t track_count = 0;
uint64_t observation_count = 0;
} shape;
};
struct ObservationKey {
uint64_t feature_set_id;
uint32_t feature_index;
bool operator<(const ObservationKey &other) const {
return std::tie(feature_set_id, feature_index) <
std::tie(other.feature_set_id, other.feature_index);
}
};
struct Reader {
Lardon3DProjectDbFeatureSet feature_set{};
Lardon3DFeatureReader *reader = nullptr;
Lardon3DFeatureFileMetadata metadata{};
};
struct Resolved {
std::vector<Lardon3DSparseIncrementalImage> images;
std::vector<Lardon3DSparseIncrementalObservation> observations;
std::map<uint64_t, Lardon3DSparseGeometryCalibration> calibrations;
std::map<ObservationKey, size_t> observation_index;
};
struct PublicationStorage {
std::vector<Lardon3DSparseComponent> components;
std::vector<Lardon3DSparseRegisteredImage> cameras;
std::vector<Lardon3DSparseLandmark> landmarks;
std::vector<Lardon3DSparseLandmarkObservation> observations;
double rmse = 0.0;
double median = 0.0;
};
void destroy_context(void *userdata) { delete static_cast<Context *>(userdata); }
bool checked_add(uint64_t left, uint64_t right, uint64_t *output) {
if (right > UINT64_MAX - left) return false;
*output = left + right;
return true;
}
bool load_calibration_membership(const Context &context, std::set<uint64_t> *images) {
Lardon3DSparseCalibrationScope scope{};
if (lardon3d_sparse_calibration_scope_load(context.database,
context.durable.calibration_scope_id, &scope) !=
LARDON3D_PROJECT_DB_OK)
return false;
uint64_t after = 0;
for (;;) {
Lardon3DSparseCalibrationMember members[LARDON3D_SPARSE_SFM_PAGE_MAX]{};
size_t count = 0;
uint64_t next = 0;
if (lardon3d_sparse_calibration_scope_list_members(
context.database, scope.scope_id, after, members, LARDON3D_SPARSE_SFM_PAGE_MAX,
&count, &next) != LARDON3D_PROJECT_DB_OK)
return false;
for (size_t index = 0; index < count; ++index)
if (!images->insert(members[index].image_id).second) return false;
if (count == 0) break;
if (next <= after) return false;
after = next;
}
return images->size() == scope.member_count;
}
bool derive_workload_shape(const Context &context, Context::WorkloadShape *shape) {
Lardon3DProjectDbTrackSet track_set{};
if (!shape ||
lardon3d_project_db_load_track_set(context.database, context.durable.track_set_id,
&track_set) != LARDON3D_PROJECT_DB_OK)
return false;
std::set<uint64_t> calibration_images;
if (!load_calibration_membership(context, &calibration_images)) return false;
std::map<uint64_t, Lardon3DProjectDbFeatureSet> feature_sets;
std::set<uint64_t> participating_images;
Context::WorkloadShape result{};
uint64_t after = 0;
for (;;) {
Lardon3DProjectDbTrack tracks[LARDON3D_SPARSE_SFM_PAGE_MAX]{};
size_t count = 0;
if (lardon3d_project_db_list_tracks(context.database, track_set.track_set_id, after, tracks,
LARDON3D_SPARSE_SFM_PAGE_MAX, &count) !=
LARDON3D_PROJECT_DB_OK)
return false;
bool ok = true;
for (size_t index = 0; ok && index < count; ++index) {
const auto &track = tracks[index];
if (track.track_id <= after || track.track_set_id != track_set.track_set_id ||
!checked_add(result.observation_count, track.observation_count,
&result.observation_count)) {
ok = false;
break;
}
for (uint32_t position = 0; position < track.observation_count; ++position) {
const auto &observation = track.observations[position];
auto feature = feature_sets.find(observation.feature_set_id);
if (feature == feature_sets.end()) {
Lardon3DProjectDbFeatureSet feature_set{};
if (lardon3d_project_db_load_feature_set(context.database,
observation.feature_set_id,
&feature_set) != LARDON3D_PROJECT_DB_OK) {
ok = false;
break;
}
feature = feature_sets.emplace(observation.feature_set_id, feature_set).first;
}
if (observation.feature_index >= feature->second.feature_count ||
!calibration_images.count(feature->second.image_id)) {
ok = false;
break;
}
participating_images.insert(feature->second.image_id);
}
after = track.track_id;
++result.track_count;
}
for (size_t index = 0; index < count; ++index)
lardon3d_project_db_free_track(&tracks[index]);
if (!ok) return false;
if (count == 0) break;
}
result.image_count = participating_images.size();
if (result.track_count != track_set.track_count || result.image_count == 0 ||
result.observation_count == 0)
return false;
*shape = result;
return true;
}
bool build_observation_index(Resolved *resolved) {
resolved->observation_index.clear();
for (size_t index = 0; index < resolved->observations.size(); ++index) {
const auto &observation = resolved->observations[index];
if (!resolved->observation_index
.emplace(ObservationKey{observation.feature_set_id, observation.feature_index}, index)
.second)
return false;
}
return true;
}
void project_landmarks(const Lardon3DSparseIncrementalLandmark *landmarks,
size_t landmark_count, const std::set<uint64_t> &component_keys,
std::vector<Lardon3DSparseLandmark> *output) {
for (size_t index = 0; index < landmark_count; ++index) {
const auto &landmark = landmarks[index];
if (!component_keys.count(landmark.component_key)) continue;
output->push_back(
{0, landmark.track_id, landmark.component_key, landmark.point.x, landmark.point.y,
landmark.point.z, landmark.reprojection_rmse_px, landmark.reprojection_median_px,
landmark.observation_count});
}
std::sort(output->begin(), output->end(), [](const auto &a, const auto &b) {
return std::tie(a.component_key, a.track_id) <
std::tie(b.component_key, b.track_id);
});
}
void close_readers(std::map<uint64_t, Reader> *readers) {
for (auto &item : *readers)
if (item.second.reader) lardon3d_feature_reader_close(item.second.reader);
readers->clear();
}
bool load_reader(const Context &context, uint64_t feature_set_id,
std::map<uint64_t, Reader> *readers, Reader **output) {
auto existing = readers->find(feature_set_id);
if (existing != readers->end()) {
*output = &existing->second;
return true;
}
Reader candidate;
if (lardon3d_project_db_load_feature_set(context.database, feature_set_id,
&candidate.feature_set) != LARDON3D_PROJECT_DB_OK ||
lardon3d_feature_reader_open(context.project_path.c_str(), &candidate.feature_set,
&candidate.reader, &candidate.metadata) !=
LARDON3D_FEATURE_STORE_OK ||
candidate.metadata.feature_count != candidate.feature_set.feature_count) {
if (candidate.reader) lardon3d_feature_reader_close(candidate.reader);
return false;
}
auto inserted = readers->emplace(feature_set_id, candidate);
*output = &inserted.first->second;
return true;
}
bool load_calibrations(const Context &context, Resolved *resolved) {
Lardon3DSparseCalibrationScope scope{};
if (lardon3d_sparse_calibration_scope_load(context.database,
context.durable.calibration_scope_id, &scope) !=
LARDON3D_PROJECT_DB_OK) return false;
uint64_t after = 0;
for (;;) {
Lardon3DSparseCalibrationMember members[LARDON3D_SPARSE_SFM_PAGE_MAX]{};
size_t count = 0;
uint64_t next = 0;
if (lardon3d_sparse_calibration_scope_list_members(
context.database, scope.scope_id, after, members, LARDON3D_SPARSE_SFM_PAGE_MAX,
&count, &next) != LARDON3D_PROJECT_DB_OK) return false;
for (size_t index = 0; index < count; ++index) {
Lardon3DSparseCalibration calibration{};
if (lardon3d_sparse_calibration_load(context.database, members[index].calibration_id,
&calibration) != LARDON3D_PROJECT_DB_OK ||
std::memcmp(calibration.scientific_hash, members[index].calibration_hash, 32) != 0)
return false;
resolved->calibrations.emplace(
members[index].image_id,
Lardon3DSparseGeometryCalibration{calibration.width, calibration.height, calibration.fx,
calibration.fy, calibration.cx, calibration.cy,
calibration.k1, calibration.k2, calibration.p1,
calibration.p2});
}
if (count == 0) break;
if (next <= after) return false;
after = next;
}
return resolved->calibrations.size() == scope.member_count;
}
bool resolve_input(const Context &context, Resolved *resolved, uint64_t *track_count) {
Lardon3DProjectDbTrackSet track_set{};
if (lardon3d_project_db_load_track_set(context.database, context.durable.track_set_id,
&track_set) != LARDON3D_PROJECT_DB_OK ||
!load_calibrations(context, resolved)) return false;
std::map<uint64_t, Reader> readers;
std::set<uint64_t> participating_images;
uint64_t after = 0;
uint64_t tracks_seen = 0;
bool ok = true;
while (ok) {
Lardon3DProjectDbTrack tracks[LARDON3D_SPARSE_SFM_PAGE_MAX]{};
size_t count = 0;
if (lardon3d_project_db_list_tracks(context.database, track_set.track_set_id, after, tracks,
LARDON3D_SPARSE_SFM_PAGE_MAX, &count) !=
LARDON3D_PROJECT_DB_OK) {
ok = false;
break;
}
for (size_t index = 0; ok && index < count; ++index) {
const Lardon3DProjectDbTrack &track = tracks[index];
if (track.track_id <= after || track.track_set_id != track_set.track_set_id) {
ok = false;
break;
}
for (uint32_t position = 0; ok && position < track.observation_count; ++position) {
const Lardon3DProjectDbTrackObservation &observation = track.observations[position];
Reader *reader = nullptr;
Lardon3DFeatureKeypoint keypoint{};
if (!load_reader(context, observation.feature_set_id, &readers, &reader) ||
observation.feature_index >= reader->metadata.feature_count ||
lardon3d_feature_reader_keypoints(reader->reader, observation.feature_index, &keypoint,
1) != LARDON3D_FEATURE_STORE_OK ||
!std::isfinite(keypoint.x) || !std::isfinite(keypoint.y) ||
resolved->calibrations.find(reader->feature_set.image_id) ==
resolved->calibrations.end()) {
ok = false;
break;
}
participating_images.insert(reader->feature_set.image_id);
resolved->observations.push_back(
{track.track_id, reader->feature_set.image_id, observation.feature_set_id,
observation.feature_index, reader->metadata.feature_count,
static_cast<double>(keypoint.x), static_cast<double>(keypoint.y)});
}
after = track.track_id;
++tracks_seen;
}
for (size_t index = 0; index < count; ++index)
lardon3d_project_db_free_track(&tracks[index]);
if (!ok || count == 0) break;
}
close_readers(&readers);
if (!ok || tracks_seen != track_set.track_count) return false;
for (uint64_t image_id : participating_images)
resolved->images.push_back({image_id, resolved->calibrations.at(image_id)});
*track_count = tracks_seen;
return !resolved->images.empty() && !resolved->observations.empty() &&
build_observation_index(resolved);
}
bool persist(Context *context, const Lardon3DTask *task) {
Lardon3DTaskDurableSnapshot snapshot{};
if (!lardon3d_task_durable_snapshot(task, &snapshot)) return false;
Lardon3DProjectDbCheckpoint checkpoint{};
std::snprintf(checkpoint.path, sizeof(checkpoint.path), ".lardon3d/checkpoints/%llu.chk",
static_cast<unsigned long long>(snapshot.id));
checkpoint.format_version = LARDON3D_TASK_CHECKPOINT_VERSION;
checkpoint.updated_at = 0;
std::string path = context->project_path + "/" + checkpoint.path;
Lardon3DTaskCheckpointResult saved = lardon3d_task_checkpoint_save(path.c_str(), &snapshot);
if (saved != LARDON3D_TASK_CHECKPOINT_OK &&
saved != LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE) return false;
checkpoint.durability = saved == LARDON3D_TASK_CHECKPOINT_OK
? LARDON3D_DB_CHECKPOINT_DURABLE
: LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
return lardon3d_project_db_record_sparse_sfm_task(
context->database, &snapshot, LARDON3D_SPARSE_SFM_TASK_KIND,
LARDON3D_SPARSE_SFM_TASK_KIND_VERSION, &checkpoint, &context->durable, 0) ==
LARDON3D_PROJECT_DB_OK;
}
const Lardon3DSparseIncrementalObservation *find_input_observation(
const Resolved &resolved, uint64_t track_id, uint64_t image_id, uint64_t feature_set_id,
uint32_t feature_index) {
auto found = resolved.observation_index.find(ObservationKey{feature_set_id, feature_index});
if (found == resolved.observation_index.end()) return nullptr;
const auto &item = resolved.observations[found->second];
return item.track_id == track_id && item.image_id == image_id ? &item : nullptr;
}
bool build_publication(const Resolved &resolved,
const Lardon3DSparseBundleAdjustmentResult &result,
PublicationStorage *storage) {
std::set<uint64_t> component_keys;
for (size_t index = 0; index < result.component_count; ++index) {
const auto &component = result.components[index];
if (lardon3d_sparse_sfm_component_persistable(component.registered_image_count,
component.landmark_count)) {
component_keys.insert(component.component_key);
storage->components.push_back({0, component.component_key, component.registered_image_count,
component.landmark_count});
}
}
if (storage->components.empty()) return false;
for (size_t index = 0; index < result.camera_count; ++index) {
const auto &camera = result.cameras[index];
if (!component_keys.count(camera.component_key)) continue;
Lardon3DSparseRegisteredImage output{};
output.image_id = camera.image_id;
output.component_key = camera.component_key;
std::memcpy(output.rotation_cw, camera.pose_cw.rotation_cw, sizeof(output.rotation_cw));
std::memcpy(output.translation_cw, camera.pose_cw.translation_cw,
sizeof(output.translation_cw));
storage->cameras.push_back(output);
}
project_landmarks(result.landmarks, result.landmark_count, component_keys,
&storage->landmarks);
std::sort(storage->cameras.begin(), storage->cameras.end(),
[](const auto &a, const auto &b) { return a.image_id < b.image_id; });
std::map<uint64_t, const Lardon3DSparseRegisteredImage *> cameras;
for (const auto &camera : storage->cameras) cameras.emplace(camera.image_id, &camera);
std::map<uint64_t, const Lardon3DSparseLandmark *> landmarks;
for (const auto &landmark : storage->landmarks) landmarks.emplace(landmark.track_id, &landmark);
std::vector<double> squared_errors;
for (size_t index = 0; index < result.observation_count; ++index) {
const auto &observation = result.observations[index];
auto landmark = landmarks.find(observation.track_id);
if (landmark == landmarks.end()) continue;
auto camera = cameras.find(observation.image_id);
auto calibration = resolved.calibrations.find(observation.image_id);
const auto *source = find_input_observation(resolved, observation.track_id,
observation.image_id,
observation.feature_set_id,
observation.feature_index);
if (camera == cameras.end() || calibration == resolved.calibrations.end() || !source)
return false;
const auto &pose = *camera->second;
const auto &point = *landmark->second;
double xc = pose.rotation_cw[0] * point.x + pose.rotation_cw[1] * point.y +
pose.rotation_cw[2] * point.z + pose.translation_cw[0];
double yc = pose.rotation_cw[3] * point.x + pose.rotation_cw[4] * point.y +
pose.rotation_cw[5] * point.z + pose.translation_cw[1];
double zc = pose.rotation_cw[6] * point.x + pose.rotation_cw[7] * point.y +
pose.rotation_cw[8] * point.z + pose.translation_cw[2];
if (!std::isfinite(xc) || !std::isfinite(yc) || !std::isfinite(zc) || zc <= 1e-9)
return false;
double xn = xc / zc, yn = yc / zc, r2 = xn * xn + yn * yn;
const auto &cal = calibration->second;
double radial = 1.0 + cal.k1 * r2 + cal.k2 * r2 * r2;
double xd = xn * radial + 2.0 * cal.p1 * xn * yn + cal.p2 * (r2 + 2.0 * xn * xn);
double yd = yn * radial + cal.p1 * (r2 + 2.0 * yn * yn) + 2.0 * cal.p2 * xn * yn;
double dx = cal.fx * xd + cal.cx - source->x;
double dy = cal.fy * yd + cal.cy - source->y;
double squared = dx * dx + dy * dy;
if (!std::isfinite(squared)) return false;
squared_errors.push_back(squared);
storage->observations.push_back(
{0, observation.track_id, observation.feature_set_id, observation.feature_index,
observation.position_in_track});
}
if (storage->cameras.size() < 2 || storage->landmarks.empty() || squared_errors.empty())
return false;
std::sort(storage->observations.begin(), storage->observations.end(), [](const auto &a,
const auto &b) {
return std::tie(a.track_id, a.position_in_track) <
std::tie(b.track_id, b.position_in_track);
});
return lardon3d_sparse_sfm_publication_metrics(squared_errors.data(), squared_errors.size(),
&storage->rmse, &storage->median);
}
bool execute(Context *context, Lardon3DTask *task, bool *reused) {
unsigned char fingerprint[32]{};
if (!lardon3d_sparse_sfm_parameter_fingerprint(&context->durable.parameters, fingerprint))
return false;
Lardon3DSparseReconstruction existing{};
Lardon3DProjectDbResult lookup = lardon3d_sparse_reconstruction_find_exact(
context->database, context->durable.track_set_id, context->durable.calibration_scope_id,
context->durable.sfm_kind, context->durable.sfm_version, fingerprint, &existing);
if (lookup == LARDON3D_PROJECT_DB_OK) {
*reused = true;
return true;
}
if (lookup != LARDON3D_PROJECT_DB_NOT_FOUND || !lardon3d_task_checkpoint(task)) return false;
Resolved resolved;
uint64_t track_count = 0;
if (!resolve_input(*context, &resolved, &track_count)) return false;
if (track_count != context->shape.track_count ||
resolved.observations.size() != context->shape.observation_count ||
resolved.images.size() != context->shape.image_count)
return false;
Lardon3DSparseIncrementalInput input{context->durable.track_set_id,
context->durable.calibration_scope_id,
resolved.images.data(), resolved.images.size(),
resolved.observations.data(), resolved.observations.size()};
Lardon3DSparseIncrementalResult incremental{};
Lardon3DSparseIncrementalStatus d_status = lardon3d_sparse_incremental_run(
&input, &context->durable.parameters, &incremental);
if (d_status != LARDON3D_SPARSE_INCREMENTAL_COMPLETE &&
d_status != LARDON3D_SPARSE_INCREMENTAL_PARTIAL) {
lardon3d_sparse_incremental_result_destroy(&incremental);
return false;
}
if (!lardon3d_task_checkpoint(task)) {
lardon3d_sparse_incremental_result_destroy(&incremental);
return false;
}
Lardon3DSparseBundleAdjustmentInput e_input{&incremental, resolved.images.data(),
resolved.images.size(),
resolved.observations.data(),
resolved.observations.size()};
Lardon3DSparseBundleAdjustmentResult adjusted{};
auto e_status = lardon3d_sparse_bundle_adjustment_run(&e_input, &adjusted);
lardon3d_sparse_incremental_result_destroy(&incremental);
if (e_status != LARDON3D_SPARSE_BUNDLE_ADJUSTMENT_EXECUTION_OK ||
!lardon3d_task_checkpoint(task)) {
lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted);
return false;
}
PublicationStorage storage;
bool valid = build_publication(resolved, adjusted, &storage);
if (!valid) {
lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted);
return false;
}
Lardon3DSparsePublication publication{
context->durable.track_set_id,
context->durable.calibration_scope_id,
context->durable.sfm_kind,
context->durable.sfm_version,
{},
storage.components.data(),
storage.components.size(),
storage.cameras.data(),
storage.cameras.size(),
storage.landmarks.data(),
storage.landmarks.size(),
storage.observations.data(),
storage.observations.size(),
storage.rmse,
storage.median,
static_cast<int64_t>(std::time(nullptr))};
std::memcpy(publication.parameter_fingerprint, fingerprint, 32);
Lardon3DSparseReconstruction published{};
Lardon3DProjectDbResult publish_result =
lardon3d_sparse_reconstruction_publish(context->database, &publication, &published);
lardon3d_sparse_bundle_adjustment_result_destroy(&adjusted);
if (publish_result == LARDON3D_PROJECT_DB_OK) return true;
if (publish_result != LARDON3D_PROJECT_DB_CONSTRAINT) return false;
lookup = lardon3d_sparse_reconstruction_find_exact(
context->database, context->durable.track_set_id, context->durable.calibration_scope_id,
context->durable.sfm_kind, context->durable.sfm_version, fingerprint, &existing);
*reused = lookup == LARDON3D_PROJECT_DB_OK;
return *reused;
}
bool run(Lardon3DTask *task, void *userdata) {
try {
bool reused = false;
if (!execute(static_cast<Context *>(userdata), task, &reused))
return lardon3d_task_fail(task, "Sparse SfM non publiable.");
return lardon3d_task_set_progress(task, 100,
reused ? "Reconstruction réutilisée."
: "Reconstruction publiée.");
} catch (const std::bad_alloc &) {
return lardon3d_task_fail(task, "Mémoire insuffisante pour Sparse SfM.");
} catch (...) {
return lardon3d_task_fail(task, "Erreur interne Sparse SfM.");
}
}
void finished(const Lardon3DTask *task, void *userdata) {
try {
(void)persist(static_cast<Context *>(userdata), task);
} catch (...) {
}
}
} // namespace
#ifdef LARDON3D_SPARSE_SFM_TASK_TESTING
extern "C" int lardon3d_sparse_sfm_task_test_observation_lookup(
const Lardon3DSparseIncrementalObservation *observations, size_t count,
uint64_t track_id, uint64_t image_id, uint64_t feature_set_id,
uint32_t feature_index, size_t *resolved_index) {
try {
if ((!observations && count != 0) || !resolved_index) return -1;
Resolved resolved;
if (count != 0) resolved.observations.assign(observations, observations + count);
if (!build_observation_index(&resolved)) return -1;
const auto *found = find_input_observation(resolved, track_id, image_id,
feature_set_id, feature_index);
if (!found) return 0;
*resolved_index = static_cast<size_t>(found - resolved.observations.data());
return 1;
} catch (...) {
return -1;
}
}
extern "C" bool lardon3d_sparse_sfm_task_test_project_landmarks(
const Lardon3DSparseIncrementalLandmark *landmarks, size_t landmark_count,
const uint64_t *component_keys, size_t component_count,
Lardon3DSparseLandmark *output, size_t output_capacity, size_t *output_count) {
try {
if ((!landmarks && landmark_count != 0) ||
(!component_keys && component_count != 0) || !output_count ||
(!output && output_capacity != 0))
return false;
std::set<uint64_t> persistable_components;
for (size_t index = 0; index < component_count; ++index)
if (!persistable_components.insert(component_keys[index]).second) return false;
std::vector<Lardon3DSparseLandmark> projected;
project_landmarks(landmarks, landmark_count, persistable_components, &projected);
if (projected.size() > output_capacity || (!output && !projected.empty())) return false;
std::copy(projected.begin(), projected.end(), output);
*output_count = projected.size();
return true;
} catch (...) {
return false;
}
}
#endif
extern "C" bool lardon3d_sparse_sfm_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *userdata,
Lardon3DTaskKindBinding *binding) {
try {
if (!snapshot || !userdata || !binding) return false;
auto *runtime = static_cast<Lardon3DTaskReconstructionContext *>(userdata);
Lardon3DProjectDbSparseSfmTask durable{};
if (lardon3d_project_db_load_sparse_sfm_task(runtime->project_db, snapshot->id, &durable) !=
LARDON3D_PROJECT_DB_OK) return false;
auto *context = new (std::nothrow) Context;
if (!context) return false;
context->project_path = runtime->project_path;
context->database = runtime->project_db;
context->durable = durable;
unsigned char fingerprint[32];
if (!lardon3d_sparse_sfm_parameter_fingerprint(&durable.parameters, fingerprint) ||
!derive_workload_shape(*context, &context->shape)) {
delete context;
return false;
}
*binding = {run, context, destroy_context, finished, context};
return true;
} catch (...) {
return false;
}
}
extern "C" Lardon3DTask *lardon3d_project_create_sparse_sfm_task(
Lardon3DAppState *state, const Lardon3DSparseSfmTaskConfiguration *configuration,
uint64_t *task_id) {
try {
if (task_id) *task_id = 0;
if (!state || !state->project_loaded || !state->project_db || !state->resource_governor ||
!configuration)
return nullptr;
Context temporary;
temporary.project_path = state->project_path;
temporary.database = state->project_db;
temporary.durable.track_set_id = configuration->track_set_id;
temporary.durable.calibration_scope_id = configuration->calibration_scope_id;
if (!derive_workload_shape(temporary, &temporary.shape)) return nullptr;
Lardon3DResourceEstimate estimate{};
if (!lardon3d_sparse_sfm_resource_estimate(
temporary.shape.image_count, temporary.shape.track_count,
temporary.shape.observation_count, &estimate))
return nullptr;
uint64_t id = 0;
if (lardon3d_project_db_allocate_task_id(state->project_db, &id) != LARDON3D_PROJECT_DB_OK)
return nullptr;
auto *context = new (std::nothrow) Context;
if (!context) return nullptr;
context->project_path = state->project_path;
context->database = state->project_db;
context->shape = temporary.shape;
context->durable = {id, configuration->track_set_id, configuration->calibration_scope_id,
LARDON3D_SPARSE_SFM_KIND_INCREMENTAL, LARDON3D_SPARSE_SFM_VERSION,
configuration->parameters};
unsigned char fingerprint[32];
if (!lardon3d_sparse_sfm_parameter_fingerprint(&context->durable.parameters, fingerprint)) {
delete context;
return nullptr;
}
Lardon3DTask *task = lardon3d_task_create_typed(
"Sparse SfM", &estimate, LARDON3D_SPARSE_SFM_TASK_KIND,
LARDON3D_SPARSE_SFM_TASK_KIND_VERSION, run, context, destroy_context);
if (!task || !lardon3d_task_assign_id(task, id) ||
!lardon3d_task_set_finished_callback(task, finished, context) || !persist(context, task)) {
if (task) lardon3d_task_destroy(task);
else delete context;
return nullptr;
}
if (task_id) *task_id = id;
return task;
} catch (...) {
if (task_id) *task_id = 0;
return nullptr;
}
}
extern "C" bool lardon3d_project_enqueue_sparse_sfm_task(
Lardon3DAppState *state, const Lardon3DSparseSfmTaskConfiguration *configuration,
uint64_t *task_id) {
Lardon3DTask *task = lardon3d_project_create_sparse_sfm_task(state, configuration, task_id);
if (!task || !lardon3d_task_queue_add(state->task_queue, task, nullptr)) {
if (task) lardon3d_task_destroy(task);
return false;
}
return true;
}

View file

@ -8,6 +8,9 @@
#ifdef LARDON3D_TRACK_BUILDER_TASK_AVAILABLE
#include <lardon3d/track_builder_task.h>
#endif
#ifdef LARDON3D_SPARSE_SFM_TASK_AVAILABLE
#include <lardon3d/sparse_sfm_task.h>
#endif
#include <lardon3d/task_kind_registry.h>
const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
@ -58,6 +61,13 @@ const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
.kind_version = LARDON3D_TRACK_BUILDER_TASK_KIND_VERSION,
.reconstruct = lardon3d_track_builder_task_reconstruct,
},
#endif
#ifdef LARDON3D_SPARSE_SFM_TASK_AVAILABLE
{
.kind = LARDON3D_SPARSE_SFM_TASK_KIND,
.kind_version = LARDON3D_SPARSE_SFM_TASK_KIND_VERSION,
.reconstruct = lardon3d_sparse_sfm_task_reconstruct,
},
#endif
};
static const Lardon3DTaskKindRegistry registry = {

View file

@ -172,6 +172,7 @@ static bool create_v11_database(const char *path) {
lardon3d_project_db_close(database);
return execute_sql(path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE IF EXISTS sparse_sfm_tasks;"
"DROP TABLE IF EXISTS sparse_landmark_observations;"
"DROP TABLE IF EXISTS sparse_landmarks;"
"DROP TABLE IF EXISTS sparse_registered_images;"
@ -194,7 +195,7 @@ static bool test_model_api(const char *path) {
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
Parents parents;
CHECK(create_parents(database, &parents));
@ -478,7 +479,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
0));
CHECK(lardon3d_project_db_open(v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
Parents parents;
CHECK(create_parents(database, &parents));
unsigned char fingerprint[32] = {0x91};
@ -493,7 +494,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
migrated_result.geometric_verification_result_id;
lardon3d_project_db_close(database);
CHECK(query_integer(
v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(
query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='index' AND "
@ -522,7 +523,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
0));
CHECK(lardon3d_project_db_open(failed_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
return true;
}

View file

@ -332,6 +332,7 @@ static bool run_task_test(void) {
CHECK(exec_sql(
database_path,
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE IF EXISTS sparse_sfm_tasks;"
"DROP TABLE IF EXISTS sparse_landmark_observations;"
"DROP TABLE IF EXISTS sparse_landmarks;"
"DROP TABLE IF EXISTS sparse_registered_images;"
@ -361,7 +362,7 @@ static bool run_task_test(void) {
0));
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
CHECK(remove_tree(root));
return true;

View file

@ -53,6 +53,7 @@ static bool create_v9_database(const char *path) {
return false;
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE IF EXISTS sparse_sfm_tasks;"
"DROP TABLE IF EXISTS sparse_landmark_observations;"
"DROP TABLE IF EXISTS sparse_landmarks;"
"DROP TABLE IF EXISTS sparse_registered_images;"
@ -116,7 +117,7 @@ static bool run_test(void) {
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 16);
CHECK(database && lardon3d_project_db_schema_version(database) == 17);
Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
@ -493,7 +494,7 @@ static bool run_test(void) {
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
/* Verify persistence: load previously created results */
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
@ -536,11 +537,11 @@ static bool run_test(void) {
v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9));
CHECK(lardon3d_project_db_open(v9_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(
query_integer(v9_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -568,7 +569,7 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;

View file

@ -89,6 +89,7 @@ static bool downgrade_project_to_historical_v10(const char *database_path) {
}
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE IF EXISTS sparse_sfm_tasks;"
"DROP TABLE IF EXISTS sparse_landmark_observations;"
"DROP TABLE IF EXISTS sparse_landmarks;"
"DROP TABLE IF EXISTS sparse_registered_images;"
@ -365,7 +366,7 @@ static bool run_test(void) {
"name='matcher_tasks'",
0));
CHECK(reopen_runtime(&fixture));
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 16);
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 17);
CHECK(
query_integer(database_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "

View file

@ -116,7 +116,7 @@ static bool create_future_database(const char *path) {
sqlite3_exec(
connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata VALUES('schema_version',17);",
"INSERT INTO metadata VALUES('schema_version',18);",
NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok;
}
@ -133,6 +133,7 @@ static bool create_v7_database(const char *path) {
return false;
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE sparse_sfm_tasks;"
"DROP TABLE sparse_landmark_observations;"
"DROP TABLE sparse_landmarks;"
"DROP TABLE sparse_registered_images;"
@ -169,6 +170,7 @@ static bool create_v6_database(const char *path) {
return false;
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE sparse_sfm_tasks;"
"DROP TABLE sparse_landmark_observations;"
"DROP TABLE sparse_landmarks;"
"DROP TABLE sparse_registered_images;"
@ -213,6 +215,7 @@ static bool create_v10_database(const char *path) {
}
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE sparse_sfm_tasks;"
"DROP TABLE sparse_landmark_observations;"
"DROP TABLE sparse_landmarks;"
"DROP TABLE sparse_registered_images;"
@ -246,6 +249,7 @@ static bool create_v13_database(const char *path) {
return false;
static const char sql[] =
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
"DROP TABLE sparse_sfm_tasks;"
"DROP TABLE sparse_landmark_observations;"
"DROP TABLE sparse_landmarks;"
"DROP TABLE sparse_registered_images;"
@ -914,7 +918,7 @@ static bool run_test(void) {
Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 16);
CHECK(database && lardon3d_project_db_schema_version(database) == 17);
bool legacy_pending = true;
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
LARDON3D_PROJECT_DB_OK &&
@ -1232,7 +1236,7 @@ static bool run_test(void) {
database = NULL;
CHECK(query_integer(database_path,
"SELECT value FROM metadata WHERE key='schema_version'",
16));
17));
CHECK(query_integer(database_path,
"SELECT count(*) FROM tasks WHERE task_id=1", 1));
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
@ -1260,7 +1264,7 @@ static bool run_test(void) {
CHECK(create_v1_database(legacy_path));
CHECK(lardon3d_project_db_open(legacy_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
CHECK(lardon3d_project_db_get_project(database, &loaded_project) ==
LARDON3D_PROJECT_DB_OK &&
strcmp(loaded_project.stable_id, "legacy-project") == 0);
@ -1275,7 +1279,7 @@ static bool run_test(void) {
database = NULL;
CHECK(query_integer(legacy_path,
"SELECT value FROM metadata WHERE key='schema_version'",
16));
17));
CHECK(create_v1_database(failed_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
@ -1306,7 +1310,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(create_v2_database(failed_v3_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
@ -1340,7 +1344,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(create_v3_database(failed_v4_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
@ -1362,7 +1366,7 @@ static bool run_test(void) {
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
}
CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
CHECK(lardon3d_project_db_load_task(database, 9, &task) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
@ -1417,7 +1421,7 @@ static bool run_test(void) {
error);
}
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1427,7 +1431,7 @@ static bool run_test(void) {
5));
CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1436,11 +1440,11 @@ static bool run_test(void) {
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
CHECK(lardon3d_project_db_open(v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(create_v7_database(failed_v8_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
@ -1457,7 +1461,7 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1476,11 +1480,11 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(v10_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(
query_integer(v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1507,7 +1511,7 @@ static bool run_test(void) {
1));
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1521,11 +1525,11 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(v13_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(query_integer(
v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 16));
v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 17));
CHECK(
query_integer(v13_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1548,7 +1552,7 @@ static bool run_test(void) {
true_v14_path);
}
CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(
@ -1663,7 +1667,7 @@ static bool run_test(void) {
if (true_v15_result != LARDON3D_PROJECT_DB_OK)
fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error);
CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
Lardon3DProjectDbProject migrated_project;
CHECK(lardon3d_project_db_get_project(database, &migrated_project) ==
LARDON3D_PROJECT_DB_OK &&
@ -1695,7 +1699,7 @@ static bool run_test(void) {
database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
CHECK(schema_compare(database_path, true_v15_path, true));
@ -1730,7 +1734,7 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(true_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 16);
CHECK(lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1749,7 +1753,7 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;
@ -1768,7 +1772,7 @@ static bool run_test(void) {
0));
CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 16);
lardon3d_project_db_schema_version(database) == 17);
lardon3d_project_db_close(database);
database = NULL;

View file

@ -0,0 +1,153 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <limits>
#include <openssl/evp.h>
#include <lardon3d/sparse_sfm_incremental.h>
#include "../src/sparse_sfm_gate_f_internal.h"
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \
return 1; \
} \
} while (false)
static bool decode_hex(const char *hex, unsigned char *output, size_t size) {
if (std::strlen(hex) != size * 2) return false;
for (size_t i = 0; i < size; ++i) {
unsigned int value = 0;
if (std::sscanf(hex + i * 2, "%2x", &value) != 1) return false;
output[i] = static_cast<unsigned char>(value);
}
return true;
}
static bool hash(const unsigned char *bytes, size_t size, unsigned char digest[32]) {
unsigned int length = 0;
return EVP_Digest(bytes, size, digest, &length, EVP_sha256(), nullptr) == 1 && length == 32;
}
int main() {
static const char golden_record_hex[] =
"4c334453464d46500100000006000000060000000600000020000000200000000010000000100000"
"40420f000000000090d003000000000000000000000000402d431cebe2361a3f000000000000f83f"
"2b8716d9cef7ef3fdc05000006000000000000000000e03f2d431cebe2361a3f000000000000e03f"
"0000000000000000000000000000f83f2b8716d9cef7ef3fe803000006000000000000000000e03f"
"00000000000000001e00000011ea2d819997713d0100000001000000010000000100000001000000"
"01000000010000000100000001000000010000000100000001000000010000000100000001000000"
"0100000001000000010000000100000095d626e80b2e113e010000000100000095d626e80b2e113e"
"01000000000000000000004001000000010000000100000001000000010000000100000001000000"
"320000008dedb5a0f7c6b03ebbbdd7d9df7cdb3d3a8c30e28e79453e000000000100000011ea2d81"
"9997713d0100000001000000";
static const char golden_digest_hex[] =
"e1c83e5b2036e49254a9426ddbace42b7831373bc896f27abdd2f61e302f9e8c";
unsigned char expected[372]{};
unsigned char expected_digest[32]{};
CHECK(decode_hex(golden_record_hex, expected, sizeof(expected)));
CHECK(decode_hex(golden_digest_hex, expected_digest, sizeof(expected_digest)));
Lardon3DSparseIncrementalParameters parameters{};
CHECK(lardon3d_sparse_incremental_parameters_default(&parameters));
unsigned char record[372]{};
unsigned char digest[32]{};
CHECK(lardon3d_sparse_sfm_fingerprint_record(&parameters, record));
CHECK(std::memcmp(record, expected, sizeof(record)) == 0);
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&parameters, digest));
CHECK(std::memcmp(digest, expected_digest, sizeof(digest)) == 0);
unsigned char repeated[32]{};
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&parameters, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) == 0);
Lardon3DSparseIncrementalParameters mutated = parameters;
mutated.minimum_seed_tracks++;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.maximum_tracks++;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.reprojection_threshold_px += 0.25;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.relative_pose.confidence -= 0.01;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.relative_pose.deterministic_seed++;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.pnp.confidence -= 0.01;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.pnp.deterministic_seed++;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.refinement.convergence_tolerance *= 2.0;
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
unsigned char policy_record[372];
std::memcpy(policy_record, record, sizeof(record));
policy_record[180] ^= 1;
CHECK(hash(policy_record, sizeof(policy_record), repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
std::memcpy(policy_record, record, sizeof(record));
policy_record[256] ^= 1;
CHECK(hash(policy_record, sizeof(policy_record), repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
std::memcpy(policy_record, record, sizeof(record));
policy_record[300] ^= 1;
CHECK(hash(policy_record, sizeof(policy_record), repeated));
CHECK(std::memcmp(digest, repeated, sizeof(digest)) != 0);
mutated = parameters;
mutated.minimum_track_parallax_rad = 0.0;
CHECK(lardon3d_sparse_sfm_fingerprint_record(&mutated, record));
mutated.minimum_track_parallax_rad = -0.0;
CHECK(lardon3d_sparse_sfm_fingerprint_record(&mutated, policy_record));
CHECK(std::memcmp(record, policy_record, sizeof(record)) == 0);
mutated = parameters;
mutated.pnp.confidence = std::numeric_limits<double>::quiet_NaN();
CHECK(!lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
mutated.pnp.confidence = std::numeric_limits<double>::infinity();
CHECK(!lardon3d_sparse_sfm_parameter_fingerprint(&mutated, repeated));
Lardon3DResourceEstimate estimate{};
CHECK(lardon3d_sparse_sfm_resource_estimate(10, 100, 1000, &estimate));
CHECK(estimate.memory_fixed_bytes == 136314880ULL);
CHECK(estimate.memory_bytes_per_item == 0 && estimate.gpu_memory_fixed_bytes == 0 &&
estimate.gpu_memory_bytes_per_item == 0 && estimate.minimum_batch_size == 1 &&
estimate.maximum_batch_size == 1 && estimate.desired_cpu_threads == 1 &&
estimate.desired_gpu_slots == 0 && estimate.desired_io_slots == 1 &&
estimate.task_class == LARDON3D_RESOURCE_TASK_CPU);
CHECK(!lardon3d_sparse_sfm_resource_estimate(UINT64_MAX, 1, 1, &estimate));
CHECK(!lardon3d_sparse_sfm_resource_estimate(1, UINT64_MAX, 1, &estimate));
CHECK(!lardon3d_sparse_sfm_resource_estimate(1, 1, UINT64_MAX, &estimate));
CHECK(lardon3d_sparse_sfm_component_persistable(2, 3));
CHECK(!lardon3d_sparse_sfm_component_persistable(0, 3));
CHECK(!lardon3d_sparse_sfm_component_persistable(2, 0));
double rmse = 0.0;
double median = 0.0;
const double zero[] = {0.0};
CHECK(lardon3d_sparse_sfm_publication_metrics(zero, 1, &rmse, &median));
CHECK(rmse == 0.0 && median == 0.0);
const double odd[] = {1.0, 9.0, 4.0};
CHECK(lardon3d_sparse_sfm_publication_metrics(odd, 3, &rmse, &median));
CHECK(std::abs(rmse - std::sqrt(14.0 / 3.0)) < 1e-15 && median == 2.0);
const double even[] = {1.0, 16.0, 4.0, 9.0};
CHECK(lardon3d_sparse_sfm_publication_metrics(even, 4, &rmse, &median));
CHECK(std::abs(rmse - std::sqrt(7.5)) < 1e-15 && median == 2.5);
const double weighted[] = {1.0, 1.0, 100.0};
CHECK(lardon3d_sparse_sfm_publication_metrics(weighted, 3, &rmse, &median));
CHECK(std::abs(rmse - std::sqrt(34.0)) < 1e-15 && median == 1.0);
const double invalid[] = {1.0, std::numeric_limits<double>::infinity()};
CHECK(!lardon3d_sparse_sfm_publication_metrics(invalid, 2, &rmse, &median));
return 0;
}

View file

@ -91,7 +91,7 @@ static bool run_test(void) {
Lardon3DProjectDb *db = NULL;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(db) == 16);
CHECK(lardon3d_project_db_schema_version(db) == 17);
Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(db, "Sparse model", &scanset) ==

View file

@ -0,0 +1,142 @@
#include <cstdio>
#include <cstring>
#include <sqlite3.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
extern "C" {
#include <lardon3d/project_db.h>
#include <lardon3d/sparse_sfm_task.h>
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \
return 1; \
} \
} while (false)
static bool same_double(double a, double b) {
uint64_t bits_a = 0;
uint64_t bits_b = 0;
std::memcpy(&bits_a, &a, sizeof(bits_a));
std::memcpy(&bits_b, &b, sizeof(bits_b));
return bits_a == bits_b;
}
static bool same_parameters(const Lardon3DSparseIncrementalParameters &a,
const Lardon3DSparseIncrementalParameters &b) {
return a.minimum_seed_tracks == b.minimum_seed_tracks &&
a.minimum_seed_landmarks == b.minimum_seed_landmarks &&
a.minimum_pnp_correspondences == b.minimum_pnp_correspondences &&
a.maximum_seed_candidates == b.maximum_seed_candidates &&
a.maximum_registration_rounds == b.maximum_registration_rounds &&
a.maximum_landmarks_per_round == b.maximum_landmarks_per_round &&
a.maximum_images == b.maximum_images &&
a.maximum_observations == b.maximum_observations && a.maximum_tracks == b.maximum_tracks &&
same_double(a.reprojection_threshold_px, b.reprojection_threshold_px) &&
same_double(a.minimum_track_parallax_rad, b.minimum_track_parallax_rad) &&
same_double(a.relative_pose.robust_threshold_px, b.relative_pose.robust_threshold_px) &&
same_double(a.relative_pose.confidence, b.relative_pose.confidence) &&
a.relative_pose.max_iterations == b.relative_pose.max_iterations &&
a.relative_pose.minimum_inliers == b.relative_pose.minimum_inliers &&
same_double(a.relative_pose.minimum_inlier_ratio,
b.relative_pose.minimum_inlier_ratio) &&
same_double(a.relative_pose.minimum_parallax_rad, b.relative_pose.minimum_parallax_rad) &&
same_double(a.relative_pose.minimum_cheirality_ratio,
b.relative_pose.minimum_cheirality_ratio) &&
a.relative_pose.deterministic_seed == b.relative_pose.deterministic_seed &&
same_double(a.pnp.reprojection_threshold_px, b.pnp.reprojection_threshold_px) &&
same_double(a.pnp.confidence, b.pnp.confidence) &&
a.pnp.max_iterations == b.pnp.max_iterations &&
a.pnp.minimum_inliers == b.pnp.minimum_inliers &&
same_double(a.pnp.minimum_inlier_ratio, b.pnp.minimum_inlier_ratio) &&
a.pnp.deterministic_seed == b.pnp.deterministic_seed &&
a.refinement.max_iterations == b.refinement.max_iterations &&
same_double(a.refinement.convergence_tolerance, b.refinement.convergence_tolerance);
}
int main() {
char directory[] = "/tmp/lardon3d-sfm-payload-XXXXXX";
CHECK(mkdtemp(directory) != nullptr);
std::string path = std::string(directory) + "/project.db";
Lardon3DProjectDb *database = nullptr;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
CHECK(lardon3d_project_db_open(path.c_str(), &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 17);
sqlite3 *raw = nullptr;
CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK);
const char *fixture_sql =
"INSERT INTO track_sets(track_set_id,builder_kind,builder_version,parameter_fingerprint,"
"verifier_kind,verifier_version,verifier_fingerprint,input_scope_hash,gvr_count,track_count,"
"created_at) VALUES(41,'track_builder',1,zeroblob(32),1,1,zeroblob(32),zeroblob(32),1,0,0);"
"INSERT INTO sparse_calibration_scopes(scope_id,scientific_hash,member_count) "
"VALUES(43,randomblob(32),1);";
CHECK(sqlite3_exec(raw, fixture_sql, nullptr, nullptr, nullptr) == SQLITE_OK);
sqlite3_close(raw);
const uint64_t values[] = {0, 1, static_cast<uint64_t>(INT64_MAX),
static_cast<uint64_t>(INT64_MAX) + 1, UINT64_MAX};
for (size_t index = 0; index < sizeof(values) / sizeof(values[0]); ++index) {
Lardon3DSparseIncrementalParameters p{};
CHECK(lardon3d_sparse_incremental_parameters_default(&p));
p.maximum_observations = values[index];
p.maximum_tracks = values[index];
p.relative_pose.deterministic_seed = values[index];
p.pnp.deterministic_seed = values[index];
p.reprojection_threshold_px = 3.125;
p.relative_pose.confidence = 0.9125;
p.pnp.confidence = 0.8875;
uint64_t id = 100 + index;
Lardon3DTaskDurableSnapshot snapshot{};
snapshot.id = id;
std::snprintf(snapshot.name, sizeof(snapshot.name), "Sparse payload %zu", index);
snapshot.saved_state = TASK_PENDING;
snapshot.recovery_state = TASK_PENDING;
snapshot.estimate = {134217728, 0, 0, 0, 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_CPU};
Lardon3DProjectDbSparseSfmTask payload{id, 41, 43, 1, 1, p};
CHECK(lardon3d_project_db_record_sparse_sfm_task(
database, &snapshot, LARDON3D_SPARSE_SFM_TASK_KIND,
LARDON3D_SPARSE_SFM_TASK_KIND_VERSION, nullptr, &payload, 0) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbSparseSfmTask loaded{};
CHECK(lardon3d_project_db_load_sparse_sfm_task(database, id, &loaded) ==
LARDON3D_PROJECT_DB_OK);
CHECK(loaded.task_id == id && loaded.track_set_id == 41 &&
loaded.calibration_scope_id == 43 && loaded.sfm_kind == 1 && loaded.sfm_version == 1);
CHECK(same_parameters(payload.parameters, loaded.parameters));
}
Lardon3DSparseIncrementalParameters p{};
CHECK(lardon3d_sparse_incremental_parameters_default(&p));
Lardon3DTaskDurableSnapshot wrong{};
wrong.id = 999;
std::snprintf(wrong.name, sizeof(wrong.name), "Wrong kind");
wrong.saved_state = TASK_PENDING;
wrong.recovery_state = TASK_PENDING;
wrong.estimate = {134217728, 0, 0, 0, 1, 1, 1, 0, 1, LARDON3D_RESOURCE_TASK_CPU};
Lardon3DProjectDbSparseSfmTask payload{999, 41, 43, 1, 1, p};
CHECK(lardon3d_project_db_record_sparse_sfm_task(database, &wrong, "other.run", 1, nullptr,
&payload, 0) ==
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, "PRAGMA ignore_check_constraints=ON; UPDATE sparse_sfm_tasks SET "
"maximum_tracks=zeroblob(7) WHERE task_id=100;",
nullptr, nullptr, nullptr) == SQLITE_OK);
sqlite3_close(raw);
Lardon3DProjectDbSparseSfmTask corrupt{};
CHECK(lardon3d_project_db_load_sparse_sfm_task(database, 100, &corrupt) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, "PRAGMA foreign_keys=ON; DELETE FROM tasks WHERE task_id=101;",
nullptr, nullptr, nullptr) == SQLITE_OK);
sqlite3_close(raw);
CHECK(lardon3d_project_db_load_sparse_sfm_task(database, 101, &corrupt) ==
LARDON3D_PROJECT_DB_NOT_FOUND);
lardon3d_project_db_close(database);
CHECK(unlink(path.c_str()) == 0);
CHECK(rmdir(directory) == 0);
return 0;
}

View file

@ -0,0 +1,368 @@
#include <cmath>
#include <cstdio>
#include <cstring>
#include <sched.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/app_state.h>
#include <lardon3d/feature_store.h>
#include <lardon3d/project_db.h>
#include <lardon3d/resource_governor.h>
#include <lardon3d/sparse_sfm_model.h>
#include <lardon3d/sparse_sfm_task.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_kind_registry.h>
#include <lardon3d/task_queue.h>
}
#include "../src/sparse_sfm_gate_f_internal.h"
extern "C" int lardon3d_sparse_sfm_task_test_observation_lookup(
const Lardon3DSparseIncrementalObservation *observations, size_t count,
uint64_t track_id, uint64_t image_id, uint64_t feature_set_id,
uint32_t feature_index, size_t *resolved_index);
extern "C" bool lardon3d_sparse_sfm_task_test_project_landmarks(
const Lardon3DSparseIncrementalLandmark *landmarks, size_t landmark_count,
const uint64_t *component_keys, size_t component_count,
Lardon3DSparseLandmark *output, size_t output_capacity, size_t *output_count);
namespace {
#define CHECK(value) \
do { \
if (!(value)) { \
std::fprintf(stderr, "sparse-sfm-task:%d: %s\n", __LINE__, #value); \
return false; \
} \
} while (0)
constexpr size_t kImageCount = 3;
constexpr size_t kTrackCount = 12;
struct Fixture {
std::string root;
std::string database_path;
Lardon3DAppState state{};
Lardon3DProjectDbScanSet scanset{};
Lardon3DProjectDbImage images[kImageCount]{};
Lardon3DProjectDbFeatureSet feature_sets[kImageCount]{};
Lardon3DProjectDbTrackSet track_set{};
Lardon3DSparseCalibrationScope scope{};
Lardon3DSparseIncrementalParameters parameters{};
};
Lardon3DResourcePolicy policy() {
return {0, 0, 0, 0, 1.0, 100.0, 100.0, 100.0, 0, 1};
}
bool wait_state(Lardon3DTaskQueue *queue, uint64_t id, Lardon3DTaskState wanted) {
for (size_t attempt = 0; attempt < 4000000; ++attempt) {
Lardon3DTaskSnapshot snapshot{};
if (lardon3d_task_queue_get(queue, id, &snapshot) && snapshot.state == wanted) return true;
sched_yield();
}
return false;
}
Lardon3DSparseGeometryPoint2 project(const Lardon3DSparseGeometryCalibration &calibration,
size_t camera,
const Lardon3DSparseGeometryPoint3 &point) {
double x = point.x + static_cast<double>(camera);
return {calibration.fx * x / point.z + calibration.cx,
calibration.fy * point.y / point.z + calibration.cy};
}
std::string asset_path(const unsigned char hash[32], const char *kind) {
static constexpr char digits[] = "0123456789abcdef";
std::string hex(64, '0');
for (size_t index = 0; index < 32; ++index) {
hex[2 * index] = digits[hash[index] >> 4];
hex[2 * index + 1] = digits[hash[index] & 15U];
}
return std::string("assets/") + kind + "/" + hex.substr(0, 2) + "/" + hex;
}
bool initialize_runtime(Fixture *fixture) {
lardon3d_app_state_init(&fixture->state);
fixture->state.hardware_profile.logical_cpu_count = 16;
fixture->state.hardware_profile.page_size_bytes = 4096;
fixture->state.hardware_profile.memory_total_bytes = 64ULL * 1024 * 1024 * 1024;
std::snprintf(fixture->state.hardware_profile.cpu_architecture,
sizeof(fixture->state.hardware_profile.cpu_architecture), "test");
auto resource_policy = policy();
fixture->state.resource_governor =
lardon3d_resource_governor_create(&fixture->state.hardware_profile, &resource_policy);
fixture->state.task_queue = fixture->state.resource_governor
? lardon3d_task_queue_create(
fixture->state.resource_governor, 8)
: nullptr;
return fixture->state.task_queue != nullptr;
}
bool create_fixture(Fixture *fixture) {
char root[] = "/tmp/lardon3d-sparse-task-XXXXXX";
CHECK(mkdtemp(root) != nullptr);
fixture->root = root;
fixture->database_path = fixture->root + "/project.db";
CHECK(mkdir((fixture->root + "/.lardon3d").c_str(), 0700) == 0);
CHECK(mkdir((fixture->root + "/.lardon3d/checkpoints").c_str(), 0700) == 0);
CHECK(initialize_runtime(fixture));
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
CHECK(lardon3d_project_db_open(fixture->database_path.c_str(),
&fixture->state.project_db, error) ==
LARDON3D_PROJECT_DB_OK);
fixture->state.project_loaded = true;
CHECK(std::snprintf(fixture->state.project_path, sizeof(fixture->state.project_path), "%s",
fixture->root.c_str()) > 0);
CHECK(lardon3d_project_db_create_scanset(fixture->state.project_db, "sfm-task",
&fixture->scanset) == LARDON3D_PROJECT_DB_OK);
const Lardon3DSparseGeometryCalibration geometry = {
4000, 3000, 2000.0, 2000.0, 2000.0, 1500.0, 0.0, 0.0, 0.0, 0.0};
std::vector<Lardon3DSparseGeometryPoint3> points;
for (size_t track = 0; track < kTrackCount; ++track)
points.push_back({-1.0 + static_cast<double>(track % 4) * 0.5,
-0.8 + static_cast<double>(track / 4) * 0.4,
5.0 + static_cast<double>(track % 3) * 0.25});
unsigned char feature_fingerprint[32]{};
for (size_t camera = 0; camera < kImageCount; ++camera) {
unsigned char image_hash[32]{};
image_hash[0] = static_cast<unsigned char>(camera + 1);
std::string relative = asset_path(image_hash, "images");
Lardon3DProjectDbImageRegisterStatus status{};
CHECK(lardon3d_project_db_register_image(
fixture->state.project_db, fixture->scanset.scanset_id, image_hash,
relative.c_str(), 1, "synthetic", relative.c_str(), 0, 1, &status,
&fixture->images[camera]) ==
LARDON3D_PROJECT_DB_OK);
std::vector<Lardon3DFeatureKeypoint> keypoints(kTrackCount);
std::vector<unsigned char> descriptors(kTrackCount * 32);
for (size_t track = 0; track < kTrackCount; ++track) {
auto pixel = project(geometry, camera, points[track]);
keypoints[track].x = static_cast<float>(pixel.x);
keypoints[track].y = static_cast<float>(pixel.y);
keypoints[track].size = 1.0F;
std::memset(descriptors.data() + track * 32, static_cast<int>(track + camera), 32);
}
Lardon3DExtractedFeatures features{};
features.image_width = 4000;
features.image_height = 3000;
features.feature_count = static_cast<uint32_t>(kTrackCount);
features.keypoints = keypoints.data();
features.descriptors = descriptors.data();
features.descriptor_bytes = descriptors.size();
CHECK(lardon3d_feature_store_publish_v2(
&fixture->state, fixture->images[camera].image_id, 0, "test", 1,
feature_fingerprint, LARDON3D_FEATURE_DESCRIPTOR_U8, 32, 0, &features,
&fixture->feature_sets[camera]) == LARDON3D_FEATURE_STORE_OK);
}
Lardon3DSparseCalibration calibration{};
calibration.model_kind = LARDON3D_SPARSE_SFM_CALIBRATION_KIND_PINHOLE;
calibration.model_version = LARDON3D_SPARSE_SFM_CALIBRATION_VERSION;
calibration.width = 4000;
calibration.height = 3000;
calibration.fx = 2000.0;
calibration.fy = 2000.0;
calibration.cx = 2000.0;
calibration.cy = 1500.0;
calibration.provenance_kind = LARDON3D_SPARSE_SFM_PROVENANCE_USER_EXPLICIT;
Lardon3DSparseCalibration stored{};
CHECK(lardon3d_sparse_calibration_create(fixture->state.project_db, &calibration, &stored) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DSparseCalibrationMember members[kImageCount]{};
for (size_t camera = 0; camera < kImageCount; ++camera) {
members[camera].image_id = fixture->images[camera].image_id;
members[camera].calibration_id = stored.calibration_id;
std::memcpy(members[camera].calibration_hash, stored.scientific_hash, 32);
}
CHECK(lardon3d_sparse_calibration_scope_create(fixture->state.project_db, members, kImageCount,
&fixture->scope) == LARDON3D_PROJECT_DB_OK);
std::vector<Lardon3DProjectDbTrackObservation> observations(kTrackCount * kImageCount);
std::vector<Lardon3DProjectDbTrack> tracks(kTrackCount);
for (size_t track = 0; track < kTrackCount; ++track) {
tracks[track].observation_count = kImageCount;
tracks[track].observations = observations.data() + track * kImageCount;
for (size_t camera = 0; camera < kImageCount; ++camera)
tracks[track].observations[camera] = {
fixture->feature_sets[camera].feature_set_id, static_cast<uint32_t>(track),
static_cast<uint32_t>(camera)};
}
Lardon3DProjectDbTrackSet configuration{};
std::snprintf(configuration.builder_kind, sizeof(configuration.builder_kind), "test");
configuration.builder_version = 1;
configuration.verifier_kind = 1;
configuration.verifier_version = 1;
configuration.gvr_count = 1;
configuration.track_count = kTrackCount;
CHECK(lardon3d_project_db_create_track_set(fixture->state.project_db, &configuration,
tracks.data(), tracks.size(),
&fixture->track_set) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_sparse_incremental_parameters_default(&fixture->parameters));
fixture->parameters.minimum_seed_tracks = 6;
fixture->parameters.minimum_seed_landmarks = 6;
fixture->parameters.minimum_pnp_correspondences = 6;
fixture->parameters.maximum_registration_rounds = 8;
return true;
}
void destroy_runtime(Fixture *fixture) {
if (fixture->state.task_queue) lardon3d_task_queue_destroy(fixture->state.task_queue);
if (fixture->state.resource_governor)
lardon3d_resource_governor_destroy(fixture->state.resource_governor);
if (fixture->state.project_db) lardon3d_project_db_close(fixture->state.project_db);
fixture->state = {};
}
Lardon3DSparseSfmTaskConfiguration configuration(Fixture *fixture) {
return {fixture->track_set.track_set_id, fixture->scope.scope_id, fixture->parameters};
}
bool test_lookup() {
Lardon3DSparseIncrementalObservation values[3] = {
{1, 10, 100, 1, 8, 0, 0}, {2, 20, 100, 2, 8, 0, 0},
{3, 30, 200, 1, 8, 0, 0}};
size_t index = 99;
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 1, 10, 100, 1,
&index) == 1 && index == 0);
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 2, 20, 100, 2,
&index) == 1 && index == 1);
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 3, 30, 200, 1,
&index) == 1 && index == 2);
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 1, 10, 999, 1,
&index) == 0);
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 99, 10, 100, 1,
&index) == 0);
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 1, 99, 100, 1,
&index) == 0);
values[2].feature_set_id = 100;
CHECK(lardon3d_sparse_sfm_task_test_observation_lookup(values, 3, 1, 10, 100, 1,
&index) == -1);
return true;
}
bool test_landmark_projection_order() {
Lardon3DSparseIncrementalLandmark landmarks[5]{};
const uint64_t component_keys[2] = {5, 10};
const uint64_t input_components[5] = {10, 5, 99, 5, 10};
const uint64_t input_tracks[5] = {2, 100, 3, 7, 1};
for (size_t index = 0; index < 5; ++index) {
landmarks[index].component_key = input_components[index];
landmarks[index].track_id = input_tracks[index];
landmarks[index].point.z = 1.0;
landmarks[index].observation_count = 2;
}
Lardon3DSparseLandmark projected[5]{};
size_t count = 0;
CHECK(lardon3d_sparse_sfm_task_test_project_landmarks(
landmarks, 5, component_keys, 2, projected, 5, &count));
CHECK(count == 4);
CHECK(projected[0].component_key == 5 && projected[0].track_id == 7);
CHECK(projected[1].component_key == 5 && projected[1].track_id == 100);
CHECK(projected[2].component_key == 10 && projected[2].track_id == 1);
CHECK(projected[3].component_key == 10 && projected[3].track_id == 2);
for (size_t index = 0; index < count; ++index)
CHECK(projected[index].component_key != 99);
return true;
}
bool test_task() {
Fixture fixture;
CHECK(create_fixture(&fixture));
auto config = configuration(&fixture);
uint64_t task_id = 0;
Lardon3DTask *task = lardon3d_project_create_sparse_sfm_task(&fixture.state, &config, &task_id);
CHECK(task && task_id != 0);
char kind[LARDON3D_TASK_KIND_CAPACITY]{};
uint32_t kind_version = 0;
CHECK(lardon3d_task_kind(task, kind, &kind_version));
CHECK(std::strcmp(kind, LARDON3D_SPARSE_SFM_TASK_KIND) == 0 && kind_version == 1);
Lardon3DResourceEstimate estimate{};
CHECK(lardon3d_task_resource_estimate(task, &estimate));
CHECK(estimate.memory_fixed_bytes == 135266304 && estimate.desired_cpu_threads == 1 &&
estimate.desired_io_slots == 1 && estimate.minimum_batch_size == 1 &&
estimate.maximum_batch_size == 1);
Lardon3DProjectDbSparseSfmTask durable{};
CHECK(lardon3d_project_db_load_sparse_sfm_task(fixture.state.project_db, task_id, &durable) ==
LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_task_queue_add(fixture.state.task_queue, task, nullptr));
CHECK(wait_state(fixture.state.task_queue, task_id, TASK_COMPLETED));
unsigned char fingerprint[32]{};
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&fixture.parameters, fingerprint));
Lardon3DSparseReconstruction reconstruction{};
CHECK(lardon3d_sparse_reconstruction_find_exact(
fixture.state.project_db, fixture.track_set.track_set_id, fixture.scope.scope_id, 1, 1,
fingerprint, &reconstruction) == LARDON3D_PROJECT_DB_OK);
CHECK(reconstruction.component_count == 1 && reconstruction.registered_image_count == 3 &&
reconstruction.landmark_count >= 6 &&
std::isfinite(reconstruction.reprojection_rmse_px) &&
std::isfinite(reconstruction.reprojection_median_px));
Lardon3DSparseLandmarkObservation persisted_observations[64]{};
Lardon3DSparseObservationPage observation_page{};
observation_page.items = persisted_observations;
CHECK(lardon3d_sparse_observation_list(fixture.state.project_db,
reconstruction.reconstruction_id, 0, 0, 64,
&observation_page) == LARDON3D_PROJECT_DB_OK);
CHECK(observation_page.count >= 18);
uint64_t reuse_id = 0;
CHECK(lardon3d_project_enqueue_sparse_sfm_task(&fixture.state, &config, &reuse_id));
CHECK(wait_state(fixture.state.task_queue, reuse_id, TASK_COMPLETED));
Lardon3DSparseReconstruction reused{};
CHECK(lardon3d_sparse_reconstruction_find_exact(
fixture.state.project_db, fixture.track_set.track_set_id, fixture.scope.scope_id, 1, 1,
fingerprint, &reused) == LARDON3D_PROJECT_DB_OK);
CHECK(reused.reconstruction_id == reconstruction.reconstruction_id);
uint64_t restart_id = 0;
Lardon3DTask *pending =
lardon3d_project_create_sparse_sfm_task(&fixture.state, &config, &restart_id);
CHECK(pending != nullptr);
Lardon3DTaskDurableSnapshot before{};
CHECK(lardon3d_task_durable_snapshot(pending, &before));
lardon3d_task_destroy(pending);
lardon3d_project_db_close(fixture.state.project_db);
fixture.state.project_db = nullptr;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
CHECK(lardon3d_project_db_open(fixture.database_path.c_str(), &fixture.state.project_db, error) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DTaskKindDescriptor descriptor{LARDON3D_SPARSE_SFM_TASK_KIND, 1,
lardon3d_sparse_sfm_task_reconstruct};
Lardon3DTaskKindRegistry registry{};
CHECK(lardon3d_task_kind_registry_init(&registry, &descriptor, 1));
Lardon3DTaskReconstructionContext runtime{fixture.root.c_str(), fixture.state.project_db,
fixture.state.resource_governor, nullptr};
Lardon3DTask *restored = nullptr;
CHECK(lardon3d_task_kind_registry_restore(&registry, LARDON3D_SPARSE_SFM_TASK_KIND, 1,
&before, &runtime, &restored) ==
LARDON3D_TASK_KIND_OK);
Lardon3DResourceEstimate restored_estimate{};
CHECK(lardon3d_task_resource_estimate(restored, &restored_estimate));
CHECK(std::memcmp(&estimate, &restored_estimate, sizeof(estimate)) == 0);
Lardon3DProjectDbSparseSfmTask restored_payload{};
CHECK(lardon3d_project_db_load_sparse_sfm_task(fixture.state.project_db, restart_id,
&restored_payload) == LARDON3D_PROJECT_DB_OK);
unsigned char restored_fingerprint[32]{};
CHECK(lardon3d_sparse_sfm_parameter_fingerprint(&restored_payload.parameters,
restored_fingerprint));
CHECK(std::memcmp(fingerprint, restored_fingerprint, 32) == 0);
unsigned char record_before[372]{};
unsigned char record_after[372]{};
CHECK(lardon3d_sparse_sfm_fingerprint_record(&fixture.parameters, record_before));
CHECK(lardon3d_sparse_sfm_fingerprint_record(&restored_payload.parameters, record_after));
CHECK(std::memcmp(record_before, record_after, sizeof(record_before)) == 0);
CHECK(lardon3d_task_queue_add(fixture.state.task_queue, restored, nullptr));
CHECK(wait_state(fixture.state.task_queue, restart_id, TASK_COMPLETED));
destroy_runtime(&fixture);
return true;
}
} // namespace
int main() {
return test_lookup() && test_landmark_projection_order() && test_task() ? 0 : 1;
}