feat: freeze photo quality triage

This commit is contained in:
fy59 2026-08-28 11:57:42 +02:00
parent e27bcf9a6f
commit 892d0c5954
28 changed files with 2759 additions and 83 deletions

View file

@ -34,7 +34,8 @@ through an explicitly authorized, explicitly scoped human ticket:
- F0 — PASS/FROZEN - F0 — PASS/FROZEN
- Phase H v1 — PASS/FROZEN - Phase H v1 — PASS/FROZEN
- MVS-M1 — PASS/FROZEN - MVS-M1 — PASS/FROZEN
- Project DB v20 — PASS/FROZEN - Project DB v21 — PASS/FROZEN
- Photo Quality Triage / Acquisition Selection — PASS/FROZEN
- S1 Capture / Asset Provenance — PASS/FROZEN - S1 Capture / Asset Provenance — PASS/FROZEN
- S2 Capture-safe Standard Ingestion — PASS/FROZEN - S2 Capture-safe Standard Ingestion — PASS/FROZEN
- S3 Capture / Acquisition Ingestion — PASS/FROZEN - S3 Capture / Acquisition Ingestion — PASS/FROZEN

View file

@ -34,8 +34,10 @@ persistante, enrichissable et versionnable.
- **Image View** : vues triées et filtrées pour la TUI - **Image View** : vues triées et filtrées pour la TUI
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences - **Task** : moteur de tâches avec pause/reprise, annulation et séquences
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre - **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
- **Project Database v20** : fondations v19 préservées, avec persistance - **Project Database v21** : fondations v20 préservées, avec persistance additive
additive des tâches de campagne d'acquisition des résultats de triage photo
- **[Photo Quality Triage](docs/architecture/photo_quality_triage.md)** : métriques JPEG
bornées et recommandations non destructives, avec persistance additive v21
- **Exécution durable de campagne d'acquisition** : tâche générique à requête - **Exécution durable de campagne d'acquisition** : tâche générique à requête
typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance
tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par

View file

@ -0,0 +1,62 @@
# Photo Quality Triage / Acquisition Selection
**PASS / FROZEN.** This non-destructive recommendation step runs
after acquisition discovery/minimal association and before materialization or features.
It consumes existing campaign groups and never treats path, basename, SHA-256, Task ID,
or group ID as Capture, Asset, or image identity.
Metrics v1 validates JPEG structure with constant memory and a 64 MiB byte-work ceiling
covering every byte consumed or skipped, including marker payloads and entropy data.
Exceeding that parser-resource ceiling is a decode error; it is an operational protection,
not a JPEG image-size or scientific dataset limit. A structurally valid proxy above the operational
8192-pixel maximum-dimension decode ceiling returns `UNAVAILABLE + SUSPECT`, not a decode
error or REJECT, then accepted input uses OpenCV JPEG 1/2, 1/4, or 1/8 grayscale
reduction so the retained raster is at most 1024 pixels on its longest edge. This ceiling
only makes a proxy metric unavailable; it is not a campaign/source-count or scientific
dataset-size limit. The implementation records raw/normalized Laplacian sharpness,
black/white clipping, raw/normalized contrast, low-texture fraction, and stable reason
codes. Confirmed RAW+JPEG uses JPEG; RAW-only returns
`METRIC_UNAVAILABLE_REQUIRES_JPEG_PROXY` without RAW development.
Recommendations are GOOD, SUSPECT, or REJECT. Default selection includes only GOOD.
Human NONE/INCLUDE/EXCLUDE override is separate and never rewrites measured metrics.
La table de décision exacte Metrics v1 est :
| Mesure/politique | Valeur exacte | Effet |
|---|---:|---|
| Netteté normalisée, très faible | `< 0.00018` | `REJECT`, raison `SHARPNESS_VERY_LOW` |
| Netteté normalisée, faible | `< 0.00045` | `SUSPECT`, raison `SHARPNESS_LOW` |
| Échantillon noir / blanc | `<= 5` / `>= 250` | Définit les fractions de clipping |
| Clipping blanc suspect / sévère | `> 0.08` / `> 0.20` | `SUSPECT` / `REJECT` |
| Clipping noir suspect / sévère | `> 0.15` / `> 0.35` | `SUSPECT` / `REJECT` |
| Contraste normalisé faible | `< 0.025` | `SUSPECT` |
| Magnitude Sobel de basse texture | `< 0.035` | Classe un pixel comme basse texture |
| Fraction de basse texture | `> 0.985` | `SUSPECT` |
| Aucun signal sévère ou suspect | — | `GOOD` |
| Au moins un signal suspect, aucun sévère | — | `SUSPECT` |
| Au moins un signal sévère | — | `REJECT` |
| Plus grand axe JPEG proxy | `8192` maximum | Au-delà : `UNAVAILABLE + SUSPECT` |
| Plus grand axe retenu pour analyse | `1024` maximum | Borne opérationnelle mémoire Metrics v1 |
Les cutoffs de clipping conservent le prototype autorisé `scan3d/tri_photos.py`
(pourcentages 8/20 et 15/35 convertis en fractions). L'ensemble de la politique est une
calibration d'ingénierie déterministe validée par fixtures synthétiques et observations
réelles Sony A6000 et Samsung S21 ; ce n'est pas une science universelle de validité
d'image. Modifier une valeur ou la correspondance GOOD/SUSPECT/REJECT exige une nouvelle
version de politique/metrics et invalide les fingerprints ou caches qui incluent cette
politique ; des lignes v1 existantes ne doivent jamais être réinterprétées silencieusement.
La recommandation et l'override restent révisables et non destructifs.
Project DB v21 additively stores an immutable typed request and results keyed by the
canonical campaign-plan `group_id` in 1..N. `next_group_id` is that same one-based identity:
it starts at 1, advances from completed group `k` to `k+1`, and equals `N+1` at completion.
Only the executor's private `group_index = group_id - 1` is zero-based. Result plus cursor is
atomic and precedes generic Task checkpoint progress. The
Governor estimate charges actual retained Task context capacity plus a 20 MiB one-group
analysis allowance covering the 1024x1024 grayscale raster, `CV_32F` normalized/Laplacian/
gradient buffers, masks, and allocator margin. Decode and analysis buffers belong to one
group and are promptly released. Recovery temporarily uses the existing bounded maximum
codec and source arrays before Queue admission, then releases them; they do not live through
the admitted execution. Execution reuses
Task/Queue/Resource Governor and `sequence_break`; no parallel runtime is introduced.

View file

@ -1,5 +1,17 @@
# Base de données projet Lardon3D # Base de données projet Lardon3D
## Photo Quality Triage — Project DB v21
**PASS / FROZEN.** La migration additive v20→v21 ajoute
`photo_quality_triage_tasks` et `photo_quality_triage_results` sans modifier les tables
v20 ni les identités Capture/Asset/image. Chaque résultat conserve le `group_id` canonique
du plan (1..N). `next_group_id` utilise la même identité à base 1 : valeur initiale 1,
avancement de `k` vers `k+1` après publication du résultat `k`, et valeur terminale `N+1`.
Un éventuel `group_index = group_id - 1` reste strictement privé à l'exécuteur. Résultat et
curseur sont atomiques avant le checkpoint Task générique. Les lectures valident les types,
signes, bornes et relations dans les valeurs SQLite 64 bits avant toute conversion vers
les champs C étroits. Recommandation mesurée et override humain restent distincts.
## Capture / Asset Provenance v1 — Project DB v19 (historique FROZEN) ## Capture / Asset Provenance v1 — Project DB v19 (historique FROZEN)
**PASS / FROZEN.** La migration transactionnelle v18→v19 **PASS / FROZEN.** La migration transactionnelle v18→v19

View file

@ -131,6 +131,16 @@ appelle `sequence_break` avant le groupe suivant. Sa reprise passe par la Queue
et le Resource Governor existants ; aucune boucle d'exécution parallèle n'est et le Resource Governor existants ; aucune boucle d'exécution parallèle n'est
introduite. introduite.
**PASS / FROZEN** — le callback `photo_quality.triage` v1 réutilise ce
même Task/Queue/Resource Governor et une `sequence_break` entre groupes. Sa requête typée
est immuable ; le `next_group_id` canonique commence à 1, avance à `k+1` après le résultat
`k`, puis vaut `N+1` à terminaison. Résultat et curseur sont durables avant le checkpoint
générique, de sorte qu'une reprise ne devine ni ne réanalyse une identité déjà publiée. La
réservation charge le contexte retenu et 20 MiB de travail par groupe ; le
JPEG au-dessus de la limite opérationnelle de décodage 8192 pixels reste en attente
`UNAVAILABLE + SUSPECT` (ni erreur de décodage ni rejet) ; une entrée admise est réduite à
1024 pixels maximum avant analyse. Cette borne ne limite ni la campagne ni le dataset.
Le chemin de production de l'import ne possède plus de thread ni de drapeau Le chemin de production de l'import ne possède plus de thread ni de drapeau
d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la d'annulation privés. Son wrapper TUI ne fait qu'enqueue/cancel/observer la
tâche générique. Chaque callback traite un lot borné, checkpoint hors mutex de tâche générique. Chaque callback traite un lot borné, checkpoint hors mutex de

View file

@ -121,13 +121,57 @@ Queue/Governor, persistance tâche/groupe→Capture et reprise sans duplication.
La capacité bornée des propositions de revue conserve un préfixe déterministe; La capacité bornée des propositions de revue conserve un préfixe déterministe;
elle ne limite jamais l'évaluation complète ni le groupement scientifique. elle ne limite jamais l'évaluation complète ni le groupement scientifique.
## NEXT REAL-DATA MILESTONE — SCIENTIFIC EXECUTION ON ENGINE BAY INPUTS ## PHOTO QUALITY TRIAGE / ACQUISITION SELECTION — PASS / FROZEN
1. exécuter features, candidats, matching, vérification, tracks et Sparse SfM ; L'étape qualité canonique implémentée se place après la découverte bornée, les
2. évaluer la qualité de la reconstruction sparse multi-campagne ; métadonnées et l'association minimale des sources, mais avant la matérialisation
3. exécuter MVS/dense avec budgets et scratch contrôlés ; normale d'une campagne et avant toute représentation scientifique, feature,
4. publier durablement nuage dense et mesh ; matching, SfM ou MVS :
5. comparer le résultat aux tentatives photogrammétriques antérieures.
```text
série source explicite
→ découverte bornée / métadonnées
→ candidats d'acquisition et confirmations existantes
→ Photo Quality Triage
→ recommandation GOOD / SUSPECT / REJECT par acquisition physique
→ sélection ou override humain explicite
→ création/exécution durable de campagne pour les groupes retenus
→ représentations scientifiques
→ features → candidats → matching → vérification → tracks → Sparse SfM
```
Le triage consomme les groupes d'acquisition existants et ne redéfinit jamais
Capture, Asset, `image_id`, SHA-256, chemin ou basename. Une paire A6000
RAW+JPEG confirmée est donc une seule unité de triage ; le JPEG caméra valide
est le proxy rapide préféré et l'analyse ne développe pas les RAW à grande
échelle. Un JPEG S21 singleton est la représentation d'analyse naturelle. Le
résultat est non destructif et explicable : `GOOD` est sélectionné par défaut,
`SUSPECT` attend une acceptation explicite et `REJECT` est exclu par défaut mais
peut être forcé par l'humain. Ces états sont une recommandation et une sélection
opérationnelle, jamais une identité ni une suppression de source.
L'implémentation reste déterministe, générique aux appareils, bornée en mémoire
et I/O, et exécutée par les Task, Queue et Resource Governor existants. Le décodage
JPEG applique une limite opérationnelle de 8192 pixels sur le plus grand axe, une
réduction à 1024 pixels et une réservation incluant le contexte retenu plus 20 MiB
de buffers d'analyse par groupe ; ces bornes ne sont pas des limites scientifiques
de campagne ou de dataset.
Elle doit conserver une voie honnête pour les RAW sans proxy plutôt que de
présumer que chaque RAW a un JPEG sibling. La revue TUI détaillée, la guidance
de capture et les keyframes vidéo restent des intégrations ultérieures qui
réutiliseront ce même chemin de qualité, sans second pipeline.
## NEXT REAL-DATA MILESTONE — SELECTED SCIENTIFIC EXECUTION ON ENGINE BAY INPUTS
Après validation de Photo Quality Triage / Acquisition Selection, l'intégration
scientifique porte uniquement sur les acquisitions sélectionnées :
1. matérialiser leurs représentations scientifiques ;
2. exécuter features, candidats, matching, vérification, tracks et Sparse SfM ;
3. évaluer la qualité de la reconstruction sparse multi-campagne ;
4. exécuter MVS/dense avec budgets et scratch contrôlés ;
5. publier durablement nuage dense et mesh ;
6. comparer le résultat aux tentatives photogrammétriques antérieures.
Ce milestone est une intégration réelle, pas une nouvelle série de micro-gates Ce milestone est une intégration réelle, pas une nouvelle série de micro-gates
S3. Les antécédents d'OOM/SIGSEGV OpenMVS, pression swap, grands intermédiaires S3. Les antécédents d'OOM/SIGSEGV OpenMVS, pression swap, grands intermédiaires

View file

@ -0,0 +1,86 @@
#ifndef LARDON3D_PHOTO_QUALITY_H
#define LARDON3D_PHOTO_QUALITY_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
enum {
LARDON3D_PHOTO_QUALITY_METRICS_VERSION = 1,
LARDON3D_PHOTO_QUALITY_REASON_CAPACITY = 256,
/* Metrics v1 retains at most a 1024-pixel longest edge. This engineering
* policy bounds deterministic per-group memory; it is not image validity. */
LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION = 1024,
/* A valid JPEG above this 8192-pixel proxy edge is operationally unavailable
* before allocation. It remains SUSPECT/pending rather than a decode error. */
LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION = 8192,
};
typedef enum {
LARDON3D_PHOTO_QUALITY_METRIC_OK = 0,
LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE,
LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT,
LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR
} Lardon3DPhotoQualityMetricStatus;
typedef enum {
LARDON3D_PHOTO_QUALITY_GOOD = 1,
LARDON3D_PHOTO_QUALITY_SUSPECT = 2,
LARDON3D_PHOTO_QUALITY_REJECT = 3
} Lardon3DPhotoQualityRecommendation;
typedef enum {
LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE = 0,
LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE = 1,
LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE = 2
} Lardon3DPhotoQualityOverride;
typedef struct {
uint32_t metrics_version;
Lardon3DPhotoQualityMetricStatus status;
Lardon3DPhotoQualityRecommendation recommendation;
uint32_t decoded_width;
uint32_t decoded_height;
uint32_t analysis_width;
uint32_t analysis_height;
double sharpness_raw;
double sharpness_normalized;
double clipped_black_fraction;
double clipped_white_fraction;
double contrast_raw;
double contrast_normalized;
double low_texture_fraction;
char reasons[LARDON3D_PHOTO_QUALITY_REASON_CAPACITY];
} Lardon3DPhotoQualityMetrics;
/* Analyze one JPEG proxy/source using one bounded reduced-resolution grayscale
* decode. JPEG dimensions above the operational 8192-pixel decode ceiling
* return UNAVAILABLE + SUSPECT without allocating the raster; this is pending
* selection evidence, not a decode error or scientific rejection. Accepted
* images are decoded at an OpenCV JPEG reduction selected to retain at most a
* 1024-pixel maximum dimension. output is required, caller-owned, and always
* initialized when provided, including on invalid input and decode failure.
* path must name a readable JPEG.
* No Capture,
* Asset, image_id, SHA-256, path, or basename identity is inferred or changed. */
Lardon3DPhotoQualityMetricStatus lardon3d_photo_quality_analyze_jpeg(
const char *path, Lardon3DPhotoQualityMetrics *output);
/* Produce the required explicit result for a group that has no decodable JPEG
* proxy. RAW bytes are not developed merely for triage. */
void lardon3d_photo_quality_raw_only(Lardon3DPhotoQualityMetrics *output);
/* Default inclusion is recommendation-derived (GOOD only); a human override
* remains a separate durable decision and never rewrites measured metrics. */
int lardon3d_photo_quality_effective_include(
Lardon3DPhotoQualityRecommendation recommendation,
Lardon3DPhotoQualityOverride override_value);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -0,0 +1,69 @@
#ifndef LARDON3D_PHOTO_QUALITY_TASK_H
#define LARDON3D_PHOTO_QUALITY_TASK_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <lardon3d/acquisition_campaign.h>
#include <lardon3d/acquisition_campaign_task.h>
#include <lardon3d/task_kind_registry.h>
#ifdef __cplusplus
extern "C" {
#endif
#define LARDON3D_PHOTO_QUALITY_TASK_KIND "photo_quality.triage"
enum { LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION = 1,
LARDON3D_PHOTO_QUALITY_REQUEST_VERSION = 1 };
/* The durable request stores discovered acquisition evidence, never Capture,
* Asset, or image identities. Grouping is reproducible before materialization
* without guessing scientific identity. */
typedef struct {
const Lardon3DAcquisitionCampaignSource *sources;
size_t source_count;
const Lardon3DAcquisitionCampaignConfirmation *confirmations;
size_t confirmation_count;
} Lardon3DPhotoQualityTaskRequest;
#define LARDON3D_PHOTO_QUALITY_TASK_REQUEST_MAX_BYTES \
LARDON3D_ACQUISITION_CAMPAIGN_TASK_REQUEST_MAX_BYTES
/* Create one durable, initially unqueued triage Task for a valid existing
* ScanSet. request arrays are borrowed only for the call and copied into the
* bounded deterministic codec; task_id is required and receives the durable
* identity on success. The caller owns the returned Task. */
Lardon3DTask *lardon3d_project_create_photo_quality_task(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DPhotoQualityTaskRequest *request, uint64_t *task_id);
/* Create and transfer the durable Task to state's Queue. Inputs follow the
* create contract; success transfers Task ownership to the Queue. */
bool lardon3d_project_enqueue_photo_quality(
Lardon3DAppState *state, uint64_t scanset_id,
const Lardon3DPhotoQualityTaskRequest *request, uint64_t *task_id);
/* Rebuild bounded callback context from an immutable durable request. snapshot,
* reconstruction context, and binding are required and caller-owned; temporary
* maximum-capacity codec arrays are released before Queue admission. */
bool lardon3d_photo_quality_task_reconstruct(
const Lardon3DTaskDurableSnapshot *snapshot, void *context,
Lardon3DTaskKindBinding *binding);
/* Encode the request deterministically using the acquisition-campaign v1 wire
* format. output may be NULL only for a size probe; size receives required or
* written bytes. No input storage is retained. */
bool lardon3d_photo_quality_request_encode(
const Lardon3DPhotoQualityTaskRequest *request, unsigned char *output,
size_t capacity, size_t *size);
/* Strictly decode a bounded immutable request into caller-owned source and
* confirmation arrays. Their capacities must cover encoded counts; request
* borrows those arrays for their caller-controlled lifetime. */
bool lardon3d_photo_quality_request_decode(
const unsigned char *input, size_t size,
Lardon3DAcquisitionCampaignSource *sources, size_t source_capacity,
Lardon3DAcquisitionCampaignConfirmation *confirmations,
size_t confirmation_capacity, Lardon3DPhotoQualityTaskRequest *request);
#ifdef __cplusplus
}
#endif
#endif

