From 02bf2c6d078544b72fd68f20bb371b11f873bca4 Mon Sep 17 00:00:00 2001 From: fy59 Date: Sun, 9 Aug 2026 09:59:07 +0200 Subject: [PATCH] feat(reconstruction): add precision features, candidate pairs and matcher pipeline --- .agents/agents/lardon-architect.md | 27 + .agents/agents/lardon-build.md | 20 + .agents/agents/lardon-concurrency.md | 20 + .agents/agents/lardon-diagnose.md | 24 + .agents/agents/lardon-docs.md | 18 + .agents/agents/lardon-orchestrator.md | 33 + .agents/agents/lardon-read.md | 20 + .agents/agents/lardon-review.md | 29 + .agents/agents/lardon-tests.md | 32 + .../plugins/lardon-safety/hooks/hooks.json | 25 + .agents/plugins/lardon-safety/plugin.json | 5 + .../plugins/lardon-safety/scripts/policy.py | 62 ++ .goosehints | 53 ++ README.md | 9 +- docs/architecture/feature_store.md | 5 +- docs/architecture/match_result.md | 64 ++ docs/architecture/matcher.md | 96 +++ docs/architecture/overview.md | 17 + docs/architecture/project_database.md | 71 +- docs/architecture/reconstruction_pipeline.md | 2 +- docs/architecture/resource_governor.md | 5 + include/lardon3d/match_file.h | 86 +++ include/lardon3d/matcher.h | 91 +++ include/lardon3d/project_db.h | 50 +- meson.build | 81 ++ src/match_file.c | 280 +++++++ src/matcher.cpp | 695 ++++++++++++++++++ src/project_db.c | 452 +++++++++++- tests/benchmark_matcher.cpp | 201 +++++ tests/test_match_result.c | 526 +++++++++++++ tests/test_matcher.c | 518 +++++++++++++ tests/test_matcher_e2e.c | 385 ++++++++++ tests/test_project_db.c | 28 +- 33 files changed, 4003 insertions(+), 27 deletions(-) create mode 100644 .agents/agents/lardon-architect.md create mode 100644 .agents/agents/lardon-build.md create mode 100644 .agents/agents/lardon-concurrency.md create mode 100644 .agents/agents/lardon-diagnose.md create mode 100644 .agents/agents/lardon-docs.md create mode 100644 .agents/agents/lardon-orchestrator.md create mode 100644 .agents/agents/lardon-read.md create mode 100644 .agents/agents/lardon-review.md create mode 100644 .agents/agents/lardon-tests.md create mode 100644 .agents/plugins/lardon-safety/hooks/hooks.json create mode 100644 .agents/plugins/lardon-safety/plugin.json create mode 100755 .agents/plugins/lardon-safety/scripts/policy.py create mode 100644 .goosehints create mode 100644 docs/architecture/match_result.md create mode 100644 docs/architecture/matcher.md create mode 100644 include/lardon3d/match_file.h create mode 100644 include/lardon3d/matcher.h create mode 100644 src/match_file.c create mode 100644 src/matcher.cpp create mode 100644 tests/benchmark_matcher.cpp create mode 100644 tests/test_match_result.c create mode 100644 tests/test_matcher.c create mode 100644 tests/test_matcher_e2e.c diff --git a/.agents/agents/lardon-architect.md b/.agents/agents/lardon-architect.md new file mode 100644 index 0000000..76438cd --- /dev/null +++ b/.agents/agents/lardon-architect.md @@ -0,0 +1,27 @@ +--- +name: lardon-architect +description: Décisions architecturales persistantes et difficiles de Lardon3D. +model: gemini-3.6-flash +--- +Tu interviens uniquement lorsqu'une vraie décision architecturale persistante +est ambiguë. + +Étudie: +- invariants; +- compatibilité; +- persistance/migrations; +- fingerprints; +- invalidation; +- reprise; +- coût mémoire/CPU; +- évolution future. + +Préfère la solution minimale compatible avec l'architecture existante. + +Retour: +- décision; +- alternatives rejetées; +- conséquences; +- invariants à documenter. + +Ne code pas. diff --git a/.agents/agents/lardon-build.md b/.agents/agents/lardon-build.md new file mode 100644 index 0000000..52abd88 --- /dev/null +++ b/.agents/agents/lardon-build.md @@ -0,0 +1,20 @@ +--- +name: lardon-build +description: Implémente une tranche Lardon3D déjà cadrée. +model: gemini-3.6-flash +--- +Tu es l'agent d'implémentation Lardon3D. + +Tu reçois un contrat, une tranche ou un diagnostic déjà cadré. + +Responsabilités: +- implémentation minimale et cohérente; +- respecter les invariants existants; +- C17/C++ selon les fichiers existants; +- lisibilité cible ~100 colonnes, maximum 120; +- pas de refactor hors périmètre; +- documentation mise à jour lorsque le contrat ou comportement change; +- uniquement les tests ciblés nécessaires pendant l'implémentation. + +Tu ne fais pas la validation indépendante finale. +Tu ne commit, push ou stage jamais. diff --git a/.agents/agents/lardon-concurrency.md b/.agents/agents/lardon-concurrency.md new file mode 100644 index 0000000..6f0f378 --- /dev/null +++ b/.agents/agents/lardon-concurrency.md @@ -0,0 +1,20 @@ +--- +name: lardon-concurrency +description: Analyse spécialisée des problèmes réels de concurrence Lardon3D. +model: gemini-3.6-flash +--- +Tu interviens uniquement lorsqu'une vraie question de concurrence existe. + +Analyse: +- ownership; +- mutex/atomics; +- transactions; +- worker races; +- publication atomique; +- pause/cancel/recovery; +- deadlocks; +- duplicate creation; +- visibility entre threads. + +Ne fais pas d'audit général. +Ne modifie rien sauf demande explicite de l'orchestrateur après diagnostic. diff --git a/.agents/agents/lardon-diagnose.md b/.agents/agents/lardon-diagnose.md new file mode 100644 index 0000000..df0c000 --- /dev/null +++ b/.agents/agents/lardon-diagnose.md @@ -0,0 +1,24 @@ +--- +name: lardon-diagnose +description: Diagnostic borné des échecs dont la cause exacte est inconnue. +model: gemini-3.6-flash +--- +Tu diagnostiques un problème précis dans Lardon3D. + +Tu ne corriges rien. + +Exigence principale: +établir une chaîne causale concrète à partir du symptôme observé. + +Évite les hypothèses générales. +Inspecte uniquement le chemin nécessaire. + +Retour obligatoire: +- symptôme confirmé; +- cause CONFIRMÉE / PROBABLE / NON ÉTABLIE; +- chaîne causale; +- fichiers et lignes concernés; +- correction minimale recommandée; +- routage suivant. + +Arrête-toi dès qu'une cause concrète suffisante est établie. diff --git a/.agents/agents/lardon-docs.md b/.agents/agents/lardon-docs.md new file mode 100644 index 0000000..4f9ce91 --- /dev/null +++ b/.agents/agents/lardon-docs.md @@ -0,0 +1,18 @@ +--- +name: lardon-docs +description: Maintient la documentation canonique de Lardon3D. +model: gemini-3.6-flash +--- +Tu es responsable de la documentation canonique de Lardon3D. + +La documentation est une partie du produit, pas une étape cosmétique finale. + +Tu dois: +- préserver la terminologie existante; +- documenter les contrats, invariants, persistance, fingerprints, + invalidation, recovery, bornes et concurrence lorsqu'ils changent; +- supprimer les promesses devenues obsolètes; +- vérifier que la documentation décrit le comportement réellement implémenté; +- éviter toute documentation artificielle pour un microfix sans changement de comportement. + +Une décision importante ne doit pas exister uniquement dans un rapport d'agent. diff --git a/.agents/agents/lardon-orchestrator.md b/.agents/agents/lardon-orchestrator.md new file mode 100644 index 0000000..4bb28c1 --- /dev/null +++ b/.agents/agents/lardon-orchestrator.md @@ -0,0 +1,33 @@ +--- +name: lardon-orchestrator +description: Pilote les tickets Lardon3D et délègue le travail aux spécialistes. +model: gemini-3.6-flash +--- +Tu es l'orchestrateur principal de Lardon3D. + +Ta responsabilité est de piloter, pas de tout faire toi-même. + +Règles: +- comprendre le ticket et l'état réel du dépôt avant d'agir; +- considérer la documentation canonique comme la mémoire durable du projet; +- maintenir .goose/work/current_ticket.md et handoff.md lorsqu'un run devient long; +- déléguer aux agents spécialisés quand leur rôle correspond au travail; +- ne pas refaire toi-même un travail déjà délégué; +- ne pas lancer deux agents d'écriture sur les mêmes fichiers en parallèle; +- éviter les appels d'agents redondants; +- utiliser des tests ciblés pendant le développement et une validation globale seulement à la fin; +- continuer automatiquement tant qu'une action sûre et utile existe. + +Tu peux faire directement uniquement une correction locale, mécanique, évidente et petite. +Une modification substantielle doit aller à lardon-build. + +Ordre normal: +documentation/contrat +→ implémentation +→ tests ciblés +→ documentation alignée +→ tranche suivante +→ validation finale +→ review finale. + +Ne commit, ne push et ne stage jamais. diff --git a/.agents/agents/lardon-read.md b/.agents/agents/lardon-read.md new file mode 100644 index 0000000..31bea48 --- /dev/null +++ b/.agents/agents/lardon-read.md @@ -0,0 +1,20 @@ +--- +name: lardon-read +description: Audit ciblé en lecture seule du code et de la documentation Lardon3D. +model: gemini-3.6-flash +--- +Tu es l'agent d'audit ciblé de Lardon3D. + +LECTURE SEULE. + +Ta mission: +- lire uniquement les fichiers nécessaires à la question; +- retrouver les contrats et invariants existants; +- identifier précisément les dépendances utiles; +- signaler les divergences documentation/code; +- rendre un résumé compact avec fichiers et preuves. + +Ne modifie aucun fichier. +Ne compile pas sauf si explicitement demandé. +Ne propose pas de refactor opportuniste. +Arrête l'audit lorsque les informations nécessaires sont établies. diff --git a/.agents/agents/lardon-review.md b/.agents/agents/lardon-review.md new file mode 100644 index 0000000..e04a62a --- /dev/null +++ b/.agents/agents/lardon-review.md @@ -0,0 +1,29 @@ +--- +name: lardon-review +description: Review finale indépendante des tickets Lardon3D. +model: gemini-3.6-flash +--- +Tu es le reviewer final indépendant. + +Ne modifie rien. + +Review prioritaire: +- correctness; +- persistance et migrations; +- recovery; +- identité/fingerprints; +- invalidation; +- bornes; +- concurrence; +- déterminisme; +- tests; +- cohérence documentation/code; +- complexité inutile. + +Classe les findings par sévérité. +Ne transforme pas la review en nouvel audit général. + +Verdict final: +APPROVED +ou +CHANGES_REQUIRED avec findings précis. diff --git a/.agents/agents/lardon-tests.md b/.agents/agents/lardon-tests.md new file mode 100644 index 0000000..4c2ce30 --- /dev/null +++ b/.agents/agents/lardon-tests.md @@ -0,0 +1,32 @@ +--- +name: lardon-tests +description: Validation indépendante des changements Lardon3D sans correction. +model: gemini-3.6-flash +--- +Tu es le validateur indépendant de Lardon3D. + +VALIDATION UNIQUEMENT. + +Tu peux: +- compiler; +- lancer les tests demandés; +- lancer sanitizers quand demandé; +- exécuter git diff --check; +- localiser exactement une assertion, cible ou commande en échec. + +Tu ne dois PAS: +- modifier le code; +- corriger les tests; +- diagnostiquer largement; +- implémenter une solution. + +En cas d'échec, rends: +- commande; +- test/cible; +- résultat attendu; +- résultat observé; +- localisation; +- reproductibilité; +- routage recommandé. + +Verdict: PASS ou FAIL. diff --git a/.agents/plugins/lardon-safety/hooks/hooks.json b/.agents/plugins/lardon-safety/hooks/hooks.json new file mode 100644 index 0000000..7e7323a --- /dev/null +++ b/.agents/plugins/lardon-safety/hooks/hooks.json @@ -0,0 +1,25 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "developer__shell|developer__text_editor", + "hooks": [ + { + "type": "command", + "command": "python3 ${PLUGIN_ROOT}/scripts/policy.py" + } + ] + } + ], + "BeforeReadFile": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${PLUGIN_ROOT}/scripts/policy.py" + } + ] + } + ] + } +} diff --git a/.agents/plugins/lardon-safety/plugin.json b/.agents/plugins/lardon-safety/plugin.json new file mode 100644 index 0000000..ca7f7e0 --- /dev/null +++ b/.agents/plugins/lardon-safety/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "lardon-safety", + "version": "1.0.0", + "description": "Hard safety policy for Goose on Lardon3D" +} diff --git a/.agents/plugins/lardon-safety/scripts/policy.py b/.agents/plugins/lardon-safety/scripts/policy.py new file mode 100755 index 0000000..311bae2 --- /dev/null +++ b/.agents/plugins/lardon-safety/scripts/policy.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import json +import re +import sys + +try: + payload = json.load(sys.stdin) +except Exception: + sys.exit(0) + +event = str(payload.get("event", "")) +tool = str(payload.get("tool_name", "")) +tool_input = payload.get("tool_input") or {} + +blob = json.dumps(tool_input, ensure_ascii=False) + +def deny(reason): + print(json.dumps({ + "decision": "block", + "reason": reason, + })) + sys.exit(0) + +# Explicitly forbidden areas. +if re.search(r'(^|[/\s"\'"])scan3d(?:/|["\'\s]|$)', blob): + deny("scan3d/** is outside the Lardon3D agent scope") + +if re.search(r'(^|[/\s"\'"])\.git(?:/|["\'\s]|$)', blob): + deny(".git/** must not be accessed directly") + +if event == "PreToolUse" and "shell" in tool: + command = str(tool_input.get("command", "")) + + forbidden = [ + r'(^|[;&|]\s*|\s)sudo(\s|$)', + r'(^|[;&|]\s*|\s)doas(\s|$)', + r'(^|[;&|]\s*|\s)(pacman|yay|paru)(\s|$)', + + r'\bgit\s+add\b', + r'\bgit\s+commit\b', + r'\bgit\s+push\b', + r'\bgit\s+reset\b', + r'\bgit\s+clean\b', + r'\bgit\s+checkout\b', + r'\bgit\s+restore\b', + r'\bgit\s+rebase\b', + r'\bgit\s+merge\b', + r'\bgit\s+switch\b', + + r'(^|[;&|]\s*|\s)rm(\s|$)', + r'(^|[;&|]\s*|\s)chmod(\s|$)', + r'(^|[;&|]\s*|\s)chown(\s|$)', + r'(^|[;&|]\s*|\s)(kill|pkill|killall)(\s|$)', + r'(^|[;&|]\s*|\s)(poweroff|reboot|shutdown)(\s|$)', + ] + + for pattern in forbidden: + if re.search(pattern, command): + deny(f"blocked by Lardon3D safety policy: {command}") + +sys.exit(0) diff --git a/.goosehints b/.goosehints new file mode 100644 index 0000000..0b10d9d --- /dev/null +++ b/.goosehints @@ -0,0 +1,53 @@ +# Lardon3D — règles Goose + +Lardon3D est un moteur persistant et incrémental de reconstruction 3D. + +## Principe prioritaire + +La documentation canonique est la mémoire durable du projet et est plus +importante que les rapports temporaires d'agents. + +Pour toute tranche substantielle: + +audit docs +→ contrat/invariants +→ documentation +→ implémentation +→ tests ciblés +→ vérification doc/code +→ tranche suivante + +Une modification de comportement, architecture, persistance, migration, +fingerprint, invalidation, recovery, concurrence ou bornes sans documentation +alignée rend la tranche PARTIAL. + +## Discipline + +- Lire AGENTS.md avant tout travail substantiel. +- Ne jamais commit, push ou stage. +- Ne jamais utiliser git add -A. +- Ne jamais annuler ou écraser des modifications existantes de l'utilisateur. +- scan3d/** est hors périmètre. +- .git/** est hors périmètre. +- Pas de sudo ni gestionnaire de paquets. +- Pas de commandes destructives. +- Pas de refactor opportuniste. +- Code lisible: cible ~100 colonnes, maximum 120. +- Tests ciblés pendant le développement. +- Suite complète uniquement au gate final sauf nécessité démontrée. + +## Multi-agent + +Utiliser les rôles: +- lardon-read: audit ciblé +- lardon-diagnose: cause inconnue +- lardon-build: implémentation +- lardon-tests: validation indépendante +- lardon-docs: documentation +- lardon-architect: décision architecturale réelle +- lardon-concurrency: vraie question de concurrence +- lardon-review: review finale unique + +Ne pas déléguer par réflexe. +Ne pas faire refaire par le parent le travail d'un subagent. +Ne pas lancer plusieurs écrivains concurrents sur les mêmes fichiers. diff --git a/README.md b/README.md index e5c3c7a..95944ea 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,9 @@ Acquisitions - [Feature Store](docs/architecture/feature_store.md) - [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md) - [Visual Index](docs/architecture/visual_index.md) +- [Candidate Pair](docs/architecture/candidate_pair.md) +- [Match Result](docs/architecture/match_result.md) +- [Matcher](docs/architecture/matcher.md) - [Viewer](docs/architecture/viewer.md) - [Revue des fondations](docs/architecture/foundation_review.md) @@ -145,9 +148,9 @@ Pour les changements sensibles à la mémoire ou à la concurrence, ajouter ASan ## Statut Lardon3D est en développement actif. La persistance des tâches, le catalogue, -le Feature Store multipasse et le Visual Index ORB sont implémentés. Candidate -Pair Generator, matching, DAG générique, SfM et viewer restent des tickets -séparés planifiés. +le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator +et Matcher v1 sont implémentés. DAG générique, vérification géométrique, +SfM et viewer restent des tickets séparés planifiés. ## Licence diff --git a/docs/architecture/feature_store.md b/docs/architecture/feature_store.md index 77d9643..d7adb15 100644 --- a/docs/architecture/feature_store.md +++ b/docs/architecture/feature_store.md @@ -115,7 +115,10 @@ l'extraction après import et planification multi-image/DAG. **IMPLEMENTED** — Visual Index v1 consomme ce reader sans changer le format. -**PLANNED** — paires candidates, matching, vérification géométrique, tracks et SfM. +**IMPLEMENTED** — paires candidates, Matcher v1 et Match Store v1 consomment +les Feature Sets persistés. + +**PLANNED** — vérification géométrique, tracks et SfM. ## Extension v2 multi-descriptor diff --git a/docs/architecture/match_result.md b/docs/architecture/match_result.md new file mode 100644 index 0000000..da2af51 --- /dev/null +++ b/docs/architecture/match_result.md @@ -0,0 +1,64 @@ +# Match Result Model v1 + +## Rôle et frontière + +Un Match Result décrit un calcul descriptor-level réussi entre deux Feature +Sets d'une Candidate Pair. Les correspondances sont dans le Match File; aucune +vérification géométrique, aucun inlier géométrique et aucun masque d'inliers ne +font partie de ce modèle. + +Les erreurs d'exécution appartiennent au Task Runtime et aux valeurs de retour. +Elles ne créent pas de Match Result. Le modèle persistant possède deux états : + +- `NO_MATCH` (0) : `match_count == 0`, aucun asset; +- `MATCHED` (1) : `match_count > 0`, asset obligatoire. + +`match_count` est exactement le nombre de correspondances descriptor-level +persistées après Lowe ratio et canonicalisation. Il n'existe ni `raw_match_count` +ambigu ni `inlier_count` dans Match Result v1. + +## Identité et ownership + +L'identité immuable est : + +``` +(candidate_pair_id, feature_set_id_a, feature_set_id_b, + matcher_kind, matcher_version, parameter_fingerprint) +``` + +Elle porte une contrainte `UNIQUE`. `feature_set_id_a` appartient à +`candidate_pair.image_id_a` et `feature_set_id_b` à `image_id_b`. Les Feature +Set IDs ne sont jamais triés numériquement. + +## Schéma Project DB v10 + +`match_results` impose en SQL : statut dans `(0,1)`, `0 <= match_count <= 8192`, +SHA de 32 octets, chemins non vides et tailles strictement positives lorsqu'ils +sont présents. Un `CHECK` conjoint impose : + +``` +NO_MATCH => match_count = 0 et SHA/path/size NULL +MATCHED => match_count > 0 et SHA/path/size tous présents +``` + +La création vérifie aussi atomiquement l'ownership A/B avant l'INSERT. Les API +load/find/list conservent l'ordre stable par `match_result_id`; close/reopen ne +change aucune identité ni métadonnée. + +## Reuse + +`NO_MATCH` est directement réutilisable sans asset. `MATCHED` ne l'est qu'après +validation du chemin, de la taille, du SHA-256 réel, du format complet, du type +et de la dimension des descripteurs, des Feature Set IDs A/B et du compte. +Une ligne existante dont l'asset est absent ou corrompu déclenche un recalcul. +Le fichier est republié atomiquement et les champs résultat/asset de cette même +ligne sont réparés transactionnellement; son ID et son identité scientifique à +six parties restent inchangés. Aucune identité artificielle ne contourne la +contrainte `UNIQUE`. + +## Suite + +Le prochain modèle possède ses propres statistiques géométriques, dont un +éventuel `inlier_count`. + +NEXT: GEOMETRIC VERIFICATION MODEL diff --git a/docs/architecture/matcher.md b/docs/architecture/matcher.md new file mode 100644 index 0000000..3e3fe4b --- /dev/null +++ b/docs/architecture/matcher.md @@ -0,0 +1,96 @@ +# Matcher v1 et Match Store v1 + +## Contrat + +Le pipeline v1 est `ORB → BFMatcher Hamming` ou `SIFT/RootSIFT → BFMatcher L2`, +puis KNN `k=2`, Lowe ratio, tri canonique, Match File content-addressed et Match +Result. Il s'arrête avant toute vérification géométrique. + +Pour chaque descripteur A, OpenCV fournit zéro, un ou deux voisins. Zéro ne +produit rien. Un seul voisin valide est accepté. Avec deux voisins, le premier +est accepté exactement si `d1 < threshold * d2`; une seconde distance nulle, +une égalité, NaN, Inf ou une distance négative est rejetée. `-0.0` est accepté +comme zéro conformément à IEEE-754. Indices invalides et +distances du premier voisin non finies ou négatives sont rejetés. + +Un `feature_index_a` produit donc au maximum une correspondance acceptée. +Sans cross-check, plusieurs indices A peuvent viser le même index B. L'ordre +final est `(feature_index_a, distance, feature_index_b)` croissant. BFMatcher +fournit une liste KNN par query et le filtre ne conserve que son premier voisin : +la déduplication est donc mathématiquement inutile. Le Matcher rejette toute +sortie anormale contenant deux fois le même index A. + +## Match File v1 + +Header fixe de 32 octets, tous les entiers en little-endian : + +``` +0..3 octets ASCII exacts "L3DM" +4 format_version = 1 +5 descriptor_type (1=U8, 2=F32) +6..7 reserved = 0 +8..11 match_count uint32 +12..15 descriptor_dimension uint32 (32 ou 128) +16..23 feature_set_id_a uint64 +24..31 feature_set_id_b uint64 +``` + +Chaque entrée fait 12 octets : index A uint32, index B uint32, distance float32. +Le format accepte zéro entrée pour tester reader/writer, mais le Matcher ne +publie pas d'asset vide. La borne est 8192 entrées et la taille maximale exacte +est `32 + 8192 * 12 = 98336` octets, environ 96 Kio. + +Le reader valide avant toute allocation : magic physique, version, reserved, +type/dimension, borne du compte, calcul de taille, taille physique exacte, +Feature Set IDs A/B sans permutation, indices contre les comptes attendus et +distance finie non négative. Fichiers tronqués, trailing bytes et versions +futures sont rejetés. + +## Publication et reuse + +Pour `NO_MATCH`, le fichier temporaire est supprimé et seule une ligne sans +asset est créée. Pour `MATCHED` : temp dans `assets/matches`, écriture complète, +`fsync` du fichier, SHA-256, chemin `assets/matches//`, puis +publication atomique par `link`. Une race de même contenu est validée; un +contenu différent déjà présent n'est jamais écrasé lors d'une publication +fraîche. Lorsqu'une ligne existante prouve que son asset content-addressed est +corrompu, le Matcher recalcule puis remplace atomiquement cet asset par `rename` +et répare les métadonnées de la même ligne sous transaction. Le temp est nettoyé +et le répertoire final synchronisé. + +Le reuse `MATCHED` exige métadonnées complètes, fichier régulier non symbolique, +taille, SHA-256, header, type/dimension, IDs A/B, compte et entrées valides. Il +n'existe aucun fallback faisant confiance au chemin ou à l'existence seuls. +La validation de reuse lit le fichier borné une seule fois et calcule le SHA-256 +sur ce même buffer. + +## Bornes mémoire et performance + +Les deux buffers de descripteurs contigus occupent au maximum 512 Kio pour ORB +ou 8 Mio pour SIFT/RootSIFT. Les résultats KNN ne contiennent que deux DMatch +par query, les matches filtrés et le Match File sont chacun bornés à environ +96 Kio. Le working set contrôlé du Matcher est donc inférieur à environ 10 Mio, +hors scratch interne borné par OpenCV; aucune structure `A × B` n'est +matérialisée. + +Le Match File complet est sérialisé dans un buffer heap borné à 98336 octets et +écrit par un unique `write_exact`, puis synchronisé une fois. Les mesures locales +restent dans `.opencode/work/current_ticket.md`, pas dans ce contrat canonique. +À 8192 features, le coût dominant mesuré reste l'évaluation exacte des +distances dans `cv::BFMatcher::knnMatch`; v1 ne remplace pas OpenCV ni BFMatcher. + +Le Matcher n'est pas encore un task kind autonome. Lorsqu'il est orchestré par +une tâche, celle-ci doit utiliser l'unique Resource Governor existant avec une +estimation couvrant ce working set; aucune seconde logique de budget n'est +introduite ici. + +## Déterminisme et fingerprint + +Le fingerprint couvre kind, version, `k=2`, threshold float32 et absence de +cross-check. À Feature Files, configuration, implémentation/version OpenCV et +environnement numérique compatibles identiques, SIFT/RootSIFT garantissent les +mêmes paires attendues, l'ordre et la sérialisation stables. Aucune promesse +bit-à-bit cross-platform n'est faite pour les distances L2 flottantes. +ORB/Hamming bénéficie d'une garantie plus forte grâce à sa distance entière. + +NEXT: GEOMETRIC VERIFICATION MODEL diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index b919f69..2954e82 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -111,6 +111,23 @@ peine d'être试探ées ? » sans validation géométrique. **Statut :** IMPLEMENTED +### Match Result +Persistance d'un calcul descriptor-level réussi entre deux Feature Sets +liés à une Candidate Pair. Identité déterministe par +`(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)`. +Validation d'appartenance Feature Set → image. Les correspondances vivent dans +le Match Store v1 content-addressed; les échecs restent dans le Task Runtime. + +**Statut :** IMPLEMENTED + +### Matcher +Matching de descripteurs entre deux Feature Sets via BFMatcher OpenCV +(ORB Hamming, SIFT/RootSIFT L2) avec Lowe ratio test. Produit un +Match File content-addressed et un Match Result dans Project DB. +Déterministe, idempotent, borné. + +**Statut :** IMPLEMENTED + ## Résultats et publication live Les traitements fonctionnent par séquences adaptatives : lire un lot borné, diff --git a/docs/architecture/project_database.md b/docs/architecture/project_database.md index cd623fe..57a8c2b 100644 --- a/docs/architecture/project_database.md +++ b/docs/architecture/project_database.md @@ -1,9 +1,10 @@ # Base de données projet Lardon3D -> Version courante : **v9**. La migration transactionnelle v8→v9 ajoute -> la table `candidate_pair_generate_tasks` pour la tâche durable Candidate -> Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour le -> sous-système Candidate Pair. +> Version courante : **v10**. La migration transactionnelle v9→v10 ajoute +> la table `match_results` pour le Match Result Model. La migration v8→v9 +> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable +> Candidate Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour +> le sous-système Candidate Pair. > Les migrations historiques restent ordonnées et les faults d'injection > vérifient le rollback. @@ -305,6 +306,66 @@ CREATE TABLE candidate_pair_generate_tasks( - `lardon3d_project_db_record_candidate_pair_generate_task()` — UPSERT checkpoint - `lardon3d_project_db_load_candidate_pair_generate_task()` — SELECT par task_id +## Schéma v10 implémenté + +La migration v9→v10 ajoute la table `match_results` pour le Match Result +Model : + +```sql +CREATE TABLE match_results( + match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0), + candidate_pair_id INTEGER NOT NULL + REFERENCES candidate_pairs(candidate_pair_id), + feature_set_id_a INTEGER NOT NULL + REFERENCES feature_sets(feature_set_id), + feature_set_id_b INTEGER NOT NULL + REFERENCES feature_sets(feature_set_id), + matcher_kind TEXT NOT NULL + CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64), + matcher_version INTEGER NOT NULL CHECK(matcher_version>0), + parameter_fingerprint BLOB NOT NULL + CHECK(length(parameter_fingerprint)=32), + result_status INTEGER NOT NULL CHECK(result_status IN (0,1)), + match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192), + match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR + length(match_asset_sha256)=32), + match_asset_path TEXT CHECK(match_asset_path IS NULL OR + length(match_asset_path)>0), + match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR + match_asset_size_bytes>0), + created_at INTEGER NOT NULL CHECK(created_at>=0), + CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL + AND match_asset_path IS NULL AND match_asset_size_bytes IS NULL) + OR (result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL + AND match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL)), + UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, + matcher_kind, matcher_version, parameter_fingerprint) +); +CREATE INDEX match_results_candidate_pair_idx + ON match_results(candidate_pair_id); +CREATE INDEX match_results_feature_set_a_idx + ON match_results(feature_set_id_a); +CREATE INDEX match_results_feature_set_b_idx + ON match_results(feature_set_id_b); +``` + +**Invariants** : +- `UNIQUE(candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint)` : identité déterministe 6 parties +- `candidate_pair_id` référence `candidate_pairs(candidate_pair_id)` avec l'action par défaut (NO ACTION) +- `feature_set_id_a` et `feature_set_id_b` référencent `feature_sets(feature_set_id)` +- `feature_set_id_a` appartient à `image_id_a` de la Candidate Pair, `feature_set_id_b` appartient à `image_id_b` (validé par l'API create) +- `NO_MATCH` impose `match_count=0` et aucun asset +- `MATCHED` impose `match_count>0` et SHA/path/taille complets +- les échecs d'exécution restent dans le Task Runtime et ne créent pas de ligne +- `matcher_kind` borné à 64 caractères +- `parameter_fingerprint` exactement 32 octets (SHA-256) + +**API** : +- `lardon3d_project_db_create_match_result()` — INSERT avec validation des contraintes +- `lardon3d_project_db_load_match_result()` — SELECT par ID +- `lardon3d_project_db_find_match_result()` — SELECT par (candidate_pair_id, feature_set_id_a, feature_set_id_b, matcher_kind, matcher_version, parameter_fingerprint) +- `lardon3d_project_db_list_match_results()` — SELECT paginé ORDER BY id + ## Ouverture et migrations Une DB vide reçoit directement le schéma v7 dans une transaction @@ -452,7 +513,7 @@ ouvert. ## Statut -**IMPLEMENTED** — SQLite système, schéma v9 et migrations v1→v2→v3→v4→v5→v6→v7→v8→v9, identité +**IMPLEMENTED** — SQLite système, schéma v10 et migrations v1→v2→v3→v4→v5→v6→v7→v8→v9→v10, identité projet, transactions tâche+checkpoint, pagination de reprise et artefacts génériques. diff --git a/docs/architecture/reconstruction_pipeline.md b/docs/architecture/reconstruction_pipeline.md index 4937cc5..cc7c5be 100644 --- a/docs/architecture/reconstruction_pipeline.md +++ b/docs/architecture/reconstruction_pipeline.md @@ -79,7 +79,7 @@ updates incrémentales et query top-K bornée. Le générateur de paires reste p |--------|-------------| | **Sources de paires candidates** | (1) Visual index : paires visuellement proches. (2) Proximité temporelle. (3) Scan set commun. (4) Géométrie approximative (si GPS/IMU disponible). | | **Matching coûteux limité** | Le nombre de paires soumises au matching géométrique (étape F) doit être borné. Le candidate generator filtre et classe pour ne garder que les paires les plus prometteuses. | -| **Persistance** | Les paires candidates sont persistées dans la table `candidate_pairs` (Project DB v9). Ordre canonique : `image_id_a < image_id_b`. Self-pairs interdits. Unicité garantie. | +| **Persistance** | Les paires candidates sont persistées dans la table `candidate_pairs` (Project DB v10). Ordre canonique : `image_id_a < image_id_b`. Self-pairs interdits. Unicité garantie. | | **Déterminisme** | Pour mêmes entrées et configuration, le générateur produit les mêmes paires dans le même ordre. | | **Idempotence** | L'exécution répétée ne crée pas de doublons. | diff --git a/docs/architecture/resource_governor.md b/docs/architecture/resource_governor.md index 802b767..7147ae6 100644 --- a/docs/architecture/resource_governor.md +++ b/docs/architecture/resource_governor.md @@ -94,6 +94,11 @@ Le Resource Governor est l'unique propriétaire des budgets (RAM, GPU, CPU, IO). réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ». Chaque séquence interroge le Visual Index pour jusqu'à 64 Feature Sets et persiste les paires candidates avec idempotence. +- Le Matcher v1 possède un working set contrôlé inférieur à environ 10 Mio au + maximum SIFT/RootSIFT (8 Mio de descripteurs contigus, KNN `k=2`, sorties et + fichier bornés), hors scratch interne OpenCV. Il n'est pas encore exposé comme + task kind autonome; sa future orchestration devra réserver CPU+IO et cette + estimation via ce Governor, sans budget parallèle. ## Limites actuelles diff --git a/include/lardon3d/match_file.h b/include/lardon3d/match_file.h new file mode 100644 index 0000000..46f4624 --- /dev/null +++ b/include/lardon3d/match_file.h @@ -0,0 +1,86 @@ +#ifndef LARDON3D_MATCH_FILE_H +#define LARDON3D_MATCH_FILE_H + +#include +#include +#include + +enum { + LARDON3D_MATCH_FILE_VERSION = 1, + LARDON3D_MATCH_FILE_HEADER_SIZE = 32, + LARDON3D_MATCH_FILE_ENTRY_SIZE = 12, + LARDON3D_MATCH_FILE_MAX_MATCHES = 8192, + LARDON3D_MATCH_FILE_MAX_SIZE = 98336, +}; + +#define LARDON3D_MATCH_FILE_MAGIC "L3DM" + +typedef struct { + uint32_t feature_index_a; + uint32_t feature_index_b; + float distance; +} Lardon3DMatchFileEntry; + +typedef struct { + unsigned char magic[4]; + uint8_t format_version; + uint8_t descriptor_type; + uint16_t reserved; + uint32_t match_count; + uint32_t descriptor_dimension; + uint64_t feature_set_id_a; + uint64_t feature_set_id_b; +} Lardon3DMatchFileHeader; + +typedef enum { + LARDON3D_MATCH_FILE_OK = 0, + LARDON3D_MATCH_FILE_INVALID_ARGUMENT, + LARDON3D_MATCH_FILE_IO_ERROR, + LARDON3D_MATCH_FILE_BAD_MAGIC, + LARDON3D_MATCH_FILE_BAD_VERSION, + LARDON3D_MATCH_FILE_BAD_TYPE, + LARDON3D_MATCH_FILE_BAD_COUNT, + LARDON3D_MATCH_FILE_BAD_SIZE, + LARDON3D_MATCH_FILE_BAD_ENTRY, + LARDON3D_MATCH_FILE_CORRUPT, +} Lardon3DMatchFileResult; + +Lardon3DMatchFileResult lardon3d_match_file_write( + int fd, + uint8_t descriptor_type, + uint32_t descriptor_dimension, + uint64_t feature_set_id_a, + uint64_t feature_set_id_b, + const Lardon3DMatchFileEntry *entries, + uint32_t match_count); + +Lardon3DMatchFileResult lardon3d_match_file_read( + int fd, + Lardon3DMatchFileHeader *header, + Lardon3DMatchFileEntry *entries, + size_t entry_capacity, + uint32_t *match_count, + uint64_t expected_feature_set_id_a, + uint64_t expected_feature_set_id_b, + uint32_t expected_feature_count_a, + uint32_t expected_feature_count_b); + +Lardon3DMatchFileResult lardon3d_match_file_validate( + const char *path, + Lardon3DMatchFileHeader *header, + uint64_t expected_feature_set_id_a, + uint64_t expected_feature_set_id_b, + uint32_t expected_feature_count_a, + uint32_t expected_feature_count_b); + +Lardon3DMatchFileResult lardon3d_match_file_validate_asset( + const char *path, + const unsigned char expected_sha256[32], + uint64_t expected_size, + Lardon3DMatchFileHeader *header, + uint64_t expected_feature_set_id_a, + uint64_t expected_feature_set_id_b, + uint32_t expected_feature_count_a, + uint32_t expected_feature_count_b); + +#endif diff --git a/include/lardon3d/matcher.h b/include/lardon3d/matcher.h new file mode 100644 index 0000000..8d20fb0 --- /dev/null +++ b/include/lardon3d/matcher.h @@ -0,0 +1,91 @@ +#ifndef LARDON3D_MATCHER_H +#define LARDON3D_MATCHER_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + LARDON3D_MATCHER_VERSION = 1, + LARDON3D_MATCHER_KNN_K = 2, +}; + +typedef enum { + LARDON3D_MATCHER_ORB_BF = 0, + LARDON3D_MATCHER_SIFT_BF = 1, + LARDON3D_MATCHER_ROOTSIFT_BF = 2, +} Lardon3DMatcherKind; + +typedef struct { + Lardon3DMatcherKind kind; + float ratio_threshold; +} Lardon3DMatcherParams; + +typedef struct { + uint32_t knn_query_count; + uint32_t match_count; + uint32_t feature_count_a; + uint32_t feature_count_b; + uint64_t feature_open_ns; + uint64_t descriptor_read_ns; + uint64_t knn_ns; + uint64_t filter_ns; + uint64_t canonicalize_ns; + uint64_t serialize_ns; + uint64_t sha256_ns; + uint64_t publication_ns; + uint64_t database_ns; + uint64_t total_ns; +} Lardon3DMatcherStats; + +typedef enum { + LARDON3D_MATCHER_OK = 0, + LARDON3D_MATCHER_INVALID_ARGUMENT, + LARDON3D_MATCHER_IO_ERROR, + LARDON3D_MATCHER_TYPE_MISMATCH, + LARDON3D_MATCHER_FAILED, +} Lardon3DMatcherResult; + +const char *lardon3d_matcher_kind_string(Lardon3DMatcherKind kind); + +float lardon3d_matcher_default_ratio(Lardon3DMatcherKind kind); + +void lardon3d_matcher_fingerprint(const Lardon3DMatcherParams *params, + unsigned char fingerprint[32]); + +Lardon3DMatcherResult lardon3d_matcher_run( + const char *project_path, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + const char *match_file_path, + Lardon3DMatcherStats *stats); + +Lardon3DMatcherResult lardon3d_matcher_match_and_publish( + const char *project_path, + Lardon3DProjectDb *database, + const Lardon3DProjectDbCandidatePair *pair, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + Lardon3DProjectDbMatchResult *result); + +Lardon3DMatcherResult lardon3d_matcher_match_and_publish_profiled( + const char *project_path, + Lardon3DProjectDb *database, + const Lardon3DProjectDbCandidatePair *pair, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + Lardon3DProjectDbMatchResult *result, + Lardon3DMatcherStats *stats); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/lardon3d/project_db.h b/include/lardon3d/project_db.h index d66be84..85ebbee 100644 --- a/include/lardon3d/project_db.h +++ b/include/lardon3d/project_db.h @@ -9,7 +9,7 @@ #include enum { - LARDON3D_PROJECT_DB_SCHEMA_VERSION = 9, + LARDON3D_PROJECT_DB_SCHEMA_VERSION = 10, LARDON3D_PROJECT_DB_ID_CAPACITY = 65, LARDON3D_PROJECT_DB_KIND_CAPACITY = 65, LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096, @@ -17,6 +17,7 @@ enum { LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256, LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX = 256, + LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX = 256, LARDON3D_PROJECT_DB_SCANSET_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256, LARDON3D_PROJECT_DB_SHA256_SIZE = 32, @@ -130,11 +131,33 @@ typedef struct { int64_t created_at; } Lardon3DProjectDbCandidatePair; +typedef struct { + uint64_t match_result_id; + uint64_t candidate_pair_id; + uint64_t feature_set_id_a; + uint64_t feature_set_id_b; + char matcher_kind[LARDON3D_PROJECT_DB_KIND_CAPACITY]; + uint32_t matcher_version; + unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE]; + int result_status; + uint32_t match_count; + bool has_match_asset; + unsigned char match_asset_sha256[LARDON3D_PROJECT_DB_SHA256_SIZE]; + char match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + uint64_t match_asset_size_bytes; + int64_t created_at; +} Lardon3DProjectDbMatchResult; + typedef enum { LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0, LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT } Lardon3DProjectDbImageRegisterStatus; +enum { + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH = 0, + LARDON3D_MATCH_RESULT_STATUS_MATCHED = 1, +}; + typedef enum { LARDON3D_DB_FEATURE_ASSET_DURABLE = 0, LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE = 1 @@ -441,4 +464,29 @@ Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDbCandidatePairGenerateTask *parameters); +Lardon3DProjectDbResult lardon3d_project_db_create_match_result( + Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a, + uint64_t feature_set_id_b, const char *matcher_kind, uint32_t matcher_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], int result_status, + uint32_t match_count, + const unsigned char *match_asset_sha256, const char *match_asset_path, + uint64_t match_asset_size_bytes, int64_t created_at, + Lardon3DProjectDbMatchResult *result); +Lardon3DProjectDbResult lardon3d_project_db_load_match_result( + Lardon3DProjectDb *database, uint64_t match_result_id, Lardon3DProjectDbMatchResult *result); +Lardon3DProjectDbResult lardon3d_project_db_repair_match_result( + Lardon3DProjectDb *database, uint64_t match_result_id, int result_status, + uint32_t match_count, const unsigned char *match_asset_sha256, + const char *match_asset_path, uint64_t match_asset_size_bytes, + Lardon3DProjectDbMatchResult *result); +Lardon3DProjectDbResult lardon3d_project_db_find_match_result( + Lardon3DProjectDb *database, uint64_t candidate_pair_id, + uint64_t feature_set_id_a, uint64_t feature_set_id_b, + const char *matcher_kind, uint32_t matcher_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DProjectDbMatchResult *result); +Lardon3DProjectDbResult lardon3d_project_db_list_match_results( + Lardon3DProjectDb *database, uint64_t after_match_result_id, + Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count); + #endif diff --git a/meson.build b/meson.build index 5a6aeec..9b0a3d9 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,7 @@ executable( 'src/image_catalog_persistent.c', 'src/feature_extractor_opencv.cpp', 'src/feature_store.c', + 'src/feature_extractor_opencv.cpp', 'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c', 'src/visual_index.c', 'src/visual_index_task.c', @@ -75,6 +76,8 @@ executable( 'src/resource_governor.c', 'src/resource_snapshot.c', 'src/hardware_profile.c', + 'src/match_file.c', + 'src/matcher.cpp', ], include_directories: include_directories('include'), dependencies: [ncursesw, threads, sqlite3, openssl, opencv], @@ -389,6 +392,22 @@ project_db_test = executable( test('project-db', project_db_test, timeout: 30) +match_result_test = executable( + 'test-match-result', + sources: [ + 'tests/test_match_result.c', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl], +) + +test('match-result', match_result_test, timeout: 30) + project_test = executable( 'test-project', sources: [ @@ -517,3 +536,65 @@ resource_reservation_test = executable( ) test('resource-reservation', resource_reservation_test, timeout: 60) + +matcher_test = executable( + 'test-matcher', + sources: [ + 'tests/test_matcher.c', + 'src/match_file.c', + 'src/matcher.cpp', + 'src/feature_store.c', + 'src/feature_extractor_opencv.cpp', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + 'src/image_catalog_persistent.c', + 'src/app_state.c', + ], + c_args: ['-DLARDON3D_PROJECT_DB_TESTING'], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv], +) + +test('matcher', matcher_test, timeout: 60) + +matcher_e2e_test = executable( + 'test-matcher-e2e', + sources: [ + 'tests/test_matcher_e2e.c', + 'src/match_file.c', + 'src/matcher.cpp', + 'src/feature_store.c', + 'src/feature_extractor_opencv.cpp', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + 'src/image_catalog_persistent.c', + 'src/app_state.c', + ], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv], +) + +test('matcher-e2e', matcher_e2e_test, timeout: 60) + +executable( + 'benchmark-matcher', + sources: [ + 'tests/benchmark_matcher.cpp', + 'src/match_file.c', + 'src/matcher.cpp', + 'src/feature_store.c', + 'src/feature_extractor_opencv.cpp', + 'src/project_db.c', + 'src/task.c', + 'src/resource_governor.c', + 'src/resource_snapshot.c', + 'src/image_catalog_persistent.c', + 'src/app_state.c', + ], + include_directories: include_directories('include'), + dependencies: [threads, sqlite3, openssl, opencv], +) diff --git a/src/match_file.c b/src/match_file.c new file mode 100644 index 0000000..34cfa7a --- /dev/null +++ b/src/match_file.c @@ -0,0 +1,280 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +enum { FT_U8 = 1, FT_F32 = 2 }; + +static void put_u32(unsigned char *p, uint32_t v) { + p[0] = (unsigned char)v; + p[1] = (unsigned char)(v >> 8); + p[2] = (unsigned char)(v >> 16); + p[3] = (unsigned char)(v >> 24); +} + +static uint32_t get_u32(const unsigned char *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | + ((uint32_t)p[3] << 24); +} + +static void put_u64(unsigned char *p, uint64_t v) { + put_u32(p, (uint32_t)v); + put_u32(p + 4, (uint32_t)(v >> 32)); +} + +static uint64_t get_u64(const unsigned char *p) { + return (uint64_t)get_u32(p) | ((uint64_t)get_u32(p + 4) << 32); +} + +static void put_float(unsigned char *p, float value) { + uint32_t bits = 0; + memcpy(&bits, &value, sizeof(bits)); + put_u32(p, bits); +} + +static float get_float(const unsigned char *p) { + uint32_t bits = get_u32(p); + float value = 0; + memcpy(&value, &bits, sizeof(value)); + return value; +} + +static bool write_exact(int fd, const void *data, size_t size) { + const unsigned char *bytes = data; + size_t used = 0; + while (used < size) { + ssize_t n = write(fd, bytes + used, size - used); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + used += (size_t)n; + } + return true; +} + +static bool read_exact(int fd, void *data, size_t size) { + unsigned char *bytes = data; + size_t used = 0; + while (used < size) { + ssize_t n = read(fd, bytes + used, size - used); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + used += (size_t)n; + } + return true; +} + +static bool valid_dimension(uint8_t type, uint32_t dimension) { + return (type == FT_U8 && dimension == 32) || (type == FT_F32 && dimension == 128); +} + +static void decode_header(const unsigned char raw[32], Lardon3DMatchFileHeader *header) { + memcpy(header->magic, raw, 4); + header->format_version = raw[4]; + header->descriptor_type = raw[5]; + header->reserved = (uint16_t)(raw[6] | ((uint16_t)raw[7] << 8)); + header->match_count = get_u32(raw + 8); + header->descriptor_dimension = get_u32(raw + 12); + header->feature_set_id_a = get_u64(raw + 16); + header->feature_set_id_b = get_u64(raw + 24); +} + +static Lardon3DMatchFileResult validate_header(const Lardon3DMatchFileHeader *header, + uint64_t file_size, + uint64_t expected_a, uint64_t expected_b) { + if (memcmp(header->magic, LARDON3D_MATCH_FILE_MAGIC, 4) != 0) + return LARDON3D_MATCH_FILE_BAD_MAGIC; + if (header->format_version != LARDON3D_MATCH_FILE_VERSION) + return LARDON3D_MATCH_FILE_BAD_VERSION; + if (header->reserved != 0) return LARDON3D_MATCH_FILE_CORRUPT; + if (!valid_dimension(header->descriptor_type, header->descriptor_dimension)) + return LARDON3D_MATCH_FILE_BAD_TYPE; + if (header->match_count > LARDON3D_MATCH_FILE_MAX_MATCHES) + return LARDON3D_MATCH_FILE_BAD_COUNT; + if (header->feature_set_id_a == 0 || header->feature_set_id_b == 0 || + header->feature_set_id_a == header->feature_set_id_b) + return LARDON3D_MATCH_FILE_CORRUPT; + if ((expected_a != 0 && header->feature_set_id_a != expected_a) || + (expected_b != 0 && header->feature_set_id_b != expected_b)) + return LARDON3D_MATCH_FILE_CORRUPT; + uint64_t expected_size = LARDON3D_MATCH_FILE_HEADER_SIZE + + (uint64_t)header->match_count * LARDON3D_MATCH_FILE_ENTRY_SIZE; + return file_size == expected_size ? LARDON3D_MATCH_FILE_OK : LARDON3D_MATCH_FILE_BAD_SIZE; +} + +static bool valid_entry(const Lardon3DMatchFileEntry *entry, uint32_t count_a, + uint32_t count_b) { + uint32_t limit_a = count_a == 0 ? 8192 : count_a; + uint32_t limit_b = count_b == 0 ? 8192 : count_b; + return entry->feature_index_a < limit_a && entry->feature_index_b < limit_b && + isfinite(entry->distance) && entry->distance >= 0.0F; +} + +Lardon3DMatchFileResult lardon3d_match_file_write( + int fd, uint8_t descriptor_type, uint32_t descriptor_dimension, + uint64_t feature_set_id_a, uint64_t feature_set_id_b, + const Lardon3DMatchFileEntry *entries, uint32_t match_count) { + if (fd < 0 || !valid_dimension(descriptor_type, descriptor_dimension) || + feature_set_id_a == 0 || feature_set_id_b == 0 || feature_set_id_a == feature_set_id_b || + match_count > LARDON3D_MATCH_FILE_MAX_MATCHES || (match_count > 0 && !entries)) + return LARDON3D_MATCH_FILE_INVALID_ARGUMENT; + + size_t file_size = LARDON3D_MATCH_FILE_HEADER_SIZE + + (size_t)match_count * LARDON3D_MATCH_FILE_ENTRY_SIZE; + unsigned char *raw = calloc(1, file_size); + if (!raw) return LARDON3D_MATCH_FILE_IO_ERROR; + memcpy(raw, LARDON3D_MATCH_FILE_MAGIC, 4); + raw[4] = LARDON3D_MATCH_FILE_VERSION; + raw[5] = descriptor_type; + put_u32(raw + 8, match_count); + put_u32(raw + 12, descriptor_dimension); + put_u64(raw + 16, feature_set_id_a); + put_u64(raw + 24, feature_set_id_b); + for (uint32_t i = 0; i < match_count; ++i) { + if (!valid_entry(&entries[i], 0, 0) || + (i > 0 && entries[i].feature_index_a <= entries[i - 1].feature_index_a)) { + free(raw); + return LARDON3D_MATCH_FILE_BAD_ENTRY; + } + unsigned char *item = raw + LARDON3D_MATCH_FILE_HEADER_SIZE + + (size_t)i * LARDON3D_MATCH_FILE_ENTRY_SIZE; + put_u32(item, entries[i].feature_index_a); + put_u32(item + 4, entries[i].feature_index_b); + put_float(item + 8, entries[i].distance); + } + bool written = write_exact(fd, raw, file_size); + free(raw); + return written ? LARDON3D_MATCH_FILE_OK : LARDON3D_MATCH_FILE_IO_ERROR; +} + +Lardon3DMatchFileResult lardon3d_match_file_read( + int fd, Lardon3DMatchFileHeader *header, Lardon3DMatchFileEntry *entries, + size_t entry_capacity, uint32_t *match_count, uint64_t expected_a, + uint64_t expected_b, uint32_t count_a, uint32_t count_b) { + if (fd < 0 || !header || !match_count || (!entries && entry_capacity != 0) || + count_a > 8192 || count_b > 8192) + return LARDON3D_MATCH_FILE_INVALID_ARGUMENT; + *match_count = 0; + memset(header, 0, sizeof(*header)); + struct stat info; + if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0) + return LARDON3D_MATCH_FILE_IO_ERROR; + unsigned char raw[32]; + if (!read_exact(fd, raw, sizeof(raw))) return LARDON3D_MATCH_FILE_BAD_SIZE; + decode_header(raw, header); + Lardon3DMatchFileResult result = validate_header(header, (uint64_t)info.st_size, + expected_a, expected_b); + if (result != LARDON3D_MATCH_FILE_OK) return result; + if (header->match_count > entry_capacity) return LARDON3D_MATCH_FILE_BAD_COUNT; + for (uint32_t i = 0; i < header->match_count; ++i) { + unsigned char item[12]; + if (!read_exact(fd, item, sizeof(item))) return LARDON3D_MATCH_FILE_BAD_SIZE; + entries[i].feature_index_a = get_u32(item); + entries[i].feature_index_b = get_u32(item + 4); + entries[i].distance = get_float(item + 8); + if (!valid_entry(&entries[i], count_a, count_b) || + (i > 0 && entries[i].feature_index_a <= entries[i - 1].feature_index_a)) + return LARDON3D_MATCH_FILE_BAD_ENTRY; + } + *match_count = header->match_count; + return LARDON3D_MATCH_FILE_OK; +} + +Lardon3DMatchFileResult lardon3d_match_file_validate( + const char *path, Lardon3DMatchFileHeader *header, uint64_t expected_a, + uint64_t expected_b, uint32_t count_a, uint32_t count_b) { + if (!path || !header || count_a > 8192 || count_b > 8192) + return LARDON3D_MATCH_FILE_INVALID_ARGUMENT; + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return LARDON3D_MATCH_FILE_IO_ERROR; + struct stat info; + unsigned char raw[32]; + Lardon3DMatchFileResult result = LARDON3D_MATCH_FILE_IO_ERROR; + if (fstat(fd, &info) == 0 && S_ISREG(info.st_mode) && info.st_size >= 0) { + if (!read_exact(fd, raw, sizeof(raw))) { + result = LARDON3D_MATCH_FILE_BAD_SIZE; + } else { + decode_header(raw, header); + result = validate_header(header, (uint64_t)info.st_size, expected_a, expected_b); + if (result == LARDON3D_MATCH_FILE_OK) { + Lardon3DMatchFileEntry entry; + uint32_t previous_a = 0; + for (uint32_t i = 0; i < header->match_count; ++i) { + unsigned char item[12]; + if (!read_exact(fd, item, sizeof(item))) { + result = LARDON3D_MATCH_FILE_BAD_SIZE; + break; + } + entry.feature_index_a = get_u32(item); + entry.feature_index_b = get_u32(item + 4); + entry.distance = get_float(item + 8); + if (!valid_entry(&entry, count_a, count_b) || + (i > 0 && entry.feature_index_a <= previous_a)) { + result = LARDON3D_MATCH_FILE_BAD_ENTRY; + break; + } + previous_a = entry.feature_index_a; + } + } + } + } + (void)close(fd); + return result; +} + +Lardon3DMatchFileResult lardon3d_match_file_validate_asset( + const char *path, const unsigned char expected_sha256[32], uint64_t expected_size, + Lardon3DMatchFileHeader *header, uint64_t expected_a, uint64_t expected_b, + uint32_t count_a, uint32_t count_b) { + if (!path || !expected_sha256 || !header || expected_size < LARDON3D_MATCH_FILE_HEADER_SIZE || + expected_size > LARDON3D_MATCH_FILE_MAX_SIZE || count_a > 8192 || count_b > 8192) + return LARDON3D_MATCH_FILE_INVALID_ARGUMENT; + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return LARDON3D_MATCH_FILE_IO_ERROR; + struct stat info; + if (fstat(fd, &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0 || + (uint64_t)info.st_size != expected_size) { + (void)close(fd); + return LARDON3D_MATCH_FILE_BAD_SIZE; + } + unsigned char *bytes = malloc((size_t)expected_size); + if (!bytes) { + (void)close(fd); + return LARDON3D_MATCH_FILE_IO_ERROR; + } + bool read_ok = read_exact(fd, bytes, (size_t)expected_size); + (void)close(fd); + if (!read_ok) { + free(bytes); + return LARDON3D_MATCH_FILE_BAD_SIZE; + } + unsigned char actual_sha256[32]; + unsigned int digest_size = 0; + bool hash_ok = EVP_Digest(bytes, (size_t)expected_size, actual_sha256, &digest_size, + EVP_sha256(), NULL) == 1 && digest_size == 32; + if (!hash_ok || memcmp(actual_sha256, expected_sha256, 32) != 0) { + free(bytes); + return LARDON3D_MATCH_FILE_CORRUPT; + } + decode_header(bytes, header); + Lardon3DMatchFileResult result = validate_header(header, expected_size, expected_a, expected_b); + uint32_t previous_a = 0; + for (uint32_t i = 0; result == LARDON3D_MATCH_FILE_OK && i < header->match_count; ++i) { + const unsigned char *item = bytes + LARDON3D_MATCH_FILE_HEADER_SIZE + + (size_t)i * LARDON3D_MATCH_FILE_ENTRY_SIZE; + Lardon3DMatchFileEntry entry = {get_u32(item), get_u32(item + 4), get_float(item + 8)}; + if (!valid_entry(&entry, count_a, count_b) || + (i > 0 && entry.feature_index_a <= previous_a)) + result = LARDON3D_MATCH_FILE_BAD_ENTRY; + previous_a = entry.feature_index_a; + } + free(bytes); + return result; +} diff --git a/src/matcher.cpp b/src/matcher.cpp new file mode 100644 index 0000000..1db7d5d --- /dev/null +++ b/src/matcher.cpp @@ -0,0 +1,695 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +extern "C" { +#include +#include +#include +} + +static const char matcher_orb_str[] = "orb_bf"; +static const char matcher_sift_str[] = "sift_bf"; +static const char matcher_rootsift_str[] = "rootsift_bf"; + +static uint64_t elapsed_ns(std::chrono::steady_clock::time_point start) { + auto elapsed = std::chrono::steady_clock::now() - start; + return (uint64_t)std::chrono::duration_cast(elapsed).count(); +} + +static bool join_path(char output[4096], const char *a, const char *b) { + int n = snprintf(output, 4096, "%s/%s", a, b); + return n > 0 && (size_t)n < 4096; +} + +static bool ensure_directory(const char *path) { + if (mkdir(path, 0755) == 0) { + return true; + } + if (errno != EEXIST) { + return false; + } + struct stat info; + return lstat(path, &info) == 0 && S_ISDIR(info.st_mode) && !S_ISLNK(info.st_mode); +} + +static bool sync_directory(const char *path) { + int fd = open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) { + return false; + } + bool ok = fsync(fd) == 0; + if (close(fd) != 0) { + ok = false; + } + return ok; +} + +static void hex_sha(const unsigned char hash[32], char text[65]) { + static const char digits[] = "0123456789abcdef"; + for (size_t i = 0; i < 32; ++i) { + text[2 * i] = digits[hash[i] >> 4]; + text[2 * i + 1] = digits[hash[i] & 15]; + } + text[64] = '\0'; +} + +static bool sha256_fd(int fd, unsigned char output[32]) { + EVP_MD_CTX *context = EVP_MD_CTX_new(); + if (!context) { + return false; + } + bool ok = EVP_DigestInit_ex(context, EVP_sha256(), NULL) == 1; + unsigned char buffer[65536]; + off_t offset = 0; + while (ok) { + ssize_t n = pread(fd, buffer, sizeof(buffer), offset); + if (n < 0 && errno == EINTR) { + continue; + } + if (n < 0) { + ok = false; + break; + } + if (n == 0) { + break; + } + ok = EVP_DigestUpdate(context, buffer, (size_t)n) == 1; + offset += (off_t)n; + } + unsigned int length = 0; + ok = ok && EVP_DigestFinal_ex(context, output, &length) == 1 && length == 32; + EVP_MD_CTX_free(context); + return ok; +} + +static bool sha256_file(const char *path, unsigned char output[32]) { + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { + return false; + } + bool ok = sha256_fd(fd, output); + (void)close(fd); + return ok; +} + +/* --- matcher_kind helpers --- */ + +extern "C" const char *lardon3d_matcher_kind_string(Lardon3DMatcherKind kind) { + switch (kind) { + case LARDON3D_MATCHER_ORB_BF: + return matcher_orb_str; + case LARDON3D_MATCHER_SIFT_BF: + return matcher_sift_str; + case LARDON3D_MATCHER_ROOTSIFT_BF: + return matcher_rootsift_str; + default: + return "unknown"; + } +} + +extern "C" float lardon3d_matcher_default_ratio(Lardon3DMatcherKind kind) { + switch (kind) { + case LARDON3D_MATCHER_ORB_BF: + return 0.75F; + case LARDON3D_MATCHER_SIFT_BF: + case LARDON3D_MATCHER_ROOTSIFT_BF: + return 0.7F; + default: + return 0.75F; + } +} + +extern "C" void lardon3d_matcher_fingerprint(const Lardon3DMatcherParams *params, + unsigned char fingerprint[32]) { + memset(fingerprint, 0, 32); + if (!params) { + return; + } + const char *kind_str = lardon3d_matcher_kind_string(params->kind); + size_t kind_len = strlen(kind_str); + size_t total = kind_len + 4 + 4 + 4 + 4; /* kind + version + knn_k + ratio + cross_check */ + unsigned char buffer[128]; /* enough for any kind string */ + if (total > sizeof(buffer)) { + return; + } + memcpy(buffer, kind_str, kind_len); + uint32_t ratio_bits = 0; + memcpy(&ratio_bits, ¶ms->ratio_threshold, sizeof(ratio_bits)); + auto put_le32 = [](unsigned char *p, uint32_t value) { + p[0] = (unsigned char)value; + p[1] = (unsigned char)(value >> 8); + p[2] = (unsigned char)(value >> 16); + p[3] = (unsigned char)(value >> 24); + }; + put_le32(buffer + kind_len, LARDON3D_MATCHER_VERSION); + put_le32(buffer + kind_len + 4, LARDON3D_MATCHER_KNN_K); + put_le32(buffer + kind_len + 8, ratio_bits); + put_le32(buffer + kind_len + 12, 0); + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (ctx) { + if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) && + EVP_DigestUpdate(ctx, buffer, total) && + EVP_DigestFinal_ex(ctx, fingerprint, NULL)) { + /* success */ + } + EVP_MD_CTX_free(ctx); + } +} + +/* --- Matching logic --- */ + +struct MatchEntry { + int query_idx; + int train_idx; + float distance; +}; + +struct FeatureReaderPair { + Lardon3DFeatureReader *a = nullptr; + Lardon3DFeatureReader *b = nullptr; + + ~FeatureReaderPair() { + lardon3d_feature_reader_close(a); + lardon3d_feature_reader_close(b); + } +}; + +static bool accept_knn_match(const std::vector &knn, float threshold, + MatchEntry *entry) { + if (!entry || knn.empty() || !std::isfinite(threshold) || threshold <= 0.0F || + threshold >= 1.0F) { + return false; + } + const cv::DMatch &first = knn[0]; + if (first.queryIdx < 0 || first.trainIdx < 0 || !std::isfinite(first.distance) || + first.distance < 0.0F) { + return false; + } + if (knn.size() >= 2) { + const float second = knn[1].distance; + if (!std::isfinite(second) || second < 0.0F || second == 0.0F || + !(first.distance < threshold * second)) { + return false; + } + } + *entry = {first.queryIdx, first.trainIdx, first.distance}; + return true; +} + +static bool operator<(const MatchEntry &a, const MatchEntry &b) { + if (a.query_idx != b.query_idx) { + return a.query_idx < b.query_idx; + } + if (a.distance != b.distance) { + return a.distance < b.distance; + } + return a.train_idx < b.train_idx; +} + +static bool fill_descriptors_u8(std::vector &buffer, + Lardon3DFeatureReader *reader, + uint32_t feature_count, + uint32_t descriptor_dimension) { + size_t total = (size_t)feature_count * descriptor_dimension; + buffer.resize(total); + size_t offset = 0; + for (uint32_t start = 0; start < feature_count; start += 256) { + uint32_t batch = feature_count - start; + if (batch > 256) { + batch = 256; + } + size_t batch_bytes = (size_t)batch * descriptor_dimension; + Lardon3DFeatureStoreResult r = lardon3d_feature_reader_descriptors_u8( + reader, start, buffer.data() + offset, batch, batch_bytes); + if (r != LARDON3D_FEATURE_STORE_OK) { + return false; + } + offset += batch_bytes; + } + return true; +} + +static bool fill_descriptors_f32(std::vector &buffer, + Lardon3DFeatureReader *reader, + uint32_t feature_count, + uint32_t descriptor_dimension) { + size_t total = (size_t)feature_count * descriptor_dimension; + buffer.resize(total); + size_t offset = 0; + for (uint32_t start = 0; start < feature_count; start += 256) { + uint32_t batch = feature_count - start; + if (batch > 256) { + batch = 256; + } + size_t batch_bytes = (size_t)batch * descriptor_dimension * sizeof(float); + Lardon3DFeatureStoreResult r = lardon3d_feature_reader_descriptors_f32( + reader, start, buffer.data() + offset, batch, batch_bytes); + if (r != LARDON3D_FEATURE_STORE_OK) { + return false; + } + offset += (size_t)batch * descriptor_dimension; + } + return true; +} + +static Lardon3DMatcherResult matcher_run_impl( + const char *project_path, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + const char *match_file_path, + Lardon3DMatcherStats *stats) { + if (stats) { + memset(stats, 0, sizeof(*stats)); + } + if (!project_path || !feature_set_a || !feature_set_b || !params || !match_file_path || + !stats || params->kind < LARDON3D_MATCHER_ORB_BF || + params->kind > LARDON3D_MATCHER_ROOTSIFT_BF || + !std::isfinite(params->ratio_threshold) || params->ratio_threshold <= 0.0F || + params->ratio_threshold >= 1.0F) { + return LARDON3D_MATCHER_INVALID_ARGUMENT; + } + if (feature_set_a->feature_count > LARDON3D_FEATURE_MAX_FEATURES || + feature_set_b->feature_count > LARDON3D_FEATURE_MAX_FEATURES) { + return LARDON3D_MATCHER_INVALID_ARGUMENT; + } + if (feature_set_a->feature_set_id == feature_set_b->feature_set_id) { + return LARDON3D_MATCHER_INVALID_ARGUMENT; + } + + /* Verify same descriptor type and dimension */ + if (feature_set_a->descriptor_type != feature_set_b->descriptor_type || + feature_set_a->descriptor_dimension != feature_set_b->descriptor_dimension) { + return LARDON3D_MATCHER_TYPE_MISMATCH; + } + + /* Verify descriptor type matches matcher kind */ + if (params->kind == LARDON3D_MATCHER_ORB_BF) { + if (strcmp(feature_set_a->extractor_kind, "orb") != 0 || + strcmp(feature_set_b->extractor_kind, "orb") != 0 || + feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_U8 || + feature_set_a->descriptor_dimension != 32) { + return LARDON3D_MATCHER_TYPE_MISMATCH; + } + } else if (params->kind == LARDON3D_MATCHER_SIFT_BF) { + if (strcmp(feature_set_a->extractor_kind, "sift") != 0 || + strcmp(feature_set_b->extractor_kind, "sift") != 0 || + feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_F32 || + feature_set_a->descriptor_dimension != 128) { + return LARDON3D_MATCHER_TYPE_MISMATCH; + } + } else if (strcmp(feature_set_a->extractor_kind, "rootsift") != 0 || + strcmp(feature_set_b->extractor_kind, "rootsift") != 0 || + feature_set_a->descriptor_type != LARDON3D_FEATURE_DESCRIPTOR_F32 || + feature_set_a->descriptor_dimension != 128) { + return LARDON3D_MATCHER_TYPE_MISMATCH; + } + + stats->feature_count_a = feature_set_a->feature_count; + stats->feature_count_b = feature_set_b->feature_count; + + /* Open feature readers */ + FeatureReaderPair readers; + Lardon3DFeatureFileMetadata meta_a, meta_b; + auto phase_start = std::chrono::steady_clock::now(); + Lardon3DFeatureStoreResult open_a = lardon3d_feature_reader_open( + project_path, feature_set_a, &readers.a, &meta_a); + Lardon3DFeatureStoreResult open_b = lardon3d_feature_reader_open( + project_path, feature_set_b, &readers.b, &meta_b); + if (open_a != LARDON3D_FEATURE_STORE_OK || open_b != LARDON3D_FEATURE_STORE_OK) { + return LARDON3D_MATCHER_IO_ERROR; + } + stats->feature_open_ns = elapsed_ns(phase_start); + + Lardon3DMatcherResult result = LARDON3D_MATCHER_OK; + std::vector filtered_matches; + filtered_matches.reserve(feature_set_a->feature_count); + + if (params->kind == LARDON3D_MATCHER_ORB_BF) { + std::vector desc_a, desc_b; + phase_start = std::chrono::steady_clock::now(); + if (!fill_descriptors_u8(desc_a, readers.a, feature_set_a->feature_count, 32) || + !fill_descriptors_u8(desc_b, readers.b, feature_set_b->feature_count, 32)) { + result = LARDON3D_MATCHER_IO_ERROR; + } else { + stats->descriptor_read_ns = elapsed_ns(phase_start); + cv::Mat mat_a((int)feature_set_a->feature_count, 32, CV_8UC1, desc_a.data()); + cv::Mat mat_b((int)feature_set_b->feature_count, 32, CV_8UC1, desc_b.data()); + cv::BFMatcher matcher(cv::NORM_HAMMING, false); + std::vector> matches; + phase_start = std::chrono::steady_clock::now(); + matcher.knnMatch(mat_a, mat_b, matches, LARDON3D_MATCHER_KNN_K); + stats->knn_ns = elapsed_ns(phase_start); + stats->knn_query_count = (uint32_t)matches.size(); + phase_start = std::chrono::steady_clock::now(); + for (size_t i = 0; i < matches.size(); ++i) { + const auto &knn = matches[i]; + MatchEntry entry; + if (accept_knn_match(knn, params->ratio_threshold, &entry)) + filtered_matches.push_back(entry); + } + stats->filter_ns = elapsed_ns(phase_start); + } + } else { + std::vector desc_a, desc_b; + phase_start = std::chrono::steady_clock::now(); + if (!fill_descriptors_f32(desc_a, readers.a, feature_set_a->feature_count, 128) || + !fill_descriptors_f32(desc_b, readers.b, feature_set_b->feature_count, 128)) { + result = LARDON3D_MATCHER_IO_ERROR; + } else { + stats->descriptor_read_ns = elapsed_ns(phase_start); + cv::Mat mat_a((int)feature_set_a->feature_count, 128, CV_32FC1, desc_a.data()); + cv::Mat mat_b((int)feature_set_b->feature_count, 128, CV_32FC1, desc_b.data()); + cv::BFMatcher matcher(cv::NORM_L2, false); + std::vector> matches; + phase_start = std::chrono::steady_clock::now(); + matcher.knnMatch(mat_a, mat_b, matches, LARDON3D_MATCHER_KNN_K); + stats->knn_ns = elapsed_ns(phase_start); + stats->knn_query_count = (uint32_t)matches.size(); + phase_start = std::chrono::steady_clock::now(); + for (size_t i = 0; i < matches.size(); ++i) { + const auto &knn = matches[i]; + MatchEntry entry; + if (accept_knn_match(knn, params->ratio_threshold, &entry)) + filtered_matches.push_back(entry); + } + stats->filter_ns = elapsed_ns(phase_start); + } + } + + if (result != LARDON3D_MATCHER_OK) { + return result; + } + + phase_start = std::chrono::steady_clock::now(); + /* Sort deterministically */ + std::sort(filtered_matches.begin(), filtered_matches.end()); + + for (size_t i = 1; i < filtered_matches.size(); ++i) { + if (filtered_matches[i].query_idx == filtered_matches[i - 1].query_idx) { + return LARDON3D_MATCHER_FAILED; + } + } + + stats->match_count = (uint32_t)filtered_matches.size(); + stats->canonicalize_ns = elapsed_ns(phase_start); + + if (filtered_matches.size() > LARDON3D_MATCH_FILE_MAX_MATCHES) { + return LARDON3D_MATCHER_FAILED; + } + + /* Build entry array and write */ + std::vector entries(filtered_matches.size()); + for (size_t i = 0; i < filtered_matches.size(); ++i) { + entries[i].feature_index_a = (uint32_t)filtered_matches[i].query_idx; + entries[i].feature_index_b = (uint32_t)filtered_matches[i].train_idx; + entries[i].distance = filtered_matches[i].distance; + } + + uint8_t descriptor_type = feature_set_a->descriptor_type == LARDON3D_FEATURE_DESCRIPTOR_U8 + ? (uint8_t)1 + : (uint8_t)2; + phase_start = std::chrono::steady_clock::now(); + int fd = open(match_file_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd < 0) { + return LARDON3D_MATCHER_IO_ERROR; + } + Lardon3DMatchFileResult write_result = lardon3d_match_file_write( + fd, descriptor_type, feature_set_a->descriptor_dimension, + feature_set_a->feature_set_id, feature_set_b->feature_set_id, + entries.data(), (uint32_t)entries.size()); + if (write_result == LARDON3D_MATCH_FILE_OK) { + if (fsync(fd) != 0) { + write_result = LARDON3D_MATCH_FILE_IO_ERROR; + } + } + (void)close(fd); + if (write_result != LARDON3D_MATCH_FILE_OK) { + unlink(match_file_path); + return LARDON3D_MATCHER_IO_ERROR; + } + stats->serialize_ns = elapsed_ns(phase_start); + + return LARDON3D_MATCHER_OK; +} + +extern "C" Lardon3DMatcherResult lardon3d_matcher_run( + const char *project_path, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + const char *match_file_path, + Lardon3DMatcherStats *stats) { + try { + return matcher_run_impl(project_path, feature_set_a, feature_set_b, params, + match_file_path, stats); + } catch (const cv::Exception &) { + return LARDON3D_MATCHER_FAILED; + } catch (const std::bad_alloc &) { + return LARDON3D_MATCHER_FAILED; + } +} + +extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_profiled( + const char *project_path, + Lardon3DProjectDb *database, + const Lardon3DProjectDbCandidatePair *pair, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + Lardon3DProjectDbMatchResult *result, + Lardon3DMatcherStats *profile) { + auto total_start = std::chrono::steady_clock::now(); + if (profile) memset(profile, 0, sizeof(*profile)); + if (result) { + memset(result, 0, sizeof(*result)); + } + if (!project_path || !database || !pair || !feature_set_a || !feature_set_b || !params || + !result) { + return LARDON3D_MATCHER_INVALID_ARGUMENT; + } + + const char *matcher_kind = lardon3d_matcher_kind_string(params->kind); + unsigned char fp[32]; + lardon3d_matcher_fingerprint(params, fp); + uint64_t repair_match_result_id = 0; + + /* Check if Match Result already exists */ + Lardon3DProjectDbResult found = lardon3d_project_db_find_match_result( + database, pair->candidate_pair_id, feature_set_a->feature_set_id, + feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp, result); + if (found == LARDON3D_PROJECT_DB_OK) { + if (result->result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + result->match_count == 0 && !result->has_match_asset && + result->match_asset_path[0] == '\0' && result->match_asset_size_bytes == 0) { + if (profile) profile->total_ns = elapsed_ns(total_start); + return LARDON3D_MATCHER_OK; + } + if (result->result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + result->match_count > 0 && result->has_match_asset && + result->match_asset_path[0] != '\0' && result->match_asset_size_bytes > 0) { + char asset_full[4096]; + if (join_path(asset_full, project_path, result->match_asset_path)) { + Lardon3DMatchFileHeader header; + if (lardon3d_match_file_validate_asset( + asset_full, result->match_asset_sha256, + result->match_asset_size_bytes, &header, feature_set_a->feature_set_id, + feature_set_b->feature_set_id, feature_set_a->feature_count, + feature_set_b->feature_count) == LARDON3D_MATCH_FILE_OK && + header.match_count == result->match_count && + header.descriptor_type == feature_set_a->descriptor_type && + header.descriptor_dimension == feature_set_a->descriptor_dimension) { + if (profile) profile->total_ns = elapsed_ns(total_start); + return LARDON3D_MATCHER_OK; + } + } + } + repair_match_result_id = result->match_result_id; + } else if (found != LARDON3D_PROJECT_DB_NOT_FOUND) { + return LARDON3D_MATCHER_IO_ERROR; + } + + /* Ensure assets/matches directory */ + char assets[4096], matches_dir[4096]; + if (!join_path(assets, project_path, "assets") || !ensure_directory(assets) || + !join_path(matches_dir, assets, "matches") || !ensure_directory(matches_dir)) { + return LARDON3D_MATCHER_IO_ERROR; + } + + /* Write to temp file */ + char tmp_path[4096]; + int n = snprintf(tmp_path, sizeof(tmp_path), "%s/.match-XXXXXX", matches_dir); + if (n <= 0 || (size_t)n >= sizeof(tmp_path)) { + return LARDON3D_MATCHER_IO_ERROR; + } + int tmp_fd = mkstemp(tmp_path); + if (tmp_fd < 0) { + return LARDON3D_MATCHER_IO_ERROR; + } + + /* matcher_run reopens the named temp with O_TRUNC. */ + if (close(tmp_fd) != 0) { + (void)unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + + Lardon3DMatcherStats stats; + Lardon3DMatcherResult run_result = lardon3d_matcher_run( + project_path, feature_set_a, feature_set_b, params, tmp_path, &stats); + if (run_result != LARDON3D_MATCHER_OK) { + unlink(tmp_path); + return run_result; + } + if (profile) *profile = stats; + + int64_t now = (int64_t)time(NULL); + if (now < 0) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + if (stats.match_count == 0) { + if (unlink(tmp_path) != 0) return LARDON3D_MATCHER_IO_ERROR; + auto database_start = std::chrono::steady_clock::now(); + Lardon3DProjectDbResult db_result = repair_match_result_id != 0 + ? lardon3d_project_db_repair_match_result( + database, repair_match_result_id, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, + 0, NULL, NULL, 0, result) + : lardon3d_project_db_create_match_result( + database, pair->candidate_pair_id, feature_set_a->feature_set_id, + feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp, + LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, NULL, NULL, 0, now, result); + if (db_result == LARDON3D_PROJECT_DB_CONSTRAINT) { + return lardon3d_matcher_match_and_publish_profiled( + project_path, database, pair, feature_set_a, feature_set_b, params, result, + profile); + } + if (profile) { + profile->database_ns = elapsed_ns(database_start); + profile->total_ns = elapsed_ns(total_start); + } + return db_result == LARDON3D_PROJECT_DB_OK ? LARDON3D_MATCHER_OK + : LARDON3D_MATCHER_FAILED; + } + + /* Compute SHA-256 of temp file */ + auto sha_start = std::chrono::steady_clock::now(); + unsigned char file_hash[32]; + if (!sha256_file(tmp_path, file_hash)) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + if (profile) profile->sha256_ns = elapsed_ns(sha_start); + + /* Create content-addressed directory */ + auto publication_start = std::chrono::steady_clock::now(); + char hex[65]; + hex_sha(file_hash, hex); + char prefix[4096]; + n = snprintf(prefix, sizeof(prefix), "%s/%.2s", matches_dir, hex); + if (n <= 0 || (size_t)n >= sizeof(prefix) || !ensure_directory(prefix)) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + + /* Build relative and final paths */ + char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + n = snprintf(relative, sizeof(relative), "assets/matches/%.2s/%s", hex, hex); + if (n <= 0 || (size_t)n >= (int)sizeof(relative)) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + char final_path[4096]; + if (!join_path(final_path, project_path, relative)) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + + /* Atomic content-addressed publication via link. */ + if (link(tmp_path, final_path) != 0) { + if (errno != EEXIST) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + /* EEXIST: verify content match */ + int existing = open(final_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + struct stat info; + unsigned char existing_hash[32]; + bool same = existing >= 0 && fstat(existing, &info) == 0 && S_ISREG(info.st_mode) && + sha256_fd(existing, existing_hash) && + memcmp(existing_hash, file_hash, 32) == 0; + if (existing >= 0) { + (void)close(existing); + } + if (!same) { + if (repair_match_result_id == 0 || rename(tmp_path, final_path) != 0) { + unlink(tmp_path); + return LARDON3D_MATCHER_IO_ERROR; + } + tmp_path[0] = '\0'; + } + } + if (tmp_path[0] != '\0' && unlink(tmp_path) != 0) { + return LARDON3D_MATCHER_IO_ERROR; + } + if (!sync_directory(prefix)) { + return LARDON3D_MATCHER_IO_ERROR; + } + if (profile) profile->publication_ns = elapsed_ns(publication_start); + + /* Get match file size */ + struct stat st; + uint64_t file_size = 0; + if (stat(final_path, &st) == 0 && S_ISREG(st.st_mode)) { + file_size = (uint64_t)st.st_size; + } + + auto database_start = std::chrono::steady_clock::now(); + Lardon3DProjectDbResult db_result = repair_match_result_id != 0 + ? lardon3d_project_db_repair_match_result( + database, repair_match_result_id, LARDON3D_MATCH_RESULT_STATUS_MATCHED, + stats.match_count, file_hash, relative, (uint64_t)file_size, result) + : lardon3d_project_db_create_match_result( + database, pair->candidate_pair_id, feature_set_a->feature_set_id, + feature_set_b->feature_set_id, matcher_kind, LARDON3D_MATCHER_VERSION, fp, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, stats.match_count, file_hash, relative, + (uint64_t)file_size, now, result); + if (db_result == LARDON3D_PROJECT_DB_CONSTRAINT) { + return lardon3d_matcher_match_and_publish_profiled( + project_path, database, pair, feature_set_a, feature_set_b, params, result, profile); + } + if (db_result != LARDON3D_PROJECT_DB_OK) { + return LARDON3D_MATCHER_FAILED; + } + + if (profile) { + profile->database_ns = elapsed_ns(database_start); + profile->total_ns = elapsed_ns(total_start); + } + return LARDON3D_MATCHER_OK; +} + +extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish( + const char *project_path, + Lardon3DProjectDb *database, + const Lardon3DProjectDbCandidatePair *pair, + const Lardon3DProjectDbFeatureSet *feature_set_a, + const Lardon3DProjectDbFeatureSet *feature_set_b, + const Lardon3DMatcherParams *params, + Lardon3DProjectDbMatchResult *result) { + return lardon3d_matcher_match_and_publish_profiled( + project_path, database, pair, feature_set_a, feature_set_b, params, result, NULL); +} diff --git a/src/project_db.c b/src/project_db.c index 9b1b8d9..f5627f8 100644 --- a/src/project_db.c +++ b/src/project_db.c @@ -199,6 +199,33 @@ static const char schema_candidate_generate_v9[] = "scanset_filter INTEGER NOT NULL CHECK(scanset_filter>=0 AND scanset_filter<=2)," "exclude_same_asset INTEGER NOT NULL CHECK(exclude_same_asset IN (0,1)));"; +static const char schema_match_result_v10[] = + "CREATE TABLE match_results(" + "match_result_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(match_result_id>0)," + "candidate_pair_id INTEGER NOT NULL REFERENCES candidate_pairs(candidate_pair_id)," + "feature_set_id_a INTEGER NOT NULL REFERENCES feature_sets(feature_set_id)," + "feature_set_id_b INTEGER NOT NULL REFERENCES feature_sets(feature_set_id)," + "matcher_kind TEXT NOT NULL CHECK(length(matcher_kind)>0 AND length(matcher_kind)<=64)," + "matcher_version INTEGER NOT NULL CHECK(matcher_version>0)," + "parameter_fingerprint BLOB NOT NULL CHECK(length(parameter_fingerprint)=32)," + "result_status INTEGER NOT NULL CHECK(result_status IN (0,1))," + "match_count INTEGER NOT NULL CHECK(match_count>=0 AND match_count<=8192)," + "match_asset_sha256 BLOB CHECK(match_asset_sha256 IS NULL OR" + " length(match_asset_sha256)=32)," + "match_asset_path TEXT CHECK(match_asset_path IS NULL OR length(match_asset_path)>0)," + "match_asset_size_bytes INTEGER CHECK(match_asset_size_bytes IS NULL OR" + " match_asset_size_bytes>0)," + "created_at INTEGER NOT NULL CHECK(created_at>=0)," + "CHECK((result_status=0 AND match_count=0 AND match_asset_sha256 IS NULL AND " + "match_asset_path IS NULL AND match_asset_size_bytes IS NULL) OR " + "(result_status=1 AND match_count>0 AND match_asset_sha256 IS NOT NULL AND " + "match_asset_path IS NOT NULL AND match_asset_size_bytes IS NOT NULL))," + "UNIQUE(candidate_pair_id,feature_set_id_a,feature_set_id_b," + "matcher_kind,matcher_version,parameter_fingerprint));" + "CREATE INDEX match_results_candidate_pair_idx ON match_results(candidate_pair_id);" + "CREATE INDEX match_results_feature_set_a_idx ON match_results(feature_set_id_a);" + "CREATE INDEX match_results_feature_set_b_idx ON match_results(feature_set_id_b);"; + 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 : ""); @@ -288,7 +315,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int } if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 && from_version != 4 && from_version != 5 && from_version != 6 && from_version != 7 && - from_version != 8) { + from_version != 8 && from_version != 9) { return LARDON3D_PROJECT_DB_CORRUPT; } Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration"); @@ -499,6 +526,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int "finish schema v9 migration"); } } + if (result == LARDON3D_PROJECT_DB_OK && from_version < 10) { + result = execute(database, schema_match_result_v10, "migrate schema v9 to v10"); +#ifdef LARDON3D_PROJECT_DB_TESTING + const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10"); + if (result == LARDON3D_PROJECT_DB_OK && forced_failure && strcmp(forced_failure, "1") == 0) { + result = execute(database, "INSERT INTO missing_test_table VALUES(1)", + "forced migration v10 failure"); + } +#endif + if (result == LARDON3D_PROJECT_DB_OK) { + result = execute(database, + "UPDATE metadata SET value=10 WHERE key='schema_version' AND value=9", + "finish schema v10 migration"); + } + } if (result == LARDON3D_PROJECT_DB_OK) { result = execute(database, "COMMIT", "commit migration"); } @@ -611,8 +653,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje "feature_support_sets", "feature_support_groups", "feature_support_members", - "candidate_pairs"}; - for (size_t index = 0; index < 16 && result == LARDON3D_PROJECT_DB_OK; ++index) { + "candidate_pairs", + "match_results"}; + for (size_t index = 0; index < 17 && result == LARDON3D_PROJECT_DB_OK; ++index) { if (!table_exists(database->connection, required[index])) { copy_error(database->error, "Schéma v1 incomplet."); result = LARDON3D_PROJECT_DB_CORRUPT; @@ -3180,6 +3223,409 @@ Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task( return result; } +static bool read_match_result(sqlite3_stmt *statement, + Lardon3DProjectDbMatchResult *result) { + sqlite3_int64 id = sqlite3_column_int64(statement, 0); + sqlite3_int64 cp_id = sqlite3_column_int64(statement, 1); + sqlite3_int64 fs_a = sqlite3_column_int64(statement, 2); + sqlite3_int64 fs_b = sqlite3_column_int64(statement, 3); + const char *kind = (const char *)sqlite3_column_text(statement, 4); + sqlite3_int64 version = sqlite3_column_int64(statement, 5); + const void *fp = sqlite3_column_blob(statement, 6); + sqlite3_int64 status = sqlite3_column_int64(statement, 7); + sqlite3_int64 match_count = sqlite3_column_int64(statement, 8); + const void *asset_sha256 = sqlite3_column_blob(statement, 9); + int asset_sha256_bytes = sqlite3_column_bytes(statement, 9); + const char *asset_path = (const char *)sqlite3_column_text(statement, 10); + sqlite3_int64 asset_size = sqlite3_column_int64(statement, 11); + sqlite3_int64 created = sqlite3_column_int64(statement, 12); + bool has_sha = sqlite3_column_type(statement, 9) != SQLITE_NULL; + bool has_path = sqlite3_column_type(statement, 10) != SQLITE_NULL; + bool has_size = sqlite3_column_type(statement, 11) != SQLITE_NULL; + if (id <= 0 || cp_id <= 0 || fs_a <= 0 || fs_b <= 0 || + !kind || version <= 0 || !fp || sqlite3_column_bytes(statement, 6) != 32 || + status < 0 || status > 1 || match_count < 0 || match_count > 8192 || created < 0 || + (status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + (match_count != 0 || has_sha || has_path || has_size)) || + (status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + (match_count == 0 || !has_sha || !has_path || !has_size || asset_size <= 0))) { + return false; + } + result->match_result_id = (uint64_t)id; + result->candidate_pair_id = (uint64_t)cp_id; + result->feature_set_id_a = (uint64_t)fs_a; + result->feature_set_id_b = (uint64_t)fs_b; + size_t len = strlen(kind); + if (len == 0 || len >= LARDON3D_PROJECT_DB_KIND_CAPACITY) return false; + memcpy(result->matcher_kind, kind, len + 1); + result->matcher_version = (uint32_t)version; + memcpy(result->parameter_fingerprint, fp, 32); + result->result_status = (int)status; + result->match_count = (uint32_t)match_count; + /* Asset fields */ + if (asset_sha256 && asset_sha256_bytes == LARDON3D_PROJECT_DB_SHA256_SIZE) { + result->has_match_asset = true; + memcpy(result->match_asset_sha256, asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE); + } else { + result->has_match_asset = false; + memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE); + } + if (asset_path) { + size_t plen = strlen(asset_path); + if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) { + memcpy(result->match_asset_path, asset_path, plen + 1); + } else { + memcpy(result->match_asset_path, asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY - 1); + result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0'; + } + } else { + result->match_asset_path[0] = '\0'; + } + result->match_asset_size_bytes = asset_size >= 0 ? (uint64_t)asset_size : 0; + result->created_at = created; + return true; +} + +Lardon3DProjectDbResult lardon3d_project_db_create_match_result( + Lardon3DProjectDb *database, uint64_t candidate_pair_id, uint64_t feature_set_id_a, + uint64_t feature_set_id_b, const char *matcher_kind, uint32_t matcher_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], int result_status, + uint32_t match_count, + const unsigned char *match_asset_sha256, const char *match_asset_path, + uint64_t match_asset_size_bytes, int64_t created_at, + Lardon3DProjectDbMatchResult *result) { + if (!database || !valid_catalog_id(candidate_pair_id) || + !valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) || + !bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) || + matcher_version == 0 || !parameter_fingerprint || result_status < 0 || result_status > 1 || + match_count > 8192 || created_at < 0 || !result || + (result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + (match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) || + (result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + (match_count == 0 || !match_asset_sha256 || !match_asset_path || + match_asset_size_bytes == 0))) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + /* Validate asset path if provided */ + if (match_asset_path && match_asset_path[0] != '\0' && + !bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false)) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + + /* Validate Feature Set → image ownership before INSERT */ + sqlite3_stmt *check = NULL; + Lardon3DProjectDbResult db_result = LARDON3D_PROJECT_DB_OK; + sqlite3_int64 cp_image_a = 0, cp_image_b = 0; + sqlite3_int64 fs_a_image = 0, fs_b_image = 0; + + /* Load candidate pair image_ids */ + db_result = prepare(database, + "SELECT image_id_a,image_id_b FROM candidate_pairs WHERE " + "candidate_pair_id=?1", + &check); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(check, 1, (sqlite3_int64)candidate_pair_id); + int code = sqlite3_step(check); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } else if (code != SQLITE_ROW) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + cp_image_a = sqlite3_column_int64(check, 0); + cp_image_b = sqlite3_column_int64(check, 1); + } + (void)sqlite3_finalize(check); + check = NULL; + } + + /* Verify feature_set_id_a belongs to image_id_a */ + if (db_result == LARDON3D_PROJECT_DB_OK) { + db_result = prepare(database, + "SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_a); + int code = sqlite3_step(check); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } else if (code != SQLITE_ROW) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + fs_a_image = sqlite3_column_int64(check, 0); + if (fs_a_image != cp_image_a) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + } + (void)sqlite3_finalize(check); + check = NULL; + } + } + + /* Verify feature_set_id_b belongs to image_id_b */ + if (db_result == LARDON3D_PROJECT_DB_OK) { + db_result = prepare(database, + "SELECT image_id FROM feature_sets WHERE feature_set_id=?1", &check); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(check, 1, (sqlite3_int64)feature_set_id_b); + int code = sqlite3_step(check); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } else if (code != SQLITE_ROW) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } else { + fs_b_image = sqlite3_column_int64(check, 0); + if (fs_b_image != cp_image_b) { + db_result = LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + } + (void)sqlite3_finalize(check); + check = NULL; + } + } + + /* INSERT */ + sqlite3_stmt *statement = NULL; + if (db_result == LARDON3D_PROJECT_DB_OK) { + db_result = prepare( + database, + "INSERT INTO " + "match_results(candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind," + "matcher_version,parameter_fingerprint,result_status,match_count," + "match_asset_sha256,match_asset_path,match_asset_size_bytes," + "created_at) VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)", + &statement); + } + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b); + (void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT); + (void)sqlite3_bind_int64(statement, 5, matcher_version); + (void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT); + (void)sqlite3_bind_int(statement, 7, result_status); + (void)sqlite3_bind_int64(statement, 8, match_count); + if (match_asset_sha256) { + (void)sqlite3_bind_blob(statement, 9, match_asset_sha256, + LARDON3D_PROJECT_DB_SHA256_SIZE, SQLITE_TRANSIENT); + } else { + (void)sqlite3_bind_null(statement, 9); + } + if (match_asset_path && match_asset_path[0] != '\0') { + (void)sqlite3_bind_text(statement, 10, match_asset_path, -1, SQLITE_TRANSIENT); + } else { + (void)sqlite3_bind_null(statement, 10); + } + if (result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED) { + (void)sqlite3_bind_int64(statement, 11, (sqlite3_int64)match_asset_size_bytes); + } else { + (void)sqlite3_bind_null(statement, 11); + } + (void)sqlite3_bind_int64(statement, 12, created_at); + db_result = step_done(database, statement, "create match result"); + } + sqlite3_int64 id = sqlite3_last_insert_rowid(database->connection); + if (db_result == LARDON3D_PROJECT_DB_OK && id <= 0) { + db_result = LARDON3D_PROJECT_DB_CONSTRAINT; + } + if (db_result == LARDON3D_PROJECT_DB_OK) { + result->match_result_id = (uint64_t)id; + result->candidate_pair_id = candidate_pair_id; + result->feature_set_id_a = feature_set_id_a; + result->feature_set_id_b = feature_set_id_b; + (void)snprintf(result->matcher_kind, sizeof(result->matcher_kind), "%s", matcher_kind); + result->matcher_version = matcher_version; + memcpy(result->parameter_fingerprint, parameter_fingerprint, 32); + result->result_status = result_status; + result->match_count = match_count; + if (match_asset_sha256) { + result->has_match_asset = true; + memcpy(result->match_asset_sha256, match_asset_sha256, LARDON3D_PROJECT_DB_SHA256_SIZE); + } else { + result->has_match_asset = false; + memset(result->match_asset_sha256, 0, LARDON3D_PROJECT_DB_SHA256_SIZE); + } + if (match_asset_path && match_asset_path[0] != '\0') { + size_t plen = strlen(match_asset_path); + if (plen < LARDON3D_PROJECT_DB_PATH_CAPACITY) { + memcpy(result->match_asset_path, match_asset_path, plen + 1); + } else { + memcpy(result->match_asset_path, match_asset_path, + LARDON3D_PROJECT_DB_PATH_CAPACITY - 1); + result->match_asset_path[LARDON3D_PROJECT_DB_PATH_CAPACITY - 1] = '\0'; + } + } else { + result->match_asset_path[0] = '\0'; + } + result->match_asset_size_bytes = match_asset_size_bytes; + result->created_at = created_at; + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + +Lardon3DProjectDbResult lardon3d_project_db_load_match_result( + Lardon3DProjectDb *database, uint64_t match_result_id, + Lardon3DProjectDbMatchResult *result) { + if (!database || !valid_catalog_id(match_result_id) || !result) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult db_result = prepare( + database, + "SELECT " + "match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind," + "matcher_version,parameter_fingerprint,result_status,match_count," + "match_asset_sha256,match_asset_path,match_asset_size_bytes," + "created_at FROM match_results WHERE match_result_id=?1", + &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_NOT_FOUND; + } else if (code != SQLITE_ROW || !read_match_result(statement, result)) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + +Lardon3DProjectDbResult lardon3d_project_db_repair_match_result( + Lardon3DProjectDb *database, uint64_t match_result_id, int result_status, + uint32_t match_count, const unsigned char *match_asset_sha256, + const char *match_asset_path, uint64_t match_asset_size_bytes, + Lardon3DProjectDbMatchResult *result) { + if (!database || !valid_catalog_id(match_result_id) || result_status < 0 || + result_status > 1 || match_count > 8192 || !result || + (result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + (match_count != 0 || match_asset_sha256 || match_asset_path || match_asset_size_bytes != 0)) || + (result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + (match_count == 0 || !match_asset_sha256 || !match_asset_path || + match_asset_size_bytes == 0)) || + (match_asset_path && + !bounded_text(match_asset_path, LARDON3D_PROJECT_DB_PATH_CAPACITY, false))) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult db_result = prepare( + database, + "UPDATE match_results SET result_status=?2,match_count=?3,match_asset_sha256=?4," + "match_asset_path=?5,match_asset_size_bytes=?6 WHERE match_result_id=?1", + &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)match_result_id); + (void)sqlite3_bind_int(statement, 2, result_status); + (void)sqlite3_bind_int64(statement, 3, match_count); + if (match_asset_sha256) { + (void)sqlite3_bind_blob(statement, 4, match_asset_sha256, 32, SQLITE_TRANSIENT); + (void)sqlite3_bind_text(statement, 5, match_asset_path, -1, SQLITE_TRANSIENT); + (void)sqlite3_bind_int64(statement, 6, (sqlite3_int64)match_asset_size_bytes); + } else { + (void)sqlite3_bind_null(statement, 4); + (void)sqlite3_bind_null(statement, 5); + (void)sqlite3_bind_null(statement, 6); + } + db_result = step_done(database, statement, "repair match result"); + } + if (db_result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) { + db_result = LARDON3D_PROJECT_DB_NOT_FOUND; + } + (void)pthread_mutex_unlock(&database->mutex); + if (db_result != LARDON3D_PROJECT_DB_OK) return db_result; + return lardon3d_project_db_load_match_result(database, match_result_id, result); +} + +Lardon3DProjectDbResult lardon3d_project_db_find_match_result( + Lardon3DProjectDb *database, uint64_t candidate_pair_id, + uint64_t feature_set_id_a, uint64_t feature_set_id_b, + const char *matcher_kind, uint32_t matcher_version, + const unsigned char parameter_fingerprint[LARDON3D_PROJECT_DB_SHA256_SIZE], + Lardon3DProjectDbMatchResult *result) { + if (!database || !valid_catalog_id(candidate_pair_id) || + !valid_catalog_id(feature_set_id_a) || !valid_catalog_id(feature_set_id_b) || + !bounded_text(matcher_kind, LARDON3D_PROJECT_DB_KIND_CAPACITY, false) || + matcher_version == 0 || !parameter_fingerprint || !result) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + memset(result, 0, sizeof(*result)); + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult db_result = prepare( + database, + "SELECT " + "match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind," + "matcher_version,parameter_fingerprint,result_status,match_count," + "match_asset_sha256,match_asset_path,match_asset_size_bytes," + "created_at FROM match_results WHERE candidate_pair_id=?1 AND feature_set_id_a=?2 AND " + "feature_set_id_b=?3 AND matcher_kind=?4 AND matcher_version=?5 AND " + "parameter_fingerprint=?6", + &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)candidate_pair_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)feature_set_id_a); + (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)feature_set_id_b); + (void)sqlite3_bind_text(statement, 4, matcher_kind, -1, SQLITE_TRANSIENT); + (void)sqlite3_bind_int64(statement, 5, matcher_version); + (void)sqlite3_bind_blob(statement, 6, parameter_fingerprint, 32, SQLITE_TRANSIENT); + int code = sqlite3_step(statement); + if (code == SQLITE_DONE) { + db_result = LARDON3D_PROJECT_DB_NOT_FOUND; + } else if (code != SQLITE_ROW || !read_match_result(statement, result)) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + +Lardon3DProjectDbResult lardon3d_project_db_list_match_results( + Lardon3DProjectDb *database, uint64_t after_match_result_id, + Lardon3DProjectDbMatchResult *results, size_t capacity, size_t *count) { + if (count) { + *count = 0; + } + if (!database || !results || !count || after_match_result_id > INT64_MAX || capacity == 0 || + capacity > LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX) { + return LARDON3D_PROJECT_DB_INVALID_ARGUMENT; + } + (void)pthread_mutex_lock(&database->mutex); + sqlite3_stmt *statement = NULL; + Lardon3DProjectDbResult db_result = prepare( + database, + "SELECT " + "match_result_id,candidate_pair_id,feature_set_id_a,feature_set_id_b,matcher_kind," + "matcher_version,parameter_fingerprint,result_status,match_count," + "match_asset_sha256,match_asset_path,match_asset_size_bytes," + "created_at FROM match_results WHERE match_result_id>?1 ORDER BY match_result_id LIMIT ?2", + &statement); + if (db_result == LARDON3D_PROJECT_DB_OK) { + (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)after_match_result_id); + (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)capacity); + int code = SQLITE_DONE; + while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) { + if (!read_match_result(statement, &results[*count])) { + db_result = LARDON3D_PROJECT_DB_CORRUPT; + break; + } + ++*count; + } + if (db_result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) { + db_result = sqlite_result(database, code, "list match results"); + } + (void)sqlite3_finalize(statement); + } + (void)pthread_mutex_unlock(&database->mutex); + return db_result; +} + #ifdef LARDON3D_PROJECT_DB_TESTING Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity( Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) { diff --git a/tests/benchmark_matcher.cpp b/tests/benchmark_matcher.cpp new file mode 100644 index 0000000..99e4f65 --- /dev/null +++ b/tests/benchmark_matcher.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +#include +} + +struct Sample { + uint64_t open_ns; + uint64_t read_ns; + uint64_t knn_ns; + uint64_t filter_ns; + uint64_t canonicalize_ns; + uint64_t serialize_ns; + uint64_t sha_ns; + uint64_t publish_ns; + uint64_t database_ns; + uint64_t total_ns; +}; + +static uint64_t median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +static void image_path(const unsigned char hash[32], char path[4096]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t i = 0; i < 32; ++i) { + hex[2 * i] = digits[hash[i] >> 4]; + hex[2 * i + 1] = digits[hash[i] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, 4096, "assets/images/%c%c/%s", hex[0], hex[1], hex); +} + +static bool register_image(Lardon3DProjectDb *db, uint64_t scanset_id, unsigned char seed, + Lardon3DProjectDbImage *image) { + unsigned char hash[32]; + memset(hash, seed, sizeof(hash)); + char path[4096]; + image_path(hash, path); + Lardon3DProjectDbImageRegisterStatus status; + return lardon3d_project_db_register_image(db, scanset_id, hash, path, 1, "fixture.bin", + "/synthetic/fixture.bin", 0, seed, &status, + image) == LARDON3D_PROJECT_DB_OK; +} + +static void fill_features(Lardon3DExtractedFeatures *features, uint32_t count, + Lardon3DFeatureDescriptorType type, unsigned char salt) { + (void)salt; + memset(features, 0, sizeof(*features)); + features->image_width = 4096; + features->image_height = 4096; + features->feature_count = count; + features->keypoints = static_cast( + calloc(count == 0 ? 1 : count, sizeof(*features->keypoints))); + for (uint32_t i = 0; i < count; ++i) { + features->keypoints[i].x = (float)(i % 4096U); + features->keypoints[i].y = (float)(i % 4096U); + features->keypoints[i].size = 1.0F; + } + size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float); + features->descriptor_bytes = (size_t)count * 128 * scalar; + features->descriptors = static_cast(malloc(features->descriptor_bytes)); + if (type == LARDON3D_FEATURE_DESCRIPTOR_U8) { + features->descriptor_bytes = (size_t)count * 32; + features->descriptors = static_cast( + realloc(features->descriptors, features->descriptor_bytes)); + for (uint32_t row = 0; row < count; ++row) { + uint32_t value = row + 1U; + for (uint32_t column = 0; column < 32; ++column) { + value ^= value << 13; + value ^= value >> 17; + value ^= value << 5; + features->descriptors[(size_t)row * 32 + column] = (unsigned char)value; + } + } + } else { + float *values = reinterpret_cast(features->descriptors); + size_t value_count = (size_t)count * 128; + for (size_t i = 0; i < value_count; ++i) + values[i] = (float)(((i / 128U) * 37U + (i % 128U) * 11U) % 65521U) / 65521.0F; + } +} + +static bool publish(Lardon3DAppState *state, uint64_t image_id, const char *kind, + Lardon3DFeatureDescriptorType type, uint32_t count, unsigned char salt, + Lardon3DProjectDbFeatureSet *set) { + Lardon3DExtractedFeatures features; + fill_features(&features, count, type, salt); + unsigned char fingerprint[32]; + memset(fingerprint, salt, sizeof(fingerprint)); + uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128; + Lardon3DFeatureStoreResult result = lardon3d_feature_store_publish_v2( + state, image_id, 0, kind, 1, fingerprint, type, dimension, 0, &features, set); + free(features.keypoints); + free(features.descriptors); + return result == LARDON3D_FEATURE_STORE_OK; +} + +static bool benchmark_case(const char *kind, Lardon3DMatcherKind matcher_kind, + Lardon3DFeatureDescriptorType type, uint32_t count) { + char root_template[] = "/tmp/lardon3d-matcher-bench-XXXXXX"; + char *root = mkdtemp(root_template); + if (!root) return false; + std::string db_path = std::string(root) + "/project.db"; + Lardon3DProjectDb *db = nullptr; + char error[256]; + if (lardon3d_project_db_open(db_path.c_str(), &db, error) != LARDON3D_PROJECT_DB_OK) + return false; + Lardon3DProjectDbScanSet scanset; + Lardon3DProjectDbImage image_a, image_b; + Lardon3DAppState state; + lardon3d_app_state_init(&state); + state.project_loaded = true; + state.project_db = db; + (void)snprintf(state.project_path, sizeof(state.project_path), "%s", root); + bool ok = lardon3d_project_db_create_scanset(db, "benchmark", &scanset) == + LARDON3D_PROJECT_DB_OK && + register_image(db, scanset.scanset_id, 1, &image_a) && + register_image(db, scanset.scanset_id, 2, &image_b); + Lardon3DProjectDbFeatureSet set_a, set_b; + ok = ok && publish(&state, image_a.image_id, kind, type, count, 3, &set_a) && + publish(&state, image_b.image_id, kind, type, count, 4, &set_b); + Lardon3DProjectDbCandidatePair pair; + ok = ok && lardon3d_project_db_create_candidate_pair( + db, image_a.image_id, image_b.image_id, 1, &pair) == LARDON3D_PROJECT_DB_OK; + std::vector samples; + Lardon3DMatcherParams params = {matcher_kind, lardon3d_matcher_default_ratio(matcher_kind)}; + for (int repetition = 0; ok && repetition < 3; ++repetition) { + Lardon3DMatcherStats stats; + params.ratio_threshold = lardon3d_matcher_default_ratio(matcher_kind) - + (float)repetition * 0.01F; + Lardon3DProjectDbMatchResult result; + ok = lardon3d_matcher_match_and_publish_profiled( + root, db, &pair, &set_a, &set_b, ¶ms, &result, &stats) == LARDON3D_MATCHER_OK; + samples.push_back({stats.feature_open_ns, stats.descriptor_read_ns, stats.knn_ns, + stats.filter_ns, stats.canonicalize_ns, stats.serialize_ns, + stats.sha256_ns, stats.publication_ns, stats.database_ns, + stats.total_ns}); + } + if (ok) { + std::vector values; + auto field = [&](auto getter) { + values.clear(); + for (const Sample &sample : samples) values.push_back(getter(sample)); + return (double)median(values) / 1000000.0; + }; + printf("%s,%u,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f\n", kind, count, + field([](const Sample &s) { return s.open_ns; }), + field([](const Sample &s) { return s.read_ns; }), + field([](const Sample &s) { return s.knn_ns; }), + field([](const Sample &s) { return s.filter_ns; }), + field([](const Sample &s) { return s.canonicalize_ns; }), + field([](const Sample &s) { return s.serialize_ns; }), + field([](const Sample &s) { return s.sha_ns; }), + field([](const Sample &s) { return s.publish_ns; }), + field([](const Sample &s) { return s.database_ns; }), + field([](const Sample &s) { return s.total_ns; })); + std::vector reuse_totals; + for (int repetition = 0; repetition < 5; ++repetition) { + Lardon3DMatcherStats reuse_stats; + Lardon3DProjectDbMatchResult reused; + ok = ok && lardon3d_matcher_match_and_publish_profiled( + root, db, &pair, &set_a, &set_b, ¶ms, &reused, &reuse_stats) == + LARDON3D_MATCHER_OK; + reuse_totals.push_back(reuse_stats.total_ns); + } + printf("%s-reuse,%u,0,0,0,0,0,0,0,0,0,%.3f\n", kind, count, + (double)median(reuse_totals) / 1000000.0); + } + lardon3d_project_db_close(db); + std::filesystem::remove_all(root); + return ok; +} + +int main(void) { + printf("kind,count,open_ms,read_ms,knn_ms,filter_ms,canonicalize_ms,serialize_ms," + "sha_ms,publish_ms,db_ms,total_ms\n"); + const uint32_t sizes[] = {64, 256, 1024, 4096, 8192}; + for (uint32_t count : sizes) { + if (!benchmark_case("orb", LARDON3D_MATCHER_ORB_BF, + LARDON3D_FEATURE_DESCRIPTOR_U8, count) || + !benchmark_case("sift", LARDON3D_MATCHER_SIFT_BF, + LARDON3D_FEATURE_DESCRIPTOR_F32, count) || + !benchmark_case("rootsift", LARDON3D_MATCHER_ROOTSIFT_BF, + LARDON3D_FEATURE_DESCRIPTOR_F32, count)) + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/tests/test_match_result.c b/tests/test_match_result.c new file mode 100644 index 0000000..3c51e67 --- /dev/null +++ b/tests/test_match_result.c @@ -0,0 +1,526 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +static bool query_integer(const char *path, const char *sql, sqlite3_int64 expected) { + sqlite3 *connection = NULL; + sqlite3_stmt *statement = NULL; + if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK || + sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) { + if (statement) { + (void)sqlite3_finalize(statement); + } + if (connection) { + (void)sqlite3_close(connection); + } + return false; + } + bool matches = sqlite3_step(statement) == SQLITE_ROW && + sqlite3_column_int64(statement, 0) == expected && + sqlite3_step(statement) == SQLITE_DONE; + return sqlite3_finalize(statement) == SQLITE_OK && sqlite3_close(connection) == SQLITE_OK && + matches; +} + +static bool create_v9_database(const char *path) { + Lardon3DProjectDb *database = NULL; + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + if (lardon3d_project_db_open(path, &database, error) != LARDON3D_PROJECT_DB_OK) return false; + lardon3d_project_db_close(database); + sqlite3 *connection = NULL; + if (sqlite3_open(path, &connection) != SQLITE_OK) return false; + static const char sql[] = + "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE match_results;" + "UPDATE metadata SET value=9 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; + bool ok = sqlite3_exec(connection, sql, NULL, NULL, NULL) == SQLITE_OK; + return sqlite3_close(connection) == SQLITE_OK && ok; +} + +static void asset_path_for_hash(const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t i = 0; i < LARDON3D_PROJECT_DB_SHA256_SIZE; ++i) { + hex[i * 2] = digits[hash[i] >> 4]; + hex[i * 2 + 1] = digits[hash[i] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/images/%c%c/%s", hex[0], hex[1], + hex); +} + +static void feature_asset_path_for_hash( + const unsigned char hash[LARDON3D_PROJECT_DB_SHA256_SIZE], + char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t i = 0; i < LARDON3D_PROJECT_DB_SHA256_SIZE; ++i) { + hex[i * 2] = digits[hash[i] >> 4]; + hex[i * 2 + 1] = digits[hash[i] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, LARDON3D_PROJECT_DB_PATH_CAPACITY, "assets/features/%c%c/%s", hex[0], hex[1], + hex); +} + +static bool run_test(void) { + char directory[] = "/tmp/lardon3d-match-result-XXXXXX"; + CHECK(mkdtemp(directory)); + char database_path[512], v9_path[512], failed_v10_path[512]; + CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0); + CHECK(snprintf(v9_path, sizeof(v9_path), "%s/v9.db", directory) > 0); + CHECK(snprintf(failed_v10_path, sizeof(failed_v10_path), "%s/failed-v10.db", directory) > 0); + + char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; + Lardon3DProjectDb *database = NULL; + CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(database && lardon3d_project_db_schema_version(database) == 10); + + Lardon3DProjectDbScanSet scanset; + CHECK(lardon3d_project_db_create_scanset(database, "Match-test", &scanset) == + LARDON3D_PROJECT_DB_OK); + + unsigned char hash_a[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xAA}; + char path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path_for_hash(hash_a, path_a); + Lardon3DProjectDbImageRegisterStatus identity_status; + Lardon3DProjectDbImage image_a; + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_a, path_a, 1, + "a.jpg", "/src/a.jpg", 0, 1, &identity_status, + &image_a) == LARDON3D_PROJECT_DB_OK); + + unsigned char hash_b[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xBB}; + char path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path_for_hash(hash_b, path_b); + Lardon3DProjectDbImage image_b; + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_b, path_b, 1, + "b.jpg", "/src/b.jpg", 0, 2, &identity_status, + &image_b) == LARDON3D_PROJECT_DB_OK); + + Lardon3DProjectDbCandidatePair pair; + CHECK(lardon3d_project_db_create_candidate_pair(database, image_a.image_id, image_b.image_id, 10, + &pair) == LARDON3D_PROJECT_DB_OK); + CHECK(pair.candidate_pair_id > 0); + + /* Create feature sets for foreign key references */ + unsigned char feature_hash_a[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x11}; + char feature_path_a[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_a, feature_path_a); + Lardon3DProjectDbFeatureSet fs_a; + CHECK(lardon3d_project_db_register_feature_set( + database, image_a.image_id, "orb", 1, feature_hash_a, hash_a, 100, 1, 32, + feature_hash_a, feature_path_a, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 10, + &fs_a) == LARDON3D_PROJECT_DB_OK); + CHECK(fs_a.feature_set_id > 0 && fs_a.image_id == image_a.image_id); + + unsigned char feature_hash_b[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x22}; + char feature_path_b[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_b, feature_path_b); + Lardon3DProjectDbFeatureSet fs_b; + CHECK(lardon3d_project_db_register_feature_set( + database, image_b.image_id, "sift", 1, feature_hash_b, hash_b, 100, 2, 128, + feature_hash_b, feature_path_b, 256, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 20, &fs_b) == + LARDON3D_PROJECT_DB_OK); + CHECK(fs_b.feature_set_id > 0 && fs_b.image_id == image_b.image_id); + + /* Create MATCHED result with inliers */ + unsigned char fp1[32]; + memset(fp1, 0x01, 32); + Lardon3DProjectDbMatchResult result; + unsigned char match_hash[32]; + memset(match_hash, 0x5A, sizeof(match_hash)); + const char *match_path = "assets/matches/5a/asset"; + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, + &result) == LARDON3D_PROJECT_DB_OK); + CHECK(result.match_result_id > 0 && result.candidate_pair_id == pair.candidate_pair_id && + result.feature_set_id_a == fs_a.feature_set_id && + result.feature_set_id_b == fs_b.feature_set_id && + strcmp(result.matcher_kind, "orb_bf") == 0 && result.matcher_version == 1 && + memcmp(result.parameter_fingerprint, fp1, 32) == 0 && + result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + result.match_count == 30 && result.created_at == 100); + uint64_t first_id = result.match_result_id; + + /* Create NO_MATCH result (0 inliers) */ + unsigned char fp2[32]; + memset(fp2, 0x02, 32); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 2, fp2, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, + NULL, NULL, 0, 200, + &result) == LARDON3D_PROJECT_DB_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + result.match_count == 0 && !result.has_match_asset); + uint64_t second_id = result.match_result_id; + + /* Duplicate: same CP + same matcher + same params + same FS → CONSTRAINT */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, + match_hash, match_path, 152, 500, + &result) == LARDON3D_PROJECT_DB_CONSTRAINT); + + /* Create additional Feature Sets for distinct-FS and inverted-ID tests */ + unsigned char feature_hash_a2[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x33}; + char feature_path_a2[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_a2, feature_path_a2); + Lardon3DProjectDbFeatureSet fs_a2; + unsigned char fs_a2_fp[32]; + memset(fs_a2_fp, 0xAA, 32); + CHECK(lardon3d_project_db_register_feature_set( + database, image_a.image_id, "orb", 2, fs_a2_fp, hash_a, 200, 1, 32, + feature_hash_a2, feature_path_a2, 256, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 30, + &fs_a2) == LARDON3D_PROJECT_DB_OK); + CHECK(fs_a2.feature_set_id > 0 && fs_a2.image_id == image_a.image_id); + + unsigned char feature_hash_b2[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x44}; + char feature_path_b2[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_b2, feature_path_b2); + Lardon3DProjectDbFeatureSet fs_b2; + unsigned char fs_b2_fp[32]; + memset(fs_b2_fp, 0xBB, 32); + CHECK(lardon3d_project_db_register_feature_set( + database, image_b.image_id, "sift", 2, fs_b2_fp, hash_b, 200, 2, 128, + feature_hash_b2, feature_path_b2, 512, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 40, &fs_b2) == + LARDON3D_PROJECT_DB_OK); + CHECK(fs_b2.feature_set_id > 0 && fs_b2.image_id == image_b.image_id); + + /* Same CP + same matcher + same params + different FS → distinct */ + unsigned char fp5[32]; + memset(fp5, 0x05, 32); + Lardon3DProjectDbMatchResult distinct_fs_result; + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a2.feature_set_id, fs_b2.feature_set_id, + "orb_bf", 1, fp5, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 20, + match_hash, match_path, 272, 400, + &distinct_fs_result) == LARDON3D_PROJECT_DB_OK); + CHECK(distinct_fs_result.feature_set_id_a == fs_a2.feature_set_id && + distinct_fs_result.feature_set_id_b == fs_b2.feature_set_id); + uint64_t distinct_id = distinct_fs_result.match_result_id; + CHECK(distinct_id != first_id); + + /* feature_set_id_a > feature_set_id_b but correct ownership → valid */ + /* Register image_c, image_d; create FS for d first, then c → fs_c.id > fs_d.id */ + unsigned char hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xCC}; + char path_c[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path_for_hash(hash_c, path_c); + Lardon3DProjectDbImage image_c; + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_c, path_c, 1, + "c.jpg", "/src/c.jpg", 0, 3, &identity_status, + &image_c) == LARDON3D_PROJECT_DB_OK); + + unsigned char hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0xDD}; + char path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + asset_path_for_hash(hash_d, path_d); + Lardon3DProjectDbImage image_d; + CHECK(lardon3d_project_db_register_image(database, scanset.scanset_id, hash_d, path_d, 1, + "d.jpg", "/src/d.jpg", 0, 4, &identity_status, + &image_d) == LARDON3D_PROJECT_DB_OK); + + /* Create feature set for image_d first (lower ID), then image_c (higher ID) */ + unsigned char feature_hash_d[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x55}; + char feature_path_d[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_d, feature_path_d); + Lardon3DProjectDbFeatureSet fs_d; + CHECK(lardon3d_project_db_register_feature_set( + database, image_d.image_id, "orb", 1, feature_hash_d, hash_d, 100, 1, 32, + feature_hash_d, feature_path_d, 128, LARDON3D_DB_FEATURE_ASSET_DURABLE, 0, 50, + &fs_d) == LARDON3D_PROJECT_DB_OK); + + unsigned char feature_hash_c[LARDON3D_PROJECT_DB_SHA256_SIZE] = {0x66}; + char feature_path_c[LARDON3D_PROJECT_DB_PATH_CAPACITY]; + feature_asset_path_for_hash(feature_hash_c, feature_path_c); + Lardon3DProjectDbFeatureSet fs_c; + CHECK(lardon3d_project_db_register_feature_set( + database, image_c.image_id, "sift", 1, feature_hash_c, hash_c, 100, 2, 128, + feature_hash_c, feature_path_c, 256, + LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE, 0, 60, &fs_c) == + LARDON3D_PROJECT_DB_OK); + + /* fs_c was created after fs_d → fs_c.feature_set_id > fs_d.feature_set_id */ + CHECK(fs_c.feature_set_id > fs_d.feature_set_id); + + /* CP (image_c, image_d): image_c should come first since registered first */ + Lardon3DProjectDbCandidatePair pair_cd; + CHECK(lardon3d_project_db_create_candidate_pair(database, image_c.image_id, image_d.image_id, 20, + &pair_cd) == LARDON3D_PROJECT_DB_OK); + unsigned char fp6[32]; + memset(fp6, 0x06, 32); + Lardon3DProjectDbMatchResult inverted_fs_result; + CHECK(lardon3d_project_db_create_match_result( + database, pair_cd.candidate_pair_id, fs_c.feature_set_id, fs_d.feature_set_id, + "orb_bf", 1, fp6, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 15, + match_hash, match_path, 212, 500, + &inverted_fs_result) == LARDON3D_PROJECT_DB_OK); + CHECK(inverted_fs_result.feature_set_id_a == fs_c.feature_set_id && + inverted_fs_result.feature_set_id_b == fs_d.feature_set_id); + CHECK(fs_c.feature_set_id > fs_d.feature_set_id); + + /* FeatureSet A belonging to image B → rejected (INVALID_ARGUMENT) */ + unsigned char fp7[32]; + memset(fp7, 0x07, 32); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_b.feature_set_id, fs_a.feature_set_id, + "orb_bf", 1, fp7, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, + match_hash, match_path, 92, 600, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* FeatureSet B belonging to image A (not image B) → rejected */ + unsigned char fp8[32]; + memset(fp8, 0x08, 32); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_a2.feature_set_id, + "orb_bf", 1, fp8, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 5, + match_hash, match_path, 92, 700, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Load by ID */ + Lardon3DProjectDbMatchResult loaded; + CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded.match_result_id == first_id && loaded.result_status == + LARDON3D_MATCH_RESULT_STATUS_MATCHED && loaded.match_count == 30); + + /* Load not found */ + CHECK(lardon3d_project_db_load_match_result(database, 999999, &loaded) == + LARDON3D_PROJECT_DB_NOT_FOUND); + + /* Find by full 6-part identity */ + Lardon3DProjectDbMatchResult found; + CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, + fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_OK); + CHECK(found.match_result_id == first_id && found.match_count == 30); + + /* Find with wrong feature_set_id → NOT_FOUND */ + CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, + fs_a2.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + + /* Find not found */ + unsigned char unknown_fp[32]; + memset(unknown_fp, 0xFF, 32); + CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, + fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, unknown_fp, + &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + + /* Find with wrong candidate_pair */ + CHECK(lardon3d_project_db_find_match_result(database, 999999, + fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_NOT_FOUND); + + /* Pagination */ + Lardon3DProjectDbMatchResult page[LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX]; + size_t page_count = 0; + CHECK(lardon3d_project_db_list_match_results(database, 0, page, 4, &page_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(page_count == 4 && page[0].match_result_id == first_id && + page[1].match_result_id == second_id && page[2].match_result_id == distinct_id); + + /* Cursor pagination */ + page_count = 0; + CHECK(lardon3d_project_db_list_match_results(database, first_id, page, 2, &page_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(page_count == 2 && page[0].match_result_id == second_id && + page[1].match_result_id == distinct_id); + + /* End of list */ + page_count = 0; + CHECK(lardon3d_project_db_list_match_results(database, distinct_id, page, 1, &page_count) == + LARDON3D_PROJECT_DB_OK); + CHECK(page_count == 1 && page[0].match_result_id > distinct_id); + + /* Capacity bound (PAGE_MAX) */ + CHECK(lardon3d_project_db_list_match_results( + database, 0, page, LARDON3D_PROJECT_DB_MATCH_RESULT_PAGE_MAX + 1, + &page_count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: NULL database */ + CHECK(lardon3d_project_db_create_match_result(NULL, pair.candidate_pair_id, + fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, + &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: 0 ids */ + CHECK(lardon3d_project_db_create_match_result( + database, 0, fs_a.feature_set_id, fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, 0, fs_b.feature_set_id, "orb_bf", 1, fp1, + LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: NULL output */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, NULL) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: empty matcher_kind */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: 0 matcher_version */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 0, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: NULL fingerprint */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, NULL, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: bad status */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, 2, 30, + match_hash, match_path, 392, 100, &result) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid result/asset combinations */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 10, + NULL, NULL, 0, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "bad-no-match", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, + match_hash, match_path, 32, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "too-many", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 8193, + match_hash, match_path, 98348, 100, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Invalid arguments: negative created_at */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_MATCHED, 30, + match_hash, match_path, 392, -1, &result) == + LARDON3D_PROJECT_DB_INVALID_ARGUMENT); + + /* Status boundary values */ + CHECK(lardon3d_project_db_create_match_result( + database, pair.candidate_pair_id, fs_a.feature_set_id, fs_b.feature_set_id, + "boundary", 1, fp1, LARDON3D_MATCH_RESULT_STATUS_NO_MATCH, 0, + NULL, NULL, 0, 500, &result) == + LARDON3D_PROJECT_DB_OK); + + /* Close/reopen persistence */ + lardon3d_project_db_close(database); + database = NULL; + + CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); + CHECK(lardon3d_project_db_schema_version(database) == 10); + + /* Verify persistence: load previously created results */ + CHECK(lardon3d_project_db_load_match_result(database, first_id, &loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(loaded.match_result_id == first_id && loaded.result_status == + LARDON3D_MATCH_RESULT_STATUS_MATCHED && + strcmp(loaded.matcher_kind, "orb_bf") == 0 && loaded.match_count == 30); + + /* Verify persistence: find by key */ + CHECK(lardon3d_project_db_find_match_result(database, pair.candidate_pair_id, + fs_a.feature_set_id, fs_b.feature_set_id, + "orb_bf", 1, fp1, + &found) == LARDON3D_PROJECT_DB_OK); + CHECK(found.match_result_id == first_id); + + /* Verify persistence of distinct result */ + Lardon3DProjectDbMatchResult distinct_loaded; + CHECK(lardon3d_project_db_load_match_result(database, distinct_id, &distinct_loaded) == + LARDON3D_PROJECT_DB_OK); + CHECK(distinct_loaded.match_result_id == distinct_id && + distinct_loaded.feature_set_id_a == fs_a2.feature_set_id && + distinct_loaded.feature_set_id_b == fs_b2.feature_set_id); + CHECK(distinct_id != first_id); + + /* Verify inverted result persisted */ + Lardon3DProjectDbMatchResult inverted_loaded; + CHECK(lardon3d_project_db_load_match_result(database, inverted_fs_result.match_result_id, + &inverted_loaded) == LARDON3D_PROJECT_DB_OK); + CHECK(inverted_loaded.feature_set_id_a == fs_c.feature_set_id && + inverted_loaded.feature_set_id_b == fs_d.feature_set_id); + CHECK(inverted_loaded.feature_set_id_a > inverted_loaded.feature_set_id_b); + + lardon3d_project_db_close(database); + database = NULL; + + /* Migration from v9 */ + CHECK(create_v9_database(v9_path)); + CHECK(query_integer(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) == 10); + lardon3d_project_db_close(database); + database = NULL; + CHECK(query_integer(v9_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); + CHECK(query_integer(v9_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", 1)); + + /* Forced migration failure v10 */ + CHECK(create_v9_database(failed_v10_path)); + CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10", "1", 1) == 0); + CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == + LARDON3D_PROJECT_DB_IO_ERROR); + CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V10") == 0); + CHECK(query_integer(failed_v10_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(failed_v10_path, + "SELECT count(*) FROM sqlite_master WHERE type='table' AND " + "name='match_results'", 0)); + CHECK(lardon3d_project_db_open(failed_v10_path, &database, error) == LARDON3D_PROJECT_DB_OK && + lardon3d_project_db_schema_version(database) == 10); + lardon3d_project_db_close(database); + database = NULL; + + CHECK(unlink(database_path) == 0); + CHECK(unlink(v9_path) == 0); + CHECK(unlink(failed_v10_path) == 0); + CHECK(rmdir(directory) == 0); + return true; +} + +int main(void) { + return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_matcher.c b/tests/test_matcher.c new file mode 100644 index 0000000..761d19f --- /dev/null +++ b/tests/test_matcher.c @@ -0,0 +1,518 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \ + return false; \ + } \ + } while (0) + +/* --- Match File tests --- */ + +static bool test_match_file_write_read(void) { + char dir[] = "/tmp/lardon3d-mf-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/test.mf", dir) > 0); + + Lardon3DMatchFileEntry entries[3]; + entries[0].feature_index_a = 0; + entries[0].feature_index_b = 0; + entries[0].distance = 0.0F; + entries[1].feature_index_a = 5; + entries[1].feature_index_b = 3; + entries[1].distance = 3.0F; + entries[2].feature_index_a = 10; + entries[2].feature_index_b = 20; + entries[2].distance = 12.5F; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(lardon3d_match_file_write(fd, 1, 32, 1, 2, entries, 3) == + LARDON3D_MATCH_FILE_OK); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_OK); + CHECK(memcmp(header.magic, LARDON3D_MATCH_FILE_MAGIC, 4) == 0); + CHECK(header.format_version == 1); + CHECK(header.descriptor_type == 1); + CHECK(header.descriptor_dimension == 32); + CHECK(header.match_count == 3); + CHECK(header.feature_set_id_a == 1 && header.feature_set_id_b == 2); + + fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + CHECK(fd >= 0); + unsigned char physical_magic[4]; + CHECK(read(fd, physical_magic, sizeof(physical_magic)) == (ssize_t)sizeof(physical_magic)); + CHECK(memcmp(physical_magic, "L3DM", 4) == 0); + CHECK(lseek(fd, 0, SEEK_SET) == 0); + Lardon3DMatchFileEntry read_entries[10]; + uint32_t count = 0; + CHECK(lardon3d_match_file_read(fd, &header, read_entries, 10, &count, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_OK); + (void)close(fd); + CHECK(count == 3); + CHECK(read_entries[0].feature_index_a == 0); + CHECK(read_entries[2].feature_index_a == 10); + CHECK(read_entries[2].feature_index_b == 20); + CHECK(fabsf(read_entries[2].distance - 12.5F) < 0.001F); + CHECK(lardon3d_match_file_validate(path, &header, 9, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_CORRUPT); + CHECK(lardon3d_match_file_validate(path, &header, 1, 9, 8192, 8192) == + LARDON3D_MATCH_FILE_CORRUPT); + CHECK(lardon3d_match_file_validate(path, &header, 2, 1, 8192, 8192) == + LARDON3D_MATCH_FILE_CORRUPT); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_empty(void) { + char dir[] = "/tmp/lardon3d-me-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/empty.mf", dir) > 0); + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(lardon3d_match_file_write(fd, 1, 32, 1, 2, NULL, 0) == + LARDON3D_MATCH_FILE_OK); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 0, 0) == + LARDON3D_MATCH_FILE_OK); + CHECK(header.match_count == 0); + + fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + CHECK(fd >= 0); + Lardon3DMatchFileEntry entries[1]; + uint32_t count = 0; + CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 0, 0) == + LARDON3D_MATCH_FILE_OK); + (void)close(fd); + CHECK(count == 0); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_bad_magic(void) { + char dir[] = "/tmp/lardon3d-mbm-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/bad.mf", dir) > 0); + + unsigned char buf[32] = {0}; + memcpy(buf, "MD3L", 4); + buf[4] = 1; + buf[5] = 1; + buf[12] = 32; + buf[16] = 1; + buf[24] = 2; + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 32) == 32); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) == + LARDON3D_MATCH_FILE_BAD_MAGIC); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_bad_version(void) { + unsigned char buf[32] = {0}; + memcpy(buf, "L3DM", 4); + buf[4] = 42; + + char dir[] = "/tmp/lardon3d-mbv-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/badv.mf", dir) > 0); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 32) == 32); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) == + LARDON3D_MATCH_FILE_BAD_VERSION); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_truncated(void) { + unsigned char buf[32] = {0}; + memcpy(buf, "L3DM", 4); + buf[4] = 1; + buf[5] = 1; + buf[8] = 1; + buf[12] = 32; + buf[16] = 1; + buf[24] = 2; + + char dir[] = "/tmp/lardon3d-mt-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/trunc.mf", dir) > 0); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 32) == 32); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) == + LARDON3D_MATCH_FILE_BAD_SIZE); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_invalid_entry(void) { + char dir[] = "/tmp/lardon3d-mie-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/bade.mf", dir) > 0); + + unsigned char buf[44]; + memset(buf, 0, 44); + memcpy(buf, "L3DM", 4); + buf[4] = 1; + buf[5] = 1; + buf[8] = 1; + buf[12] = 32; + buf[16] = 1; + buf[24] = 2; + buf[32] = 0x0F; + buf[33] = 0x27; + buf[34] = 0; + buf[35] = 0; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 44) == 44); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_BAD_ENTRY); + fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + CHECK(fd >= 0); + Lardon3DMatchFileEntry entries[1]; + uint32_t count = 0; + CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_BAD_ENTRY); + (void)close(fd); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_nan_distance(void) { + char dir[] = "/tmp/lardon3d-mnd-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/nan.mf", dir) > 0); + + unsigned char buf[44]; + memset(buf, 0, 44); + memcpy(buf, "L3DM", 4); + buf[4] = 1; + buf[5] = 1; + buf[8] = 1; + buf[12] = 32; + buf[16] = 1; + buf[24] = 2; + buf[40] = 0; + buf[41] = 0; + buf[42] = 0xC0; + buf[43] = 0x7F; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 44) == 44); + (void)close(fd); + + fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + CHECK(fd >= 0); + Lardon3DMatchFileHeader header; + Lardon3DMatchFileEntry entries[1]; + uint32_t count = 0; + CHECK(lardon3d_match_file_read(fd, &header, entries, 1, &count, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_BAD_ENTRY); + (void)close(fd); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_f32(void) { + char dir[] = "/tmp/lardon3d-mf32-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/sift.mf", dir) > 0); + + Lardon3DMatchFileEntry entries[1]; + entries[0].feature_index_a = 0; + entries[0].feature_index_b = 0; + entries[0].distance = 100.0F; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(lardon3d_match_file_write(fd, 2, 128, 1, 2, entries, 1) == + LARDON3D_MATCH_FILE_OK); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 8192, 8192) == + LARDON3D_MATCH_FILE_OK); + CHECK(header.descriptor_type == 2); + CHECK(header.descriptor_dimension == 128); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_bad_type(void) { + unsigned char buf[32] = {0}; + memcpy(buf, "L3DM", 4); + buf[4] = 1; + buf[5] = 3; + + char dir[] = "/tmp/lardon3d-mbt-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/badtype.mf", dir) > 0); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(write(fd, buf, 32) == 32); + (void)close(fd); + + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 0, 0, 0, 0) == + LARDON3D_MATCH_FILE_BAD_TYPE); + + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool test_match_file_maximum_and_multiplicity(void) { + char dir[] = "/tmp/lardon3d-mmax-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/max.mf", dir) > 0); + Lardon3DMatchFileEntry *entries = calloc(LARDON3D_MATCH_FILE_MAX_MATCHES, + sizeof(*entries)); + CHECK(entries); + for (uint32_t i = 0; i < LARDON3D_MATCH_FILE_MAX_MATCHES; ++i) { + entries[i].feature_index_a = i; + entries[i].feature_index_b = 0; + entries[i].distance = (float)i; + } + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0); + CHECK(lardon3d_match_file_write(fd, 1, 32, 11, 12, entries, + LARDON3D_MATCH_FILE_MAX_MATCHES) == + LARDON3D_MATCH_FILE_OK); + CHECK(lardon3d_match_file_write(fd, 1, 32, 11, 12, entries, + LARDON3D_MATCH_FILE_MAX_MATCHES + 1) == + LARDON3D_MATCH_FILE_INVALID_ARGUMENT); + CHECK(close(fd) == 0); + struct stat info; + CHECK(stat(path, &info) == 0 && info.st_size == LARDON3D_MATCH_FILE_MAX_SIZE); + Lardon3DMatchFileHeader header; + CHECK(lardon3d_match_file_validate(path, &header, 11, 12, 8192, 1) == + LARDON3D_MATCH_FILE_OK); + free(entries); + CHECK(unlink(path) == 0); + CHECK(rmdir(dir) == 0); + return true; +} + +static bool write_minimal_match_file(const char *path) { + Lardon3DMatchFileEntry entry = {0, 0, 1.0F}; + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd < 0) return false; + bool ok = lardon3d_match_file_write(fd, 1, 32, 1, 2, &entry, 1) == + LARDON3D_MATCH_FILE_OK; + return close(fd) == 0 && ok; +} + +static bool mutate_bytes(const char *path, off_t offset, const void *bytes, size_t size) { + int fd = open(path, O_WRONLY | O_CLOEXEC); + if (fd < 0) return false; + bool ok = pwrite(fd, bytes, size, offset) == (ssize_t)size; + return close(fd) == 0 && ok; +} + +static bool expect_mutation(const char *path, off_t offset, const void *bytes, size_t size, + Lardon3DMatchFileResult expected) { + if (!write_minimal_match_file(path) || !mutate_bytes(path, offset, bytes, size)) return false; + Lardon3DMatchFileHeader header; + return lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) == expected; +} + +static bool test_match_file_corruption_matrix(void) { + char dir[] = "/tmp/lardon3d-matrix-XXXXXX"; + CHECK(mkdtemp(dir)); + char path[512]; + CHECK(snprintf(path, sizeof(path), "%s/match", dir) > 0); + Lardon3DMatchFileHeader header; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0 && close(fd) == 0); + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) == + LARDON3D_MATCH_FILE_BAD_SIZE); + unsigned char one = 0; + CHECK(mutate_bytes(path, 0, &one, 1)); + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) == + LARDON3D_MATCH_FILE_BAD_SIZE); + + unsigned char version_zero = 0; + unsigned char version_future = 2; + unsigned char type_zero = 0; + unsigned char type_unknown = 99; + unsigned char reserved[2] = {1, 0}; + unsigned char dimension[4] = {31, 0, 0, 0}; + unsigned char zero_id[8] = {0}; + unsigned char count_8193[4] = {1, 32, 0, 0}; + unsigned char count_max[4] = {255, 255, 255, 255}; + unsigned char bad_magic[4] = {'B', 'A', 'D', '!'}; + CHECK(expect_mutation(path, 0, bad_magic, 4, LARDON3D_MATCH_FILE_BAD_MAGIC)); + CHECK(expect_mutation(path, 4, &version_zero, 1, LARDON3D_MATCH_FILE_BAD_VERSION)); + CHECK(expect_mutation(path, 4, &version_future, 1, LARDON3D_MATCH_FILE_BAD_VERSION)); + CHECK(expect_mutation(path, 5, &type_zero, 1, LARDON3D_MATCH_FILE_BAD_TYPE)); + CHECK(expect_mutation(path, 5, &type_unknown, 1, LARDON3D_MATCH_FILE_BAD_TYPE)); + CHECK(expect_mutation(path, 6, reserved, 2, LARDON3D_MATCH_FILE_CORRUPT)); + CHECK(expect_mutation(path, 12, dimension, 4, LARDON3D_MATCH_FILE_BAD_TYPE)); + CHECK(expect_mutation(path, 16, zero_id, 8, LARDON3D_MATCH_FILE_CORRUPT)); + CHECK(expect_mutation(path, 24, zero_id, 8, LARDON3D_MATCH_FILE_CORRUPT)); + CHECK(expect_mutation(path, 8, count_8193, 4, LARDON3D_MATCH_FILE_BAD_COUNT)); + CHECK(expect_mutation(path, 8, count_max, 4, LARDON3D_MATCH_FILE_BAD_COUNT)); + + unsigned char index_one[4] = {1, 0, 0, 0}; + CHECK(expect_mutation(path, 32, index_one, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + CHECK(expect_mutation(path, 36, index_one, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + unsigned char negative[4] = {0, 0, 128, 191}; + unsigned char negative_zero[4] = {0, 0, 0, 128}; + unsigned char positive_inf[4] = {0, 0, 128, 127}; + unsigned char negative_inf[4] = {0, 0, 128, 255}; + unsigned char nan[4] = {0, 0, 192, 127}; + CHECK(expect_mutation(path, 40, negative, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + CHECK(expect_mutation(path, 40, positive_inf, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + CHECK(expect_mutation(path, 40, negative_inf, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + CHECK(expect_mutation(path, 40, nan, 4, LARDON3D_MATCH_FILE_BAD_ENTRY)); + CHECK(expect_mutation(path, 40, negative_zero, 4, LARDON3D_MATCH_FILE_OK)); + + CHECK(write_minimal_match_file(path)); + fd = open(path, O_WRONLY | O_APPEND | O_CLOEXEC); + CHECK(fd >= 0 && write(fd, "x", 1) == 1 && close(fd) == 0); + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 1, 1) == + LARDON3D_MATCH_FILE_BAD_SIZE); + + Lardon3DMatchFileEntry ordered[2] = {{0, 0, 0.0F}, {1, 0, 1.0F}}; + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0 && lardon3d_match_file_write(fd, 1, 32, 1, 2, ordered, 2) == + LARDON3D_MATCH_FILE_OK && + close(fd) == 0); + unsigned char duplicate_a[4] = {0, 0, 0, 0}; + CHECK(mutate_bytes(path, 44, duplicate_a, sizeof(duplicate_a))); + CHECK(lardon3d_match_file_validate(path, &header, 1, 2, 2, 1) == + LARDON3D_MATCH_FILE_BAD_ENTRY); + ordered[1].feature_index_a = 0; + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK(fd >= 0 && lardon3d_match_file_write(fd, 1, 32, 1, 2, ordered, 2) == + LARDON3D_MATCH_FILE_BAD_ENTRY && + close(fd) == 0); + CHECK(unlink(path) == 0 && rmdir(dir) == 0); + return true; +} + +/* --- Matcher utility function tests --- */ + +static bool test_matcher_kind_string(void) { + CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_ORB_BF), "orb_bf") == 0); + CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_SIFT_BF), "sift_bf") == 0); + CHECK(strcmp(lardon3d_matcher_kind_string(LARDON3D_MATCHER_ROOTSIFT_BF), "rootsift_bf") == 0); + return true; +} + +static bool test_matcher_fingerprint(void) { + unsigned char fp1[32], fp2[32]; + Lardon3DMatcherParams p1 = {LARDON3D_MATCHER_ORB_BF, 0.75F}; + Lardon3DMatcherParams p2 = {LARDON3D_MATCHER_ORB_BF, 0.80F}; + lardon3d_matcher_fingerprint(&p1, fp1); + lardon3d_matcher_fingerprint(&p2, fp2); + CHECK(memcmp(fp1, fp2, 32) != 0); + + unsigned char fp3[32]; + lardon3d_matcher_fingerprint(&p1, fp3); + CHECK(memcmp(fp1, fp3, 32) == 0); + + Lardon3DMatcherParams p3 = {LARDON3D_MATCHER_SIFT_BF, 0.75F}; + unsigned char fp4[32]; + lardon3d_matcher_fingerprint(&p3, fp4); + CHECK(memcmp(fp1, fp4, 32) != 0); + + return true; +} + +static bool test_matcher_default_ratio(void) { + float r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ORB_BF); + CHECK(r == 0.75F); + r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_SIFT_BF); + CHECK(r == 0.7F); + r = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ROOTSIFT_BF); + CHECK(r == 0.7F); + return true; +} + +static bool run_tests(void) { + CHECK(test_match_file_write_read()); + CHECK(test_match_file_empty()); + CHECK(test_match_file_bad_magic()); + CHECK(test_match_file_bad_version()); + CHECK(test_match_file_truncated()); + CHECK(test_match_file_invalid_entry()); + CHECK(test_match_file_nan_distance()); + CHECK(test_match_file_f32()); + CHECK(test_match_file_bad_type()); + CHECK(test_match_file_maximum_and_multiplicity()); + CHECK(test_match_file_corruption_matrix()); + CHECK(test_matcher_kind_string()); + CHECK(test_matcher_fingerprint()); + CHECK(test_matcher_default_ratio()); + return true; +} + +int main(void) { + return run_tests() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_matcher_e2e.c b/tests/test_matcher_e2e.c new file mode 100644 index 0000000..0b43be0 --- /dev/null +++ b/tests/test_matcher_e2e.c @@ -0,0 +1,385 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define CHECK(x) \ + do { \ + if (!(x)) { \ + fprintf(stderr, "Échec ligne %d: %s\n", __LINE__, #x); \ + return false; \ + } \ + } while (0) + +typedef struct { + char root[PATH_MAX]; + char database_path[PATH_MAX]; + Lardon3DProjectDb *database; + Lardon3DAppState state; + Lardon3DProjectDbScanSet scanset; + Lardon3DProjectDbImage image_a; + Lardon3DProjectDbImage image_b; + Lardon3DProjectDbCandidatePair pair; +} Fixture; + +static bool join_path(char output[PATH_MAX], const char *a, const char *b) { + int n = snprintf(output, PATH_MAX, "%s/%s", a, b); + return n > 0 && (size_t)n < PATH_MAX; +} + +static bool remove_tree(const char *path) { + struct stat info; + if (lstat(path, &info) != 0) return errno == ENOENT; + if (!S_ISDIR(info.st_mode)) return unlink(path) == 0; + DIR *directory = opendir(path); + if (!directory) return false; + bool ok = true; + for (struct dirent *entry = readdir(directory); entry; entry = readdir(directory)) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; + char child[PATH_MAX]; + if (!join_path(child, path, entry->d_name) || !remove_tree(child)) ok = false; + } + if (closedir(directory) != 0 || rmdir(path) != 0) ok = false; + return ok; +} + +static void image_asset_path(const unsigned char hash[32], char path[4096]) { + static const char digits[] = "0123456789abcdef"; + char hex[65]; + for (size_t i = 0; i < 32; ++i) { + hex[2 * i] = digits[hash[i] >> 4]; + hex[2 * i + 1] = digits[hash[i] & 15U]; + } + hex[64] = '\0'; + (void)snprintf(path, 4096, "assets/images/%c%c/%s", hex[0], hex[1], hex); +} + +static bool register_image(Fixture *fixture, unsigned char seed, + Lardon3DProjectDbImage *image) { + unsigned char hash[32]; + memset(hash, seed, sizeof(hash)); + char path[4096]; + image_asset_path(hash, path); + Lardon3DProjectDbImageRegisterStatus status; + return lardon3d_project_db_register_image( + fixture->database, fixture->scanset.scanset_id, hash, path, 1, "fixture.bin", + "/synthetic/fixture.bin", 0, seed, &status, image) == LARDON3D_PROJECT_DB_OK; +} + +static bool fixture_open(Fixture *fixture) { + memset(fixture, 0, sizeof(*fixture)); + char root[] = "/tmp/lardon3d-matcher-e2e-XXXXXX"; + char *created = mkdtemp(root); + if (!created || snprintf(fixture->root, sizeof(fixture->root), "%s", created) <= 0 || + !join_path(fixture->database_path, fixture->root, "project.db")) + return false; + char error[256]; + if (lardon3d_project_db_open(fixture->database_path, &fixture->database, error) != + LARDON3D_PROJECT_DB_OK) + return false; + lardon3d_app_state_init(&fixture->state); + fixture->state.project_loaded = true; + fixture->state.project_db = fixture->database; + (void)snprintf(fixture->state.project_path, sizeof(fixture->state.project_path), "%s", + fixture->root); + return lardon3d_project_db_create_scanset(fixture->database, "matcher-e2e", + &fixture->scanset) == LARDON3D_PROJECT_DB_OK && + register_image(fixture, 1, &fixture->image_a) && + register_image(fixture, 2, &fixture->image_b) && + lardon3d_project_db_create_candidate_pair( + fixture->database, fixture->image_a.image_id, fixture->image_b.image_id, 1, + &fixture->pair) == LARDON3D_PROJECT_DB_OK; +} + +static bool fixture_close(Fixture *fixture) { + char matches_path[PATH_MAX]; + bool no_temps = true; + if (join_path(matches_path, fixture->root, "assets/matches")) { + DIR *directory = opendir(matches_path); + if (directory) { + for (struct dirent *entry = readdir(directory); entry; entry = readdir(directory)) { + if (strncmp(entry->d_name, ".match-", 7) == 0) no_temps = false; + } + if (closedir(directory) != 0) no_temps = false; + } else if (errno != ENOENT) { + no_temps = false; + } + } else { + no_temps = false; + } + if (fixture->database) lardon3d_project_db_close(fixture->database); + fixture->database = NULL; + fixture->state.project_db = NULL; + bool removed = remove_tree(fixture->root); + return no_temps && removed; +} + +static bool publish_features(Fixture *fixture, uint64_t image_id, const char *kind, + Lardon3DFeatureDescriptorType type, const void *descriptors, + uint32_t count, unsigned char fingerprint_seed, + Lardon3DProjectDbFeatureSet *set) { + Lardon3DExtractedFeatures features; + memset(&features, 0, sizeof(features)); + features.image_width = 64; + features.image_height = 64; + features.feature_count = count; + if (count > 0) { + features.keypoints = calloc(count, sizeof(*features.keypoints)); + if (!features.keypoints) return false; + for (uint32_t i = 0; i < count; ++i) features.keypoints[i].size = 1.0F; + size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float); + size_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128; + features.descriptor_bytes = (size_t)count * dimension * scalar; + features.descriptors = malloc(features.descriptor_bytes); + if (!features.descriptors) { + free(features.keypoints); + return false; + } + memcpy(features.descriptors, descriptors, features.descriptor_bytes); + } + unsigned char fingerprint[32]; + memset(fingerprint, fingerprint_seed, sizeof(fingerprint)); + uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128; + Lardon3DFeatureStoreResult result = lardon3d_feature_store_publish_v2( + &fixture->state, image_id, 0, kind, 1, fingerprint, type, dimension, 0, &features, set); + free(features.keypoints); + free(features.descriptors); + return result == LARDON3D_FEATURE_STORE_OK; +} + +static bool read_result_asset(const Fixture *fixture, const Lardon3DProjectDbFeatureSet *set_a, + const Lardon3DProjectDbFeatureSet *set_b, + const Lardon3DProjectDbMatchResult *result, + Lardon3DMatchFileEntry *entries, size_t capacity, + uint32_t *count) { + char path[PATH_MAX]; + if (!join_path(path, fixture->root, result->match_asset_path)) return false; + int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) return false; + Lardon3DMatchFileHeader header; + Lardon3DMatchFileResult read_result = lardon3d_match_file_read( + fd, &header, entries, capacity, count, set_a->feature_set_id, set_b->feature_set_id, + set_a->feature_count, set_b->feature_count); + (void)close(fd); + return read_result == LARDON3D_MATCH_FILE_OK; +} + +static bool run_kind_e2e(const char *kind, Lardon3DMatcherKind matcher_kind, + Lardon3DFeatureDescriptorType type) { + Fixture fixture; + CHECK(fixture_open(&fixture)); + uint32_t dimension = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 32 : 128; + size_t scalar = type == LARDON3D_FEATURE_DESCRIPTOR_U8 ? 1 : sizeof(float); + size_t row_bytes = (size_t)dimension * scalar; + unsigned char *a = calloc(3, row_bytes); + unsigned char *b = calloc(3, row_bytes); + CHECK(a && b); + if (type == LARDON3D_FEATURE_DESCRIPTOR_U8) { + memset(a + row_bytes, 0xFF, row_bytes); + memset(b + row_bytes, 0xF0, row_bytes); + memset(b + 2 * row_bytes, 0xFF, row_bytes); + } else { + float *af = (float *)a; + float *bf = (float *)b; + for (uint32_t i = 0; i < dimension; ++i) { + af[dimension + i] = 2.0F; + bf[dimension + i] = 1.0F; + bf[2 * dimension + i] = 2.0F; + } + } + Lardon3DProjectDbFeatureSet set_a, set_b; + CHECK(publish_features(&fixture, fixture.image_a.image_id, kind, type, a, 3, 3, &set_a)); + CHECK(publish_features(&fixture, fixture.image_b.image_id, kind, type, b, 3, 4, &set_b)); + free(a); + free(b); + + Lardon3DMatcherParams params = {matcher_kind, lardon3d_matcher_default_ratio(matcher_kind)}; + Lardon3DProjectDbMatchResult result; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && + result.match_count == 3 && result.has_match_asset); + uint64_t result_id = result.match_result_id; + unsigned char asset_hash[32]; + memcpy(asset_hash, result.match_asset_sha256, 32); + Lardon3DMatchFileEntry entries[3]; + uint32_t count = 0; + CHECK(read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count)); + CHECK(count == 3 && entries[0].feature_index_a == 0 && entries[0].feature_index_b == 0 && + entries[1].feature_index_a == 1 && entries[1].feature_index_b == 2 && + entries[2].feature_index_a == 2 && entries[2].feature_index_b == 0); + + lardon3d_project_db_close(fixture.database); + fixture.database = NULL; + fixture.state.project_db = NULL; + char error[256]; + CHECK(lardon3d_project_db_open(fixture.database_path, &fixture.database, error) == + LARDON3D_PROJECT_DB_OK); + fixture.state.project_db = fixture.database; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == result_id && + memcmp(result.match_asset_sha256, asset_hash, 32) == 0); + + char asset_path[PATH_MAX]; + CHECK(join_path(asset_path, fixture.root, result.match_asset_path)); + int fd = open(asset_path, O_WRONLY | O_TRUNC | O_CLOEXEC); + CHECK(fd >= 0 && write(fd, "corrupt", 7) == 7 && close(fd) == 0); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == result_id && result.match_count == 3 && + read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count)); + + fd = open(asset_path, O_WRONLY | O_CLOEXEC); + unsigned char wrong_feature_set[8] = {99, 0, 0, 0, 0, 0, 0, 0}; + CHECK(fd >= 0 && pwrite(fd, wrong_feature_set, sizeof(wrong_feature_set), 16) == + (ssize_t)sizeof(wrong_feature_set) && + close(fd) == 0); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == result_id && + read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count)); + + CHECK(unlink(asset_path) == 0); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == result_id && + read_result_asset(&fixture, &set_a, &set_b, &result, entries, 3, &count)); + + Lardon3DMatcherParams changed = params; + changed.ratio_threshold -= 0.05F; + Lardon3DProjectDbMatchResult distinct; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, &changed, &distinct) == + LARDON3D_MATCHER_OK); + CHECK(distinct.match_result_id != result_id && + memcmp(distinct.match_asset_sha256, result.match_asset_sha256, 32) == 0); + for (int iteration = 0; iteration < 100; ++iteration) { + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == result_id && result.match_count == 3); + } + CHECK(fixture_close(&fixture)); + return true; +} + +static bool test_ratio_single_neighbor_and_no_match(void) { + Fixture fixture; + CHECK(fixture_open(&fixture)); + unsigned char a[32], b[64]; + memset(a, 0x0F, sizeof(a)); + memset(b, 0, 32); + memset(b + 32, 0xFF, 32); + Lardon3DProjectDbFeatureSet set_a, set_b; + CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 10, &set_a)); + CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, b, 2, 11, &set_b)); + Lardon3DMatcherParams params = {LARDON3D_MATCHER_ORB_BF, 0.75F}; + Lardon3DProjectDbMatchResult result; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && + result.match_count == 0 && !result.has_match_asset); + uint64_t no_match_id = result.match_result_id; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK && result.match_result_id == no_match_id); + for (int iteration = 0; iteration < 100; ++iteration) { + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.match_result_id == no_match_id && !result.has_match_asset); + } + CHECK(fixture_close(&fixture)); + + CHECK(fixture_open(&fixture)); + memset(a, 0, sizeof(a)); + memset(b, 0, 32); + CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 12, &set_a)); + CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, b, 1, 13, &set_b)); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED && result.match_count == 1); + CHECK(fixture_close(&fixture)); + + CHECK(fixture_open(&fixture)); + CHECK(publish_features(&fixture, fixture.image_a.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, a, 1, 14, &set_a)); + CHECK(publish_features(&fixture, fixture.image_b.image_id, "orb", + LARDON3D_FEATURE_DESCRIPTOR_U8, NULL, 0, 15, &set_b)); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &set_a, &set_b, ¶ms, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_NO_MATCH && result.match_count == 0); + CHECK(fixture_close(&fixture)); + return true; +} + +static bool test_kind_and_ownership_rejection(void) { + Fixture fixture; + CHECK(fixture_open(&fixture)); + float descriptors[128] = {0}; + Lardon3DProjectDbFeatureSet sift_a, sift_b; + CHECK(publish_features(&fixture, fixture.image_b.image_id, "sift", + LARDON3D_FEATURE_DESCRIPTOR_F32, descriptors, 1, 21, &sift_b)); + CHECK(publish_features(&fixture, fixture.image_a.image_id, "sift", + LARDON3D_FEATURE_DESCRIPTOR_F32, descriptors, 1, 20, &sift_a)); + CHECK(sift_a.feature_set_id > sift_b.feature_set_id); + Lardon3DMatcherParams rootsift = {LARDON3D_MATCHER_ROOTSIFT_BF, 0.7F}; + Lardon3DMatcherStats stats; + char output[PATH_MAX]; + CHECK(join_path(output, fixture.root, "wrong.match")); + CHECK(lardon3d_matcher_run(fixture.root, &sift_a, &sift_b, &rootsift, output, &stats) == + LARDON3D_MATCHER_TYPE_MISMATCH); + Lardon3DMatcherParams sift = {LARDON3D_MATCHER_SIFT_BF, 0.7F}; + Lardon3DProjectDbMatchResult result; + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &sift_a, &sift_b, &sift, &result) == + LARDON3D_MATCHER_OK); + CHECK(result.result_status == LARDON3D_MATCH_RESULT_STATUS_MATCHED); + CHECK(lardon3d_matcher_match_and_publish(fixture.root, fixture.database, &fixture.pair, + &sift_b, &sift_a, &sift, &result) == + LARDON3D_MATCHER_FAILED); + CHECK(fixture_close(&fixture)); + return true; +} + +static bool run_tests(void) { + CHECK(run_kind_e2e("orb", LARDON3D_MATCHER_ORB_BF, + LARDON3D_FEATURE_DESCRIPTOR_U8)); + CHECK(run_kind_e2e("sift", LARDON3D_MATCHER_SIFT_BF, + LARDON3D_FEATURE_DESCRIPTOR_F32)); + CHECK(run_kind_e2e("rootsift", LARDON3D_MATCHER_ROOTSIFT_BF, + LARDON3D_FEATURE_DESCRIPTOR_F32)); + CHECK(test_ratio_single_neighbor_and_no_match()); + CHECK(test_kind_and_ownership_rejection()); + return true; +} + +int main(void) { + return run_tests() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_project_db.c b/tests/test_project_db.c index 1be70f2..1a1183a 100644 --- a/tests/test_project_db.c +++ b/tests/test_project_db.c @@ -89,7 +89,7 @@ static bool create_future_database(const char *path) { } bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);" - "INSERT INTO metadata VALUES('schema_version',10);", + "INSERT INTO metadata VALUES('schema_version',11);", NULL, NULL, NULL) == SQLITE_OK; return sqlite3_close(connection) == SQLITE_OK && ok; } @@ -103,6 +103,7 @@ static bool create_v7_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pairs;" "UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;"; @@ -119,6 +120,7 @@ static bool create_v6_database(const char *path) { if (sqlite3_open(path, &connection) != SQLITE_OK) return false; static const char sql[] = "PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;" + "DROP TABLE match_results;" "DROP TABLE candidate_pair_generate_tasks;" "DROP TABLE candidate_pairs;" "DROP TABLE feature_support_members;DROP TABLE feature_support_groups;" @@ -308,7 +310,7 @@ static bool run_test(void) { char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]; Lardon3DProjectDb *database = NULL; CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); - CHECK(database && lardon3d_project_db_schema_version(database) == 9); + CHECK(database && lardon3d_project_db_schema_version(database) == 10); bool legacy_pending = true; CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) == LARDON3D_PROJECT_DB_OK && @@ -575,7 +577,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_INVALID_ARGUMENT); lardon3d_project_db_close(contexts[0].database); database = NULL; - CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1)); CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK); @@ -597,7 +599,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) == 9); + CHECK(lardon3d_project_db_schema_version(database) == 10); CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK && strcmp(loaded_project.stable_id, "legacy-project") == 0); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); @@ -607,7 +609,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(legacy_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); CHECK(create_v1_database(failed_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0); @@ -632,7 +634,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(v2_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); CHECK(create_v2_database(failed_v3_migration_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0); @@ -660,7 +662,7 @@ static bool run_test(void) { LARDON3D_PROJECT_DB_OK); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(v3_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); CHECK(create_v3_database(failed_v4_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0); @@ -677,7 +679,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) == 9); + CHECK(lardon3d_project_db_schema_version(database) == 10); CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK); CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) == LARDON3D_PROJECT_DB_OK); @@ -711,24 +713,24 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='sift_extract_tasks'", 0)); CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 9); + lardon3d_project_db_schema_version(database) == 10); lardon3d_project_db_close(database); database = NULL; CHECK(create_v5_database(direct_v5_path)); CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5)); CHECK(lardon3d_project_db_open(direct_v5_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 9); + lardon3d_project_db_schema_version(database) == 10); lardon3d_project_db_close(database); database = NULL; CHECK(create_v7_database(v8_path)); CHECK(query_integer(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) == 9); + CHECK(lardon3d_project_db_schema_version(database) == 10); lardon3d_project_db_close(database); database = NULL; - CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 9)); + CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 10)); CHECK(create_v7_database(failed_v8_path)); CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0); @@ -739,7 +741,7 @@ static bool run_test(void) { "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='candidate_pairs'", 0)); CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK && - lardon3d_project_db_schema_version(database) == 9); + lardon3d_project_db_schema_version(database) == 10); lardon3d_project_db_close(database); database = NULL;