925 lines
35 KiB
C++
925 lines
35 KiB
C++
|
|
#include <lardon3d/calibration_workflow.h>
|
|
|
|
#include <openssl/evp.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <fcntl.h>
|
|
#include <locale.h>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
constexpr size_t kShaBytes = 32;
|
|
constexpr size_t kMaxObservations =
|
|
LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS * 48u;
|
|
|
|
struct ShaLess {
|
|
bool operator()(const std::array<unsigned char, 32>& a,
|
|
const std::array<unsigned char, 32>& b) const {
|
|
return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end());
|
|
}
|
|
};
|
|
|
|
|
|
bool hex_digit(char c, unsigned *value) {
|
|
if (c >= '0' && c <= '9') *value = static_cast<unsigned>(c - '0');
|
|
else if (c >= 'a' && c <= 'f') *value = static_cast<unsigned>(c - 'a' + 10);
|
|
else if (c >= 'A' && c <= 'F') *value = static_cast<unsigned>(c - 'A' + 10);
|
|
else return false;
|
|
return true;
|
|
}
|
|
|
|
bool parse_sha(std::string_view text, unsigned char output[32]) {
|
|
if (text.size() != 64) return false;
|
|
for (size_t i = 0; i < 32; ++i) {
|
|
unsigned hi = 0, lo = 0;
|
|
if (!hex_digit(text[2 * i], &hi) || !hex_digit(text[2 * i + 1], &lo))
|
|
return false;
|
|
output[i] = static_cast<unsigned char>((hi << 4u) | lo);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::array<unsigned char, 32> sha_array(const unsigned char value[32]) {
|
|
std::array<unsigned char, 32> out{};
|
|
std::memcpy(out.data(), value, 32);
|
|
return out;
|
|
}
|
|
|
|
bool digest(const unsigned char *data, size_t size, unsigned char output[32]) {
|
|
unsigned int length = 0;
|
|
return EVP_Digest(data, size, output, &length, EVP_sha256(), nullptr) == 1 &&
|
|
length == 32;
|
|
}
|
|
|
|
Lardon3DCalibrationWorkflowResult read_verified(
|
|
const char *path, const unsigned char expected[32], std::string *output) {
|
|
if (!path || !expected || !output)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
|
|
int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOFOLLOW);
|
|
if (fd < 0) {
|
|
if (errno == ELOOP) return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
|
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
}
|
|
struct stat st{};
|
|
if (fstat(fd, &st) != 0) {
|
|
close(fd);
|
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
}
|
|
if (!S_ISREG(st.st_mode)) {
|
|
close(fd);
|
|
return LARDON3D_CALIBRATION_WORKFLOW_NON_REGULAR_FILE;
|
|
}
|
|
if (st.st_size <= 0) {
|
|
close(fd);
|
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
|
}
|
|
if (static_cast<uint64_t>(st.st_size) >
|
|
LARDON3D_CALIBRATION_WORKFLOW_MAX_FILE_BYTES) {
|
|
close(fd);
|
|
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
|
|
}
|
|
const size_t size = static_cast<size_t>(st.st_size);
|
|
output->assign(size, '\0');
|
|
size_t used = 0;
|
|
while (used < size) {
|
|
ssize_t n = pread(fd, output->data() + used, size - used,
|
|
static_cast<off_t>(used));
|
|
if (n < 0 && errno == EINTR) continue;
|
|
if (n <= 0) {
|
|
close(fd);
|
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
}
|
|
used += static_cast<size_t>(n);
|
|
}
|
|
if (close(fd) != 0) return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
unsigned char actual[32]{};
|
|
if (!digest(reinterpret_cast<const unsigned char *>(output->data()),
|
|
output->size(), actual))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
if (std::memcmp(actual, expected, 32) != 0)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
|
return LARDON3D_CALIBRATION_WORKFLOW_OK;
|
|
}
|
|
|
|
bool parse_u64(std::string_view text, uint64_t *value) {
|
|
if (!value || text.empty()) return false;
|
|
uint64_t v = 0;
|
|
auto result = std::from_chars(text.data(), text.data() + text.size(), v, 10);
|
|
if (result.ec != std::errc() || result.ptr != text.data() + text.size())
|
|
return false;
|
|
*value = v;
|
|
return true;
|
|
}
|
|
|
|
bool parse_u32(std::string_view text, uint32_t *value) {
|
|
uint64_t v = 0;
|
|
if (!parse_u64(text, &v) || v > UINT32_MAX) return false;
|
|
*value = static_cast<uint32_t>(v);
|
|
return true;
|
|
}
|
|
|
|
bool parse_double_c(std::string_view text, double *value) {
|
|
if (!value || text.empty() || text.size() > 128) return false;
|
|
char buffer[129];
|
|
std::memcpy(buffer, text.data(), text.size());
|
|
buffer[text.size()] = '\0';
|
|
locale_t locale = newlocale(LC_NUMERIC_MASK, "C", static_cast<locale_t>(0));
|
|
if (!locale) return false;
|
|
char *end = nullptr;
|
|
errno = 0;
|
|
double v = strtod_l(buffer, &end, locale);
|
|
freelocale(locale);
|
|
if (errno == ERANGE || end != buffer + text.size() || !std::isfinite(v))
|
|
return false;
|
|
*value = v;
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string_view> words(std::string_view line) {
|
|
std::vector<std::string_view> out;
|
|
size_t at = 0;
|
|
while (at < line.size()) {
|
|
while (at < line.size() && (line[at] == ' ' || line[at] == '\t')) ++at;
|
|
if (at == line.size()) break;
|
|
size_t end = at;
|
|
while (end < line.size() && line[end] != ' ' && line[end] != '\t') ++end;
|
|
out.push_back(line.substr(at, end - at));
|
|
at = end;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool token_ok(std::string_view token) {
|
|
if (token.empty() ||
|
|
token.size() >= LARDON3D_CALIBRATION_WORKFLOW_OPTICAL_STATE_TOKEN_CAPACITY)
|
|
return false;
|
|
for (unsigned char c : token) {
|
|
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.' ||
|
|
c == ':'))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct SessionView {
|
|
std::array<unsigned char, 32> sha{};
|
|
uint32_t orientation = 0;
|
|
double pre_solve = -1.0;
|
|
double clipping = -1.0;
|
|
double distance = -1.0;
|
|
uint32_t distance_band = UINT32_MAX;
|
|
uint32_t coordinate_orientation = UINT32_MAX;
|
|
uint32_t coordinate_width = 0;
|
|
uint32_t coordinate_height = 0;
|
|
uint32_t coordinate_count = 0;
|
|
unsigned coordinate_coverage = 0;
|
|
std::vector<Lardon3DCalibrationToolingCoordinateCheck> checks;
|
|
};
|
|
|
|
struct SessionData {
|
|
std::string target_id;
|
|
std::string instrument;
|
|
std::string decoder;
|
|
std::string decoder_version;
|
|
std::string optical_token;
|
|
unsigned char generator_sha[32]{};
|
|
unsigned char planarity_sha[32]{};
|
|
unsigned char optical_sha[32]{};
|
|
double measurements[10]{};
|
|
double measurement_resolution = 0.0;
|
|
double white_border = 0.0;
|
|
std::vector<SessionView> views;
|
|
};
|
|
|
|
int coverage_bit(std::string_view label) {
|
|
static const std::array<std::string_view, 9> labels = {
|
|
"center", "top", "right", "bottom", "left",
|
|
"top_left", "top_right", "bottom_left", "bottom_right"};
|
|
for (size_t i = 0; i < labels.size(); ++i)
|
|
if (label == labels[i]) return static_cast<int>(i);
|
|
return -1;
|
|
}
|
|
|
|
bool parse_session_materialization(std::string_view text, SessionData *out) {
|
|
if (!out || text.empty() || text.back() != '\n' ||
|
|
text.find('\0') != std::string_view::npos ||
|
|
text.find('\r') != std::string_view::npos)
|
|
return false;
|
|
std::map<std::array<unsigned char, 32>, size_t, ShaLess> index;
|
|
bool target = false, measurement = false, border = false, planarity = false;
|
|
bool decoder = false, optical = false;
|
|
size_t at = 0, line_number = 0;
|
|
while (at < text.size()) {
|
|
size_t end = text.find('\n', at);
|
|
if (end == std::string_view::npos) return false;
|
|
std::string_view line = text.substr(at, end - at);
|
|
at = end + 1;
|
|
++line_number;
|
|
if (line_number == 1) {
|
|
if (line != "L3DCAL_SESSION_V1") return false;
|
|
continue;
|
|
}
|
|
if (line.empty()) continue;
|
|
auto w = words(line);
|
|
if (w.empty()) return false;
|
|
if (w[0] == "target") {
|
|
uint32_t sx = 0, sy = 0;
|
|
double square = 0.0, marker = 0.0;
|
|
if (target || w.size() != 8 || !token_ok(w[1]) ||
|
|
!parse_sha(w[2], out->generator_sha) ||
|
|
w[3] != "DICT_5X5_100" || !parse_u32(w[4], &sx) ||
|
|
!parse_u32(w[5], &sy) || !parse_double_c(w[6], &square) ||
|
|
!parse_double_c(w[7], &marker) || sx != 9 || sy != 7 ||
|
|
square != 30.0 || marker != 21.0)
|
|
return false;
|
|
out->target_id.assign(w[1]);
|
|
target = true;
|
|
} else if (w[0] == "measurement") {
|
|
if (measurement || w.size() != 13 || !token_ok(w[1]) ||
|
|
!parse_double_c(w[2], &out->measurement_resolution))
|
|
return false;
|
|
out->instrument.assign(w[1]);
|
|
for (size_t i = 0; i < 10; ++i)
|
|
if (!parse_double_c(w[i + 3], &out->measurements[i])) return false;
|
|
measurement = true;
|
|
} else if (w[0] == "white_border") {
|
|
if (border || w.size() != 2 || !parse_double_c(w[1], &out->white_border))
|
|
return false;
|
|
border = true;
|
|
} else if (w[0] == "planarity") {
|
|
if (planarity || w.size() != 3 || w[1] != "PASS" ||
|
|
!parse_sha(w[2], out->planarity_sha))
|
|
return false;
|
|
planarity = true;
|
|
} else if (w[0] == "decoder") {
|
|
if (decoder || w.size() != 3 || !token_ok(w[1]) || !token_ok(w[2]))
|
|
return false;
|
|
out->decoder.assign(w[1]);
|
|
out->decoder_version.assign(w[2]);
|
|
decoder = true;
|
|
} else if (w[0] == "optical_state") {
|
|
if (optical || w.size() != 3 || !parse_sha(w[1], out->optical_sha) ||
|
|
!token_ok(w[2]) || w[2] == "UNKNOWN")
|
|
return false;
|
|
out->optical_token.assign(w[2]);
|
|
optical = true;
|
|
} else if (w[0] == "image") {
|
|
if (w.size() != 4 || w[1].empty() || w[1].size() > 4096) return false;
|
|
SessionView view;
|
|
if (!parse_sha(w[2], view.sha.data()) || !parse_u32(w[3], &view.orientation) ||
|
|
(view.orientation != 0 && view.orientation != 90 &&
|
|
view.orientation != 180 && view.orientation != 270))
|
|
return false;
|
|
auto inserted = index.emplace(view.sha, out->views.size());
|
|
if (!inserted.second ||
|
|
out->views.size() >= LARDON3D_CALIBRATION_WORKFLOW_MAX_SELECTED_ITEMS)
|
|
return false;
|
|
out->views.push_back(view);
|
|
} else if (w[0] == "pre_solve" || w[0] == "clipping" ||
|
|
w[0] == "distance" || w[0] == "coordinate" ||
|
|
w[0] == "coordinate_point") {
|
|
if (w.size() < 2) return false;
|
|
std::array<unsigned char, 32> sha{};
|
|
if (!parse_sha(w[1], sha.data())) return false;
|
|
auto found = index.find(sha);
|
|
if (found == index.end()) return false;
|
|
SessionView& view = out->views[found->second];
|
|
if (w[0] == "pre_solve") {
|
|
if (w.size() != 3 || view.pre_solve >= 0 ||
|
|
!parse_double_c(w[2], &view.pre_solve))
|
|
return false;
|
|
} else if (w[0] == "clipping") {
|
|
if (w.size() != 3 || view.clipping >= 0 ||
|
|
!parse_double_c(w[2], &view.clipping))
|
|
return false;
|
|
} else if (w[0] == "distance") {
|
|
if (w.size() != 4 || view.distance > 0 ||
|
|
!parse_double_c(w[2], &view.distance) ||
|
|
!parse_u32(w[3], &view.distance_band) ||
|
|
view.distance <= 0 || view.distance_band > 2)
|
|
return false;
|
|
} else if (w[0] == "coordinate") {
|
|
double initial_dx = 0.0, initial_dy = 0.0;
|
|
if (w.size() != 10 || view.coordinate_count != 0 ||
|
|
w[2] != out->decoder || w[3] != out->decoder_version ||
|
|
!parse_u32(w[4], &view.coordinate_orientation) ||
|
|
!parse_u32(w[5], &view.coordinate_width) ||
|
|
!parse_u32(w[6], &view.coordinate_height) ||
|
|
!parse_u32(w[7], &view.coordinate_count) ||
|
|
!parse_double_c(w[8], &initial_dx) ||
|
|
!parse_double_c(w[9], &initial_dy) ||
|
|
view.coordinate_orientation != view.orientation ||
|
|
view.coordinate_width == 0 || view.coordinate_height == 0 ||
|
|
view.coordinate_count < 20 || view.coordinate_count > 48 ||
|
|
initial_dx < 0 || initial_dy < 0)
|
|
return false;
|
|
view.checks.reserve(view.coordinate_count);
|
|
} else {
|
|
double sx = 0.0, sy = 0.0, fx = 0.0, fy = 0.0;
|
|
if (w.size() != 7 || view.coordinate_count == 0 ||
|
|
view.checks.size() >= view.coordinate_count ||
|
|
!parse_double_c(w[3], &sx) || !parse_double_c(w[4], &sy) ||
|
|
!parse_double_c(w[5], &fx) || !parse_double_c(w[6], &fy))
|
|
return false;
|
|
int bit = coverage_bit(w[2]);
|
|
if (bit < 0) return false;
|
|
view.coordinate_coverage |= 1u << static_cast<unsigned>(bit);
|
|
Lardon3DCalibrationToolingCoordinateCheck check{};
|
|
std::memcpy(check.source_sha256, view.sha.data(), 32);
|
|
check.orientation_degrees = view.orientation;
|
|
check.dx_px = sx - fx;
|
|
check.dy_px = sy - fy;
|
|
view.checks.push_back(check);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
if (!target || !measurement || !border || !planarity || !decoder || !optical ||
|
|
out->views.empty())
|
|
return false;
|
|
for (const SessionView& view : out->views) {
|
|
if (view.pre_solve < 0 || view.clipping < 0 || view.distance <= 0 ||
|
|
view.distance_band > 2 || view.coordinate_count < 20 ||
|
|
view.checks.size() != view.coordinate_count ||
|
|
view.coordinate_coverage != 0x1ffu)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class JsonCursor {
|
|
public:
|
|
explicit JsonCursor(std::string_view text) : text_(text) {}
|
|
void ws() {
|
|
while (at_ < text_.size() &&
|
|
(text_[at_] == ' ' || text_[at_] == '\n' ||
|
|
text_[at_] == '\r' || text_[at_] == '\t'))
|
|
++at_;
|
|
}
|
|
bool ch(char expected) {
|
|
ws();
|
|
if (at_ >= text_.size() || text_[at_] != expected) return false;
|
|
++at_;
|
|
return true;
|
|
}
|
|
bool key(std::string_view expected) {
|
|
std::string value;
|
|
return string(&value) && value == expected && ch(':');
|
|
}
|
|
bool string(std::string *output) {
|
|
ws();
|
|
if (at_ >= text_.size() || text_[at_] != '"') return false;
|
|
++at_;
|
|
output->clear();
|
|
while (at_ < text_.size()) {
|
|
unsigned char c = static_cast<unsigned char>(text_[at_++]);
|
|
if (c == '"') return true;
|
|
if (c < 0x20 || c == '\\') return false;
|
|
output->push_back(static_cast<char>(c));
|
|
}
|
|
return false;
|
|
}
|
|
bool u64(uint64_t *output) {
|
|
ws();
|
|
size_t start = at_;
|
|
if (start >= text_.size() || text_[start] < '0' || text_[start] > '9')
|
|
return false;
|
|
while (at_ < text_.size() && text_[at_] >= '0' && text_[at_] <= '9') ++at_;
|
|
return parse_u64(text_.substr(start, at_ - start), output);
|
|
}
|
|
bool u32(uint32_t *output) {
|
|
uint64_t v = 0;
|
|
if (!u64(&v) || v > UINT32_MAX) return false;
|
|
*output = static_cast<uint32_t>(v);
|
|
return true;
|
|
}
|
|
bool boolean(bool *output) {
|
|
ws();
|
|
if (text_.substr(at_, 4) == "true") {
|
|
at_ += 4; *output = true; return true;
|
|
}
|
|
if (text_.substr(at_, 5) == "false") {
|
|
at_ += 5; *output = false; return true;
|
|
}
|
|
return false;
|
|
}
|
|
bool hex_double(double *output) {
|
|
std::string value;
|
|
return string(&value) && parse_double_c(value, output);
|
|
}
|
|
bool done() {
|
|
ws();
|
|
return at_ == text_.size();
|
|
}
|
|
private:
|
|
std::string_view text_;
|
|
size_t at_ = 0;
|
|
};
|
|
|
|
bool comma(JsonCursor *j) { return j->ch(','); }
|
|
|
|
uint32_t rejection_reason(std::string_view reason) {
|
|
static const std::array<std::string_view, 10> reasons = {
|
|
"source_size", "decode", "decoded_dimensions", "insufficient_charuco",
|
|
"invalid_charuco_id", "occupancy", "target_physical_quadrants",
|
|
"clipping", "pre_solve_corner_rms", "coordinate_equivalence"};
|
|
for (size_t i = 0; i < reasons.size(); ++i)
|
|
if (reason == reasons[i]) return static_cast<uint32_t>(i + 1);
|
|
return 0;
|
|
}
|
|
|
|
struct DetectionView {
|
|
Lardon3DCalibrationToolingView tooling{};
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
bool coordinate_pass = false;
|
|
std::set<uint32_t> corner_ids;
|
|
};
|
|
|
|
bool parse_detection(std::string_view text, const SessionData& session,
|
|
std::vector<DetectionView> *views) {
|
|
JsonCursor j(text);
|
|
std::string format, decoder, decoder_version;
|
|
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
|
|
format != "L3DCAL_DETECTION_V1" || !comma(&j) ||
|
|
!j.key("decoder") || !j.string(&decoder) || decoder != session.decoder ||
|
|
!comma(&j) || !j.key("decoder_version") ||
|
|
!j.string(&decoder_version) || decoder_version != session.decoder_version ||
|
|
!comma(&j) || !j.key("views") || !j.ch('['))
|
|
return false;
|
|
|
|
std::map<std::array<unsigned char, 32>, size_t, ShaLess> session_index;
|
|
for (size_t i = 0; i < session.views.size(); ++i)
|
|
session_index.emplace(session.views[i].sha, i);
|
|
std::set<std::array<unsigned char, 32>, ShaLess> seen;
|
|
std::array<unsigned char, 32> previous{};
|
|
bool have_previous = false;
|
|
|
|
j.ws();
|
|
if (!j.ch(']')) {
|
|
while (true) {
|
|
DetectionView dv;
|
|
std::string sha_text, decision, reason;
|
|
uint32_t physical_quadrants = 0, comparison_points = 0;
|
|
double coordinate_dx = 0.0, coordinate_dy = 0.0;
|
|
bool coordinate_pass = false, holdout = false;
|
|
if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) ||
|
|
!parse_sha(sha_text, dv.tooling.source_sha256) || !comma(&j) ||
|
|
!j.key("orientation") || !j.u32(&dv.tooling.orientation_degrees) ||
|
|
!comma(&j) || !j.key("oriented_width") || !j.u32(&dv.width) ||
|
|
!comma(&j) || !j.key("oriented_height") || !j.u32(&dv.height) ||
|
|
!comma(&j) || !j.key("decision") || !j.string(&decision) ||
|
|
!comma(&j) || !j.key("reason") || !j.string(&reason) ||
|
|
!comma(&j) || !j.key("frame_region") || !j.u32(&dv.tooling.quadrant) ||
|
|
!comma(&j) || !j.key("distance_band") ||
|
|
!j.u32(&dv.tooling.distance_band) || !comma(&j) ||
|
|
!j.key("holdout") || !j.boolean(&holdout) || !comma(&j) ||
|
|
!j.key("target_occupancy") || !j.hex_double(&dv.tooling.target_occupancy) ||
|
|
!comma(&j) || !j.key("normal_angle_degrees") ||
|
|
!j.hex_double(&dv.tooling.normal_angle_degrees) || !comma(&j) ||
|
|
!j.key("measured_distance_metres") ||
|
|
!j.hex_double(&dv.tooling.distance_metres) || !comma(&j) ||
|
|
!j.key("pre_solve_corner_rms_px") ||
|
|
!j.hex_double(&dv.tooling.corner_rms_px) || !comma(&j) ||
|
|
!j.key("clipping_fraction") ||
|
|
!j.hex_double(&dv.tooling.clipped_fraction) || !comma(&j) ||
|
|
!j.key("physical_target_quadrants") || !j.u32(&physical_quadrants) ||
|
|
!comma(&j) || !j.key("target_corner_quadrant_mask") ||
|
|
!j.u32(&dv.tooling.target_corner_quadrant_mask) || !comma(&j) ||
|
|
!j.key("corner_count") || !j.u32(&dv.tooling.corner_count) ||
|
|
!comma(&j) || !j.key("residual_count") ||
|
|
!j.u32(&dv.tooling.residual_count) || !comma(&j) ||
|
|
!j.key("high_residual_count") ||
|
|
!j.u32(&dv.tooling.high_residual_count) || !comma(&j) ||
|
|
!j.key("reprojection_rmse_px") ||
|
|
!j.hex_double(&dv.tooling.reprojection_rmse_px) || !comma(&j) ||
|
|
!j.key("maximum_residual_px") ||
|
|
!j.hex_double(&dv.tooling.maximum_residual_px) || !comma(&j) ||
|
|
!j.key("coordinate_equivalence") || !j.ch('{') ||
|
|
!j.key("comparison_points") || !j.u32(&comparison_points) ||
|
|
!comma(&j) || !j.key("max_abs_dx_px") || !j.hex_double(&coordinate_dx) ||
|
|
!comma(&j) || !j.key("max_abs_dy_px") || !j.hex_double(&coordinate_dy) ||
|
|
!comma(&j) || !j.key("pass") || !j.boolean(&coordinate_pass) ||
|
|
!j.ch('}') || !comma(&j) || !j.key("corners") || !j.ch('['))
|
|
return false;
|
|
|
|
auto key = sha_array(dv.tooling.source_sha256);
|
|
auto sit = session_index.find(key);
|
|
if (sit == session_index.end() || !seen.insert(key).second) return false;
|
|
if (have_previous &&
|
|
!std::lexicographical_compare(previous.begin(), previous.end(),
|
|
key.begin(), key.end()))
|
|
return false;
|
|
previous = key;
|
|
have_previous = true;
|
|
const SessionView& sv = session.views[sit->second];
|
|
|
|
size_t corner_count = 0;
|
|
j.ws();
|
|
if (!j.ch(']')) {
|
|
while (true) {
|
|
uint32_t id = 0;
|
|
double x = 0.0, y = 0.0;
|
|
if (!j.ch('{') || !j.key("id") || !j.u32(&id) || !comma(&j) ||
|
|
!j.key("x") || !j.hex_double(&x) || !comma(&j) ||
|
|
!j.key("y") || !j.hex_double(&y) || !j.ch('}') ||
|
|
!dv.corner_ids.insert(id).second)
|
|
return false;
|
|
++corner_count;
|
|
j.ws();
|
|
if (j.ch(']')) break;
|
|
if (!comma(&j)) return false;
|
|
}
|
|
}
|
|
if (!j.ch('}')) return false;
|
|
|
|
dv.tooling.accepted = decision == "accepted" ? 1u : 0u;
|
|
if (decision != "accepted" && decision != "rejected") return false;
|
|
if (dv.tooling.accepted) {
|
|
if (reason != "-" || !coordinate_pass || dv.width == 0 || dv.height == 0 ||
|
|
dv.tooling.orientation_degrees != sv.orientation ||
|
|
dv.tooling.corner_rms_px != sv.pre_solve ||
|
|
dv.tooling.distance_metres != sv.distance ||
|
|
dv.tooling.distance_band != sv.distance_band ||
|
|
comparison_points != sv.coordinate_count ||
|
|
dv.tooling.corner_count != corner_count ||
|
|
dv.tooling.residual_count != dv.tooling.corner_count)
|
|
return false;
|
|
} else {
|
|
dv.tooling.rejection_reason = rejection_reason(reason);
|
|
if (reason == "-" || dv.tooling.rejection_reason == 0 || holdout)
|
|
return false;
|
|
}
|
|
dv.tooling.holdout = holdout ? 1u : 0u;
|
|
unsigned pop = 0;
|
|
for (unsigned bit = 0; bit < 4; ++bit)
|
|
pop += (dv.tooling.target_corner_quadrant_mask >> bit) & 1u;
|
|
if (physical_quadrants != pop ||
|
|
dv.tooling.high_residual_count > dv.tooling.residual_count ||
|
|
coordinate_dx < 0 || coordinate_dy < 0)
|
|
return false;
|
|
dv.coordinate_pass = coordinate_pass;
|
|
views->push_back(std::move(dv));
|
|
j.ws();
|
|
if (j.ch(']')) break;
|
|
if (!comma(&j)) return false;
|
|
}
|
|
}
|
|
if (!j.ch('}') || !j.done() || views->size() != session.views.size())
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool parse_parameter_array(JsonCursor *j, double output[8]) {
|
|
if (!j->ch('[')) return false;
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
if (!j->hex_double(&output[i])) return false;
|
|
if (i + 1 != 8 && !comma(j)) return false;
|
|
}
|
|
return j->ch(']');
|
|
}
|
|
|
|
bool parse_vec3(JsonCursor *j) {
|
|
if (!j->ch('[')) return false;
|
|
for (size_t i = 0; i < 3; ++i) {
|
|
double value = 0.0;
|
|
if (!j->hex_double(&value)) return false;
|
|
if (i + 1 != 3 && !comma(j)) return false;
|
|
}
|
|
return j->ch(']');
|
|
}
|
|
|
|
struct SolveData {
|
|
double repeated[3][8]{};
|
|
double fit[8]{};
|
|
};
|
|
|
|
bool parse_solve(std::string_view text, size_t accepted_count, SolveData *out) {
|
|
JsonCursor j(text);
|
|
std::string format;
|
|
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
|
|
format != "L3DCAL_SOLVE_V1" || !comma(&j) ||
|
|
!j.key("runs") || !j.ch('['))
|
|
return false;
|
|
for (uint32_t run = 0; run < 3; ++run) {
|
|
uint32_t run_id = UINT32_MAX;
|
|
double opencv_rms = 0.0;
|
|
if (!j.ch('{') || !j.key("run") || !j.u32(&run_id) || run_id != run ||
|
|
!comma(&j) || !j.key("params") ||
|
|
!parse_parameter_array(&j, out->repeated[run]) || !comma(&j) ||
|
|
!j.key("opencv_rms_px") || !j.hex_double(&opencv_rms) ||
|
|
opencv_rms < 0 || !comma(&j) || !j.key("poses") || !j.ch('['))
|
|
return false;
|
|
size_t poses = 0;
|
|
j.ws();
|
|
if (!j.ch(']')) {
|
|
while (true) {
|
|
if (!j.ch('{') || !j.key("rvec") || !parse_vec3(&j) ||
|
|
!comma(&j) || !j.key("tvec_m") || !parse_vec3(&j) || !j.ch('}'))
|
|
return false;
|
|
++poses;
|
|
j.ws();
|
|
if (j.ch(']')) break;
|
|
if (!comma(&j)) return false;
|
|
}
|
|
}
|
|
if (poses != accepted_count || !j.ch('}')) return false;
|
|
if (run != 2 && !comma(&j)) return false;
|
|
}
|
|
if (!j.ch(']') || !comma(&j) || !j.key("fit_params") ||
|
|
!parse_parameter_array(&j, out->fit) || !j.ch('}') || !j.done())
|
|
return false;
|
|
for (size_t run = 1; run < 3; ++run)
|
|
if (std::memcmp(out->repeated[0], out->repeated[run],
|
|
sizeof(out->repeated[0])) != 0)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
struct EvidenceData {
|
|
double global_rmse = 0.0;
|
|
double global_max = 0.0;
|
|
double high_fraction = 0.0;
|
|
double holdout_rmse = 0.0;
|
|
double holdout_max = 0.0;
|
|
double maximum_delta = 0.0;
|
|
uint32_t flags = 0;
|
|
};
|
|
|
|
bool parse_flags(std::string_view text, uint32_t *flags) {
|
|
if (text.size() != 3 || text[0] != '0' || text[1] != 'x') return false;
|
|
unsigned v = 0;
|
|
if (!hex_digit(text[2], &v) || v > UINT32_MAX) return false;
|
|
*flags = static_cast<uint32_t>(v);
|
|
return true;
|
|
}
|
|
|
|
bool parse_evidence(std::string_view text, const SessionData& session,
|
|
const std::vector<DetectionView>& views,
|
|
EvidenceData *out) {
|
|
JsonCursor j(text);
|
|
std::string format, target_id, generator, instrument, planarity, optical,
|
|
optical_state, flags_text;
|
|
double resolution = 0.0;
|
|
bool deterministic = false;
|
|
if (!j.ch('{') || !j.key("format") || !j.string(&format) ||
|
|
format != "L3DCAL_EVIDENCE_BUNDLE_V1" || !comma(&j) ||
|
|
!j.key("target") || !j.ch('{') ||
|
|
!j.key("id") || !j.string(&target_id) || !comma(&j) ||
|
|
!j.key("generator_sha256") || !j.string(&generator) || !comma(&j) ||
|
|
!j.key("instrument") || !j.string(&instrument) || !comma(&j) ||
|
|
!j.key("resolution_mm") || !j.hex_double(&resolution) || !comma(&j) ||
|
|
!j.key("planarity_evidence_sha256") || !j.string(&planarity) ||
|
|
!j.ch('}') || !comma(&j) ||
|
|
!j.key("optical_sha256") || !j.string(&optical) || !comma(&j) ||
|
|
!j.key("optical_state") || !j.string(&optical_state) || !comma(&j) ||
|
|
!j.key("validation_flags") || !j.string(&flags_text) || !comma(&j) ||
|
|
!j.key("global_rmse_px") || !j.hex_double(&out->global_rmse) ||
|
|
!comma(&j) || !j.key("maximum_residual_px") ||
|
|
!j.hex_double(&out->global_max) || !comma(&j) ||
|
|
!j.key("high_residual_fraction") || !j.hex_double(&out->high_fraction) ||
|
|
!comma(&j) || !j.key("holdout") || !j.ch('{') ||
|
|
!j.key("rmse_px") || !j.hex_double(&out->holdout_rmse) || !comma(&j) ||
|
|
!j.key("maximum_px") || !j.hex_double(&out->holdout_max) ||
|
|
!j.ch('}') || !comma(&j) ||
|
|
!j.key("maximum_parameter_delta_px") ||
|
|
!j.hex_double(&out->maximum_delta) || !comma(&j) ||
|
|
!j.key("deterministic_full_solve_equality") ||
|
|
!j.boolean(&deterministic) || !comma(&j) ||
|
|
!j.key("residuals") || !j.ch('['))
|
|
return false;
|
|
|
|
unsigned char generator_sha[32]{}, planarity_sha[32]{}, optical_sha[32]{};
|
|
if (target_id != session.target_id || instrument != session.instrument ||
|
|
resolution != session.measurement_resolution ||
|
|
!parse_sha(generator, generator_sha) ||
|
|
!parse_sha(planarity, planarity_sha) || !parse_sha(optical, optical_sha) ||
|
|
std::memcmp(generator_sha, session.generator_sha, 32) != 0 ||
|
|
std::memcmp(planarity_sha, session.planarity_sha, 32) != 0 ||
|
|
std::memcmp(optical_sha, session.optical_sha, 32) != 0 ||
|
|
optical_state != session.optical_token ||
|
|
!parse_flags(flags_text, &out->flags) || out->flags != 0x0fu ||
|
|
!deterministic || out->global_rmse < 0 || out->global_max < 0 ||
|
|
out->high_fraction < 0 || out->high_fraction > 1 ||
|
|
out->holdout_rmse < 0 || out->holdout_max < 0 ||
|
|
out->maximum_delta < 0)
|
|
return false;
|
|
|
|
std::map<std::array<unsigned char, 32>, size_t, ShaLess> view_index;
|
|
for (size_t i = 0; i < views.size(); ++i)
|
|
view_index.emplace(sha_array(views[i].tooling.source_sha256), i);
|
|
std::vector<uint32_t> counts(views.size(), 0), highs(views.size(), 0);
|
|
std::set<std::pair<size_t, uint32_t>> residual_ids;
|
|
uint64_t total = 0, total_high = 0;
|
|
|
|
j.ws();
|
|
if (!j.ch(']')) {
|
|
while (true) {
|
|
std::string sha_text;
|
|
uint32_t corner_id = 0;
|
|
double dx = 0.0, dy = 0.0, rmse = 0.0;
|
|
if (!j.ch('{') || !j.key("source_sha256") || !j.string(&sha_text) ||
|
|
!comma(&j) || !j.key("corner_id") || !j.u32(&corner_id) ||
|
|
!comma(&j) || !j.key("dx_px") || !j.hex_double(&dx) ||
|
|
!comma(&j) || !j.key("dy_px") || !j.hex_double(&dy) ||
|
|
!comma(&j) || !j.key("rmse_px") || !j.hex_double(&rmse) ||
|
|
!j.ch('}'))
|
|
return false;
|
|
unsigned char sha[32]{};
|
|
if (!parse_sha(sha_text, sha)) return false;
|
|
auto found = view_index.find(sha_array(sha));
|
|
if (found == view_index.end()) return false;
|
|
size_t vi = found->second;
|
|
const DetectionView& dv = views[vi];
|
|
if (!dv.tooling.accepted || dv.corner_ids.find(corner_id) == dv.corner_ids.end() ||
|
|
!residual_ids.emplace(vi, corner_id).second ||
|
|
rmse != dv.tooling.reprojection_rmse_px)
|
|
return false;
|
|
++counts[vi];
|
|
++total;
|
|
const double squared = dx * dx + dy * dy;
|
|
if (!std::isfinite(squared)) return false;
|
|
if (squared > 1.0) {
|
|
++highs[vi];
|
|
++total_high;
|
|
}
|
|
if (total > kMaxObservations) return false;
|
|
j.ws();
|
|
if (j.ch(']')) break;
|
|
if (!comma(&j)) return false;
|
|
}
|
|
}
|
|
if (!j.ch('}') || !j.done() || total == 0) return false;
|
|
for (size_t i = 0; i < views.size(); ++i) {
|
|
if (views[i].tooling.accepted) {
|
|
if (counts[i] != views[i].tooling.residual_count ||
|
|
highs[i] != views[i].tooling.high_residual_count)
|
|
return false;
|
|
} else if (counts[i] != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
const double calculated_fraction =
|
|
static_cast<double>(total_high) / static_cast<double>(total);
|
|
if (calculated_fraction != out->high_fraction) return false;
|
|
return true;
|
|
}
|
|
|
|
bool validation_binding(const Lardon3DCalibrationWorkflowInputBoundary& boundary,
|
|
unsigned char output[32]) {
|
|
static const char domain[] = "L3DCAL_WORKFLOW_VALIDATION_V1\n";
|
|
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
|
|
if (!ctx) return false;
|
|
bool ok = EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) == 1 &&
|
|
EVP_DigestUpdate(ctx, domain, sizeof(domain) - 1) == 1 &&
|
|
EVP_DigestUpdate(ctx, boundary.detection_sha256, 32) == 1 &&
|
|
EVP_DigestUpdate(ctx, boundary.solve_sha256, 32) == 1 &&
|
|
EVP_DigestUpdate(ctx, boundary.evidence_sha256, 32) == 1 &&
|
|
EVP_DigestUpdate(ctx, boundary.producer_sha256, 32) == 1;
|
|
unsigned int length = 0;
|
|
ok = ok && EVP_DigestFinal_ex(ctx, output, &length) == 1 && length == 32;
|
|
EVP_MD_CTX_free(ctx);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" Lardon3DCalibrationWorkflowResult
|
|
lardon3d_calibration_workflow_materialize_external_evidence(
|
|
const Lardon3DCalibrationWorkflowInputFiles *files,
|
|
Lardon3DCalibrationToolingView *views, size_t view_capacity,
|
|
Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks,
|
|
size_t coordinate_check_capacity,
|
|
Lardon3DCalibrationWorkflowExternalEvidence *output) {
|
|
if (!files || !views || !coordinate_checks || !output)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_INVALID_ARGUMENT;
|
|
std::memset(output, 0, sizeof(*output));
|
|
|
|
Lardon3DCalibrationWorkflowInputBoundary boundary{};
|
|
Lardon3DCalibrationWorkflowResult result =
|
|
lardon3d_calibration_workflow_validate_input_boundary(files, &boundary);
|
|
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
|
|
|
|
std::string session_text, detection_text, solve_text, evidence_text;
|
|
const struct {
|
|
const char *path;
|
|
const unsigned char *sha;
|
|
std::string *text;
|
|
} inputs[] = {
|
|
{files->session_path, boundary.session_sha256, &session_text},
|
|
{files->detection_path, boundary.detection_sha256, &detection_text},
|
|
{files->solve_path, boundary.solve_sha256, &solve_text},
|
|
{files->evidence_path, boundary.evidence_sha256, &evidence_text},
|
|
};
|
|
for (const auto& input : inputs) {
|
|
result = read_verified(input.path, input.sha, input.text);
|
|
if (result != LARDON3D_CALIBRATION_WORKFLOW_OK) return result;
|
|
}
|
|
|
|
SessionData session;
|
|
if (!parse_session_materialization(session_text, &session))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_MALFORMED_EVIDENCE;
|
|
if (std::memcmp(session.optical_sha, boundary.optical_state_sha256, 32) != 0 ||
|
|
session.optical_token != boundary.optical_state_token)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
|
|
|
std::vector<DetectionView> parsed_views;
|
|
if (!parse_detection(detection_text, session, &parsed_views))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
|
|
|
size_t accepted = 0;
|
|
uint64_t support_observations = 0;
|
|
for (const DetectionView& view : parsed_views) {
|
|
if (view.tooling.accepted) {
|
|
++accepted;
|
|
support_observations += view.tooling.residual_count;
|
|
if (support_observations > UINT32_MAX)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
|
|
}
|
|
}
|
|
|
|
SolveData solve;
|
|
if (!parse_solve(solve_text, accepted, &solve))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
|
EvidenceData evidence;
|
|
if (!parse_evidence(evidence_text, session, parsed_views, &evidence))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH;
|
|
|
|
size_t check_count = 0;
|
|
for (const SessionView& view : session.views) {
|
|
if (SIZE_MAX - check_count < view.checks.size())
|
|
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
|
|
check_count += view.checks.size();
|
|
}
|
|
if (parsed_views.size() > view_capacity ||
|
|
check_count > coordinate_check_capacity)
|
|
return LARDON3D_CALIBRATION_WORKFLOW_CAPACITY;
|
|
|
|
for (size_t i = 0; i < parsed_views.size(); ++i)
|
|
views[i] = parsed_views[i].tooling;
|
|
size_t at = 0;
|
|
for (const SessionView& view : session.views)
|
|
for (const auto& check : view.checks) coordinate_checks[at++] = check;
|
|
|
|
output->boundary = boundary;
|
|
std::memcpy(output->target_sha256, session.generator_sha, 32);
|
|
std::memcpy(output->optical_state_sha256, boundary.optical_state_sha256, 32);
|
|
std::memcpy(output->solver_executable_sha256,
|
|
boundary.solver_executable_sha256, 32);
|
|
std::memcpy(output->solver_configuration_sha256,
|
|
boundary.solver_configuration_sha256, 32);
|
|
std::memcpy(output->initialization_evidence_sha256,
|
|
boundary.session_sha256, 32);
|
|
if (!validation_binding(boundary, output->validation_evidence_sha256))
|
|
return LARDON3D_CALIBRATION_WORKFLOW_IO_ERROR;
|
|
|
|
output->target_family =
|
|
LARDON3D_CALIBRATION_TOOLING_TARGET_CHARUCO_9X7_DICT_5X5_100;
|
|
output->target_squares_x = 9;
|
|
output->target_squares_y = 7;
|
|
output->target_square_length_mm = 30.0;
|
|
output->target_marker_length_mm = 21.0;
|
|
output->target_active_width_mm = 270.0;
|
|
output->target_active_height_mm = 210.0;
|
|
output->target_white_border_mm = session.white_border;
|
|
for (size_t i = 0; i < 10; ++i)
|
|
output->target_measurements_mm[i] = session.measurements[i];
|
|
output->measurement_resolution_mm = session.measurement_resolution;
|
|
output->target_flatness_mm = NAN;
|
|
output->holdout_rmse_px = evidence.holdout_rmse;
|
|
output->holdout_maximum_residual_px = evidence.holdout_max;
|
|
output->extra_distortion_coefficient_count = 0;
|
|
output->views = views;
|
|
output->view_count = parsed_views.size();
|
|
output->coordinate_checks = coordinate_checks;
|
|
output->coordinate_check_count = check_count;
|
|
std::memcpy(output->repeated_parameters, solve.repeated,
|
|
sizeof(output->repeated_parameters));
|
|
std::memcpy(output->fit_parameters, solve.fit,
|
|
sizeof(output->fit_parameters));
|
|
output->support_images = static_cast<uint32_t>(accepted);
|
|
output->support_observations = static_cast<uint32_t>(support_observations);
|
|
output->reprojection_rmse_px = evidence.global_rmse;
|
|
output->maximum_residual_px = evidence.global_max;
|
|
output->high_residual_fraction = evidence.high_fraction;
|
|
output->maximum_parameter_delta = evidence.maximum_delta;
|
|
output->validation_flags = evidence.flags;
|
|
return LARDON3D_CALIBRATION_WORKFLOW_OK;
|
|
}
|