A constraint‑driven palette optimizer for Rust.
Chromoxide solves for optimal color palettes given image evidence, slot‑wise hard domains, and pairwise constraints. It uses continuous optimization in Oklab/OkLCh color space with multi‑start L‑BFGS.
Note: This project is in early development. APIs may change.
chromoxide– Core optimization engine, domain definitions, and solver.chromoxide‑image– Image preprocessing, saliency detection, sampling, and support extraction.chrox– CLI palette generation tool built on top ofchromoxideandchromoxide-image.
Cap construction and cap enforcement are separate:
chromoxide-image::CapEstimatordecides how a cap surface is built from image evidence (MaxObservedorStatistical). The default builds a statistical conditional cap and a same-lightness global chroma profile from all prepared pixels, not from the exported 24 samples.CapPolicydecides how a slot enforces the already-built cap (Ignore,HardIntersect,SoftPenalty, orAdaptiveSoftPenalty).
HardIntersect never lowers a slot's user chroma.min; problems whose required
minimum exceeds the cap over the whole slot domain are rejected during
validation.
SoftPenalty remains strict: it uses evidence at the queried (L, h).
AdaptiveSoftPenalty blends that conditional cap with the global profile using
the original, pre-smoothing support confidence. Supported hues stay
conditional; an unsupported semantic hue inherits only the image's chroma style
at the same lightness. The fallback therefore remains tied to source evidence
and the slot's existing user chroma interval.
Add to your Cargo.toml (replace the git URL with your own):
[dependencies]
chromoxide = { git = "https://github.com/werdxz/chromoxide" }
chromoxide-image = { git = "https://github.com/werdxz/chromoxide" }Install the CLI with:
cargo install --git https://github.com/werdxz/chromoxide chroxBasic example using pre‑computed samples:
use chromoxide::*;
let samples = vec![
WeightedSample::new(Oklch { l: 0.35, c: 0.12, h: 0.2 }.to_oklab(), 2.0, 0.5),
WeightedSample::new(Oklch { l: 0.75, c: 0.10, h: 2.8 }.to_oklab(), 2.0, 0.8),
];
let slots = vec![
SlotSpec {
name: "a".into(),
domain: SlotDomain {
lightness: Interval { min: 0.2, max: 0.9 },
chroma: Interval { min: 0.0, max: 0.2 },
hue: HueDomain::Any,
cap_policy: CapPolicy::Ignore,
chroma_epsilon: 0.02,
},
},
SlotSpec {
name: "b".into(),
domain: SlotDomain {
lightness: Interval { min: 0.2, max: 0.9 },
chroma: Interval { min: 0.0, max: 0.2 },
hue: HueDomain::Any,
cap_policy: CapPolicy::Ignore,
chroma_epsilon: 0.02,
},
},
];
let problem = PaletteProblem {
slots,
samples,
image_cap: None,
terms: vec![WeightedTerm {
weight: 3.0,
name: Some("cover".into()),
term: Term::Cover(CoverTerm {
slots: vec![0, 1],
tau: 0.02,
delta: 0.03,
}),
}],
config: SolveConfig::default(),
};
let solution = solve(&problem)?;For a full image‑based pipeline, see the examples in chromoxide‑image.
For CLI-driven palette generation and template rendering, see crates/chrox/README.md.
The chrox CLI defaults to content-derived deterministic runs: it hashes the
image bytes plus the image and global solve configuration, then derives
domain-separated RNG seeds for image sampling and each palette solve. Use
--seed <U64> to select an explicit deterministic master seed, or
--randomize to generate and print a fresh master seed for exploration.
The core library keeps solve as a random
convenience API and solve_with_rng as the
caller-controlled RNG API. Use
solve_with_seed for the stable deterministic
contract; every local start gets an independent ChaCha stream.
Determinism is versioned with the algorithm. Bitwise-identical OkLCh values are not promised across different algorithm versions; within one algorithm version, the goal is stable final hex output for identical inputs.
Run the workspace examples with:
cargo run --example neutral_ladder --release
cargo run --example basic_pipeline --releaseBuild local documentation:
cargo doc --workspace --openFor project terminology, see VOCABULARY.md. For pipeline and parameter explanations, see ALGORITHMS.md.
This project is licensed under the MIT License – see the LICENSE file for details.