lardon3d/docs/architecture/tracks.md

20 KiB
Raw Blame History

Track Model v1

Scope

Track Model v1 est le contrat persistant qui transforme les correspondances géométriquement vérifiées en structures multi-view cohérentes. Il stocke des ensembles d'observations 2D liées à un même point physique supposé. Il ne calcule rien, ne triangule pas, ne contient aucune coordonnée 3D et ne résout aucun conflit. Le Track Builder, la triangulation, le Sparse SfM et le Bundle Adjustment sont des étapes séparées ; Gate E a gelé le Builder v1 sans implémenter ces étapes 3D.

Track definition

Un Track est un ensemble d'observations 2D cohérentes d'un même point physique supposé, observé à travers plusieurs images. Chaque observation est identifiée par (feature_set_id, feature_index).

Un Track n'est pas un point 3D. Il ne contient aucune coordonnée 3D, aucune erreur de reprojection, aucun statut de triangulation. La triangulation appartient à une étape ultérieure.

La chaîne scientifique correcte est :

Matcher → Match Result → Geometric Verification → Track Builder v1
→ Track Model → Sparse SfM (futur)

Le Matcher ne produit pas les Tracks. Le Track Builder v1 les assemble à partir des Geometric Verification Results.

Observation identity

Une observation est identifiée par :

(feature_set_id, feature_index)
  • feature_set_id : identifiant SQLite AUTOINCREMENT du Feature Set. Le Feature Set porte directement image_id comme colonne NOT NULL FK. L'image est dérivable par SELECT image_id FROM feature_sets WHERE feature_set_id=?.
  • feature_index : ordinal zero-based dans le tableau de keypoints du Feature File, stable tant que le Feature Set existe. Un Feature Set publié est immutable : aucune API de production ne modifie ses colonnes après INSERT.

L'identité (feature_set_id, feature_index) est suffisante. Il est inutile de porter image_id dans la table d'observations car il est dérivable via feature_sets.image_id.

Note : feature_sets ne possède pas de colonne d'état. L'existence d'une ligne publiée dans la table constitue le contrat réel de disponibilité du Feature Set.

Scientific inputs

Les Tracks sont construits exclusivement à partir de :

Geometric Verification Result
  status == GEOMETRIC_VERIFIED (2)

correspondant exactement au VERIFICATION_SELECTOR du Track Set.

Pour chaque résultat vérifié, les entrées du Match File dont le bit correspondant dans le masque d'inliers vaut 1 fournissent les correspondances valides. La chaîne de dérivation est :

GVR → match_result_id
  → candidate_pair + feature_set_id_a + feature_set_id_b
  → Match File entry[i] = (feature_index_a, feature_index_b, distance)
  → bit i du masque d'inliers = 1
  → observation A: (feature_set_id_a, feature_index_a)
  → observation B: (feature_set_id_b, feature_index_b)

Un GEOMETRIC_REJECTED ne produit aucun track. Un Match Result non vérifié géométriquement ne suffit pas.

VERIFICATION_SELECTOR

Le VERIFICATION_SELECTOR définit la configuration de Geometric Verification éligible pour un Track Set. Il est stocké sur le Track Set et fait partie de son identité de reuse.

(
    verifier_kind      INTEGER,  -- ex: 1 = FUNDAMENTAL
    verifier_version   INTEGER,
    parameter_fingerprint BLOB(32)
)

Le Track Builder ne consomme que les GVR avec status == GEOMETRIC_VERIFIED correspondant exactement à ce tuple. Aucune sélection par timestamp, "latest" ou ordre d'insertion n'est permise.

