Skip to content

Maximum accuracy v2: UCS-space engine, global separation, Bayesian smoothing, spectral model — plan + promotion gates (follow-up to #122) #123

Description

@itsab1989

Goal

Evolve the Maximum accuracy engine mode (shipped in v3.13.7, #122 follow-up) from "best colprof-class profiler" toward the best profile-creation algorithm we can build on a single static chart — explicitly without the closed measurement loop (active chart design / print–measure–refit), which is held back because it would restructure the user workflow. Everything below works one-shot on the existing workflow and is independent of that future step.

Non-goals: colprof parity (the fast and bit-exact modes keep that role), changing any default behaviour before promotion gates pass, and any UI/workflow restructuring.

Ground rule on test data: the owner's own measurements (ET8550, Canon Pro-300, …) are test anchors only, never calibration targets. The primary referee is a synthetic ground-truth battery (W0) where the true answer is known exactly. Real measurements are secondary smoke tests; community-contributed .ti3s (see #72/#122) widen coverage over time.


W0 — Evaluation & promotion harness (must land first)

Nothing replaces the current accurate mode until it provably wins. All later workstreams land behind a hidden candidate flag and are judged by this harness.

Candidate flag

# BuildSettings — hidden, never in the UI until promotion:
engine_candidates: frozenset[str] = frozenset()   # e.g. {"ucs", "joint-sep"}
# resolved from env CHROMIQ_ENGINE_NEXT="ucs,joint-sep" or an expert setting;
# empty set == shipped accurate mode, bit-for-bit.

Each workstream is one candidate token, so improvements are benchmarked individually and in combination (ablation), not as one big bang.

Synthetic ground-truth battery (the referee)

Six analytic printers where f_true(device) → spectrum → XYZ is known exactly; the harness scores a built profile by comparing its A2B/B2A against f_true on ~50 000 quasi-random points (Sobol), not against the chart:

id device character
S1 RGB glossy, mild dot gain (γ≈0.85 per channel)
S2 RGB matte, strong dot gain + compressed shadows (black L*≈18)
S3 CMYK TAC 280, realistic K interaction
S4 CMYK noisy instrument model (heteroscedastic, σ ∝ dark)
S5 CMYKOG TAC 320
S6 CMYKV violet with off-anchor hue (tests measured-hue path)

Noise model applied to the "measured" chart only: XYZ_meas = XYZ_true + ε, ε ~ N(0, Σ(Y)) with σ_Y = 0.05 + 0.10·exp(−Y/10) (matches i1-class repeatability incl. the dark-patch blow-up), plus a 0.5 % misread probability (uniform smudge 5–40 ΔE) to score robustness.

Charts: 900 / 1500 patches via the existing generators, seeds fixed. Metrics per build, all in ΔE2000 against f_true: median, p95, p99, max; neutral-axis separation smoothness (K total-variation vs net); round-trip median/max; OOG hue error; build time.

Real-measurement leg (secondary)

The held-out protocol from the ET8550 benchmark (90 % train / 10 % judge, endpoints protected) formalised into benchmarks/heldout.py, run over every real .ti3 in a local corpus folder + any donated measurements. Interpretation rule written into the harness README: real-data deltas < ~0.05 ΔE00 median are noise; only the synthetic battery decides ties.

Promotion gates (candidate → replaces accurate internals)

  1. Synthetic battery: aggregate median ΔE00 improves ≥ 5 %, and no device class regresses > 2 % on median or p95, and max/round-trip-max not worse.
  2. Robustness: S4 misread scenario — flagged-outlier F1 not worse; clean-chart false-flag rate ≤ current.
  3. Smoothness: neutral K TV-vs-net not worse on S3/S5/S6.
  4. Full test suite green; build time ≤ 2 × current accurate mode at -qm.
  5. Real-measurement leg: no consistent regression across the corpus.

Effort: ~1 week (bench harness exists in prototype form from the ET8550 work; the synthetic battery is a formalisation of the test-suite synth printers + noise models).


W1 — Optimise end-to-end in CAM16-UCS (candidate ucs)

Why: we now judge in ΔE2000 but still fit/invert/smooth in CIELAB — ΔE00 is a formula, not a space, so it cannot be the inner metric of a linear-algebra pipeline. CAM16-UCS is a true metric space (Euclidean ≈ perceptual); moving the optimisation there makes every existing least-squares component perceptually correct by construction and removes all per-point weighting machinery (the ΔE00 clip weights collapse to identity in UCS).

Maths

CAM16 (Li et al. 2017) under print-viewing conditions — D50 white, L_A = 63.7 cd/m² (318.3/5), Y_b = 20, average surround — then the UCS mapping (same constants as CAM02-UCS, Luo et al.):

$$J' = \frac{1.7,J}{1 + 0.007,J},\qquad M' = \frac{\ln(1 + 0.0228,M)}{0.0228},\qquad a' = M'\cos h,; b' = M'\sin h$$

$$\Delta E_{UCS} = \sqrt{(\Delta J')^2 + (\Delta a')^2 + (\Delta b')^2};\approx; \Delta E_{00}\ \text{(agreement within ~10 % over the print gamut)}$$

Integration points (all mechanical)

# workflow/profile_engine/ucs.py  (new, ~150 lines numpy)
def xyz_to_ucs(xyz100, vc=PRINT_VC) -> np.ndarray: ...
def ucs_to_xyz(ucs,   vc=PRINT_VC) -> np.ndarray: ...
# gated by round-trip self-test < 1e-4 at import, like gammap_port.cam02
  • fit_forward_model(..., target_space=ucs): fit device → UCS; the model's grid nodes stay the A2B CLUT contents after a final ucs→Lab conversion of node values — the mft2 layout is untouched, only the space in which residuals/curvature are minimised changes.
  • B2A inversion: GN residual r = target_ucs − f_ucs(d); the hue-clip weight matrices are deleted (identity in UCS is already hue-honest; keep only the deliberate ×3 hue emphasis as a diagonal in the (J',C',h') frame).
  • Smoothness regulariser: the Laplacian penalty now acts on UCS values ⇒ visible curvature is what gets smoothed.
  • CV/robust statistics: residuals in ΔE_UCS (drop the ΔE00 calls in the loop — cheaper too).
  • Existing CIECAM02 code (gammap_port/cam02.py) is the implementation template; CAM16 differs only in the M_16 matrix and small fixes.

Validation: pinned to the CAM16 paper's numerical examples; battery ablation ucs vs baseline. Risk: viewing-condition constants (documented, one place). Effort: 2–3 weeks.


W2 — Globally optimal separation for CMYK/+N (candidate joint-sep)

Why: per-node GN + soft priors is local; adjacent nodes can still settle on different metameric splits (we fixed the worst of it with the firm neutral prior, but the formulation remains local). Solve the whole B2A grid jointly:

$$\min_{D\in[0,1]^{G\times n}}; \sum_{i}\big|f_{UCS}(D_i) - T_i\big|^2 ;+; \lambda_s \sum_{(i,j)\in\mathcal{E}}|D_i - D_j|^2 ;+; \lambda_k \sum_i w_i,(D_{i,K} - K^*(L_i))^2 ;+; \lambda_{ink}\sum_i \mathbf{1}^\top D_i$$

subject to 0 ≤ D ≤ 1, Σ_c D_{i,c} ≤ TAC — where 𝓔 is the 6-neighbour grid graph in PCS space (graph Laplacian), K* the user's −k curve (already ported: argyll_k_curve), w_i the chroma-faded neutral weight.

Solver: ADMM (splitting keeps every piece we already have)

x-step:  linearised LS in D (Gauss–Newton around current D) + Laplacian
         → normal equations solved by CG (structured: Laplacian is the
         same stencil as forward_model.curvature — code reuse)
z-step:  z = project_tac(clip(x + u, 0, 1), TAC)     # existing function!
u-step:  u += x − z

Size: 33³ nodes × 4–8 ch = 144–290 k variables; CG with the existing _interp_weights-style operators ≈ seconds/iteration, ~50–150 ADMM iterations ⇒ ~1–3 min — inside the accurate-mode time budget. Warm start from the current per-node result (which then only needs 1–2 GN iterations).

Bonus deliverables: true -K locus semantics (per-node feasible K range [K_min(T_i), K_max(T_i)] from two bounded inversions — cheap inside the joint solve), and Pareto knobs (λ_ink exposed later as "ink saving" expert option). Banding becomes structurally impossible: smoothness is in the objective, not a post-hoc refit.

Validation: S3/S5/S6 K-TV metric, gamut volume must not shrink > 0.5 % (the soft-prior lesson from #122 — hard policies cost gamut). Effort: 3–4 weeks.


W3 — Bayesian smoothing & uncertainty (candidate gp)

Why: the CV ladder picks λ from 5 candidates on one split; a Gaussian-process view gives the continuous, principled version of the same thing (marginal likelihood), plus the per-region uncertainty map that any future active-design step (held-back item 1) would need — and a user-facing confidence report today.

Maths

Grid-structured GP via SKI (structured kernel interpolation): with our existing multilinear weights W (literally _interp_weights),

$$K \approx W,K_{grid},W^\top,\qquad K_{grid} = K_1 \otimes K_2 \otimes \cdots \otimes K_n \ \text{(Kronecker per axis, Matérn-5/2)}$$

  • Fast MVMs ⇒ CG solves; hyperparameters (lengthscale ℓ per axis, signal σ_f, heteroscedastic noise σ_n(Y) seeded from duplicate-patch scatter) by maximising
    $$\log p(y) = -\tfrac12 y^\top(K+\Sigma_n)^{-1}y - \tfrac12\log\det(K+\Sigma_n) + C$$
    with a Hutchinson/Lanczos log-det estimator — or, pragmatic v1: 3-point grid search on ℓ only (direct upgrade of today's ladder, no trace estimation needed).
  • Posterior variance at probe points → an uncertainty map rendered as a per-region table in the build log: "shadows ±0.35 ΔE00, midtones ±0.12, saturated reds ±0.28 (95 %)".

The MAD/Huber robustness pipeline survives unchanged (weights multiply Σ_n). Effort: 2–3 weeks (v1 with grid-searched ℓ: 1 week).


W4 — Spectral-first physical forward model (candidate spectral)

Why: physics extrapolates correctly where charts are sparse and enables illuminant-true profiles + metamerism control. Scope honestly: cellular Yule–Nielsen spectral Neugebauer applies to devices whose channels we drive (CMYK/+N .ti3s); RGB-driver printers hide the inks, so RGB keeps the grid (optionally Kubelka–Munk-flavoured basis). Requires SPEC_* data; silently falls back otherwise.

Maths

Yule–Nielsen modified spectral Neugebauer over the 2ⁿ (cellular: subdivided) primaries:

$$R(\lambda)^{1/\nu} = \sum_{p} w_p(a), R_p(\lambda)^{1/\nu},\qquad w_p(a) = \prod_c \left[a_c\ \text{or}\ (1-a_c)\right] \ \text{(Demichel)}$$

Effective coverages from per-channel dot-gain curves fitted on the chart's own ramps (Murray–Davies inversion):

$$a_{eff} = \frac{R_{paper}^{1/\nu} - R_{ramp}^{1/\nu}}{R_{paper}^{1/\nu} - R_{solid}^{1/\nu}}$$

ν ∈ [1, 3] fitted globally; the current nonparametric grid remains as residual on top (hybrid mean-function architecture), so the model never underperforms the status quo. Deliverables stack: exact FWA under any illuminant, profile optimised for the user's light, metamerism index in the separation objective (plugs into W2's cost as an extra term). Effort: 4–8 weeks; do after W1–W3.


W5 — Rendering intents beyond Argyll (candidate render2, additive)

A new perceptual/saturation implementation in CAM16-UCS — the bit-exact Argyll mapping stays available; this is an additional choice, validated psychophysically (image panels), not numerically.

Key design: bijective-by-construction radial maps. For focal point F(h) and knee k, use a strictly monotone C¹ radial function γ (e.g. knee + rational tail); because γ is monotone, the inverse γ⁻¹ exists in closed form ⇒ A2B0 = γ⁻¹ ∘ A2B1 exactly — round-trip identity replaces the -nI fixed-point machinery. Hue-dependent compression strength from the gamut-mapping literature (Morovič); modern black-point compensation integrated. Effort: 4–6 weeks + panel time; last of the algorithmic workstreams.


W6 — Container ceiling (exploratory)

ICC v2 mft2 = 16-bit, grid ≤ 45³. Add optional v4 (lutAtoBType/lutBtoAType) output alongside v2, later iccMAX spectral PCS. Months; zero algorithmic gain by itself; keep parked until W1–W5 are promoted.


Order, effort, risk

# workstream effort risk independent of held-back item 1?
W0 harness + gates 1 w low yes
W1 CAM16-UCS core 2–3 w low-med (VC constants) yes
W2 joint separation 3–4 w med (solver tuning) yes
W3 GP smoothing + uncertainty 1–3 w low yes (and enables item 1 later)
W4 spectral/YNSN hybrid 4–8 w med-high yes
W5 new rendering intents 4–6 w med (needs panels) yes
W6 v4/iccMAX months high yes

Recommended sequence: W0 → W1 → W2 → W3 → W4 → W5 → W6, each landing dark behind its candidate token, each with an ablation row in the benchmark report posted to this issue. The shipped accurate mode changes only when W0's gates say the candidate wins.

Data-integrity policy (explicit)

Relationship to the held-back "closed loop"

Active chart design + looped refit stays out of scope here. For the record: ChromIQ's existing Check & Refine is already a manual version of that loop, and W3's uncertainty map is the exact input an active-design step needs — so nothing in this plan blocks it, and W3 quietly prepares it.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions