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 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 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 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 PASS/FROZEN. `CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN` now provides a deterministic
them. `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 Calibration Tooling v1 consumes an already acquired Science v1 evidence bundle, validates the bounded
contract and produces deterministic `L3DCALB1` v1. 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_workflow_v2.c',
'src/calibration_tooling.c', 'src/calibration_tooling.c',
'src/calibration_tooling_v2.c', 'src/calibration_tooling_v2.c',
'src/calibration_af_study.c',
'src/calibration_workflow.cpp', 'src/calibration_workflow.cpp',
'src/calibration_workflow_materialize.cpp', 'src/calibration_workflow_materialize.cpp',
'src/calibration_workflow_bind.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) 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( calibration_workflow_v2_test = executable(
'test-calibration-workflow-v2', 'test-calibration-workflow-v2',
sources: [ sources: [

View file

@ -41,6 +41,7 @@ Calibration v2 heterogeneous publication PASS/FROZEN
Calibration Workflow v2 PASS/FROZEN Calibration Workflow v2 PASS/FROZEN
Adaptive capture settings semantics PASS/FROZEN Adaptive capture settings semantics PASS/FROZEN
Autofocus v2 foundation PASS/FROZEN Autofocus v2 foundation PASS/FROZEN
Calibration AF study evidence v1 PASS/FROZEN
Calibration Tooling planarity alignment 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 CALIBRATION_V2_WORKFLOW_READY=PASS/FROZEN
ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PASS/FROZEN ADAPTIVE_CAPTURE_SETTINGS_CONTRACT=PASS/FROZEN
AUTOFOCUS_V2_FOUNDATION=PASS/FROZEN AUTOFOCUS_V2_FOUNDATION=PASS/FROZEN
CALIBRATION_AF_STUDY_EVIDENCE_V1=PASS/FROZEN
CURRENT_CALIBRATION_NEXT=PHYSICAL_AUTOFOCUS_OPTICAL_APPLICABILITY_VALIDATION 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; 4. Heterogeneous calibration publication / Tooling / Bootstrap evolution — PASS/FROZEN;
5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN; 5. Heterogeneous Workflow v2 truthful READY proof — PASS/FROZEN;
6. Adaptive capture settings / generic autofocus foundation — 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; 8. real Sparse SfM proof;
9. durable Dense/OpenMVS orchestration; 9. durable Dense/OpenMVS orchestration;
10. mesh / refinement / texturing / export; 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;
}