test: harden precision feature consolidation

This commit is contained in:
fy59 2026-08-08 19:50:38 +02:00
parent 9bffaa6fd4
commit 36c68fcbff
3 changed files with 1818 additions and 6 deletions

View file

@ -0,0 +1,143 @@
# Precision Feature Pipeline v1A
## Contrat multipasse
**IMPLEMENTED.** Une image peut porter plusieurs `FeatureSet` immutables et
indépendants. PASS 0 conserve ORB v1 (`orb`, U8×32, Feature File v1,
`features.extract`) pour le Visual Index. PASS 1 ajoute SIFT v1 (`sift`,
F32×128, Feature File v2, `features.extract.sift`). PASS 1B applique RootSIFT
dans un Feature Set distinct (`rootsift`, version 1,
`features.extract.rootsift`). Une panne SIFT ne modifie jamais ORB et SIFT ne
dépend pas d'ORB pour être correct.
Le profil conceptuel `PRECISION_CLASSIC_V1` persiste toujours ses valeurs
effectives : 4096 features au plus, 3 couches par octave, seuil de contraste
0,02, seuil d'arête 10, sigma 1,6, grille 8×8 et quota 96 par cellule. Les
bornes sont respectivement 1..8192, 1..8, 0,001..0,2, 1..100, 0,5..3,
1..32 pour chaque dimension de grille et 1..8192 par cellule. Le fingerprint
SHA-256 encode les binary64 exacts en little-endian, tous les entiers, la
grille et le choix RootSIFT ; il ne dépend ni de la locale ni d'un JSON.
## Extraction et qualité spatiale
OpenCV 5 est appelé explicitement par `cv::SIFT::create(max_candidates,
octave_layers, contrast_threshold, edge_threshold, sigma, true)`. Le dernier
argument active le precise upscale d'OpenCV 5. L'appel `detectAndCompute` est
une frontière C++ non interruptible ; pause et annulation sont observées avant
et après. Les exceptions restent confinées à la façade C.
OpenCV peut dépasser légèrement `nfeatures` lorsque des réponses sont à
égalité. La sélection publiée reste strictement limitée par `max_features`; un
plafond défensif de 65 536 candidats rejette une sortie backend anormale avant
toute copie Lardon3D.
SIFT localise déjà les extrema à une position subpixel. Aucun `cornerSubPix`
supplémentaire n'est appliqué. CLAHE n'est pas activé ; il reste benchmark-only
et exigerait un fingerprint distinct s'il devenait un preprocessing production.
Les candidats sont classés de façon déterministe par réponse décroissante,
puis y, x et index OpenCV initial. Un quota par cellule est appliqué avant la
limite globale. Les coordonnées ne sont pas transformées. Une modification de
grille produit un nouveau fingerprint et donc de nouveaux indices immutables.
`occupied_cells`, `total_cells`, `coverage_ratio` et
`feature_density_per_megapixel` sont persistés dans ProjectDb ; une couverture
faible reste un diagnostic.
RootSIFT conserve exactement les keypoints SIFT. Chaque descriptor est
normalisé par sa somme L1 puis transformé composante par composante par racine
carrée. Un descriptor nul reste nul ; toute valeur non finie est rejetée. Le
benchmark test-only emploie L2, tandis qu'ORB reste dans son espace Hamming.
## Tâches, ressources et reprise
Une tâche traite une image et persiste ses paramètres dans
`sift_extract_tasks` avant enqueue. Une tâche READY identique devient un no-op
validé. Après crash, la registry statique reconstruit le même `task_id`; l'image
est recommencée, sans micro-checkpoint trompeur. Aucun Feature Set partiel ne
devient READY. Le SHA-256 de l'Image Asset géré est vérifié avant décodage.
L'estimation SIFT réserve un slot CPU, un slot IO, aucun GPU et environ
1,06 Gio structurels : image décodée, pyramides OpenCV, candidats et
descriptors. Le pic de lot est zéro. OpenCV peut utiliser son backend parallèle
interne ; Lardon3D ne modifie aucun état global OpenCV. Le scheduler v1 reste à
un worker, ce qui borne actuellement l'oversubscription entre tâches.
Le modèle n'est pas présenté comme un RSS mesuré : l'image grayscale atteint
100 MB à la limite post-décodage, le plafond défensif de candidats représente
au plus 32 Mio de descriptors F32 bruts, la sortie publiée au plus 4 Mio, et le
reste de la réservation couvre pyramides, buffers et overhead OpenCV. ORB
conserve son estimation historique de 576 Mio. Une API fiable de dimensions
multi-format avant décodage n'étant pas utilisée, le rejet 100 Mpx intervient
après `imread`.
La build OpenCV/TBB système n'est pas instrumentée par TSan et rapporte ses
propres accès de teardown TLS/`cv::Mat`. Les tests TSan suppriment uniquement
les frames des objets partagés `libopencv_core.so` et
`libopencv_features.so`; les frames Lardon3D et toutes les autres bibliothèques
restent contrôlées.
## Consolidation intra-image
PASS 3 crée un `feature_support_set` immutable pour une paire ORB/SIFT et un
rayon fingerprinté (4 px par défaut, 0,5..64). Une grille spatiale hachée trouve
le plus proche ORB non encore utilisé pour chaque SIFT. La complexité attendue
est O(N+M+candidats locaux), les entrées sont bornées à 8192 par Feature Set et
les égalités sont résolues par index. La position SIFT, déjà subpixel, devient
la représentante d'un groupe confirmé ; les observations isolées sont gardées.
Les tables normalisées `feature_support_groups` et
`feature_support_members` enregistrent `feature_set_id + feature_index` et un
`support_count` de 1 ou 2. La distance spatiale est persistée. L'API de ranking
transparente classe d'abord `support_count=2`, puis l'identité durable comme
tie-break; aucun score opaque n'est inventé. Aucun descriptor ORB n'est comparé, converti ou
fusionné avec un descriptor SIFT. Ces groupes sont intra-image : ce ne sont ni
des matches multivues, ni des tracks, ni une preuve géométrique.
## Durcissement et tests de consolidation
Le fichier `tests/test_precision_consolidation.c` contient 19 tests de
durcissement couvrant la consolidation ORB/SIFT. Ces tests sont uniquement des
tests unitaires ; aucun code de production n'est modifié.
**Couverture :**
- **zero-features** : consolidation avec zéro descripteur SIFT ou ORB.
- **rayon boundaries** : rayon à la borne inférieure (0,5 px) et supérieure
(64 px).
- **grid boundary** : grille spatiale en bordure d'image.
- **support_count** : vérifie `support_count=1` (isolé) et `support_count=2`
(paire confirmée).
- **mismatch** : aucun appariement lorsque les descripteurs sont trop éloignés.
- **large set** : performance avec un grand nombre de features (borné à 8192).
- **idempotence** : deux exécutions successives produisent le même résultat
immutable.
- **concurrence** : accès concurrents à la consolidation sans corruption.
- **stress** : charge élevée pour valider les bornes mémoire et les
invariants.
- **pagination** : lecture paginée des résultats de consolidation.
- **fingerprint** : reproductibilité du fingerprint SHA-256 des paramètres.
- **quality comparison** : comparaison de qualité entre zones consolidées et
non consolidées.
Ces tests garantissent la robustesse des invariants de consolidation sans
modifier le comportement production. La validation ASan/UBSan et TSan est
recommandée pour les chemins concurrence et stress.
## Benchmark, stockage et frontières futures
`precision-features` construit une fixture et des homographies déterministes :
crop, rotations 5/15/30°, échelles 0,75/1,25/1,5, illumination, flou et faible
texture. Il rapporte count, couverture, répétabilité, localisation médiane/P90,
temps informatif et correction d'un BFMatcher/ratio test strictement test-only.
Ces résultats synthétiques ne prédisent pas la qualité sur de vraies photos.
À 4096 points, les descriptors SIFT occupent 2 Mio/image, plus environ 96 Kio
de keypoints et 176 octets de header. À 3700 images, cela représente 7,76 GB
(7,23 Gio) de descriptors et 8,12 GB (7,57 Gio) avec keypoints/header. Au
maximum 8192, les totaux sont 15,52 GB (14,45 Gio) de descriptors et 16,25 GB
(15,13 Gio) complets. RootSIFT en supplément double cette part SIFT ; 3700
images ne sont jamais chargées ensemble.
Le Visual Index v1 reste exclusivement ORB-LSH. Candidate Pair Generator,
matching production, vérification géométrique, tracks et SfM sont **PLANNED**.
ALIKED est **PLANNED / BLOCKED ON MODEL PROVENANCE + VALIDATED ONNX EXPORT** :
aucun code ALIKED, ONNX ou Python production n'appartient à v1A.

