Skip to main content

Crate zenanalyze

Crate zenanalyze 

Source
Expand description

Image content analyzers for adaptive codec decisions.

Computes the numeric features used by oracle-trained decision trees (originally coefficient::scripts/fit_oracle_tree.py) to drive per-image encoder configuration. Every shipped feature is wired into at least one fitted tree’s splits per the 2026-04-25 audit.

§Public API

All public types are opaque. Construction goes through the feature module:

§Entry points

  • analyze_features — preferred. Takes any PixelSlice and an feature::AnalysisQuery. Native zero-copy on RGB8/RGB8_SRGB inputs; one-row scratch + RowConverter on anything else (every zenpixels descriptor: RGBA/BGRA u8/u16, GRAY u8/u16, RGB/RGBA f32 linear, PQ/HLG/Bt709, BT.709/DisplayP3/BT.2020/AdobeRGB).
  • analyze_features_rgb8 — convenience for packed RGB8 buffers.

§Composition pattern

use zenanalyze::feature::{AnalysisFeature, AnalysisQuery, FeatureSet};

const JPEG_FEATURES: FeatureSet = FeatureSet::new()
    .with(AnalysisFeature::Variance)
    .with(AnalysisFeature::EdgeDensity)
    .with(AnalysisFeature::DctCompressibilityY);
const WEBP_FEATURES: FeatureSet = FeatureSet::new()
    .with(AnalysisFeature::Variance)
    .with(AnalysisFeature::AlphaPresent);

// Orchestrator unions, runs once, hands results down.
let needed = JPEG_FEATURES.union(WEBP_FEATURES);
let r = zenanalyze::analyze_features(slice, &AnalysisQuery::new(needed))?;
let var = r.get_f32(AnalysisFeature::Variance);

§Threshold contract — iterating during 0.1.x

Numeric thresholds and normalization scales in this crate are converging during the 0.1.x line. Downstream consumers that compile-in fitted models (oracle decision trees, content selectors) must pin to a specific zenanalyze patch version and re-validate / retrain whenever they bump it — feature outputs change between patches whenever a real bug is fixed.

Every breaking numeric change ships with a CHANGELOG entry under the version it landed in. When the algorithm stabilizes (1.0), the contract will freeze. Sampling budgets (Tier 1/2 stripe step, Tier 3 block cap) are part of that contract — not exposed as per-call knobs.

§Tier architecture

Five passes, each gated by what the requested feature::FeatureSet actually needs. None of them materialize a full RGB8 buffer:

PassIterates overReadsCost (4 MP)Drives
Tier 1Stripe-sampled rowsRGB8 (via RowStream)~1 msluma stats, edges, chroma, uniformity, grayscale_score
Tier 23-row sliding windowRGB8~2 msper-axis Cb/Cr sharpness
Tier 3Sampled 8×8 DCT blocksRGB8~3 msDCT energy, entropy, AQ map, noise floor, line-art, patch fraction
PaletteFull-image (every pixel)RGB8~1 msdistinct_color_bins
AlphaStride-sampled rowsSource bytes~0.3 msalpha presence / used / bimodal
tier_depthStride-sampled rowsSource bytes~0.5 ms HDR / ~0 ms SDRpeak nits, headroom, bit depth, HDR-present

Tier 1/2/3 + Palette read RGB8, going through RowStream’s Native zero-copy path on RGB8 layouts and RowConverter row-by-row on everything else. Alpha and tier_depth read source samples directly — that’s load-bearing for HDR (RowConverter doesn’t tonemap PQ/HLG; its narrowing clips to [0, 1] sRGB-display) and for the alpha pass (avoids a precision-losing pre-multiply round- trip on u16/f32 alpha).

§Why a separate tier_depth

The standard tiers’ threshold contract is calibrated on display- space RGB8 bytes. HDR content’s source samples encode dynamic range that the RGB8 narrowing destroys; a 4000-nit PQ source and a 100-nit-clipped SDR source produce byte-identical RGB8 streams. Routing the depth tier through RowStream would have made it literally impossible to surface HDR-aware features.

Adding tier_depth as a fifth const bool axis to the dispatch table would have doubled it from 16 to 32 monomorphizations for near-zero benefit — the depth tier reads source bytes, not RGB8 rows, and shares no inner loop with T1/T2/T3 to specialize. So it’s wired as a runtime branch outside the const-bool dispatch.

§Empirical calibration (corpus-eval 2026-04-27)

Pre-0.1.0-ship calibration baseline measured on a 219-image labeled corpus from coefficient/benchmarks/classifier-eval/labels.tsv (174 photo, 36 screen, 9 illustration, 44 marked synthetic; pooled from cid22-train/val, clic2025-1024, gb82, gb82-sc, imageflow, kadid10k, qoi-benchmark). Full per-class distributions, ROC-AUC ranking for every feature, recommended operating thresholds, and the recalibration candidates that were considered and rejected are recorded in docs/calibration-corpus-2026-04-27.md.

Top-line empirical findings:

  • Strongest single screen-vs-photo discriminator: [feature::AnalysisFeature::PatchFraction] (AUC = 0.880, F1 = 0.769 at >= 0.27).
  • Strongest photo classifier: [feature::AnalysisFeature::NaturalLikelihood] (F1 = 0.924 at >= 0.06).
  • Near-deterministic line-art signal: [feature::AnalysisFeature::LineArtScore] > 0 (F1 = 0.978).
  • Derived likelihoods empirically saturate at ~0.70, not 1.0 — operating thresholds live in the 0.3–0.6 band, not 0.8+.

Spearman ρ |≥ 0.85| pairs (mostly structural, all kept):

  • distinct_color_binspalette_density: ρ = +1.00 — derived.
  • flat_color_block_ratioscreen_content_likelihood: ρ = +0.99 — structural, the former is the latter’s primary input.
  • uniformityaq_map_mean: ρ = −0.97. Both measure block flatness; drive different knobs (T1 fast-path vs T3 AQ-driven trellis-λ).
  • chroma_complexitycolourfulness: ρ = +0.97. Both quantify chroma spread; one is normalised, the other is the raw Hasler-Süsstrunk M3 published scale — keep both for ergonomics.
  • aq_map_meannoise_floor_y: ρ = +0.91. Both fall when blocks are flat. Different downstream knobs (zenjpeg trellis-λ vs pre_blur), keep both.
  • noise_floor_yscreen_content_likelihood: ρ = −0.89. Screen content is clean-by-construction; photos carry sensor noise. Different passes, different knobs.
  • cb_sharpnesscr_sharpness: ρ = +0.89. Co-vary by construction in natural content.

No deletion candidates were found. Numeric drift in 0.1.x patches is permitted by the threshold contract above; the committed docs/calibration-corpus-2026-04-27.md is the pre-ship baseline against which patch drift can be compared.

Modules§

feature
Public API surface: a stable, opaque, set-composable feature interface that lets multiple codecs share one analysis pass.

Enums§

AnalyzeError
Errors returned from the public analyzer entries.

Functions§

analyze_features
Run the analyzer over any PixelSlice for the requested feature set. Returns an opaque feature::AnalysisResults holding only the features the query asked for.
analyze_features_rgb8
Convenience entry for callers holding a packed RGB8 buffer plus a query. Panics on length mismatch or stride-construction failure.
try_analyze_features_rgb8
Fallible parallel of analyze_features_rgb8. Returns AnalyzeError::InvalidInput when the buffer length doesn’t match width * height * 3 or the resulting stride is invalid; returns AnalyzeError::OutOfMemory when (in a future fallible- allocation build) the analyzer’s working buffers can’t be reserved.