feat: add calibration autofocus study evidence
This commit is contained in:
parent
34c6a82b35
commit
524f08d1b3
9 changed files with 727 additions and 3 deletions
149
docs/architecture/calibration_af_study.md
Normal file
149
docs/architecture/calibration_af_study.md
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
# Calibration AF Study Evidence v1
|
||||
|
||||
**Status: PASS / FROZEN.**
|
||||
|
||||
This boundary is an offline scientific-evidence helper for Calibration Science
|
||||
v2. It does not perform camera calibration, Project DB mutation, autofocus
|
||||
control, calibration selection, or physical-validity decisions.
|
||||
|
||||
## Purpose
|
||||
|
||||
Project DB v27 can persist a physically validated exact-token focus domain, but
|
||||
it deliberately does not decide whether a set of focus observations is
|
||||
physically compatible.
|
||||
|
||||
`Calibration AF Study Evidence v1` supplies the missing measurement artifact:
|
||||
|
||||
```text
|
||||
independent calibration results at observed focus states
|
||||
-> deterministic pairwise image-space projection deltas
|
||||
-> L3DAFST1 artifact
|
||||
-> SHA-256 retained evidence
|
||||
-> later human/scientific applicability decision
|
||||
-> v27 focus-domain creation only after that decision
|
||||
```
|
||||
|
||||
The real A6000 + E PZ 16-50 autofocus applicability remains:
|
||||
|
||||
```text
|
||||
A6000_E_PZ_16_50_AF_APPLICABILITY=BLOCKED_BY_PHYSICAL_VALIDATION
|
||||
```
|
||||
|
||||
This tool cannot change that state by itself.
|
||||
|
||||
## Input contract
|
||||
|
||||
The caller supplies one exact study context:
|
||||
|
||||
- nonzero SHA-256 of the retained body/lens/focal/non-focus geometric context;
|
||||
- decoded/oriented width and height;
|
||||
- 2..64 already acquired calibration samples.
|
||||
|
||||
Each sample contains:
|
||||
|
||||
- `FIT` or `HOLDOUT` role;
|
||||
- one bounded, nonempty opaque exact focus token;
|
||||
- nonzero SHA-256 of that calibration's retained evidence;
|
||||
- exact binary64 `fx, fy, cx, cy, k1, k2, p1, p2`.
|
||||
|
||||
Repeated samples at one focus token are allowed only when they identify distinct
|
||||
calibration evidence. Repeating the exact same `(focus token, calibration
|
||||
evidence SHA-256)` is rejected and cannot masquerade as repeatability evidence.
|
||||
|
||||
The API performs no metadata interpretation. A Sony MakerNote value, for
|
||||
example, must first be converted by the future acquisition/evidence layer into
|
||||
the exact retained token policy selected for that study.
|
||||
|
||||
## Probe model
|
||||
|
||||
Version 1 measures the same frozen pinhole + `k1/k2/p1/p2` forward projection
|
||||
model used by calibration/Sparse SfM.
|
||||
|
||||
Nine normalized ideal rays are evaluated:
|
||||
|
||||
```text
|
||||
centre
|
||||
(0, 0)
|
||||
|
||||
edge probes
|
||||
(-0.7, 0) (+0.7, 0) (0, -0.7) (0, +0.7)
|
||||
|
||||
corner probes
|
||||
(-0.7, -0.7) (+0.7, -0.7)
|
||||
(-0.7, +0.7) (+0.7, +0.7)
|
||||
```
|
||||
|
||||
For every pair of calibration samples, the artifact stores:
|
||||
|
||||
- centre delta in pixels;
|
||||
- maximum cardinal edge-probe delta;
|
||||
- maximum corner-probe delta;
|
||||
- maximum over all nine probes;
|
||||
- whether both samples use the same focus token;
|
||||
- whether the pair crosses FIT/HOLDOUT roles.
|
||||
|
||||
These are measurements, not acceptance thresholds.
|
||||
|
||||
## L3DAFST1
|
||||
|
||||
The binary artifact is little-endian and bounded to 128 KiB.
|
||||
|
||||
It contains:
|
||||
|
||||
```text
|
||||
magic = L3DAFST1
|
||||
artifact version
|
||||
probe-model version
|
||||
study-context SHA-256
|
||||
width / height
|
||||
sample / pair / role counts
|
||||
canonical sample records
|
||||
all canonical pair records and projection metrics
|
||||
```
|
||||
|
||||
Sample order is canonicalized by:
|
||||
|
||||
```text
|
||||
focus-token bytes
|
||||
calibration-evidence SHA-256
|
||||
sample role
|
||||
```
|
||||
|
||||
Therefore caller input order does not change artifact bytes or SHA-256.
|
||||
|
||||
Every floating value is finite binary64. Negative zero is normalized to positive
|
||||
zero before serialization.
|
||||
|
||||
The artifact SHA-256 is suitable as retained evidence for a later v27
|
||||
`lardon3d_optical_focus_domain_v2_create(...)` call only after the physical
|
||||
study has been reviewed and its scientific applicability decision has been made.
|
||||
|
||||
## Summary output
|
||||
|
||||
The API also returns bounded aggregate measurements:
|
||||
|
||||
- FIT/HOLDOUT/sample/pair counts;
|
||||
- same-focus / cross-focus / FIT-HOLDOUT pair counts;
|
||||
- maximum centre, edge-probe, corner-probe and global pairwise delta;
|
||||
- maximum same-focus global delta;
|
||||
- maximum cross-focus global delta;
|
||||
- maximum FIT/HOLDOUT global delta.
|
||||
|
||||
The summary is for inspection and orchestration. It does not encode PASS/FAIL.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This v1 boundary does not:
|
||||
|
||||
- solve ChArUco calibration;
|
||||
- parse EXIF/MakerNotes;
|
||||
- invent physical focus distances;
|
||||
- derive autofocus envelopes;
|
||||
- define an acceptance pixel threshold;
|
||||
- interpolate or extrapolate focus;
|
||||
- open Project DB;
|
||||
- create a v27 focus domain;
|
||||
- make an execution READY.
|
||||
|
||||
The future physical study supplies the evidence needed to decide whether the
|
||||
A6000 + E PZ 16-50 supports one domain, discrete domains, or exact focus only.
|
||||
|
|
@ -542,8 +542,10 @@ publication. `CALIBRATION_WORKFLOW_V2=PASS/FROZEN`: the workflow now proves exac
|
|||
applicability and matching per-image calibration IDs before its sole final scope attachment. The next
|
||||
dependency is physical autofocus/optical applicability validation and a dedicated calibrated real
|
||||
campaign. The generic adaptive-settings audit and bounded exact-token autofocus foundation are
|
||||
PASS/FROZEN. Device-specific autofocus envelopes remain blocked until physical evidence validates
|
||||
them.
|
||||
PASS/FROZEN. `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` now provides a deterministic
|
||||
`L3DAFST1` measurement artifact for repeated/split-focus calibration results, including centre,
|
||||
edge/corner and FIT/HOLDOUT projection deltas without freezing an acceptance threshold. Device-specific
|
||||
autofocus envelopes remain blocked until physical evidence validates them.
|
||||
|
||||
Calibration Tooling v1 consumes an already acquired Science v1 evidence bundle, validates the bounded
|
||||
contract and produces deterministic `L3DCALB1` v1.
|
||||
|
|
|
|||
97
include/lardon3d/calibration_af_study.h
Normal file
97
include/lardon3d/calibration_af_study.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#ifndef LARDON3D_CALIBRATION_AF_STUDY_H
|
||||
#define LARDON3D_CALIBRATION_AF_STUDY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum {
|
||||
LARDON3D_CALIBRATION_AF_STUDY_VERSION = 1,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_PROBE_MODEL_VERSION = 1,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE = 32,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY = 128,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES = 64,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_MAX_PAIRS = 2016,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES = 131072,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_CALIBRATION_AF_STUDY_OK = 0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_CAPACITY,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR,
|
||||
} Lardon3DCalibrationAfStudyResult;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT = 1,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT = 2,
|
||||
} Lardon3DCalibrationAfStudySampleRole;
|
||||
|
||||
typedef struct {
|
||||
Lardon3DCalibrationAfStudySampleRole role;
|
||||
char focus_token[LARDON3D_CALIBRATION_AF_STUDY_FOCUS_TOKEN_CAPACITY];
|
||||
unsigned char calibration_evidence_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE];
|
||||
double fx;
|
||||
double fy;
|
||||
double cx;
|
||||
double cy;
|
||||
double k1;
|
||||
double k2;
|
||||
double p1;
|
||||
double p2;
|
||||
} Lardon3DCalibrationAfStudySample;
|
||||
|
||||
typedef struct {
|
||||
unsigned char study_context_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE];
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
const Lardon3DCalibrationAfStudySample *samples;
|
||||
size_t sample_count;
|
||||
} Lardon3DCalibrationAfStudyInput;
|
||||
|
||||
typedef struct {
|
||||
uint32_t sample_count;
|
||||
uint32_t fit_count;
|
||||
uint32_t holdout_count;
|
||||
uint32_t pair_count;
|
||||
uint32_t same_focus_pair_count;
|
||||
uint32_t cross_focus_pair_count;
|
||||
uint32_t fit_holdout_pair_count;
|
||||
double all_center_max_px;
|
||||
double all_edge_probe_max_px;
|
||||
double all_corner_probe_max_px;
|
||||
double all_global_probe_max_px;
|
||||
double same_focus_global_probe_max_px;
|
||||
double cross_focus_global_probe_max_px;
|
||||
double fit_holdout_global_probe_max_px;
|
||||
} Lardon3DCalibrationAfStudySummary;
|
||||
|
||||
/* Produce deterministic AF-study evidence from already acquired calibration
|
||||
* results. This API performs no calibration solve, no Project DB access and no
|
||||
* scientific PASS/FAIL decision. `study_context_sha256` is the caller-retained
|
||||
* identity of the exact body/lens/focal/non-focus geometric study context.
|
||||
* Focus tokens are opaque exact observations and may repeat across independent
|
||||
* calibration samples. A repeated exact (focus token, calibration evidence
|
||||
* SHA-256) pair is rejected because it is not independent evidence.
|
||||
*
|
||||
* Projection deltas use the frozen pinhole + k1/k2/p1/p2 forward model on nine
|
||||
* canonical normalized probes: centre, four edge probes and four corner probes
|
||||
* at +/-0.7. Metrics are measurements only; this v1 API freezes no acceptance
|
||||
* threshold. The binary L3DAFST1 artifact canonicalizes samples independent of
|
||||
* caller order, includes every pairwise metric, and is suitable for hashing as
|
||||
* retained evidence. */
|
||||
Lardon3DCalibrationAfStudyResult lardon3d_calibration_af_study_produce(
|
||||
const Lardon3DCalibrationAfStudyInput *input,
|
||||
unsigned char *artifact, size_t artifact_capacity, size_t *written,
|
||||
unsigned char artifact_sha256[LARDON3D_CALIBRATION_AF_STUDY_SHA256_SIZE],
|
||||
Lardon3DCalibrationAfStudySummary *summary);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
13
meson.build
13
meson.build
|
|
@ -183,6 +183,7 @@ lardon3d_app = executable(
|
|||
'src/calibration_workflow_v2.c',
|
||||
'src/calibration_tooling.c',
|
||||
'src/calibration_tooling_v2.c',
|
||||
'src/calibration_af_study.c',
|
||||
'src/calibration_workflow.cpp',
|
||||
'src/calibration_workflow_materialize.cpp',
|
||||
'src/calibration_workflow_bind.cpp',
|
||||
|
|
@ -917,6 +918,18 @@ calibration_publication_v2_test = executable(
|
|||
|
||||
test('calibration-publication-v2', calibration_publication_v2_test, timeout: 30)
|
||||
|
||||
calibration_af_study_test = executable(
|
||||
'test-calibration-af-study',
|
||||
sources: [
|
||||
'tests/test_calibration_af_study.c',
|
||||
'src/calibration_af_study.c',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [openssl, cc.find_library('m')],
|
||||
)
|
||||
|
||||
test('calibration-af-study', calibration_af_study_test, timeout: 30)
|
||||
|
||||
calibration_workflow_v2_test = executable(
|
||||
'test-calibration-workflow-v2',
|
||||
sources: [
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ Calibration v2 heterogeneous publication PASS/FROZEN
|
|||
Calibration Workflow v2 PASS/FROZEN
|
||||
Adaptive capture settings semantics PASS/FROZEN
|
||||
Autofocus v2 foundation PASS/FROZEN
|
||||
Calibration AF study evidence v1 PASS/FROZEN
|
||||
Calibration Tooling planarity alignment PASS/FROZEN
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ CALIBRATION_V2_HETEROGENEOUS_CALIBRATION_PUBLICATION=PASS/FROZEN
|
|||
CALIBRATION_V2_WORKFLOW_READY=PASS/FROZEN
|
||||
ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PASS/FROZEN
|
||||
AUTOFOCUS_V2_FOUNDATION=PASS/FROZEN
|
||||
CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN
|
||||
CURRENT_CALIBRATION_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Default dependency order:
|
|||
4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — PASS/FROZEN;
|
||||
5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN;
|
||||
6. Adaptive capture settings / generic autofocus foundation — PASS/FROZEN;
|
||||
7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT;
|
||||
7. physical autofocus/optical applicability validation and dedicated calibrated real campaign — CURRENT; `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` supplies deterministic measurement evidence but makes no physical applicability decision;
|
||||
8. real Sparse SfM proof;
|
||||
9. durable Dense/OpenMVS orchestration;
|
||||
10. mesh / refinement / texturing / export;
|
||||
|
|
|
|||
275
src/calibration_af_study.c
Normal file
275
src/calibration_af_study.c
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
#include <lardon3d/calibration_af_study.h>
|
||||
|
||||
#include <math.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
enum {
|
||||
kHeaderSize = 72,
|
||||
kSampleSize = 232,
|
||||
kPairSize = 44,
|
||||
kPairFlagSameFocus = 1,
|
||||
kPairFlagFitHoldout = 2,
|
||||
};
|
||||
|
||||
static const unsigned char kMagic[8] = {'L','3','D','A','F','S','T','1'};
|
||||
static const double kProbes[9][2] = {
|
||||
{0.0, 0.0},
|
||||
{-0.7, 0.0}, {0.7, 0.0}, {0.0, -0.7}, {0.0, 0.7},
|
||||
{-0.7, -0.7}, {0.7, -0.7}, {-0.7, 0.7}, {0.7, 0.7},
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
size_t original_index;
|
||||
size_t token_length;
|
||||
} CanonicalSample;
|
||||
|
||||
typedef struct {
|
||||
double center;
|
||||
double edge;
|
||||
double corner;
|
||||
double global;
|
||||
} PairMetric;
|
||||
|
||||
static bool nonzero_digest(const unsigned char value[32]) {
|
||||
unsigned char any = 0;
|
||||
for (size_t i = 0; i < 32; ++i) any |= value[i];
|
||||
return any != 0;
|
||||
}
|
||||
|
||||
static bool bounded_token_length(const char token[128], size_t *length) {
|
||||
if (!token || !length) return false;
|
||||
for (size_t i = 0; i < 128; ++i) {
|
||||
if (token[i] == '\0') {
|
||||
if (i == 0) return false;
|
||||
*length = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool finite_parameters(const Lardon3DCalibrationAfStudySample *s,
|
||||
uint32_t width, uint32_t height) {
|
||||
const double p[8] = {s->fx,s->fy,s->cx,s->cy,s->k1,s->k2,s->p1,s->p2};
|
||||
for (size_t i = 0; i < 8; ++i)
|
||||
if (!isfinite(p[i])) return false;
|
||||
return s->fx > 0.0 && s->fy > 0.0 && s->cx >= 0.0 && s->cy >= 0.0 &&
|
||||
s->cx < (double)width && s->cy < (double)height;
|
||||
}
|
||||
|
||||
static int byte_compare(const unsigned char *a, size_t an,
|
||||
const unsigned char *b, size_t bn) {
|
||||
const size_t n = an < bn ? an : bn;
|
||||
const int cmp = memcmp(a, b, n);
|
||||
if (cmp != 0) return cmp;
|
||||
if (an < bn) return -1;
|
||||
if (an > bn) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int canonical_compare(const Lardon3DCalibrationAfStudyInput *input,
|
||||
const CanonicalSample *a,
|
||||
const CanonicalSample *b) {
|
||||
const Lardon3DCalibrationAfStudySample *sa = &input->samples[a->original_index];
|
||||
const Lardon3DCalibrationAfStudySample *sb = &input->samples[b->original_index];
|
||||
int cmp = byte_compare((const unsigned char *)sa->focus_token, a->token_length,
|
||||
(const unsigned char *)sb->focus_token, b->token_length);
|
||||
if (cmp != 0) return cmp;
|
||||
cmp = memcmp(sa->calibration_evidence_sha256,
|
||||
sb->calibration_evidence_sha256, 32);
|
||||
if (cmp != 0) return cmp;
|
||||
if ((uint32_t)sa->role < (uint32_t)sb->role) return -1;
|
||||
if ((uint32_t)sa->role > (uint32_t)sb->role) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void canonical_sort(const Lardon3DCalibrationAfStudyInput *input,
|
||||
CanonicalSample *values, size_t count) {
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
CanonicalSample value = values[i];
|
||||
size_t j = i;
|
||||
while (j > 0 && canonical_compare(input, &value, &values[j - 1]) < 0) {
|
||||
values[j] = values[j - 1];
|
||||
--j;
|
||||
}
|
||||
values[j] = value;
|
||||
}
|
||||
}
|
||||
|
||||
static void project(const Lardon3DCalibrationAfStudySample *s,
|
||||
double x, double y, double *u, double *v) {
|
||||
const double r2 = x*x + y*y;
|
||||
const double radial = 1.0 + s->k1*r2 + s->k2*r2*r2;
|
||||
const double xd = x*radial + 2.0*s->p1*x*y + s->p2*(r2 + 2.0*x*x);
|
||||
const double yd = y*radial + s->p1*(r2 + 2.0*y*y) + 2.0*s->p2*x*y;
|
||||
*u = s->fx*xd + s->cx;
|
||||
*v = s->fy*yd + s->cy;
|
||||
}
|
||||
|
||||
static bool pair_metric(const Lardon3DCalibrationAfStudySample *a,
|
||||
const Lardon3DCalibrationAfStudySample *b,
|
||||
PairMetric *out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
for (size_t i = 0; i < 9; ++i) {
|
||||
double au, av, bu, bv;
|
||||
project(a, kProbes[i][0], kProbes[i][1], &au, &av);
|
||||
project(b, kProbes[i][0], kProbes[i][1], &bu, &bv);
|
||||
const double delta = hypot(au - bu, av - bv);
|
||||
if (!isfinite(delta)) return false;
|
||||
if (i == 0) out->center = delta;
|
||||
else if (i <= 4 && delta > out->edge) out->edge = delta;
|
||||
else if (i >= 5 && delta > out->corner) out->corner = delta;
|
||||
if (delta > out->global) out->global = delta;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void put_u32(unsigned char **p, uint32_t value) {
|
||||
for (size_t i = 0; i < 4; ++i) (*p)[i] = (unsigned char)(value >> (8u*i));
|
||||
*p += 4;
|
||||
}
|
||||
|
||||
static void put_u64(unsigned char **p, uint64_t value) {
|
||||
for (size_t i = 0; i < 8; ++i) (*p)[i] = (unsigned char)(value >> (8u*i));
|
||||
*p += 8;
|
||||
}
|
||||
|
||||
static void put_f64(unsigned char **p, double value) {
|
||||
uint64_t bits = 0;
|
||||
if (value == 0.0) value = 0.0;
|
||||
memcpy(&bits, &value, sizeof(bits));
|
||||
put_u64(p, bits);
|
||||
}
|
||||
|
||||
static bool sha256(const unsigned char *bytes, size_t size, unsigned char out[32]) {
|
||||
unsigned int length = 0;
|
||||
return EVP_Digest(bytes, size, out, &length, EVP_sha256(), NULL) == 1 && length == 32;
|
||||
}
|
||||
|
||||
Lardon3DCalibrationAfStudyResult lardon3d_calibration_af_study_produce(
|
||||
const Lardon3DCalibrationAfStudyInput *input,
|
||||
unsigned char *artifact, size_t artifact_capacity, size_t *written,
|
||||
unsigned char artifact_sha256[32],
|
||||
Lardon3DCalibrationAfStudySummary *summary) {
|
||||
if (written) *written = 0;
|
||||
if (artifact_sha256) memset(artifact_sha256, 0, 32);
|
||||
if (summary) memset(summary, 0, sizeof(*summary));
|
||||
if (!input || !artifact || !written || !artifact_sha256 || !summary ||
|
||||
!input->samples || input->sample_count < 2 ||
|
||||
input->sample_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES ||
|
||||
input->width == 0 || input->height == 0 ||
|
||||
!nonzero_digest(input->study_context_sha256))
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT;
|
||||
|
||||
CanonicalSample canonical[LARDON3D_CALIBRATION_AF_STUDY_MAX_SAMPLES];
|
||||
uint32_t fit_count = 0, holdout_count = 0;
|
||||
for (size_t i = 0; i < input->sample_count; ++i) {
|
||||
const Lardon3DCalibrationAfStudySample *s = &input->samples[i];
|
||||
size_t token_length = 0;
|
||||
if ((s->role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT &&
|
||||
s->role != LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT) ||
|
||||
!bounded_token_length(s->focus_token, &token_length) ||
|
||||
!nonzero_digest(s->calibration_evidence_sha256) ||
|
||||
!finite_parameters(s, input->width, input->height))
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
|
||||
canonical[i].original_index = i;
|
||||
canonical[i].token_length = token_length;
|
||||
if (s->role == LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT) ++fit_count;
|
||||
else ++holdout_count;
|
||||
}
|
||||
canonical_sort(input, canonical, input->sample_count);
|
||||
for (size_t i = 1; i < input->sample_count; ++i) {
|
||||
const Lardon3DCalibrationAfStudySample *a = &input->samples[canonical[i-1].original_index];
|
||||
const Lardon3DCalibrationAfStudySample *b = &input->samples[canonical[i].original_index];
|
||||
if (canonical[i-1].token_length == canonical[i].token_length &&
|
||||
memcmp(a->focus_token, b->focus_token, canonical[i].token_length) == 0 &&
|
||||
memcmp(a->calibration_evidence_sha256, b->calibration_evidence_sha256, 32) == 0)
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
|
||||
}
|
||||
|
||||
const size_t pair_count = input->sample_count * (input->sample_count - 1) / 2;
|
||||
if (pair_count > LARDON3D_CALIBRATION_AF_STUDY_MAX_PAIRS)
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_CAPACITY;
|
||||
const size_t required = (size_t)kHeaderSize + input->sample_count*(size_t)kSampleSize + pair_count*(size_t)kPairSize;
|
||||
if (required > LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES || artifact_capacity < required)
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_CAPACITY;
|
||||
|
||||
unsigned char *p = artifact;
|
||||
memcpy(p, kMagic, 8); p += 8;
|
||||
put_u32(&p, LARDON3D_CALIBRATION_AF_STUDY_VERSION);
|
||||
put_u32(&p, LARDON3D_CALIBRATION_AF_STUDY_PROBE_MODEL_VERSION);
|
||||
memcpy(p, input->study_context_sha256, 32); p += 32;
|
||||
put_u32(&p, input->width);
|
||||
put_u32(&p, input->height);
|
||||
put_u32(&p, (uint32_t)input->sample_count);
|
||||
put_u32(&p, (uint32_t)pair_count);
|
||||
put_u32(&p, fit_count);
|
||||
put_u32(&p, holdout_count);
|
||||
|
||||
for (size_t rank = 0; rank < input->sample_count; ++rank) {
|
||||
const CanonicalSample *c = &canonical[rank];
|
||||
const Lardon3DCalibrationAfStudySample *s = &input->samples[c->original_index];
|
||||
put_u32(&p, (uint32_t)s->role);
|
||||
put_u32(&p, (uint32_t)c->token_length);
|
||||
memset(p, 0, 128);
|
||||
memcpy(p, s->focus_token, c->token_length); p += 128;
|
||||
memcpy(p, s->calibration_evidence_sha256, 32); p += 32;
|
||||
put_f64(&p, s->fx); put_f64(&p, s->fy); put_f64(&p, s->cx); put_f64(&p, s->cy);
|
||||
put_f64(&p, s->k1); put_f64(&p, s->k2); put_f64(&p, s->p1); put_f64(&p, s->p2);
|
||||
}
|
||||
|
||||
Lardon3DCalibrationAfStudySummary local_summary = {0};
|
||||
local_summary.sample_count = (uint32_t)input->sample_count;
|
||||
local_summary.fit_count = fit_count;
|
||||
local_summary.holdout_count = holdout_count;
|
||||
local_summary.pair_count = (uint32_t)pair_count;
|
||||
|
||||
for (size_t ai = 0; ai < input->sample_count; ++ai) {
|
||||
for (size_t bi = ai + 1; bi < input->sample_count; ++bi) {
|
||||
const CanonicalSample *ca = &canonical[ai];
|
||||
const CanonicalSample *cb = &canonical[bi];
|
||||
const Lardon3DCalibrationAfStudySample *a = &input->samples[ca->original_index];
|
||||
const Lardon3DCalibrationAfStudySample *b = &input->samples[cb->original_index];
|
||||
PairMetric metric;
|
||||
if (!pair_metric(a, b, &metric))
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE;
|
||||
const bool same_focus = ca->token_length == cb->token_length &&
|
||||
memcmp(a->focus_token, b->focus_token, ca->token_length) == 0;
|
||||
const bool fit_holdout = a->role != b->role;
|
||||
uint32_t flags = 0;
|
||||
if (same_focus) flags |= kPairFlagSameFocus;
|
||||
if (fit_holdout) flags |= kPairFlagFitHoldout;
|
||||
put_u32(&p, (uint32_t)ai);
|
||||
put_u32(&p, (uint32_t)bi);
|
||||
put_u32(&p, flags);
|
||||
put_f64(&p, metric.center);
|
||||
put_f64(&p, metric.edge);
|
||||
put_f64(&p, metric.corner);
|
||||
put_f64(&p, metric.global);
|
||||
|
||||
if (same_focus) ++local_summary.same_focus_pair_count;
|
||||
else ++local_summary.cross_focus_pair_count;
|
||||
if (fit_holdout) ++local_summary.fit_holdout_pair_count;
|
||||
if (metric.center > local_summary.all_center_max_px) local_summary.all_center_max_px = metric.center;
|
||||
if (metric.edge > local_summary.all_edge_probe_max_px) local_summary.all_edge_probe_max_px = metric.edge;
|
||||
if (metric.corner > local_summary.all_corner_probe_max_px) local_summary.all_corner_probe_max_px = metric.corner;
|
||||
if (metric.global > local_summary.all_global_probe_max_px) local_summary.all_global_probe_max_px = metric.global;
|
||||
if (same_focus && metric.global > local_summary.same_focus_global_probe_max_px)
|
||||
local_summary.same_focus_global_probe_max_px = metric.global;
|
||||
if (!same_focus && metric.global > local_summary.cross_focus_global_probe_max_px)
|
||||
local_summary.cross_focus_global_probe_max_px = metric.global;
|
||||
if (fit_holdout && metric.global > local_summary.fit_holdout_global_probe_max_px)
|
||||
local_summary.fit_holdout_global_probe_max_px = metric.global;
|
||||
}
|
||||
}
|
||||
if ((size_t)(p - artifact) != required)
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR;
|
||||
if (!sha256(artifact, required, artifact_sha256))
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_ENCODING_ERROR;
|
||||
*summary = local_summary;
|
||||
*written = required;
|
||||
return LARDON3D_CALIBRATION_AF_STUDY_OK;
|
||||
}
|
||||
186
tests/test_calibration_af_study.c
Normal file
186
tests/test_calibration_af_study.c
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
#include <lardon3d/calibration_af_study.h>
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CHECK(expression) \
|
||||
do { \
|
||||
if (!(expression)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expression); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void fill_digest(unsigned char output[32], unsigned char value) {
|
||||
memset(output, value, 32);
|
||||
}
|
||||
|
||||
static Lardon3DCalibrationAfStudySample make_sample(
|
||||
const char *focus_token, unsigned char digest_value, double focal_px,
|
||||
Lardon3DCalibrationAfStudySampleRole role) {
|
||||
Lardon3DCalibrationAfStudySample sample = {0};
|
||||
sample.role = role;
|
||||
(void)snprintf(sample.focus_token, sizeof(sample.focus_token), "%s",
|
||||
focus_token);
|
||||
fill_digest(sample.calibration_evidence_sha256, digest_value);
|
||||
sample.fx = focal_px;
|
||||
sample.fy = focal_px + 2.0;
|
||||
sample.cx = 3000.0;
|
||||
sample.cy = 2000.0;
|
||||
sample.k1 = -0.1;
|
||||
sample.k2 = 0.01;
|
||||
sample.p1 = 0.001;
|
||||
sample.p2 = -0.001;
|
||||
return sample;
|
||||
}
|
||||
|
||||
static Lardon3DCalibrationAfStudyInput make_input(
|
||||
Lardon3DCalibrationAfStudySample *samples, size_t sample_count) {
|
||||
Lardon3DCalibrationAfStudyInput input = {0};
|
||||
fill_digest(input.study_context_sha256, 0x91);
|
||||
input.width = 6000;
|
||||
input.height = 4000;
|
||||
input.samples = samples;
|
||||
input.sample_count = sample_count;
|
||||
return input;
|
||||
}
|
||||
|
||||
static int test_deterministic_and_summary(void) {
|
||||
Lardon3DCalibrationAfStudySample samples[3] = {
|
||||
make_sample("sony-focus:137", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT),
|
||||
make_sample("sony-focus:137", 0x22, 4001.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT),
|
||||
make_sample("sony-focus:165", 0x33, 4010.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT),
|
||||
};
|
||||
Lardon3DCalibrationAfStudyInput input = make_input(samples, 3);
|
||||
|
||||
unsigned char artifact_a[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES];
|
||||
unsigned char artifact_b[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES];
|
||||
unsigned char sha_a[32];
|
||||
unsigned char sha_b[32];
|
||||
size_t written_a = 0;
|
||||
size_t written_b = 0;
|
||||
Lardon3DCalibrationAfStudySummary summary_a;
|
||||
Lardon3DCalibrationAfStudySummary summary_b;
|
||||
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact_a, sizeof(artifact_a), &written_a, sha_a,
|
||||
&summary_a) == LARDON3D_CALIBRATION_AF_STUDY_OK);
|
||||
CHECK(written_a == 900);
|
||||
CHECK(memcmp(artifact_a, "L3DAFST1", 8) == 0);
|
||||
CHECK(summary_a.sample_count == 3);
|
||||
CHECK(summary_a.fit_count == 2);
|
||||
CHECK(summary_a.holdout_count == 1);
|
||||
CHECK(summary_a.pair_count == 3);
|
||||
CHECK(summary_a.same_focus_pair_count == 1);
|
||||
CHECK(summary_a.cross_focus_pair_count == 2);
|
||||
CHECK(summary_a.fit_holdout_pair_count == 2);
|
||||
CHECK(summary_a.all_global_probe_max_px > 0.0);
|
||||
CHECK(summary_a.same_focus_global_probe_max_px > 0.0);
|
||||
CHECK(summary_a.cross_focus_global_probe_max_px >
|
||||
summary_a.same_focus_global_probe_max_px);
|
||||
CHECK(summary_a.fit_holdout_global_probe_max_px > 0.0);
|
||||
|
||||
Lardon3DCalibrationAfStudySample reordered[3] = {
|
||||
samples[2], samples[0], samples[1],
|
||||
};
|
||||
input.samples = reordered;
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact_b, sizeof(artifact_b), &written_b, sha_b,
|
||||
&summary_b) == LARDON3D_CALIBRATION_AF_STUDY_OK);
|
||||
CHECK(written_a == written_b);
|
||||
CHECK(memcmp(artifact_a, artifact_b, written_a) == 0);
|
||||
CHECK(memcmp(sha_a, sha_b, 32) == 0);
|
||||
CHECK(memcmp(&summary_a, &summary_b, sizeof(summary_a)) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_capacity_is_failure_atomic_for_outputs(void) {
|
||||
Lardon3DCalibrationAfStudySample samples[2] = {
|
||||
make_sample("focus:a", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT),
|
||||
make_sample("focus:b", 0x22, 4005.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT),
|
||||
};
|
||||
Lardon3DCalibrationAfStudyInput input = make_input(samples, 2);
|
||||
unsigned char artifact[16] = {0};
|
||||
unsigned char sha[32];
|
||||
memset(sha, 0xA5, sizeof(sha));
|
||||
size_t written = 999;
|
||||
Lardon3DCalibrationAfStudySummary summary;
|
||||
memset(&summary, 0xA5, sizeof(summary));
|
||||
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact, sizeof(artifact), &written, sha, &summary) ==
|
||||
LARDON3D_CALIBRATION_AF_STUDY_CAPACITY);
|
||||
CHECK(written == 0);
|
||||
unsigned char zero[32] = {0};
|
||||
CHECK(memcmp(sha, zero, 32) == 0);
|
||||
Lardon3DCalibrationAfStudySummary zero_summary = {0};
|
||||
CHECK(memcmp(&summary, &zero_summary, sizeof(summary)) == 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_duplicate_evidence_rejected(void) {
|
||||
Lardon3DCalibrationAfStudySample samples[2] = {
|
||||
make_sample("focus:repeat", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT),
|
||||
make_sample("focus:repeat", 0x11, 4001.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT),
|
||||
};
|
||||
Lardon3DCalibrationAfStudyInput input = make_input(samples, 2);
|
||||
unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES];
|
||||
unsigned char sha[32];
|
||||
size_t written = 0;
|
||||
Lardon3DCalibrationAfStudySummary summary;
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact, sizeof(artifact), &written, sha, &summary) ==
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_invalid_values_rejected(void) {
|
||||
Lardon3DCalibrationAfStudySample samples[2] = {
|
||||
make_sample("focus:a", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT),
|
||||
make_sample("focus:b", 0x22, 4005.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_HOLDOUT),
|
||||
};
|
||||
Lardon3DCalibrationAfStudyInput input = make_input(samples, 2);
|
||||
unsigned char artifact[LARDON3D_CALIBRATION_AF_STUDY_MAX_ARTIFACT_BYTES];
|
||||
unsigned char sha[32];
|
||||
size_t written = 0;
|
||||
Lardon3DCalibrationAfStudySummary summary;
|
||||
|
||||
samples[0].fx = NAN;
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact, sizeof(artifact), &written, sha, &summary) ==
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE);
|
||||
samples[0] = make_sample("focus:a", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT);
|
||||
|
||||
memset(samples[0].focus_token, 'x', sizeof(samples[0].focus_token));
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact, sizeof(artifact), &written, sha, &summary) ==
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_EVIDENCE);
|
||||
samples[0] = make_sample("focus:a", 0x11, 4000.0,
|
||||
LARDON3D_CALIBRATION_AF_STUDY_SAMPLE_FIT);
|
||||
|
||||
memset(input.study_context_sha256, 0, sizeof(input.study_context_sha256));
|
||||
CHECK(lardon3d_calibration_af_study_produce(
|
||||
&input, artifact, sizeof(artifact), &written, sha, &summary) ==
|
||||
LARDON3D_CALIBRATION_AF_STUDY_INVALID_ARGUMENT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
CHECK(test_deterministic_and_summary() == 0);
|
||||
CHECK(test_capacity_is_failure_atomic_for_outputs() == 0);
|
||||
CHECK(test_duplicate_evidence_rejected() == 0);
|
||||
CHECK(test_invalid_values_rejected() == 0);
|
||||
puts("CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue