Skip to main content

image_palette/
lib.rs

1use std::{cell::RefCell, collections::HashMap, path::Path, rc::Rc, str::FromStr};
2
3use error::ImageError;
4use image::{
5    DynamicImage,
6    ImageError::{IoError, Unsupported},
7    RgbImage, RgbaImage,
8};
9
10mod error;
11
12/// Open the image located at the path specified, return 16 dominant colors.
13///
14/// # Examples
15/// ```no_run
16/// let (colors, width, height) = image_palette::load("test.jpg").unwrap();
17/// println!("total: {}", width * height);
18/// for color in colors {
19///   println!("{}: {}", color.rgb().to_hex(), color.count());
20/// }
21/// ```
22pub fn load<P>(path: P) -> Result<(Vec<Record>, u32, u32), ImageError>
23where
24    P: AsRef<Path>,
25{
26    OcTree::load_with_maxcolor(path, 16)
27}
28
29/// Open the image located at the path specified, return {max_color} dominant colors.
30///
31/// # Examples
32/// ```no_run
33/// let (colors, width, height) = image_palette::load_with_maxcolor("test.jpg", 8).unwrap();
34/// println!("total: {}", width * height);
35/// for color in colors {
36///   println!("{}: {}", color.rgb().to_hex(), color.count());
37/// }
38/// ```
39pub fn load_with_maxcolor<P>(path: P, max_color: u8) -> Result<(Vec<Record>, u32, u32), ImageError>
40where
41    P: AsRef<Path>,
42{
43    OcTree::load_with_maxcolor(path, max_color)
44}
45
46/// Open the image with image crate, return {max_color} dominant colors.
47///
48/// # Examples
49/// ```no_run
50/// let image = image::open("test.jpg").unwrap();
51/// let (colors, width, height) = image_palette::load_by_image_with_maxcolor(&image, 8).unwrap();
52/// println!("total: {}", width * height);
53/// for color in colors {
54///   println!("{}: {}", color.rgb().to_hex(), color.count());
55/// }
56/// ```
57pub fn load_by_image_with_maxcolor(
58    image: &DynamicImage,
59    max_color: u8,
60) -> Result<(Vec<Record>, u32, u32), ImageError> {
61    // 等价于旧行为:不合并、不按占比过滤、不截断输出数量
62    let options = PaletteOptions {
63        extract_max: max_color,
64        output_max: max_color,
65        merge_delta_e: 0.0,
66        min_ratio: 0.0,
67    };
68    load_by_image_with_options(image, &options)
69}
70
71/// 主色提取配置。
72///
73/// `extract_max` 控制 OctTree 内部 bin 数(越大越能区分相近色);
74/// `output_max` 控制最终返回的主色数量上限;
75/// `merge_delta_e` 为 CIELAB ΔE 合并阈值,`0.0` 表示不合并(需启用 `lab` feature 才生效);
76/// `min_ratio` 为最小像素占比(百分比),低于此值的颜色会被丢弃。
77#[derive(Debug, Clone, Copy)]
78pub struct PaletteOptions {
79    pub extract_max: u8,
80    pub output_max: u8,
81    pub merge_delta_e: f32,
82    pub min_ratio: f32,
83}
84
85impl Default for PaletteOptions {
86    fn default() -> Self {
87        Self {
88            extract_max: 16,
89            output_max: 8,
90            merge_delta_e: 10.0,
91            min_ratio: 0.01,
92        }
93    }
94}
95
96impl PaletteOptions {
97    pub fn new() -> Self {
98        Self::default()
99    }
100    pub fn with_extract_max(mut self, v: u8) -> Self {
101        self.extract_max = v;
102        self
103    }
104    pub fn with_output_max(mut self, v: u8) -> Self {
105        self.output_max = v;
106        self
107    }
108    pub fn with_merge_delta_e(mut self, v: f32) -> Self {
109        self.merge_delta_e = v;
110        self
111    }
112    pub fn with_min_ratio(mut self, v: f32) -> Self {
113        self.min_ratio = v;
114        self
115    }
116}
117
118/// 按配置提取主色:OctTree 提取 → 相近色合并(可选)→ 占比过滤 → 数量截断。
119///
120/// # Examples
121/// ```no_run
122/// let image = image::open("test.jpg").unwrap();
123/// let opts = image_palette::PaletteOptions::default();
124/// let (colors, width, height) = image_palette::load_by_image_with_options(&image, &opts).unwrap();
125/// for color in &colors {
126///   println!("{}: {}", color.rgb().to_hex(), color.count());
127/// }
128/// ```
129pub fn load_by_image_with_options(
130    image: &DynamicImage,
131    options: &PaletteOptions,
132) -> Result<(Vec<Record>, u32, u32), ImageError> {
133    let (mut list, width, height) = OcTree::load_by_image(image, options.extract_max as u32);
134
135    #[cfg(feature = "lab")]
136    {
137        if options.merge_delta_e > 0.0 {
138            list = merge_similar(list, options.merge_delta_e);
139        }
140    }
141    #[cfg(not(feature = "lab"))]
142    {
143        if options.merge_delta_e > 0.0 {
144            return Err(ImageError::InvalidParameter);
145        }
146    }
147
148    let total = (width * height) as f32;
149    if options.min_ratio > 0.0 {
150        list.retain(|r| r.count as f32 / total * 100.0 >= options.min_ratio);
151    }
152    list.sort_by(|a, b| b.count.cmp(&a.count));
153    if list.len() > options.output_max as usize {
154        list.truncate(options.output_max as usize);
155    }
156    Ok((list, width, height))
157}
158
159/// 基于 CIELAB ΔE 的相近色层次聚类合并(质心链接)。
160///
161/// 每个 OctTree 色先各成一簇,反复合并当前距离最小的两簇,
162/// 直到最近距离超过 `delta_e`。距离用折算明度的 CIE76:
163///
164/// ```text
165/// ΔE' = sqrt((ΔL / 2)^2 + Δa^2 + Δb^2)
166/// ```
167///
168/// 明度差权重减半,让同色相的明度渐变(如纸纹的浅棕→深棕)
169/// 收成少数主色,而不是凑满 8 个;红/蓝等不同色相主要在 a/b 轴,不受影响。
170/// 合并时 RGB 与 Lab 均按像素 count 加权平均。
171#[cfg(feature = "lab")]
172fn merge_similar(records: Vec<Record>, delta_e: f32) -> Vec<Record> {
173    if delta_e <= 0.0 || records.is_empty() {
174        return records;
175    }
176
177    // 簇:(sum_r*count, sum_g*count, sum_b*count, count, sum_l*count, sum_a*count, sum_b_lab*count)
178    // RGB 必须按像素 count 加权累加,否则回除 count 后会得到接近 #000000 的错误均值
179    let mut clusters: Vec<(u64, u64, u64, u32, f64, f64, f64)> = records
180        .into_iter()
181        .map(|rec| {
182            let lab = rec.rgb.to_lab();
183            let count = rec.count as u64;
184            (
185                rec.rgb.r as u64 * count,
186                rec.rgb.g as u64 * count,
187                rec.rgb.b as u64 * count,
188                rec.count,
189                lab.l as f64 * rec.count as f64,
190                lab.a as f64 * rec.count as f64,
191                lab.b as f64 * rec.count as f64,
192            )
193        })
194        .collect();
195
196    // 反复合并当前 ΔE' 最小的两簇,直到最小距离超过阈值
197    loop {
198        let mut best: Option<(usize, usize, f32)> = None;
199        for i in 0..clusters.len() {
200            let ci = &clusters[i];
201            let ci_cnt = ci.3 as f64;
202            let cil = ci.4 / ci_cnt;
203            let cia = ci.5 / ci_cnt;
204            let cib = ci.6 / ci_cnt;
205            for j in (i + 1)..clusters.len() {
206                let cj = &clusters[j];
207                let cj_cnt = cj.3 as f64;
208                let cjl = cj.4 / cj_cnt;
209                let cja = cj.5 / cj_cnt;
210                let cjb = cj.6 / cj_cnt;
211                let dl = (cil - cjl) / 2.0;
212                let da = cia - cja;
213                let db = cib - cjb;
214                let dist = ((dl * dl + da * da + db * db) as f32).sqrt();
215                match best {
216                    None => best = Some((i, j, dist)),
217                    Some((_, _, d)) if dist < d => best = Some((i, j, dist)),
218                    _ => {}
219                }
220            }
221        }
222
223        match best {
224            Some((i, j, dist)) if dist <= delta_e => {
225                // 把 j 并入 i(i < j),然后丢弃 j
226                let cj = clusters[j].clone();
227                let ci = &mut clusters[i];
228                ci.0 += cj.0;
229                ci.1 += cj.1;
230                ci.2 += cj.2;
231                ci.3 += cj.3;
232                ci.4 += cj.4;
233                ci.5 += cj.5;
234                ci.6 += cj.6;
235                clusters.remove(j);
236            }
237            _ => break,
238        }
239    }
240
241    let mut result: Vec<Record> = clusters
242        .into_iter()
243        .map(|c| {
244            let cnt = c.3 as u64;
245            // 四舍五入,避免整数截断把浅色系统性压暗
246            let r = ((c.0 + cnt / 2) / cnt) as u8;
247            let g = ((c.1 + cnt / 2) / cnt) as u8;
248            let b = ((c.2 + cnt / 2) / cnt) as u8;
249            Record {
250                rgb: RGB { r, g, b },
251                count: c.3,
252            }
253        })
254        .collect();
255    result.sort_by(|a, b| b.count.cmp(&a.count));
256    result
257}
258
259#[derive(Debug)]
260struct OcTree {
261    leaf_num: u32,
262    to_reduce: [Vec<Rc<RefCell<Node>>>; 8],
263    max_color: u32,
264}
265
266impl OcTree {
267    fn load_with_maxcolor<P>(path: P, max_color: u8) -> Result<(Vec<Record>, u32, u32), ImageError>
268    where
269        P: AsRef<Path>,
270    {
271        let image = image::open(path).map_err(|error| match error {
272            Unsupported(error) => ImageError::UnsupportedFile(error),
273            IoError(error) => ImageError::IoError(error),
274            error => ImageError::Unknown(error),
275        })?;
276
277        Ok(Self::load_by_image(&image, max_color.into()))
278    }
279
280    fn load_by_image(image: &DynamicImage, max_color: u32) -> (Vec<Record>, u32, u32) {
281        const ARRAY_REPEAT_VALUE: Vec<Rc<RefCell<Node>>> = Vec::new();
282        let mut tree = OcTree {
283            leaf_num: 0,
284            to_reduce: [ARRAY_REPEAT_VALUE; 8],
285            max_color,
286        };
287
288        let rgb = image.to_rgb8();
289        let image_data = ImageData::from(&rgb);
290
291        let root_share = tree.create_node(0);
292
293        for color in image_data.data {
294            tree.add_color(&root_share, color, 0);
295            while tree.leaf_num > tree.max_color {
296                tree.reduce_tree();
297            }
298        }
299
300        let mut map: HashMap<RGB, u32> = HashMap::new();
301        colors_stats(&root_share, &mut map);
302        let mut list = Vec::new();
303        for (rgb, count) in map {
304            list.push(Record { rgb, count });
305        }
306        list.sort_by(|a, b| b.count.cmp(&a.count));
307        (list, image_data.width, image_data.height)
308    }
309
310    fn create_node(&mut self, level: usize) -> Rc<RefCell<Node>> {
311        let node = Node::new();
312        let node_share: Rc<RefCell<Node>> = Rc::new(RefCell::new(node));
313
314        if level == 7 {
315            let mut node_mut: std::cell::RefMut<Node> = node_share.borrow_mut();
316            node_mut.is_leaf = true;
317            self.leaf_num += 1;
318        } else {
319            let a: Rc<RefCell<Node>> = Rc::clone(&node_share);
320            self.to_reduce[level].push(a);
321            self.to_reduce[level].sort_by_key(|k: &Rc<RefCell<Node>>| k.borrow().pixel_count);
322        }
323
324        node_share
325    }
326
327    fn add_color(&mut self, node_share: &Rc<RefCell<Node>>, rgb: RGB, level: usize) {
328        let mut node: std::cell::RefMut<Node> = node_share.borrow_mut();
329        if node.is_leaf {
330            node.pixel_count += 1;
331            node.r += rgb.r as u32;
332            node.g += rgb.g as u32;
333            node.b += rgb.b as u32;
334        } else {
335            let r = rgb.r >> (7 - level) & 1;
336            let g = rgb.g >> (7 - level) & 1;
337            let b = rgb.b >> (7 - level) & 1;
338
339            let idx = ((r << 2) + (g << 1) + b) as usize;
340
341            if node.children[idx].is_none() {
342                let child_share: Rc<RefCell<Node>> = self.create_node(level + 1);
343                node.children[idx] = Some(child_share);
344            }
345
346            self.add_color(node.children[idx].as_ref().unwrap(), rgb, level + 1);
347        }
348    }
349
350    fn reduce_tree(&mut self) {
351        // find the deepest level of node
352        let mut lv: isize = 6;
353
354        while lv >= 0 && self.to_reduce[lv as usize].len() == 0 {
355            lv -= 1;
356        }
357        if lv < 0 {
358            return;
359        }
360
361        let node_share = self.to_reduce[lv as usize].pop().unwrap();
362        let mut node = node_share.borrow_mut();
363
364        // merge children
365        let mut r = 0;
366        let mut g = 0;
367        let mut b = 0;
368        let mut pixel_count = 0;
369
370        for i in 0..8 {
371            if node.children[i].is_none() {
372                continue;
373            }
374            let child_share = node.children[i].as_ref().unwrap();
375            let child = child_share.borrow();
376
377            r += child.r;
378            g += child.g;
379            b += child.b;
380            pixel_count += child.pixel_count;
381            self.leaf_num -= 1;
382        }
383
384        node.is_leaf = true;
385        node.r = r;
386        node.g = g;
387        node.b = b;
388        node.pixel_count = pixel_count;
389
390        self.leaf_num += 1;
391    }
392}
393
394fn colors_stats(node_share: &Rc<RefCell<Node>>, map: &mut HashMap<RGB, u32>) {
395    let node = node_share.borrow_mut();
396    if node.is_leaf {
397        let r = (node.r / node.pixel_count) as u8;
398        let g = (node.g / node.pixel_count) as u8;
399        let b = (node.b / node.pixel_count) as u8;
400        let rgb = RGB::from(&[r, g, b]);
401        if let Some(x) = map.get_mut(&rgb) {
402            *x = *x + node.pixel_count;
403        } else {
404            map.insert(rgb, node.pixel_count);
405        }
406    } else {
407        for i in 0..8 {
408            if node.children[i].is_some() {
409                colors_stats(node.children[i].as_ref().unwrap(), map);
410            }
411        }
412    }
413}
414
415impl From<&RgbImage> for ImageData {
416    fn from(image: &RgbImage) -> Self {
417        let (width, height) = image.dimensions();
418        let size = (width * height) as usize;
419
420        let data = image
421            .pixels()
422            .fold(Vec::with_capacity(size), |mut pixels, pixel| {
423                pixels.push(RGB::from(&[pixel[0], pixel[1], pixel[2]]));
424                pixels
425            });
426
427        Self {
428            data,
429            width,
430            height,
431        }
432    }
433}
434
435impl From<&RgbaImage> for ImageData {
436    fn from(image: &RgbaImage) -> Self {
437        let (width, height) = image.dimensions();
438        let size = (width * height) as usize;
439
440        let data = image.pixels().filter(|pixels| pixels[3] > 0).fold(
441            Vec::with_capacity(size),
442            |mut pixels, pixel| {
443                pixels.push(RGB::from(&[pixel[0], pixel[1], pixel[2]]));
444                pixels
445            },
446        );
447
448        Self {
449            data,
450            width,
451            height,
452        }
453    }
454}
455
456#[derive(Debug, Clone, Eq, Hash, PartialEq)]
457pub struct RGB {
458    pub r: u8,
459    pub g: u8,
460    pub b: u8,
461}
462
463impl RGB {
464    pub fn from(rgb: &[u8; 3]) -> RGB {
465        RGB {
466            r: rgb[0],
467            g: rgb[1],
468            b: rgb[2],
469        }
470    }
471
472    pub fn to_hex(&self) -> String {
473        let r = format!("{:0>2}", format!("{:X}", self.r));
474        let g = format!("{:0>2}", format!("{:X}", self.g));
475        let b = format!("{:0>2}", format!("{:X}", self.b));
476        format!("#{}{}{}", r, g, b)
477    }
478
479    #[cfg(feature = "lab")]
480    pub fn to_lab(&self) -> lab::Lab {
481        lab::Lab::from_rgb(&[self.r, self.g, self.b])
482    }
483}
484
485impl FromStr for RGB {
486    type Err = std::num::ParseIntError;
487
488    fn from_str(hex_code: &str) -> Result<Self, Self::Err> {
489        let r: u8 = u8::from_str_radix(&hex_code[1..3], 16)?;
490        let g: u8 = u8::from_str_radix(&hex_code[3..5], 16)?;
491        let b: u8 = u8::from_str_radix(&hex_code[5..7], 16)?;
492
493        Ok(RGB { r, g, b })
494    }
495}
496
497struct ImageData {
498    data: Vec<RGB>,
499    width: u32,
500    height: u32,
501}
502
503#[derive(Debug)]
504struct Node {
505    is_leaf: bool,
506    r: u32,
507    g: u32,
508    b: u32,
509    pixel_count: u32,
510    children: [Option<Rc<RefCell<Node>>>; 8],
511}
512
513impl Node {
514    fn new() -> Node {
515        const ARRAY_REPEAT_VALUE: Option<Rc<RefCell<Node>>> = None;
516        Node {
517            is_leaf: false,
518            r: 0,
519            g: 0,
520            b: 0,
521            pixel_count: 0,
522            children: [ARRAY_REPEAT_VALUE; 8],
523        }
524    }
525}
526
527#[derive(Debug, Clone)]
528pub struct Record {
529    rgb: RGB,
530    count: u32,
531}
532
533impl Record {
534    pub fn new(rgb: RGB, count: u32) -> Self {
535        Record { rgb, count }
536    }
537    pub fn rgb(&self) -> &RGB {
538        &self.rgb
539    }
540    pub fn count(&self) -> u32 {
541        self.count
542    }
543}