diff --git a/docs/architecture/calibration_af_study.md b/docs/architecture/calibration_af_study.md new file mode 100644 index 0000000..466abb7 --- /dev/null +++ b/docs/architecture/calibration_af_study.md @@ -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. diff --git a/docs/roadmap/roadmap.md b/docs/roadmap/roadmap.md index 5a0149d..e1ba9f0 100644 --- a/docs/roadmap/roadmap.md +++ b/docs/roadmap/roadmap.md @@ -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. diff --git a/include/lardon3d/calibration_af_study.h b/include/lardon3d/calibration_af_study.h new file mode 100644 index 0000000..c3e15b1 --- /dev/null +++ b/include/lardon3d/calibration_af_study.h @@ -0,0 +1,97 @@ +#ifndef LARDON3D_CALIBRATION_AF_STUDY_H +#define LARDON3D_CALIBRATION_AF_STUDY_H + +#include +#include + +#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 diff --git a/meson.build b/meson.build index 454f789..e985f4b 100644 --- a/meson.build +++ b/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: [ diff --git a/prompt/02_CURRENT_FROZEN_STATE.md b/prompt/02_CURRENT_FROZEN_STATE.md index e6edeee..f86fee9 100644 --- a/prompt/02_CURRENT_FROZEN_STATE.md +++ b/prompt/02_CURRENT_FROZEN_STATE.md @@ -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 ``` diff --git a/prompt/12_CALIBRATION.md b/prompt/12_CALIBRATION.md index cdd5371..8bcfa81 100644 --- a/prompt/12_CALIBRATION.md +++ b/prompt/12_CALIBRATION.md @@ -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 ``` diff --git a/prompt/31_IMPLEMENTATION_ORDER.md b/prompt/31_IMPLEMENTATION_ORDER.md index 97df2d2..a7ed391 100644 --- a/prompt/31_IMPLEMENTATION_ORDER.md +++ b/prompt/31_IMPLEMENTATION_ORDER.md @@ -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; diff --git a/src/calibration_af_study.c b/src/calibration_af_study.c new file mode 100644 index 0000000..55d3088 --- /dev/null +++ b/src/calibration_af_study.c @@ -0,0 +1,275 @@ +#include + +#include +#include +#include +#include +#include + +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; +} diff --git a/tests/test_calibration_af_study.c b/tests/test_calibration_af_study.c new file mode 100644 index 0000000..c4ed8b4 --- /dev/null +++ b/tests/test_calibration_af_study.c @@ -0,0 +1,186 @@ +#include + +#include +#include +#include + +#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; +}