Valeur production : (1, 1, SHA-256 de l'encodage canonique 84 octets).

INPUT_SCOPE

L'INPUT_SCOPE représente l'ensemble scientifique réel des entrées consommées par une Track Generation donnée. Il est distinct du VERIFICATION_SELECTOR : le selector dit quels GVR sont admissibles, le scope dit quels GVR ont effectivement été consommés.

input_scope_hash  BLOB(32)  -- SHA-256 canonique
gvr_count         INTEGER   -- nombre de GVR consommés

INPUT_SCOPE_HASH

Propriété Valeur
Domain/version L3DTSIS1 (8 octets ASCII)
Items geometric_verification_result_id des GVR consommés
Canonical ordering IDs triés par ordre croissant
Serialization Chaque ID : 8 octets little-endian
Digest SHA-256
DB-local IDs OUI — le reuse est scoped à une DB projet
Duplicate handling Inutile — les IDs sont uniques par construction
Empty scope Interdit — un Track Set sans GVR n'a pas de sens

Le digest est calculé sur L3DTSIS1 (8 octets) suivi des IDs sérialisés : SHA-256(L3DTSIS1 || id_0 || id_1 || ... || id_N) où chaque id_i est 8 octets little-endian et les IDs sont triés par ordre croissant.

Le gvr_count est stocké comme métadonnée de validation. Il permet de détecter un scope incomplet sans re-hasher. Il ne fait pas partie du hash lui-même.

Le scope_hash est DB-local : il utilise les geometric_verification_result_id SQLite. Deux DB distinctes avec les mêmes données produiront des IDs différents. Le reuse est donc scoped à une seule DB projet.

Track membership invariants

  1. Minimum structurel : un Track contient au moins 2 observations. Une seule observation ne constitue aucune relation multi-view. Le futur Track Builder v1, la triangulation ou le Sparse SfM pourront appliquer des critères plus stricts. Le Model ne fixe pas de plafond de reconstruction.

  2. One observation per image : un Track ne contient pas deux observations issues de la même image. Cette contrainte est validée par l'API lors de la création. Le schéma v1 ne dénormalise pas image_id dans track_observations ; l'API vérifie déterministement la relation via feature_sets.image_id avant publication sous BEGIN IMMEDIATE.

    SQL : non protégé (pas de colonne image_id dans track_observations). API : validation par jointure feature_sets.image_id avant INSERT.

  3. Observation unique across tracks : dans un même Track Set, une observation (feature_set_id, feature_index) n'appartient qu'à un seul Track.

    SQL : PRIMARY KEY(track_set_id, feature_set_id, feature_index) sur track_observations. Le track_set_id est dénormalisé depuis tracks. API : validation que track_set_id correspond au track_set_id du track_id parent.

  4. Feature Set existence : chaque feature_set_id référencé existe dans la table feature_sets. La FK SQLite garantit la référence.

    SQL : REFERENCES feature_sets(feature_set_id).

  5. Feature index bounds : feature_index < feature_sets.feature_count pour l'observation correspondante.

    SQL : CHECK(feature_index >= 0). API : validation de la borne supérieure via feature_sets.feature_count (SQLite CHECK ne peut pas référencer une autre table).

Track identity

Un Track persistant possède un identifiant opaque :

track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0)

Il n'a pas d'identité scientifique dérivée de son contenu en v1. Les raisons :

  • un hash de membership rendrait les INSERTs dépendants de l'ordre ;
  • le contenu d'un track peut être reconstruit depuis les GVR sources ;
  • un track_id opaque suffit pour la persistance, le référencement et la pagination ;
  • la corruption est détectée par cohérence interne (doublons, images manquantes, index hors bornes) plutôt que par re-hash.

La reproductibilité est assurée au niveau du Track Set (parent), pas du Track individuel.

Track Set / Generation

Un Track Set est le parent obligatoire de tout Track persistant. Il représente une génération complète de Track Building.

Champs :

track_set_id              INTEGER PK AUTOINCREMENT
builder_kind              TEXT(1..64)
builder_version           INTEGER > 0
parameter_fingerprint     BLOB(32)
verifier_kind             INTEGER   -- VERIFICATION_SELECTOR
verifier_version          INTEGER
verifier_fingerprint      BLOB(32)
input_scope_hash          BLOB(32)
gvr_count                 INTEGER >= 1
track_count               INTEGER >= 0
created_at                INTEGER >= 0

Identité de reuse

(
    builder_kind,
    builder_version,
    parameter_fingerprint,
    verifier_kind,
    verifier_version,
    verifier_fingerprint,
    input_scope_hash
)

gvr_count est stocké comme métadonnée de validation mais ne fait pas partie de l'identité de reuse. Le même input_scope_hash avec un gvr_count différent indiquerait une corruption (hash cohérent mais nombre de sources incohérent).

Un set existant avec cette identité exacte est réutilisé. INSERT OR REPLACE est interdit.

Immutabilité

Un Track Set publié est immutable. Aucune opération d'append, remove ou merge n'est permise sur un track ou un set existant.

L'invalidation scientifique (nouvelle entrée, nouveau scope, nouvelle configuration) produit un nouveau Track Set. Le set précédent reste intact.

La suppression référentielle utilise ON DELETE CASCADE : supprimer un Track Set supprime ses tracks et observations.

Justification

  • chaque rebuild crée un nouveau set, les anciens restent intacts ;
  • plusieurs configurations peuvent coexister (expérimentation) ;
  • l'invalidation est simple : supprimer un set supprime ses tracks via CASCADE ;
  • la reproductibilité est portée par le fingerprint et le scope_hash ;
  • pas d'UPDATE/INSERT/MERGE sur des tracks existants ;
  • cohérent avec tous les résultats publiés existants (Feature Sets, Match Results, GVRs) qui sont immutables après publication.

Le Track Builder v1 construit en mémoire, puis publie un set complet dans une transaction. Aucun track n'est visible avant que le set entier soit validé.

Immutability / incrementality

Un Track publié dans un set est immutable.

L'incrémentalité est gérée par création de nouveaux sets :

  1. nouvelles images → nouveaux Match Results → nouveaux GVR → nouveau Track Set ;
  2. le set précédent reste valide et consultable ;
  3. le futur Sparse SfM choisira quel set consommer.

Cette approche est cohérente avec la philosophie Lardon3D :

  • résultats atomiques ;
  • pas de destruction silencieuse ;
  • reprise à frontière connue ;
  • conservation de l'historique.

Persistence

Conceptual schema

CREATE TABLE track_sets(
    track_set_id INTEGER PRIMARY KEY AUTOINCREMENT
        CHECK(track_set_id > 0),
    builder_kind TEXT NOT NULL
        CHECK(length(builder_kind) > 0 AND length(builder_kind) <= 64),
    builder_version INTEGER NOT NULL CHECK(builder_version > 0),
    parameter_fingerprint BLOB NOT NULL
        CHECK(length(parameter_fingerprint) = 32),
    verifier_kind INTEGER NOT NULL CHECK(verifier_kind > 0),
    verifier_version INTEGER NOT NULL CHECK(verifier_version > 0),
    verifier_fingerprint BLOB NOT NULL
        CHECK(length(verifier_fingerprint) = 32),
    input_scope_hash BLOB NOT NULL
        CHECK(length(input_scope_hash) = 32),
    gvr_count INTEGER NOT NULL CHECK(gvr_count >= 1),
    track_count INTEGER NOT NULL CHECK(track_count >= 0),
    created_at INTEGER NOT NULL CHECK(created_at >= 0),
    UNIQUE(builder_kind, builder_version, parameter_fingerprint,
           verifier_kind, verifier_version, verifier_fingerprint,
           input_scope_hash)
);

CREATE TABLE tracks(
    track_id INTEGER PRIMARY KEY AUTOINCREMENT CHECK(track_id > 0),
    track_set_id INTEGER NOT NULL
        REFERENCES track_sets(track_set_id) ON DELETE CASCADE,
    observation_count INTEGER NOT NULL CHECK(observation_count >= 2)
);

CREATE INDEX tracks_set_idx
    ON tracks(track_set_id, track_id);

