test: harden precision feature consolidation
This commit is contained in:
parent
9bffaa6fd4
commit
36c68fcbff
3 changed files with 1818 additions and 6 deletions
143
docs/architecture/precision_feature_pipeline.md
Normal file
143
docs/architecture/precision_feature_pipeline.md
Normal 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.
|
||||
62
meson.build
62
meson.build
|
|
@ -5,6 +5,7 @@ project(
|
|||
license: 'MIT',
|
||||
default_options: [
|
||||
'c_std=c17',
|
||||
'cpp_std=c++17',
|
||||
'warning_level=3',
|
||||
'buildtype=debugoptimized',
|
||||
],
|
||||
|
|
@ -17,7 +18,7 @@ add_project_arguments(
|
|||
'-Wshadow',
|
||||
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)
|
||||
threads = dependency('threads')
|
||||
|
|
@ -29,6 +30,20 @@ opencv = dependency(
|
|||
include_type: 'system',
|
||||
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(
|
||||
'lardon3d',
|
||||
|
|
@ -44,7 +59,7 @@ executable(
|
|||
'src/image_catalog_persistent.c',
|
||||
'src/feature_extractor_opencv.cpp',
|
||||
'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_task.c',
|
||||
'src/image_view.c',
|
||||
|
|
@ -94,7 +109,7 @@ import_task_test = executable(
|
|||
'src/task_checkpoint.c',
|
||||
'src/task_kind_registry.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/visual_index.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/import.c', 'src/import_task.c', 'src/image_catalog.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/feature_extractor_opencv.cpp', 'src/resource_governor.c',
|
||||
'src/resource_snapshot.c',
|
||||
|
|
@ -210,7 +226,41 @@ feature_task_test = executable(
|
|||
include_directories: include_directories('include'),
|
||||
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(
|
||||
'test-image-view',
|
||||
|
|
@ -282,7 +332,7 @@ project_test = executable(
|
|||
'src/task_checkpoint.c',
|
||||
'src/task_kind_registry.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/visual_index.c',
|
||||
'src/visual_index_task.c',
|
||||
|
|
|
|||
1619
tests/test_precision_consolidation.c
Normal file
1619
tests/test_precision_consolidation.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue