feat: freeze photo quality triage
This commit is contained in:
parent
e27bcf9a6f
commit
892d0c5954
28 changed files with 2759 additions and 83 deletions
|
|
@ -34,7 +34,8 @@ through an explicitly authorized, explicitly scoped human ticket:
|
|||
- F0 — PASS/FROZEN
|
||||
- Phase H v1 — 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
|
||||
- S2 Capture-safe Standard Ingestion — PASS/FROZEN
|
||||
- S3 Capture / Acquisition Ingestion — PASS/FROZEN
|
||||
|
|
|
|||
|
|
@ -34,8 +34,10 @@ persistante, enrichissable et versionnable.
|
|||
- **Image View** : vues triées et filtrées pour la TUI
|
||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||
- **Project Database v20** : fondations v19 préservées, avec persistance
|
||||
additive des tâches de campagne d'acquisition
|
||||
- **Project Database v21** : fondations v20 préservées, avec persistance additive
|
||||
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
|
||||
typée immuable, confirmations `CALLER_EXPLICIT`, curseur et correspondance
|
||||
tâche/groupe→Capture persistants ; un groupe S3-E par séquence, reprise par
|
||||
|
|
|
|||
62
docs/architecture/photo_quality_triage.md
Normal file
62
docs/architecture/photo_quality_triage.md
Normal 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.
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
# 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)
|
||||
|
||||
**PASS / FROZEN.** La migration transactionnelle v18→v19
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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 ;
|
||||
2. évaluer la qualité de la reconstruction sparse multi-campagne ;
|
||||
3. exécuter MVS/dense avec budgets et scratch contrôlés ;
|
||||
4. publier durablement nuage dense et mesh ;
|
||||
5. comparer le résultat aux tentatives photogrammétriques antérieures.
|
||||
L'étape qualité canonique implémentée se place après la découverte bornée, les
|
||||
métadonnées et l'association minimale des sources, mais avant la matérialisation
|
||||
normale d'une campagne et avant toute représentation scientifique, feature,
|
||||
matching, SfM ou MVS :
|
||||
|
||||
```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
|
||||
S3. Les antécédents d'OOM/SIGSEGV OpenMVS, pression swap, grands intermédiaires
|
||||
|
|
|
|||
86
include/lardon3d/photo_quality.h
Normal file
86
include/lardon3d/photo_quality.h
Normal 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
|
||||
69
include/lardon3d/photo_quality_task.h
Normal file
69
include/lardon3d/photo_quality_task.h
Normal 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
|
||||
|
|
@ -61,6 +61,12 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
|
|||
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task(
|
||||
Lardon3DAppState *state, const Lardon3DTask *task,
|
||||
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(
|
||||
Lardon3DAppState *state, const Lardon3DTask *task,
|
||||
const Lardon3DProjectDbFeatureExtractTask *parameters);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@
|
|||
#include <time.h>
|
||||
|
||||
#include <lardon3d/task.h>
|
||||
#include <lardon3d/photo_quality.h>
|
||||
#include <lardon3d/sparse_sfm_incremental.h>
|
||||
|
||||
enum {
|
||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 20,
|
||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 21,
|
||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||
|
|
@ -114,6 +115,25 @@ typedef struct {
|
|||
uint64_t capture_id;
|
||||
} 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 {
|
||||
uint64_t scanset_id;
|
||||
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(
|
||||
Lardon3DProjectDb *database, uint64_t task_id, uint32_t group_id,
|
||||
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,
|
||||
uint64_t *task_id);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id,
|
||||
|
|
|
|||
65
meson.build
65
meson.build
|
|
@ -153,6 +153,8 @@ executable(
|
|||
'src/acquisition_ingest.cpp',
|
||||
'src/acquisition_campaign.cpp',
|
||||
'src/acquisition_campaign_task.cpp',
|
||||
'src/photo_quality.cpp',
|
||||
'src/photo_quality_task.cpp',
|
||||
'src/feature_extractor_opencv.cpp',
|
||||
'src/feature_store.c',
|
||||
'src/feature_extractor_opencv.cpp',
|
||||
|
|
@ -197,6 +199,7 @@ executable(
|
|||
'-DLARDON3D_SPARSE_SFM_TASK_AVAILABLE',
|
||||
'-DLARDON3D_INCREMENTAL_RECONSTRUCTION_TASK_AVAILABLE',
|
||||
'-DLARDON3D_ACQUISITION_CAMPAIGN_TASK_AVAILABLE',
|
||||
'-DLARDON3D_PHOTO_QUALITY_TASK_AVAILABLE',
|
||||
],
|
||||
dependencies: [
|
||||
ncursesw,
|
||||
|
|
@ -397,6 +400,68 @@ acquisition_campaign_task_test = executable(
|
|||
)
|
||||
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(
|
||||
'test-feature-store',
|
||||
sources: [
|
||||
|
|
|
|||
|
|
@ -129,6 +129,11 @@ def main() -> int:
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("images", type=Path)
|
||||
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()
|
||||
|
||||
images_dir = args.images.expanduser().resolve()
|
||||
|
|
@ -172,16 +177,22 @@ def main() -> int:
|
|||
print()
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
for category in ("bonnes", "suspectes", "mauvaises"):
|
||||
category_dir = output_dir / category
|
||||
if category_dir.exists():
|
||||
for child in category_dir.iterdir():
|
||||
if child.is_symlink() or child.is_file():
|
||||
child.unlink()
|
||||
category_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_path = output_dir / "resultats.csv"
|
||||
if report_path.exists():
|
||||
print(
|
||||
f"Rapport déjà présent, aucun fichier modifié : {report_path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
with (output_dir / "resultats.csv").open(
|
||||
"w", encoding="utf-8", newline=""
|
||||
if args.compatibility_links:
|
||||
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:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(
|
||||
|
|
@ -199,8 +210,16 @@ def main() -> int:
|
|||
counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0}
|
||||
for result in results:
|
||||
counters[result.category] += 1
|
||||
if args.compatibility_links:
|
||||
link_name = f"{counters[result.category]:06d}_{result.path.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())
|
||||
|
||||
writer.writerow(
|
||||
|
|
|
|||
327
src/photo_quality.cpp
Normal file
327
src/photo_quality.cpp
Normal 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
255
src/photo_quality_task.cpp
Normal 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, ¶meters) ==
|
||||
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; }
|
||||
}
|
||||
|
|
@ -675,7 +675,8 @@ checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
|
|||
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters,
|
||||
const Lardon3DProjectDbMatcherTask *matcher_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) {
|
||||
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(
|
||||
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
|
||||
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,
|
||||
task_kind_version, &checkpoint, now.tv_sec);
|
||||
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,
|
||||
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
|
||||
|
|
@ -771,7 +777,7 @@ lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lar
|
|||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
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(
|
||||
|
|
@ -779,6 +785,14 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_camp
|
|||
const Lardon3DProjectDbAcquisitionCampaignTask *parameters) {
|
||||
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -789,7 +803,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_
|
|||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL,
|
||||
NULL);
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task(
|
||||
|
|
@ -797,7 +811,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_tas
|
|||
const Lardon3DProjectDbSiftExtractTask *parameters) {
|
||||
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
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(
|
||||
|
|
@ -807,7 +821,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_upd
|
|||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
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(
|
||||
|
|
@ -817,7 +831,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_g
|
|||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL,
|
||||
NULL);
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
|
||||
|
|
@ -827,7 +841,7 @@ Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
|
|||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL,
|
||||
NULL);
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
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 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,
|
||||
|
|
|
|||
407
src/project_db.c
407
src/project_db.c
|
|
@ -622,6 +622,33 @@ static const char schema_acquisition_campaign_v20[] =
|
|||
"capture_id INTEGER NOT NULL REFERENCES captures(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) {
|
||||
if (destination) {
|
||||
(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 != 8 && from_version != 9 && from_version != 10 && from_version != 11 &&
|
||||
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;
|
||||
}
|
||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||
|
|
@ -1126,6 +1154,15 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
|
|||
"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) {
|
||||
result = execute(database, "COMMIT", "commit migration");
|
||||
}
|
||||
|
|
@ -1263,7 +1300,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
|
|||
"capture_selections",
|
||||
"asset_derivations",
|
||||
"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]) &&
|
||||
result == LARDON3D_PROJECT_DB_OK;
|
||||
++index) {
|
||||
|
|
@ -1488,6 +1527,7 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
|
|||
const Lardon3DProjectDbGeometricVerifierTask *geometric_verifier,
|
||||
const Lardon3DProjectDbSparseSfmTask *sparse_sfm,
|
||||
const Lardon3DProjectDbAcquisitionCampaignTask *campaign,
|
||||
const Lardon3DProjectDbPhotoQualityTask *photo_quality,
|
||||
int64_t updated_at) {
|
||||
bool typed = task_kind != NULL;
|
||||
if (!database || !valid_durable_task(snapshot, updated_at) ||
|
||||
|
|
@ -1559,6 +1599,12 @@ record_task_internal(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnaps
|
|||
campaign->group_count > 4096 ||
|
||||
campaign->next_group_id > campaign->group_count || !campaign->request ||
|
||||
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))) {
|
||||
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) {
|
||||
result =
|
||||
prepare(database,
|
||||
|
|
@ -1977,7 +2048,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_task(
|
|||
Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot, const char *task_kind,
|
||||
uint32_t task_kind_version, const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at) {
|
||||
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
|
||||
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
}
|
||||
|
||||
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,
|
||||
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(
|
||||
|
|
@ -2000,7 +2071,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_feature_extract_task(
|
|||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
|
||||
parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
return record_task_internal(database, snapshot, task_kind, task_kind_version, checkpoint, NULL, 0,
|
||||
NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
NULL, parameters, NULL, NULL, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
}
|
||||
|
||||
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
|
||||
never observes a valid campaign Task without its immutable request blob. */
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -2399,6 +2470,316 @@ Lardon3DProjectDbResult lardon3d_project_db_load_acquisition_campaign_capture(
|
|||
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_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 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(
|
||||
|
|
@ -5031,7 +5412,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_candidate_pair_generate_task(
|
|||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
|
||||
NULL, NULL, parameters, NULL, NULL, NULL, NULL, updated_at);
|
||||
NULL, NULL, parameters, NULL, NULL, NULL, NULL, NULL, updated_at);
|
||||
}
|
||||
|
||||
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 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(
|
||||
|
|
@ -5145,7 +5526,7 @@ Lardon3DProjectDbResult lardon3d_project_db_record_geometric_verifier_task(
|
|||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
return record_task_internal(db, snapshot, kind, version, checkpoint, NULL, 0, NULL,
|
||||
NULL, NULL, NULL, NULL, parameters, NULL, NULL, updated_at);
|
||||
NULL, NULL, NULL, NULL, parameters, NULL, NULL, NULL, updated_at);
|
||||
}
|
||||
|
||||
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(
|
||||
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;
|
||||
(void)pthread_mutex_lock(&db->mutex);
|
||||
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) {
|
||||
if (!parameters) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
return record_task_internal(database, snapshot, kind, version, checkpoint, NULL, 0, NULL, NULL,
|
||||
NULL, NULL, NULL, NULL, parameters, 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) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
#include <lardon3d/acquisition_campaign_task.h>
|
||||
#endif
|
||||
#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/import_task.h>
|
||||
#include <lardon3d/matcher_task.h>
|
||||
|
|
@ -21,6 +24,13 @@
|
|||
|
||||
const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
|
||||
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
|
||||
{
|
||||
.kind = LARDON3D_ACQUISITION_CAMPAIGN_TASK_KIND,
|
||||
|
|
|
|||
418
tests/photo_quality_real_scan.cpp
Normal file
418
tests/photo_quality_real_scan.cpp
Normal 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;
|
||||
}
|
||||
350
tests/photo_quality_restart_scan.cpp
Normal file
350
tests/photo_quality_restart_scan.cpp
Normal 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 ®istry;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -202,7 +202,7 @@ static bool test_model_api(const char *path) {
|
|||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
Parents parents;
|
||||
CHECK(create_parents(database, &parents));
|
||||
|
||||
|
|
@ -486,7 +486,7 @@ static bool test_migration(const char *v11_path, const char *failed_path) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(v11_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
Parents parents;
|
||||
CHECK(create_parents(database, &parents));
|
||||
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;
|
||||
lardon3d_project_db_close(database);
|
||||
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(
|
||||
query_integer(v11_path,
|
||||
"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));
|
||||
CHECK(lardon3d_project_db_open(failed_path, &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);
|
||||
lardon3d_project_db_close(database);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ static bool run_task_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
lardon3d_project_db_close(database);
|
||||
CHECK(remove_tree(root));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ static bool run_test(void) {
|
|||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
|
||||
Lardon3DProjectDbScanSet 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) ==
|
||||
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 */
|
||||
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));
|
||||
CHECK(lardon3d_project_db_open(v9_path, &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);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
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(
|
||||
query_integer(v9_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
|
|
@ -576,7 +577,7 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -373,7 +373,8 @@ static bool run_test(void) {
|
|||
"name='matcher_tasks'",
|
||||
0));
|
||||
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(
|
||||
query_integer(database_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
|
|
|
|||
166
tests/test_photo_quality.cpp
Normal file
166
tests/test_photo_quality.cpp
Normal 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;
|
||||
}
|
||||
322
tests/test_photo_quality_task.cpp
Normal file
322
tests/test_photo_quality_task.cpp
Normal 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, ¶meters, 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;
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ static bool create_future_database(const char *path) {
|
|||
sqlite3_exec(
|
||||
connection,
|
||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||
"INSERT INTO metadata VALUES('schema_version',21);",
|
||||
"INSERT INTO metadata VALUES('schema_version',22);",
|
||||
NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
}
|
||||
|
|
@ -940,7 +940,7 @@ static bool run_test(void) {
|
|||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 21);
|
||||
bool legacy_pending = true;
|
||||
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
|
|
@ -1499,7 +1499,7 @@ static bool run_test(void) {
|
|||
database = NULL;
|
||||
CHECK(query_integer(database_path,
|
||||
"SELECT value FROM metadata WHERE key='schema_version'",
|
||||
20));
|
||||
21));
|
||||
CHECK(query_integer(database_path,
|
||||
"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';"));
|
||||
CHECK(lardon3d_project_db_open(v17_path, &database, error) ==
|
||||
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);
|
||||
database = NULL;
|
||||
CHECK(query_integer(v17_path,
|
||||
|
|
@ -1566,7 +1566,7 @@ static bool run_test(void) {
|
|||
"name='captures'", 0));
|
||||
CHECK(lardon3d_project_db_open(failed_v19_path, &database, error) ==
|
||||
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);
|
||||
database = NULL;
|
||||
CHECK(execute_test_sql(
|
||||
|
|
@ -1618,7 +1618,7 @@ static bool run_test(void) {
|
|||
CHECK(create_v1_database(legacy_path));
|
||||
CHECK(lardon3d_project_db_open(legacy_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
CHECK(lardon3d_project_db_get_project(database, &loaded_project) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
||||
|
|
@ -1633,7 +1633,7 @@ static bool run_test(void) {
|
|||
database = NULL;
|
||||
CHECK(query_integer(legacy_path,
|
||||
"SELECT value FROM metadata WHERE key='schema_version'",
|
||||
20));
|
||||
21));
|
||||
|
||||
CHECK(create_v1_database(failed_migration_path));
|
||||
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);
|
||||
database = NULL;
|
||||
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(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);
|
||||
database = NULL;
|
||||
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(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);
|
||||
}
|
||||
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) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact",
|
||||
|
|
@ -1775,7 +1775,7 @@ static bool run_test(void) {
|
|||
error);
|
||||
}
|
||||
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);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -1785,7 +1785,7 @@ static bool run_test(void) {
|
|||
5));
|
||||
CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -1794,11 +1794,11 @@ static bool run_test(void) {
|
|||
v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
|
||||
CHECK(lardon3d_project_db_open(v8_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
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(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
|
||||
|
|
@ -1815,7 +1815,7 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -1834,11 +1834,11 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(v10_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
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(
|
||||
query_integer(v10_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
|
|
@ -1865,7 +1865,7 @@ static bool run_test(void) {
|
|||
1));
|
||||
CHECK(lardon3d_project_db_open(failed_v11_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -1879,11 +1879,11 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(v13_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
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(
|
||||
query_integer(v13_path,
|
||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||
|
|
@ -1906,7 +1906,7 @@ static bool run_test(void) {
|
|||
true_v14_path);
|
||||
}
|
||||
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);
|
||||
database = NULL;
|
||||
CHECK(
|
||||
|
|
@ -2021,7 +2021,7 @@ static bool run_test(void) {
|
|||
if (true_v15_result != LARDON3D_PROJECT_DB_OK)
|
||||
fprintf(stderr, "true v15 upgrade: %d %s\n", true_v15_result, error);
|
||||
CHECK(true_v15_result == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
Lardon3DProjectDbProject migrated_project;
|
||||
CHECK(lardon3d_project_db_get_project(database, &migrated_project) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
|
|
@ -2067,7 +2067,7 @@ static bool run_test(void) {
|
|||
database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
CHECK(schema_compare(database_path, true_v15_path, true));
|
||||
|
|
@ -2102,7 +2102,7 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(true_v15_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -2121,7 +2121,7 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(failed_v14_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
@ -2140,7 +2140,7 @@ static bool run_test(void) {
|
|||
0));
|
||||
CHECK(lardon3d_project_db_open(failed_v15_path, &database, error) ==
|
||||
LARDON3D_PROJECT_DB_OK &&
|
||||
lardon3d_project_db_schema_version(database) == 20);
|
||||
lardon3d_project_db_schema_version(database) == 21);
|
||||
lardon3d_project_db_close(database);
|
||||
database = NULL;
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ static bool run_test(void) {
|
|||
Lardon3DProjectDb *db = NULL;
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
CHECK(lardon3d_project_db_open(path, &db, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(db) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(db) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
|
||||
Lardon3DProjectDbScanSet scanset;
|
||||
CHECK(lardon3d_project_db_create_scanset(db, "Sparse model", &scanset) ==
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ int main() {
|
|||
Lardon3DProjectDb *database = nullptr;
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
|
||||
CHECK(lardon3d_project_db_open(path.c_str(), &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == 20);
|
||||
CHECK(lardon3d_project_db_schema_version(database) == LARDON3D_PROJECT_DB_SCHEMA_VERSION);
|
||||
|
||||
sqlite3 *raw = nullptr;
|
||||
CHECK(sqlite3_open(path.c_str(), &raw) == SQLITE_OK);
|
||||
|
|
|
|||
Loading…
Reference in a new issue