CREATE TABLE track_observations(
    track_set_id INTEGER NOT NULL,
    track_id INTEGER NOT NULL
        REFERENCES tracks(track_id) ON DELETE CASCADE,
    feature_set_id INTEGER NOT NULL
        REFERENCES feature_sets(feature_set_id),
    feature_index INTEGER NOT NULL CHECK(feature_index >= 0),
    position_in_track INTEGER NOT NULL CHECK(position_in_track >= 0),
    PRIMARY KEY(track_set_id, feature_set_id, feature_index),
    UNIQUE(track_id, position_in_track)
);

CREATE INDEX track_observations_lookup_idx
    ON track_observations(feature_set_id, feature_index, track_set_id);

Schema invariants

SQL-enforced :

  • track_observations.PRIMARY KEY(track_set_id, feature_set_id, feature_index)
    dans un Track Set donné, une observation n'apparaît qu'une fois. Cela garantit qu'une observation scientifique appartient à au plus un Track dans ce set.
  • REFERENCES tracks(track_id) ON DELETE CASCADE : l'observation appartient à un track existant ; supprimer le track supprime l'observation.
  • REFERENCES feature_sets(feature_set_id) : le Feature Set existe.
  • CHECK(observation_count >= 2) : minimum structurel.
  • UNIQUE(builder_kind, builder_version, parameter_fingerprint, verifier_kind, verifier_version, verifier_fingerprint, input_scope_hash) sur track_sets : identité de reuse, empêche les doublons de set pour une même configuration et un même scope.
  • ON DELETE CASCADE depuis track_sets : supprimer un set supprime tout.
  • CHECK(feature_index >= 0) : borne inférieure de l'index.
  • UNIQUE(track_id, position_in_track) : chaque position dans un track est unique. L'ordre est déterminé par le Track Builder lors de la publication.

API-enforced :

  • track_set_id dans track_observations correspond au track_set_id du track_id parent. Le schéma ne comporte pas de FK composite (aucun précédent dans le codebase). L'API valide cette cohérence avant INSERT sous BEGIN IMMEDIATE.
  • Une seule observation par image par track. L'API valide via jointure à feature_sets.image_id.
  • feature_index < feature_sets.feature_count. L'API valide la borne supérieure.
  • observation_count cohérent avec le nombre réel d'observations insérées.
  • track_count cohérent avec le nombre réel de tracks insérés.
  • position_in_track contigu à partir de 0 pour chaque track.

Note sur la dénormalisation

track_set_id dans track_observations dénormalise une clé grandparent, après le même pattern utilisé par visual_index_memberships.visual_index_id. Le pattern parent-key-in-UNIQUE est déjà répandu dans le codebase. La cohérence repose sur le chemin d'écriture unique du Track Builder et la validation API sous transaction.

track_observations.track_set_id n'a pas de FK directe vers track_sets pour éviter un second chemin CASCADE depuis track_sets vers track_observations (le premier chemin passe par tracks). La cohérence est garantie par l'API sous BEGIN IMMEDIATE.

Provenance

Track Set provenance

Chaque Track Set conserve :

  • builder_kind, builder_version, parameter_fingerprint : configuration du Track Builder ;
  • verifier_kind, verifier_version, verifier_fingerprint : configuration du Geometric Verifier consommé ;
  • input_scope_hash, gvr_count : ensemble réel des GVR consommés.

Ces champs suffisent pour identifier la configuration scientifique complète ayant produit le set.

Edge provenance (deferred)

En v1, la provenance détaillée (quels GVR spécifiques ont contribué à quel track individuel) n'est pas persistée. Les raisons :

  • elle peut être reconstruite en comparant les memberships du set aux GVR disponibles ;
  • une table track_set_sources volumineuse complexifie la DB sans bénéfice immédiat ;
  • une future version du Track Builder pourra l'ajouter dans une migration ultérieure.

Invalidation

Invalidation scientifique

Un nouveau scope, une nouvelle configuration de verifier ou un nouveau builder produit un nouveau Track Set avec une identité différente. Le set précédent reste intact et consultable. Aucune mutation silencieuse n'est permise.

Suppression référentielle

