397 lines
19 KiB
C
397 lines
19 KiB
C
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <lardon3d/candidate_pair_gen.h>
|
|
#include <lardon3d/feature_store.h>
|
|
#include <lardon3d/image_catalog.h>
|
|
#include <lardon3d/project_db.h>
|
|
#include <lardon3d/visual_index.h>
|
|
|
|
#include "../src/candidate_pair_gen_internal.h"
|
|
|
|
#define CHECK(condition) \
|
|
do { \
|
|
if (!(condition)) { \
|
|
fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
|
|
return false; \
|
|
} \
|
|
} while (0)
|
|
|
|
static bool join_path(char output[PATH_MAX], const char *left, const char *right) {
|
|
int written = snprintf(output, PATH_MAX, "%s/%s", left, right);
|
|
return written > 0 && (size_t)written < PATH_MAX;
|
|
}
|
|
|
|
static bool remove_tree(const char *path) {
|
|
struct stat status;
|
|
if (lstat(path, &status) != 0) {
|
|
return errno == ENOENT;
|
|
}
|
|
if (!S_ISDIR(status.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;
|
|
}
|
|
}
|
|
return closedir(directory) == 0 && rmdir(path) == 0 && ok;
|
|
}
|
|
|
|
static bool write_source(const char *path, unsigned char value) {
|
|
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
|
|
if (descriptor < 0) {
|
|
return false;
|
|
}
|
|
unsigned char bytes[64];
|
|
memset(bytes, value, sizeof(bytes));
|
|
bool ok = write(descriptor, bytes, sizeof(bytes)) == (ssize_t)sizeof(bytes);
|
|
return close(descriptor) == 0 && ok;
|
|
}
|
|
|
|
static bool publish_features(Lardon3DAppState *state, uint64_t image_id, unsigned int family,
|
|
Lardon3DProjectDbFeatureSet *set) {
|
|
enum { COUNT = 16 };
|
|
Lardon3DFeatureKeypoint keypoints[COUNT];
|
|
unsigned char descriptors[COUNT * 32];
|
|
for (uint32_t i = 0; i < COUNT; ++i) {
|
|
keypoints[i] = (Lardon3DFeatureKeypoint){(float)i, (float)i, 8.0F, 0.0F,
|
|
(float)(COUNT - i), 0};
|
|
for (uint32_t byte = 0; byte < 32; ++byte) {
|
|
descriptors[i * 32 + byte] =
|
|
family == 0 || (family == 1 && i < 10)
|
|
? (unsigned char)(i * 17U + byte * 29U)
|
|
: (unsigned char)(family * 73U + i * 31U + byte * 7U);
|
|
}
|
|
}
|
|
Lardon3DExtractedFeatures features = {
|
|
.image_width = 64,
|
|
.image_height = 64,
|
|
.feature_count = COUNT,
|
|
.keypoints = keypoints,
|
|
.descriptors = descriptors,
|
|
.descriptor_bytes = sizeof(descriptors),
|
|
};
|
|
Lardon3DFeatureExtractorParameters parameters = {64, 4, 20};
|
|
Lardon3DFeatureStoreResult result =
|
|
lardon3d_feature_store_publish(state, image_id, 0, ¶meters, &features, set);
|
|
return result == LARDON3D_FEATURE_STORE_OK ||
|
|
result == LARDON3D_FEATURE_STORE_PUBLISHED_NOT_DURABLE;
|
|
}
|
|
|
|
static bool pair_exists(const Lardon3DProjectDbCandidatePair *pairs, size_t count,
|
|
uint64_t image_a, uint64_t image_b) {
|
|
uint64_t canonical_a = image_a < image_b ? image_a : image_b;
|
|
uint64_t canonical_b = image_a < image_b ? image_b : image_a;
|
|
for (size_t i = 0; i < count; ++i) {
|
|
if (pairs[i].image_id_a == canonical_a && pairs[i].image_id_b == canonical_b) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool retain_sparse_memberships(const char *database_path, uint64_t index_id,
|
|
uint64_t first_id, uint64_t last_id) {
|
|
sqlite3 *database = NULL;
|
|
if (sqlite3_open_v2(database_path, &database, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
|
|
sqlite3_close(database);
|
|
return false;
|
|
}
|
|
char sql[256];
|
|
int n = snprintf(sql, sizeof(sql),
|
|
"DELETE FROM visual_index_memberships WHERE visual_index_id=%lu "
|
|
"AND feature_set_id NOT IN(%lu,%lu)",
|
|
(unsigned long)index_id, (unsigned long)first_id,
|
|
(unsigned long)last_id);
|
|
bool ok = n > 0 && (size_t)n < sizeof(sql) &&
|
|
sqlite3_exec(database, sql, NULL, NULL, NULL) == SQLITE_OK;
|
|
sqlite3_close(database);
|
|
return ok;
|
|
}
|
|
|
|
static bool run_test(void) {
|
|
char root[] = "/tmp/lardon3d-candidate-pair-gen-XXXXXX";
|
|
CHECK(mkdtemp(root));
|
|
char internal[PATH_MAX];
|
|
char database_path[PATH_MAX];
|
|
char source_paths[6][PATH_MAX];
|
|
CHECK(join_path(internal, root, ".lardon3d") && mkdir(internal, 0700) == 0 &&
|
|
join_path(database_path, root, "project.db"));
|
|
for (size_t i = 0; i < 6; ++i) {
|
|
CHECK(snprintf(source_paths[i], sizeof(source_paths[i]), "%s/source-%zu.bin", root, i) > 0 &&
|
|
write_source(source_paths[i], (unsigned char)(i + 1)));
|
|
}
|
|
Lardon3DProjectDb *database = NULL;
|
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
|
Lardon3DAppState state;
|
|
lardon3d_app_state_init(&state);
|
|
state.project_loaded = true;
|
|
state.project_db = database;
|
|
CHECK(snprintf(state.project_path, sizeof(state.project_path), "%s", root) > 0);
|
|
Lardon3DProjectDbScanSet scan_a;
|
|
Lardon3DProjectDbScanSet scan_b;
|
|
CHECK(lardon3d_image_catalog_create_scanset(&state, "A", &scan_a) &&
|
|
lardon3d_image_catalog_create_scanset(&state, "B", &scan_b));
|
|
Lardon3DProjectDbImage images[6];
|
|
Lardon3DProjectDbImageAsset asset;
|
|
const uint64_t scansets[6] = {scan_a.scanset_id, scan_a.scanset_id, scan_a.scanset_id,
|
|
scan_b.scanset_id, scan_b.scanset_id, scan_b.scanset_id};
|
|
const unsigned int families[6] = {0, 0, 0, 1, 2, 0};
|
|
for (size_t i = 0; i < 6; ++i) {
|
|
CHECK(lardon3d_image_catalog_import_file(&state, scansets[i], source_paths[i], 0, &images[i],
|
|
&asset) == LARDON3D_IMAGE_CATALOG_IMPORTED);
|
|
}
|
|
Lardon3DProjectDbFeatureSet feature_sets[6];
|
|
for (size_t i = 0; i < 6; ++i) {
|
|
CHECK(publish_features(&state, images[i].image_id, families[i], &feature_sets[i]));
|
|
}
|
|
Lardon3DVisualIndexConfiguration configuration = {1, 16, 4096};
|
|
uint64_t index_id = 0;
|
|
CHECK(lardon3d_visual_index_create(database, &feature_sets[0], &configuration, &index_id) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
index_id > 0);
|
|
uint64_t last = 0;
|
|
size_t indexed = 0;
|
|
CHECK(lardon3d_visual_index_update_once(root, database, index_id, 0, 0, 16, &last, &indexed) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
indexed == 6);
|
|
|
|
Lardon3DVisualIndexQueryOptions options = {
|
|
.top_k = 2,
|
|
.minimum_evidence_count = 1,
|
|
.scanset_filter = LARDON3D_VISUAL_INDEX_ANY_SCANSET,
|
|
};
|
|
Lardon3DCandidatePairGenStats stats;
|
|
Lardon3DProjectDbCandidatePair pairs[LARDON3D_PROJECT_DB_CANDIDATE_PAIR_PAGE_MAX];
|
|
size_t pair_count = 0;
|
|
|
|
/* Top-K borné : seuls les 2 meilleurs candidats sont persistés. */
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 2 && stats.generated_count == 2 && stats.skipped_count == 0);
|
|
CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pairs, 256, &pair_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
pair_count == 2);
|
|
CHECK(pair_exists(pairs, pair_count, images[0].image_id, images[1].image_id) &&
|
|
pair_exists(pairs, pair_count, images[0].image_id, images[2].image_id) &&
|
|
!pair_exists(pairs, pair_count, images[0].image_id, images[5].image_id));
|
|
|
|
/* Moins de K candidats : 4 candidats retournés, 2 nouveaux, 2 déjà présents. */
|
|
options.top_k = 10;
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 4 && stats.generated_count == 2 && stats.skipped_count == 2);
|
|
CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pairs, 256, &pair_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
pair_count == 4);
|
|
CHECK(pair_exists(pairs, pair_count, images[0].image_id, images[5].image_id) &&
|
|
pair_exists(pairs, pair_count, images[0].image_id, images[3].image_id));
|
|
|
|
/* Canonicalisation A,B / B,A : génération depuis l'autre extrémité. */
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[1].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 4 && stats.generated_count == 3 && stats.skipped_count == 1);
|
|
Lardon3DProjectDbCandidatePair found;
|
|
CHECK(lardon3d_project_db_find_candidate_pair(database, images[1].image_id, images[0].image_id,
|
|
&found) == LARDON3D_PROJECT_DB_OK &&
|
|
found.image_id_a == images[0].image_id && found.image_id_b == images[1].image_id);
|
|
|
|
/* Génération répétée idempotente : aucune nouvelle paire. */
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 4 && stats.generated_count == 0 && stats.skipped_count == 4);
|
|
|
|
/* Absence de doublons et exclusion self-pair. */
|
|
CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pairs, 256, &pair_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
pair_count == 7);
|
|
for (size_t i = 0; i < pair_count; ++i) {
|
|
CHECK(pairs[i].image_id_a < pairs[i].image_id_b);
|
|
for (size_t j = i + 1; j < pair_count; ++j) {
|
|
CHECK(pairs[i].image_id_a != pairs[j].image_id_a ||
|
|
pairs[i].image_id_b != pairs[j].image_id_b);
|
|
}
|
|
}
|
|
|
|
/* Plusieurs images partageant des candidats. */
|
|
CHECK(pair_exists(pairs, pair_count, images[1].image_id, images[2].image_id) &&
|
|
pair_exists(pairs, pair_count, images[1].image_id, images[5].image_id) &&
|
|
pair_exists(pairs, pair_count, images[1].image_id, images[3].image_id));
|
|
|
|
/* Image sans candidat : famille 2 sans correspondance. */
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[4].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 0 && stats.generated_count == 0 && stats.skipped_count == 0);
|
|
|
|
/* Index vide : aucun segment, aucun candidat. */
|
|
Lardon3DVisualIndexConfiguration empty_configuration = {1, 15, 4096};
|
|
uint64_t empty_index = 0;
|
|
CHECK(lardon3d_visual_index_create(database, &feature_sets[0], &empty_configuration,
|
|
&empty_index) == LARDON3D_VISUAL_INDEX_OK &&
|
|
empty_index != index_id);
|
|
Lardon3DVisualIndexResult empty_result = lardon3d_candidate_pair_generate(
|
|
root, database, empty_index, feature_sets[0].feature_set_id, &options, &stats);
|
|
CHECK(empty_result == LARDON3D_VISUAL_INDEX_OK);
|
|
CHECK(stats.queried_count == 0 && stats.generated_count == 0 && stats.skipped_count == 0);
|
|
|
|
/* Ordre et scores déterministes du Visual Index. */
|
|
options.top_k = 4;
|
|
Lardon3DVisualIndexCandidate candidates[LARDON3D_VISUAL_INDEX_TOP_K_MAX];
|
|
size_t count_a = 0;
|
|
size_t count_b = 0;
|
|
CHECK(lardon3d_visual_index_query(root, database, index_id, feature_sets[0].feature_set_id,
|
|
&options, candidates, 4, &count_a) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
count_a == 4);
|
|
Lardon3DVisualIndexCandidate first[4];
|
|
memcpy(first, candidates, count_a * sizeof(*first));
|
|
CHECK(lardon3d_visual_index_query(root, database, index_id, feature_sets[0].feature_set_id,
|
|
&options, candidates, 4, &count_b) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
count_b == count_a);
|
|
for (size_t i = 0; i < count_a; ++i) {
|
|
CHECK(candidates[i].feature_set_id == first[i].feature_set_id &&
|
|
candidates[i].image_id == first[i].image_id &&
|
|
candidates[i].score == first[i].score &&
|
|
candidates[i].evidence_count == first[i].evidence_count);
|
|
}
|
|
|
|
/* Fingerprint déterministe. */
|
|
unsigned char fp1[32], fp2[32];
|
|
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
|
&options, fp1);
|
|
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
|
&options, fp2);
|
|
CHECK(memcmp(fp1, fp2, 32) == 0);
|
|
options.top_k = 8;
|
|
lardon3d_candidate_pair_generation_fingerprint(index_id, feature_sets[0].feature_set_id,
|
|
&options, fp2);
|
|
CHECK(memcmp(fp1, fp2, 32) != 0);
|
|
options.top_k = 4;
|
|
|
|
/* Erreurs d'arguments. */
|
|
CHECK(lardon3d_candidate_pair_generate(NULL, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_candidate_pair_generate(root, NULL, index_id, feature_sets[0].feature_set_id,
|
|
&options, &stats) == LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, 0, feature_sets[0].feature_set_id,
|
|
&options, &stats) == LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id, 0, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id, feature_sets[0].feature_set_id,
|
|
NULL, &stats) == LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id, feature_sets[0].feature_set_id,
|
|
&options, NULL) == LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
options.top_k = 0;
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
options.top_k = LARDON3D_VISUAL_INDEX_TOP_K_MAX + 1;
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id,
|
|
feature_sets[0].feature_set_id, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_INVALID_ARGUMENT);
|
|
options.top_k = 4;
|
|
CHECK(lardon3d_candidate_pair_generate(root, database, index_id, 999999, &options, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_NOT_FOUND);
|
|
|
|
/* A staged duplicate retains the established find-before-create semantics:
|
|
* the first proposal creates and the second is accounted as skipped. */
|
|
uint64_t duplicate_a = images[0].image_id < images[4].image_id
|
|
? images[0].image_id : images[4].image_id;
|
|
uint64_t duplicate_b = images[0].image_id < images[4].image_id
|
|
? images[4].image_id : images[0].image_id;
|
|
Lardon3DCandidatePairComputation duplicate = {
|
|
.source_feature_set_id = feature_sets[0].feature_set_id,
|
|
.queried_count = 2,
|
|
.proposal_count = 2,
|
|
.proposals = {{duplicate_a, duplicate_b}, {duplicate_a, duplicate_b}},
|
|
};
|
|
CHECK(lardon3d_candidate_pair_publish(database, &duplicate, &stats) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
stats.queried_count == 2 && stats.generated_count == 1 &&
|
|
stats.skipped_count == 1);
|
|
|
|
/* Génération batch : tous les FeatureSets. */
|
|
Lardon3DCandidatePairGenStats batch_stats;
|
|
uint64_t last_feature_set_id = 0;
|
|
options.top_k = 4;
|
|
CHECK(lardon3d_candidate_pair_generate_batch(root, database, index_id, 0, &options,
|
|
&batch_stats, &last_feature_set_id) ==
|
|
LARDON3D_VISUAL_INDEX_OK &&
|
|
batch_stats.queried_count > 0 && batch_stats.generated_count > 0);
|
|
CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pairs, 256, &pair_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
pair_count >= 7);
|
|
size_t batch_pair_count = pair_count;
|
|
|
|
/* Persistance puis réouverture de la base. */
|
|
lardon3d_project_db_close(database);
|
|
database = NULL;
|
|
state.project_db = NULL;
|
|
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
|
state.project_db = database;
|
|
CHECK(lardon3d_project_db_list_candidate_pairs(database, 0, pairs, 256, &pair_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
pair_count == batch_pair_count);
|
|
CHECK(lardon3d_project_db_find_candidate_pair(database, images[1].image_id, images[0].image_id,
|
|
&found) == LARDON3D_PROJECT_DB_OK &&
|
|
found.image_id_a == images[0].image_id && found.image_id_b == images[1].image_id);
|
|
lardon3d_project_db_close(database);
|
|
|
|
/* Membership progress is a row rank, not arithmetic on sparse IDs. This
|
|
* fixture intentionally retains only the first and last indexed source. */
|
|
CHECK(retain_sparse_memberships(database_path, index_id,
|
|
feature_sets[0].feature_set_id,
|
|
feature_sets[5].feature_set_id) &&
|
|
lardon3d_project_db_open(database_path, &database, error) ==
|
|
LARDON3D_PROJECT_DB_OK);
|
|
uint64_t membership_ids[4] = {0};
|
|
size_t membership_count = 0;
|
|
uint64_t completed = 0, total = 0;
|
|
CHECK(lardon3d_project_db_list_visual_index_memberships(
|
|
database, index_id, 0, membership_ids, 4, &membership_count) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
membership_count == 2 &&
|
|
membership_ids[0] == feature_sets[0].feature_set_id &&
|
|
membership_ids[1] == feature_sets[5].feature_set_id);
|
|
CHECK(lardon3d_project_db_visual_index_membership_progress(
|
|
database, index_id, feature_sets[0].feature_set_id, &completed, &total) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
completed == 1 && total == 2);
|
|
CHECK(lardon3d_project_db_visual_index_membership_progress(
|
|
database, index_id, feature_sets[5].feature_set_id, &completed, &total) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
completed == 2 && total == 2);
|
|
lardon3d_project_db_close(database);
|
|
CHECK(remove_tree(root));
|
|
return true;
|
|
}
|
|
|
|
int main(void) { return run_test() ? 0 : 1; }
|