Skip to content

Commit

Permalink
Speed up Lab conversion with Srgb/LinSrgb fast path
Browse files Browse the repository at this point in the history
Srgb/LinSrgb conversion between u8 and f32/f64 has been special-cased
to use a lookup table and approximation. This PR uses that
implementation where possible.

Speedup gains increase as image size increases. On a 4500x3000 image,
a 44% speedup was seen reducing the time from 7.8s to 4.3s after
switching from `into_format` to using `into_linear`. The speedup is
closer to 15-17% on smaller images (320x200) but still noticeable
when measured.

Swap to using palette's FromStr implementation for colors. Now 3 or 6
digit hex colors are permitted.
  • Loading branch information
okaneco committed Aug 2, 2023
1 parent 3068455 commit a5f396a
Show file tree
Hide file tree
Showing 5 changed files with 24 additions and 52 deletions.
18 changes: 9 additions & 9 deletions src/bin/kmeans_colors/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ pub fn run(opt: Opt) -> Result<(), Box<dyn Error>> {
let lab: Vec<Lab<D65, f32>> = if !opt.transparent {
from_component_slice::<Srgba<u8>>(img_vec)
.iter()
.map(|x| x.into_format::<_, f32>().into_color())
.map(|x| x.into_linear::<_, f32>().into_color())
.collect()
} else {
from_component_slice::<Srgba<u8>>(img_vec)
.iter()
.filter(|x| x.alpha == 255)
.map(|x| x.into_format::<_, f32>().into_color())
.map(|x| x.into_linear::<_, f32>().into_color())
.collect()
};

Expand Down Expand Up @@ -112,7 +112,7 @@ pub fn run(opt: Opt) -> Result<(), Box<dyn Error>> {
let centroids = &result
.centroids
.iter()
.map(|x| Srgb::from_color(*x).into_format())
.map(|&x| Srgb::from_linear(x.into_color()))
.collect::<Vec<Srgb<u8>>>();
let lab: Vec<Srgb<u8>> = Srgb::map_indices_to_centroids(centroids, &result.indices);

Expand All @@ -130,7 +130,7 @@ pub fn run(opt: Opt) -> Result<(), Box<dyn Error>> {
let mut indices = Vec::with_capacity(img_vec.len());
let lab: Vec<Lab<D65, f32>> = from_component_slice::<Srgba<u8>>(img_vec)
.iter()
.map(|x| x.into_format::<_, f32>().into_color())
.map(|x| x.into_linear::<_, f32>().into_color())
.collect();
Lab::<D65, f32>::get_closest_centroid(&lab, &result.centroids, &mut indices);

Expand Down Expand Up @@ -329,7 +329,7 @@ pub fn find_colors(
for c in colors {
centroids.push(
(parse_color(c.trim_start_matches('#'))?)
.into_format()
.into_linear::<f32>()
.into_color(),
);
}
Expand All @@ -346,13 +346,13 @@ pub fn find_colors(
let lab: Vec<Lab<D65, f32>> = if !transparent {
from_component_slice::<Srgba<u8>>(img_vec)
.iter()
.map(|x| x.into_format::<_, f32>().into_color())
.map(|x| x.into_linear::<_, f32>().into_color())
.collect()
} else {
from_component_slice::<Srgba<u8>>(img_vec)
.iter()
.filter(|x| x.alpha == 255)
.map(|x| x.into_format::<_, f32>().into_color())
.map(|x| x.into_linear::<_, f32>().into_color())
.collect()
};

Expand All @@ -371,7 +371,7 @@ pub fn find_colors(
if !transparent {
let rgb_centroids = &centroids
.iter()
.map(|x| Srgb::from_color(*x).into_format())
.map(|&x| Srgb::from_linear(x.into_color()))
.collect::<Vec<Srgb<u8>>>();
let lab: Vec<Srgb<u8>> =
Srgb::map_indices_to_centroids(rgb_centroids, &indices);
Expand All @@ -386,7 +386,7 @@ pub fn find_colors(
} else {
let rgb_centroids = &centroids
.iter()
.map(|x| Srgb::from_color(*x).into_format())
.map(|&x| Srgb::from_linear(x.into_color()))
.collect::<Vec<Srgb>>();

let mut indices = Vec::with_capacity(img_vec.len());
Expand Down
14 changes: 6 additions & 8 deletions src/bin/kmeans_colors/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@ impl From<std::time::SystemTimeError> for CliError {

impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
CliError::File(ref err) => write!(f, "File error: {}", err),
CliError::InvalidHex => {
write!(f, "Error: Invalid hex color length, must be 6 characters.")
}
CliError::Parse(ref err) => write!(f, "Parse error: {}", err),
CliError::Time(ref err) => write!(f, "Time error: {}", err),
match self {
CliError::File(err) => write!(f, "{err}"),
CliError::Parse(err) => write!(f, "{err}"),
CliError::Time(err) => write!(f, "{err}"),
CliError::InvalidHex => write!(f, "Invalid hex color, must be 3 or 6 digts"),
}
}
}
Expand All @@ -41,9 +39,9 @@ impl std::error::Error for CliError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CliError::File(err) => Some(err),
CliError::InvalidHex => None,
CliError::Parse(err) => Some(err),
CliError::Time(err) => Some(err),
CliError::InvalidHex => None,
}
}
}
2 changes: 1 addition & 1 deletion src/bin/kmeans_colors/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use args::{Command, Opt};

fn main() {
if let Err(e) = try_main() {
eprintln!("{}", e);
eprintln!("kmeans_colors: {e}");
process::exit(1);
}
}
Expand Down
36 changes: 5 additions & 31 deletions src/bin/kmeans_colors/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fmt::Write;
use std::fs::File;
use std::io::BufWriter;
use std::path::Path;
use std::str::FromStr;

use image::ImageEncoder;
use palette::{IntoColor, Srgb};
Expand All @@ -12,37 +13,10 @@ use kmeans_colors::{Calculate, CentroidData};

/// Parse hex string to Rgb color.
pub fn parse_color(c: &str) -> Result<Srgb<u8>, CliError> {
let red = u8::from_str_radix(
match &c.get(0..2) {
Some(x) => x,
None => {
eprintln!("Invalid color: {}", c);
return Err(CliError::InvalidHex);
}
},
16,
)?;
let green = u8::from_str_radix(
match &c.get(2..4) {
Some(x) => x,
None => {
eprintln!("Invalid color: {}", c);
return Err(CliError::InvalidHex);
}
},
16,
)?;
let blue = u8::from_str_radix(
match &c.get(4..6) {
Some(x) => x,
None => {
eprintln!("Invalid color: {}", c);
return Err(CliError::InvalidHex);
}
},
16,
)?;
Ok(Srgb::new(red, green, blue))
Srgb::from_str(c).or_else(|_| {
eprintln!("Invalid color: {c}");
Err(CliError::InvalidHex)
})
}

/// Prints colors and percentage of their appearance in an image buffer.
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
//! // Convert RGB [u8] buffer to Lab for k-means
//! let lab: Vec<Lab> = from_component_slice::<Srgb<u8>>(&img_vec)
//! .iter()
//! .map(|x| x.into_format().into_color())
//! .map(|x| x.into_linear().into_color())
//! .collect();
//!
//! // Iterate over the runs, keep the best results
Expand All @@ -93,7 +93,7 @@
//! // Convert indexed colors back to Srgb<u8> for output
//! let rgb = &result.centroids
//! .iter()
//! .map(|x| Srgb::from_color(*x).into_format())
//! .map(|&x| Srgb::from_linear(x.into_color()))
//! .collect::<Vec<Srgb<u8>>>();
//! let buffer = Srgb::map_indices_to_centroids(&rgb, &result.indices);
//! # assert_eq!(into_component_slice(&buffer), [119, 119, 119, 119, 119, 119]);
Expand All @@ -115,7 +115,7 @@
//! # // Convert indexed colors back to Srgb<u8> for output
//! # let rgb = &result.centroids
//! # .iter()
//! # .map(|x| Srgb::from_color(*x).into_format())
//! # .map(|&x| Srgb::from_linear(x.into_color()))
//! # .collect::<Vec<Srgb<u8>>>();
//! # let buffer = Srgb::map_indices_to_centroids(&rgb, &result.indices);
//! # assert_eq!(into_component_slice(&buffer), [119, 119, 119, 119, 119, 119]);
Expand Down

0 comments on commit a5f396a

Please sign in to comment.