Bench and test auto
All checks were successful
/ clippy (push) Successful in 9s
/ tests (push) Successful in 48s
/ check (aarch64-unknown-linux-musl) (push) Successful in 6s
/ check (armv7-unknown-linux-musleabihf) (push) Successful in 7s
/ check (x86_64-unknown-linux-musl) (push) Successful in 6s

This commit is contained in:
asonix 2024-02-22 22:38:53 -06:00
parent f25d41a29d
commit 1f7957778f
3 changed files with 167 additions and 12 deletions

View file

@ -59,6 +59,33 @@ pub fn criterion_benchmark(c: &mut Criterion) {
group.finish();
let mut group = c.benchmark_group("blurhash-update-auto");
let inputs = [
"data/19dd1c444d1c7939.png",
"data/f73d2ee39133d871.jpg",
"data/shenzi.png",
];
for input in inputs {
group.bench_with_input(BenchmarkId::from_parameter(input), input, |b, i| {
let img = image::open(i).unwrap();
let width = img.width();
let height = img.height();
let rgba = img.to_rgba8();
let bytes = rgba.as_bytes();
b.iter(|| {
let _bhash = black_box(blurhash_update::auto_encode(
ImageBounds { width, height },
bytes,
));
});
});
}
group.finish();
let mut group = c.benchmark_group("blurhash-update-skip");
let inputs = [

View file

@ -12,6 +12,9 @@ struct Args {
/// Height of the provided image
#[clap(long)]
height: u32,
#[clap(long, default_value = "8")]
skip: u32,
}
// Example usage:
@ -20,8 +23,16 @@ struct Args {
// cargo r --example --release -- --width blah --height blah
// ```
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Args { width, height } = Args::parse();
let mut encoder = Encoder::new(Components { x: 4, y: 3 }, ImageBounds { width, height }, 8)?;
let Args {
width,
height,
skip,
} = Args::parse();
let mut encoder = Encoder::new(
Components { x: 4, y: 3 },
ImageBounds { width, height },
skip,
)?;
let mut stdin = std::io::stdin().lock();
let mut buf = [0u8; 1024];

View file

@ -42,7 +42,13 @@ struct ComponentState {
/// Error raised when too many components are requested
#[derive(Debug)]
pub struct ComponentError;
pub enum ConfigurationError {
/// Component values are not within the required range.
InvalidComponentCount,
/// Skip value must not be zero
ZeroSkip,
}
/// Encoder type used to produce blurhashes
pub struct Encoder {
@ -60,15 +66,95 @@ pub fn encode(
components: Components,
bounds: ImageBounds,
rgba8_image: &[u8],
) -> Result<String, ComponentError> {
) -> Result<String, ConfigurationError> {
let mut encoder = Encoder::new(components, bounds, 1)?;
encoder.update(rgba8_image);
Ok(encoder.finalize())
}
/// A simple "encode this image please" function that automatically selects component and skip
/// values
///
/// The input image must be in the sRGB colorspace and be formatted as 8bit RGBA
pub fn auto_encode(bounds: ImageBounds, rgba8_image: &[u8]) -> String {
let mut encoder = Encoder::auto(bounds);
encoder.update(rgba8_image);
encoder.finalize()
}
// determine closest component ratio to input bounds
fn calculate_components(ImageBounds { width, height }: ImageBounds) -> Components {
let mut out = Components { x: 0, y: 0 };
let (out_longer, out_shorter, in_longer, in_shorter) = if width > height {
(&mut out.x, &mut out.y, width as f32, height as f32)
} else {
(&mut out.y, &mut out.x, height as f32, width as f32)
};
struct State {
similarity: f32,
ratio: (u32, u32),
}
let ratios = [(3, 3), (4, 3), (5, 3), (6, 3), (5, 2), (6, 2), (7, 2)];
let in_ratio = in_longer / in_shorter;
let State { ratio, .. } = ratios.into_iter().fold(
State {
similarity: f32::MAX,
ratio: (0, 0),
},
|state, (ratio_longer, ratio_shorter)| {
let ratio = ratio_longer as f32 / ratio_shorter as f32;
let diff = (ratio - in_ratio).abs();
if diff < state.similarity {
State {
similarity: diff,
ratio: (ratio_longer, ratio_shorter),
}
} else {
state
}
},
);
*out_longer = ratio.0;
*out_shorter = ratio.1;
out
}
// target 256ish total pixels to process
fn calculate_skip(ImageBounds { width, height }: ImageBounds) -> u32 {
let target_1d = f32::sqrt((width * height / 256) as f32).floor() as u32;
let mut base = 1;
loop {
if base * 2 < target_1d {
base *= 2;
} else {
break base;
}
}
}
impl Encoder {
/// Create an encoder that automatically picks Compoent and Skip values
///
/// This is a best-effort configuration
pub fn auto(bounds: ImageBounds) -> Self {
Self::new(calculate_components(bounds), bounds, calculate_skip(bounds))
.expect("Generated bounds are always valid")
}
/// Create a new Encoder to produce a blurhash
///
/// The provided component x and y values must be between 1 and 9 inclusive.
///
/// The `skip` value indicates how many pixels can be skipped when proccessing the image. this
/// value will be squared to produce the final skip value. When set to 1, no pixels will be
/// skipped, when set to 2, one in four pixels will be processed. when set to 3, one in 9
@ -79,9 +165,13 @@ impl Encoder {
Components { x, y }: Components,
bounds: ImageBounds,
skip: u32,
) -> Result<Self, ComponentError> {
) -> Result<Self, ConfigurationError> {
if !(1..=9).contains(&x) || !(1..=9).contains(&y) {
return Err(ComponentError);
return Err(ConfigurationError::InvalidComponentCount);
}
if skip == 0 {
return Err(ConfigurationError::ZeroSkip);
}
Ok(Self {
@ -266,13 +356,16 @@ fn sign_pow(val: f32, exp: f32) -> f32 {
sign(val) * val.abs().powf(exp)
}
impl std::fmt::Display for ComponentError {
impl std::fmt::Display for ConfigurationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Components out of bounds")
match self {
Self::InvalidComponentCount => write!(f, "Components out of bounds"),
Self::ZeroSkip => write!(f, "Skip value cannot be zero"),
}
}
}
impl std::error::Error for ComponentError {}
impl std::error::Error for ConfigurationError {}
#[cfg(test)]
mod tests {
@ -317,7 +410,31 @@ mod tests {
)
.unwrap();
assert_eq!(hash, output, "expected output wrong");
assert_eq!(hash, output, "wrong output for {input}");
}
}
#[test]
fn auto() {
let inputs = [
("data/19dd1c444d1c7939.png", (5, 3), 64),
("data/f73d2ee39133d871.jpg", (4, 3), 64),
("data/shenzi.png", (3, 3), 16),
];
for (input, expected_components, expected_skip) in inputs {
let img = image::open(input).unwrap();
let (width, height) = img.dimensions();
let components = super::calculate_components(crate::ImageBounds { width, height });
let skip = super::calculate_skip(crate::ImageBounds { width, height });
assert_eq!(
(components.x, components.y),
expected_components,
"wrong ratio for {input}"
);
assert_eq!(skip, expected_skip, "wrong skip for {input}");
}
}
@ -340,7 +457,7 @@ mod tests {
)
.unwrap();
assert_eq!(hash, output, "expected output wrong");
assert_eq!(hash, output, "wrong output for {input}");
}
}
@ -381,7 +498,7 @@ mod tests {
let b2 = encoder.finalize();
assert_eq!(b1, b2);
assert_eq!(b1, b2, "wrong hash for {input} with {chunk_count} chunks");
}
}
}