349 lines
16 KiB
C++
349 lines
16 KiB
C++
#include "track_builder_internal.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <new>
|
|
#include <openssl/evp.h>
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
namespace tb = lardon3d::track_builder_internal;
|
|
using Observation = Lardon3DTrackBuilderObservation;
|
|
|
|
static_assert(sizeof(tb::Node) == 16);
|
|
static_assert(sizeof(tb::Edge) == 8);
|
|
static_assert(sizeof(tb::IdentitySlot) == 16);
|
|
static_assert(sizeof(tb::Membership) == 16);
|
|
|
|
namespace {
|
|
bool meta_equal(const tb::FeatureMetadata &a, const tb::FeatureMetadata &b,
|
|
bool include_identity) {
|
|
return (!include_identity || (a.feature_set_id == b.feature_set_id &&
|
|
a.image_id == b.image_id)) &&
|
|
a.extractor_version == b.extractor_version &&
|
|
a.descriptor_type == b.descriptor_type &&
|
|
a.descriptor_dimension == b.descriptor_dimension &&
|
|
std::strncmp(a.extractor_kind, b.extractor_kind,
|
|
LARDON3D_TRACK_BUILDER_KIND_CAPACITY) == 0 &&
|
|
std::memcmp(a.parameter_fingerprint, b.parameter_fingerprint, 32) == 0;
|
|
}
|
|
bool meta_valid(const tb::FeatureMetadata &m) {
|
|
return m.feature_set_id != 0 && m.image_id != 0 &&
|
|
std::memchr(m.extractor_kind, '\0', sizeof(m.extractor_kind));
|
|
}
|
|
uint64_t hash_key(uint64_t set, uint32_t index) {
|
|
uint64_t v = set ^ (static_cast<uint64_t>(index) + 0x9e3779b97f4a7c15ULL +
|
|
(set << 6U) + (set >> 2U));
|
|
v ^= v >> 30U; v *= 0xbf58476d1ce4e5b9ULL;
|
|
v ^= v >> 27U; v *= 0x94d049bb133111ebULL;
|
|
return v ^ (v >> 31U);
|
|
}
|
|
size_t table_capacity(uint64_t nodes) {
|
|
if (nodes == 0) return 0;
|
|
if (nodes > std::numeric_limits<size_t>::max() / 2U) throw std::bad_alloc();
|
|
const size_t required = static_cast<size_t>(nodes + nodes / 2U + 1U);
|
|
size_t capacity = 8;
|
|
while (capacity < required) {
|
|
if (capacity > std::numeric_limits<size_t>::max() / 2U) throw std::bad_alloc();
|
|
capacity *= 2U;
|
|
}
|
|
return capacity;
|
|
}
|
|
struct Dsu {
|
|
std::vector<uint32_t> parent;
|
|
std::vector<unsigned char> rank;
|
|
explicit Dsu(size_t n) : parent(n), rank(n, 0) {
|
|
for (size_t i = 0; i < n; ++i) parent[i] = static_cast<uint32_t>(i);
|
|
}
|
|
uint32_t root(uint32_t v) {
|
|
while (parent[v] != v) { parent[v] = parent[parent[v]]; v = parent[v]; }
|
|
return v;
|
|
}
|
|
void unite(uint32_t a, uint32_t b) {
|
|
a = root(a); b = root(b); if (a == b) return;
|
|
if (rank[a] < rank[b]) std::swap(a, b);
|
|
parent[b] = a; if (rank[a] == rank[b]) ++rank[a];
|
|
}
|
|
};
|
|
tb::FeatureMetadata project(const Observation &o) {
|
|
tb::FeatureMetadata m{};
|
|
m.feature_set_id = o.feature_set_id; m.image_id = o.image_id;
|
|
std::memcpy(m.extractor_kind, o.extractor_kind, sizeof(m.extractor_kind));
|
|
m.extractor_version = o.extractor_version;
|
|
std::memcpy(m.parameter_fingerprint, o.parameter_fingerprint, 32);
|
|
m.descriptor_type = o.descriptor_type; m.descriptor_dimension = o.descriptor_dimension;
|
|
return m;
|
|
}
|
|
bool digest(const unsigned char *input, size_t size, unsigned char output[32]) {
|
|
unsigned int length = 0;
|
|
return EVP_Digest(input, size, output, &length, EVP_sha256(), nullptr) == 1 &&
|
|
length == 32;
|
|
}
|
|
} // namespace
|
|
|
|
tb::CompactGraph::CompactGraph(uint64_t edge_hint) {
|
|
if (edge_hint > UINT32_MAX / 2U || edge_hint > std::numeric_limits<size_t>::max())
|
|
throw std::bad_alloc();
|
|
const uint64_t node_hint = edge_hint * 2U;
|
|
nodes_.reserve(static_cast<size_t>(node_hint));
|
|
edges_.reserve(static_cast<size_t>(edge_hint));
|
|
identity_.resize(table_capacity(node_hint));
|
|
}
|
|
|
|
uint32_t tb::CompactGraph::register_feature(const FeatureMetadata &m) {
|
|
if (!meta_valid(m)) throw std::invalid_argument("invalid feature metadata");
|
|
/* CONTRACT: CompactGraph validates metadata inside each connected component;
|
|
* it must not globally bind a feature_set_id. The public C ABI historically
|
|
* permits unrelated observations from that set to carry divergent metadata,
|
|
* while the Project adapter enforces immutable Feature Set metadata in its
|
|
* own cache before registering exactly one entry per set. */
|
|
if (metadata_.size() == UINT32_MAX) throw std::bad_alloc();
|
|
metadata_.push_back(m);
|
|
return static_cast<uint32_t>(metadata_.size() - 1U);
|
|
}
|
|
|
|
void tb::CompactGraph::insert_identity(const Node &node, uint32_t index) {
|
|
const size_t mask = identity_.size() - 1U;
|
|
size_t slot = static_cast<size_t>(hash_key(node.feature_set_id, node.feature_index)) & mask;
|
|
uint64_t probes = 1;
|
|
while (identity_[slot].node_plus_one) {
|
|
slot = (slot + 1U) & mask;
|
|
++probes;
|
|
}
|
|
identity_[slot] = {node.feature_set_id, node.feature_index, index + 1U};
|
|
++identity_size_;
|
|
++profile_.identity_inserts;
|
|
profile_.identity_probes += probes;
|
|
profile_.identity_max_probe = std::max(profile_.identity_max_probe, probes);
|
|
}
|
|
|
|
uint32_t tb::CompactGraph::resolve_node(uint32_t metadata_index,
|
|
uint32_t feature_index) {
|
|
if (metadata_index >= metadata_.size() || identity_.empty())
|
|
throw std::invalid_argument("invalid metadata index");
|
|
const uint64_t set = metadata_[metadata_index].feature_set_id;
|
|
const size_t mask = identity_.size() - 1U;
|
|
size_t slot = static_cast<size_t>(hash_key(set, feature_index)) & mask;
|
|
uint64_t probes = 1;
|
|
while (identity_[slot].node_plus_one) {
|
|
const IdentitySlot &found = identity_[slot];
|
|
if (found.feature_set_id == set && found.feature_index == feature_index) {
|
|
const uint32_t index = found.node_plus_one - 1U;
|
|
if (nodes_[index].metadata_index != metadata_index)
|
|
throw std::invalid_argument("contradictory observation");
|
|
++profile_.identity_lookups;
|
|
profile_.identity_probes += probes;
|
|
profile_.identity_max_probe = std::max(profile_.identity_max_probe, probes);
|
|
return index;
|
|
}
|
|
slot = (slot + 1U) & mask;
|
|
++probes;
|
|
}
|
|
if (nodes_.size() == UINT32_MAX || identity_size_ == identity_.size())
|
|
throw std::bad_alloc();
|
|
nodes_.push_back({set, feature_index, metadata_index});
|
|
const uint32_t index = static_cast<uint32_t>(nodes_.size() - 1U);
|
|
insert_identity(nodes_.back(), index);
|
|
++profile_.identity_lookups;
|
|
profile_.identity_probes += probes;
|
|
profile_.identity_max_probe = std::max(profile_.identity_max_probe, probes);
|
|
return index;
|
|
}
|
|
|
|
bool tb::CompactGraph::add_edge(uint32_t am, uint32_t ai, uint32_t bm,
|
|
uint32_t bi) {
|
|
const uint32_t a = resolve_node(am, ai), b = resolve_node(bm, bi);
|
|
if (a == b) return false;
|
|
edges_.push_back(a < b ? Edge{a, b} : Edge{b, a});
|
|
++raw_edge_count_;
|
|
return true;
|
|
}
|
|
|
|
const tb::FeatureMetadata &tb::CompactGraph::metadata(uint32_t i) const {
|
|
return metadata_.at(i);
|
|
}
|
|
uint64_t tb::CompactGraph::node_image(uint32_t i) const {
|
|
return metadata_.at(nodes_.at(i).metadata_index).image_id;
|
|
}
|
|
|
|
Lardon3DTrackBuilderResult tb::CompactGraph::build(Output *output) {
|
|
if (!output) return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT;
|
|
output->memberships.clear(); output->tracks.clear();
|
|
try {
|
|
std::sort(edges_.begin(), edges_.end(), [](const Edge &a, const Edge &b) {
|
|
return a.low < b.low || (a.low == b.low && a.high < b.high);
|
|
});
|
|
edges_.erase(std::unique(edges_.begin(), edges_.end(), [](const Edge &a, const Edge &b) {
|
|
return a.low == b.low && a.high == b.high;
|
|
}), edges_.end());
|
|
Dsu dsu(nodes_.size());
|
|
for (const Edge &edge : edges_) dsu.unite(edge.low, edge.high);
|
|
|
|
/* INVARIANT: one flat grouping owns every node exactly once. DSU roots
|
|
* delimit complete components but never determine persisted order. */
|
|
std::vector<uint32_t> grouped(nodes_.size());
|
|
for (size_t i = 0; i < grouped.size(); ++i) grouped[i] = static_cast<uint32_t>(i);
|
|
std::sort(grouped.begin(), grouped.end(), [&](uint32_t a, uint32_t b) {
|
|
const uint32_t ra = dsu.root(a), rb = dsu.root(b);
|
|
if (ra != rb) return ra < rb;
|
|
const Node &x = nodes_[a], &y = nodes_[b];
|
|
return x.feature_set_id < y.feature_set_id ||
|
|
(x.feature_set_id == y.feature_set_id && x.feature_index < y.feature_index);
|
|
});
|
|
std::vector<TrackRange> accepted;
|
|
for (size_t begin = 0; begin < grouped.size();) {
|
|
size_t end = begin + 1U; const uint32_t root = dsu.root(grouped[begin]);
|
|
while (end < grouped.size() && dsu.root(grouped[end]) == root) ++end;
|
|
bool valid = end - begin >= 2U;
|
|
const FeatureMetadata &first = metadata_[nodes_[grouped[begin]].metadata_index];
|
|
std::vector<uint64_t> images; images.reserve(end - begin);
|
|
for (size_t i = begin; i < end; ++i) {
|
|
const FeatureMetadata &m = metadata_[nodes_[grouped[i]].metadata_index];
|
|
valid = valid && meta_equal(first, m, false); images.push_back(m.image_id);
|
|
}
|
|
std::sort(images.begin(), images.end());
|
|
valid = valid && std::adjacent_find(images.begin(), images.end()) == images.end();
|
|
if (valid) accepted.push_back({begin, end - begin});
|
|
begin = end;
|
|
}
|
|
auto less = [&](const TrackRange &a, const TrackRange &b) {
|
|
const size_t count = std::min(a.count, b.count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
const Node &x = nodes_[grouped[a.begin + i]], &y = nodes_[grouped[b.begin + i]];
|
|
if (x.feature_set_id != y.feature_set_id) return x.feature_set_id < y.feature_set_id;
|
|
if (x.feature_index != y.feature_index) return x.feature_index < y.feature_index;
|
|
}
|
|
return a.count < b.count;
|
|
};
|
|
std::sort(accepted.begin(), accepted.end(), less);
|
|
size_t count = 0; for (const TrackRange &range : accepted) count += range.count;
|
|
output->memberships.reserve(count); output->tracks.reserve(accepted.size());
|
|
for (const TrackRange &range : accepted) {
|
|
const size_t begin = output->memberships.size();
|
|
for (size_t i = 0; i < range.count; ++i) {
|
|
const uint32_t ni = grouped[range.begin + i]; const Node &n = nodes_[ni];
|
|
output->memberships.push_back({n.feature_set_id, n.feature_index, ni});
|
|
}
|
|
output->tracks.push_back({begin, range.count});
|
|
}
|
|
return LARDON3D_TRACK_BUILDER_OK;
|
|
} catch (const std::bad_alloc &) {
|
|
output->memberships.clear(); output->tracks.clear();
|
|
return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY;
|
|
}
|
|
}
|
|
|
|
extern "C" bool lardon3d_track_builder_fingerprint_bytes(unsigned char bytes[48]) {
|
|
if (!bytes) return false;
|
|
std::memset(bytes, 0, 48);
|
|
std::memcpy(bytes, "L3DTBFP1", 8);
|
|
const uint32_t fields[] = {1,1,1,1,1,1,1,1,2,0};
|
|
for (size_t f = 0; f < 10; ++f) for (size_t b = 0; b < 4; ++b)
|
|
bytes[8 + f * 4 + b] = static_cast<unsigned char>(fields[f] >> (b * 8));
|
|
return true;
|
|
}
|
|
extern "C" bool lardon3d_track_builder_fingerprint(unsigned char output[32]) {
|
|
unsigned char bytes[48]; return output && lardon3d_track_builder_fingerprint_bytes(bytes) &&
|
|
digest(bytes, sizeof(bytes), output);
|
|
}
|
|
extern "C" void lardon3d_track_builder_result_free(Lardon3DTrackBuilderResultSet *result) {
|
|
if (!result) return;
|
|
if (result->tracks) {
|
|
for (size_t i = 0; i < result->track_count; ++i)
|
|
delete[] result->tracks[i].observations;
|
|
delete[] result->tracks;
|
|
}
|
|
result->tracks = nullptr; result->track_count = 0;
|
|
}
|
|
|
|
extern "C" Lardon3DTrackBuilderResult lardon3d_track_builder_build(
|
|
const Observation *observations, size_t observation_count,
|
|
const Lardon3DTrackBuilderEdge *edges, size_t edge_count,
|
|
Lardon3DTrackBuilderResultSet *result) {
|
|
if (!result || (observation_count && !observations) || (edge_count && !edges))
|
|
return LARDON3D_TRACK_BUILDER_INVALID_ARGUMENT;
|
|
lardon3d_track_builder_result_free(result);
|
|
try {
|
|
tb::CompactGraph graph(edge_count);
|
|
struct PublicIdentity {
|
|
uint64_t feature_set_id;
|
|
uint32_t feature_index;
|
|
tb::FeatureMetadata metadata;
|
|
uint32_t metadata_index = 0;
|
|
bool registered = false;
|
|
};
|
|
std::vector<PublicIdentity> input_identities;
|
|
input_identities.reserve(observation_count);
|
|
for (size_t i = 0; i < observation_count; ++i) {
|
|
const tb::FeatureMetadata metadata = project(observations[i]);
|
|
if (!meta_valid(metadata)) return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
input_identities.push_back({observations[i].feature_set_id,
|
|
observations[i].feature_index, metadata});
|
|
}
|
|
std::sort(input_identities.begin(), input_identities.end(),
|
|
[](const PublicIdentity &a, const PublicIdentity &b) {
|
|
return std::make_pair(a.feature_set_id, a.feature_index) <
|
|
std::make_pair(b.feature_set_id, b.feature_index);
|
|
});
|
|
for (size_t i = 1; i < input_identities.size(); ++i) {
|
|
if (input_identities[i - 1].feature_set_id == input_identities[i].feature_set_id &&
|
|
input_identities[i - 1].feature_index == input_identities[i].feature_index &&
|
|
!meta_equal(input_identities[i - 1].metadata, input_identities[i].metadata, true))
|
|
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
}
|
|
auto find_identity = [&](uint64_t set, uint32_t index) -> PublicIdentity * {
|
|
auto found = std::lower_bound(
|
|
input_identities.begin(), input_identities.end(), std::make_pair(set, index),
|
|
[](const PublicIdentity &value, const std::pair<uint64_t, uint32_t> &key) {
|
|
return std::make_pair(value.feature_set_id, value.feature_index) < key;
|
|
});
|
|
return found != input_identities.end() && found->feature_set_id == set &&
|
|
found->feature_index == index ? &*found : nullptr;
|
|
};
|
|
for (size_t i = 0; i < edge_count; ++i) {
|
|
if (!edges[i].first || !edges[i].second)
|
|
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
PublicIdentity *first = find_identity(edges[i].first->feature_set_id,
|
|
edges[i].first->feature_index);
|
|
PublicIdentity *second = find_identity(edges[i].second->feature_set_id,
|
|
edges[i].second->feature_index);
|
|
if (!first || !second || !meta_equal(first->metadata, project(*edges[i].first), true) ||
|
|
!meta_equal(second->metadata, project(*edges[i].second), true))
|
|
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
/* WHY: only observations participating in an edge enter CompactGraph.
|
|
* Isolated public inputs cannot invalidate an unrelated component. */
|
|
if (!first->registered) {
|
|
first->metadata_index = graph.register_feature(first->metadata);
|
|
first->registered = true;
|
|
}
|
|
if (!second->registered) {
|
|
second->metadata_index = graph.register_feature(second->metadata);
|
|
second->registered = true;
|
|
}
|
|
if (!graph.add_edge(first->metadata_index, first->feature_index,
|
|
second->metadata_index, second->feature_index))
|
|
return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
}
|
|
tb::Output output; const auto status = graph.build(&output); if (status) return status;
|
|
if (!output.tracks.empty()) {
|
|
result->tracks = new Lardon3DTrackBuilderTrack[output.tracks.size()]();
|
|
result->track_count = output.tracks.size();
|
|
for (size_t i = 0; i < output.tracks.size(); ++i) {
|
|
const tb::TrackRange &range = output.tracks[i];
|
|
result->tracks[i].observation_count = range.count;
|
|
result->tracks[i].observations = new Lardon3DTrackBuilderTrackObservation[range.count];
|
|
for (size_t j = 0; j < range.count; ++j) {
|
|
const tb::Membership &m = output.memberships[range.begin + j];
|
|
result->tracks[i].observations[j] =
|
|
{m.feature_set_id, m.feature_index, graph.node_image(m.node_index)};
|
|
}
|
|
}
|
|
}
|
|
return LARDON3D_TRACK_BUILDER_OK;
|
|
} catch (const std::invalid_argument &) {
|
|
lardon3d_track_builder_result_free(result); return LARDON3D_TRACK_BUILDER_CORRUPT_INPUT;
|
|
} catch (...) {
|
|
lardon3d_track_builder_result_free(result); return LARDON3D_TRACK_BUILDER_OUT_OF_MEMORY;
|
|
}
|
|
}
|