View file

@ -5,6 +5,7 @@ project(
license: 'MIT', license: 'MIT',
default_options: [ default_options: [
'c_std=c17', 'c_std=c17',
'cpp_std=c++17',
'warning_level=3', 'warning_level=3',
'buildtype=debugoptimized', 'buildtype=debugoptimized',
], ],
@ -17,7 +18,7 @@ add_project_arguments(
'-Wshadow', '-Wshadow',
language: 'c', language: 'c',
) )
add_project_arguments('-std=c++17', '-Wpedantic', '-Wconversion', '-Wshadow', language: 'cpp') add_project_arguments('-Wpedantic', '-Wconversion', '-Wshadow', language: 'cpp')
ncursesw = dependency('ncursesw', required: true) ncursesw = dependency('ncursesw', required: true)
threads = dependency('threads') threads = dependency('threads')
@ -29,6 +30,20 @@ opencv = dependency(
include_type: 'system', include_type: 'system',
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d'], modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d'],
) )
opencv_benchmark = dependency(
'opencv5',
required: true,
include_type: 'system',
modules: ['opencv_core', 'opencv_imgcodecs', 'opencv_features2d', 'opencv_imgproc'],
)
opencv_test_environment = environment()
if get_option('b_sanitize').contains('thread')
opencv_test_environment.set(
'TSAN_OPTIONS',
'suppressions=' + meson.project_source_root() + '/tests/tsan-opencv.supp',
)
endif
executable( executable(
'lardon3d', 'lardon3d',
@ -44,7 +59,7 @@ executable(
'src/image_catalog_persistent.c', 'src/image_catalog_persistent.c',
'src/feature_extractor_opencv.cpp', 'src/feature_extractor_opencv.cpp',
'src/feature_store.c', 'src/feature_store.c',
'src/feature_task.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/visual_index.c', 'src/visual_index.c',
'src/visual_index_task.c', 'src/visual_index_task.c',
'src/image_view.c', 'src/image_view.c',
@ -94,7 +109,7 @@ import_task_test = executable(
'src/task_checkpoint.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/task_kinds.c',
'src/feature_task.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_store.c', 'src/feature_store.c',
'src/visual_index.c', 'src/visual_index.c',
'src/visual_index_task.c', 'src/visual_index_task.c',
@ -198,7 +213,8 @@ feature_task_test = executable(
'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c', 'src/task_kind_registry.c', 'src/task_kinds.c', 'src/task_queue.c',
'src/import.c', 'src/import_task.c', 'src/image_catalog.c', 'src/import.c', 'src/import_task.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_view.c', 'src/image_catalog_persistent.c', 'src/image_view.c',
'src/feature_task.c', 'src/feature_store.c', 'src/visual_index.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_store.c', 'src/visual_index.c',
'src/visual_index_task.c', 'src/visual_index_task.c',
'src/feature_extractor_opencv.cpp', 'src/resource_governor.c', 'src/feature_extractor_opencv.cpp', 'src/resource_governor.c',
'src/resource_snapshot.c', 'src/resource_snapshot.c',
@ -210,7 +226,41 @@ feature_task_test = executable(
include_directories: include_directories('include'), include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv], dependencies: [threads, sqlite3, openssl, opencv],
) )
test('feature-task', feature_task_test, timeout: 60) test('feature-task', feature_task_test, timeout: 60, env: opencv_test_environment)
precision_features_test = executable(
'test-precision-features',
sources: ['tests/test_precision_features.cpp', 'src/feature_extractor_opencv.cpp'],
include_directories: include_directories('include'),
dependencies: [openssl, opencv_benchmark],
)
test('precision-features', precision_features_test, timeout: 60, env: opencv_test_environment)
precision_consolidation_test = executable(
'test-precision-consolidation',
sources: [
'tests/test_precision_consolidation.c', 'src/app_state.c',
'src/project.c', 'src/project_db.c', 'src/task.c',
'src/task_checkpoint.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/task_queue.c', 'src/import.c',
'src/import_task.c', 'src/image_catalog.c',
'src/image_catalog_persistent.c', 'src/image_view.c',
'src/feature_task.c', 'src/sift_task.c',
'src/precision_features.c', 'src/feature_store.c',
'src/visual_index.c', 'src/visual_index_task.c',
'src/feature_extractor_opencv.cpp',
'src/resource_governor.c', 'src/resource_snapshot.c',
],
c_args: [
'-DLARDON3D_PROJECT_DB_TESTING',
'-DLARDON3D_FEATURE_TASK_TESTING',
'-DLARDON3D_VISUAL_INDEX_TASK_TESTING',
],
include_directories: include_directories('include'),
dependencies: [threads, sqlite3, openssl, opencv],
)
test('precision-consolidation', precision_consolidation_test,
timeout: 120, env: opencv_test_environment)
image_view_test = executable( image_view_test = executable(
'test-image-view', 'test-image-view',
@ -282,7 +332,7 @@ project_test = executable(
'src/task_checkpoint.c', 'src/task_checkpoint.c',
'src/task_kind_registry.c', 'src/task_kind_registry.c',
'src/task_kinds.c', 'src/task_kinds.c',
'src/feature_task.c', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
'src/feature_store.c', 'src/feature_store.c',
'src/visual_index.c', 'src/visual_index.c',
'src/visual_index_task.c', 'src/visual_index_task.c',

File diff suppressed because it is too large Load diff