ON DELETE CASCADE s'applique :

  • track_setstrackstrack_observations : supprimer un set supprime tous ses tracks et observations ;
  • feature_sets → (pas de CASCADE vers track_observations) : la FK utilise le comportement par défaut (NO ACTION). Supprimer un Feature Set référencé par une observation est interdit tant que l'observation existe.

Atomic publication

L'unité persistante est le Track Set complet. La publication est une seule transaction BEGIN IMMEDIATE contenant l'INSERT du set, de tous ses tracks et de toutes ses observations.

  • aucun track n'est visible avant le COMMIT du set entier ;
  • un rollback ne laisse aucune ligne partielle ;
  • le created_at du set est le timestamp de la transaction ;
  • le track_count et gvr_count sont validés contre les INSERTs réels.

Le Track Builder v1 utilise le Task Runtime pour le checkpoint/reprise et le Resource Governor pour l'admission. Le Model ne contient aucune logique d'exécution.

Resource bounds

  • Pas de plafond de longueur arbitraire : le Model ne fixe pas de maximum sur le nombre d'observations par Track. Un projet avec N images peut produire des tracks de longueur jusqu'à N.
  • Lecture paginée : list_tracks et list_track_sets utilisent une page de 64 entrées avec curseur.
  • Chargement borné : load track by id charge les observations du track ; la taille est bornée naturellement par le nombre d'images dans le scope.
  • Pas de chargement complet du graphe : aucune API ne charge tous les tracks et toutes les observations d'un projet en une seule fois.
  • Pas de matrice dense : aucune matrice de co-visibilité N×N n'est matérialisée par le Model.

Corruption handling

Le loader doit détecter :

  • track absent (track_id référencé mais inexistant) ;
  • observation invalide (feature_set_id inexistant) ;
  • duplicate observation dans un même track set ;
  • deux observations de la même image dans un même track ;
  • feature_index hors bornes du Feature Set ;
  • observation_count incohérent avec le nombre réel d'observations ;
  • track_set_id dans track_observations ne correspondant pas au track_set_id du track_id parent ;
  • track_set parent absent.

Toute corruption retourne CORRUPT sans résultat partiel.

API

L'API publique implémente :

  • lardon3d_project_db_create_track_set() — INSERT set + ses tracks + observations dans une seule transaction BEGIN IMMEDIATE.
  • lardon3d_project_db_load_track_set() — SELECT par ID.
  • lardon3d_project_db_find_track_set() — SELECT par identité exacte.
  • lardon3d_project_db_list_track_sets() — SELECT paginé ORDER BY id, page 64.
  • lardon3d_project_db_load_track() — SELECT par ID avec observations.
  • lardon3d_project_db_list_tracks() — SELECT par set, paginé ORDER BY id, page 64.
  • lardon3d_project_db_find_track_by_observation() — recherche par (feature_set_id, feature_index) dans un set donné.

La création valide en C : existence des Feature Sets, bornes des feature_index, unicité des observations, unicité image par track, observation_count cohérent, track_set_id cohérent. L'INSERT est transactionnel.

Explicitly out of scope

  • Track Builder algorithmique (union-find, connected components) ;
  • triangulation ;
  • coordonnées 3D ;
  • Essential matrix ;
  • camera pose ;
  • bundle adjustment ;
  • sparse reconstruction / Sparse SfM ;
  • reprojection error ;
  • dense reconstruction ;
  • Track optimization ou merge ;
  • mutation de tracks existants ;
  • co-visibilité (matrice ou calcul) ;
  • sélection par timestamp ou "latest".

Track rejected state

Le Model v1 ne persiste pas d'état Track rejected. Le Model représente des Tracks structurellement valides (≥ 2 observations, cohérents). Le Track Builder v1 décide quels candidats publier. Les candidats non publiés n'existent pas dans le Model ; cette séparation reste la frontière scientifique figée.

Versioning

Project DB v14 introduced the Track storage and v15 adds only durable Track Builder task payload persistence. builder_version et verifier_version décrivent indépendamment les contrats scientifiques. Changer un algorithme n'impose une migration DB que si la représentation persistante change.