1318 lines
59 KiB
C++
1318 lines
59 KiB
C++
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <openssl/evp.h>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
extern "C" {
|
|
#include <lardon3d/acquisition_campaign.h>
|
|
#include <lardon3d/acquisition_campaign_task.h>
|
|
#include <lardon3d/candidate_pair_task.h>
|
|
#include <lardon3d/feature_extractor.h>
|
|
#include <lardon3d/feature_task.h>
|
|
#include <lardon3d/geometric_verifier_task.h>
|
|
#include <lardon3d/hardware_profile.h>
|
|
#include <lardon3d/matcher_task.h>
|
|
#include <lardon3d/photo_quality_task.h>
|
|
#include <lardon3d/project.h>
|
|
#include <lardon3d/raw_development_task.h>
|
|
#include <lardon3d/task_queue.h>
|
|
#include <lardon3d/track_builder.h>
|
|
#include <lardon3d/track_builder_task.h>
|
|
#include <lardon3d/visual_index.h>
|
|
#include <lardon3d/visual_index_task.h>
|
|
}
|
|
|
|
namespace {
|
|
|
|
enum class Mode { kA6000, kS21 };
|
|
enum class RestartBoundary { kNone, kRepresentations, kFeatures, kGeometry };
|
|
|
|
struct Options {
|
|
Mode mode{};
|
|
bool has_mode{};
|
|
bool resume_geometry_existing{};
|
|
bool resume_pre_gv_existing{};
|
|
bool resume_candidate_existing{};
|
|
unsigned int cpu_budget{};
|
|
std::filesystem::path project_dir;
|
|
std::vector<std::filesystem::path> roots;
|
|
size_t limit{LARDON3D_VISUAL_INDEX_CANDIDATE_MAX};
|
|
RestartBoundary restart{RestartBoundary::kNone};
|
|
};
|
|
|
|
struct Campaign {
|
|
std::vector<Lardon3DAcquisitionCampaignSource> sources;
|
|
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
|
|
};
|
|
|
|
struct Runtime {
|
|
Lardon3DAppState state{};
|
|
std::string project_path;
|
|
std::string database_path;
|
|
unsigned int cpu_budget{};
|
|
};
|
|
|
|
struct Evidence {
|
|
size_t selected{};
|
|
size_t features{};
|
|
size_t pairs{};
|
|
size_t matches{};
|
|
size_t verified{};
|
|
size_t rejected{};
|
|
size_t tracks{};
|
|
};
|
|
|
|
void usage(const char *program) {
|
|
std::fprintf(
|
|
stderr,
|
|
"Usage: %s --mode a6000|s21 --project-dir ABSOLUTE_EMPTY_DIR "
|
|
"--root ABSOLUTE_DIR [--root ABSOLUTE_DIR ...] [--limit 1..4096] "
|
|
"[--restart-boundary representations|features|geometry]\n"
|
|
" %s --resume-geometry-existing --project-dir "
|
|
"ABSOLUTE_EXISTING_DIR\n"
|
|
" %s --resume-pre-gv-existing --project-dir "
|
|
"ABSOLUTE_EXISTING_DIR\n"
|
|
" %s --resume-candidate-existing --project-dir "
|
|
"ABSOLUTE_EXISTING_DIR [--cpu-budget 1..12]\n",
|
|
program,
|
|
program,
|
|
program,
|
|
program);
|
|
}
|
|
|
|
bool parse_size(const char *text, size_t &value) {
|
|
char *end = nullptr;
|
|
errno = 0;
|
|
const unsigned long long parsed = std::strtoull(text, &end, 10);
|
|
if (errno != 0 || end == text || *end != '\0' || parsed == 0 ||
|
|
parsed > LARDON3D_VISUAL_INDEX_CANDIDATE_MAX)
|
|
return false;
|
|
value = static_cast<size_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
bool parse_cpu_budget(const char *text, unsigned int &value) {
|
|
size_t parsed = 0;
|
|
if (!parse_size(text, parsed) || parsed > 12) return false;
|
|
value = static_cast<unsigned int>(parsed);
|
|
return true;
|
|
}
|
|
|
|
bool parse_options(int argc, char **argv, Options &options) {
|
|
for (int index = 1; index < argc; ++index) {
|
|
const std::string argument(argv[index]);
|
|
if (argument == "--help") {
|
|
usage(argv[0]);
|
|
std::exit(0);
|
|
}
|
|
if (argument == "--resume-geometry-existing") {
|
|
options.resume_geometry_existing = true;
|
|
continue;
|
|
}
|
|
if (argument == "--resume-pre-gv-existing") {
|
|
options.resume_pre_gv_existing = true;
|
|
continue;
|
|
}
|
|
if (argument == "--resume-candidate-existing") {
|
|
options.resume_candidate_existing = true;
|
|
continue;
|
|
}
|
|
if ((argument == "--mode" || argument == "--project-dir" || argument == "--root" ||
|
|
argument == "--limit" || argument == "--restart-boundary" ||
|
|
argument == "--cpu-budget") &&
|
|
index + 1 >= argc)
|
|
return false;
|
|
if (argument == "--mode") {
|
|
const std::string value(argv[++index]);
|
|
if (value == "a6000") options.mode = Mode::kA6000;
|
|
else if (value == "s21") options.mode = Mode::kS21;
|
|
else return false;
|
|
options.has_mode = true;
|
|
} else if (argument == "--project-dir") {
|
|
options.project_dir = argv[++index];
|
|
} else if (argument == "--root") {
|
|
options.roots.emplace_back(argv[++index]);
|
|
} else if (argument == "--limit") {
|
|
if (!parse_size(argv[++index], options.limit)) return false;
|
|
} else if (argument == "--restart-boundary") {
|
|
const std::string value(argv[++index]);
|
|
if (value == "representations") options.restart = RestartBoundary::kRepresentations;
|
|
else if (value == "features") options.restart = RestartBoundary::kFeatures;
|
|
else if (value == "geometry") options.restart = RestartBoundary::kGeometry;
|
|
else return false;
|
|
} else if (argument == "--cpu-budget") {
|
|
if (!parse_cpu_budget(argv[++index], options.cpu_budget)) return false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
const unsigned int resume_mode_count = options.resume_geometry_existing +
|
|
options.resume_pre_gv_existing +
|
|
options.resume_candidate_existing;
|
|
if (resume_mode_count != 0)
|
|
return !options.has_mode && !options.project_dir.empty() && options.roots.empty() &&
|
|
options.restart == RestartBoundary::kNone &&
|
|
resume_mode_count == 1 &&
|
|
(options.resume_candidate_existing || options.cpu_budget == 0);
|
|
if (options.cpu_budget != 0) return false;
|
|
return options.has_mode && !options.project_dir.empty() && !options.roots.empty() &&
|
|
options.roots.size() <= LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS;
|
|
}
|
|
|
|
bool prepare_existing_project(const std::filesystem::path &input, Runtime &runtime) {
|
|
if (!input.is_absolute() || input.lexically_normal() != input) return false;
|
|
std::error_code error;
|
|
if (!std::filesystem::is_directory(input, error) || error ||
|
|
!std::filesystem::is_regular_file(input / "project.lardon3d", error) || error ||
|
|
!std::filesystem::is_directory(input / ".lardon3d" / "checkpoints", error) || error)
|
|
return false;
|
|
runtime.project_path = input.string();
|
|
runtime.database_path = (input / "project.lardon3d").string();
|
|
return runtime.project_path.size() < LARDON3D_APP_STATE_PATH_CAPACITY;
|
|
}
|
|
|
|
bool prepare_empty_project(const std::filesystem::path &input, std::string &path) {
|
|
if (!input.is_absolute() || input.lexically_normal() != input) return false;
|
|
std::error_code error;
|
|
if (std::filesystem::exists(input, error)) {
|
|
if (error || !std::filesystem::is_directory(input, error) || error ||
|
|
!std::filesystem::is_empty(input, error) || error)
|
|
return false;
|
|
} else if (!std::filesystem::create_directories(input, error) || error) {
|
|
return false;
|
|
}
|
|
path = input.string();
|
|
if (path.size() >= LARDON3D_APP_STATE_PATH_CAPACITY) return false;
|
|
return std::filesystem::create_directories(input / ".lardon3d" / "checkpoints", error) &&
|
|
!error;
|
|
}
|
|
|
|
bool canonical_root(const std::filesystem::path &input, Lardon3DAcquisitionCampaignRoot &root) {
|
|
if (!input.is_absolute()) return false;
|
|
std::error_code error;
|
|
const auto canonical = std::filesystem::canonical(input, error);
|
|
if (error || !std::filesystem::is_directory(canonical, error) || error) return false;
|
|
const std::string path = canonical.string();
|
|
if (path.size() >= sizeof(root.path)) return false;
|
|
std::memcpy(root.path, path.c_str(), path.size() + 1u);
|
|
return true;
|
|
}
|
|
|
|
std::string source_stem(const char *path) {
|
|
const char *name = std::strrchr(path, '/');
|
|
name = name == nullptr ? path : name + 1;
|
|
const char *dot = std::strrchr(name, '.');
|
|
return std::string(name, dot == nullptr ? std::strlen(name)
|
|
: static_cast<size_t>(dot - name));
|
|
}
|
|
|
|
bool discover_campaign(const Options &options, Campaign &campaign) {
|
|
std::vector<Lardon3DAcquisitionCampaignRoot> roots(options.roots.size());
|
|
for (size_t index = 0; index < roots.size(); ++index)
|
|
if (!canonical_root(options.roots[index], roots[index])) {
|
|
std::fprintf(stderr, "campaign root %zu is not an absolute readable directory: %s\n",
|
|
index, options.roots[index].c_str());
|
|
return false;
|
|
}
|
|
auto discovery = std::make_unique<Lardon3DAcquisitionCampaignDiscovery>();
|
|
const auto discovery_result =
|
|
lardon3d_acquisition_campaign_discover(roots.data(), roots.size(), discovery.get());
|
|
if (discovery_result != LARDON3D_ACQUISITION_CAMPAIGN_OK) {
|
|
std::fprintf(stderr, "campaign discovery failed with result %d\n",
|
|
static_cast<int>(discovery_result));
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::vector<size_t>> groups;
|
|
if (options.mode == Mode::kS21) {
|
|
for (size_t index = 0; index < discovery->source_count && groups.size() < options.limit;
|
|
++index) {
|
|
if (discovery->sources[index].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG)
|
|
groups.push_back({index});
|
|
}
|
|
} else {
|
|
struct Pair { size_t raw{SIZE_MAX}; size_t jpeg{SIZE_MAX}; };
|
|
std::map<std::string, Pair> pairs;
|
|
for (size_t index = 0; index < discovery->source_count; ++index) {
|
|
Pair &pair = pairs[source_stem(discovery->sources[index].path)];
|
|
if (discovery->sources[index].source_kind == LARDON3D_ACQUISITION_SOURCE_RAW) {
|
|
if (pair.raw != SIZE_MAX) {
|
|
std::fprintf(stderr, "duplicate RAW stem in discovery: %s\n",
|
|
source_stem(discovery->sources[index].path).c_str());
|
|
return false;
|
|
}
|
|
pair.raw = index;
|
|
} else if (discovery->sources[index].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG) {
|
|
if (pair.jpeg != SIZE_MAX) {
|
|
std::fprintf(stderr, "duplicate JPEG stem in discovery: %s\n",
|
|
source_stem(discovery->sources[index].path).c_str());
|
|
return false;
|
|
}
|
|
pair.jpeg = index;
|
|
}
|
|
}
|
|
for (const auto &entry : pairs) {
|
|
if (entry.second.raw == SIZE_MAX || entry.second.jpeg == SIZE_MAX) {
|
|
std::fprintf(stderr, "unpaired A6000 stem: %s (%s missing)\n", entry.first.c_str(),
|
|
entry.second.raw == SIZE_MAX ? "RAW" : "JPEG");
|
|
return false;
|
|
}
|
|
groups.push_back({entry.second.raw, entry.second.jpeg});
|
|
if (groups.size() == options.limit) break;
|
|
}
|
|
}
|
|
if (groups.empty()) {
|
|
std::fprintf(stderr, "campaign discovery found no usable groups among %zu sources\n",
|
|
discovery->source_count);
|
|
return false;
|
|
}
|
|
|
|
std::vector<size_t> selected_indices;
|
|
for (const auto &group : groups)
|
|
selected_indices.insert(selected_indices.end(), group.begin(), group.end());
|
|
std::sort(selected_indices.begin(), selected_indices.end());
|
|
std::vector<size_t> compact_index(discovery->source_count, SIZE_MAX);
|
|
for (const size_t discovery_index : selected_indices) {
|
|
compact_index[discovery_index] = campaign.sources.size();
|
|
campaign.sources.push_back(discovery->sources[discovery_index]);
|
|
}
|
|
|
|
/* Filename stems are used only to form explicit CALLER_EXPLICIT input before
|
|
* Captures exist. The compact source array retains discovery's global canonical
|
|
* path order, while confirmations map each pair independently of that order.
|
|
* Once S3-E returns, every phase below follows durable group, Capture, Asset and
|
|
* image IDs and never treats this convenience key as identity. */
|
|
for (const auto &group : groups) {
|
|
Lardon3DAcquisitionCampaignConfirmation confirmation{};
|
|
confirmation.source_count = group.size();
|
|
for (size_t member = 0; member < group.size(); ++member)
|
|
confirmation.source_indices[member] = compact_index[group[member]];
|
|
campaign.confirmations.push_back(confirmation);
|
|
}
|
|
auto plan = std::make_unique<Lardon3DAcquisitionCampaignPlan>();
|
|
const auto plan_result = lardon3d_acquisition_campaign_plan(
|
|
campaign.sources.data(), campaign.sources.size(), campaign.confirmations.data(),
|
|
campaign.confirmations.size(), plan.get());
|
|
if (plan_result != LARDON3D_ACQUISITION_CAMPAIGN_OK ||
|
|
plan->group_count != campaign.confirmations.size()) {
|
|
std::fprintf(stderr,
|
|
"explicit campaign plan rejected: result=%d sources=%zu confirmations=%zu "
|
|
"groups=%zu\n",
|
|
static_cast<int>(plan_result), campaign.sources.size(),
|
|
campaign.confirmations.size(), plan->group_count);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void stop_runtime(Runtime &runtime) {
|
|
if (runtime.state.task_queue) lardon3d_task_queue_destroy(runtime.state.task_queue);
|
|
if (runtime.state.resource_governor)
|
|
lardon3d_resource_governor_destroy(runtime.state.resource_governor);
|
|
if (runtime.state.project_db) lardon3d_project_db_close(runtime.state.project_db);
|
|
runtime.state.task_queue = nullptr;
|
|
runtime.state.resource_governor = nullptr;
|
|
runtime.state.project_db = nullptr;
|
|
}
|
|
|
|
bool start_runtime(Runtime &runtime) {
|
|
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
|
|
lardon3d_app_state_init(&runtime.state);
|
|
if (lardon3d_project_db_open(runtime.database_path.c_str(), &runtime.state.project_db, error) !=
|
|
LARDON3D_PROJECT_DB_OK) {
|
|
std::fprintf(stderr, "project DB open failed: %s\n", error);
|
|
return false;
|
|
}
|
|
char hardware_error[256]{};
|
|
Lardon3DResourcePolicy policy{};
|
|
if (!lardon3d_hardware_profile_detect(&runtime.state.hardware_profile, hardware_error,
|
|
sizeof(hardware_error)) ||
|
|
!lardon3d_resource_policy_default(&runtime.state.hardware_profile, &policy)) {
|
|
std::fprintf(stderr, "hardware policy failed: %s\n", hardware_error);
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
if (runtime.cpu_budget != 0) {
|
|
if (runtime.cpu_budget > runtime.state.hardware_profile.logical_cpu_count) {
|
|
std::fprintf(stderr, "requested CPU budget %u exceeds logical CPU count %u\n",
|
|
runtime.cpu_budget, runtime.state.hardware_profile.logical_cpu_count);
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
// This runner-only override is applied before Governor creation and is never
|
|
// persisted. The Governor remains the sole admission owner: reserving N
|
|
// logical CPUs constrains the recovered Candidate Task's execution contract.
|
|
policy.system_cpu_reserve = runtime.state.hardware_profile.logical_cpu_count -
|
|
runtime.cpu_budget;
|
|
}
|
|
unsigned int feature_threads = runtime.state.hardware_profile.logical_cpu_count -
|
|
policy.system_cpu_reserve;
|
|
if (feature_threads > 12) feature_threads = 12;
|
|
/* Match the production startup contract: OpenCV's process-wide pool is
|
|
* configured from the same detected profile and Governor policy before any
|
|
* Queue worker exists. The runner-only CPU override therefore constrains
|
|
* both admission and nested feature extraction consistently. */
|
|
if (!lardon3d_feature_opencv_configure_threads(feature_threads)) {
|
|
std::fprintf(stderr, "OpenCV thread configuration failed for %u threads\n",
|
|
feature_threads);
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
runtime.state.resource_governor =
|
|
lardon3d_resource_governor_create(&runtime.state.hardware_profile, &policy);
|
|
runtime.state.task_queue = runtime.state.resource_governor
|
|
? lardon3d_task_queue_create(runtime.state.resource_governor, 2)
|
|
: nullptr;
|
|
if (!runtime.state.task_queue) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
runtime.state.project_loaded = true;
|
|
std::memcpy(runtime.state.project_path, runtime.project_path.c_str(),
|
|
runtime.project_path.size() + 1u);
|
|
/* Task owns execution state, Queue owns bounded dispatch/backpressure, and
|
|
* Governor owns each task's admission reservation. Sequential enqueue/wait
|
|
* below releases that reservation at the task boundary and never reserves an
|
|
* entire campaign or creates a parallel worker/scheduler subsystem. */
|
|
return true;
|
|
}
|
|
|
|
bool wait_completed(Runtime &runtime, uint64_t task_id, const char *phase) {
|
|
for (;;) {
|
|
Lardon3DTaskSnapshot snapshot{};
|
|
if (!lardon3d_task_queue_get(runtime.state.task_queue, task_id, &snapshot)) return false;
|
|
if (snapshot.state == TASK_COMPLETED || snapshot.state == TASK_FAILED ||
|
|
snapshot.state == TASK_CANCELLED) {
|
|
Lardon3DProjectDbTask durable{};
|
|
if (lardon3d_project_db_load_task(runtime.state.project_db, task_id, &durable) ==
|
|
LARDON3D_PROJECT_DB_OK &&
|
|
durable.saved_state == snapshot.state) {
|
|
if (snapshot.state != TASK_COMPLETED) {
|
|
std::fprintf(stderr, "%s task %llu ended in %s: %s\n", phase,
|
|
static_cast<unsigned long long>(task_id),
|
|
lardon3d_task_state_name(snapshot.state), snapshot.message);
|
|
return false;
|
|
}
|
|
return lardon3d_task_queue_remove(runtime.state.task_queue, task_id);
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
|
}
|
|
}
|
|
|
|
bool restart_runtime(Runtime &runtime, uint64_t execution_id, const char *boundary) {
|
|
stop_runtime(runtime);
|
|
if (!start_runtime(runtime)) return false;
|
|
Lardon3DProjectDbSelectedExecution execution{};
|
|
if (lardon3d_project_db_load_selected_execution(runtime.state.project_db, execution_id,
|
|
&execution) != LARDON3D_PROJECT_DB_OK) {
|
|
std::fprintf(stderr, "restart at %s lost retained execution %llu\n", boundary,
|
|
static_cast<unsigned long long>(execution_id));
|
|
return false;
|
|
}
|
|
/* This boundary deliberately has no sidecar or durable top-level phase. The
|
|
* caller retains only execution_id; the next phase reconstructs exact image,
|
|
* FeatureSet or result IDs from v22 durable records and their cursors. */
|
|
std::printf("{\"record\":\"restart\",\"kind\":\"REOPEN_ONLY\","
|
|
"\"boundary\":\"%s\",\"execution_id\":%llu}\n",
|
|
boundary, static_cast<unsigned long long>(execution_id));
|
|
return true;
|
|
}
|
|
|
|
bool recover_quality_task(Runtime &runtime, uint64_t task_id) {
|
|
/* Durable Task creation publishes its typed request and initial cursor before
|
|
* Queue transfer. The original unqueued object has been discarded to model
|
|
* loss before first dispatch. Recovery must use the existing registry to
|
|
* reconstruct callback state, then transfer the Task through Queue and
|
|
* Governor admission; the runner adds no top-level durable state, test hook,
|
|
* or alternate worker path. */
|
|
stop_runtime(runtime);
|
|
if (!start_runtime(runtime)) return false;
|
|
Lardon3DProjectRecoverySummary recovery{};
|
|
if (lardon3d_project_resume_recoverable_tasks(
|
|
&runtime.state, lardon3d_task_kind_registry_production(), &recovery) !=
|
|
LARDON3D_PROJECT_DB_OK ||
|
|
recovery.resumed != 1 || recovery.failed != 0)
|
|
return false;
|
|
std::printf("{\"record\":\"restart\","
|
|
"\"kind\":\"REGISTRY_QUEUE_GOVERNOR_RESTART\","
|
|
"\"task_id\":%llu,\"inspected\":%zu,\"resumed\":%zu}\n",
|
|
static_cast<unsigned long long>(task_id), recovery.inspected, recovery.resumed);
|
|
return wait_completed(runtime, task_id, "photo_quality.triage recovered");
|
|
}
|
|
|
|
bool run_task_pair(Runtime &runtime, uint64_t scanset_id, const Campaign &campaign,
|
|
bool prove_restart, uint64_t &quality_task_id, uint64_t &campaign_task_id) {
|
|
Lardon3DPhotoQualityTaskRequest quality_request{
|
|
campaign.sources.data(), campaign.sources.size(), campaign.confirmations.data(),
|
|
campaign.confirmations.size()};
|
|
if (prove_restart) {
|
|
Lardon3DTask *pending = lardon3d_project_create_photo_quality_task(
|
|
&runtime.state, scanset_id, &quality_request, &quality_task_id);
|
|
if (!pending) return false;
|
|
lardon3d_task_destroy(pending);
|
|
if (!recover_quality_task(runtime, quality_task_id)) {
|
|
std::fprintf(stderr, "quality Task could not be restored at its pending boundary\n");
|
|
return false;
|
|
}
|
|
} else if (!lardon3d_project_enqueue_photo_quality(&runtime.state, scanset_id, &quality_request,
|
|
&quality_task_id) ||
|
|
!wait_completed(runtime, quality_task_id, "photo_quality.triage")) {
|
|
return false;
|
|
}
|
|
Lardon3DAcquisitionCampaignTaskRequest campaign_request{};
|
|
campaign_request.sources = campaign.sources.data();
|
|
campaign_request.source_count = campaign.sources.size();
|
|
campaign_request.confirmations = campaign.confirmations.data();
|
|
campaign_request.confirmation_count = campaign.confirmations.size();
|
|
campaign_request.ingest_options.representation = LARDON3D_ACQUISITION_SELECT_JPEG_SOURCE;
|
|
campaign_request.ingest_options.select_representation = 1;
|
|
campaign_request.ingest_options.imported_at = std::time(nullptr);
|
|
campaign_request.ingest_options.max_source_bytes = UINT64_MAX;
|
|
return lardon3d_project_enqueue_acquisition_campaign(
|
|
&runtime.state, scanset_id, &campaign_request, &campaign_task_id) &&
|
|
wait_completed(runtime, campaign_task_id, "acquisition_campaign.run");
|
|
}
|
|
|
|
bool create_selection(Runtime &runtime, Mode mode, uint64_t quality_task_id,
|
|
uint64_t campaign_task_id, size_t group_count,
|
|
Lardon3DProjectDbSelectedExecution &execution) {
|
|
std::vector<Lardon3DProjectDbSelectedExecutionItem> items;
|
|
for (size_t index = 0; index < group_count; ++index) {
|
|
const uint32_t group_id = static_cast<uint32_t>(index + 1u);
|
|
Lardon3DProjectDbPhotoQualityResult quality{};
|
|
Lardon3DProjectDbAcquisitionCampaignCapture campaign{};
|
|
if (lardon3d_project_db_load_photo_quality_result(runtime.state.project_db, quality_task_id,
|
|
group_id, &quality) !=
|
|
LARDON3D_PROJECT_DB_OK ||
|
|
lardon3d_project_db_load_acquisition_campaign_capture(
|
|
runtime.state.project_db, campaign_task_id, group_id, &campaign) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
if (quality.metrics.recommendation != LARDON3D_PHOTO_QUALITY_GOOD ||
|
|
quality.override_value != LARDON3D_PHOTO_QUALITY_OVERRIDE_NONE)
|
|
continue;
|
|
Lardon3DProjectDbSelectedExecutionItem item{};
|
|
item.item_index = static_cast<uint32_t>(items.size());
|
|
item.quality_group_id = group_id;
|
|
item.campaign_group_id = group_id;
|
|
item.capture_id = campaign.capture_id;
|
|
if (mode == Mode::kA6000) {
|
|
Lardon3DProjectDbCaptureSourceAsset sources[4]{};
|
|
size_t source_count = 0;
|
|
if (lardon3d_project_db_list_capture_source_assets(runtime.state.project_db,
|
|
campaign.capture_id, 0, sources, 4,
|
|
&source_count) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
size_t raw_count = 0;
|
|
for (size_t source = 0; source < source_count; ++source) {
|
|
if (sources[source].source_kind == LARDON3D_DB_CAPTURE_SOURCE_RAW) {
|
|
item.source_asset_id = sources[source].asset_id;
|
|
++raw_count;
|
|
}
|
|
}
|
|
if (raw_count != 1) return false;
|
|
item.representation_source = LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET;
|
|
} else {
|
|
item.representation_source = LARDON3D_SELECTED_REPRESENTATION_SOURCE_IMAGE;
|
|
}
|
|
items.push_back(item);
|
|
}
|
|
if (items.empty()) {
|
|
std::printf("{\"record\":\"selection\",\"selected\":0,"
|
|
"\"reason\":\"no GOOD+NONE groups\"}\n");
|
|
return true;
|
|
}
|
|
/* These item records are the only bridge between independent quality and
|
|
* campaign group namespaces. Equal numeric IDs are supplied explicitly and
|
|
* never used to infer Capture, Asset, or image identity. */
|
|
return lardon3d_project_db_create_selected_execution(
|
|
runtime.state.project_db, quality_task_id, campaign_task_id, items.data(),
|
|
items.size(), std::time(nullptr), &execution) == LARDON3D_PROJECT_DB_OK;
|
|
}
|
|
|
|
bool publish_representations(Runtime &runtime, uint64_t execution_id,
|
|
std::vector<uint64_t> &image_ids) {
|
|
Lardon3DProjectDbSelectedExecution execution{};
|
|
if (lardon3d_project_db_load_selected_execution(runtime.state.project_db, execution_id,
|
|
&execution) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (uint32_t index = execution.next_item_index; index < execution.item_count; ++index) {
|
|
Lardon3DProjectDbSelectedExecutionItem item{};
|
|
if (lardon3d_project_db_load_selected_execution_item(runtime.state.project_db, execution_id,
|
|
index, &item) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
uint64_t image_id = 0;
|
|
if (item.representation_source == LARDON3D_SELECTED_REPRESENTATION_RAW_ASSET) {
|
|
uint64_t task_id = 0;
|
|
if (!lardon3d_project_enqueue_raw_development(&runtime.state, item.capture_id,
|
|
item.source_asset_id, &task_id) ||
|
|
!wait_completed(runtime, task_id, "raw.develop"))
|
|
return false;
|
|
Lardon3DProjectDbRawDevelopmentTask raw{};
|
|
if (lardon3d_project_db_load_raw_development_task(runtime.state.project_db, task_id, &raw) !=
|
|
LARDON3D_PROJECT_DB_OK ||
|
|
raw.phase != LARDON3D_RAW_DEVELOPMENT_TASK_PUBLISHED || !raw.has_image ||
|
|
raw.capture_id != item.capture_id || raw.source_asset_id != item.source_asset_id)
|
|
return false;
|
|
image_id = raw.image_id;
|
|
} else {
|
|
if (lardon3d_project_db_get_selected_capture_image(runtime.state.project_db, item.capture_id,
|
|
&image_id) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
}
|
|
if (lardon3d_project_db_record_selected_representation(runtime.state.project_db, execution_id,
|
|
index, image_id, index + 1u) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
}
|
|
if (lardon3d_project_db_load_selected_execution(runtime.state.project_db, execution_id,
|
|
&execution) != LARDON3D_PROJECT_DB_OK ||
|
|
execution.stage != LARDON3D_SELECTED_EXECUTION_CALIBRATION)
|
|
return false;
|
|
image_ids.clear();
|
|
for (uint32_t index = 0; index < execution.item_count; ++index) {
|
|
Lardon3DProjectDbSelectedExecutionItem item{};
|
|
if (lardon3d_project_db_load_selected_execution_item(runtime.state.project_db, execution_id,
|
|
index, &item) != LARDON3D_PROJECT_DB_OK ||
|
|
!item.has_image)
|
|
return false;
|
|
image_ids.push_back(item.image_id);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool extract_features(Runtime &runtime, const std::vector<uint64_t> &image_ids,
|
|
const Lardon3DFeatureExtractorParameters ¶meters,
|
|
std::vector<Lardon3DProjectDbFeatureSet> &feature_sets) {
|
|
unsigned char fingerprint[32]{};
|
|
lardon3d_feature_extractor_parameter_fingerprint(¶meters, fingerprint);
|
|
feature_sets.clear();
|
|
for (const uint64_t image_id : image_ids) {
|
|
Lardon3DProjectDbFeatureSet feature{};
|
|
if (lardon3d_project_db_find_feature_set(runtime.state.project_db, image_id,
|
|
LARDON3D_FEATURE_EXTRACTOR_KIND,
|
|
LARDON3D_FEATURE_EXTRACTOR_VERSION, fingerprint,
|
|
&feature) != LARDON3D_PROJECT_DB_OK) {
|
|
uint64_t task_id = 0;
|
|
if (!lardon3d_project_enqueue_feature_extract(&runtime.state, image_id, ¶meters,
|
|
&task_id) ||
|
|
!wait_completed(runtime, task_id, "features.extract") ||
|
|
lardon3d_project_db_find_feature_set(
|
|
runtime.state.project_db, image_id, LARDON3D_FEATURE_EXTRACTOR_KIND,
|
|
LARDON3D_FEATURE_EXTRACTOR_VERSION, fingerprint, &feature) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
}
|
|
feature_sets.push_back(feature);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool collect_verified_ids(Runtime &runtime, const unsigned char verifier_fingerprint[32],
|
|
std::vector<uint64_t> &verified_ids,
|
|
size_t *applicable = nullptr, size_t *verified = nullptr,
|
|
size_t *rejected = nullptr) {
|
|
verified_ids.clear();
|
|
if (applicable) *applicable = 0;
|
|
if (verified) *verified = 0;
|
|
if (rejected) *rejected = 0;
|
|
uint64_t match_cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbMatchResult matches[64]{};
|
|
size_t match_count = 0;
|
|
if (lardon3d_project_db_list_match_results(runtime.state.project_db, match_cursor, matches, 64,
|
|
&match_count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t index = 0; index < match_count; ++index) {
|
|
match_cursor = matches[index].match_result_id;
|
|
if (matches[index].result_status != LARDON3D_MATCH_RESULT_STATUS_MATCHED ||
|
|
matches[index].match_count == 0)
|
|
continue;
|
|
if (applicable) ++*applicable;
|
|
Lardon3DProjectDbGeometricVerificationResult result{};
|
|
if (lardon3d_project_db_find_geometric_verification_result(
|
|
runtime.state.project_db, matches[index].match_result_id,
|
|
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, LARDON3D_GEOMETRIC_VERIFIER_VERSION,
|
|
verifier_fingerprint, &result) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
if (result.status == LARDON3D_GEOMETRIC_VERIFIED) {
|
|
if (verified) ++*verified;
|
|
verified_ids.push_back(result.geometric_verification_result_id);
|
|
} else if (result.status == LARDON3D_GEOMETRIC_REJECTED) {
|
|
if (rejected) ++*rejected;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
if (match_count < 64) break;
|
|
}
|
|
std::sort(verified_ids.begin(), verified_ids.end());
|
|
return std::adjacent_find(verified_ids.begin(), verified_ids.end()) == verified_ids.end();
|
|
}
|
|
|
|
bool downstream(Runtime &runtime, const std::vector<Lardon3DProjectDbFeatureSet> &feature_sets,
|
|
const Lardon3DFeatureExtractorParameters &orb, uint64_t &geometry_task_id,
|
|
std::vector<uint64_t> &verified_ids) {
|
|
Lardon3DVisualIndexConfiguration index_configuration{
|
|
LARDON3D_VISUAL_INDEX_VERSION, 1024, 256};
|
|
uint64_t visual_index_id = 0;
|
|
if (feature_sets.empty() ||
|
|
lardon3d_visual_index_create(runtime.state.project_db, &feature_sets.front(),
|
|
&index_configuration, &visual_index_id) !=
|
|
LARDON3D_VISUAL_INDEX_OK)
|
|
return false;
|
|
uint64_t task_id = 0;
|
|
if (!lardon3d_project_enqueue_visual_index_update(&runtime.state, visual_index_id, &task_id) ||
|
|
!wait_completed(runtime, task_id, "visual_index.update"))
|
|
return false;
|
|
|
|
Lardon3DVisualIndexQueryOptions query{
|
|
std::min<uint32_t>(64, static_cast<uint32_t>(feature_sets.size())), 1,
|
|
LARDON3D_VISUAL_INDEX_SAME_SCANSET, true};
|
|
if (!lardon3d_project_enqueue_candidate_pair_generate(&runtime.state, visual_index_id, &query,
|
|
&task_id) ||
|
|
!wait_completed(runtime, task_id, "candidate_pair.generate"))
|
|
return false;
|
|
Lardon3DMatcherTaskConfiguration matcher{};
|
|
std::snprintf(matcher.feature_extractor_kind, sizeof(matcher.feature_extractor_kind), "%s",
|
|
LARDON3D_FEATURE_EXTRACTOR_KIND);
|
|
matcher.feature_extractor_version = LARDON3D_FEATURE_EXTRACTOR_VERSION;
|
|
lardon3d_feature_extractor_parameter_fingerprint(&orb, matcher.feature_parameter_fingerprint);
|
|
matcher.matcher.kind = LARDON3D_MATCHER_ORB_BF;
|
|
matcher.matcher.ratio_threshold = lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ORB_BF);
|
|
if (!lardon3d_project_enqueue_matcher_task(&runtime.state, &matcher, &task_id) ||
|
|
!wait_completed(runtime, task_id, "matcher.run"))
|
|
return false;
|
|
Lardon3DGeometricVerifierTaskConfiguration verifier{
|
|
lardon3d_geometric_verifier_default_parameters()};
|
|
if (!lardon3d_project_enqueue_geometric_verifier_task(&runtime.state, &verifier,
|
|
&geometry_task_id) ||
|
|
!wait_completed(runtime, geometry_task_id, "geometric_verifier.run"))
|
|
return false;
|
|
|
|
unsigned char verifier_fingerprint[32]{};
|
|
lardon3d_geometric_verifier_fingerprint(&verifier.verifier, verifier_fingerprint);
|
|
return collect_verified_ids(runtime, verifier_fingerprint, verified_ids);
|
|
}
|
|
|
|
bool build_tracks(Runtime &runtime, const std::vector<uint64_t> &verified_ids,
|
|
const unsigned char verifier_fingerprint[32],
|
|
uint64_t *task_id_output = nullptr,
|
|
Lardon3DProjectDbTrackSet *track_set_output = nullptr) {
|
|
if (task_id_output) *task_id_output = 0;
|
|
if (track_set_output) *track_set_output = {};
|
|
if (verified_ids.empty()) return true;
|
|
if (verified_ids.size() > (std::numeric_limits<size_t>::max() - 8U) / 8U) return false;
|
|
std::vector<unsigned char> scope_bytes(8U + verified_ids.size() * 8U);
|
|
std::memcpy(scope_bytes.data(), "L3DTSIS1", 8);
|
|
for (size_t index = 0; index < verified_ids.size(); ++index) {
|
|
uint64_t value = verified_ids[index];
|
|
for (size_t byte = 0; byte < 8; ++byte) {
|
|
scope_bytes[8U + index * 8U + byte] = static_cast<unsigned char>(value & 0xffU);
|
|
value >>= 8U;
|
|
}
|
|
}
|
|
Lardon3DProjectDbTrackSet exact{};
|
|
std::snprintf(exact.builder_kind, sizeof(exact.builder_kind), "track_builder");
|
|
exact.builder_version = LARDON3D_TRACK_BUILDER_VERSION;
|
|
unsigned int digest_size = 0;
|
|
if (!lardon3d_track_builder_fingerprint(exact.parameter_fingerprint) ||
|
|
EVP_Digest(scope_bytes.data(), scope_bytes.size(), exact.input_scope_hash, &digest_size,
|
|
EVP_sha256(), nullptr) != 1 ||
|
|
digest_size != 32)
|
|
return false;
|
|
exact.verifier_kind = LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL;
|
|
exact.verifier_version = LARDON3D_GEOMETRIC_VERIFIER_VERSION;
|
|
std::memcpy(exact.verifier_fingerprint, verifier_fingerprint, 32);
|
|
exact.gvr_count = verified_ids.size();
|
|
Lardon3DProjectDbTrackSet existing{};
|
|
const Lardon3DProjectDbResult found =
|
|
lardon3d_project_db_find_track_set(runtime.state.project_db, &exact, &existing);
|
|
if (found == LARDON3D_PROJECT_DB_OK) {
|
|
if (track_set_output) *track_set_output = existing;
|
|
return true;
|
|
}
|
|
if (found != LARDON3D_PROJECT_DB_NOT_FOUND) return false;
|
|
Lardon3DTrackBuilderTaskConfiguration configuration{
|
|
runtime.project_path.c_str(), runtime.state.project_db,
|
|
LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL, LARDON3D_GEOMETRIC_VERIFIER_VERSION,
|
|
verifier_fingerprint, verified_ids.data(), verified_ids.size()};
|
|
uint64_t task_id = 0;
|
|
if (!lardon3d_project_enqueue_track_builder_task(&runtime.state, &configuration, &task_id) ||
|
|
!wait_completed(runtime, task_id, "track_builder.run"))
|
|
return false;
|
|
Lardon3DProjectDbTrackBuilderTask durable{};
|
|
Lardon3DProjectDbTrackSet identity{};
|
|
if (lardon3d_project_db_load_track_builder_task(runtime.state.project_db, task_id, &durable) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
std::memcpy(identity.builder_kind, durable.builder_kind, sizeof(identity.builder_kind));
|
|
identity.builder_version = durable.builder_version;
|
|
std::memcpy(identity.parameter_fingerprint, durable.builder_fingerprint,
|
|
sizeof(identity.parameter_fingerprint));
|
|
identity.verifier_kind = durable.verifier_kind;
|
|
identity.verifier_version = durable.verifier_version;
|
|
std::memcpy(identity.verifier_fingerprint, durable.verifier_fingerprint,
|
|
sizeof(identity.verifier_fingerprint));
|
|
std::memcpy(identity.input_scope_hash, durable.input_scope_hash,
|
|
sizeof(identity.input_scope_hash));
|
|
identity.gvr_count = durable.gvr_count;
|
|
Lardon3DProjectDbTrackSet published{};
|
|
if (lardon3d_project_db_find_track_set(runtime.state.project_db, &identity, &published) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
if (task_id_output) *task_id_output = task_id;
|
|
if (track_set_output) *track_set_output = published;
|
|
return true;
|
|
}
|
|
|
|
bool collect_evidence(Runtime &runtime, uint64_t execution_id, Evidence &evidence) {
|
|
Lardon3DProjectDbSelectedExecution execution{};
|
|
if (execution_id != 0 &&
|
|
lardon3d_project_db_load_selected_execution(runtime.state.project_db, execution_id,
|
|
&execution) == LARDON3D_PROJECT_DB_OK)
|
|
evidence.selected = execution.item_count;
|
|
uint64_t cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbFeatureSet page[64]{};
|
|
size_t count = 0;
|
|
if (lardon3d_project_db_list_feature_sets(runtime.state.project_db, cursor, page, 64, &count) !=
|
|
LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
evidence.features += count;
|
|
if (count) cursor = page[count - 1].feature_set_id;
|
|
if (count < 64) break;
|
|
}
|
|
cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbCandidatePair page[64]{};
|
|
size_t count = 0;
|
|
if (lardon3d_project_db_list_candidate_pairs(runtime.state.project_db, cursor, page, 64,
|
|
&count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
evidence.pairs += count;
|
|
if (count) cursor = page[count - 1].candidate_pair_id;
|
|
if (count < 64) break;
|
|
}
|
|
cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbMatchResult page[64]{};
|
|
size_t count = 0;
|
|
if (lardon3d_project_db_list_match_results(runtime.state.project_db, cursor, page, 64,
|
|
&count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
evidence.matches += count;
|
|
for (size_t index = 0; index < count; ++index) {
|
|
Lardon3DProjectDbGeometricVerificationResult geometries[4]{};
|
|
size_t geometry_count = 0;
|
|
if (lardon3d_project_db_list_geometric_verification_results(
|
|
runtime.state.project_db, page[index].match_result_id, 0, geometries, 4,
|
|
&geometry_count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t geometry = 0; geometry < geometry_count; ++geometry) {
|
|
if (geometries[geometry].status == LARDON3D_GEOMETRIC_VERIFIED) ++evidence.verified;
|
|
else ++evidence.rejected;
|
|
}
|
|
}
|
|
if (count) cursor = page[count - 1].match_result_id;
|
|
if (count < 64) break;
|
|
}
|
|
Lardon3DProjectDbTrackSet tracks[8]{};
|
|
size_t track_set_count = 0;
|
|
if (lardon3d_project_db_list_track_sets(runtime.state.project_db, 0, tracks, 8,
|
|
&track_set_count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t index = 0; index < track_set_count; ++index) evidence.tracks += tracks[index].track_count;
|
|
return true;
|
|
}
|
|
|
|
bool count_exact_gvrs(Runtime &runtime, const unsigned char fingerprint[32],
|
|
size_t &count) {
|
|
count = 0;
|
|
uint64_t match_cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbMatchResult matches[64]{};
|
|
size_t match_count = 0;
|
|
if (lardon3d_project_db_list_match_results(runtime.state.project_db, match_cursor, matches, 64,
|
|
&match_count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t index = 0; index < match_count; ++index) {
|
|
match_cursor = matches[index].match_result_id;
|
|
uint64_t geometry_cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectDbGeometricVerificationResult page[8]{};
|
|
size_t geometry_count = 0;
|
|
if (lardon3d_project_db_list_geometric_verification_results(
|
|
runtime.state.project_db, matches[index].match_result_id, geometry_cursor, page, 8,
|
|
&geometry_count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t geometry = 0; geometry < geometry_count; ++geometry) {
|
|
if (page[geometry].verifier_kind == LARDON3D_GEOMETRIC_VERIFIER_FUNDAMENTAL &&
|
|
page[geometry].verifier_version == LARDON3D_GEOMETRIC_VERIFIER_VERSION &&
|
|
std::memcmp(page[geometry].parameter_fingerprint, fingerprint, 32) == 0)
|
|
++count;
|
|
geometry_cursor = page[geometry].geometric_verification_result_id;
|
|
}
|
|
if (geometry_count < 8) break;
|
|
}
|
|
}
|
|
if (match_count < 64) break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool find_pending_pre_gv_task(Runtime &runtime, uint64_t &candidate_task_id,
|
|
uint64_t &matcher_task_id) {
|
|
candidate_task_id = 0;
|
|
matcher_task_id = 0;
|
|
uint64_t cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectRecoveryEntry entries[8]{};
|
|
size_t count = 0;
|
|
if (lardon3d_project_list_recoverable(
|
|
&runtime.state, lardon3d_task_kind_registry_production(), cursor,
|
|
entries, 8, &count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t index = 0; index < count; ++index) {
|
|
const auto &entry = entries[index];
|
|
cursor = entry.task_id;
|
|
if ((entry.status != LARDON3D_PROJECT_RECOVERABLE &&
|
|
entry.status !=
|
|
LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE) ||
|
|
entry.snapshot.recovery_state != TASK_PENDING) {
|
|
std::fprintf(stderr,
|
|
"pending task %llu is not safely recoverable (status=%d)\n",
|
|
static_cast<unsigned long long>(entry.task_id),
|
|
static_cast<int>(entry.status));
|
|
return false;
|
|
}
|
|
if (std::strcmp(entry.task_kind,
|
|
LARDON3D_CANDIDATE_PAIR_GENERATE_TASK_KIND) == 0) {
|
|
if (candidate_task_id != 0) return false;
|
|
candidate_task_id = entry.task_id;
|
|
} else if (std::strcmp(entry.task_kind, LARDON3D_MATCHER_TASK_KIND) == 0) {
|
|
if (matcher_task_id != 0) return false;
|
|
matcher_task_id = entry.task_id;
|
|
} else {
|
|
// The production recovery API resumes every pending task. Refuse an
|
|
// unrelated task rather than replaying an earlier pipeline stage.
|
|
std::fprintf(stderr,
|
|
"PRE-GV recovery refuses pending task %llu of kind %s\n",
|
|
static_cast<unsigned long long>(entry.task_id),
|
|
entry.task_kind);
|
|
return false;
|
|
}
|
|
}
|
|
if (count < 8) break;
|
|
}
|
|
return !(candidate_task_id != 0 && matcher_task_id != 0);
|
|
}
|
|
|
|
bool recover_one_pre_gv_task(Runtime &runtime, uint64_t task_id,
|
|
const char *phase,
|
|
Lardon3DProjectRecoverySummary &recovery) {
|
|
recovery = {};
|
|
if (lardon3d_project_resume_recoverable_tasks(
|
|
&runtime.state, lardon3d_task_kind_registry_production(),
|
|
&recovery) != LARDON3D_PROJECT_DB_OK ||
|
|
recovery.resumed != 1 || recovery.failed != 0 || recovery.queue_full)
|
|
return false;
|
|
return wait_completed(runtime, task_id, phase);
|
|
}
|
|
|
|
bool find_pending_candidate_only_task(Runtime &runtime, uint64_t &candidate_task_id) {
|
|
candidate_task_id = 0;
|
|
uint64_t cursor = 0;
|
|
for (;;) {
|
|
Lardon3DProjectRecoveryEntry entries[8]{};
|
|
size_t count = 0;
|
|
if (lardon3d_project_list_recoverable(
|
|
&runtime.state, lardon3d_task_kind_registry_production(), cursor,
|
|
entries, 8, &count) != LARDON3D_PROJECT_DB_OK)
|
|
return false;
|
|
for (size_t index = 0; index < count; ++index) {
|
|
const auto &entry = entries[index];
|
|
cursor = entry.task_id;
|
|
if ((entry.status != LARDON3D_PROJECT_RECOVERABLE &&
|
|
entry.status != LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE) ||
|
|
entry.snapshot.recovery_state != TASK_PENDING ||
|
|
std::strcmp(entry.task_kind,
|
|
LARDON3D_CANDIDATE_PAIR_GENERATE_TASK_KIND) != 0 ||
|
|
candidate_task_id != 0) {
|
|
std::fprintf(stderr,
|
|
"candidate-only recovery refuses unsafe pending task %llu of kind %s\n",
|
|
static_cast<unsigned long long>(entry.task_id), entry.task_kind);
|
|
return false;
|
|
}
|
|
candidate_task_id = entry.task_id;
|
|
}
|
|
if (count < 8) break;
|
|
}
|
|
// An absent entry is intentionally rejected: it means the requested pending
|
|
// Candidate boundary is missing or already complete, neither of which this
|
|
// repair mode may reinterpret or replace.
|
|
return candidate_task_id != 0;
|
|
}
|
|
|
|
// This repair route owns exactly one durable Candidate Task. Registry recovery
|
|
// enqueues through the production Queue and Governor, but the preflight makes
|
|
// that all-task API safe by admitting no Matcher, GV, Track, SfM, or prior work.
|
|
bool run_existing_candidate(Runtime &runtime) {
|
|
if (!start_runtime(runtime)) return false;
|
|
Evidence before{};
|
|
uint64_t candidate_task_id = 0;
|
|
if (!collect_evidence(runtime, 0, before) ||
|
|
!find_pending_candidate_only_task(runtime, candidate_task_id)) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
Lardon3DProjectRecoverySummary recovery{};
|
|
const bool recovered = recover_one_pre_gv_task(
|
|
runtime, candidate_task_id, "candidate_pair.generate recovered", recovery);
|
|
Evidence after{};
|
|
const bool ok = recovered && collect_evidence(runtime, 0, after) &&
|
|
after.features == before.features && after.pairs >= before.pairs &&
|
|
after.matches == before.matches && after.verified == before.verified &&
|
|
after.rejected == before.rejected && after.tracks == before.tracks;
|
|
std::printf(
|
|
"{\"record\":\"existing_candidate_summary\",\"ok\":%s,"
|
|
"\"candidate_task_id\":%llu,\"candidate_action\":\"recovered\","
|
|
"\"candidate_recovery_inspected\":%zu,\"candidate_recovery_resumed\":%zu,"
|
|
"\"cpu_budget\":%u,\"feature_sets_before\":%zu,\"feature_sets_after\":%zu,"
|
|
"\"candidate_pairs_before\":%zu,\"candidate_pairs_after\":%zu,"
|
|
"\"match_results_before\":%zu,\"match_results_after\":%zu,"
|
|
"\"gvrs_before\":%zu,\"gvrs_after\":%zu,\"tracks_before\":%zu,"
|
|
"\"tracks_after\":%zu,\"prior_stages_replayed\":false,"
|
|
"\"matcher_enqueued\":false,\"gv_enqueued\":false,"
|
|
"\"track_builder_enqueued\":false,\"sparse_sfm_run\":false}\n",
|
|
ok ? "true" : "false", static_cast<unsigned long long>(candidate_task_id),
|
|
recovery.inspected, recovery.resumed, runtime.cpu_budget, before.features,
|
|
after.features, before.pairs, after.pairs, before.matches, after.matches,
|
|
before.verified + before.rejected, after.verified + after.rejected,
|
|
before.tracks, after.tracks);
|
|
stop_runtime(runtime);
|
|
return ok;
|
|
}
|
|
|
|
// This repair route owns only the already-durable Candidate/Matcher boundary.
|
|
// It may recover those production Tasks or enqueue the missing Matcher, but it
|
|
// must never reconstruct prior acquisition stages or cross into GV/Tracks/SfM.
|
|
bool run_existing_pre_gv(Runtime &runtime) {
|
|
if (!start_runtime(runtime)) return false;
|
|
Evidence before{};
|
|
if (!collect_evidence(runtime, 0, before) || before.verified != 0 ||
|
|
before.rejected != 0 || before.tracks != 0) {
|
|
std::fprintf(stderr,
|
|
"PRE-GV mode requires a project with no GV or Track evidence\n");
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
uint64_t candidate_task_id = 0;
|
|
uint64_t pending_matcher_task_id = 0;
|
|
if (!find_pending_pre_gv_task(runtime, candidate_task_id,
|
|
pending_matcher_task_id)) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
Lardon3DProjectRecoverySummary candidate_recovery{};
|
|
bool candidate_recovered = false;
|
|
if (candidate_task_id != 0) {
|
|
if (!recover_one_pre_gv_task(runtime, candidate_task_id,
|
|
"candidate_pair.generate recovered",
|
|
candidate_recovery)) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
candidate_recovered = true;
|
|
}
|
|
|
|
Evidence after_candidate{};
|
|
if (!collect_evidence(runtime, 0, after_candidate) ||
|
|
after_candidate.matches > after_candidate.pairs ||
|
|
after_candidate.verified != before.verified ||
|
|
after_candidate.rejected != before.rejected ||
|
|
after_candidate.tracks != before.tracks) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
uint64_t newly_pending_candidate = 0;
|
|
uint64_t matcher_task_id = 0;
|
|
if (!find_pending_pre_gv_task(runtime, newly_pending_candidate,
|
|
matcher_task_id) ||
|
|
newly_pending_candidate != 0) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
if (pending_matcher_task_id != 0) matcher_task_id = pending_matcher_task_id;
|
|
|
|
Lardon3DProjectRecoverySummary matcher_recovery{};
|
|
const char *matcher_action = "already_complete";
|
|
if (after_candidate.matches < after_candidate.pairs) {
|
|
if (matcher_task_id != 0) {
|
|
if (!recover_one_pre_gv_task(runtime, matcher_task_id,
|
|
"matcher.run recovered",
|
|
matcher_recovery)) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
matcher_action = "recovered";
|
|
} else {
|
|
const Lardon3DFeatureExtractorParameters orb{8192, 8, 20};
|
|
Lardon3DMatcherTaskConfiguration matcher{};
|
|
std::snprintf(matcher.feature_extractor_kind,
|
|
sizeof(matcher.feature_extractor_kind), "%s",
|
|
LARDON3D_FEATURE_EXTRACTOR_KIND);
|
|
matcher.feature_extractor_version = LARDON3D_FEATURE_EXTRACTOR_VERSION;
|
|
lardon3d_feature_extractor_parameter_fingerprint(
|
|
&orb, matcher.feature_parameter_fingerprint);
|
|
matcher.matcher.kind = LARDON3D_MATCHER_ORB_BF;
|
|
matcher.matcher.ratio_threshold =
|
|
lardon3d_matcher_default_ratio(LARDON3D_MATCHER_ORB_BF);
|
|
if (!lardon3d_project_enqueue_matcher_task(&runtime.state, &matcher,
|
|
&matcher_task_id) ||
|
|
!wait_completed(runtime, matcher_task_id, "matcher.run existing")) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
matcher_action = "enqueued";
|
|
}
|
|
} else if (matcher_task_id != 0) {
|
|
std::fprintf(stderr,
|
|
"complete Match Results coexist with a pending Matcher task\n");
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
Evidence after{};
|
|
const bool ok = collect_evidence(runtime, 0, after) &&
|
|
after.features == before.features &&
|
|
after.pairs >= before.pairs && after.matches == after.pairs &&
|
|
after.verified == before.verified &&
|
|
after.rejected == before.rejected &&
|
|
after.tracks == before.tracks;
|
|
std::printf(
|
|
"{\"record\":\"existing_pre_gv_summary\",\"ok\":%s,"
|
|
"\"candidate_task_id\":%llu,\"candidate_action\":\"%s\","
|
|
"\"candidate_recovery_inspected\":%zu,"
|
|
"\"candidate_recovery_resumed\":%zu,\"matcher_task_id\":%llu,"
|
|
"\"matcher_action\":\"%s\",\"matcher_recovery_resumed\":%zu,"
|
|
"\"feature_sets_before\":%zu,\"feature_sets_after\":%zu,"
|
|
"\"candidate_pairs_before\":%zu,\"candidate_pairs_after\":%zu,"
|
|
"\"match_results_before\":%zu,\"match_results_after\":%zu,"
|
|
"\"gvrs_before\":%zu,\"gvrs_after\":%zu,"
|
|
"\"tracks_before\":%zu,\"tracks_after\":%zu,"
|
|
"\"prior_stages_replayed\":false,\"gv_enqueued\":false,"
|
|
"\"track_builder_enqueued\":false,\"sparse_sfm_run\":false}\n",
|
|
ok ? "true" : "false",
|
|
static_cast<unsigned long long>(candidate_task_id),
|
|
candidate_recovered ? "recovered" : "already_complete",
|
|
candidate_recovery.inspected, candidate_recovery.resumed,
|
|
static_cast<unsigned long long>(matcher_task_id), matcher_action,
|
|
matcher_recovery.resumed, before.features, after.features, before.pairs,
|
|
after.pairs, before.matches, after.matches,
|
|
before.verified + before.rejected, after.verified + after.rejected,
|
|
before.tracks, after.tracks);
|
|
stop_runtime(runtime);
|
|
return ok;
|
|
}
|
|
|
|
bool run_existing_geometry(Runtime &runtime) {
|
|
if (!start_runtime(runtime)) return false;
|
|
Lardon3DGeometricVerifierTaskConfiguration configuration{
|
|
lardon3d_geometric_verifier_default_parameters()};
|
|
unsigned char fingerprint[32]{};
|
|
lardon3d_geometric_verifier_fingerprint(&configuration.verifier, fingerprint);
|
|
size_t exact_before = 0;
|
|
if (!count_exact_gvrs(runtime, fingerprint, exact_before)) {
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
/* This route deliberately starts at the immutable Match Result boundary.
|
|
* It enqueues no quality, campaign, representation, feature, index,
|
|
* candidate, or Matcher work. Task/Queue/Governor retain their production
|
|
* ownership, and exact current identity reuse makes a full rerun convergent. */
|
|
uint64_t geometry_task_id = 0;
|
|
if (!lardon3d_project_enqueue_geometric_verifier_task(
|
|
&runtime.state, &configuration, &geometry_task_id) ||
|
|
!wait_completed(runtime, geometry_task_id,
|
|
"geometric_verifier.run current existing")) {
|
|
std::printf("{\"record\":\"existing_geometry_summary\",\"ok\":false,"
|
|
"\"stage\":\"geometric_verifier.run\",\"task_errors\":1}\n");
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
std::vector<uint64_t> verified_ids;
|
|
size_t applicable = 0;
|
|
size_t verified = 0;
|
|
size_t rejected = 0;
|
|
size_t exact_after = 0;
|
|
if (!collect_verified_ids(runtime, fingerprint, verified_ids, &applicable, &verified,
|
|
&rejected) ||
|
|
!count_exact_gvrs(runtime, fingerprint, exact_after) || exact_after != applicable) {
|
|
std::printf("{\"record\":\"existing_geometry_summary\",\"ok\":false,"
|
|
"\"stage\":\"current_evidence_audit\",\"task_errors\":1}\n");
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
// Track Builder receives only exact current-policy VERIFIED IDs. Historical
|
|
// v1/v2 evidence is neither replayed nor mixed, and the exact scope identity
|
|
// reuses an existing Track Set instead of publishing duplicate lineage.
|
|
uint64_t track_task_id = 0;
|
|
Lardon3DProjectDbTrackSet track_set{};
|
|
if (!build_tracks(runtime, verified_ids, fingerprint, &track_task_id, &track_set)) {
|
|
std::printf("{\"record\":\"existing_geometry_summary\",\"ok\":false,"
|
|
"\"stage\":\"track_builder.run\",\"task_errors\":1}\n");
|
|
stop_runtime(runtime);
|
|
return false;
|
|
}
|
|
|
|
const size_t created = exact_after >= exact_before ? exact_after - exact_before : 0;
|
|
std::printf(
|
|
"{\"record\":\"existing_geometry_summary\",\"ok\":true,"
|
|
"\"verifier_version\":%u,\"geometry_task_id\":%llu,"
|
|
"\"track_task_id\":%llu,\"applicable_matched_parents\":%zu,"
|
|
"\"verified\":%zu,\"rejected\":%zu,\"gvrs_before\":%zu,"
|
|
"\"gvrs_created\":%zu,\"gvrs_after\":%zu,"
|
|
"\"track_set_id\":%llu,\"track_count\":%llu,"
|
|
"\"track_set_reused\":%s,"
|
|
"\"task_errors\":0,\"upstream_replayed\":false,"
|
|
"\"mixed_verifier_lineage\":false,\"sparse_sfm_run\":false}\n",
|
|
LARDON3D_GEOMETRIC_VERIFIER_VERSION,
|
|
static_cast<unsigned long long>(geometry_task_id),
|
|
static_cast<unsigned long long>(track_task_id), applicable, verified, rejected, exact_before,
|
|
created, exact_after, static_cast<unsigned long long>(track_set.track_set_id),
|
|
static_cast<unsigned long long>(track_set.track_count),
|
|
track_task_id == 0 && track_set.track_set_id != 0 ? "true" : "false");
|
|
stop_runtime(runtime);
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char **argv) {
|
|
Options options;
|
|
if (!parse_options(argc, argv, options)) {
|
|
usage(argv[0]);
|
|
return 2;
|
|
}
|
|
Runtime runtime;
|
|
if (options.resume_geometry_existing || options.resume_pre_gv_existing ||
|
|
options.resume_candidate_existing) {
|
|
if (!prepare_existing_project(options.project_dir, runtime)) {
|
|
std::fprintf(stderr, "existing project must be absolute, normalized, and durable\n");
|
|
return 2;
|
|
}
|
|
runtime.cpu_budget = options.cpu_budget;
|
|
if (options.resume_candidate_existing)
|
|
return run_existing_candidate(runtime) ? 0 : 1;
|
|
return (options.resume_pre_gv_existing ? run_existing_pre_gv(runtime)
|
|
: run_existing_geometry(runtime)) ? 0 : 1;
|
|
}
|
|
if (!prepare_empty_project(options.project_dir, runtime.project_path)) {
|
|
std::fprintf(stderr, "project directory must be absolute, normalized and empty\n");
|
|
return 2;
|
|
}
|
|
runtime.database_path = (options.project_dir / "project.lardon3d").string();
|
|
Campaign campaign;
|
|
if (!discover_campaign(options, campaign) || !start_runtime(runtime)) {
|
|
std::fprintf(stderr, "campaign discovery or runtime setup failed\n");
|
|
return 1;
|
|
}
|
|
|
|
const char *mode_name = options.mode == Mode::kA6000 ? "a6000" : "s21";
|
|
Lardon3DProjectDbScanSet scanset{};
|
|
if (lardon3d_project_db_create_scanset(runtime.state.project_db, mode_name, &scanset) !=
|
|
LARDON3D_PROJECT_DB_OK) {
|
|
stop_runtime(runtime);
|
|
return 1;
|
|
}
|
|
uint64_t quality_task_id = 0;
|
|
uint64_t campaign_task_id = 0;
|
|
Lardon3DProjectDbSelectedExecution execution{};
|
|
bool ok = run_task_pair(runtime, scanset.scanset_id, campaign,
|
|
options.restart != RestartBoundary::kNone, quality_task_id,
|
|
campaign_task_id) &&
|
|
create_selection(runtime, options.mode, quality_task_id, campaign_task_id,
|
|
campaign.confirmations.size(), execution);
|
|
if (!ok || execution.execution_id == 0) {
|
|
stop_runtime(runtime);
|
|
return ok ? 0 : 1;
|
|
}
|
|
|
|
std::vector<uint64_t> image_ids;
|
|
ok = publish_representations(runtime, execution.execution_id, image_ids);
|
|
if (ok && options.restart == RestartBoundary::kRepresentations)
|
|
ok = restart_runtime(runtime, execution.execution_id, "representations") &&
|
|
publish_representations(runtime, execution.execution_id, image_ids);
|
|
|
|
/* ORB is run only for exact images retained by the selected execution. The
|
|
* 4096 option limit is an operational Visual Index v1 bound, not a scientific
|
|
* campaign-size rule; callers use one empty project per campaign. */
|
|
const Lardon3DFeatureExtractorParameters orb{8192, 8, 20};
|
|
std::vector<Lardon3DProjectDbFeatureSet> features;
|
|
if (ok) ok = extract_features(runtime, image_ids, orb, features);
|
|
if (ok && options.restart == RestartBoundary::kFeatures) {
|
|
ok = restart_runtime(runtime, execution.execution_id, "features") &&
|
|
publish_representations(runtime, execution.execution_id, image_ids) &&
|
|
extract_features(runtime, image_ids, orb, features);
|
|
}
|
|
|
|
uint64_t geometry_task_id = 0;
|
|
std::vector<uint64_t> verified_ids;
|
|
Lardon3DGeometricVerifierParameters verifier = lardon3d_geometric_verifier_default_parameters();
|
|
unsigned char verifier_fingerprint[32]{};
|
|
lardon3d_geometric_verifier_fingerprint(&verifier, verifier_fingerprint);
|
|
if (ok) ok = downstream(runtime, features, orb, geometry_task_id, verified_ids);
|
|
if (ok && options.restart == RestartBoundary::kGeometry) {
|
|
ok = restart_runtime(runtime, execution.execution_id, "geometry");
|
|
if (ok) ok = collect_verified_ids(runtime, verifier_fingerprint, verified_ids);
|
|
}
|
|
if (ok) ok = build_tracks(runtime, verified_ids, verifier_fingerprint);
|
|
if (ok && verified_ids.empty())
|
|
std::printf("{\"record\":\"tracks\",\"created\":false,"
|
|
"\"reason\":\"empty verified GVR set\"}\n");
|
|
|
|
/* Real A6000/S21 calibration is unavailable. The selected execution remains
|
|
* truthfully at CALIBRATION, and this runner never fabricates a scope or runs
|
|
* sparse SfM; verified geometry and tracks are the terminal evidence. */
|
|
if (ok) {
|
|
stop_runtime(runtime);
|
|
ok = start_runtime(runtime);
|
|
}
|
|
Evidence evidence{};
|
|
if (ok) ok = collect_evidence(runtime, execution.execution_id, evidence);
|
|
if (ok) {
|
|
std::printf("{\"record\":\"summary\",\"mode\":\"%s\","
|
|
"\"execution_id\":%llu,\"selected\":%zu,\"feature_sets\":%zu,"
|
|
"\"candidate_pairs\":%zu,\"matches\":%zu,\"verified\":%zu,"
|
|
"\"rejected\":%zu,\"tracks\":%zu,"
|
|
"\"orb_max_features_per_set\":8192,"
|
|
"\"visual_index_max_features_per_set\":1024,"
|
|
"\"visual_index_feature_set_limit\":4096,"
|
|
"\"candidate_top_k_limit\":64,"
|
|
"\"calibration_attached\":false,\"sparse_sfm_run\":false}\n",
|
|
mode_name, static_cast<unsigned long long>(execution.execution_id),
|
|
evidence.selected, evidence.features, evidence.pairs, evidence.matches,
|
|
evidence.verified, evidence.rejected, evidence.tracks);
|
|
}
|
|
stop_runtime(runtime);
|
|
return ok ? 0 : 1;
|
|
}
|