feat: add calibration autofocus study evidence

This commit is contained in:
fy59 2026-09-03 14:24:29 +02:00
parent 34c6a82b35
commit 524f08d1b3
9 changed files with 727 additions and 3 deletions

View 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.

View file

@ -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.

View 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

View file

@ -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: [

View file

@ -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
```

View file

@ -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
```

View file

@ -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
View 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;
}

View 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;
}