Build & bench

This commit is contained in:
asonix 2024-02-18 00:13:20 -06:00
parent a0df593342
commit a07038a61d
8 changed files with 658 additions and 6 deletions

View file

@ -6,3 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
blurhash = { version = "0.2.0", path = "../blurhash-rs" }
criterion = "0.5.1"
image = "0.24.8"
[[bench]]
name = "benchy"
harness = false

63
benches/benchy.rs Normal file
View file

@ -0,0 +1,63 @@
use blurhash_update::{Components, ImageBounds};
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use image::EncodableLayout;
pub fn criterion_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("blurhash");
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::encode(4, 3, width, height, black_box(bytes)).unwrap());
});
});
}
group.finish();
let mut group = c.benchmark_group("blurhash-update");
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 mut encoder = blurhash_update::encoder(
Components { x: 4, y: 3 },
ImageBounds { width, height },
)
.unwrap();
encoder.update(bytes);
let _bhash = black_box(encoder.finalize());
});
});
}
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

BIN
data/19dd1c444d1c7939.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

BIN
data/f73d2ee39133d871.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

BIN
data/shenzi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

15
src/base83.rs Normal file
View file

@ -0,0 +1,15 @@
static CHARACTERS: [u8; 83] = [
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F',
b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V',
b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l',
b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', b'#', b'$',
b'%', b'*', b'+', b',', b'-', b'.', b':', b';', b'=', b'?', b'@', b'[', b']', b'^', b'_', b'{',
b'|', b'}', b'~',
];
pub(crate) fn encode(value: u32, length: u32, s: &mut String) {
for i in 1..=length {
let digit: u32 = (value / u32::pow(83, length - i)) % 83;
s.push(CHARACTERS[digit as usize] as char);
}
}

View file

