feat: materialize calibration workflow evidence
This commit is contained in:
parent
e1cf83a158
commit
6a670fd9e9
9 changed files with 1360 additions and 21 deletions
|
|
@ -5,7 +5,8 @@
|
||||||
```text
|
```text
|
||||||
CALIBRATION_WORKFLOW=IN_PROGRESS
|
CALIBRATION_WORKFLOW=IN_PROGRESS
|
||||||
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
||||||
CURRENT_WORKFLOW_NEXT=EVIDENCE_MATERIALIZATION_V1
|
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN
|
||||||
|
CURRENT_WORKFLOW_NEXT=SELECTED_EXECUTION_BINDING_V1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -84,6 +85,60 @@ Input Boundary v1 does not:
|
||||||
- invoke Calibration Bootstrap;
|
- invoke Calibration Bootstrap;
|
||||||
- change selected-execution state.
|
- change selected-execution state.
|
||||||
|
|
||||||
|
## Evidence Materialization v1
|
||||||
|
|
||||||
|
`CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN`.
|
||||||
|
|
||||||
|
The implementation is additive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/calibration_workflow_materialize.cpp
|
||||||
|
tests/test_calibration_workflow_materialize.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
It consumes only inputs that first pass Input Boundary v1 and performs no
|
||||||
|
Project DB access or mutation.
|
||||||
|
|
||||||
|
The caller owns bounded arrays for materialized views and coordinate checks.
|
||||||
|
On success the output borrows those arrays and retains:
|
||||||
|
|
||||||
|
- exact target generator SHA-256 and physical target measurements;
|
||||||
|
- measured white border and categorical planarity evidence;
|
||||||
|
- exact optical-state SHA-256;
|
||||||
|
- exact solver executable and configuration SHA-256;
|
||||||
|
- exact accepted/rejected per-view classifications and rejection reasons;
|
||||||
|
- hold-out assignment, frame region, distance band and target coverage;
|
||||||
|
- retained per-view residual counts and metrics;
|
||||||
|
- coordinate-equivalence checks derived from the retained session points;
|
||||||
|
- all three exact full-solve parameter vectors;
|
||||||
|
- exact fit parameter vector;
|
||||||
|
- support image/observation counts;
|
||||||
|
- global RMSE, maximum residual and high-residual fraction;
|
||||||
|
- hold-out RMSE and maximum residual;
|
||||||
|
- maximum parameter delta and `validation_flags=0x0f`.
|
||||||
|
|
||||||
|
The stage consumes published solver evidence; it does not reclassify views,
|
||||||
|
rerun calibration, average repeated solves or manufacture missing values.
|
||||||
|
|
||||||
|
`initialization_evidence_sha256` is the exact `session.l3dcal` SHA-256.
|
||||||
|
|
||||||
|
`validation_evidence_sha256` is deterministic and domain separated:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SHA256(
|
||||||
|
ASCII("L3DCAL_WORKFLOW_VALIDATION_V1\n")
|
||||||
|
|| detection_sha256_raw32
|
||||||
|
|| solve_sha256_raw32
|
||||||
|
|| evidence_sha256_raw32
|
||||||
|
|| producer_sha256_raw32
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
This boundary deliberately does not construct per-campaign
|
||||||
|
`Lardon3DCalibrationToolingEntry` rows. Those rows require Project DB proof of
|
||||||
|
the selected image identities, representation bytes/dimensions and explicit
|
||||||
|
Capture optical assignments, which belongs to Selected Execution Binding v1.
|
||||||
|
|
||||||
## Campaign optical-state evidence
|
## Campaign optical-state evidence
|
||||||
|
|
||||||
Project DB v23 retains exact explicit optical configuration identity, including
|
Project DB v23 retains exact explicit optical configuration identity, including
|
||||||
|
|
@ -102,21 +157,25 @@ Absence or disagreement remains `CALIBRATION_UNAVAILABLE`.
|
||||||
## Current next boundary
|
## Current next boundary
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1
|
CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING_V1
|
||||||
```
|
```
|
||||||
|
|
||||||
The next stage parses the already validated bundle into bounded in-memory
|
The next stage may read Project DB, but must not mutate it. It must:
|
||||||
Science v1 evidence:
|
|
||||||
|
|
||||||
- exact per-view evidence;
|
- load the exact selected execution and require the calibration/ready stage
|
||||||
- exact repeated full-solve parameters;
|
appropriate to the existing FROZEN contract;
|
||||||
- fit parameters;
|
- require exact selected item count and order;
|
||||||
- global and hold-out validation metrics;
|
- match every campaign-state Capture row to the selected item;
|
||||||
- coordinate-equivalence evidence;
|
- load each Capture's explicit v23 optical assignment and require the exact
|
||||||
- immutable provenance digests.
|
declared optical configuration;
|
||||||
|
- load every selected image/asset identity;
|
||||||
|
- read the exact managed representation as a bounded regular file;
|
||||||
|
- require asset byte size and SHA-256 equality;
|
||||||
|
- decode the exact geometric representation and require oriented dimensions
|
||||||
|
compatible with the calibration evidence;
|
||||||
|
- construct the per-image `Lardon3DCalibrationToolingEntry` rows without
|
||||||
|
changing any scientific value.
|
||||||
|
|
||||||
It still performs no Project DB mutation.
|
Only after that read-only binding passes may a final workflow boundary invoke
|
||||||
|
the FROZEN Tooling/Bootstrap path and transition the selected execution to
|
||||||
Only after that boundary passes may the workflow bind the exact selected
|
truthful `READY`.
|
||||||
execution, campaign representations and optical assignments and invoke the
|
|
||||||
FROZEN Tooling/Bootstrap path.
|
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,8 @@ The solver session contract now also requires explicit measured `white_border >=
|
||||||
|
|
||||||
`CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`: the workflow now has a bounded non-mutating input boundary for session, solver bundle and campaign optical-state evidence. It rejects special/symlink/oversize files, invalid JSON, provenance digest mismatches and incompatible optical state. The next boundary is Evidence Materialization v1.
|
`CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN`: the workflow now has a bounded non-mutating input boundary for session, solver bundle and campaign optical-state evidence. It rejects special/symlink/oversize files, invalid JSON, provenance digest mismatches and incompatible optical state. The next boundary is Evidence Materialization v1.
|
||||||
|
|
||||||
|
`CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN`: the validated external calibration inputs are now materialized into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without Project DB access or mutation. The next boundary is exact Selected Execution Binding v1 before any Tooling/Bootstrap import.
|
||||||
|
|
||||||
A bounded Tooling correction aligned planarity handling with Calibration Science v1: Science v1
|
A bounded Tooling correction aligned planarity handling with Calibration Science v1: Science v1
|
||||||
defines a categorical physical planarity attestation, not a numeric flatness threshold. Tooling
|
defines a categorical physical planarity attestation, not a numeric flatness threshold. Tooling
|
||||||
therefore rejects invented finite `target_flatness_mm` values. `L3DCALB1` v1 and Calibration
|
therefore rejects invented finite `target_flatness_mm` values. `L3DCALB1` v1 and Calibration
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <lardon3d/calibration_tooling.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -72,6 +74,69 @@ Lardon3DCalibrationWorkflowResult lardon3d_calibration_workflow_validate_input_b
|
||||||
const Lardon3DCalibrationWorkflowInputFiles *files,
|
const Lardon3DCalibrationWorkflowInputFiles *files,
|
||||||
Lardon3DCalibrationWorkflowInputBoundary *boundary);
|
Lardon3DCalibrationWorkflowInputBoundary *boundary);
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_SOURCE_SIZE = 1,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODE = 2,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_DECODED_DIMENSIONS = 3,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INSUFFICIENT_CHARUCO = 4,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_INVALID_CHARUCO_ID = 5,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_OCCUPANCY = 6,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_TARGET_PHYSICAL_QUADRANTS = 7,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_CLIPPING = 8,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_PRE_SOLVE_CORNER_RMS = 9,
|
||||||
|
LARDON3D_CALIBRATION_WORKFLOW_REJECTION_COORDINATE_EQUIVALENCE = 10,
|
||||||
|
} Lardon3DCalibrationWorkflowRejectionReason;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DCalibrationWorkflowInputBoundary boundary;
|
||||||
|
unsigned char target_sha256[32];
|
||||||
|
unsigned char optical_state_sha256[32];
|
||||||
|
unsigned char solver_executable_sha256[32];
|
||||||
|
unsigned char solver_configuration_sha256[32];
|
||||||
|
unsigned char initialization_evidence_sha256[32];
|
||||||
|
unsigned char validation_evidence_sha256[32];
|
||||||
|
uint32_t target_family;
|
||||||
|
uint32_t target_squares_x;
|
||||||
|
uint32_t target_squares_y;
|
||||||
|
double target_square_length_mm;
|
||||||
|
double target_marker_length_mm;
|
||||||
|
double target_active_width_mm;
|
||||||
|
double target_active_height_mm;
|
||||||
|
double target_white_border_mm;
|
||||||
|
double target_measurements_mm[LARDON3D_CALIBRATION_TOOLING_TARGET_MEASUREMENTS];
|
||||||
|
double measurement_resolution_mm;
|
||||||
|
double target_flatness_mm;
|
||||||
|
double holdout_rmse_px;
|
||||||
|
double holdout_maximum_residual_px;
|
||||||
|
uint32_t extra_distortion_coefficient_count;
|
||||||
|
const Lardon3DCalibrationToolingView *views;
|
||||||
|
size_t view_count;
|
||||||
|
const Lardon3DCalibrationToolingCoordinateCheck *coordinate_checks;
|
||||||
|
size_t coordinate_check_count;
|
||||||
|
double repeated_parameters[3][8];
|
||||||
|
double fit_parameters[8];
|
||||||
|
uint32_t support_images;
|
||||||
|
uint32_t support_observations;
|
||||||
|
double reprojection_rmse_px;
|
||||||
|
double maximum_residual_px;
|
||||||
|
double high_residual_fraction;
|
||||||
|
double maximum_parameter_delta;
|
||||||
|
uint32_t validation_flags;
|
||||||
|
} Lardon3DCalibrationWorkflowExternalEvidence;
|
||||||
|
|
||||||
|
/* Materialize the already validated external session and solver bundle into
|
||||||
|
* bounded Science-v1 evidence. Caller owns `views` and `coordinate_checks`;
|
||||||
|
* output borrows those arrays on success. This stage performs no Project DB
|
||||||
|
* access, no Tooling import and no selected-execution mutation. */
|
||||||
|
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);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
19
meson.build
19
meson.build
|
|
@ -181,6 +181,7 @@ lardon3d_app = executable(
|
||||||
'src/calibration_bootstrap.c',
|
'src/calibration_bootstrap.c',
|
||||||
'src/calibration_tooling.c',
|
'src/calibration_tooling.c',
|
||||||
'src/calibration_workflow.cpp',
|
'src/calibration_workflow.cpp',
|
||||||
|
'src/calibration_workflow_materialize.cpp',
|
||||||
'src/sparse_sfm_geometry.cpp',
|
'src/sparse_sfm_geometry.cpp',
|
||||||
'src/sparse_sfm_incremental.cpp',
|
'src/sparse_sfm_incremental.cpp',
|
||||||
'src/sparse_sfm_bundle_adjustment.cpp',
|
'src/sparse_sfm_bundle_adjustment.cpp',
|
||||||
|
|
@ -908,6 +909,24 @@ calibration_workflow_test = executable(
|
||||||
|
|
||||||
test('calibration-workflow', calibration_workflow_test, timeout: 30)
|
test('calibration-workflow', calibration_workflow_test, timeout: 30)
|
||||||
|
|
||||||
|
|
||||||
|
calibration_workflow_materialization_test = executable(
|
||||||
|
'test-calibration-workflow-materialization',
|
||||||
|
sources: [
|
||||||
|
'tests/test_calibration_workflow_materialize.cpp',
|
||||||
|
'src/calibration_workflow.cpp',
|
||||||
|
'src/calibration_workflow_materialize.cpp',
|
||||||
|
],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [openssl],
|
||||||
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'calibration-workflow-materialization',
|
||||||
|
calibration_workflow_materialization_test,
|
||||||
|
timeout: 30,
|
||||||
|
)
|
||||||
|
|
||||||
optical_profiles_test = executable(
|
optical_profiles_test = executable(
|
||||||
'test-optical-profiles',
|
'test-optical-profiles',
|
||||||
sources: [
|
sources: [
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
CURRENT_PROJECT_DB_SCHEMA=v25
|
CURRENT_PROJECT_DB_SCHEMA=v25
|
||||||
PRODUCTION_TASK_KINDS=16
|
PRODUCTION_TASK_KINDS=16
|
||||||
USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS
|
USER_FACING_UI_LANGUAGE_NORMALIZATION=PASS
|
||||||
CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION
|
CURRENT_IMPLEMENTATION_CURSOR=1_CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -32,6 +32,7 @@ Calibration solver producer identity PASS/FROZEN
|
||||||
Calibration solver per-view evidence PASS/FROZEN
|
Calibration solver per-view evidence PASS/FROZEN
|
||||||
Calibration solver bundle repair PASS/FROZEN
|
Calibration solver bundle repair PASS/FROZEN
|
||||||
Calibration workflow input boundary PASS/FROZEN
|
Calibration workflow input boundary PASS/FROZEN
|
||||||
|
Calibration workflow evidence materialization PASS/FROZEN
|
||||||
Calibration Tooling planarity alignment PASS/FROZEN
|
Calibration Tooling planarity alignment PASS/FROZEN
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ CALIBRATION_SOLVER_PER_VIEW_EVIDENCE_V1=PASS/FROZEN
|
||||||
CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN
|
CALIBRATION_SOLVER_BUNDLE_REPAIR_V1=PASS/FROZEN
|
||||||
CALIBRATION_WORKFLOW=IN_PROGRESS
|
CALIBRATION_WORKFLOW=IN_PROGRESS
|
||||||
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
CALIBRATION_WORKFLOW_INPUT_BOUNDARY_V1=PASS/FROZEN
|
||||||
CURRENT_CALIBRATION_NEXT=WORKFLOW_EVIDENCE_MATERIALIZATION_V1
|
CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS/FROZEN
|
||||||
|
CURRENT_CALIBRATION_NEXT=WORKFLOW_SELECTED_EXECUTION_BINDING_V1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -23,7 +24,7 @@ CURRENT_CALIBRATION_NEXT=WORKFLOW_EVIDENCE_MATERIALIZATION_V1
|
||||||
|
|
||||||
`docs/architecture/calibration_tooling.md` is the specialized Tooling authority.
|
`docs/architecture/calibration_tooling.md` is the specialized Tooling authority.
|
||||||
|
|
||||||
`docs/architecture/calibration_workflow.md` is the specialized workflow authority. The public API remains `include/lardon3d/calibration_tooling.h`.
|
`docs/architecture/calibration_workflow.md` is the specialized workflow authority. Its public API is `include/lardon3d/calibration_workflow.h`; the FROZEN Tooling API remains `include/lardon3d/calibration_tooling.h`.
|
||||||
|
|
||||||
A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS <sha256>` evidence is bound through immutable initialization evidence.
|
A bounded corrective review established that Calibration Science v1 defines target planarity as a categorical physical attestation, not a numeric flatness tolerance. Tooling preserves its public structure layout while requiring `target_flatness_mm` to be NaN, so callers cannot invent a millimetre measurement. The canonical session's `planarity PASS <sha256>` evidence is bound through immutable initialization evidence.
|
||||||
|
|
||||||
|
|
@ -57,7 +58,7 @@ dedicated physical calibration acquisition
|
||||||
-> real Sparse SfM
|
-> real Sparse SfM
|
||||||
```
|
```
|
||||||
|
|
||||||
The workflow coordinator is now implemented through its first bounded checkpoint. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality without Project DB mutation. The current implementation gap is Evidence Materialization v1. It consumes the immutable `session.l3dcal` plus `detection.json`, `solve.json` and `evidence.json`, binds them to the exact selected execution and optical state, constructs the bounded Tooling evidence and never manufactures missing physical evidence.
|
The workflow coordinator now has two PASS/FROZEN non-mutating checkpoints. Input Boundary v1 validates immutable files, hashes, formats and complete optical-state equality. Evidence Materialization v1 parses the retained session and solver bundle into bounded Science v1 target, per-view, coordinate, repeated-solve, fit, residual, hold-out and provenance evidence without opening Project DB. The current implementation gap is Selected Execution Binding v1: bind this external evidence to the exact selected execution, Capture optical assignments and campaign representation bytes, then construct the per-image Tooling entries. Missing or mismatched evidence is rejected; nothing is inferred.
|
||||||
|
|
||||||
## REQUIRED_PRODUCT_TARGET
|
## REQUIRED_PRODUCT_TARGET
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN
|
IMPLEMENTATION_ORDER=DEPENDENCY_DRIVEN
|
||||||
IMPLEMENTATION_AUTHORIZATION=NO
|
IMPLEMENTATION_AUTHORIZATION=NO
|
||||||
STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS
|
STEP_0_USER_FACING_LANGUAGE_NORMALIZATION=PASS
|
||||||
CURRENT_NEXT=1_CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION
|
CURRENT_NEXT=1_CALIBRATION_WORKFLOW_SELECTED_EXECUTION_BINDING
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authority
|
## Authority
|
||||||
|
|
@ -20,7 +20,7 @@ Implementation remains unauthorized until the human explicitly authorizes a tran
|
||||||
Default dependency order:
|
Default dependency order:
|
||||||
|
|
||||||
0. user-facing repository/UI language normalization where appropriate — PASS;
|
0. user-facing repository/UI language normalization where appropriate — PASS;
|
||||||
1. final usable calibration workflow — IN PROGRESS; Input Boundary v1 PASS/FROZEN; current next sub-boundary: Evidence Materialization v1;
|
1. final usable calibration workflow — IN PROGRESS; Input Boundary v1 and Evidence Materialization v1 PASS/FROZEN; current next sub-boundary: Selected Execution Binding v1;
|
||||||
2. dedicated physical calibrated real campaign;
|
2. dedicated physical calibrated real campaign;
|
||||||
3. real Sparse SfM proof;
|
3. real Sparse SfM proof;
|
||||||
4. durable Dense/OpenMVS orchestration;
|
4. durable Dense/OpenMVS orchestration;
|
||||||
|
|
|
||||||
925
src/calibration_workflow_materialize.cpp
Normal file
925
src/calibration_workflow_materialize.cpp
Normal file
|
|
@ -0,0 +1,925 @@
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
267
tests/test_calibration_workflow_materialize.cpp
Normal file
267
tests/test_calibration_workflow_materialize.cpp
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
|
||||||
|
#include <lardon3d/calibration_workflow.h>
|
||||||
|
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define CHECK(x) do { if(!(x)){ std::fprintf(stderr,"materialize failure %d: %s\n",__LINE__,#x); return false; } } while(0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string hex_repeat(char c) { return std::string(64, c); }
|
||||||
|
|
||||||
|
std::string hf(double value) {
|
||||||
|
std::ostringstream o;
|
||||||
|
o.imbue(std::locale::classic());
|
||||||
|
o << std::hexfloat << value;
|
||||||
|
return o.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_text(const std::filesystem::path& path, const std::string& text) {
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out << text;
|
||||||
|
return static_cast<bool>(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sha_file(const std::filesystem::path& path, unsigned char out[32]) {
|
||||||
|
std::ifstream in(path, std::ios::binary);
|
||||||
|
std::ostringstream bytes;
|
||||||
|
bytes << in.rdbuf();
|
||||||
|
const std::string data = bytes.str();
|
||||||
|
unsigned int n = 0;
|
||||||
|
return in && EVP_Digest(data.data(), data.size(), out, &n, EVP_sha256(), nullptr) == 1 &&
|
||||||
|
n == 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sha_file_hex(const std::filesystem::path& path) {
|
||||||
|
unsigned char value[32]{};
|
||||||
|
if (!sha_file(path, value)) return {};
|
||||||
|
static const char digits[] = "0123456789abcdef";
|
||||||
|
std::string out(64, '0');
|
||||||
|
for (size_t i = 0; i < 32; ++i) {
|
||||||
|
out[2 * i] = digits[value[i] >> 4];
|
||||||
|
out[2 * i + 1] = digits[value[i] & 15];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
std::filesystem::path root, session, detection, solve, evidence, producer, campaign;
|
||||||
|
Lardon3DCalibrationWorkflowInputFiles files{};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string coordinate_lines(const std::string& sha, double distance, unsigned band) {
|
||||||
|
static const std::array<const char*, 9> labels = {
|
||||||
|
"center","top","right","bottom","left","top_left","top_right","bottom_left","bottom_right"};
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "pre_solve " << sha << " 0.1\n"
|
||||||
|
<< "clipping " << sha << " 0\n"
|
||||||
|
<< "coordinate " << sha << " qualified_decoder 1 0 1000 800 20 0 0\n";
|
||||||
|
for (int i = 0; i < 20; ++i)
|
||||||
|
s << "coordinate_point " << sha << " " << labels[static_cast<size_t>(i)%labels.size()]
|
||||||
|
<< " " << i << " " << i+1 << " " << i << " " << i+1 << "\n";
|
||||||
|
s << "distance " << sha << " " << std::setprecision(17)
|
||||||
|
<< std::defaultfloat << distance << " " << band << "\n";
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string detection_view(const std::string& sha, unsigned region, unsigned band,
|
||||||
|
double distance, bool last) {
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "{\"source_sha256\":\"" << sha
|
||||||
|
<< "\",\"orientation\":0,\"oriented_width\":1000,\"oriented_height\":800"
|
||||||
|
<< ",\"decision\":\"accepted\",\"reason\":\"-\""
|
||||||
|
<< ",\"frame_region\":" << region
|
||||||
|
<< ",\"distance_band\":" << band
|
||||||
|
<< ",\"holdout\":false"
|
||||||
|
<< ",\"target_occupancy\":\"" << hf(.3)
|
||||||
|
<< "\",\"normal_angle_degrees\":\"" << hf(25.0)
|
||||||
|
<< "\",\"measured_distance_metres\":\"" << hf(distance)
|
||||||
|
<< "\",\"pre_solve_corner_rms_px\":\"" << hf(.1)
|
||||||
|
<< "\",\"clipping_fraction\":\"" << hf(0.0)
|
||||||
|
<< "\",\"physical_target_quadrants\":3"
|
||||||
|
<< ",\"target_corner_quadrant_mask\":7"
|
||||||
|
<< ",\"corner_count\":20,\"residual_count\":20,\"high_residual_count\":0"
|
||||||
|
<< ",\"reprojection_rmse_px\":\"" << hf(.1)
|
||||||
|
<< "\",\"maximum_residual_px\":\"" << hf(.2)
|
||||||
|
<< "\",\"coordinate_equivalence\":{\"comparison_points\":20"
|
||||||
|
<< ",\"max_abs_dx_px\":\"" << hf(0.0)
|
||||||
|
<< "\",\"max_abs_dy_px\":\"" << hf(0.0)
|
||||||
|
<< "\",\"pass\":true},\"corners\":[";
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
if (i) s << ",";
|
||||||
|
s << "{\"id\":" << i << ",\"x\":\"" << hf(100.0+i)
|
||||||
|
<< "\",\"y\":\"" << hf(200.0+i) << "\"}";
|
||||||
|
}
|
||||||
|
s << "]}" << (last ? "\n" : " ,\n");
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string params(double first = 1000.0) {
|
||||||
|
const double p[8] = {first,1001,500,400,.01,-.01,.001,-.001};
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "[";
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
if (i) s << ",";
|
||||||
|
s << "\"" << hf(p[i]) << "\"";
|
||||||
|
}
|
||||||
|
s << "]";
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pose() {
|
||||||
|
return "{\"rvec\":[\"" + hf(0) + "\",\"" + hf(0) + "\",\"" + hf(0) +
|
||||||
|
"\"],\"tvec_m\":[\"" + hf(0) + "\",\"" + hf(0) + "\",\"" + hf(1) + "\"]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string solve_text(bool mismatch) {
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "{\n\"format\":\"L3DCAL_SOLVE_V1\",\n\"runs\":[\n";
|
||||||
|
for (int run = 0; run < 3; ++run) {
|
||||||
|
s << "{\"run\":" << run << ",\"params\":" << params(mismatch && run==1 ? 999.0 : 1000.0)
|
||||||
|
<< ",\"opencv_rms_px\":\"" << hf(.1) << "\",\"poses\":["
|
||||||
|
<< pose() << "," << pose() << "]}" << (run==2 ? "\n" : " ,\n");
|
||||||
|
}
|
||||||
|
s << "],\n\"fit_params\":" << params() << "\n}\n";
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string evidence_text(const std::string& optical,
|
||||||
|
const std::string& generator,
|
||||||
|
const std::string& planarity,
|
||||||
|
const std::string& a,
|
||||||
|
const std::string& b) {
|
||||||
|
std::ostringstream s;
|
||||||
|
s << "{\n\"format\":\"L3DCAL_EVIDENCE_BUNDLE_V1\",\n"
|
||||||
|
<< "\"target\":{\"id\":\"board\",\"generator_sha256\":\"" << generator
|
||||||
|
<< "\",\"instrument\":\"caliper\",\"resolution_mm\":\"" << hf(.1)
|
||||||
|
<< "\",\"planarity_evidence_sha256\":\"" << planarity << "\"},\n"
|
||||||
|
<< "\"optical_sha256\":\"" << optical << "\",\n"
|
||||||
|
<< "\"optical_state\":\"body_objective_zoom_focus_stabilization_format_pipeline\",\n"
|
||||||
|
<< "\"validation_flags\":\"0xf\",\n"
|
||||||
|
<< "\"global_rmse_px\":\"" << hf(.1)
|
||||||
|
<< "\",\"maximum_residual_px\":\"" << hf(.2)
|
||||||
|
<< "\",\"high_residual_fraction\":\"" << hf(0.0) << "\",\n"
|
||||||
|
<< "\"holdout\":{\"rmse_px\":\"" << hf(.1)
|
||||||
|
<< "\",\"maximum_px\":\"" << hf(.2) << "\"},\n"
|
||||||
|
<< "\"maximum_parameter_delta_px\":\"" << hf(0.0) << "\",\n"
|
||||||
|
<< "\"deterministic_full_solve_equality\":true,\n\"residuals\":[";
|
||||||
|
bool first = true;
|
||||||
|
for (const std::string* sha : {&a, &b}) {
|
||||||
|
for (int i = 0; i < 20; ++i) {
|
||||||
|
if (!first) s << ",";
|
||||||
|
first = false;
|
||||||
|
s << "{\"source_sha256\":\"" << *sha << "\",\"corner_id\":" << i
|
||||||
|
<< ",\"dx_px\":\"" << hf(.1) << "\",\"dy_px\":\"" << hf(0.0)
|
||||||
|
<< "\",\"rmse_px\":\"" << hf(.1) << "\"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s << "]\n}\n";
|
||||||
|
return s.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool make_fixture(Fixture *f, bool mismatch_solve = false) {
|
||||||
|
char temp[]="/tmp/lardon3d-materialize-XXXXXX";
|
||||||
|
char *r=mkdtemp(temp); if(!r) return false;
|
||||||
|
f->root=r; f->session=f->root/"session"; f->detection=f->root/"detection";
|
||||||
|
f->solve=f->root/"solve"; f->evidence=f->root/"evidence";
|
||||||
|
f->producer=f->root/"producer"; f->campaign=f->root/"campaign";
|
||||||
|
const std::string optical=hex_repeat('1'), generator=hex_repeat('2'),
|
||||||
|
planarity=hex_repeat('3'), a=hex_repeat('4'), b=hex_repeat('5');
|
||||||
|
std::ostringstream session;
|
||||||
|
session << "L3DCAL_SESSION_V1\n"
|
||||||
|
<< "target board " << generator << " DICT_5X5_100 9 7 30 21\n"
|
||||||
|
<< "measurement caliper 0.1 30 30 30 30 30 30 30 30 30 30\n"
|
||||||
|
<< "white_border 30\n"
|
||||||
|
<< "planarity PASS " << planarity << "\n"
|
||||||
|
<< "decoder qualified_decoder 1\n"
|
||||||
|
<< "optical_state " << optical << " body_objective_zoom_focus_stabilization_format_pipeline\n"
|
||||||
|
<< "image /a " << a << " 0\n"
|
||||||
|
<< "image /b " << b << " 0\n"
|
||||||
|
<< coordinate_lines(a,.3,0) << coordinate_lines(b,.5,1);
|
||||||
|
if(!write_text(f->session,session.str())) return false;
|
||||||
|
if(!write_text(f->detection,
|
||||||
|
"{\n\"format\":\"L3DCAL_DETECTION_V1\",\n\"decoder\":\"qualified_decoder\",\n"
|
||||||
|
"\"decoder_version\":\"1\",\n\"views\":[\n" +
|
||||||
|
detection_view(a,0,0,.3,false)+detection_view(b,1,1,.5,true)+"]\n}\n")) return false;
|
||||||
|
if(!write_text(f->solve,solve_text(mismatch_solve))) return false;
|
||||||
|
if(!write_text(f->evidence,evidence_text(optical,generator,planarity,a,b))) return false;
|
||||||
|
const std::string session_sha = sha_file_hex(f->session);
|
||||||
|
if (session_sha.empty()) return false;
|
||||||
|
if(!write_text(f->producer,
|
||||||
|
"{\n\"format\":\"L3DCAL_PRODUCER_V1\",\n"
|
||||||
|
"\"solver_executable_sha256\":\"" + hex_repeat('a') + "\",\n"
|
||||||
|
"\"solver_configuration_sha256\":\"" + hex_repeat('b') + "\",\n"
|
||||||
|
"\"session_sha256\":\"" + session_sha + "\",\n"
|
||||||
|
"\"opencv_version\":\"5.0.0\",\n"
|
||||||
|
"\"opencv_build_sha256\":\"" + hex_repeat('c') + "\",\n"
|
||||||
|
"\"threads\":1,\n\"rng_seed\":1278432342,\n"
|
||||||
|
"\"optical_sha256\":\"" + optical + "\"\n}\n")) return false;
|
||||||
|
if(!write_text(f->campaign,
|
||||||
|
"L3DCAL_CAMPAIGN_STATE_V1\n"
|
||||||
|
"execution 42\n"
|
||||||
|
"optical_configuration 7\n"
|
||||||
|
"optical_state " + optical +
|
||||||
|
" body_objective_zoom_focus_stabilization_format_pipeline\n"
|
||||||
|
"capture 0 101\ncapture 1 102\n")) return false;
|
||||||
|
|
||||||
|
f->files={f->session.c_str(),f->detection.c_str(),f->solve.c_str(),
|
||||||
|
f->evidence.c_str(),f->producer.c_str(),f->campaign.c_str()};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid() {
|
||||||
|
Fixture f; CHECK(make_fixture(&f));
|
||||||
|
Lardon3DCalibrationToolingView views[2]{};
|
||||||
|
Lardon3DCalibrationToolingCoordinateCheck checks[40]{};
|
||||||
|
Lardon3DCalibrationWorkflowExternalEvidence out{};
|
||||||
|
CHECK(lardon3d_calibration_workflow_materialize_external_evidence(
|
||||||
|
&f.files,views,2,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_OK);
|
||||||
|
CHECK(out.view_count==2 && out.coordinate_check_count==40);
|
||||||
|
CHECK(out.support_images==2 && out.support_observations==40);
|
||||||
|
CHECK(out.validation_flags==15 && out.target_white_border_mm==30.0);
|
||||||
|
CHECK(out.repeated_parameters[0][0]==1000.0 && out.fit_parameters[0]==1000.0);
|
||||||
|
CHECK(views[0].accepted==1 && views[0].corner_count==20 &&
|
||||||
|
views[1].distance_band==1);
|
||||||
|
CHECK(checks[0].dx_px==0.0 && checks[0].dy_px==0.0);
|
||||||
|
unsigned char session_sha[32]{};
|
||||||
|
CHECK(sha_file(f.session, session_sha));
|
||||||
|
CHECK(std::memcmp(out.initialization_evidence_sha256, session_sha, 32) == 0);
|
||||||
|
std::filesystem::remove_all(f.root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool capacity_and_mismatch() {
|
||||||
|
Fixture f; CHECK(make_fixture(&f));
|
||||||
|
Lardon3DCalibrationToolingView views[2]{};
|
||||||
|
Lardon3DCalibrationToolingCoordinateCheck checks[40]{};
|
||||||
|
Lardon3DCalibrationWorkflowExternalEvidence out{};
|
||||||
|
CHECK(lardon3d_calibration_workflow_materialize_external_evidence(
|
||||||
|
&f.files,views,1,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_CAPACITY);
|
||||||
|
std::filesystem::remove_all(f.root);
|
||||||
|
|
||||||
|
CHECK(make_fixture(&f,true));
|
||||||
|
CHECK(lardon3d_calibration_workflow_materialize_external_evidence(
|
||||||
|
&f.files,views,2,checks,40,&out)==LARDON3D_CALIBRATION_WORKFLOW_PROVENANCE_MISMATCH);
|
||||||
|
std::filesystem::remove_all(f.root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(){
|
||||||
|
if(!valid()||!capacity_and_mismatch()) return 1;
|
||||||
|
std::puts("CALIBRATION_WORKFLOW_EVIDENCE_MATERIALIZATION_V1=PASS");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue