mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Merge pull request #4048 from dotCooCoo/fix/trace-library-neutral-tones
Image Trace: Document Library keeps neutral tones off saturated library colors
This commit is contained in:
@@ -257,7 +257,9 @@ pub enum Palette {
|
||||
#[serde(alias = "Automatic")]
|
||||
Automatic,
|
||||
/// The most used colours of a swatch library ([`TraceParams::swatches`]), at most `colors` of
|
||||
/// them, exactly as they are.
|
||||
/// them, exactly as they are. Each pixel takes the library color nearest in lightness and
|
||||
/// hue, with hue and chroma counted three times as much as lightness, so that neutral tones
|
||||
/// stay off saturated colors.
|
||||
#[serde(alias = "library", alias = "Document Library")]
|
||||
DocumentLibrary,
|
||||
}
|
||||
|
||||
@@ -110,19 +110,17 @@ pub(crate) fn quantize_keeping(img: &Raster, params: &TraceParams) -> (Quantized
|
||||
}
|
||||
let mut lut = vec![0u16; 1 << 15];
|
||||
let center_labs = labs(¢ers);
|
||||
let library = params.palette == Palette::DocumentLibrary && !params.swatches.is_empty();
|
||||
let nearest = if library { nearest_library } else { nearest_lab };
|
||||
for (i, c, _) in &every {
|
||||
lut[*i] = nearest_lab(¢er_labs, oklab(*c)) as u16;
|
||||
lut[*i] = nearest(¢er_labs, oklab(*c)) as u16;
|
||||
}
|
||||
let labels =
|
||||
img.rgba.as_chunks::<4>().0.iter().map(|p| if p[3] < 128 { TRANSPARENT } else { lut[hk.key(p[0], p[1], p[2])] }).collect();
|
||||
let palette = centers.iter().map(|c| [c[0].round() as u8, c[1].round() as u8, c[2].round() as u8]).collect();
|
||||
let q = compact(labels, palette);
|
||||
// A swatch library's colours are used as they are.
|
||||
if params.palette == Palette::DocumentLibrary && !params.swatches.is_empty() {
|
||||
q
|
||||
} else {
|
||||
merge_mixed(q, img.width as usize, img.height as usize, &rare)
|
||||
}
|
||||
if library { q } else { merge_mixed(q, img.width as usize, img.height as usize, &rare) }
|
||||
};
|
||||
let q = reduce(params.colors);
|
||||
let q = settled(q, img, &own);
|
||||
@@ -614,6 +612,24 @@ fn nearest_lab(labs: &[[f64; 3]], c: [f64; 3]) -> usize {
|
||||
best.1
|
||||
}
|
||||
|
||||
/// How much more a difference in hue and chroma (Oklab a and b) counts than one in lightness when
|
||||
/// a pixel takes a library color.
|
||||
const LIBRARY_CHROMA_WEIGHT: f64 = 3.0;
|
||||
|
||||
/// The index of the library color of `labs` (Oklab) nearest to `c` (Oklab), with differences in
|
||||
/// hue and chroma counted [`LIBRARY_CHROMA_WEIGHT`] times: a neutral or muted tone takes a neutral
|
||||
/// library color of another lightness rather than a saturated one of its own lightness.
|
||||
fn nearest_library(labs: &[[f64; 3]], c: [f64; 3]) -> usize {
|
||||
let mut best = (f64::INFINITY, 0);
|
||||
for (i, m) in labs.iter().enumerate() {
|
||||
let d = (m[0] - c[0]).powi(2) + (LIBRARY_CHROMA_WEIGHT * (m[1] - c[1])).powi(2) + (LIBRARY_CHROMA_WEIGHT * (m[2] - c[2])).powi(2);
|
||||
if d < best.0 {
|
||||
best = (d, i);
|
||||
}
|
||||
}
|
||||
best.1
|
||||
}
|
||||
|
||||
fn labs(colors: &[[f64; 3]]) -> Vec<[f64; 3]> {
|
||||
colors.iter().map(|c| oklab(*c)).collect()
|
||||
}
|
||||
@@ -678,7 +694,8 @@ fn flat_colors(used: &[Used], detail: f64) -> Option<usize> {
|
||||
/// The most histogram bins flat art's main colours spread over (a few bins per colour).
|
||||
const FLAT_BINS: usize = 96;
|
||||
|
||||
/// Document Library: the `k` swatches (at most) that the most pixels are nearest to.
|
||||
/// Document Library: the `k` swatches (at most) that the most pixels are nearest to (see
|
||||
/// [`nearest_library`]).
|
||||
fn library_colors(used: &[Used], swatches: &[[u8; 3]], k: usize) -> Vec<[f64; 3]> {
|
||||
let mut all: Vec<[f64; 3]> = vec![];
|
||||
for s in swatches {
|
||||
@@ -690,7 +707,7 @@ fn library_colors(used: &[Used], swatches: &[[u8; 3]], k: usize) -> Vec<[f64; 3]
|
||||
let mut weight = vec![0.0; all.len()];
|
||||
let all_labs = labs(&all);
|
||||
for (_, c, n) in used {
|
||||
if let Some(w) = weight.get_mut(nearest_lab(&all_labs, oklab(*c))) {
|
||||
if let Some(w) = weight.get_mut(nearest_library(&all_labs, oklab(*c))) {
|
||||
*w += n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,6 +687,40 @@ fn document_library_uses_the_most_used_swatches_exactly() {
|
||||
assert_eq!(trace(&hard, &none).palette.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_library_keeps_neutral_tones_off_a_saturated_swatch() {
|
||||
// Black, gray, brown and blue patches on a light background. The gray and the brown lie
|
||||
// nearer to the blue than to the black or the light color in plain Oklab distance.
|
||||
let img = Raster::from_fn(400, 100, |x, y| {
|
||||
let inside = (10..90).contains(&(x % 100)) && (10..90).contains(&y);
|
||||
match (inside, x / 100) {
|
||||
(false, _) => [240, 240, 220, 255],
|
||||
(true, 0) => [0, 0, 0, 255],
|
||||
(true, 1) => [128, 128, 128, 255],
|
||||
(true, 2) => [112, 88, 64, 255],
|
||||
(true, _) => [40, 120, 240, 255],
|
||||
}
|
||||
});
|
||||
let params = TraceParams {
|
||||
mode: Mode::Color,
|
||||
palette: Palette::DocumentLibrary,
|
||||
swatches: vec![[0, 0, 0], [240, 240, 220], [40, 120, 240]],
|
||||
colors: 3,
|
||||
paths: 90.0,
|
||||
corners: 60.0,
|
||||
noise: 4,
|
||||
method: Method::Abutting,
|
||||
..TraceParams::default()
|
||||
};
|
||||
let res = trace(&img, ¶ms);
|
||||
let blue: Vec<_> = res.paths.iter().filter(|p| p.color == [40, 120, 240]).collect();
|
||||
assert!(!blue.is_empty(), "the blue patch stays blue: {:?}", res.palette);
|
||||
for p in blue {
|
||||
let b = p.path.bounds().unwrap();
|
||||
assert!(b.x0 > 299.0, "a blue path reaches {b:?}: the gray or the brown patch traced as blue");
|
||||
}
|
||||
}
|
||||
|
||||
fn strokes(fills: bool) -> TraceParams {
|
||||
TraceParams { strokes: true, fills, stroke_width: 6.0, noise: 4, ..bw() }
|
||||
}
|
||||
@@ -1424,7 +1458,7 @@ fn a_color_whose_every_path_straightens_to_no_area_is_left_out_of_the_palette()
|
||||
// A light rectangle with a black block on it and a one-pixel row of a pale blue along its lower
|
||||
// edge, on a transparent background. Traced with a library of the light, the black and a
|
||||
// blue, the row is the only part in the blue.
|
||||
const BLUE: [u8; 3] = [40, 120, 240];
|
||||
const BLUE: [u8; 3] = [60, 130, 235];
|
||||
let img = Raster::from_fn(200, 120, |x, y| {
|
||||
if (60..140).contains(&x) && (70..90).contains(&y) {
|
||||
BLACK
|
||||
|
||||
@@ -32,6 +32,11 @@ white beside a cream), at most six; scattered specks of a colour add none. Adobe
|
||||
its own palettes are chosen; this is VectorCraft's own method (see
|
||||
[image-trace-quality.md](image-trace-quality.md#auto-color-keeps-rare-colours)).
|
||||
|
||||
**Document Library** traces with the library's colors as they are. Each pixel takes the library
|
||||
color nearest to it in lightness and hue, with a difference in hue or chroma counted three times
|
||||
as much as one in lightness, so a gray or a muted brown takes a black, white or other neutral
|
||||
library color rather than a saturated color of the same lightness.
|
||||
|
||||
**Abutting and Overlapping.** Abutting is the default Method, and the Default, Auto Color, 3 Colors,
|
||||
6 Colors and 16 Colors presets use it. With Abutting, two neighboring colors share one exact edge. The
|
||||
canvas smooths the edges of each path on its own, so each of the two paths covers only part of the
|
||||
@@ -43,7 +48,6 @@ pixels and 16,384 pixels on a side. A raster export shows the line as the canvas
|
||||
Type Optimized anti-aliasing, or when it uses Art Optimized and is larger than either limit.
|
||||
Overlapping stacks the colors instead: a lower color continues under the edge of the color above it,
|
||||
and no background shows along that edge.
|
||||
|
||||
**Large images.** An image of more than 1500 pixels on a side is traced as any other, only more
|
||||
slowly, and the trace says so: `imageTrace.make` returns the note in `warnings` and the status bar
|
||||
shows it (the [results optimization](https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html)
|
||||
|
||||
@@ -788,6 +788,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Image Trace, thin closed outlines:** generated `fit::snap_area_tests` verify nonzero interiors after straightening, opposite closed-boundary winding and Noise controls; see [quality notes](image-trace-quality.md#thin-closed-outlines-with-snap-curves-to-lines). Scores and hours unchanged; open-piece fitting is unchanged.
|
||||
- **Image Trace, flattened slivers:** in a Color, Grayscale or Black and White trace with Fills, a traced region whose outline straightens onto one line (a single pixel, or a one-pixel row along an edge, its pieces shared with the regions beside it) is left out instead of becoming a selectable path with no area, which paints nothing. Snap Curves To Lines straightens more outlines, so it leaves out more of these. Without Fills, the region's 1 px stroke draws it, and it is kept. Code-drawn tests in `crates/trace`.
|
||||
- **Image Trace, Auto Color rare colours:** the Automatic palette keeps colours too rare for its area count when they are distinct (Oklab) and drawn as coherent regions, at most six, protected from mixed-colour merging and Noise; code-drawn stem, highlight, low-contrast and scattered-noise tests (see [quality notes](image-trace-quality.md#auto-color-keeps-rare-colours)).
|
||||
- **Image Trace, Document Library keeps neutral tones neutral:** each pixel takes the library color nearest in Oklab with differences in hue and chroma counted three times, so a gray or a muted brown traces as a neutral library color instead of a saturated one of similar lightness; a code-drawn test of gray and brown patches with a black, light and blue library in `crates/trace`.
|
||||
- **Image Trace, source image on hold:** while the eye button next to the Image Trace panel's View is held down, the canvas shows the selected Image Trace objects' source image whatever their View, and shows their View again on release, with no change to the document or the undo history.
|
||||
- **Image Trace, large images:** images over 1500 px on a side trace with a warning in the result and the status bar; the anchor limit is 8,000,000 (2,000,000 on the web), measured at about 90 bytes an anchor (see [quality notes](image-trace-quality.md#large-images)).
|
||||
- **Image Trace, Flat Logo working size:** Flat Logo traces at most 1024 px on a side; a larger decoded raster (the 64-megapixel copy for a bigger image) is averaged down in whole-number blocks first, and the trace's `warnings` name the size it was traced at and the image's own size, in place of the slower-trace and 64-megapixel notes; code-drawn 1024 and 1025 px tests in `crates/trace` and `crates/engine`.
|
||||
|
||||
Reference in New Issue
Block a user