feat(reconstruction): add precision features, candidate pairs and matcher pipeline
This commit is contained in:
parent
e08e609fc4
commit
02bf2c6d07
33 changed files with 4003 additions and 27 deletions
27
.agents/agents/lardon-architect.md
Normal file
27
.agents/agents/lardon-architect.md
Normal file
|
|
@ -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.
|
||||||
20
.agents/agents/lardon-build.md
Normal file
20
.agents/agents/lardon-build.md
Normal file
|
|
@ -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.
|
||||||
20
.agents/agents/lardon-concurrency.md
Normal file
20
.agents/agents/lardon-concurrency.md
Normal file
|
|
@ -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.
|
||||||
24
.agents/agents/lardon-diagnose.md
Normal file
24
.agents/agents/lardon-diagnose.md
Normal file
|
|
@ -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.
|
||||||
18
.agents/agents/lardon-docs.md
Normal file
18
.agents/agents/lardon-docs.md
Normal file
|
|
@ -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.
|
||||||
33
.agents/agents/lardon-orchestrator.md
Normal file
33
.agents/agents/lardon-orchestrator.md
Normal file
|
|
@ -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.
|
||||||
20
.agents/agents/lardon-read.md
Normal file
20
.agents/agents/lardon-read.md
Normal file
|
|
@ -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.
|
||||||
29
.agents/agents/lardon-review.md
Normal file
29
.agents/agents/lardon-review.md
Normal file
|
|
@ -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.
|
||||||
32
.agents/agents/lardon-tests.md
Normal file
32
.agents/agents/lardon-tests.md
Normal file
|
|
@ -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.
|
||||||
25
.agents/plugins/lardon-safety/hooks/hooks.json
Normal file
25
.agents/plugins/lardon-safety/hooks/hooks.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.agents/plugins/lardon-safety/plugin.json
Normal file
5
.agents/plugins/lardon-safety/plugin.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "lardon-safety",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Hard safety policy for Goose on Lardon3D"
|
||||||
|
}
|
||||||
62
.agents/plugins/lardon-safety/scripts/policy.py
Executable file
62
.agents/plugins/lardon-safety/scripts/policy.py
Executable file
|
|
@ -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)
|
||||||
53
.goosehints
Normal file
53
.goosehints
Normal file
|
|
@ -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.
|
||||||
|
|
@ -107,6 +107,9 @@ Acquisitions
|
||||||
- [Feature Store](docs/architecture/feature_store.md)
|
- [Feature Store](docs/architecture/feature_store.md)
|
||||||
- [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md)
|
- [Precision Feature Pipeline v1A](docs/architecture/precision_feature_pipeline.md)
|
||||||
- [Visual Index](docs/architecture/visual_index.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)
|
- [Viewer](docs/architecture/viewer.md)
|
||||||
- [Revue des fondations](docs/architecture/foundation_review.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
|
## Statut
|
||||||
|
|
||||||
Lardon3D est en développement actif. La persistance des tâches, le catalogue,
|
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
|
le Feature Store multipasse, le Visual Index ORB, Candidate Pair Generator
|
||||||
Pair Generator, matching, DAG générique, SfM et viewer restent des tickets
|
et Matcher v1 sont implémentés. DAG générique, vérification géométrique,
|
||||||
séparés planifiés.
|
SfM et viewer restent des tickets séparés planifiés.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
**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
|
## Extension v2 multi-descriptor
|
||||||
|
|
||||||
|
|
|
||||||
64
docs/architecture/match_result.md
Normal file
64
docs/architecture/match_result.md
Normal file
|
|
@ -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
|
||||||
96
docs/architecture/matcher.md
Normal file
96
docs/architecture/matcher.md
Normal file
|
|
@ -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/<préfixe>/<sha256>`, 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
|
||||||
|
|
@ -111,6 +111,23 @@ peine d'être试探ées ? » sans validation géométrique.
|
||||||
|
|
||||||
**Statut :** IMPLEMENTED
|
**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
|
## Résultats et publication live
|
||||||
|
|
||||||
Les traitements fonctionnent par séquences adaptatives : lire un lot borné,
|
Les traitements fonctionnent par séquences adaptatives : lire un lot borné,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Base de données projet Lardon3D
|
# Base de données projet Lardon3D
|
||||||
|
|
||||||
> Version courante : **v9**. La migration transactionnelle v8→v9 ajoute
|
> Version courante : **v10**. La migration transactionnelle v9→v10 ajoute
|
||||||
> la table `candidate_pair_generate_tasks` pour la tâche durable Candidate
|
> la table `match_results` pour le Match Result Model. La migration v8→v9
|
||||||
> Pair. La migration v7→v8 ajoute la table `candidate_pairs` pour le
|
> ajoute la table `candidate_pair_generate_tasks` pour la tâche durable
|
||||||
> sous-système Candidate Pair.
|
> 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
|
> Les migrations historiques restent ordonnées et les faults d'injection
|
||||||
> vérifient le rollback.
|
> 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_record_candidate_pair_generate_task()` — UPSERT checkpoint
|
||||||
- `lardon3d_project_db_load_candidate_pair_generate_task()` — SELECT par task_id
|
- `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
|
## Ouverture et migrations
|
||||||
|
|
||||||
Une DB vide reçoit directement le schéma v7 dans une transaction
|
Une DB vide reçoit directement le schéma v7 dans une transaction
|
||||||
|
|
@ -452,7 +513,7 @@ ouvert.
|
||||||
|
|
||||||
## Statut
|
## 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
|
projet, transactions tâche+checkpoint, pagination de reprise et artefacts
|
||||||
génériques.
|
génériques.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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). |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **Idempotence** | L'exécution répétée ne crée pas de doublons. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ».
|
réelle du lot ; `peak_memory_bytes == 0` signifie « mesure inconnue ».
|
||||||
Chaque séquence interroge le Visual Index pour jusqu'à 64 Feature Sets et
|
Chaque séquence interroge le Visual Index pour jusqu'à 64 Feature Sets et
|
||||||
persiste les paires candidates avec idempotence.
|
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
|
## Limites actuelles
|
||||||
|
|
||||||
|
|
|
||||||
86
include/lardon3d/match_file.h
Normal file
86
include/lardon3d/match_file.h
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
#ifndef LARDON3D_MATCH_FILE_H
|
||||||
|
#define LARDON3D_MATCH_FILE_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
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
|
||||||
91
include/lardon3d/matcher.h
Normal file
91
include/lardon3d/matcher.h
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#ifndef LARDON3D_MATCHER_H
|
||||||
|
#define LARDON3D_MATCHER_H
|
||||||
|
|
||||||
|
#include <lardon3d/feature_store.h>
|
||||||
|
#include <lardon3d/match_file.h>
|
||||||
|
#include <lardon3d/project_db.h>
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
#include <lardon3d/task.h>
|
#include <lardon3d/task.h>
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 9,
|
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 10,
|
||||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||||
|
|
@ -17,6 +17,7 @@ enum {
|
||||||
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
|
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
|
||||||
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256,
|
LARDON3D_PROJECT_DB_CATALOG_PAGE_MAX = 256,
|
||||||
LARDON3D_PROJECT_DB_CANDIDATE_PAIR_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_SCANSET_NAME_CAPACITY = 256,
|
||||||
LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256,
|
LARDON3D_PROJECT_DB_IMAGE_NAME_CAPACITY = 256,
|
||||||
LARDON3D_PROJECT_DB_SHA256_SIZE = 32,
|
LARDON3D_PROJECT_DB_SHA256_SIZE = 32,
|
||||||
|
|
@ -130,11 +131,33 @@ typedef struct {
|
||||||
int64_t created_at;
|
int64_t created_at;
|
||||||
} Lardon3DProjectDbCandidatePair;
|
} 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 {
|
typedef enum {
|
||||||
LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0,
|
LARDON3D_PROJECT_DB_IMAGE_REGISTERED = 0,
|
||||||
LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
|
LARDON3D_PROJECT_DB_IMAGE_ALREADY_PRESENT
|
||||||
} Lardon3DProjectDbImageRegisterStatus;
|
} Lardon3DProjectDbImageRegisterStatus;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
LARDON3D_MATCH_RESULT_STATUS_NO_MATCH = 0,
|
||||||
|
LARDON3D_MATCH_RESULT_STATUS_MATCHED = 1,
|
||||||
|
};
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LARDON3D_DB_FEATURE_ASSET_DURABLE = 0,
|
LARDON3D_DB_FEATURE_ASSET_DURABLE = 0,
|
||||||
LARDON3D_DB_FEATURE_ASSET_PUBLISHED_NOT_DURABLE = 1
|
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,
|
Lardon3DProjectDb *database, uint64_t task_id,
|
||||||
Lardon3DProjectDbCandidatePairGenerateTask *parameters);
|
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
|
#endif
|
||||||
|
|
|
||||||
81
meson.build
81
meson.build
|
|
@ -59,6 +59,7 @@ executable(
|
||||||
'src/image_catalog_persistent.c',
|
'src/image_catalog_persistent.c',
|
||||||
'src/feature_extractor_opencv.cpp',
|
'src/feature_extractor_opencv.cpp',
|
||||||
'src/feature_store.c',
|
'src/feature_store.c',
|
||||||
|
'src/feature_extractor_opencv.cpp',
|
||||||
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
|
'src/feature_task.c', 'src/sift_task.c', 'src/precision_features.c',
|
||||||
'src/visual_index.c',
|
'src/visual_index.c',
|
||||||
'src/visual_index_task.c',
|
'src/visual_index_task.c',
|
||||||
|
|
@ -75,6 +76,8 @@ executable(
|
||||||
'src/resource_governor.c',
|
'src/resource_governor.c',
|
||||||
'src/resource_snapshot.c',
|
'src/resource_snapshot.c',
|
||||||
'src/hardware_profile.c',
|
'src/hardware_profile.c',
|
||||||
|
'src/match_file.c',
|
||||||
|
'src/matcher.cpp',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw, threads, sqlite3, openssl, opencv],
|
dependencies: [ncursesw, threads, sqlite3, openssl, opencv],
|
||||||
|
|
@ -389,6 +392,22 @@ project_db_test = executable(
|
||||||
|
|
||||||
test('project-db', project_db_test, timeout: 30)
|
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(
|
project_test = executable(
|
||||||
'test-project',
|
'test-project',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
@ -517,3 +536,65 @@ resource_reservation_test = executable(
|
||||||
)
|
)
|
||||||
|
|
||||||
test('resource-reservation', resource_reservation_test, timeout: 60)
|
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],
|
||||||
|
)
|
||||||
|
|
|
||||||
280
src/match_file.c
Normal file
280
src/match_file.c
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/match_file.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
695
src/matcher.cpp
Normal file
695
src/matcher.cpp
Normal file
|
|
@ -0,0 +1,695 @@
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <new>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <opencv2/core.hpp>
|
||||||
|
#include <opencv2/features2d.hpp>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <lardon3d/feature_extractor.h>
|
||||||
|
#include <lardon3d/match_file.h>
|
||||||
|
#include <lardon3d/matcher.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<std::chrono::nanoseconds>(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<cv::DMatch> &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<unsigned char> &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<float> &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<MatchEntry> filtered_matches;
|
||||||
|
filtered_matches.reserve(feature_set_a->feature_count);
|
||||||
|
|
||||||
|
if (params->kind == LARDON3D_MATCHER_ORB_BF) {
|
||||||
|
std::vector<unsigned char> 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<std::vector<cv::DMatch>> 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<float> 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<std::vector<cv::DMatch>> 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<Lardon3DMatchFileEntry> 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);
|
||||||
|
}
|
||||||
452
src/project_db.c
452
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),"
|
"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)));";
|
"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) {
|
static void copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text) {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
|
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
|
||||||
|
|
@ -288,7 +315,7 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
|
||||||
}
|
}
|
||||||
if (from_version != 0 && from_version != 1 && from_version != 2 && from_version != 3 &&
|
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 != 4 && from_version != 5 && from_version != 6 && from_version != 7 &&
|
||||||
from_version != 8) {
|
from_version != 8 && from_version != 9) {
|
||||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
}
|
}
|
||||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||||
|
|
@ -499,6 +526,21 @@ static Lardon3DProjectDbResult migrate(Lardon3DProjectDb *database, unsigned int
|
||||||
"finish schema v9 migration");
|
"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) {
|
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||||
result = execute(database, "COMMIT", "commit migration");
|
result = execute(database, "COMMIT", "commit migration");
|
||||||
}
|
}
|
||||||
|
|
@ -611,8 +653,9 @@ Lardon3DProjectDbResult lardon3d_project_db_open(const char *path, Lardon3DProje
|
||||||
"feature_support_sets",
|
"feature_support_sets",
|
||||||
"feature_support_groups",
|
"feature_support_groups",
|
||||||
"feature_support_members",
|
"feature_support_members",
|
||||||
"candidate_pairs"};
|
"candidate_pairs",
|
||||||
for (size_t index = 0; index < 16 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
"match_results"};
|
||||||
|
for (size_t index = 0; index < 17 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
||||||
if (!table_exists(database->connection, required[index])) {
|
if (!table_exists(database->connection, required[index])) {
|
||||||
copy_error(database->error, "Schéma v1 incomplet.");
|
copy_error(database->error, "Schéma v1 incomplet.");
|
||||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||||
|
|
@ -3180,6 +3223,409 @@ Lardon3DProjectDbResult lardon3d_project_db_load_candidate_pair_generate_task(
|
||||||
return result;
|
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
|
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||||
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
|
Lardon3DProjectDbResult lardon3d_project_db_test_delete_feature_identity(
|
||||||
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {
|
Lardon3DProjectDb *database, uint64_t feature_set_id, uint64_t feature_asset_id) {
|
||||||
|
|
|
||||||
201
tests/benchmark_matcher.cpp
Normal file
201
tests/benchmark_matcher.cpp
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/feature_store.h>
|
||||||
|
#include <lardon3d/matcher.h>
|
||||||
|
#include <lardon3d/project_db.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<uint64_t> 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<Lardon3DFeatureKeypoint *>(
|
||||||
|
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<unsigned char *>(malloc(features->descriptor_bytes));
|
||||||
|
if (type == LARDON3D_FEATURE_DESCRIPTOR_U8) {
|
||||||
|
features->descriptor_bytes = (size_t)count * 32;
|
||||||
|
features->descriptors = static_cast<unsigned char *>(
|
||||||
|
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<float *>(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<Sample> 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<uint64_t> 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<uint64_t> 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;
|
||||||
|
}
|
||||||
526
tests/test_match_result.c
Normal file
526
tests/test_match_result.c
Normal file
|
|
@ -0,0 +1,526 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/project_db.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
518
tests/test_matcher.c
Normal file
518
tests/test_matcher.c
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/match_file.h>
|
||||||
|
#include <lardon3d/matcher.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
385
tests/test_matcher_e2e.c
Normal file
385
tests/test_matcher_e2e.c
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/feature_store.h>
|
||||||
|
#include <lardon3d/matcher.h>
|
||||||
|
#include <lardon3d/project_db.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,7 @@ static bool create_future_database(const char *path) {
|
||||||
}
|
}
|
||||||
bool ok = sqlite3_exec(connection,
|
bool ok = sqlite3_exec(connection,
|
||||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||||
"INSERT INTO metadata VALUES('schema_version',10);",
|
"INSERT INTO metadata VALUES('schema_version',11);",
|
||||||
NULL, NULL, NULL) == SQLITE_OK;
|
NULL, NULL, NULL) == SQLITE_OK;
|
||||||
return sqlite3_close(connection) == SQLITE_OK && 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;
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
static const char sql[] =
|
static const char sql[] =
|
||||||
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
||||||
|
"DROP TABLE match_results;"
|
||||||
"DROP TABLE candidate_pair_generate_tasks;"
|
"DROP TABLE candidate_pair_generate_tasks;"
|
||||||
"DROP TABLE candidate_pairs;"
|
"DROP TABLE candidate_pairs;"
|
||||||
"UPDATE metadata SET value=7 WHERE key='schema_version';COMMIT;PRAGMA foreign_keys=ON;";
|
"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;
|
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||||
static const char sql[] =
|
static const char sql[] =
|
||||||
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
"PRAGMA foreign_keys=OFF;BEGIN IMMEDIATE;"
|
||||||
|
"DROP TABLE match_results;"
|
||||||
"DROP TABLE candidate_pair_generate_tasks;"
|
"DROP TABLE candidate_pair_generate_tasks;"
|
||||||
"DROP TABLE candidate_pairs;"
|
"DROP TABLE candidate_pairs;"
|
||||||
"DROP TABLE feature_support_members;DROP TABLE feature_support_groups;"
|
"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];
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||||
Lardon3DProjectDb *database = NULL;
|
Lardon3DProjectDb *database = NULL;
|
||||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
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;
|
bool legacy_pending = true;
|
||||||
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
|
CHECK(lardon3d_project_db_legacy_catalog_pending(database, &legacy_pending) ==
|
||||||
LARDON3D_PROJECT_DB_OK &&
|
LARDON3D_PROJECT_DB_OK &&
|
||||||
|
|
@ -575,7 +577,7 @@ static bool run_test(void) {
|
||||||
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||||
lardon3d_project_db_close(contexts[0].database);
|
lardon3d_project_db_close(contexts[0].database);
|
||||||
database = NULL;
|
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(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_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == 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(create_v1_database(legacy_path));
|
||||||
CHECK(lardon3d_project_db_open(legacy_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
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 &&
|
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK &&
|
||||||
strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
strcmp(loaded_project.stable_id, "legacy-project") == 0);
|
||||||
CHECK(lardon3d_project_db_load_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
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_OK);
|
||||||
lardon3d_project_db_close(database);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
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(create_v1_database(failed_migration_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V2", "1", 1) == 0);
|
||||||
|
|
@ -632,7 +634,7 @@ static bool run_test(void) {
|
||||||
LARDON3D_PROJECT_DB_OK);
|
LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_db_close(database);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
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(create_v2_database(failed_v3_migration_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V3", "1", 1) == 0);
|
||||||
|
|
@ -660,7 +662,7 @@ static bool run_test(void) {
|
||||||
LARDON3D_PROJECT_DB_OK);
|
LARDON3D_PROJECT_DB_OK);
|
||||||
lardon3d_project_db_close(database);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
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(create_v3_database(failed_v4_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V4", "1", 1) == 0);
|
||||||
|
|
@ -677,7 +679,7 @@ static bool run_test(void) {
|
||||||
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
|
fprintf(stderr, "Migration v4 (%d): %s\n", (int)v4_result, error);
|
||||||
}
|
}
|
||||||
CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
|
CHECK(v4_result == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_schema_version(database) == 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_task(database, 9, &task) == LARDON3D_PROJECT_DB_OK);
|
||||||
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) ==
|
CHECK(lardon3d_project_db_load_artifact(database, "legacy-artifact", &loaded_artifact) ==
|
||||||
LARDON3D_PROJECT_DB_OK);
|
LARDON3D_PROJECT_DB_OK);
|
||||||
|
|
@ -711,24 +713,24 @@ static bool run_test(void) {
|
||||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||||
"name='sift_extract_tasks'", 0));
|
"name='sift_extract_tasks'", 0));
|
||||||
CHECK(lardon3d_project_db_open(failed_v7_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
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);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
database = NULL;
|
||||||
|
|
||||||
CHECK(create_v5_database(direct_v5_path));
|
CHECK(create_v5_database(direct_v5_path));
|
||||||
CHECK(query_integer(direct_v5_path, "SELECT value FROM metadata WHERE key='schema_version'", 5));
|
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 &&
|
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);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
database = NULL;
|
||||||
|
|
||||||
CHECK(create_v7_database(v8_path));
|
CHECK(create_v7_database(v8_path));
|
||||||
CHECK(query_integer(v8_path, "SELECT value FROM metadata WHERE key='schema_version'", 7));
|
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_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);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
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(create_v7_database(failed_v8_path));
|
||||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
|
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_MIGRATION_V8", "1", 1) == 0);
|
||||||
|
|
@ -739,7 +741,7 @@ static bool run_test(void) {
|
||||||
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
"SELECT count(*) FROM sqlite_master WHERE type='table' AND "
|
||||||
"name='candidate_pairs'", 0));
|
"name='candidate_pairs'", 0));
|
||||||
CHECK(lardon3d_project_db_open(failed_v8_path, &database, error) == LARDON3D_PROJECT_DB_OK &&
|
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);
|
lardon3d_project_db_close(database);
|
||||||
database = NULL;
|
database = NULL;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue