lardon3d/tests/photo_quality_real_scan.cpp
2026-08-28 11:57:42 +02:00

418 lines
18 KiB
C++

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <sys/resource.h>
#include <unistd.h>
#include <vector>
extern "C" {
#include <lardon3d/hardware_profile.h>
#include <lardon3d/photo_quality_task.h>
#include <lardon3d/project.h>
#include <lardon3d/task_queue.h>
}
namespace {
constexpr uint64_t kAnalysisWorkingBytes = UINT64_C(20) * 1024u * 1024u;
enum class Mode { kA6000, kS21 };
struct Options {
Mode mode{};
bool has_mode{};
std::filesystem::path project_dir;
std::vector<std::filesystem::path> roots;
};
/* This validation-only mirror keeps the reported admission estimate aligned
* with the v1 Task contract: retained request/context storage plus one bounded
* 20 MiB group analysis allowance. It does not own or execute Task state. */
struct PhotoQualityContextLayout {
char project_path[LARDON3D_APP_STATE_PATH_CAPACITY];
Lardon3DProjectDb *database;
Lardon3DResourceGovernor *governor;
uint64_t scanset_id;
std::vector<Lardon3DAcquisitionCampaignSource> sources;
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
std::vector<unsigned char> encoded;
Lardon3DAcquisitionCampaignPlan plan;
};
void usage(const char *program) {
std::fprintf(stderr,
"Usage: %s --mode a6000|s21 --project-dir ABSOLUTE_EMPTY_DIR "
"--root ABSOLUTE_DIR [--root ABSOLUTE_DIR ...]\n",
program);
}
bool parse_options(int argc, char **argv, Options &options) {
for (int i = 1; i < argc; ++i) {
const std::string argument(argv[i]);
if ((argument == "--mode" || argument == "--project-dir" || argument == "--root") &&
i + 1 >= argc)
return false;
if (argument == "--mode") {
const std::string value(argv[++i]);
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[++i];
} else if (argument == "--root") {
options.roots.emplace_back(argv[++i]);
} else if (argument == "--help") {
usage(argv[0]);
std::exit(0);
} else {
return false;
}
}
return options.has_mode && !options.project_dir.empty() && !options.roots.empty() &&
options.roots.size() <= LARDON3D_ACQUISITION_CAMPAIGN_MAX_ROOTS;
}
bool canonical_directory(const std::filesystem::path &input, std::filesystem::path &output) {
std::error_code error;
if (!input.is_absolute()) return false;
output = std::filesystem::canonical(input, error);
return !error && std::filesystem::is_directory(output, error) && !error &&
output.string().size() < LARDON3D_ACQUISITION_CAMPAIGN_PATH_CAPACITY;
}
bool prepare_project_directory(const std::filesystem::path &input,
std::filesystem::path &output) {
if (!input.is_absolute()) return false;
output = input.lexically_normal();
if (output != input || output.string().size() >= LARDON3D_APP_STATE_PATH_CAPACITY) return false;
std::error_code error;
if (std::filesystem::exists(output, error)) {
if (error || !std::filesystem::is_directory(output, error) || error ||
!std::filesystem::is_empty(output, error) || error)
return false;
} else if (!std::filesystem::create_directories(output, error) || error) {
return false;
}
return std::filesystem::create_directories(output / ".lardon3d" / "checkpoints", error) &&
!error;
}
const char *mode_name(Mode mode) { return mode == Mode::kA6000 ? "a6000" : "s21"; }
std::string 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 build_confirmations(Mode mode, const Lardon3DAcquisitionCampaignDiscovery &discovery,
std::vector<Lardon3DAcquisitionCampaignConfirmation> &confirmations) {
if (mode == Mode::kS21) {
confirmations.reserve(discovery.source_count);
for (size_t i = 0; i < discovery.source_count; ++i) {
if (discovery.sources[i].source_kind != LARDON3D_ACQUISITION_SOURCE_JPEG) return false;
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 1;
confirmation.source_indices[0] = i;
confirmations.push_back(confirmation);
}
return true;
}
struct Pair { size_t raw = SIZE_MAX; size_t jpeg = SIZE_MAX; };
std::map<std::string, Pair> pairs;
for (size_t i = 0; i < discovery.source_count; ++i) {
Pair &pair = pairs[stem(discovery.sources[i].path)];
if (discovery.sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_RAW) {
if (pair.raw != SIZE_MAX) return false;
pair.raw = i;
} else if (discovery.sources[i].source_kind == LARDON3D_ACQUISITION_SOURCE_JPEG) {
if (pair.jpeg != SIZE_MAX) return false;
pair.jpeg = i;
} else {
return false;
}
}
confirmations.reserve(pairs.size());
for (const auto &entry : pairs) {
if (entry.second.raw == SIZE_MAX || entry.second.jpeg == SIZE_MAX) return false;
Lardon3DAcquisitionCampaignConfirmation confirmation{};
confirmation.source_count = 2;
confirmation.source_indices[0] = entry.second.raw;
confirmation.source_indices[1] = entry.second.jpeg;
confirmations.push_back(confirmation);
}
return confirmations.size() * 2u == discovery.source_count;
}
void json_string(const char *text) {
std::putchar('"');
for (const unsigned char *p = reinterpret_cast<const unsigned char *>(text); *p; ++p) {
switch (*p) {
case '"': std::fputs("\\\"", stdout); break;
case '\\': std::fputs("\\\\", stdout); break;
case '\b': std::fputs("\\b", stdout); break;
case '\f': std::fputs("\\f", stdout); break;
case '\n': std::fputs("\\n", stdout); break;
case '\r': std::fputs("\\r", stdout); break;
case '\t': std::fputs("\\t", stdout); break;
default:
if (*p < 0x20u) std::printf("\\u%04x", static_cast<unsigned>(*p));
else std::putchar(*p);
}
}
std::putchar('"');
}
const char *status_name(Lardon3DPhotoQualityMetricStatus status) {
switch (status) {
case LARDON3D_PHOTO_QUALITY_METRIC_OK: return "OK";
case LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE: return "UNAVAILABLE";
case LARDON3D_PHOTO_QUALITY_METRIC_INVALID_INPUT: return "INVALID_INPUT";
case LARDON3D_PHOTO_QUALITY_METRIC_DECODE_ERROR: return "DECODE_ERROR";
}
return "UNKNOWN";
}
const char *recommendation_name(Lardon3DPhotoQualityRecommendation recommendation) {
switch (recommendation) {
case LARDON3D_PHOTO_QUALITY_GOOD: return "GOOD";
case LARDON3D_PHOTO_QUALITY_SUSPECT: return "SUSPECT";
case LARDON3D_PHOTO_QUALITY_REJECT: return "REJECT";
}
return "NONE";
}
bool wait_terminal(Lardon3DTaskQueue *queue, Lardon3DProjectDb *database, uint64_t task_id,
Lardon3DTaskSnapshot &snapshot, Lardon3DProjectDbTask &durable) {
for (;;) {
if (!lardon3d_task_queue_get(queue, task_id, &snapshot)) return false;
if (snapshot.state == TASK_COMPLETED || snapshot.state == TASK_FAILED ||
snapshot.state == TASK_CANCELLED) {
/* Queue terminal state precedes the finished callback. Report only after
* that callback has durably published the matching generic Task state and
* sequence count, so validation never samples an intermediate row. */
if (lardon3d_project_db_load_task(database, task_id, &durable) != LARDON3D_PROJECT_DB_OK)
return false;
if (durable.saved_state == snapshot.state) return true;
}
usleep(100000);
}
}
void emit_group(const Lardon3DAcquisitionCampaignDiscovery &discovery,
const Lardon3DAcquisitionCampaignGroup &group,
const Lardon3DProjectDbPhotoQualityResult &result) {
std::printf("{\"record\":\"group\",\"group_id\":%u,\"basis\":%u,\"source_indices\":[",
result.group_id, static_cast<unsigned>(group.basis));
for (size_t i = 0; i < group.source_count; ++i)
std::printf("%s%zu", i == 0 ? "" : ",", group.source_indices[i]);
std::fputs("],\"source_paths\":[", stdout);
for (size_t i = 0; i < group.source_count; ++i) {
if (i != 0) std::putchar(',');
json_string(discovery.sources[group.source_indices[i]].path);
}
std::fputs("],\"proxy_source_index\":", stdout);
if (result.proxy_source_index == UINT32_MAX) std::fputs("null,\"proxy_path\":null", stdout);
else {
std::printf("%u,\"proxy_path\":", result.proxy_source_index);
json_string(discovery.sources[result.proxy_source_index].path);
}
std::printf(",\"status\":\"%s\",\"recommendation\":\"%s\",\"reasons\":",
status_name(result.metrics.status),
recommendation_name(result.metrics.recommendation));
json_string(result.metrics.reasons);
std::printf(",\"decoded_width\":%u,\"decoded_height\":%u,\"analysis_width\":%u,"
"\"analysis_height\":%u,\"sharpness_raw\":%.17g,"
"\"sharpness_normalized\":%.17g,\"clipped_black_fraction\":%.17g,"
"\"clipped_white_fraction\":%.17g,\"contrast_raw\":%.17g,"
"\"contrast_normalized\":%.17g,\"low_texture_fraction\":%.17g}\n",
result.metrics.decoded_width, result.metrics.decoded_height,
result.metrics.analysis_width, result.metrics.analysis_height,
result.metrics.sharpness_raw, result.metrics.sharpness_normalized,
result.metrics.clipped_black_fraction, result.metrics.clipped_white_fraction,
result.metrics.contrast_raw, result.metrics.contrast_normalized,
result.metrics.low_texture_fraction);
}
} // namespace
extern "C" const Lardon3DTaskKindRegistry *lardon3d_task_kind_registry_production(void) {
return nullptr;
}
int main(int argc, char **argv) {
Options options;
if (!parse_options(argc, argv, options)) { usage(argv[0]); return 2; }
std::filesystem::path project_dir;
if (!prepare_project_directory(options.project_dir, project_dir)) {
std::fprintf(stderr, "project directory must be an absolute, normalized empty directory\n");
return 2;
}
std::vector<Lardon3DAcquisitionCampaignRoot> roots(options.roots.size());
for (size_t i = 0; i < options.roots.size(); ++i) {
std::filesystem::path canonical;
if (!canonical_directory(options.roots[i], canonical)) {
std::fprintf(stderr, "root must resolve to a bounded directory: %s\n",
options.roots[i].c_str());
return 2;
}
const std::string path = canonical.string();
std::memcpy(roots[i].path, path.c_str(), path.size() + 1u);
}
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 || discovery->source_count == 0) {
std::fprintf(stderr, "acquisition discovery failed: %u\n",
static_cast<unsigned>(discovery_result));
return 1;
}
std::vector<Lardon3DAcquisitionCampaignConfirmation> confirmations;
if (!build_confirmations(options.mode, *discovery, confirmations)) {
std::fprintf(stderr, "%s source set does not satisfy the required exact grouping shape\n",
mode_name(options.mode));
return 1;
}
auto plan = std::make_unique<Lardon3DAcquisitionCampaignPlan>();
if (lardon3d_acquisition_campaign_plan(discovery->sources, discovery->source_count,
confirmations.data(), confirmations.size(), plan.get()) !=
LARDON3D_ACQUISITION_CAMPAIGN_OK ||
plan->group_count != confirmations.size() ||
plan->summary.explicit_group_count != confirmations.size()) {
std::fprintf(stderr, "explicit campaign planning failed\n");
return 1;
}
const std::string database_path = (project_dir / "project.lardon3d").string();
char database_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]{};
Lardon3DProjectDb *database = nullptr;
if (lardon3d_project_db_open(database_path.c_str(), &database, database_error) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "project database open failed: %s\n", database_error);
return 1;
}
Lardon3DProjectDbScanSet scanset{};
if (lardon3d_project_db_create_scanset(database, mode_name(options.mode), &scanset) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "ScanSet creation failed\n");
lardon3d_project_db_close(database);
return 1;
}
Lardon3DHardwareProfile profile{};
char hardware_error[256]{};
Lardon3DResourcePolicy policy{};
if (!lardon3d_hardware_profile_detect(&profile, hardware_error, sizeof(hardware_error)) ||
!lardon3d_resource_policy_default(&profile, &policy)) {
std::fprintf(stderr, "hardware/resource policy detection failed: %s\n", hardware_error);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create(&profile, &policy);
Lardon3DTaskQueue *queue = governor ? lardon3d_task_queue_create(governor, 2) : nullptr;
if (!queue) {
std::fprintf(stderr, "Queue/Governor creation failed\n");
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DAppState state{};
lardon3d_app_state_init(&state);
state.project_loaded = true;
state.project_db = database;
state.resource_governor = governor;
state.task_queue = queue;
state.hardware_profile = profile;
const std::string project_path = project_dir.string();
std::memcpy(state.project_path, project_path.c_str(), project_path.size() + 1u);
Lardon3DPhotoQualityTaskRequest request{discovery->sources, discovery->source_count,
confirmations.data(), confirmations.size()};
size_t encoded_size = 0;
if (!lardon3d_photo_quality_request_encode(&request, nullptr, 0, &encoded_size)) {
std::fprintf(stderr, "request size calculation failed\n");
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
const uint64_t task_memory_estimate =
kAnalysisWorkingBytes + sizeof(PhotoQualityContextLayout) +
discovery->source_count * sizeof(Lardon3DAcquisitionCampaignSource) +
confirmations.size() * sizeof(Lardon3DAcquisitionCampaignConfirmation) + encoded_size;
const auto started = std::chrono::steady_clock::now();
uint64_t task_id = 0;
if (!lardon3d_project_enqueue_photo_quality(&state, scanset.scanset_id, &request, &task_id)) {
std::fprintf(stderr, "photo_quality.triage enqueue failed\n");
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
Lardon3DTaskSnapshot terminal{};
Lardon3DProjectDbTask durable_task{};
if (!wait_terminal(queue, database, task_id, terminal, durable_task) ||
terminal.state != TASK_COMPLETED) {
std::fprintf(stderr, "photo_quality.triage ended in %s: %s\n",
lardon3d_task_state_name(terminal.state), terminal.message);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
size_t good = 0, suspect = 0, reject = 0, unavailable = 0, metric_errors = 0;
for (uint32_t group_index = 0; group_index < plan->group_count; ++group_index) {
const uint32_t group_id = plan->groups[group_index].group_id;
Lardon3DProjectDbPhotoQualityResult result{};
if (lardon3d_project_db_load_photo_quality_result(database, task_id, group_id, &result) !=
LARDON3D_PROJECT_DB_OK) {
std::fprintf(stderr, "missing durable result for group %u\n", group_id);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return 1;
}
if (result.metrics.status == LARDON3D_PHOTO_QUALITY_METRIC_UNAVAILABLE) ++unavailable;
else if (result.metrics.status != LARDON3D_PHOTO_QUALITY_METRIC_OK) ++metric_errors;
if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_GOOD) ++good;
else if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_SUSPECT) ++suspect;
else if (result.metrics.recommendation == LARDON3D_PHOTO_QUALITY_REJECT) ++reject;
emit_group(*discovery, plan->groups[group_index], result);
}
struct rusage usage{};
const bool loaded_usage = getrusage(RUSAGE_SELF, &usage) == 0;
const double elapsed =
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
std::printf("{\"record\":\"summary\",\"mode\":\"%s\",\"task_id\":%llu,"
"\"sources\":%zu,\"groups\":%zu,\"good\":%zu,\"suspect\":%zu,"
"\"reject\":%zu,\"unavailable\":%zu,\"metric_errors\":%zu,"
"\"discovered_entries\":%zu,\"unsupported_entries\":%zu,"
"\"metadata_ok\":%zu,\"metadata_errors\":%zu,"
"\"request_encoded_bytes\":%zu,\"task_memory_estimate_bytes\":%llu,"
"\"sequence_count\":%u,\"elapsed_seconds\":%.6f,\"peak_rss_kib\":%ld}\n",
mode_name(options.mode), static_cast<unsigned long long>(task_id),
discovery->source_count, plan->group_count, good, suspect, reject, unavailable,
metric_errors, discovery->summary.discovered_entry_count,
discovery->summary.unsupported_entry_count, discovery->summary.metadata_ok_count,
discovery->summary.metadata_error_count, encoded_size,
static_cast<unsigned long long>(task_memory_estimate),
durable_task.sequence_count, elapsed,
loaded_usage ? usage.ru_maxrss : -1L);
lardon3d_task_queue_destroy(queue);
lardon3d_resource_governor_destroy(governor);
lardon3d_project_db_close(database);
return loaded_usage ? 0 : 1;
}