773 lines
30 KiB
C++
773 lines
30 KiB
C++
#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 accept_orb_top2(const Lardon3DOrbTop2 &top2, uint32_t query_index,
|
|
float threshold, MatchEntry *entry) {
|
|
if (!entry || top2.neighbor_count == 0 || top2.neighbor_count > 2 ||
|
|
!std::isfinite(threshold) || threshold <= 0.0F || threshold >= 1.0F) {
|
|
return false;
|
|
}
|
|
if (top2.neighbor_count == 2 &&
|
|
(top2.second_distance == 0 ||
|
|
!(static_cast<float>(top2.best_distance) <
|
|
threshold * static_cast<float>(top2.second_distance)))) {
|
|
return false;
|
|
}
|
|
*entry = {
|
|
static_cast<int>(query_index),
|
|
static_cast<int>(top2.best_index),
|
|
static_cast<float>(top2.best_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,
|
|
Lardon3DOrbVulkanBackend *backend,
|
|
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);
|
|
bool completed = false;
|
|
if (backend && lardon3d_orb_vulkan_should_use(
|
|
feature_set_a->feature_count,
|
|
feature_set_b->feature_count)) {
|
|
std::vector<Lardon3DOrbTop2> top2(feature_set_a->feature_count);
|
|
phase_start = std::chrono::steady_clock::now();
|
|
Lardon3DOrbVulkanResult vulkan_result = lardon3d_orb_vulkan_top2(
|
|
backend, desc_a.data(), feature_set_a->feature_count,
|
|
desc_b.data(), feature_set_b->feature_count, top2.data(), top2.size());
|
|
stats->knn_ns = elapsed_ns(phase_start);
|
|
if (vulkan_result == LARDON3D_ORB_VULKAN_OK) {
|
|
stats->used_vulkan = true;
|
|
stats->knn_query_count = feature_set_a->feature_count;
|
|
phase_start = std::chrono::steady_clock::now();
|
|
for (uint32_t index = 0; index < feature_set_a->feature_count; ++index) {
|
|
MatchEntry entry;
|
|
if (accept_orb_top2(top2[index], index, params->ratio_threshold,
|
|
&entry)) {
|
|
filtered_matches.push_back(entry);
|
|
}
|
|
}
|
|
stats->filter_ns = elapsed_ns(phase_start);
|
|
completed = true;
|
|
} else {
|
|
stats->vulkan_fallback = true;
|
|
}
|
|
}
|
|
if (!completed) {
|
|
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 (const auto &knn : matches) {
|
|
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) {
|
|
return lardon3d_matcher_run_with_backend(
|
|
project_path, feature_set_a, feature_set_b, params, match_file_path, nullptr, stats);
|
|
}
|
|
|
|
extern "C" Lardon3DMatcherResult lardon3d_matcher_run_with_backend(
|
|
const char *project_path,
|
|
const Lardon3DProjectDbFeatureSet *feature_set_a,
|
|
const Lardon3DProjectDbFeatureSet *feature_set_b,
|
|
const Lardon3DMatcherParams *params,
|
|
const char *match_file_path,
|
|
Lardon3DOrbVulkanBackend *backend,
|
|
Lardon3DMatcherStats *stats) {
|
|
try {
|
|
return matcher_run_impl(project_path, feature_set_a, feature_set_b, params,
|
|
match_file_path, backend, 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) {
|
|
return lardon3d_matcher_match_and_publish_with_backend(
|
|
project_path, database, pair, feature_set_a, feature_set_b, params, nullptr,
|
|
result, profile);
|
|
}
|
|
|
|
extern "C" Lardon3DMatcherResult lardon3d_matcher_match_and_publish_with_backend(
|
|
const char *project_path,
|
|
Lardon3DProjectDb *database,
|
|
const Lardon3DProjectDbCandidatePair *pair,
|
|
const Lardon3DProjectDbFeatureSet *feature_set_a,
|
|
const Lardon3DProjectDbFeatureSet *feature_set_b,
|
|
const Lardon3DMatcherParams *params,
|
|
Lardon3DOrbVulkanBackend *backend,
|
|
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_with_backend(
|
|
project_path, feature_set_a, feature_set_b, params, tmp_path, backend, &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_with_backend(
|
|
project_path, database, pair, feature_set_a, feature_set_b, params, backend,
|
|
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_with_backend(
|
|
project_path, database, pair, feature_set_a, feature_set_b, params, backend,
|
|
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);
|
|
}
|