Removed the experimental luma-chroma color format.
It was a worthwhile experiment, but for it to really work it needs a really proper luma-chroma separation, which is both slower than I really want, and requires knowing the colorspace being used. I might make another go at this based on the TIFF LogLUV color format, requiring XYZ as input.
This commit is contained in:
parent
c1f516c2b6
commit
f13ffac7bd
|
@ -1,6 +1,6 @@
|
||||||
use bencher::{benchmark_group, benchmark_main, black_box, Bencher};
|
use bencher::{benchmark_group, benchmark_main, black_box, Bencher};
|
||||||
use rand::{rngs::SmallRng, FromEntropy, Rng};
|
use rand::{rngs::SmallRng, FromEntropy, Rng};
|
||||||
use trifloat::{rgb32, signed48, unsigned32};
|
use trifloat::{signed48, unsigned32};
|
||||||
|
|
||||||
//----
|
//----
|
||||||
|
|
||||||
|
@ -48,28 +48,6 @@ fn signed48_decode_100_values(bench: &mut Bencher) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rgb32_encode_100_values(bench: &mut Bencher) {
|
|
||||||
let mut rng = SmallRng::from_entropy();
|
|
||||||
bench.iter(|| {
|
|
||||||
let y = rng.gen::<f32>();
|
|
||||||
let x = rng.gen::<f32>();
|
|
||||||
let z = rng.gen::<f32>();
|
|
||||||
for _ in 0..100 {
|
|
||||||
black_box(rgb32::encode(black_box((x, y, z))));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rgb32_decode_100_values(bench: &mut Bencher) {
|
|
||||||
let mut rng = SmallRng::from_entropy();
|
|
||||||
bench.iter(|| {
|
|
||||||
let v = rng.gen::<u32>();
|
|
||||||
for _ in 0..100 {
|
|
||||||
black_box(rgb32::decode(black_box(v)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//----
|
//----
|
||||||
|
|
||||||
benchmark_group!(
|
benchmark_group!(
|
||||||
|
@ -78,7 +56,5 @@ benchmark_group!(
|
||||||
unsigned32_decode_100_values,
|
unsigned32_decode_100_values,
|
||||||
signed48_encode_100_values,
|
signed48_encode_100_values,
|
||||||
signed48_decode_100_values,
|
signed48_decode_100_values,
|
||||||
rgb32_encode_100_values,
|
|
||||||
rgb32_decode_100_values,
|
|
||||||
);
|
);
|
||||||
benchmark_main!(benches);
|
benchmark_main!(benches);
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
//! The motivating use-case for this is compactly storing HDR RGB colors. But
|
//! The motivating use-case for this is compactly storing HDR RGB colors. But
|
||||||
//! it may be useful for other things as well.
|
//! it may be useful for other things as well.
|
||||||
|
|
||||||
pub mod rgb32;
|
|
||||||
pub mod signed48;
|
pub mod signed48;
|
||||||
pub mod unsigned32;
|
pub mod unsigned32;
|
||||||
|
|
||||||
|
|
|
@ -1,321 +0,0 @@
|
||||||
//! Encoding/decoding for a specialized HDR RGB 32-bit storage format.
|
|
||||||
//!
|
|
||||||
//! The motivation for this format is to separate out the luma of
|
|
||||||
//! the color from its chromaticity, in the same spirit as most
|
|
||||||
//! image and video compression approaches, and then allocate more
|
|
||||||
//! bits to storing the luma component since that's what the human
|
|
||||||
//! eye is most sensitive to.
|
|
||||||
//!
|
|
||||||
//! This encoding first transforms the color into a Y (luma) component
|
|
||||||
//! and two chroma components (C1 and C2), and then fiddles those
|
|
||||||
//! components into a special 32-bit format. The Y component is stored
|
|
||||||
//! as an unsigned float, with 6 bits of exponent and 10 bits of
|
|
||||||
//! mantissa. The two chroma components are each stored as 8-bit
|
|
||||||
//! integers.
|
|
||||||
//!
|
|
||||||
//! The layout is:
|
|
||||||
//!
|
|
||||||
//! 1. Y-exponent: 6 bits
|
|
||||||
//! 2. Y-mantissa: 10 bits
|
|
||||||
//! 3. C1: 8 bits
|
|
||||||
//! 4. C2: 8 bits
|
|
||||||
//!
|
|
||||||
//! The Y-mantissa has an implicit leading one, giving 11 bits of
|
|
||||||
//! precision.
|
|
||||||
|
|
||||||
const EXP_BIAS: i32 = 23;
|
|
||||||
const CHROMA_QUANT: u32 = 254;
|
|
||||||
|
|
||||||
/// The largest value this format can store.
|
|
||||||
///
|
|
||||||
/// More precisely, this is the largest value that can be *reliably*
|
|
||||||
/// stored.
|
|
||||||
///
|
|
||||||
/// This can be exceeded by individual channels in limited cases due
|
|
||||||
/// to the color transform used. But values *at least* this large
|
|
||||||
/// can be relied on.
|
|
||||||
pub const MAX: f32 = ((1u64 << (63 - EXP_BIAS)) - (1 << (52 - EXP_BIAS))) as f32;
|
|
||||||
|
|
||||||
/// The smallest non-zero value this format can store.
|
|
||||||
///
|
|
||||||
/// Note that since this is effectively a shared-exponent format,
|
|
||||||
/// the numerical precision of all channels depends on the magnitude
|
|
||||||
/// of the over-all RGB color.
|
|
||||||
pub const MIN: f32 = 1.0 / (1 << (EXP_BIAS - 2)) as f32;
|
|
||||||
|
|
||||||
/// The output c1 and c2 values are always in the range [0, 1].
|
|
||||||
#[inline(always)]
|
|
||||||
fn rgb2ycc(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
|
|
||||||
let rb = (r + b) * 0.5;
|
|
||||||
let y = g + rb;
|
|
||||||
|
|
||||||
if r > b {
|
|
||||||
(y, rb / y, b / y)
|
|
||||||
} else {
|
|
||||||
(y, r / y, rb / y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The input c1 and c2 values should always be in the range [0, 1].
|
|
||||||
#[inline(always)]
|
|
||||||
fn ycc2rgb(y: f32, c1: f32, c2: f32) -> (f32, f32, f32) {
|
|
||||||
if c1 > c2 {
|
|
||||||
let rb = y * c1;
|
|
||||||
let g = y - rb;
|
|
||||||
let b = y * c2;
|
|
||||||
let r = (rb * 2.0) - b;
|
|
||||||
(r, g, b)
|
|
||||||
} else {
|
|
||||||
let rb = y * c2;
|
|
||||||
let g = y - rb;
|
|
||||||
let r = y * c1;
|
|
||||||
let b = (rb * 2.0) - r;
|
|
||||||
(r, g, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encodes three floating point RGB values into a packed 32-bit format.
|
|
||||||
///
|
|
||||||
/// Warning: negative values and NaN's are _not_ supported. There are
|
|
||||||
/// debug-only assertions in place to catch such values in the input
|
|
||||||
/// floats. Infinity in any channel will saturate the whole color to
|
|
||||||
/// the brightest representable white.
|
|
||||||
#[inline]
|
|
||||||
pub fn encode(floats: (f32, f32, f32)) -> u32 {
|
|
||||||
debug_assert!(
|
|
||||||
floats.0 >= 0.0
|
|
||||||
&& floats.1 >= 0.0
|
|
||||||
&& floats.2 >= 0.0
|
|
||||||
&& !floats.0.is_nan()
|
|
||||||
&& !floats.1.is_nan()
|
|
||||||
&& !floats.2.is_nan(),
|
|
||||||
"trifloat::rgb32::encode(): encoding to unsigned tri-floats only \
|
|
||||||
works correctly for positive, non-NaN numbers, but the numbers passed \
|
|
||||||
were: ({}, {}, {})",
|
|
||||||
floats.0,
|
|
||||||
floats.1,
|
|
||||||
floats.2
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert to luma-chroma format.
|
|
||||||
let (y, c1, c2) = rgb2ycc(floats.0, floats.1, floats.2);
|
|
||||||
|
|
||||||
// Bit-fiddle to get the float components of Y.
|
|
||||||
// This assumes we're working with a standard 32-bit IEEE float.
|
|
||||||
let y_ieee_bits = y.to_bits();
|
|
||||||
let y_mantissa = (y_ieee_bits >> 13) & 0b11_1111_1111;
|
|
||||||
let y_exp = ((y_ieee_bits >> 23) & 0b1111_1111) as i32 - 127;
|
|
||||||
|
|
||||||
// Encode C1 and C2 as 8-bit integers.
|
|
||||||
let c1_8bit = ((c1 * CHROMA_QUANT as f32) + 0.5) as u32;
|
|
||||||
let c2_8bit = ((c2 * CHROMA_QUANT as f32) + 0.5) as u32;
|
|
||||||
|
|
||||||
// Pack values into a u32 and return.
|
|
||||||
if y_exp <= (0 - EXP_BIAS) {
|
|
||||||
// Early-out corner-case:
|
|
||||||
// Luma is so dark that it will be zero at our precision,
|
|
||||||
// and hence black.
|
|
||||||
0
|
|
||||||
} else if y_exp >= (63 - EXP_BIAS) {
|
|
||||||
// Corner-case:
|
|
||||||
// Luma is so bright that it exceeds our max value, so saturate
|
|
||||||
// the luma.
|
|
||||||
if y.is_infinite() {
|
|
||||||
// If luma is infinity, our chroma values are bogus, so
|
|
||||||
// just go with white.
|
|
||||||
let tmp = CHROMA_QUANT / 2;
|
|
||||||
0xffff0000 | tmp << 8 | tmp
|
|
||||||
} else {
|
|
||||||
0xffff0000 | (c1_8bit << 8) | c2_8bit
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Common case.
|
|
||||||
let exp = (y_exp + EXP_BIAS) as u32;
|
|
||||||
(exp << 26) | (y_mantissa << 16) | (c1_8bit << 8) | c2_8bit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decodes a packed HDR RGB 32-bit format into three full
|
|
||||||
/// floating point RGB numbers.
|
|
||||||
#[inline]
|
|
||||||
pub fn decode(packed_rgb: u32) -> (f32, f32, f32) {
|
|
||||||
// Pull out Y, C1, and C2 from the packed bits.
|
|
||||||
let y = {
|
|
||||||
let exp = (packed_rgb & 0xfc00_0000) >> 26;
|
|
||||||
if exp == 0 {
|
|
||||||
0.0
|
|
||||||
} else {
|
|
||||||
f32::from_bits(
|
|
||||||
((exp + (127 - EXP_BIAS as u32)) << 23) | ((packed_rgb & 0x03ff_0000) >> 3),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let c1 = {
|
|
||||||
let c1_8bit = (packed_rgb >> 8) & 0xff;
|
|
||||||
(c1_8bit as f32) * (1.0 / CHROMA_QUANT as f32)
|
|
||||||
};
|
|
||||||
let c2 = {
|
|
||||||
let c2_8bit = packed_rgb & 0xff;
|
|
||||||
(c2_8bit as f32) * (1.0 / CHROMA_QUANT as f32)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert back to RGB.
|
|
||||||
ycc2rgb(y, c1, c2)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) {
|
|
||||||
decode(encode(floats))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn all_zeros() {
|
|
||||||
let fs = (0.0f32, 0.0f32, 0.0f32);
|
|
||||||
|
|
||||||
let tri = encode(fs);
|
|
||||||
let fs2 = decode(tri);
|
|
||||||
|
|
||||||
assert_eq!(tri, 0u32);
|
|
||||||
assert_eq!(fs, fs2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn powers_of_two() {
|
|
||||||
let mut n = 1.0f32 / 65536.0;
|
|
||||||
for _ in 0..48 {
|
|
||||||
let fs = (n, n, n);
|
|
||||||
|
|
||||||
assert_eq!(fs, round_trip(fs));
|
|
||||||
n *= 2.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn integers() {
|
|
||||||
let mut n = 1.0f32;
|
|
||||||
for _ in 0..2048 {
|
|
||||||
let fs = (n, n, n);
|
|
||||||
|
|
||||||
assert_eq!(fs, round_trip(fs));
|
|
||||||
n += 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn color_saturation() {
|
|
||||||
let fs1 = (1.0, 0.0, 0.0);
|
|
||||||
let fs2 = (0.0, 1.0, 0.0);
|
|
||||||
let fs3 = (0.0, 0.0, 1.0);
|
|
||||||
|
|
||||||
assert_eq!(fs1, round_trip(fs1));
|
|
||||||
assert_eq!(fs2, round_trip(fs2));
|
|
||||||
assert_eq!(fs3, round_trip(fs3));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn num_saturate() {
|
|
||||||
let fs = (10000000000000.0, 10000000000000.0, 10000000000000.0);
|
|
||||||
|
|
||||||
assert_eq!((MAX, MAX, MAX), round_trip(fs));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn num_inf_saturate() {
|
|
||||||
use std::f32::INFINITY;
|
|
||||||
let fs = (INFINITY, INFINITY, INFINITY);
|
|
||||||
|
|
||||||
assert_eq!((MAX, MAX, MAX), round_trip(fs));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn num_partial_saturate() {
|
|
||||||
let fs1 = (10000000000000.0, 0.0, 0.0);
|
|
||||||
let fs2 = (0.0, 10000000000000.0, 0.0);
|
|
||||||
let fs3 = (0.0, 0.0, 10000000000000.0);
|
|
||||||
|
|
||||||
assert_eq!((MAX * 4.0, 0.0, 0.0), round_trip(fs1));
|
|
||||||
assert_eq!((0.0, MAX * 2.0, 0.0), round_trip(fs2));
|
|
||||||
assert_eq!((0.0, 0.0, MAX * 4.0), round_trip(fs3));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn largest_value() {
|
|
||||||
let fs1 = (MAX, MAX, MAX);
|
|
||||||
let fs2 = (MAX, 0.0, 0.0);
|
|
||||||
let fs3 = (0.0, MAX, 0.0);
|
|
||||||
let fs4 = (0.0, 0.0, MAX);
|
|
||||||
|
|
||||||
assert_eq!(fs1, round_trip(fs1));
|
|
||||||
assert_eq!(fs2, round_trip(fs2));
|
|
||||||
assert_eq!(fs3, round_trip(fs3));
|
|
||||||
assert_eq!(fs4, round_trip(fs4));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn smallest_value() {
|
|
||||||
let fs1 = (MIN, MIN, MIN);
|
|
||||||
let fs2 = (MIN, 0.0, 0.0);
|
|
||||||
let fs3 = (0.0, MIN, 0.0);
|
|
||||||
let fs4 = (0.0, 0.0, MIN);
|
|
||||||
|
|
||||||
assert_eq!(fs1, round_trip(fs1));
|
|
||||||
assert_eq!(fs2, round_trip(fs2));
|
|
||||||
assert_eq!(fs3, round_trip(fs3));
|
|
||||||
assert_eq!(fs4, round_trip(fs4));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn underflow() {
|
|
||||||
let fs1 = (MIN * 0.5, 0.0, 0.0);
|
|
||||||
let fs2 = (0.0, MIN * 0.25, 0.0);
|
|
||||||
let fs3 = (0.0, 0.0, MIN * 0.5);
|
|
||||||
|
|
||||||
assert_eq!(round_trip(fs1), (0.0, 0.0, 0.0));
|
|
||||||
assert_eq!(round_trip(fs2), (0.0, 0.0, 0.0));
|
|
||||||
assert_eq!(round_trip(fs3), (0.0, 0.0, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn nans_01() {
|
|
||||||
encode((std::f32::NAN, 0.0, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn nans_02() {
|
|
||||||
encode((0.0, std::f32::NAN, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn nans_03() {
|
|
||||||
encode((0.0, 0.0, std::f32::NAN));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn negative_01() {
|
|
||||||
encode((-1.0, 0.0, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn negative_02() {
|
|
||||||
encode((0.0, -1.0, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn negative_03() {
|
|
||||||
encode((0.0, 0.0, -1.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn negative_04() {
|
|
||||||
encode((-0.0, -0.0, -0.0));
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user