View file

@ -61,6 +61,12 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbAcquisitionCampaignTask *parameters); const Lardon3DProjectDbAcquisitionCampaignTask *parameters);
/* Persist a durable photo-quality Task snapshot, immutable typed request and durable cursor.
* state, task and parameters are required; OK means the generic checkpoint and v21 typed
* record are durable, while PUBLISHED_NOT_DURABLE leaves post-crash durability unconfirmed. */
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbPhotoQualityTask *parameters);
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task(
Lardon3DAppState *state, const Lardon3DTask *task, Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbFeatureExtractTask *parameters); const Lardon3DProjectDbFeatureExtractTask *parameters);

View file

@ -7,10 +7,11 @@
#include <time.h> #include <time.h>
#include <lardon3d/task.h> #include <lardon3d/task.h>
#include <lardon3d/photo_quality.h>
#include <lardon3d/sparse_sfm_incremental.h> #include <lardon3d/sparse_sfm_incremental.h>
enum { enum {
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 20, LARDON3D_PROJECT_DB_SCHEMA_VERSION = 21,
LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
@ -114,6 +115,25 @@ typedef struct {
uint64_t capture_id; uint64_t capture_id;
} Lardon3DProjectDbAcquisitionCampaignCapture; } Lardon3DProjectDbAcquisitionCampaignCapture;
typedef struct {
uint64_t task_id;
uint64_t scanset_id;
/* Canonical one-based group ID of the next work item. Valid durable values
* are 1..group_count+1, where group_count+1 denotes completion. */
uint32_t next_group_id;
uint32_t group_count;
const unsigned char *request;
size_t request_size;
} Lardon3DProjectDbPhotoQualityTask;
typedef struct {
uint64_t task_id;
uint32_t group_id;
uint32_t proxy_source_index;
Lardon3DPhotoQualityMetrics metrics;
Lardon3DPhotoQualityOverride override_value;
} Lardon3DProjectDbPhotoQualityResult;
typedef struct { typedef struct {
uint64_t scanset_id; uint64_t scanset_id;
char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY]; char name[LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY];
@ -598,6 +618,40 @@ Lardon3DProjectDbResult lardon3d_project_db_retain_acquisition_campaign_capture(
Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture( Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id, Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DProjectDbAcquisitionCampaignCapture *capture); Lardon3DProjectDbAcquisitionCampaignCapture *capture);
/* Atomically record generic Task state and the immutable typed request for an
* existing ScanSet. All pointers are required except checkpoint; request bytes
* are borrowed only for the call. The request identifies campaign groups
* operationally and never creates or infers Capture/Asset/image identities. */
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbPhotoQualityTask *parameters, int64_t updated_at);
/* On success, parameters receives validated one-based cursor/count fields and
* borrows caller-owned request storage containing the immutable request bytes.
* Insufficient capacity is reported as corrupt durable state, never truncated. */
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task(
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters);
/* Result publication and next_group_id advance are atomic. result->group_id is
* the canonical plan ID in 1..N and must equal the task's current one-based
* cursor; next_group_id must equal result->group_id+1 (N+1 after the last
* group). Recovery must never checkpoint generic Task progress beyond a group
* whose result is not durable. Retry converges through the retained result and
* advanced cursor; neither ID is Capture, Asset, or image identity. */
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_result(
Lardon3DProjectDb *database, const Lardon3DProjectDbPhotoQualityResult *result,
uint32_t next_group_id);
/* Load one canonical group result into required caller-owned storage. SQLite
* integer/enum/range/status relations are validated before public narrowing. */
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_result(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DProjectDbPhotoQualityResult *result);
/* Human override is deliberately separate from immutable measured metrics.
* group_id is canonical and one-based; retrying the same value is idempotent. */
Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DPhotoQualityOverride override_value);
Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database, Lardon3DProjectDbResult lardon3d_project_db_allocate_task_id(Lardon3DProjectDb *database,
uint64_t *task_id); uint64_t *task_id);
Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id,

View file