@ -1,14 +1,321 @@
pub fn add(left: usize, right: usize) -> usize {
left + right
mod base83;
mod srgb_lookup;
use std::f32::consts::PI;
use srgb_lookup::SRGB_LOOKUP;
pub struct Components {
pub x: u32,
pub y: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct ImageBounds {
pub width: u32,
pub height: u32,
}
struct ComponentState {
x: u32,
y: u32,
basis: f32,
}
#[derive(Debug)]
pub struct ComponentError;
impl std::fmt::Display for ComponentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Components out of bounds")
}
}
pub struct Encoder {
index: usize,
components: Components,
factors: Vec<(ComponentState, [f32; 3])>,
bounds: ImageBounds,
}
pub fn encoder(components: Components, bounds: ImageBounds) -> Result<Encoder, ComponentError> {
Encoder::new(components, bounds)
}
impl Encoder {
fn new(Components { x, y }: Components, bounds: ImageBounds) -> Result<Self, ComponentError> {
if !(1..=9).contains(&x) || !(1..=9).contains(&y) {
return Err(ComponentError);
}
Ok(Self {
index: 0,
components: Components { x, y },
factors: (0..y)
.flat_map(|y| {
(0..x).map(move |x| (ComponentState { x, y, basis: 0. }, [0., 0., 0.]))
})
.collect(),
bounds,
})
}
pub fn update(&mut self, buf: &[u8]) {
const BYTES_PER_PIXEL: usize = 4;
// get offset in terms of already-processed bytes
let offset = self.index % BYTES_PER_PIXEL;
// get offset in terms of remaining bytes on head of buf
let offset = (BYTES_PER_PIXEL - offset) % BYTES_PER_PIXEL;
for (ComponentState { basis, .. }, [_, g, b]) in self.factors.iter_mut() {
for (byte, value) in buf[..offset].iter().zip(
[&mut *b, &mut *g][0..offset.saturating_sub(2)]
.iter_mut()
.rev(),
) {
**value += *basis * SRGB_LOOKUP[*byte as usize]
}
}
let pixels = ((self.index + offset) / BYTES_PER_PIXEL) as u32;
let mut chunks = buf[offset..].chunks_exact(BYTES_PER_PIXEL);
for (i, chunk) in (&mut chunks).enumerate() {
let px = pixels + i as u32;
let px_x = px % self.bounds.width;
let px_y = px / self.bounds.width;
for (ComponentState { x, y, .. }, [r, g, b]) in self.factors.iter_mut() {
let basis = compute_basis(
*x as _,
*y as _,
px_x as _,
px_y as _,
self.bounds.width as _,
self.bounds.height as _,
);
*r += basis * SRGB_LOOKUP[chunk[0] as usize];
*g += basis * SRGB_LOOKUP[chunk[1] as usize];
*b += basis * SRGB_LOOKUP[chunk[2] as usize];
}
}
if !chunks.remainder().is_empty() {
let px = pixels + (buf[offset..].len() / BYTES_PER_PIXEL) as u32;
let px_x = px % self.bounds.width;
let px_y = px / self.bounds.width;
for (ComponentState { x, y, basis }, [r, g, b]) in self.factors.iter_mut() {
*basis = compute_basis(
*x as _,
*y as _,
px_x as _,
px_y as _,
self.bounds.width as _,
self.bounds.height as _,
);
for (byte, value) in chunks.remainder().iter().zip([&mut *r, &mut *g, &mut *b]) {
*value += *basis * SRGB_LOOKUP[*byte as usize]
}
}
}
self.index += buf.len();
}
pub fn finalize(mut self) -> String {
for (ComponentState { x, y, .. }, [r, g, b]) in &mut self.factors {
let normalisation = if *x == 0 && *y == 0 { 1. } else { 2. };
let scale = normalisation / (self.bounds.width * self.bounds.height) as f32;
*r *= scale;
*g *= scale;
*b *= scale;
}
let mut blurhash = String::new();
let (_, dc) = self.factors[0];
let ac = &self.factors[1..];
let size_flag = self.components.x - 1 + (self.components.y - 1) * 9;
base83::encode(size_flag, 1, &mut blurhash);
let maximum_value = if !ac.is_empty() {
let maximum = ac.iter().fold(0.0_f32, |maximum, (_, [r, g, b])| {
maximum.max(r.abs()).max(g.abs()).max(b.abs())
});
let quantized_maximum = (maximum * 166. - 0.5).floor().max(0.) as u32;
base83::encode(quantized_maximum, 1, &mut blurhash);
(quantized_maximum + 1) as f32 / 166.
} else {
base83::encode(0, 1, &mut blurhash);
1.
};
base83::encode(encode_dc(dc), 4, &mut blurhash);
for (_, rgb) in ac {
base83::encode(encode_ac(*rgb, maximum_value), 2, &mut blurhash);
}
blurhash
}
}
fn compute_basis(
component_x: f32,
component_y: f32,
px_x: f32,
px_y: f32,
width: f32,
height: f32,
) -> f32 {
f32::cos(PI * component_x * px_x / width) * f32::cos(PI * component_y * px_y / height)
}
fn encode_dc([r, g, b]: [f32; 3]) -> u32 {
let r = linear_to_srgb(r);
let g = linear_to_srgb(g);
let b = linear_to_srgb(b);
(r << 16) + (g << 8) + b
}
fn encode_ac([r, g, b]: [f32; 3], maximum_value: f32) -> u32 {
let r = encode_ac_digit(r, maximum_value);
let g = encode_ac_digit(g, maximum_value);
let b = encode_ac_digit(b, maximum_value);
r * 19 * 19 + g * 19 + b
}
fn encode_ac_digit(d: f32, maximum_value: f32) -> u32 {
((sign_pow(d / maximum_value, 0.5) * 9. + 9.5) as i32).clamp(0, 18) as u32
}
pub fn linear_to_srgb(value: f32) -> u32 {
let v = f32::max(0., f32::min(1., value));
if v <= 0.003_130_8 {
(v * 12.92 * 255. + 0.5).round() as u32
} else {
((1.055 * f32::powf(v, 1. / 2.4) - 0.055) * 255. + 0.5).round() as u32
}
}
fn sign(n: f32) -> f32 {
if n < 0. {
-1.
} else {
1.
}
}
pub fn sign_pow(val: f32, exp: f32) -> f32 {
sign(val) * val.abs().powf(exp)
}
#[cfg(test)]
mod tests {
use super::*;
use image::{EncodableLayout, GenericImageView};
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
fn contrived() {
let input = [
0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120, 0, 0, 60, 120,
0, 0, 60, 120, 0, 0, 60, 120, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60,
0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0, 120, 60, 0, 0,
];
let width = 4;
let height = 4;
let mut encoder = super::encoder(
crate::Components { x: 4, y: 3 },
crate::ImageBounds { width, height },
)
.unwrap();
encoder.update(&input);
let b1 = encoder.finalize();
let b2 = blurhash::encode(4, 3, width, height, &input).unwrap();
assert_eq!(b1, b2);
}
#[test]
fn matches_blurhash() {
let inputs = [
"data/19dd1c444d1c7939.png",
"data/f73d2ee39133d871.jpg",
"data/shenzi.png",
];
for input in inputs {
let img = image::open(input).unwrap();
let (width, height) = img.dimensions();
let mut encoder = super::encoder(
crate::Components { x: 4, y: 3 },
crate::ImageBounds { width, height },
)
.unwrap();
encoder.update(img.to_rgba8().as_bytes());
let b1 = encoder.finalize();
let b2 = blurhash::encode(4, 3, width, height, img.to_rgba8().as_bytes()).unwrap();
assert_eq!(b1, b2);
}
}
#[test]
fn matches_self_when_split() {
let inputs = [
"data/19dd1c444d1c7939.png",
"data/f73d2ee39133d871.jpg",
"data/shenzi.png",
];
for input in inputs {
let img = image::open(input).unwrap();
let (width, height) = img.dimensions();
let rgba8_img = img.to_rgba8();
let bytes = rgba8_img.as_bytes();
let mut encoder = super::encoder(
crate::Components { x: 4, y: 3 },
crate::ImageBounds { width, height },
)
.unwrap();
encoder.update(bytes);
let b1 = encoder.finalize();
for chunk_count in 2..20 {
encoder = super::encoder(
crate::Components { x: 4, y: 3 },
crate::ImageBounds { width, height },
)
.unwrap();
let chunk_size = bytes.len() / chunk_count;
for chunk in bytes.chunks(chunk_size) {
encoder.update(chunk);
}
let b2 = encoder.finalize();
assert_eq!(b1, b2);
}
}
}
}

258
src/srgb_lookup.rs Normal file
View file

@ -0,0 +1,258 @@
pub(crate) static SRGB_LOOKUP: [f32; 256] = [
0.0,
0.000303527,
0.000607054,
0.000910581,
0.001214108,
0.001517635,
0.001821162,
0.0021246888,
0.002428216,
0.002731743,
0.00303527,
0.0033465356,
0.003676507,
0.004024717,
0.004391442,
0.0047769533,
0.005181517,
0.0056053917,
0.0060488326,
0.006512091,
0.00699541,
0.0074990317,
0.008023192,
0.008568125,
0.009134057,
0.009721218,
0.010329823,
0.010960094,
0.011612245,
0.012286487,
0.012983031,
0.013702081,
0.014443844,
0.015208514,
0.015996292,
0.016807375,
0.017641952,
0.018500218,
0.019382361,
0.020288562,
0.02121901,
0.022173883,
0.023153365,
0.02415763,
0.025186857,
0.026241222,
0.027320892,
0.028426038,
0.029556833,
0.03071344,
0.03189603,
0.033104762,
0.034339808,
0.035601314,
0.036889445,
0.038204364,
0.039546236,
0.0409152,
0.04231141,
0.043735027,
0.045186203,
0.046665084,
0.048171822,
0.049706563,
0.051269468,
0.052860655,
0.05448028,
0.056128494,
0.057805434,
0.05951124,
0.06124607,
0.06301003,
0.06480328,
0.06662595,
0.06847818,
0.07036011,
0.07227186,
0.07421358,
0.07618539,
0.07818743,
0.08021983,
0.082282715,
0.084376216,
0.086500466,
0.088655606,
0.09084173,
0.09305898,
0.095307484,
0.09758736,
0.09989874,
0.10224175,
0.10461649,
0.10702311,
0.10946172,
0.111932434,
0.11443538,
0.11697067,
0.119538434,
0.1221388,
0.12477184,
0.1274377,
0.13013649,
0.13286833,
0.13563335,
0.13843162,
0.1412633,
0.14412849,
0.14702728,
0.1499598,
0.15292616,
0.15592647,
0.15896086,
0.1620294,
0.16513222,
0.1682694,
0.1714411,
0.17464739,
0.17788841,
0.18116423,
0.18447499,
0.18782076,
0.19120167,
0.19461781,
0.1980693,
0.20155624,
0.2050787,
0.20863685,
0.21223073,
0.21586053,
0.21952623,
0.22322798,
0.22696589,
0.23074007,
0.23455065,
0.23839766,
0.2422812,
0.2462014,
0.25015837,
0.25415218,
0.2581829,
0.26225072,
0.26635566,
0.27049786,
0.27467737,
0.27889434,
0.2831488,
0.2874409,
0.2917707,
0.29613832,
0.30054384,
0.30498737,
0.30946895,
0.31398875,
0.31854683,
0.32314324,
0.32777813,
0.33245158,
0.33716366,
0.34191445,
0.3467041,
0.3515327,
0.35640025,
0.36130688,
0.3662527,
0.37123778,
0.37626222,
0.3813261,
0.38642952,
0.39157256,
0.3967553,
0.40197787,
0.4072403,
0.4125427,
0.41788515,
0.42326775,
0.42869055,
0.4341537,
0.43965724,
0.44520125,
0.45078585,
0.45641106,
0.46207705,
0.46778384,
0.47353154,
0.47932023,
0.48514998,
0.4910209,
0.49693304,
0.5028866,
0.50888145,
0.5149178,
0.5209957,
0.5271152,
0.5332765,
0.5394796,
0.5457246,
0.5520115,
0.5583405,
0.56471163,
0.5711249,
0.5775805,
0.5840785,
0.5906189,
0.5972019,
0.6038274,
0.6104956,
0.61720663,
0.62396044,
0.6307572,
0.63759696,
0.64447975,
0.6514057,
0.65837485,
0.66538733,
0.6724432,
0.67954254,
0.68668544,
0.6938719,
0.701102,
0.70837593,
0.71569365,
0.72305524,
0.7304609,
0.73791057,
0.74540436,
0.7529423,
0.76052463,
0.7681513,
0.77582234,
0.7835379,
0.79129803,
0.79910284,
0.80695236,
0.8148467,
0.82278585,
0.83076996,
0.8387991,
0.8468733,
0.8549927,
0.8631573,
0.8713672,
0.87962234,
0.8879232,
0.8962694,
0.90466136,
0.9130987,
0.92158204,
0.9301109,
0.9386859,
0.9473066,
0.9559735,
0.9646863,
0.9734455,
0.9822506,
0.9911022,
1.0,
];