@ -153,6 +153,8 @@ executable(
'src/acquisition_ingest.cpp', 'src/acquisition_ingest.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_campaign.cpp',
'src/acquisition_campaign_task.cpp', 'src/acquisition_campaign_task.cpp',
'src/photo_quality.cpp',
'src/photo_quality_task.cpp',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
'src/feature_store.c', 'src/feature_store.c',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
@ -197,6 +199,7 @@ executable(
'-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE', '-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE',
'-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE', '-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE',
'-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE', '-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE',
'-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE',
], ],
dependencies: [ dependencies: [
ncursesw, ncursesw,
@ -397,6 +400,68 @@ acquisition_campaign_task_test = executable(
) )
test('acquisition-campaign-task', acquisition_campaign_task_test, timeout: 30) test('acquisition-campaign-task', acquisition_campaign_task_test, timeout: 30)
photo_quality_test = executable(
'test-photo-quality',
sources: ['tests/test_photo_quality.cpp', 'src/photo_quality.cpp'],
include_directories: include_directories('include'),
dependencies: [opencv],
)
test('photo-quality', photo_quality_test, timeout: 30)
photo_quality_task_test = executable(
'test-photo-quality-task',
sources: [
'tests/test_photo_quality_task.cpp',
'src/photo_quality_task.cpp', 'src/photo_quality.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_pairing.cpp',
'src/acquisition_campaign_task.cpp', 'src/acquisition_ingest.cpp',
'src/raw_development.cpp', 'src/app_state.c',
'src/image_catalog_persistent.c', 'src/image_catalog.c', 'src/image_view.c',
'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.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',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate],
)
test('photo-quality-task', photo_quality_task_test, timeout: 30)
photo_quality_real_scan = executable(
'photo-quality-real-scan',
sources: [
'tests/photo_quality_real_scan.cpp',
'src/photo_quality_task.cpp', 'src/photo_quality.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_pairing.cpp',
'src/acquisition_campaign_task.cpp', 'src/acquisition_ingest.cpp',
'src/raw_development.cpp', 'src/app_state.c',
'src/image_catalog_persistent.c', 'src/image_catalog.c', 'src/image_view.c',
'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.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',
'src/hardware_profile.c',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate],
)
photo_quality_restart_scan = executable(
'photo-quality-restart-scan',
sources: [
'tests/photo_quality_restart_scan.cpp',
'src/photo_quality_task.cpp', 'src/photo_quality.cpp',
'src/acquisition_campaign.cpp', 'src/acquisition_pairing.cpp',
'src/acquisition_campaign_task.cpp', 'src/acquisition_ingest.cpp',
'src/raw_development.cpp', 'src/app_state.c',
'src/image_catalog_persistent.c', 'src/image_catalog.c', 'src/image_view.c',
'src/project.c', 'src/project_db.c', 'src/project_db_sparse_sfm.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',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv, libraw, libexif, libpng, libdeflate],
)
feature_store_test = executable( feature_store_test = executable(
'test-feature-store', 'test-feature-store',
sources: [ sources: [

View file

@ -129,6 +129,11 @@ def main() -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("images", type=Path) parser.add_argument("images", type=Path)
parser.add_argument("--output", type=Path, required=True) parser.add_argument("--output", type=Path, required=True)
parser.add_argument(
"--compatibility-links",
action="store_true",
help="crée des vues par liens symboliques sans supprimer de contenu existant",
)
args = parser.parse_args() args = parser.parse_args()
images_dir = args.images.expanduser().resolve() images_dir = args.images.expanduser().resolve()
@ -172,16 +177,22 @@ def main() -> int:
print() print()
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
for category in ("bonnes", "suspectes", "mauvaises"): report_path = output_dir / "resultats.csv"
category_dir = output_dir / category if report_path.exists():
if category_dir.exists(): print(
for child in category_dir.iterdir(): f"Rapport déjà présent, aucun fichier modifié : {report_path}",
if child.is_symlink() or child.is_file(): file=sys.stderr,
child.unlink() )
category_dir.mkdir(parents=True, exist_ok=True) return 2
with (output_dir / "resultats.csv").open( if args.compatibility_links:
"w", encoding="utf-8", newline="" for category in ("bonnes", "suspectes", "mauvaises"):
(output_dir / category).mkdir(parents=True, exist_ok=True)
# Exclusive creation makes the analysis/report default non-destructive even
# when two invocations target the same output directory.
with report_path.open(
"x", encoding="utf-8", newline=""
) as csv_file: ) as csv_file:
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
writer.writerow( writer.writerow(
@ -199,8 +210,16 @@ def main() -> int:
counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0} counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0}
for result in results: for result in results:
counters[result.category] += 1 counters[result.category] += 1
if args.compatibility_links:
link_name = f"{counters[result.category]:06d}_{result.path.name}" link_name = f"{counters[result.category]:06d}_{result.path.name}"
link_path = output_dir / result.category / link_name link_path = output_dir / result.category / link_name
if link_path.exists() or link_path.is_symlink():
print(
f"Lien existant conservé : {link_path}", file=sys.stderr
)
else:
# Compatibility output references originals; it never moves
# or deletes immutable source files.
link_path.symlink_to(result.path.resolve()) link_path.symlink_to(result.path.resolve())
writer.writerow( writer.writerow(

327
src/photo_quality.cpp Normal file
View file

@ -0,0 +1,327 @@
#include <lardon3d/photo_quality.h>
#include <algorithm>
#include <climits>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
namespace {
/* Metrics v1 is a deterministic engineering recommendation policy calibrated
* with synthetic fixtures and real Sony A6000 / Samsung S21 captures. These
* cutoffs are not universal scientific validity criteria. Any value or mapping
* change requires a new metrics/policy version and corresponding cache or
* fingerprint invalidation rather than silently reinterpreting stored rows. */
constexpr double kSharpnessVeryLow = 0.00018; // REJECT: severe normalized blur.
constexpr double kSharpnessLow = 0.00045; // SUSPECT: review normalized blur.
constexpr unsigned char kBlackSampleMaximum = 5; // Prototype-compatible black sample.
constexpr unsigned char kWhiteSampleMinimum = 250; // Prototype-compatible white sample.
constexpr double kWhiteClippingSuspect = 0.08; // SUSPECT above 8% white samples.
constexpr double kWhiteClippingSevere = 0.20; // REJECT above 20% white samples.
constexpr double kBlackClippingSuspect = 0.15; // SUSPECT above 15% black samples.
constexpr double kBlackClippingSevere = 0.35; // REJECT above 35% black samples.
constexpr double kContrastLow = 0.025; // SUSPECT below normalized standard deviation.
constexpr double kLowTextureFraction = 0.985; // SUSPECT above low-gradient fraction.
constexpr double kSobelLowTextureMagnitude = 0.035; // Low-gradient normalized cutoff.
/* Structural validation is deliberately bounded independently of JPEG pixel
* dimensions. This is parser resource admission, not a scientific image-size
* limit: every byte read or skipped consumes the same finite work budget. */
constexpr uint64_t kJpegStructuralByteLimit = 64ULL * 1024ULL * 1024ULL;
bool jpeg_dimensions(const char *path, uint32_t &width, uint32_t &height) {
std::FILE *file = std::fopen(path, "rb");
if (!file)
return false;
const auto close = [&file]() { std::fclose(file); };
uint64_t structural_bytes = 0;
const auto read_byte = [&file, &structural_bytes]() {
if (structural_bytes == kJpegStructuralByteLimit)
return EOF;
const int value = std::fgetc(file);
if (value != EOF)
++structural_bytes;
return value;
};
const auto read_exact = [&file, &structural_bytes](void *destination, size_t count) {
if (count > kJpegStructuralByteLimit - structural_bytes)
return false;
const size_t actual = std::fread(destination, 1, count, file);
structural_bytes += actual;
return actual == count;
};
const auto skip = [&file, &structural_bytes](uint64_t count) {
if (count > kJpegStructuralByteLimit - structural_bytes || count > LONG_MAX)
return false;
if (std::fseek(file, static_cast<long>(count), SEEK_CUR) != 0)
return false;
structural_bytes += count;
return true;
};
if (read_byte() != 0xff || read_byte() != 0xd8) {
close();
return false;
}
bool have_dimensions = false;
bool in_entropy = false;
int marker = -1;
for (;;) {
if (marker < 0) {
int prefix = read_byte();
if (in_entropy) {
while (prefix != EOF) {
if (prefix != 0xff) {
prefix = read_byte();
continue;
}
marker = read_byte();
while (marker == 0xff)
marker = read_byte();
if (marker == 0x00 || (marker >= 0xd0 && marker <= 0xd7)) {
prefix = read_byte();
continue;
}
break;
}
} else {
if (prefix != 0xff) {
close();
return false;
}
marker = read_byte();
}
}
while (marker == 0xff)
marker = read_byte();
if (marker == EOF || marker == 0x00 || marker == 0xd8 ||
(!in_entropy && marker >= 0xd0 && marker <= 0xd7)) {
close();
return false;
}
in_entropy = false;
if (marker == 0xd9) {
const int trailing = read_byte();
close();
return have_dimensions && trailing == EOF;
}
if (marker == 0x01) {
marker = -1;
continue;
}
int high = read_byte();
int low = read_byte();
if (high == EOF || low == EOF) {
close();
return false;
}
const unsigned length = (static_cast<unsigned>(high) << 8) |
static_cast<unsigned>(low);
if (length < 2) {
close();
return false;
}
const bool sof = (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 &&
marker != 0xc8 && marker != 0xcc);
if (sof) {
unsigned char header[6];
if (length < 8 || !read_exact(header, sizeof(header))) {
close();
return false;
}
height = (static_cast<uint32_t>(header[1]) << 8) | header[2];
width = (static_cast<uint32_t>(header[3]) << 8) | header[4];
if (width == 0 || height == 0 || have_dimensions ||
!skip(length - 8)) {
close();
return false;
}
have_dimensions = true;
} else if (!skip(length - 2)) {
close();
return false;
}
if (marker == 0xda) {
if (!have_dimensions) {
close();
return false;
}
in_entropy = true;
}
marker = -1;
if (std::ferror(file)) {
close();
return false;
}
}
}
int reduced_grayscale_flag(uint32_t maximum_dimension) {
if (maximum_dimension > 4u * LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_8;
if (maximum_dimension > 2u * LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_4;
if (maximum_dimension > LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION)
return cv::IMREAD_REDUCED_GRAYSCALE_2;
return cv::IMREAD_GRAYSCALE;
}
void initialize(Lardon3DPhotoQualityMetrics *output) {
std::memset(output, 0, sizeof(*output));
output->metrics_version = LARDON3D_PHOTO_QUALITY_METRICS_VERSION;
output->status = LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT;
output->recommendation = LARDON3D_PHOTO_QUALITY_REJECT;
}
void append_reason(char reasons[LARDON3D_PHOTO_QUALITY_REASON_CAPACITY], const char *reason) {
const size_t used = std::strlen(reasons);
if (used >= LARDON3D_PHOTO_QUALITY_REASON_CAPACITY - 1)
return;
(void)std::snprintf(reasons + used, LARDON3D_PHOTO_QUALITY_REASON_CAPACITY - used,
"%s%s", used ? ";" : "", reason);
}
} // namespace
extern "C" Lardon3DPhotoQualityMetricStatus lardon3d_photo_quality_analyze_jpeg(
const char *path, Lardon3DPhotoQualityMetrics *output) {
if (!output)
return LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT;
initialize(output);
if (!path || !path[0])
return output->status;
try {
uint32_t source_width = 0;
uint32_t source_height = 0;
if (!jpeg_dimensions(path, source_width, source_height)) {
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
const uint32_t source_maximum = std::max(source_width, source_height);
output->decoded_width = source_width;
output->decoded_height = source_height;
if (source_maximum > LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION) {
/* This is an operational admission ceiling, not malformed input and not
* a scientific rejection. Preserve a pending SUSPECT result so a proxy,
* policy change, or explicit human override can resolve selection. */
output->status = LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE;
output->recommendation = LARDON3D_PHOTO_QUALITY_SUSPECT;
append_reason(output->reasons, "JPEG_DIMENSIONS_EXCEED_OPERATIONAL_LIMIT");
return output->status;
}
/* Complete structural validation through EOI precedes allocation. This is
* what distinguishes an operationally oversized proxy from a truncated
* file that merely contains a syntactically sufficient oversized SOF. */
cv::Mat decoded = cv::imread(path, reduced_grayscale_flag(source_maximum));
if (decoded.empty() || decoded.cols <= 0 || decoded.rows <= 0) {
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
const double scale = static_cast<double>(LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION) /
static_cast<double>(std::max(decoded.cols, decoded.rows));
const int width = std::max(1, static_cast<int>(std::lround(decoded.cols * scale)));
const int height = std::max(1, static_cast<int>(std::lround(decoded.rows * scale)));
cv::Mat analysis;
cv::resize(decoded, analysis, cv::Size(width, height), 0.0, 0.0,
scale < 1.0 ? cv::INTER_AREA : cv::INTER_NEAREST);
decoded.release();
output->analysis_width = static_cast<uint32_t>(analysis.cols);
output->analysis_height = static_cast<uint32_t>(analysis.rows);
cv::Mat normalized;
analysis.convertTo(normalized, CV_32F, 1.0 / 255.0);
cv::Scalar mean;
cv::Scalar deviation;
cv::meanStdDev(normalized, mean, deviation);
output->contrast_raw = deviation[0] * 255.0;
output->contrast_normalized = deviation[0];
cv::Mat laplacian;
cv::Laplacian(normalized, laplacian, CV_32F, 3);
cv::Scalar lap_mean;
cv::Scalar lap_deviation;
cv::meanStdDev(laplacian, lap_mean, lap_deviation);
output->sharpness_raw = lap_deviation[0] * lap_deviation[0] * 255.0 * 255.0;
output->sharpness_normalized = lap_deviation[0] * lap_deviation[0];
laplacian.release();
cv::Mat gradient_x;
cv::Mat gradient_y;
cv::Sobel(normalized, gradient_x, CV_32F, 1, 0, 3);
cv::Sobel(normalized, gradient_y, CV_32F, 0, 1, 3);
cv::Mat magnitude;
cv::magnitude(gradient_x, gradient_y, magnitude);
output->low_texture_fraction =
static_cast<double>(cv::countNonZero(magnitude < kSobelLowTextureMagnitude)) /
static_cast<double>(magnitude.total());
output->clipped_black_fraction =
static_cast<double>(cv::countNonZero(analysis <= kBlackSampleMaximum)) /
static_cast<double>(analysis.total());
output->clipped_white_fraction =
static_cast<double>(cv::countNonZero(analysis >= kWhiteSampleMinimum)) /
static_cast<double>(analysis.total());
bool severe = false;
bool suspect = false;
if (output->sharpness_normalized < kSharpnessVeryLow) {
append_reason(output->reasons, "SHARPNESS_VERY_LOW");
severe = true;
} else if (output->sharpness_normalized < kSharpnessLow) {
append_reason(output->reasons, "SHARPNESS_LOW");
suspect = true;
}
if (output->clipped_white_fraction > kWhiteClippingSevere ||
output->clipped_black_fraction > kBlackClippingSevere) {
append_reason(output->reasons, "EXPOSURE_CLIPPING_SEVERE");
severe = true;
} else if (output->clipped_white_fraction > kWhiteClippingSuspect ||
output->clipped_black_fraction > kBlackClippingSuspect) {
append_reason(output->reasons, "EXPOSURE_CLIPPING");
suspect = true;
}
if (output->contrast_normalized < kContrastLow ||
output->low_texture_fraction > kLowTextureFraction) {
append_reason(output->reasons, "LOW_TEXTURE_OR_CONTRAST");
suspect = true;
}
output->status = LARDON3D_PHOTO_QUALITY_METRIC_OK;
output->recommendation = severe ? LARDON3D_PHOTO_QUALITY_REJECT
: suspect ? LARDON3D_PHOTO_QUALITY_SUSPECT
: LARDON3D_PHOTO_QUALITY_GOOD;
if (!output->reasons[0])
append_reason(output->reasons, "OK");
return output->status;
} catch (...) {
// C++ and OpenCV exceptions are contained at the public C17 ABI boundary.
initialize(output);
output->status = LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR;
append_reason(output->reasons, "JPEG_DECODE_ERROR");
return output->status;
}
}
extern "C" void lardon3d_photo_quality_raw_only(Lardon3DPhotoQualityMetrics *output) {
if (!output)
return;
initialize(output);
output->status = LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE;
output->recommendation = LARDON3D_PHOTO_QUALITY_SUSPECT;
append_reason(output->reasons, "METRIC_UNAVAILABLE_REQUIRES_JPEG_PROXY");
}
extern "C" int lardon3d_photo_quality_effective_include(
Lardon3DPhotoQualityRecommendation recommendation,
Lardon3DPhotoQualityOverride override_value) {
if (override_value == LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE)
return 1;
if (override_value == LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE)
return 0;
return recommendation == LARDON3D_PHOTO_QUALITY_GOOD ? 1 : 0;
}

255
src/photo_quality_task.cpp Normal file
View file

@ -0,0 +1,255 @@
extern "C" {
#include <lardon3d/photo_quality_task.h>
#include <lardon3d/photo_quality.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
}
#include <cstdio>
#include <memory>
#include <new>
#include <vector>
namespace {
// One admitted group owns this Metrics v1 operational working allowance. It
// covers the bounded 1024-edge raster and float intermediates, not dataset size.
constexpr uint64_t kAnalysisWorkingBytes = UINT64_C(20) * 1024u * 1024u;
struct Context {
char project_path[LARDON3D_APP_STATE_PATH_CAPACITY]{};
Lardon3DProjectDb *db{};
Lardon3DResourceGovernor *governor{};
uint64_t scanset{};
std::vector<Lardon3DAcquisitionCampaignSource> sources;
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
std::vector<unsigned char> encoded;
Lardon3DAcquisitionCampaignPlan plan{};
};
uint64_t context_owned_bytes(const Context &context) {
// The Context and its vector storage live for the complete Task lifetime.
// Charging their actual retained capacities plus the exact one-group analysis
// allowance makes each Governor admission truthful without imposing a global
// source-count or scientific dataset-size limit.
return sizeof(Context) +
context.sources.capacity() * sizeof(Lardon3DAcquisitionCampaignSource) +
context.confirmations.capacity() *
sizeof(Lardon3DAcquisitionCampaignConfirmation) +
context.encoded.capacity();
}
void destroy(void *value) { delete static_cast<Context *>(value); }
void runtime(Context *context, Lardon3DAppState &state) {
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = context->db;
state.resource_governor = context->governor;
std::snprintf(state.project_path, sizeof(state.project_path), "%s", context->project_path);
}
bool checkpoint(Context *context, Lardon3DTask *task, uint32_t next_group_id) {
Lardon3DAppState state;
runtime(context, state);
Lardon3DProjectDbPhotoQualityTask parameters{
lardon3d_task_id(task), context->scanset, next_group_id,
static_cast<uint32_t>(context->plan.group_count), context->encoded.data(),
context->encoded.size()};
return lardon3d_project_checkpoint_photo_quality_task(&state, task, &parameters) ==
LARDON3D_PROJECT_TASK_CHECKPOINT_OK;
}
bool run_impl(Lardon3DTask *task, void *value) {
auto *context = static_cast<Context *>(value);
Lardon3DProjectDbPhotoQualityTask persisted{};
if (lardon3d_project_db_load_photo_quality_task(context->db, lardon3d_task_id(task),
context->encoded.data(), context->encoded.size(), &persisted) != LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "Triage photo durable introuvable.");
for (uint32_t group_id = persisted.next_group_id; group_id <= context->plan.group_count;
++group_id) {
if (!lardon3d_task_checkpoint(task)) return false;
/* Durable group_id is the canonical one-based acquisition-plan identity.
* Only this private index converts it to vector position; neither value is
* scientific Capture identity. */
const uint32_t group_index = group_id - 1u;
const auto &group = context->plan.groups[group_index];
if (group.group_id != group_id)
return lardon3d_task_fail(task, "Identité de groupe de triage incohérente.");
uint32_t proxy = UINT32_MAX;
for (size_t i = 0; i < group.source_count; ++i) {
size_t source_index = group.source_indices[i];
if (context->sources[source_index].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG) {
proxy = static_cast<uint32_t>(source_index);
break;
}
}
Lardon3DProjectDbPhotoQualityResult result{};
result.task_id = lardon3d_task_id(task);
result.group_id = group_id;
result.proxy_source_index = proxy;
result.override_value = LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE;
if (proxy == UINT32_MAX)
lardon3d_photo_quality_raw_only(&result.metrics);
else
(void)lardon3d_photo_quality_analyze_jpeg(context->sources[proxy].path, &result.metrics);
/* Publish measurement and the next canonical group ID in one transaction.
* Generic Task progress may lag after a crash, but can never lead a missing
* result; restart resumes at group_id+1 without guessing identity. */
const uint32_t next_group_id = group_id + 1u;
if (lardon3d_project_db_record_photo_quality_result(context->db, &result, next_group_id) !=
LARDON3D_PROJECT_DB_OK)
return lardon3d_task_fail(task, "Publication du résultat de triage impossible.");
unsigned progress = static_cast<unsigned>((uint64_t(group_id) * 100u) /
context->plan.group_count);
if (!lardon3d_task_set_progress(task, progress, "Groupe photo analysé.") ||
!checkpoint(context, task, next_group_id))
return lardon3d_task_fail(task, "Checkpoint du triage photo impossible.");
if (group_id < context->plan.group_count) {
/* One acquisition group owns one bounded admission. sequence_break releases
* it before pause/cancel handling and Governor re-admission for the next. */
Lardon3DTaskExecutionContract contract{};
Lardon3DResourceReservation *reservation = nullptr;
if (!lardon3d_task_sequence_break(task, context->governor, &reservation, &contract))
return false;
}
}
return true;
}
bool run(Lardon3DTask *task, void *value) noexcept {
try { return run_impl(task, value); }
catch (const std::bad_alloc &) { return lardon3d_task_fail(task, "Mémoire insuffisante."); }
catch (...) { return lardon3d_task_fail(task, "Erreur interne du triage photo."); }
}
void finished(const Lardon3DTask *task, void *value) noexcept {
try {
Lardon3DAppState state;
runtime(static_cast<Context *>(value), state);
(void)lardon3d_project_checkpoint_task(&state, task);
} catch (...) {}
}
Context *make_context(const char *path, Lardon3DProjectDb *db, Lardon3DResourceGovernor *governor,
uint64_t scanset, const Lardon3DPhotoQualityTaskRequest &request,
const unsigned char *encoded, size_t encoded_size) {
std::unique_ptr<Context> context(new (std::nothrow) Context);
if (!context || !path || !path[0] ||
std::snprintf(context->project_path, sizeof(context->project_path), "%s", path) >=
static_cast<int>(sizeof(context->project_path))) return nullptr;
context->db = db; context->governor = governor; context->scanset = scanset;
context->sources.assign(request.sources, request.sources + request.source_count);
context->confirmations.assign(request.confirmations,
request.confirmations + request.confirmation_count);
context->encoded.assign(encoded, encoded + encoded_size);
if (lardon3d_acquisition_campaign_plan(context->sources.data(), context->sources.size(),
context->confirmations.data(), context->confirmations.size(), &context->plan) !=
LARDON3D_ACQUISITION_CAMPAIGN_OK || context->plan.group_count == 0) return nullptr;
return context.release();
}
Lardon3DAcquisitionCampaignTaskRequest campaign_request(const Lardon3DPhotoQualityTaskRequest &r) {
Lardon3DAcquisitionCampaignTaskRequest wire{};
wire.sources = r.sources; wire.source_count = r.source_count;
wire.confirmations = r.confirmations; wire.confirmation_count = r.confirmation_count;
wire.ingest_options.representation = LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE;
wire.ingest_options.max_source_bytes = 1;
return wire;
}
}
extern "C" bool lardon3d_photo_quality_request_encode(const Lardon3DPhotoQualityTaskRequest *r,
unsigned char *out, size_t cap, size_t *size) {
if (!r) { if (size) *size = 0; return false; }
try { auto wire = campaign_request(*r); return lardon3d_acquisition_campaign_request_encode(&wire, out, cap, size); }
catch (...) { if (size) *size = 0; return false; }
}
extern "C" bool lardon3d_photo_quality_request_decode(const unsigned char *input, size_t size,
Lardon3DAcquisitionCampaignSource *sources, size_t source_capacity,
Lardon3DAcquisitionCampaignConfirmation *confirmations, size_t confirmation_capacity,
Lardon3DPhotoQualityTaskRequest *request) {
if (!request) return false;
try {
Lardon3DAcquisitionCampaignTaskRequest wire{};
if (!lardon3d_acquisition_campaign_request_decode(input, size, sources, source_capacity,
confirmations, confirmation_capacity, &wire)) return false;
request->sources = wire.sources; request->source_count = wire.source_count;
request->confirmations = wire.confirmations; request->confirmation_count = wire.confirmation_count;
return true;
} catch (...) { return false; }
}
extern "C" bool lardon3d_photo_quality_task_reconstruct(const Lardon3DTaskDurableSnapshot *snapshot,
void *userdata, Lardon3DTaskKindBinding *binding) {
try {
auto *rt = static_cast<Lardon3DTaskReconstructionContext *>(userdata);
if (!snapshot || !rt || !binding) return false;
/* Restoration happens before Queue admission. These temporary buffers are
* bounded by the durable codec/source capacities and are destroyed when
* reconstruction returns; only the compact Context survives admission. */
std::vector<unsigned char> blob(LARDON3D_PHOTO_QUALITY_TASK_REQUEST_MAX_BYTES);
Lardon3DProjectDbPhotoQualityTask persisted{};
if (lardon3d_project_db_load_photo_quality_task(rt->project_db, snapshot->id, blob.data(),
blob.size(), &persisted) != LARDON3D_PROJECT_DB_OK) return false;
std::vector<Lardon3DAcquisitionCampaignSource> sources(LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES);
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations(LARDON3D_ACQUISITION_CAMPAIGN_MAX_SOURCES);
Lardon3DPhotoQualityTaskRequest request{};
if (!lardon3d_photo_quality_request_decode(blob.data(), persisted.request_size, sources.data(),
sources.size(), confirmations.data(), confirmations.size(), &request)) return false;
auto *context = make_context(rt->project_path, rt->project_db, rt->resource_governor,
persisted.scanset_id, request, blob.data(), persisted.request_size);
if (!context) return false;
*binding = {run, context, destroy, finished, context};
return true;
} catch (...) { return false; }
}
extern "C" Lardon3DTask *lardon3d_project_create_photo_quality_task(Lardon3DAppState *state,
uint64_t scanset, const Lardon3DPhotoQualityTaskRequest *request, uint64_t *task_id) {
if (task_id) *task_id = 0;
try {
if (!state || !state->project_loaded || !state->project_db || !state->resource_governor ||
!request || !task_id) return nullptr;
Lardon3DProjectDbScanSet row{};
if (lardon3d_project_db_load_scanset(state->project_db, scanset, &row) != LARDON3D_PROJECT_DB_OK) return nullptr;
size_t size = 0;
if (!lardon3d_photo_quality_request_encode(request, nullptr, 0, &size)) return nullptr;
std::vector<unsigned char> blob(size);
if (!lardon3d_photo_quality_request_encode(request, blob.data(), blob.size(), &size)) return nullptr;
auto *context = make_context(state->project_path, state->project_db, state->resource_governor,
scanset, *request, blob.data(), size);
if (!context) return nullptr;
uint64_t id = 0;
if (lardon3d_project_db_allocate_task_id(state->project_db, &id) !=
LARDON3D_PROJECT_DB_OK) {
delete context;
return nullptr;
}
const uint64_t retained = context_owned_bytes(*context);
if (retained > UINT64_MAX - kAnalysisWorkingBytes) { delete context; return nullptr; }
Lardon3DResourceEstimate estimate{retained + kAnalysisWorkingBytes, 0, 0, 0,
1, 1, 1, 0, 1,
LARDON3D_RESOURCE_TASK_IMPORT};
auto *task = lardon3d_task_create_typed("Triage qualité photo", &estimate,
LARDON3D_PHOTO_QUALITY_TASK_KIND, 1, run, context, destroy);
if (!task || !lardon3d_task_assign_id(task, id) ||
!lardon3d_task_set_finished_callback(task, finished, context) || !checkpoint(context, task, 1)) {
lardon3d_task_destroy(task); return nullptr;
}
*task_id = id; return task;
} catch (...) { return nullptr; }
}
extern "C" bool lardon3d_project_enqueue_photo_quality(Lardon3DAppState *state, uint64_t scanset,
const Lardon3DPhotoQualityTaskRequest *request, uint64_t *task_id) {
try {
if (!state || !state->task_queue) return false;
auto *task = lardon3d_project_create_photo_quality_task(state, scanset, request, task_id);
if (!task) return false;
if (!lardon3d_task_queue_add(state->task_queue, task, nullptr)) { lardon3d_task_destroy(task); return false; }
return true;
} catch (...) { return false; }
}

View file

@ -675,7 +675,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters, const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters,
const Lardon3DProjectDbMatcherTask *matcher_parameters, const Lardon3DProjectDbMatcherTask *matcher_parameters,
const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters, const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters,
const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters) { const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters,
const Lardon3DProjectDbPhotoQualityTask *quality_parameters) {
if (!state || !state->project_loaded || !state->project_db) { if (!state || !state->project_loaded || !state->project_db) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT; return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT;
} }
@ -746,6 +747,10 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
? lardon3d_project_db_record_acquisition_campaign_task( ? lardon3d_project_db_record_acquisition_campaign_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint, state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
campaign_parameters, now.tv_sec) campaign_parameters, now.tv_sec)
: quality_parameters
? lardon3d_project_db_record_photo_quality_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
quality_parameters, now.tv_sec)
: lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind, : lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind,
task_kind_version, &checkpoint, now.tv_sec); task_kind_version, &checkpoint, now.tv_sec);
if (recorded == LARDON3D_PROJECT_DB_BUSY) { if (recorded == LARDON3D_PROJECT_DB_BUSY) {
@ -761,7 +766,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state, Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state,
const Lardon3DTask *task) { const Lardon3DTask *task) {
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL); return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL);
} }
Lardon3DProjectTaskCheckpointResult Lardon3DProjectTaskCheckpointResult
@ -771,7 +777,7 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal( return checkpoint_task_internal(
state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL); state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task(
@ -779,6 +785,14 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp
const Lardon3DProjectDbAcquisitionCampaignTask *parameters) { const Lardon3DProjectDbAcquisitionCampaignTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL,
parameters, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbPhotoQualityTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
parameters); parameters);
} }
@ -789,7 +803,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task(
@ -797,7 +811,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_tas
const Lardon3DProjectDbSiftExtractTask *parameters) { const Lardon3DProjectDbSiftExtractTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task(
@ -807,7 +821,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_upd
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task(
@ -817,7 +831,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
@ -827,7 +841,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL,
NULL); NULL, NULL);
} }
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task( Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task(
@ -837,7 +851,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifi
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK; return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
} }
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL,
parameters, NULL); parameters, NULL, NULL);
} }
static bool coherent_recovery(const Lardon3DProjectDbTask *database_task, static bool coherent_recovery(const Lardon3DProjectDbTask *database_task,

View file

@ -622,6 +622,33 @@ static const char schema_acquisition_campaign_v20[] =
"capture_id INTEGER NOT NULL REFERENCES captures(capture_id)," "capture_id INTEGER NOT NULL REFERENCES captures(capture_id),"
"PRIMARY KEY(task_id,group_id),UNIQUE(task_id,capture_id));"; "PRIMARY KEY(task_id,group_id),UNIQUE(task_id,capture_id));";
static const char schema_photo_quality_v21[] =
/* v21 stores pre-materialization operational group analysis. These rows
never define Capture, Asset, image_id, SHA-256, path, or basename identity.
next_group_id is canonical and one-based: 1 initially, N+1 when done. */
"CREATE TABLE IF NOT EXISTS photo_quality_triage_tasks("
"task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
"scanset_id INTEGER NOT NULL REFERENCES scansets(scanset_id),"
"next_group_id INTEGER NOT NULL CHECK(next_group_id>=1 AND next_group_id<=4097),"
"group_count INTEGER NOT NULL CHECK(group_count>0 AND group_count<=4096),"
"request BLOB NOT NULL CHECK(length(request)>0),"
"CHECK(next_group_id<=group_count+1));"
"CREATE TABLE IF NOT EXISTS photo_quality_triage_results("
"task_id INTEGER NOT NULL REFERENCES photo_quality_triage_tasks(task_id) ON DELETE CASCADE,"
"group_id INTEGER NOT NULL CHECK(group_id>0 AND group_id<=4096),"
"proxy_source_index INTEGER NOT NULL CHECK(proxy_source_index>=0 AND proxy_source_index<=4294967295),"
"metrics_version INTEGER NOT NULL CHECK(metrics_version>0),"
"metric_status INTEGER NOT NULL CHECK(metric_status BETWEEN 0 AND 3),"
"recommendation INTEGER NOT NULL CHECK(recommendation BETWEEN 1 AND 3),"
"human_override INTEGER NOT NULL DEFAULT 0 CHECK(human_override BETWEEN 0 AND 2),"
"decoded_width INTEGER NOT NULL,decoded_height INTEGER NOT NULL,"
"analysis_width INTEGER NOT NULL,analysis_height INTEGER NOT NULL,"
"sharpness_raw REAL NOT NULL,sharpness_normalized REAL NOT NULL,"
"clipped_black_fraction REAL NOT NULL,clipped_white_fraction REAL NOT NULL,"
"contrast_raw REAL NOT NULL,contrast_normalized REAL NOT NULL,"
"low_texture_fraction REAL NOT NULL,reasons TEXT NOT NULL CHECK(length(reasons)<256),"
"PRIMARY KEY(task_id,group_id));";
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) { static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
if (destination) { if (destination) {
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : ""); (void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
@ -713,7 +740,8 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 && from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 && from_version != 8 && from_version != 9 && from_version != 10 && from_version != 11 &&
from_version != 12 && from_version != 13 && from_version != 14 && from_version != 12 && from_version != 13 && from_version != 14 &&
from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18) { from_version != 15 && from_version != 16 && from_version != 17 && from_version != 18 &&
from_version != 19 && from_version != 20) {
return LARDON3D_PROJECT_DB_CORRUPT; return LARDON3D_PROJECT_DB_CORRUPT;
} }
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
@ -1126,6 +1154,15 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
"finish schema v20 migration"); "finish schema v20 migration");
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && from_version < 21) {
result = execute(database, schema_photo_quality_v21,
"migrate photo quality triage v20 to v21");
if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database,
"UPDATE metadata SET value=21 WHERE key='schema_version' AND value=20",
"finish schema v21 migration");
}
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = execute(database, "COMMIT", "commit migration"); result = execute(database, "COMMIT", "commit migration");
} }
@ -1263,7 +1300,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
"capture_selections", "capture_selections",
"asset_derivations", "asset_derivations",
"acquisition_campaign_tasks", "acquisition_campaign_tasks",
"acquisition_campaign_captures"}; "acquisition_campaign_captures",
"photo_quality_triage_tasks",
"photo_quality_triage_results"};
for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) && for (size_t index = 0; index < sizeof(required) / sizeof(required[0]) &&
result == LARDON3D_PROJECT_DB_OK; result == LARDON3D_PROJECT_DB_OK;
++index) { ++index) {
@ -1488,6 +1527,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier, const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier,
const Lardon3DProjectDbSparseSfmTask *sparse_sfm, const Lardon3DProjectDbSparseSfmTask *sparse_sfm,
const Lardon3DProjectDbAcquisitionCampaignTask *campaign, const Lardon3DProjectDbAcquisitionCampaignTask *campaign,
const Lardon3DProjectDbPhotoQualityTask *photo_quality,
int64_t updated_at) { int64_t updated_at) {
bool typed = task_kind != NULL; bool typed = task_kind != NULL;
if (!database || !valid_durable_task(snapshot, updated_at) || if (!database || !valid_durable_task(snapshot, updated_at) ||
@ -1559,6 +1599,12 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
campaign->group_count > 4096 || campaign->group_count > 4096 ||
campaign->next_group_id > campaign->group_count || !campaign->request || campaign->next_group_id > campaign->group_count || !campaign->request ||
campaign->request_size == 0 || campaign->request_size > INT_MAX)) || campaign->request_size == 0 || campaign->request_size > INT_MAX)) ||
(photo_quality &&
(!valid_task_id(photo_quality->task_id) || photo_quality->task_id != snapshot->id ||
!valid_task_id(photo_quality->scanset_id) || photo_quality->group_count == 0 ||
photo_quality->group_count > 4096 || photo_quality->next_group_id == 0 ||
photo_quality->next_group_id > photo_quality->group_count + 1u || !photo_quality->request ||
photo_quality->request_size == 0 || photo_quality->request_size > INT_MAX)) ||
(checkpoint && !valid_checkpoint(checkpoint))) { (checkpoint && !valid_checkpoint(checkpoint))) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
@ -1643,6 +1689,31 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
} }
} }
} }
if (result == LARDON3D_PROJECT_DB_OK && photo_quality) {
/* Generic checkpoints may lag the atomic business-result transaction after
a crash. A stale retry may repeat the cursor but must never move it back. */
result = prepare(
database,
"INSERT INTO photo_quality_triage_tasks(task_id,scanset_id,next_group_id,group_count,"
"request) VALUES(?1,?2,?3,?4,?5) ON CONFLICT(task_id) DO UPDATE SET "
"next_group_id=excluded.next_group_id WHERE scanset_id=excluded.scanset_id AND "
"group_count=excluded.group_count AND request=excluded.request AND "
"excluded.next_group_id>=photo_quality_triage_tasks.next_group_id",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)photo_quality->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)photo_quality->scanset_id);
sqlite3_bind_int64(statement, 3, photo_quality->next_group_id);
sqlite3_bind_int64(statement, 4, photo_quality->group_count);
sqlite3_bind_blob(statement, 5, photo_quality->request, (int)photo_quality->request_size,
SQLITE_TRANSIENT);
result = step_done(database, statement, "upsert photo quality task");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
copy_error(database->error, "Payload de triage photo immuable.");
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
}
if (result == LARDON3D_PROJECT_DB_OK) { if (result == LARDON3D_PROJECT_DB_OK) {
result = result =
prepare(database, prepare(database,
@ -1977,7 +2048,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind, Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) { 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, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task( Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
@ -1989,7 +2060,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_image_import_task(
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, updated_at); NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
@ -2000,7 +2071,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task( Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
@ -2010,7 +2081,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sift_extract_task(
const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) { const Lardon3DProjectDbSiftExtractTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) { static bool read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task) {
@ -2254,7 +2325,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_acquisition_campaign_task(
/* The generic Task/checkpoint and typed request persist atomically: recovery /* The generic Task/checkpoint and typed request persist atomically: recovery
never observes a valid campaign Task without its immutable request blob. */ never observes a valid campaign Task without its immutable request blob. */
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint,
NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters, NULL,
updated_at); updated_at);
} }
@ -2399,6 +2470,316 @@ Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture(
return result; return result;
} }
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_task(
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
const char *task_kind, uint32_t task_kind_version,
const Lardon3DProjectDbCheckpoint *checkpoint,
const Lardon3DProjectDbPhotoQualityTask *parameters, int64_t updated_at) {
if (!parameters)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
/* Generic runtime state and typed immutable request share the transaction;
recovery cannot observe one without the other. */
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, parameters,
updated_at);
}
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_task(
Lardon3DProjectDb *database, uint64_t task_id, unsigned char *request,
size_t request_capacity, Lardon3DProjectDbPhotoQualityTask *parameters) {
if (!database || !valid_task_id(task_id) || !request || !request_capacity || !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 scanset_id,next_group_id,group_count,request FROM photo_quality_triage_tasks "
"WHERE task_id=?1",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load photo quality task");
else {
bool integer_columns = sqlite3_column_type(statement, 0) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 1) == SQLITE_INTEGER &&
sqlite3_column_type(statement, 2) == SQLITE_INTEGER;
sqlite3_int64 scanset = sqlite3_column_int64(statement, 0);
sqlite3_int64 next = sqlite3_column_int64(statement, 1);
sqlite3_int64 count = sqlite3_column_int64(statement, 2);
int bytes = sqlite3_column_bytes(statement, 3);
const void *blob = sqlite3_column_blob(statement, 3);
/* SQLite exposes signed 64-bit storage. Validate identity, operational
bounds, and the one-based cursor relation before narrowing to the C API.
N+1 is the only completed cursor; zero and values beyond N+1 are corrupt. */
if (!integer_columns || sqlite3_column_type(statement, 3) != SQLITE_BLOB || scanset <= 0 ||
next < 1 || count < 1 || count > 4096 ||
next > count + 1 || bytes <= 0 ||
(size_t)bytes > request_capacity || !blob) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
memcpy(request, blob, (size_t)bytes);
parameters->task_id = task_id;
parameters->scanset_id = (uint64_t)scanset;
parameters->next_group_id = (uint32_t)next;
parameters->group_count = (uint32_t)count;
parameters->request = request;
parameters->request_size = (size_t)bytes;
}
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_quality_result(const Lardon3DProjectDbPhotoQualityResult *r) {
const Lardon3DPhotoQualityMetrics *m = r ? &r->metrics : NULL;
bool decoded_pair = m && ((m->decoded_width == 0) == (m->decoded_height == 0));
bool analysis_pair = m && ((m->analysis_width == 0) == (m->analysis_height == 0));
bool ok_dimensions = m && m->status == LARDON3D_PHOTO_QUALITY_METRIC_OK &&
m->decoded_width > 0 && m->analysis_width > 0 &&
m->decoded_width <= LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION &&
m->decoded_height <= LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION;
bool unavailable_dimensions = m && m->status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE &&
(m->decoded_width == 0 ||
m->decoded_width > LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION ||
m->decoded_height > LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION);
bool failed_dimensions = m &&
(m->status == LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT ||
m->status == LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR) &&
m->analysis_width == 0;
bool truthful_recommendation = m &&
((m->status == LARDON3D_PHOTO_QUALITY_METRIC_OK) ||
(m->status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE &&
m->recommendation == LARDON3D_PHOTO_QUALITY_SUSPECT) ||
((m->status == LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT ||
m->status == LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR) &&
m->recommendation == LARDON3D_PHOTO_QUALITY_REJECT));
return r && valid_task_id(r->task_id) && m &&
m->metrics_version == LARDON3D_PHOTO_QUALITY_METRICS_VERSION &&
m->status >= LARDON3D_PHOTO_QUALITY_METRIC_OK &&
m->status <= LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR &&
m->recommendation >= LARDON3D_PHOTO_QUALITY_GOOD &&
m->recommendation <= LARDON3D_PHOTO_QUALITY_REJECT &&
r->override_value >= LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE &&
r->override_value <= LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE &&
decoded_pair && analysis_pair &&
m->analysis_width <= LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION &&
m->analysis_height <= LARDON3D_PHOTO_QUALITY_ANALYSIS_MAX_DIMENSION &&
(ok_dimensions || unavailable_dimensions || failed_dimensions) &&
truthful_recommendation &&
bounded_text(m->reasons, sizeof(m->reasons), false) && isfinite(m->sharpness_raw) &&
isfinite(m->sharpness_normalized) && isfinite(m->clipped_black_fraction) &&
isfinite(m->clipped_white_fraction) && isfinite(m->contrast_raw) &&
isfinite(m->contrast_normalized) && isfinite(m->low_texture_fraction) &&
m->sharpness_raw >= 0.0 && m->sharpness_normalized >= 0.0 && m->contrast_raw >= 0.0 &&
m->contrast_normalized >= 0.0 && m->clipped_black_fraction >= 0.0 &&
m->clipped_black_fraction <= 1.0 && m->clipped_white_fraction >= 0.0 &&
m->clipped_white_fraction <= 1.0 && m->low_texture_fraction >= 0.0 &&
m->low_texture_fraction <= 1.0;
}
Lardon3DProjectDbResult lardon3d_project_db_record_photo_quality_result(
Lardon3DProjectDb *database, const Lardon3DProjectDbPhotoQualityResult *r,
uint32_t next_group_id) {
/* First publication requires the canonical one-based cursor at group_id and
atomically advances it to group_id+1. An exact retained replay also
converges when that advance is already durable; it may neither replace
result identity/data nor accept any other cursor. N+1 is completion. */
if (!database || !valid_quality_result(r) || r->group_id == 0 ||
r->group_id == UINT32_MAX || next_group_id != r->group_id + 1u)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin quality result");
sqlite3_stmt *statement = NULL;
if (result == LARDON3D_PROJECT_DB_OK) {
const Lardon3DPhotoQualityMetrics *m = &r->metrics;
result = prepare(database,
"INSERT INTO photo_quality_triage_results(task_id,group_id,proxy_source_index,"
"metrics_version,metric_status,recommendation,human_override,decoded_width,decoded_height,"
"analysis_width,analysis_height,sharpness_raw,sharpness_normalized,clipped_black_fraction,"
"clipped_white_fraction,contrast_raw,contrast_normalized,low_texture_fraction,reasons) "
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19) "
"ON CONFLICT(task_id,group_id) DO UPDATE SET group_id=excluded.group_id WHERE "
"proxy_source_index IS excluded.proxy_source_index AND "
"metrics_version IS excluded.metrics_version AND metric_status IS excluded.metric_status "
"AND recommendation IS excluded.recommendation AND human_override IS excluded.human_override "
"AND decoded_width IS excluded.decoded_width AND decoded_height IS excluded.decoded_height "
"AND analysis_width IS excluded.analysis_width AND analysis_height IS excluded.analysis_height "
"AND sharpness_raw IS excluded.sharpness_raw AND "
"sharpness_normalized IS excluded.sharpness_normalized AND "
"clipped_black_fraction IS excluded.clipped_black_fraction AND "
"clipped_white_fraction IS excluded.clipped_white_fraction AND "
"contrast_raw IS excluded.contrast_raw AND "
"contrast_normalized IS excluded.contrast_normalized AND "
"low_texture_fraction IS excluded.low_texture_fraction AND reasons IS excluded.reasons",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)r->task_id);
sqlite3_bind_int64(statement, 2, r->group_id);
sqlite3_bind_int64(statement, 3, r->proxy_source_index);
sqlite3_bind_int64(statement, 4, m->metrics_version);
sqlite3_bind_int(statement, 5, (int)m->status);
sqlite3_bind_int(statement, 6, (int)m->recommendation);
sqlite3_bind_int(statement, 7, (int)r->override_value);
sqlite3_bind_int64(statement, 8, m->decoded_width);
sqlite3_bind_int64(statement, 9, m->decoded_height);
sqlite3_bind_int64(statement, 10, m->analysis_width);
sqlite3_bind_int64(statement, 11, m->analysis_height);
sqlite3_bind_double(statement, 12, m->sharpness_raw);
sqlite3_bind_double(statement, 13, m->sharpness_normalized);
sqlite3_bind_double(statement, 14, m->clipped_black_fraction);
sqlite3_bind_double(statement, 15, m->clipped_white_fraction);
sqlite3_bind_double(statement, 16, m->contrast_raw);
sqlite3_bind_double(statement, 17, m->contrast_normalized);
sqlite3_bind_double(statement, 18, m->low_texture_fraction);
sqlite3_bind_text(statement, 19, m->reasons, -1, SQLITE_TRANSIENT);
result = step_done(database, statement, "insert quality result");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (result == LARDON3D_PROJECT_DB_OK) {
result = prepare(database,
"UPDATE photo_quality_triage_tasks SET next_group_id=?3 WHERE task_id=?1 AND "
"(next_group_id=?2 OR next_group_id=?3) AND group_count>=?2 AND ?3=?2+1", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)r->task_id);
sqlite3_bind_int64(statement, 2, (sqlite3_int64)r->group_id);
sqlite3_bind_int64(statement, 3, next_group_id);
result = step_done(database, statement, "advance quality cursor");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_CONSTRAINT;
}
}
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit quality result");
else (void)execute(database, "ROLLBACK", "rollback quality result");
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_load_photo_quality_result(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DProjectDbPhotoQualityResult *result_row) {
if (!database || !valid_task_id(task_id) || group_id == 0 || !result_row)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
memset(result_row, 0, sizeof(*result_row));
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"SELECT proxy_source_index,metrics_version,metric_status,recommendation,human_override,"
"decoded_width,decoded_height,analysis_width,analysis_height,sharpness_raw,"
"sharpness_normalized,clipped_black_fraction,clipped_white_fraction,contrast_raw,"
"r.contrast_normalized,r.low_texture_fraction,r.reasons,t.group_count,t.next_group_id "
"FROM photo_quality_triage_results AS r JOIN photo_quality_triage_tasks AS t "
"ON t.task_id=r.task_id WHERE r.task_id=?1 AND r.group_id=?2", &statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
sqlite3_bind_int64(statement, 2, group_id);
int code = sqlite3_step(statement);
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
else if (code != SQLITE_ROW) result = sqlite_result(database, code, "load quality result");
else {
bool integer_columns = true;
for (int column = 0; column <= 8; ++column)
integer_columns = integer_columns &&
sqlite3_column_type(statement, column) == SQLITE_INTEGER;
bool numeric_columns = true;
for (int column = 9; column <= 15; ++column) {
int type = sqlite3_column_type(statement, column);
numeric_columns = numeric_columns && (type == SQLITE_FLOAT || type == SQLITE_INTEGER);
}
sqlite3_int64 proxy = sqlite3_column_int64(statement, 0);
sqlite3_int64 version = sqlite3_column_int64(statement, 1);
sqlite3_int64 status = sqlite3_column_int64(statement, 2);
sqlite3_int64 recommendation = sqlite3_column_int64(statement, 3);
sqlite3_int64 override_value = sqlite3_column_int64(statement, 4);
sqlite3_int64 decoded_width = sqlite3_column_int64(statement, 5);
sqlite3_int64 decoded_height = sqlite3_column_int64(statement, 6);
sqlite3_int64 analysis_width = sqlite3_column_int64(statement, 7);
sqlite3_int64 analysis_height = sqlite3_column_int64(statement, 8);
/* Corrupt SQLite rows must not wrap through enum or uint32 casts. Keep
values wide until every sign/range constraint has passed. */
sqlite3_int64 group_count = sqlite3_column_int64(statement, 17);
sqlite3_int64 next_group_id = sqlite3_column_int64(statement, 18);
/* A retained result is publishable only after its canonical plan group
is durably completed. Validate the typed cursor while values are still
SQLite-width so corruption cannot wrap through public uint32 fields. */
if (!integer_columns || !numeric_columns ||
sqlite3_column_type(statement, 16) != SQLITE_TEXT || proxy < 0 || proxy > UINT32_MAX ||
sqlite3_column_type(statement, 17) != SQLITE_INTEGER ||
sqlite3_column_type(statement, 18) != SQLITE_INTEGER || group_count < 1 ||
group_count > 4096 || next_group_id < 1 || next_group_id > group_count + 1 ||
(sqlite3_int64)group_id > group_count || (sqlite3_int64)group_id >= next_group_id ||
version != LARDON3D_PHOTO_QUALITY_METRICS_VERSION ||
status < LARDON3D_PHOTO_QUALITY_METRIC_OK ||
status > LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR ||
recommendation < LARDON3D_PHOTO_QUALITY_GOOD ||
recommendation > LARDON3D_PHOTO_QUALITY_REJECT ||
override_value < LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE ||
override_value > LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE ||
decoded_width < 0 || decoded_width > UINT32_MAX || decoded_height < 0 ||
decoded_height > UINT32_MAX || analysis_width < 0 || analysis_width > UINT32_MAX ||
analysis_height < 0 || analysis_height > UINT32_MAX) {
result = LARDON3D_PROJECT_DB_CORRUPT;
} else {
result_row->task_id = task_id;
result_row->group_id = group_id;
result_row->proxy_source_index = (uint32_t)proxy;
Lardon3DPhotoQualityMetrics *m = &result_row->metrics;
m->metrics_version = (uint32_t)version;
m->status = (Lardon3DPhotoQualityMetricStatus)status;
m->recommendation = (Lardon3DPhotoQualityRecommendation)recommendation;
result_row->override_value = (Lardon3DPhotoQualityOverride)override_value;
m->decoded_width = (uint32_t)decoded_width;
m->decoded_height = (uint32_t)decoded_height;
m->analysis_width = (uint32_t)analysis_width;
m->analysis_height = (uint32_t)analysis_height;
m->sharpness_raw = sqlite3_column_double(statement, 9);
m->sharpness_normalized = sqlite3_column_double(statement, 10);
m->clipped_black_fraction = sqlite3_column_double(statement, 11);
m->clipped_white_fraction = sqlite3_column_double(statement, 12);
m->contrast_raw = sqlite3_column_double(statement, 13);
m->contrast_normalized = sqlite3_column_double(statement, 14);
m->low_texture_fraction = sqlite3_column_double(statement, 15);
const unsigned char *reasons = sqlite3_column_text(statement, 16);
if (!reasons || snprintf(m->reasons, sizeof(m->reasons), "%s", reasons) >=
(int)sizeof(m->reasons) || !valid_quality_result(result_row))
result = LARDON3D_PROJECT_DB_CORRUPT;
}
}
sqlite3_finalize(statement);
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
Lardon3DProjectDbResult lardon3d_project_db_set_photo_quality_override(
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DPhotoQualityOverride override_value) {
if (!database || !valid_task_id(task_id) || group_id == 0 || override_value < 0 ||
override_value > 2)
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
(void)pthread_mutex_lock(&database->mutex);
sqlite3_stmt *statement = NULL;
Lardon3DProjectDbResult result = prepare(database,
"UPDATE photo_quality_triage_results SET human_override=?3 WHERE task_id=?1 AND group_id=?2",
&statement);
if (result == LARDON3D_PROJECT_DB_OK) {
sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id);
sqlite3_bind_int64(statement, 2, group_id);
sqlite3_bind_int(statement, 3, (int)override_value);
result = step_done(database, statement, "set quality override");
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1)
result = LARDON3D_PROJECT_DB_NOT_FOUND;
}
(void)pthread_mutex_unlock(&database->mutex);
return result;
}
static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; } static bool valid_catalog_id(uint64_t value) { return value > 0 && value <= INT64_MAX; }
static bool valid_original_name(const char *name) { static bool valid_original_name(const char *name) {
@ -4984,7 +5365,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_visual_index_update_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at); NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task( Lardon3DProjectDbResult lardon3d_project_db_load_visual_index_update_task(
@ -5031,7 +5412,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at); NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
@ -5087,7 +5468,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_matcher_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at); NULL, NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task( Lardon3DProjectDbResult lardon3d_project_db_load_matcher_task(
@ -5145,7 +5526,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
} }
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL, return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
NULL, NULL, NULL, NULL, parameters, NULL, NULL, updated_at); NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at);
} }
Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task( Lardon3DProjectDbResult lardon3d_project_db_load_geometric_verifier_task(
@ -5218,7 +5599,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_track_builder_task(
} }
Lardon3DProjectDbResult result = record_task_internal( Lardon3DProjectDbResult result = record_task_internal(
db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, db, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, updated_at); NULL, NULL, updated_at);
if (result != LARDON3D_PROJECT_DB_OK) return result; if (result != LARDON3D_PROJECT_DB_OK) return result;
(void)pthread_mutex_lock(&db->mutex); (void)pthread_mutex_lock(&db->mutex);
result = execute(db, "BEGIN IMMEDIATE", "begin track builder task"); result = execute(db, "BEGIN IMMEDIATE", "begin track builder task");
@ -5329,7 +5710,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_sparse_sfm_task(
const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) { const Lardon3DProjectDbSparseSfmTask *parameters, int64_t updated_at) {
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL, return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL,
NULL, NULL, NULL, NULL, parameters, NULL, updated_at); NULL, NULL, NULL, NULL, parameters, NULL, NULL, updated_at);
} }
static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) { static bool read_u64_blob(sqlite3_stmt *statement, int column, uint64_t *value) {

View file

@ -3,6 +3,9 @@
#include <lardon3d/acquisition_campaign_task.h> #include <lardon3d/acquisition_campaign_task.h>
#endif #endif
#include <lardon3d/feature_task.h> #include <lardon3d/feature_task.h>
#ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE
#include <lardon3d/photo_quality_task.h>
#endif
#include <lardon3d/geometric_verifier_task.h> #include <lardon3d/geometric_verifier_task.h>
#include <lardon3d/import_task.h> #include <lardon3d/import_task.h>
#include <lardon3d/matcher_task.h> #include <lardon3d/matcher_task.h>
@ -21,6 +24,13 @@
const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) { const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
static const Lardon3DTaskKindDescriptor descriptors[] = { static const Lardon3DTaskKindDescriptor descriptors[] = {
#ifdef LARDON3D_PHOTO_QUALITY_TASK_AVAILABLE
{
.kind = LARDON3D_PHOTO_QUALITY_TASK_KIND,
.kind_version = LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION,
.reconstruct = lardon3d_photo_quality_task_reconstruct,
},
#endif
#ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE #ifdef LARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE
{ {
.kind = LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND, .kind = LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,

View file

@ -0,0 +1,418 @@
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <sys/resource.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/hardware_profile.h>
#include <lardon3d/photo_quality_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
}
namespace {
constexpr uint64_t kAnalysisWorkingBytes = UINT64_C(20) * 1024u * 1024u;
enum class Mode { kA6000, kS21 };
struct Options {
Mode mode{};
bool has_mode{};
std::filesystem::path project_dir;
std::vector<std::filesystem::path> roots;
};
/* This validation-only mirror keeps the reported admission estimate aligned
* with the v1 Task contract: retained request/context storage plus one bounded
* 20 MiB group analysis allowance. It does not own or execute Task state. */
struct PhotoQualityContextLayout {
char project_path[LARDON3D_APP_STATE_PATH_CAPACITY];
Lardon3DProjectDb *database;
Lardon3DResourceGovernor *governor;
uint64_t scanset_id;
std::vector<Lardon3DAcquisitionCampaignSource> sources;
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
std::vector<unsigned char> encoded;
Lardon3DAcquisitionCampaignPlan plan;
};
void usage(const char *program) {
std::fprintf(stderr,
"Usage: %s --mode a6000|s21 --project-dir ABSOLUTE_EMPTY_DIR "
"--root ABSOLUTE_DIR [--root ABSOLUTE_DIR ...]\n",
program);
}
bool parse_options(int argc, char **argv, Options &options) {
for (int i = 1; i < argc; ++i) {
const std::string argument(argv[i]);
if ((argument == "--mode" || argument == "--project-dir" || argument == "--root") &&
i + 1 >= argc)
return false;
if (argument == "--mode") {
const std::string value(argv[++i]);
if (value == "a6000") options.mode = Mode::kA6000;
else if (value == "s21") options.mode = Mode::kS21;
else return false;
options.has_mode = true;
} else if (argument == "--project-dir") {
options.project_dir = argv[++i];
} else if (argument == "--root") {
options.roots.emplace_back(argv[++i]);
} else if (argument == "--help") {
usage(argv[0]);
std::exit(0);
} else {
return false;
}
}
return options.has_mode && !options.project_dir.empty() && !options.roots.empty() &&
options.roots.size() <= LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS;
}
bool canonical_directory(const std::filesystem::path &input, std::filesystem::path &output) {
std::error_code error;
if (!input.is_absolute()) return false;
output = std::filesystem::canonical(input, error);
return !error && std::filesystem::is_directory(output, error) && !error &&
output.string().size() < LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY;
}
bool prepare_project_directory(const std::filesystem::path &input,
std::filesystem::path &output) {
if (!input.is_absolute()) return false;
output = input.lexically_normal();
if (output != input || output.string().size() >= LARDON3D_APP_STATE_PATH_CAPACITY) return false;
std::error_code error;
if (std::filesystem::exists(output, error)) {
if (error || !std::filesystem::is_directory(output, error) || error ||
!std::filesystem::is_empty(output, error) || error)
return false;
} else if (!std::filesystem::create_directories(output, error) || error) {
return false;
}
return std::filesystem::create_directories(output / ".lardon3d" / "checkpoints", error) &&
!error;
}
const char *mode_name(Mode mode) { return mode == Mode::kA6000 ? "a6000" : "s21"; }
std::string stem(const char *path) {
const char *name = std::strrchr(path, '/');
name = name == nullptr ? path : name + 1;
const char *dot = std::strrchr(name, '.');
return std::string(name, dot == nullptr ? std::strlen(name) : static_cast<size_t>(dot - name));
}
bool build_confirmations(Mode mode, const Lardon3DAcquisitionCampaignDiscovery &discovery,
std::vector<Lardon3DAcquisitionCampaignConfirmation> &confirmations) {
if (mode == Mode::kS21) {
confirmations.reserve(discovery.source_count);
for (size_t i = 0; i < discovery.source_count; ++i) {
if (discovery.sources[i].source_kind != LARDON3D_ACQUISITION_SOURCE_JPEG) return false;
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 1;
confirmation.source_indices[0] = i;
confirmations.push_back(confirmation);
}
return true;
}
struct Pair { size_t raw = SIZE_MAX; size_t jpeg = SIZE_MAX; };
std::map<std::string, Pair> pairs;
for (size_t i = 0; i < discovery.source_count; ++i) {
Pair &pair = pairs[stem(discovery.sources[i].path)];
if (discovery.sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_RAW) {
if (pair.raw != SIZE_MAX) return false;
pair.raw = i;
} else if (discovery.sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG) {
if (pair.jpeg != SIZE_MAX) return false;
pair.jpeg = i;
} else {
return false;
}
}
confirmations.reserve(pairs.size());
for (const auto &entry : pairs) {
if (entry.second.raw == SIZE_MAX || entry.second.jpeg == SIZE_MAX) return false;
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 2;
confirmation.source_indices[0] = entry.second.raw;
confirmation.source_indices[1] = entry.second.jpeg;
confirmations.push_back(confirmation);
}
return confirmations.size() * 2u == discovery.source_count;
}
void json_string(const char *text) {
std::putchar('"');
for (const unsigned char *p = reinterpret_cast<const unsigned char *>(text); *p; ++p) {
switch (*p) {
case '"': std::fputs("\\\"", stdout); break;
case '\\': std::fputs("\\\\", stdout); break;
case '\b': std::fputs("\\b", stdout); break;
case '\f': std::fputs("\\f", stdout); break;
case '\n': std::fputs("\\n", stdout); break;
case '\r': std::fputs("\\r", stdout); break;
case '\t': std::fputs("\\t", stdout); break;
default:
if (*p < 0x20u) std::printf("\\u%04x", static_cast<unsigned>(*p));
else std::putchar(*p);
}
}
std::putchar('"');
}
const char *status_name(Lardon3DPhotoQualityMetricStatus status) {
switch (status) {
case LARDON3D_PHOTO_QUALITY_METRIC_OK: return "OK";
case LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE: return "UNAVAILABLE";
case LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT: return "INVALID_INPUT";
case LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR: return "DECODE_ERROR";
}
return "UNKNOWN";
}
const char *recommendation_name(Lardon3DPhotoQualityRecommendation recommendation) {
switch (recommendation) {
case LARDON3D_PHOTO_QUALITY_GOOD: return "GOOD";
case LARDON3D_PHOTO_QUALITY_SUSPECT: return "SUSPECT";
case LARDON3D_PHOTO_QUALITY_REJECT: return "REJECT";
}
return "NONE";
}
bool wait_terminal(Lardon3DTaskQueue *queue, Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DTaskSnapshot &snapshot, Lardon3DProjectDbTask &durable) {
for (;;) {
if (!lardon3d_task_queue_get(queue, task_id, &snapshot)) return false;
if (snapshot.state == TASK_COMPLETED || snapshot.state == TASK_FAILED ||
snapshot.state == TASK_CANCELLED) {
/* Queue terminal state precedes the finished callback. Report only after
* that callback has durably published the matching generic Task state and
* sequence count, so validation never samples an intermediate row. */
if (lardon3d_project_db_load_task(database, task_id, &durable) != LARDON3D_PROJECT_DB_OK)
return false;
if (durable.saved_state == snapshot.state) return true;
}
usleep(100000);
}
}
void emit_group(const Lardon3DAcquisitionCampaignDiscovery &discovery,
const Lardon3DAcquisitionCampaignGroup &group,
const Lardon3DProjectDbPhotoQualityResult &result) {
std::printf("{\"record\":\"group\",\"group_id\":%u,\"basis\":%u,\"source_indices\":[",
result.group_id, static_cast<unsigned>(group.basis));
for (size_t i = 0; i < group.source_count; ++i)
std::printf("%s%zu", i == 0 ? "" : ",", group.source_indices[i]);
std::fputs("],\"source_paths\":[", stdout);
for (size_t i = 0; i < group.source_count; ++i) {
if (i != 0) std::putchar(',');
json_string(discovery.sources[group.source_indices[i]].path);
}
std::fputs("],\"proxy_source_index\":", stdout);
if (result.proxy_source_index == UINT32_MAX) std::fputs("null,\"proxy_path\":null", stdout);
else {
std::printf("%u,\"proxy_path\":", result.proxy_source_index);
json_string(discovery.sources[result.proxy_source_index].path);
}
std::printf(",\"status\":\"%s\",\"recommendation\":\"%s\",\"reasons\":",
status_name(result.metrics.status),
recommendation_name(result.metrics.recommendation));
json_string(result.metrics.reasons);
std::printf(",\"decoded_width\":%u,\"decoded_height\":%u,\"analysis_width\":%u,"
"\"analysis_height\":%u,\"sharpness_raw\":%.17g,"
"\"sharpness_normalized\":%.17g,\"clipped_black_fraction\":%.17g,"
"\"clipped_white_fraction\":%.17g,\"contrast_raw\":%.17g,"
"\"contrast_normalized\":%.17g,\"low_texture_fraction\":%.17g}\n",
result.metrics.decoded_width, result.metrics.decoded_height,
result.metrics.analysis_width, result.metrics.analysis_height,
result.metrics.sharpness_raw, result.metrics.sharpness_normalized,
result.metrics.clipped_black_fraction, result.metrics.clipped_white_fraction,
result.metrics.contrast_raw, result.metrics.contrast_normalized,
result.metrics.low_texture_fraction);
}
} // namespace
extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
return nullptr;
}
int main(int argc, char **argv) {
Options options;
if (!parse_options(argc, argv, options)) { usage(argv[0]); return 2; }
std::filesystem::path project_dir;
if (!prepare_project_directory(options.project_dir, project_dir)) {
std::fprintf(stderr, "project directory must be an absolute, normalized empty directory\n");
return 2;
}
std::vector<Lardon3DAcquisitionCampaignRoot> roots(options.roots.size());
for (size_t i = 0; i < options.roots.size(); ++i) {
std::filesystem::path canonical;
if (!canonical_directory(options.roots[i], canonical)) {
std::fprintf(stderr, "root must resolve to a bounded directory: %s\n",
options.roots[i].c_str());
return 2;
}
const std::string path = canonical.string();
std::memcpy(roots[i].path, path.c_str(), path.size() + 1u);
}
auto discovery = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
const auto discovery_result =
lardon3d_acquisition_campaign_discover(roots.data(), roots.size(), discovery.get());
if (discovery_result != LARDON3D_ACQUISITION_CAMPAIGN_OK || discovery->source_count == 0) {
std::fprintf(stderr, "acquisition discovery failed: %u\n",
static_cast<unsigned>(discovery_result));
return 1;
}
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
if (!build_confirmations(options.mode, *discovery, confirmations)) {
std::fprintf(stderr, "%s source set does not satisfy the required exact grouping shape\n",
mode_name(options.mode));
return 1;
}
auto plan = std::make_unique<Lardon3DAcquisitionCampaignPlan>();
if (lardon3d_acquisition_campaign_plan(discovery->sources, discovery->source_count,
confirmations.data(), confirmations.size(), plan.get()) !=
LARDON3D_ACQUISITION_CAMPAIGN_OK ||
plan->group_count != confirmations.size() ||
plan->summary.explicit_group_count != confirmations.size()) {
std::fprintf(stderr, "explicit campaign planning failed\n");
return 1;
}
const std::string database_path = (project_dir / "project.lardon3d").string();
char database_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
Lardon3DProjectDb *database = nullptr;
if (lardon3d_project_db_open(database_path.c_str(), &database, database_error) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "project database open failed: %s\n", database_error);
return 1;
}
Lardon3DProjectDbScanSet scanset{};
if (lardon3d_project_db_create_scanset(database, mode_name(options.mode), &scanset) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "ScanSet creation failed\n");
lardon3d_project_db_close(database);
return 1;
}
Lardon3DHardwareProfile profile{};
char hardware_error[256]{};
Lardon3DResourcePolicy policy{};
if (!lardon3d_hardware_profile_detect(&profile, hardware_error, sizeof(hardware_error)) ||
!lardon3d_resource_policy_default(&profile, &policy)) {
std::fprintf(stderr, "hardware/resource policy detection failed: %s\n", hardware_error);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create(&profile, &policy);
Lardon3DTaskQueue *queue = governor ? lardon3d_task_queue_create(governor, 2) : nullptr;
if (!queue) {
std::fprintf(stderr, "Queue/Governor creation failed\n");
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = database;
state.resource_governor = governor;
state.task_queue = queue;
state.hardware_profile = profile;
const std::string project_path = project_dir.string();
std::memcpy(state.project_path, project_path.c_str(), project_path.size() + 1u);
Lardon3DPhotoQualityTaskRequest request{discovery->sources, discovery->source_count,
confirmations.data(), confirmations.size()};
size_t encoded_size = 0;
if (!lardon3d_photo_quality_request_encode(&request, nullptr, 0, &encoded_size)) {
std::fprintf(stderr, "request size calculation failed\n");
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
const uint64_t task_memory_estimate =
kAnalysisWorkingBytes + sizeof(PhotoQualityContextLayout) +
discovery->source_count * sizeof(Lardon3DAcquisitionCampaignSource) +
confirmations.size() * sizeof(Lardon3DAcquisitionCampaignConfirmation) + encoded_size;
const auto started = std::chrono::steady_clock::now();
uint64_t task_id = 0;
if (!lardon3d_project_enqueue_photo_quality(&state, scanset.scanset_id, &request, &task_id)) {
std::fprintf(stderr, "photo_quality.triage enqueue failed\n");
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DTaskSnapshot terminal{};
Lardon3DProjectDbTask durable_task{};
if (!wait_terminal(queue, database, task_id, terminal, durable_task) ||
terminal.state != TASK_COMPLETED) {
std::fprintf(stderr, "photo_quality.triage ended in %s: %s\n",
lardon3d_task_state_name(terminal.state), terminal.message);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
size_t good = 0, suspect = 0, reject = 0, unavailable = 0, metric_errors = 0;
for (uint32_t group_index = 0; group_index < plan->group_count; ++group_index) {
const uint32_t group_id = plan->groups[group_index].group_id;
Lardon3DProjectDbPhotoQualityResult result{};
if (lardon3d_project_db_load_photo_quality_result(database, task_id, group_id, &result) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "missing durable result for group %u\n", group_id);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
if (result.metrics.status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE) ++unavailable;
else if (result.metrics.status != LARDON3D_PHOTO_QUALITY_METRIC_OK) ++metric_errors;
if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_GOOD) ++good;
else if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_SUSPECT) ++suspect;
else if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_REJECT) ++reject;
emit_group(*discovery, plan->groups[group_index], result);
}
struct rusage usage{};
const bool loaded_usage = getrusage(RUSAGE_SELF, &usage) == 0;
const double elapsed =
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
std::printf("{\"record\":\"summary\",\"mode\":\"%s\",\"task_id\":%llu,"
"\"sources\":%zu,\"groups\":%zu,\"good\":%zu,\"suspect\":%zu,"
"\"reject\":%zu,\"unavailable\":%zu,\"metric_errors\":%zu,"
"\"discovered_entries\":%zu,\"unsupported_entries\":%zu,"
"\"metadata_ok\":%zu,\"metadata_errors\":%zu,"
"\"request_encoded_bytes\":%zu,\"task_memory_estimate_bytes\":%llu,"
"\"sequence_count\":%u,\"elapsed_seconds\":%.6f,\"peak_rss_kib\":%ld}\n",
mode_name(options.mode), static_cast<unsigned long long>(task_id),
discovery->source_count, plan->group_count, good, suspect, reject, unavailable,
metric_errors, discovery->summary.discovered_entry_count,
discovery->summary.unsupported_entry_count, discovery->summary.metadata_ok_count,
discovery->summary.metadata_error_count, encoded_size,
static_cast<unsigned long long>(task_memory_estimate),
durable_task.sequence_count, elapsed,
loaded_usage ? usage.ru_maxrss : -1L);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return loaded_usage ? 0 : 1;
}

View file

@ -0,0 +1,350 @@
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/acquisition_campaign.h>
#include <lardon3d/photo_quality_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
}
namespace {
constexpr size_t kGroupsPerCamera = 128;
struct TaskIds {
uint64_t a6000{};
uint64_t s21{};
};
struct Campaign {
std::vector<Lardon3DAcquisitionCampaignSource> sources;
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
};
bool setup_runtime(Lardon3DAppState &state) {
state.hardware_profile.logical_cpu_count = 8;
state.hardware_profile.page_size_bytes = 4096;
state.hardware_profile.memory_total_bytes = UINT64_C(8) << 30;
std::snprintf(state.hardware_profile.cpu_architecture,
sizeof(state.hardware_profile.cpu_architecture), "restart-validation");
Lardon3DResourcePolicy policy{};
policy.maximum_cpu_load_ratio = 1.0;
policy.maximum_io_pressure_avg10 = 100.0;
policy.io_slot_capacity = 2;
state.resource_governor =
lardon3d_resource_governor_create(&state.hardware_profile, &policy);
state.task_queue = state.resource_governor
? lardon3d_task_queue_create(state.resource_governor, 4)
: nullptr;
return state.task_queue != nullptr;
}
std::string stem(const char *path) {
const char *name = std::strrchr(path, '/');
name = name ? name + 1 : path;
const char *dot = std::strrchr(name, '.');
return std::string(name, dot ? static_cast<size_t>(dot - name) : std::strlen(name));
}
bool root(const char *path, Lardon3DAcquisitionCampaignRoot &result) {
std::error_code error;
const auto canonical = std::filesystem::canonical(path, error);
if (error || !std::filesystem::is_directory(canonical, error) || error) return false;
const std::string value = canonical.string();
if (value.size() >= sizeof(result.path)) return false;
std::memcpy(result.path, value.c_str(), value.size() + 1u);
return true;
}
bool discover_a6000(const char *raw, const char *jpeg, Campaign &campaign) {
Lardon3DAcquisitionCampaignRoot roots[2]{};
if (!root(raw, roots[0]) || !root(jpeg, roots[1])) return false;
auto discovery = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
if (lardon3d_acquisition_campaign_discover(roots, 2, discovery.get()) !=
LARDON3D_ACQUISITION_CAMPAIGN_OK)
return false;
struct Pair { size_t raw = SIZE_MAX; size_t jpeg = SIZE_MAX; };
std::map<std::string, Pair> pairs;
for (size_t i = 0; i < discovery->source_count; ++i) {
Pair &pair = pairs[stem(discovery->sources[i].path)];
if (discovery->sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_RAW)
pair.raw = i;
else if (discovery->sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG)
pair.jpeg = i;
}
std::vector<Pair> selected;
for (const auto &entry : pairs) {
if (entry.second.raw == SIZE_MAX || entry.second.jpeg == SIZE_MAX) continue;
selected.push_back(entry.second);
if (selected.size() == kGroupsPerCamera) break;
}
if (selected.size() != kGroupsPerCamera) return false;
std::vector<size_t> indices;
indices.reserve(selected.size() * 2u);
for (const Pair &pair : selected) {
indices.push_back(pair.raw);
indices.push_back(pair.jpeg);
}
std::sort(indices.begin(), indices.end());
std::map<size_t, size_t> remap;
for (size_t old_index : indices) {
remap.emplace(old_index, campaign.sources.size());
campaign.sources.push_back(discovery->sources[old_index]);
}
for (const Pair &pair : selected) {
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 2;
confirmation.source_indices[0] = remap[pair.raw];
confirmation.source_indices[1] = remap[pair.jpeg];
campaign.confirmations.push_back(confirmation);
}
return campaign.confirmations.size() == kGroupsPerCamera;
}
bool discover_s21(const char *directory, Campaign &campaign) {
Lardon3DAcquisitionCampaignRoot source_root{};
if (!root(directory, source_root)) return false;
auto discovery = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
if (lardon3d_acquisition_campaign_discover(&source_root, 1, discovery.get()) !=
LARDON3D_ACQUISITION_CAMPAIGN_OK)
return false;
for (size_t i = 0; i < discovery->source_count &&
campaign.confirmations.size() < kGroupsPerCamera; ++i) {
if (discovery->sources[i].source_kind != LARDON3D_ACQUISITION_SOURCE_JPEG) continue;
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 1;
confirmation.source_indices[0] = campaign.sources.size();
campaign.sources.push_back(discovery->sources[i]);
campaign.confirmations.push_back(confirmation);
}
return campaign.confirmations.size() == kGroupsPerCamera;
}
bool wait_cursors_then_pause(Lardon3DAppState &state, uint64_t first_id, uint64_t second_id) {
std::vector<unsigned char> first_request(LARDON3D_PHOTO_QUALITY_TASK_REQUEST_MAX_BYTES);
bool first_requested = false;
for (size_t attempt = 0; attempt < 10000; ++attempt) {
Lardon3DProjectDbPhotoQualityTask first{};
if (!first_requested &&
lardon3d_project_db_load_photo_quality_task(
state.project_db, first_id, first_request.data(), first_request.size(), &first) ==
LARDON3D_PROJECT_DB_OK && first.next_group_id >= 2 &&
first.next_group_id <= first.group_count) {
first_requested = lardon3d_task_queue_pause(state.task_queue, first_id);
}
if (first_requested) {
Lardon3DTaskSnapshot first_snapshot{}, second_snapshot{};
if (lardon3d_task_queue_get(state.task_queue, first_id, &first_snapshot) &&
lardon3d_task_queue_get(state.task_queue, second_id, &second_snapshot) &&
first_snapshot.state == TASK_PAUSED && second_snapshot.state == TASK_PENDING)
return true;
}
usleep(1000);
}
Lardon3DTaskSnapshot first_snapshot{}, second_snapshot{};
Lardon3DProjectDbPhotoQualityTask first{};
(void)lardon3d_task_queue_get(state.task_queue, first_id, &first_snapshot);
(void)lardon3d_task_queue_get(state.task_queue, second_id, &second_snapshot);
(void)lardon3d_project_db_load_photo_quality_task(
state.project_db, first_id, first_request.data(), first_request.size(), &first);
std::fprintf(stderr,
"pause timeout: A state=%s cursor=%u requested=%d message=%s; "
"S state=%s message=%s\n",
lardon3d_task_state_name(first_snapshot.state), first.next_group_id,
first_requested, first_snapshot.message,
lardon3d_task_state_name(second_snapshot.state), second_snapshot.message);
return false;
}
bool wait_completed(Lardon3DAppState &state, uint64_t task_id) {
for (size_t attempt = 0; attempt < 180000; ++attempt) {
Lardon3DTaskSnapshot snapshot{};
if (lardon3d_task_queue_get(state.task_queue, task_id, &snapshot)) {
if (snapshot.state == TASK_COMPLETED) return true;
if (snapshot.state == TASK_FAILED || snapshot.state == TASK_CANCELLED) return false;
}
usleep(10000);
}
return false;
}
bool enqueue_campaign(Lardon3DAppState &state, uint64_t scanset_id,
const Campaign &campaign, uint64_t &task_id) {
Lardon3DPhotoQualityTaskRequest request{
campaign.sources.data(), campaign.sources.size(), campaign.confirmations.data(),
campaign.confirmations.size()};
Lardon3DAcquisitionCampaignPlan plan{};
if (lardon3d_acquisition_campaign_plan(
request.sources, request.source_count, request.confirmations,
request.confirmation_count, &plan) != LARDON3D_ACQUISITION_CAMPAIGN_OK) {
std::fprintf(stderr, "quality request planning failed\n");
return false;
}
size_t encoded_size = 0;
if (!lardon3d_photo_quality_request_encode(&request, nullptr, 0, &encoded_size)) {
std::fprintf(stderr, "quality request encoding probe failed\n");
return false;
}
Lardon3DTask *task =
lardon3d_project_create_photo_quality_task(&state, scanset_id, &request, &task_id);
if (!task) {
std::fprintf(stderr, "quality durable Task creation failed: encoded=%zu\n", encoded_size);
return false;
}
if (!lardon3d_task_queue_add(state.task_queue, task, nullptr)) {
std::fprintf(stderr, "quality Queue transfer failed\n");
lardon3d_task_destroy(task);
return false;
}
return true;
}
int child_run(int output, const Campaign &a6000, const Campaign &s21) {
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
if (!setup_runtime(state) || !lardon3d_project_create(&state, "Quality Restart")) return 10;
Lardon3DProjectDbScanSet a6000_scanset{}, s21_scanset{};
if (lardon3d_project_db_create_scanset(state.project_db, "A6000", &a6000_scanset) !=
LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_create_scanset(state.project_db, "S21 FE", &s21_scanset) !=
LARDON3D_PROJECT_DB_OK)
return 11;
TaskIds ids{};
if (!enqueue_campaign(state, a6000_scanset.scanset_id, a6000, ids.a6000)) {
std::fprintf(stderr, "A6000 quality enqueue failed: %s\n", state.status_message);
return 12;
}
if (!enqueue_campaign(state, s21_scanset.scanset_id, s21, ids.s21)) {
std::fprintf(stderr, "S21 quality enqueue failed: %s\n", state.status_message);
return 12;
}
if (!wait_cursors_then_pause(state, ids.a6000, ids.s21))
return 13;
/* Overrides are operator decisions independent of measured metrics. They are
* intentionally durable before this validation process dies. */
if (lardon3d_project_db_set_photo_quality_override(
state.project_db, ids.a6000, 1, LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE) !=
LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_set_photo_quality_override(
state.project_db, ids.a6000, 2, LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE) !=
LARDON3D_PROJECT_DB_OK)
return 14;
if (write(output, &ids, sizeof(ids)) != static_cast<ssize_t>(sizeof(ids))) return 15;
/* Deliberately bypass Task/Queue destruction. The parent must recover only
* from production checkpoints and typed cursors, as after process loss. */
return 0;
}
bool verify_override(Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
Lardon3DPhotoQualityOverride expected) {
Lardon3DProjectDbPhotoQualityResult result{};
return lardon3d_project_db_load_photo_quality_result(database, task_id, group_id, &result) ==
LARDON3D_PROJECT_DB_OK &&
/* Overrides are valid for every measured recommendation, including the
* deliberate non-GOOD A6000 target used by this restart smoke test. */
result.override_value == expected;
}
} // namespace
extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
static const Lardon3DTaskKindDescriptor descriptors[] = {{
LARDON3D_PHOTO_QUALITY_TASK_KIND, LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION,
lardon3d_photo_quality_task_reconstruct}};
static const Lardon3DTaskKindRegistry registry{descriptors, 1};
return &registry;
}
int main(int argc, char **argv) {
if (argc != 4) {
std::fprintf(stderr, "Usage: %s A6000_RAW_DIR A6000_JPEG_DIR S21_DIR\n", argv[0]);
return 2;
}
Campaign a6000, s21;
if (!discover_a6000(argv[1], argv[2], a6000) || !discover_s21(argv[3], s21)) {
std::fprintf(stderr, "real corpus discovery or bounded grouping failed\n");
return 1;
}
char project_root[] = "/tmp/lardon3d-quality-restart-XXXXXX";
if (!mkdtemp(project_root) || setenv("LARDON3D_PROJECTS_ROOT", project_root, 1) != 0)
return 1;
int descriptors[2]{};
if (pipe(descriptors) != 0) return 1;
const pid_t child = fork();
if (child < 0) return 1;
if (child == 0) {
close(descriptors[0]);
const int result = child_run(descriptors[1], a6000, s21);
close(descriptors[1]);
_exit(result);
}
close(descriptors[1]);
TaskIds ids{};
const ssize_t read_size = read(descriptors[0], &ids, sizeof(ids));
close(descriptors[0]);
int status = 0;
if (waitpid(child, &status, 0) != child || !WIFEXITED(status) || WEXITSTATUS(status) != 0 ||
read_size != static_cast<ssize_t>(sizeof(ids))) {
std::fprintf(stderr, "restart fixture child failed: status=%d read=%zd\n", status, read_size);
return 1;
}
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
if (!setup_runtime(state) || !lardon3d_project_open(&state, "Quality Restart")) return 1;
Lardon3DProjectRecoverySummary recovery{};
if (!lardon3d_project_last_recovery_summary(&state, &recovery) || recovery.inspected != 2 ||
recovery.resumed != 2 || recovery.skipped != 0 || recovery.failed != 0 ||
!wait_completed(state, ids.a6000) || !wait_completed(state, ids.s21))
return 1;
if (lardon3d_project_db_set_photo_quality_override(
state.project_db, ids.s21, 1, LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE) !=
LARDON3D_PROJECT_DB_OK ||
lardon3d_project_db_set_photo_quality_override(
state.project_db, ids.s21, 2, LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE) !=
LARDON3D_PROJECT_DB_OK)
return 1;
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = nullptr;
lardon3d_project_close(&state);
state.task_queue = lardon3d_task_queue_create(state.resource_governor, 4);
if (!state.task_queue || !lardon3d_project_open(&state, "Quality Restart")) return 1;
Lardon3DProjectRecoverySummary completed_reopen{};
if (!lardon3d_project_last_recovery_summary(&state, &completed_reopen) ||
completed_reopen.inspected != 0 || completed_reopen.resumed != 0)
return 1;
if (!verify_override(state.project_db, ids.a6000, 1,
LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE) ||
!verify_override(state.project_db, ids.a6000, 2,
LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE) ||
!verify_override(state.project_db, ids.s21, 1,
LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE) ||
!verify_override(state.project_db, ids.s21, 2,
LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE) ||
lardon3d_resource_governor_reservation_count(state.resource_governor) != 0)
return 1;
std::printf("QUALITY_RESTART_RESULT=PASS A6000_GROUPS=%zu S21_GROUPS=%zu "
"RECOVERY_INSPECTED=%zu RECOVERY_RESUMED=%zu INCLUDE_EXCLUDE=PERSISTED\n",
a6000.confirmations.size(), s21.confirmations.size(), recovery.inspected,
recovery.resumed);
lardon3d_task_queue_destroy(state.task_queue);
state.task_queue = nullptr;
lardon3d_project_close(&state);
lardon3d_resource_governor_destroy(state.resource_governor);
std::error_code error;
std::filesystem::remove_all(project_root, error);
unsetenv("LARDON3D_PROJECTS_ROOT");
return error ? 1 : 0;
}

View file

@ -202,7 +202,7 @@ static bool test_model_api(const char *path) {
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(path, &database, error) == CHECK(lardon3d_project_db_open(path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
Parents parents; Parents parents;
CHECK(create_parents(database, &parents)); CHECK(create_parents(database, &parents));
@ -486,7 +486,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
0)); 0));
CHECK(lardon3d_project_db_open(v11_path, &database, error) == CHECK(lardon3d_project_db_open(v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
Parents parents; Parents parents;
CHECK(create_parents(database, &parents)); CHECK(create_parents(database, &parents));
unsigned char fingerprint[32] = {0x91}; unsigned char fingerprint[32] = {0x91};
@ -501,7 +501,8 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
migrated_result.geometric_verification_result_id; migrated_result.geometric_verification_result_id;
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
CHECK(query_integer( CHECK(query_integer(
v11_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v11_path, "SELECT value FROM metadata WHERE key='schema_version'",
LARDON3D_PROJECT_DB_SCHEMA_VERSION));
CHECK( CHECK(
query_integer(v11_path, query_integer(v11_path,
"SELECT count(*) FROM sqlite_master WHERE type='index' AND " "SELECT count(*) FROM sqlite_master WHERE type='index' AND "
@ -530,7 +531,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_path, &database, error) == CHECK(lardon3d_project_db_open(failed_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
return true; return true;
} }

View file

@ -369,7 +369,7 @@ static bool run_task_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
CHECK(remove_tree(root)); CHECK(remove_tree(root));
return true; return true;

View file

@ -124,7 +124,7 @@ static bool run_test(void) {
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 20); CHECK(database && lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
Lardon3DProjectDbScanSet scanset; Lardon3DProjectDbScanSet scanset;
CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) ==
@ -501,7 +501,7 @@ static bool run_test(void) {
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
/* Verify persistence: load previously created results */ /* Verify persistence: load previously created results */
CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) ==
@ -544,11 +544,12 @@ static bool run_test(void) {
v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 9));
CHECK(lardon3d_project_db_open(v9_path, &database, error) == CHECK(lardon3d_project_db_open(v9_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v9_path, "SELECT value FROM metadata WHERE key='schema_version'",
LARDON3D_PROJECT_DB_SCHEMA_VERSION));
CHECK( CHECK(
query_integer(v9_path, query_integer(v9_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -576,7 +577,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;

View file

@ -373,7 +373,8 @@ static bool run_test(void) {
"name='matcher_tasks'", "name='matcher_tasks'",
0)); 0));
CHECK(reopen_runtime(&fixture)); CHECK(reopen_runtime(&fixture));
CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) == 20); CHECK(lardon3d_project_db_schema_version(fixture.state.project_db) ==
LARDON3D_PROJECT_DB_SCHEMA_VERSION);
CHECK( CHECK(
query_integer(database_path, query_integer(database_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "

View file

@ -0,0 +1,166 @@
#include <lardon3d/photo_quality.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, \
#condition); \
return 1; \
} \
} while (0)
static cv::Mat checker(int size) {
cv::Mat image(size, size, CV_8UC1);
for (int y = 0; y < size; ++y)
for (int x = 0; x < size; ++x)
image.at<unsigned char>(y, x) = ((x / (size / 16)) + (y / (size / 16))) % 2 ? 220 : 30;
return image;
}
int main() {
char directory_template[] = "/tmp/lardon3d-photo-quality-XXXXXX";
char *directory = mkdtemp(directory_template);
CHECK(directory != nullptr);
const std::string sharp_path = std::string(directory) + "/sharp.jpg";
const std::string blur_path = std::string(directory) + "/blur.jpg";
const std::string large_path = std::string(directory) + "/large.jpg";
const std::string white_path = std::string(directory) + "/white.jpg";
const std::string oversized_path = std::string(directory) + "/oversized.jpg";
const std::string truncated_oversized_path =
std::string(directory) + "/truncated-oversized.jpg";
const std::string over_budget_oversized_path =
std::string(directory) + "/over-budget-oversized.jpg";
const std::string malformed_path = std::string(directory) + "/malformed.jpg";
cv::Mat sharp = checker(512);
cv::Mat blurred;
cv::GaussianBlur(sharp, blurred, cv::Size(31, 31), 8.0);
cv::Mat large;
cv::resize(sharp, large, cv::Size(1024, 1024), 0.0, 0.0, cv::INTER_NEAREST);
cv::Mat white(512, 512, CV_8UC1, cv::Scalar(255));
CHECK(cv::imwrite(sharp_path, sharp));
CHECK(cv::imwrite(blur_path, blurred));
CHECK(cv::imwrite(large_path, large));
CHECK(cv::imwrite(white_path, white));
cv::Mat oversized(2, LARDON3D_PHOTO_QUALITY_JPEG_MAX_DIMENSION + 1, CV_8UC1,
cv::Scalar(127));
CHECK(cv::imwrite(oversized_path, oversized));
{
std::FILE *over_budget = std::fopen(over_budget_oversized_path.c_str(), "wb");
CHECK(over_budget != nullptr);
const unsigned char header[] = {
0xff, 0xd8, // SOI
0xff, 0xc0, 0x00, 0x08, 0x08, 0x00, 0x02, 0x20, 0x01, 0x00,
0xff, 0xda, 0x00, 0x02 // SOS with an empty synthetic header.
};
CHECK(std::fwrite(header, 1, sizeof(header), over_budget) == sizeof(header));
unsigned char entropy[64 * 1024];
std::memset(entropy, 0x11, sizeof(entropy));
/* The validator must reject finite-but-excessive structural work without
* allocating the oversized raster or treating the parser cap as science. */
for (size_t i = 0; i < 1025; ++i)
CHECK(std::fwrite(entropy, 1, sizeof(entropy), over_budget) == sizeof(entropy));
CHECK(std::fclose(over_budget) == 0);
}
{
std::FILE *source = std::fopen(oversized_path.c_str(), "rb");
std::FILE *truncated = std::fopen(truncated_oversized_path.c_str(), "wb");
CHECK(source != nullptr && truncated != nullptr);
unsigned char bytes[128];
const size_t count = std::fread(bytes, 1, sizeof(bytes), source);
CHECK(count > 16 && std::fwrite(bytes, 1, count, truncated) == count);
CHECK(std::fclose(source) == 0 && std::fclose(truncated) == 0);
}
{
std::FILE *malformed = std::fopen(malformed_path.c_str(), "wb");
CHECK(malformed != nullptr);
CHECK(std::fwrite("not-a-jpeg", 1, 10, malformed) == 10);
CHECK(std::fclose(malformed) == 0);
}
Lardon3DPhotoQualityMetrics sharp_metrics{};
Lardon3DPhotoQualityMetrics blur_metrics{};
Lardon3DPhotoQualityMetrics large_metrics{};
Lardon3DPhotoQualityMetrics white_metrics{};
CHECK(lardon3d_photo_quality_analyze_jpeg(sharp_path.c_str(), &sharp_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(lardon3d_photo_quality_analyze_jpeg(blur_path.c_str(), &blur_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(sharp_metrics.sharpness_normalized > blur_metrics.sharpness_normalized * 4.0);
CHECK(lardon3d_photo_quality_analyze_jpeg(white_path.c_str(), &white_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(white_metrics.clipped_white_fraction > 0.99);
CHECK(white_metrics.recommendation == LARDON3D_PHOTO_QUALITY_REJECT);
CHECK(lardon3d_photo_quality_analyze_jpeg(large_path.c_str(), &large_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
const double scale_ratio = sharp_metrics.sharpness_normalized /
large_metrics.sharpness_normalized;
CHECK(scale_ratio > 0.8 && scale_ratio < 1.25);
/* An oversized SOF becomes pending evidence before OpenCV can allocate its
* raster. The operational ceiling is neither a decode error nor rejection. */
Lardon3DPhotoQualityMetrics oversized_metrics{};
CHECK(lardon3d_photo_quality_analyze_jpeg(oversized_path.c_str(), &oversized_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE);
CHECK(oversized_metrics.recommendation == LARDON3D_PHOTO_QUALITY_SUSPECT);
CHECK(std::strstr(oversized_metrics.reasons, "DIMENSIONS_EXCEED") != nullptr);
CHECK(!lardon3d_photo_quality_effective_include(oversized_metrics.recommendation,
LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE));
/* An oversized SOF does not suppress structural errors after that header. */
Lardon3DPhotoQualityMetrics truncated_oversized_metrics{};
CHECK(lardon3d_photo_quality_analyze_jpeg(truncated_oversized_path.c_str(),
&truncated_oversized_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR);
CHECK(std::strcmp(truncated_oversized_metrics.reasons, "JPEG_DECODE_ERROR") == 0);
Lardon3DPhotoQualityMetrics over_budget_oversized_metrics{};
CHECK(lardon3d_photo_quality_analyze_jpeg(over_budget_oversized_path.c_str(),
&over_budget_oversized_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR);
CHECK(std::strcmp(over_budget_oversized_metrics.reasons, "JPEG_DECODE_ERROR") == 0);
/* Malformed input is a decode error and remains observably distinct from a
* structurally valid JPEG rejected only by the pre-allocation proxy bound. */
Lardon3DPhotoQualityMetrics malformed_metrics{};
CHECK(lardon3d_photo_quality_analyze_jpeg(malformed_path.c_str(), &malformed_metrics) ==
LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR);
CHECK(malformed_metrics.recommendation == LARDON3D_PHOTO_QUALITY_REJECT);
CHECK(std::strcmp(malformed_metrics.reasons, "JPEG_DECODE_ERROR") == 0);
/* Reversing analysis order must not leak state or alter deterministic output. */
Lardon3DPhotoQualityMetrics second_blur{};
Lardon3DPhotoQualityMetrics second_sharp{};
CHECK(lardon3d_photo_quality_analyze_jpeg(blur_path.c_str(), &second_blur) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(lardon3d_photo_quality_analyze_jpeg(sharp_path.c_str(), &second_sharp) ==
LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(second_blur.sharpness_normalized == blur_metrics.sharpness_normalized);
CHECK(second_sharp.sharpness_normalized == sharp_metrics.sharpness_normalized);
Lardon3DPhotoQualityMetrics raw_only{};
lardon3d_photo_quality_raw_only(&raw_only);
CHECK(raw_only.status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE);
CHECK(std::strstr(raw_only.reasons, "REQUIRES_JPEG_PROXY") != nullptr);
CHECK(!lardon3d_photo_quality_effective_include(LARDON3D_PHOTO_QUALITY_SUSPECT,
LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE));
CHECK(lardon3d_photo_quality_effective_include(LARDON3D_PHOTO_QUALITY_REJECT,
LARDON3D_PHOTO_QUALITY_OVERRIDE_INCLUDE));
CHECK(!lardon3d_photo_quality_effective_include(LARDON3D_PHOTO_QUALITY_GOOD,
LARDON3D_PHOTO_QUALITY_OVERRIDE_EXCLUDE));
std::filesystem::remove_all(directory);
return 0;
}

View file

@ -0,0 +1,322 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <string>
#include <utility>
#include <unistd.h>
#include <vector>
#include <sqlite3.h>
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
extern "C" {
#include <lardon3d/photo_quality_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
}
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, \
#condition); \
return 1; \
} \
} while (0)
extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
return nullptr;
}
static bool wait_terminal(Lardon3DTaskQueue *queue, uint64_t task_id,
Lardon3DTaskSnapshot *snapshot) {
for (size_t attempt = 0; attempt < 500; ++attempt) {
if (lardon3d_task_queue_get(queue, task_id, snapshot) &&
(snapshot->state == TASK_COMPLETED || snapshot->state == TASK_FAILED ||
snapshot->state == TASK_CANCELLED))
return true;
usleep(10000);
}
return false;
}
static void source(Lardon3DAcquisitionCampaignSource *value, const std::string &path,
Lardon3DAcquisitionSourceKind kind) {
std::snprintf(value->path, sizeof(value->path), "%s", path.c_str());
value->source_kind = value->metadata.source_kind = kind;
value->metadata_result = LARDON3D_ACQUISITION_OK;
value->metadata.policy_version = LARDON3D_ACQUISITION_PAIRING_POLICY_VERSION;
}
int main() {
char directory_template[] = "/tmp/lardon3d-photo-quality-task-XXXXXX";
char *directory = mkdtemp(directory_template);
CHECK(directory != nullptr);
const std::string root(directory);
const std::string database_path = root + "/project.lardon3d";
const std::string jpeg_path = root + "/1-pair.jpg";
CHECK(std::filesystem::create_directories(root + "/.lardon3d/checkpoints"));
cv::Mat image(128, 128, CV_8UC1);
for (int y = 0; y < image.rows; ++y)
for (int x = 0; x < image.cols; ++x)
image.at<unsigned char>(y, x) = ((x / 8) + (y / 8)) % 2 ? 220 : 30;
CHECK(cv::imwrite(jpeg_path, image));
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
Lardon3DProjectDb *database = nullptr;
CHECK(lardon3d_project_db_open(database_path.c_str(), &database, error) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbScanSet scanset{};
CHECK(lardon3d_project_db_create_scanset(database, "quality", &scanset) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = database;
std::snprintf(state.project_path, sizeof(state.project_path), "%s", root.c_str());
state.hardware_profile.logical_cpu_count = 8;
state.hardware_profile.page_size_bytes = 4096;
state.hardware_profile.memory_total_bytes = UINT64_C(8) << 30;
std::snprintf(state.hardware_profile.cpu_architecture,
sizeof(state.hardware_profile.cpu_architecture), "test");
Lardon3DResourcePolicy policy{};
policy.maximum_cpu_load_ratio = 1.0;
policy.maximum_io_pressure_avg10 = 100.0;
policy.io_slot_capacity = 2;
state.resource_governor =
lardon3d_resource_governor_create(&state.hardware_profile, &policy);
state.task_queue = state.resource_governor
? lardon3d_task_queue_create(state.resource_governor, 4)
: nullptr;
CHECK(state.task_queue != nullptr);
Lardon3DAcquisitionCampaignSource sources[3]{};
source(&sources[0], root + "/0-pair.arw", LARDON3D_ACQUISITION_SOURCE_RAW);
source(&sources[1], jpeg_path, LARDON3D_ACQUISITION_SOURCE_JPEG);
source(&sources[2], root + "/2-only.arw", LARDON3D_ACQUISITION_SOURCE_RAW);
Lardon3DAcquisitionCampaignConfirmation pair{};
pair.source_count = 2;
pair.source_indices[0] = 0;
pair.source_indices[1] = 1;
Lardon3DPhotoQualityTaskRequest request{sources, 3, &pair, 1};
Lardon3DAcquisitionCampaignPlan planned{};
CHECK(lardon3d_acquisition_campaign_plan(sources, 3, &pair, 1, &planned) ==
LARDON3D_ACQUISITION_CAMPAIGN_OK &&
planned.group_count == 2);
size_t request_probe_size = 0;
CHECK(lardon3d_photo_quality_request_encode(&request, nullptr, 0, &request_probe_size));
uint64_t task_id = 0;
CHECK(lardon3d_project_enqueue_photo_quality(&state, scanset.scanset_id, &request, &task_id));
Lardon3DTaskSnapshot terminal{};
CHECK(wait_terminal(state.task_queue, task_id, &terminal));
CHECK(terminal.state == TASK_COMPLETED && terminal.progress == 100);
Lardon3DProjectDbPhotoQualityResult paired{}, raw_only{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 1, &paired) ==
LARDON3D_PROJECT_DB_OK);
CHECK(paired.proxy_source_index == 1 &&
paired.metrics.status == LARDON3D_PHOTO_QUALITY_METRIC_OK);
CHECK(paired.group_id == planned.groups[0].group_id);
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 2, &raw_only) ==
LARDON3D_PROJECT_DB_OK);
CHECK(raw_only.proxy_source_index == UINT32_MAX &&
raw_only.metrics.status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE &&
std::strstr(raw_only.metrics.reasons, "REQUIRES_JPEG_PROXY") != nullptr);
Lardon3DProjectDbTask completed_generic{};
CHECK(lardon3d_project_db_load_task(database, task_id, &completed_generic) ==
LARDON3D_PROJECT_DB_OK &&
completed_generic.sequence_count == 1);
std::vector<unsigned char> corrupt_request(LARDON3D_PHOTO_QUALITY_TASK_REQUEST_MAX_BYTES);
Lardon3DProjectDbPhotoQualityTask completed_parameters{};
CHECK(lardon3d_project_db_load_photo_quality_task(database, task_id, corrupt_request.data(),
corrupt_request.size(), &completed_parameters) == LARDON3D_PROJECT_DB_OK);
CHECK(completed_parameters.group_count == 2 && completed_parameters.next_group_id == 3);
CHECK(lardon3d_resource_governor_reservation_count(state.resource_governor) == 0);
/* Corrupt values above the v21 operational group bound must be rejected
* while still 64-bit SQLite integers, before narrowing into public fields. */
sqlite3 *corruptor = nullptr;
CHECK(sqlite3_open(database_path.c_str(), &corruptor) == SQLITE_OK);
CHECK(sqlite3_exec(corruptor, "PRAGMA ignore_check_constraints=ON", nullptr, nullptr,
nullptr) == SQLITE_OK);
const auto execute_corruption = [&](const std::string &assignment) {
const std::string sql = "UPDATE photo_quality_triage_tasks SET " + assignment +
" WHERE task_id=" + std::to_string(task_id);
return sqlite3_exec(corruptor, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK;
};
Lardon3DProjectDbPhotoQualityTask corrupt_parameters{};
for (const char *assignment : {"group_count=-1", "group_count=5000", "group_count=4294967296",
"next_group_id=0", "next_group_id=4", "next_group_id=1e20",
"scanset_id=-1"}) {
CHECK(execute_corruption(assignment));
CHECK(lardon3d_project_db_load_photo_quality_task(database, task_id, corrupt_request.data(),
corrupt_request.size(), &corrupt_parameters) == LARDON3D_PROJECT_DB_CORRUPT);
CHECK(execute_corruption("scanset_id=" + std::to_string(scanset.scanset_id) +
",group_count=2,next_group_id=3"));
}
const auto execute_result_corruption = [&](const std::string &assignment) {
const std::string sql = "UPDATE photo_quality_triage_results SET " + assignment +
" WHERE task_id=" + std::to_string(task_id) + " AND group_id=1";
return sqlite3_exec(corruptor, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK;
};
for (const auto &values : std::vector<std::pair<std::string, std::string>>{
{"proxy_source_index=-1", "proxy_source_index=1"},
{"proxy_source_index=4294967296", "proxy_source_index=1"},
{"proxy_source_index=1e20", "proxy_source_index=1"},
{"metrics_version=-1", "metrics_version=1"},
{"metric_status=-1", "metric_status=0"},
{"recommendation=4", "recommendation=1"},
{"human_override=3", "human_override=0"},
{"decoded_width=-1", "decoded_width=128"},
{"analysis_width=4294967296", "analysis_width=128"},
{"analysis_height=0", "analysis_height=128"},
{"sharpness_normalized=-1.0",
"sharpness_normalized=" + std::to_string(paired.metrics.sharpness_normalized)},
{"clipped_white_fraction=2.0",
"clipped_white_fraction=" + std::to_string(paired.metrics.clipped_white_fraction)}}) {
CHECK(execute_result_corruption(values.first));
Lardon3DProjectDbPhotoQualityResult corrupt_result{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 1, &corrupt_result) ==
LARDON3D_PROJECT_DB_CORRUPT);
CHECK(execute_result_corruption(values.second));
}
/* Result identity must belong to the typed plan and precede its durable
* cursor; otherwise publication would expose uncompleted plan state. */
CHECK(execute_result_corruption("group_id=3"));
Lardon3DProjectDbPhotoQualityResult corrupt_plan_result{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 3,
&corrupt_plan_result) == LARDON3D_PROJECT_DB_CORRUPT);
CHECK(sqlite3_exec(corruptor,
("UPDATE photo_quality_triage_results SET group_id=1 WHERE task_id=" +
std::to_string(task_id) + " AND group_id=3").c_str(), nullptr, nullptr, nullptr) ==
SQLITE_OK);
CHECK(execute_corruption("next_group_id=1"));
CHECK(lardon3d_project_db_load_photo_quality_result(database, task_id, 1,
&corrupt_plan_result) == LARDON3D_PROJECT_DB_CORRUPT);
CHECK(execute_corruption("next_group_id=3"));
sqlite3_close(corruptor);
uint64_t cancelled_id = 0;
Lardon3DTask *cancelled = lardon3d_project_create_photo_quality_task(
&state, scanset.scanset_id, &request, &cancelled_id);
CHECK(cancelled != nullptr);
Lardon3DResourceEstimate quality_estimate{};
CHECK(lardon3d_task_resource_estimate(cancelled, &quality_estimate));
/* Admission includes the retained durable context in addition to the
* documented 20 MiB one-group analyzer working set. */
CHECK(quality_estimate.memory_fixed_bytes > UINT64_C(20) * 1024u * 1024u);
CHECK(quality_estimate.memory_bytes_per_item == 0);
lardon3d_task_request_cancel(cancelled);
CHECK(lardon3d_task_queue_add(state.task_queue, cancelled, nullptr));
CHECK(wait_terminal(state.task_queue, cancelled_id, &terminal));
CHECK(terminal.state == TASK_CANCELLED);
Lardon3DProjectDbPhotoQualityResult absent{};
CHECK(raw_only.group_id == planned.groups[1].group_id);
CHECK(lardon3d_project_db_load_photo_quality_result(database, cancelled_id, 1, &absent) ==
LARDON3D_PROJECT_DB_NOT_FOUND);
/* Seed the documented crash shape: plan group 1 and next group ID 2 are durable while
* generic progress still lags. Registry recovery must begin at plan group 2 and
* must neither re-analyze nor replace the already published pair result. */
size_t encoded_size = 0;
CHECK(lardon3d_photo_quality_request_encode(&request, nullptr, 0, &encoded_size));
std::vector<unsigned char> encoded(encoded_size);
CHECK(lardon3d_photo_quality_request_encode(&request, encoded.data(), encoded.size(),
&encoded_size));
uint64_t restart_id = 0;
CHECK(lardon3d_project_db_allocate_task_id(database, &restart_id) == LARDON3D_PROJECT_DB_OK);
Lardon3DTaskDurableSnapshot durable{};
durable.id = restart_id;
std::snprintf(durable.name, sizeof(durable.name), "quality restart");
durable.saved_state = durable.recovery_state = TASK_PENDING;
Lardon3DProjectDbPhotoQualityTask parameters{restart_id, scanset.scanset_id, 1, 2,
encoded.data(), encoded.size()};
CHECK(lardon3d_project_db_record_photo_quality_task(
database, &durable, LARDON3D_PHOTO_QUALITY_TASK_KIND,
LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION, nullptr, &parameters, 1) ==
LARDON3D_PROJECT_DB_OK);
Lardon3DProjectDbPhotoQualityResult sentinel = paired;
sentinel.task_id = restart_id;
sentinel.metrics.sharpness_raw = 12345.0;
CHECK(lardon3d_project_db_record_photo_quality_result(database, &sentinel, 2) ==
LARDON3D_PROJECT_DB_OK);
/* Exact replay after the atomic cursor advance converges without replacing
* identity or creating another result row. */
CHECK(lardon3d_project_db_record_photo_quality_result(database, &sentinel, 2) ==
LARDON3D_PROJECT_DB_OK);
sqlite3 *replay_reader = nullptr;
CHECK(sqlite3_open(database_path.c_str(), &replay_reader) == SQLITE_OK);
sqlite3_stmt *count_statement = nullptr;
CHECK(sqlite3_prepare_v2(replay_reader,
"SELECT COUNT(*) FROM photo_quality_triage_results WHERE task_id=?1 AND group_id=1",
-1, &count_statement, nullptr) == SQLITE_OK);
CHECK(sqlite3_bind_int64(count_statement, 1, (sqlite3_int64)restart_id) == SQLITE_OK);
CHECK(sqlite3_step(count_statement) == SQLITE_ROW &&
sqlite3_column_int64(count_statement, 0) == 1);
sqlite3_finalize(count_statement);
Lardon3DProjectDbPhotoQualityResult conflicting = sentinel;
conflicting.metrics.sharpness_raw += 1.0;
CHECK(lardon3d_project_db_record_photo_quality_result(database, &conflicting, 2) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(sqlite3_exec(replay_reader,
("UPDATE photo_quality_triage_tasks SET next_group_id=3 WHERE task_id=" +
std::to_string(restart_id)).c_str(), nullptr, nullptr, nullptr) == SQLITE_OK);
CHECK(lardon3d_project_db_record_photo_quality_result(database, &sentinel, 2) ==
LARDON3D_PROJECT_DB_CONSTRAINT);
CHECK(sqlite3_exec(replay_reader,
("UPDATE photo_quality_triage_tasks SET next_group_id=2 WHERE task_id=" +
std::to_string(restart_id)).c_str(), nullptr, nullptr, nullptr) == SQLITE_OK);
sqlite3_close(replay_reader);
Lardon3DProjectDbPhotoQualityResult replay_retained{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, restart_id, 1,
&replay_retained) == LARDON3D_PROJECT_DB_OK &&
replay_retained.metrics.sharpness_raw == sentinel.metrics.sharpness_raw);
Lardon3DProjectDbTask generic{};
CHECK(lardon3d_project_db_load_task(database, restart_id, &generic) == LARDON3D_PROJECT_DB_OK);
Lardon3DTaskDurableSnapshot restart_snapshot{};
restart_snapshot.id = generic.task_id;
std::memcpy(restart_snapshot.name, generic.name, sizeof(restart_snapshot.name));
restart_snapshot.progress = generic.progress;
restart_snapshot.saved_state = generic.saved_state;
restart_snapshot.recovery_state = generic.recovery_state;
restart_snapshot.started_at = generic.started_at;
restart_snapshot.finished_at = generic.finished_at;
restart_snapshot.sequence_count = generic.sequence_count;
restart_snapshot.estimate = quality_estimate;
Lardon3DTaskReconstructionContext reconstruction{root.c_str(), database,
state.resource_governor, nullptr};
const Lardon3DTaskKindDescriptor quality_descriptor = {
LARDON3D_PHOTO_QUALITY_TASK_KIND, LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION,
lardon3d_photo_quality_task_reconstruct};
Lardon3DTaskKindRegistry quality_registry{};
CHECK(lardon3d_task_kind_registry_init(&quality_registry, &quality_descriptor, 1));
const Lardon3DTaskKindRegistry *registry = &quality_registry;
const Lardon3DTaskKindDescriptor *descriptor = nullptr;
CHECK(lardon3d_task_kind_registry_lookup(registry, LARDON3D_PHOTO_QUALITY_TASK_KIND,
LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION,
&descriptor) == LARDON3D_TASK_KIND_OK);
Lardon3DTask *restored = nullptr;
CHECK(lardon3d_task_kind_registry_restore(registry, LARDON3D_PHOTO_QUALITY_TASK_KIND,
LARDON3D_PHOTO_QUALITY_TASK_KIND_VERSION,
&restart_snapshot, &reconstruction, &restored) ==
LARDON3D_TASK_KIND_OK &&
restored != nullptr);
CHECK(lardon3d_task_queue_add(state.task_queue, restored, nullptr));
CHECK(wait_terminal(state.task_queue, restart_id, &terminal));
CHECK(terminal.state == TASK_COMPLETED);
Lardon3DProjectDbPhotoQualityResult retained{}, resumed_raw{};
CHECK(lardon3d_project_db_load_photo_quality_result(database, restart_id, 1, &retained) ==
LARDON3D_PROJECT_DB_OK && retained.metrics.sharpness_raw == 12345.0);
CHECK(lardon3d_project_db_load_photo_quality_result(database, restart_id, 2, &resumed_raw) ==
LARDON3D_PROJECT_DB_OK &&
resumed_raw.metrics.status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE);
lardon3d_task_queue_destroy(state.task_queue);
lardon3d_resource_governor_destroy(state.resource_governor);
lardon3d_project_db_close(database);
std::filesystem::remove_all(root);
return 0;
}

View file

@ -116,7 +116,7 @@ static bool create_future_database(const char *path) {
sqlite3_exec( sqlite3_exec(
connection, connection,
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
"INSERT INTO metadata VALUES('schema_version',21);", "INSERT INTO metadata VALUES('schema_version',22);",
NULL, NULL, NULL) == SQLITE_OK; NULL, NULL, NULL) == SQLITE_OK;
return sqlite3_close(connection) == SQLITE_OK && ok; return sqlite3_close(connection) == SQLITE_OK && ok;
} }
@ -940,7 +940,7 @@ static bool run_test(void) {
Lardon3DProjectDb *database = NULL; Lardon3DProjectDb *database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(database && lardon3d_project_db_schema_version(database) == 20); CHECK(database && lardon3d_project_db_schema_version(database) == 21);
bool legacy_pending = true; bool legacy_pending = true;
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -1499,7 +1499,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(query_integer(database_path, CHECK(query_integer(database_path,
"SELECT value FROM metadata WHERE key='schema_version'", "SELECT value FROM metadata WHERE key='schema_version'",
20)); 21));
CHECK(query_integer(database_path, CHECK(query_integer(database_path,
"SELECT count(*) FROM tasks WHERE task_id=1", 1)); "SELECT count(*) FROM tasks WHERE task_id=1", 1));
@ -1527,7 +1527,7 @@ static bool run_test(void) {
"UPDATE metadata SET value=17 WHERE key='schema_version';")); "UPDATE metadata SET value=17 WHERE key='schema_version';"));
CHECK(lardon3d_project_db_open(v17_path, &database, error) == CHECK(lardon3d_project_db_open(v17_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer(v17_path, CHECK(query_integer(v17_path,
@ -1566,7 +1566,7 @@ static bool run_test(void) {
"name='captures'", 0)); "name='captures'", 0));
CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(execute_test_sql( CHECK(execute_test_sql(
@ -1618,7 +1618,7 @@ static bool run_test(void) {
CHECK(create_v1_database(legacy_path)); CHECK(create_v1_database(legacy_path));
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == CHECK(lardon3d_project_db_open(legacy_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == CHECK(lardon3d_project_db_get_project(database, &loaded_project) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
strcmp(loaded_project.stable_id, "legacy-project") == 0); strcmp(loaded_project.stable_id, "legacy-project") == 0);
@ -1633,7 +1633,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(query_integer(legacy_path, CHECK(query_integer(legacy_path,
"SELECT value FROM metadata WHERE key='schema_version'", "SELECT value FROM metadata WHERE key='schema_version'",
20)); 21));
CHECK(create_v1_database(failed_migration_path)); CHECK(create_v1_database(failed_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
@ -1664,7 +1664,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 21));
CHECK(create_v2_database(failed_v3_migration_path)); CHECK(create_v2_database(failed_v3_migration_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
@ -1698,7 +1698,7 @@ static bool run_test(void) {
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 21));
CHECK(create_v3_database(failed_v4_path)); CHECK(create_v3_database(failed_v4_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
@ -1720,7 +1720,7 @@ static bool run_test(void) {
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error); fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
} }
CHECK(v4_result == LARDON3D_PROJECT_DB_OK); CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
CHECK(lardon3d_project_db_load_task(database, 9, &task) == CHECK(lardon3d_project_db_load_task(database, 9, &task) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
@ -1775,7 +1775,7 @@ static bool run_test(void) {
error); error);
} }
CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK && CHECK(retry_v7 == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1785,7 +1785,7 @@ static bool run_test(void) {
5)); 5));
CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1794,11 +1794,11 @@ static bool run_test(void) {
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7)); v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
CHECK(lardon3d_project_db_open(v8_path, &database, error) == CHECK(lardon3d_project_db_open(v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 21));
CHECK(create_v7_database(failed_v8_path)); CHECK(create_v7_database(failed_v8_path));
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
@ -1815,7 +1815,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1834,11 +1834,11 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(v10_path, &database, error) == CHECK(lardon3d_project_db_open(v10_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 21));
CHECK( CHECK(
query_integer(v10_path, query_integer(v10_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1865,7 +1865,7 @@ static bool run_test(void) {
1)); 1));
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -1879,11 +1879,11 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(v13_path, &database, error) == CHECK(lardon3d_project_db_open(v13_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(query_integer( CHECK(query_integer(
v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 20)); v13_path, "SELECT value FROM metadata WHERE key='schema_version'", 21));
CHECK( CHECK(
query_integer(v13_path, query_integer(v13_path,
"SELECT count(*) FROM sqlite_master WHERE type='table' AND " "SELECT count(*) FROM sqlite_master WHERE type='table' AND "
@ -1906,7 +1906,7 @@ static bool run_test(void) {
true_v14_path); true_v14_path);
} }
CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK && CHECK(true_v14_result == LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK( CHECK(
@ -2021,7 +2021,7 @@ static bool run_test(void) {
if (true_v15_result != LARDON3D_PROJECT_DB_OK) if (true_v15_result != LARDON3D_PROJECT_DB_OK)
fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error); fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error);
CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK); CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
Lardon3DProjectDbProject migrated_project; Lardon3DProjectDbProject migrated_project;
CHECK(lardon3d_project_db_get_project(database, &migrated_project) == CHECK(lardon3d_project_db_get_project(database, &migrated_project) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
@ -2067,7 +2067,7 @@ static bool run_test(void) {
database = NULL; database = NULL;
CHECK(lardon3d_project_db_open(database_path, &database, error) == CHECK(lardon3d_project_db_open(database_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
CHECK(schema_compare(database_path, true_v15_path, true)); CHECK(schema_compare(database_path, true_v15_path, true));
@ -2102,7 +2102,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(true_v15_path, &database, error) == CHECK(lardon3d_project_db_open(true_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK); LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -2121,7 +2121,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;
@ -2140,7 +2140,7 @@ static bool run_test(void) {
0)); 0));
CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) == CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) ==
LARDON3D_PROJECT_DB_OK && LARDON3D_PROJECT_DB_OK &&
lardon3d_project_db_schema_version(database) == 20); lardon3d_project_db_schema_version(database) == 21);
lardon3d_project_db_close(database); lardon3d_project_db_close(database);
database = NULL; database = NULL;

View file

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

View file

@ -66,7 +66,7 @@ int main() {
Lardon3DProjectDb *database = nullptr; Lardon3DProjectDb *database = nullptr;
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{}; 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_open(path.c_str(), &database, error) == LARDON3D_PROJECT_DB_OK);
CHECK(lardon3d_project_db_schema_version(database) == 20); CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
sqlite3 *raw = nullptr; sqlite3 *raw = nullptr;
CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK); CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK);