use crate::blur::{blur_mirrored_5x5, compute_separable5_weights};
use crate::image::{BufferPool, Image3F};
use imgref::ImgRef;
use rgb::{RGB, RGB8};
const MIXI0: f64 = 0.299_565_503_400_583_19;
const MIXI1: f64 = 0.633_730_878_338_259_36;
const MIXI2: f64 = 0.077_705_617_820_981_968;
const MIXI3: f64 = 1.755_748_364_328_735_3;
const MIXI4: f64 = 0.221_586_911_045_747_74;
const MIXI5: f64 = 0.693_913_880_441_161_42;
const MIXI6: f64 = 0.098_731_358_842_2;
const MIXI7: f64 = 1.755_748_364_328_735_3;
const MIXI8: f64 = 0.02;
const MIXI9: f64 = 0.02;
const MIXI10: f64 = 0.204_801_290_410_261_29;
const MIXI11: f64 = 12.226_454_707_163_354;
const MIN_01: f32 = 1.755_748_364_328_735_3;
const MIN_2: f32 = 12.226_454_707_163_354;
const K_INV_LOG2E: f32 = 1.0 / std::f32::consts::LOG2_E;
#[inline]
pub fn fast_log2f(x: f32) -> f32 {
const P0: f32 = -1.8503833400518310E-06;
const P1: f32 = 1.4287160470083755;
const P2: f32 = 7.4245873327820566E-01;
const Q0: f32 = 9.9032814277590719E-01;
const Q1: f32 = 1.0096718572241148;
const Q2: f32 = 1.7409343003366853E-01;
let x_bits = x.to_bits() as i32;
let exp_bits = x_bits.wrapping_sub(0x3f2aaaab_u32 as i32);
let exp_shifted = exp_bits >> 23;
let mantissa_bits = (x_bits - (exp_shifted << 23)) as u32;
let mantissa = f32::from_bits(mantissa_bits);
let exp_val = exp_shifted as f32;
let m = mantissa - 1.0;
let yp = P2 * m + P1;
let yp = yp * m + P0;
let yq = Q2 * m + Q1;
let yq = yq * m + Q0;
yp / yq + exp_val
}
#[inline]
pub fn gamma(v: f32) -> f32 {
const K_RET_MUL: f32 = 19.245_013_259_874_995 * K_INV_LOG2E;
const K_RET_ADD: f32 = -23.160_462_398_057_55;
const K_BIAS: f32 = 9.971_063_576_929_914_5;
let v = v.max(0.0);
let biased = v + K_BIAS;
let log = fast_log2f(biased);
K_RET_MUL * log + K_RET_ADD
}
#[inline]
pub fn opsin_absorbance(r: f32, g: f32, b: f32, clamp: bool) -> (f32, f32, f32) {
let out0 = (MIXI0 as f32) * r + (MIXI1 as f32) * g + (MIXI2 as f32) * b + (MIXI3 as f32);
let out1 = (MIXI4 as f32) * r + (MIXI5 as f32) * g + (MIXI6 as f32) * b + (MIXI7 as f32);
let out2 = (MIXI8 as f32) * r + (MIXI9 as f32) * g + (MIXI10 as f32) * b + (MIXI11 as f32);
if clamp {
(out0.max(MIN_01), out1.max(MIN_01), out2.max(MIN_2))
} else {
(out0, out1, out2)
}
}
#[archmage::autoversion]
pub fn opsin_dynamics_image(
_token: archmage::SimdToken,
rgb: &Image3F,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
let width = rgb.plane(0).width();
let height = rgb.plane(0).height();
let sigma = 1.2;
let weights = compute_separable5_weights(sigma);
let blurred_r = blur_mirrored_5x5(rgb.plane(0), &weights, pool);
let blurred_g = blur_mirrored_5x5(rgb.plane(1), &weights, pool);
let blurred_b = blur_mirrored_5x5(rgb.plane(2), &weights, pool);
let mut xyb = Image3F::from_pool_dirty(width, height, pool);
let min_val = 1e-4_f32;
let mixi0 = MIXI0 as f32;
let mixi1 = MIXI1 as f32;
let mixi2 = MIXI2 as f32;
let mixi3 = MIXI3 as f32;
let mixi4 = MIXI4 as f32;
let mixi5 = MIXI5 as f32;
let mixi6 = MIXI6 as f32;
let mixi7 = MIXI7 as f32;
let mixi8 = MIXI8 as f32;
let mixi9 = MIXI9 as f32;
let mixi10 = MIXI10 as f32;
let mixi11 = MIXI11 as f32;
let (plane_x, plane_y, plane_b) = xyb.planes_mut();
for y in 0..height {
let row_r = rgb.plane(0).row(y);
let row_g = rgb.plane(1).row(y);
let row_b = rgb.plane(2).row(y);
let row_blur_r = blurred_r.row(y);
let row_blur_g = blurred_g.row(y);
let row_blur_b = blurred_b.row(y);
let out_x = plane_x.row_mut(y);
let out_y = plane_y.row_mut(y);
let out_b = plane_b.row_mut(y);
for x in 0..width {
let r = row_r[x] * intensity_target;
let g = row_g[x] * intensity_target;
let b = row_b[x] * intensity_target;
let blurred_r_val = row_blur_r[x] * intensity_target;
let blurred_g_val = row_blur_g[x] * intensity_target;
let blurred_b_val = row_blur_b[x] * intensity_target;
let pre0 =
(mixi0 * blurred_r_val + mixi1 * blurred_g_val + mixi2 * blurred_b_val + mixi3)
.max(MIN_01)
.max(min_val);
let pre1 =
(mixi4 * blurred_r_val + mixi5 * blurred_g_val + mixi6 * blurred_b_val + mixi7)
.max(MIN_01)
.max(min_val);
let pre2 =
(mixi8 * blurred_r_val + mixi9 * blurred_g_val + mixi10 * blurred_b_val + mixi11)
.max(MIN_2)
.max(min_val);
let sensitivity0 = (gamma(pre0) / pre0).max(min_val);
let sensitivity1 = (gamma(pre1) / pre1).max(min_val);
let sensitivity2 = (gamma(pre2) / pre2).max(min_val);
let cur0 = ((mixi0 * r + mixi1 * g + mixi2 * b + mixi3) * sensitivity0).max(MIN_01);
let cur1 = ((mixi4 * r + mixi5 * g + mixi6 * b + mixi7) * sensitivity1).max(MIN_01);
let cur2 = ((mixi8 * r + mixi9 * g + mixi10 * b + mixi11) * sensitivity2).max(MIN_2);
out_x[x] = cur0 - cur1;
out_y[x] = cur0 + cur1;
out_b[x] = cur2;
}
}
blurred_r.recycle(pool);
blurred_g.recycle(pool);
blurred_b.recycle(pool);
xyb
}
pub fn srgb_to_xyb_butteraugli(
rgb: &[u8],
width: usize,
height: usize,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
assert_eq!(rgb.len(), width * height * 3);
let lut = &*SRGB_TO_LINEAR_LUT;
let mut linear = Image3F::from_pool_dirty(width, height, pool);
for y in 0..height {
let row_offset = y * width * 3;
let out_r = linear.plane_mut(0).row_mut(y);
for x in 0..width {
out_r[x] = lut[rgb[row_offset + x * 3] as usize];
}
}
for y in 0..height {
let row_offset = y * width * 3;
let out_g = linear.plane_mut(1).row_mut(y);
for x in 0..width {
out_g[x] = lut[rgb[row_offset + x * 3 + 1] as usize];
}
}
for y in 0..height {
let row_offset = y * width * 3;
let out_b = linear.plane_mut(2).row_mut(y);
for x in 0..width {
out_b[x] = lut[rgb[row_offset + x * 3 + 2] as usize];
}
}
let xyb = opsin_dynamics_image(&linear, intensity_target, pool);
linear.recycle(pool);
xyb
}
#[inline]
fn srgb_to_linear_slow(v: u8) -> f32 {
let v = v as f32 / 255.0;
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
}
pub(crate) static SRGB_TO_LINEAR_LUT: std::sync::LazyLock<[f32; 256]> =
std::sync::LazyLock::new(|| {
let mut lut = [0.0f32; 256];
for i in 0..256 {
lut[i] = srgb_to_linear_slow(i as u8);
}
lut
});
#[inline]
pub fn srgb_to_linear(v: u8) -> f32 {
SRGB_TO_LINEAR_LUT[v as usize]
}
pub fn linear_rgb_to_xyb_butteraugli(
rgb: &[f32],
width: usize,
height: usize,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
assert_eq!(rgb.len(), width * height * 3);
let mut linear = Image3F::from_pool_dirty(width, height, pool);
let (out_r, out_g, out_b) = linear.planes_mut();
for y in 0..height {
let row_offset = y * width * 3;
let row_r = out_r.row_mut(y);
let row_g = out_g.row_mut(y);
let row_b = out_b.row_mut(y);
for x in 0..width {
let idx = row_offset + x * 3;
row_r[x] = rgb[idx];
row_g[x] = rgb[idx + 1];
row_b[x] = rgb[idx + 2];
}
}
let xyb = opsin_dynamics_image(&linear, intensity_target, pool);
linear.recycle(pool);
xyb
}
#[allow(clippy::too_many_arguments)]
pub fn linear_planar_to_xyb_butteraugli(
r: &[f32],
g: &[f32],
b: &[f32],
width: usize,
height: usize,
stride: usize,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
assert!(stride >= width);
assert!(r.len() >= stride * height);
assert!(g.len() >= stride * height);
assert!(b.len() >= stride * height);
let mut linear = Image3F::from_pool_dirty(width, height, pool);
let (out_r, out_g, out_b) = linear.planes_mut();
for y in 0..height {
let src_offset = y * stride;
let row_r = out_r.row_mut(y);
let row_g = out_g.row_mut(y);
let row_b = out_b.row_mut(y);
row_r.copy_from_slice(&r[src_offset..src_offset + width]);
row_g.copy_from_slice(&g[src_offset..src_offset + width]);
row_b.copy_from_slice(&b[src_offset..src_offset + width]);
}
let xyb = opsin_dynamics_image(&linear, intensity_target, pool);
linear.recycle(pool);
xyb
}
#[allow(dead_code)]
pub(crate) fn imgref_srgb_to_xyb(
img: ImgRef<RGB8>,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
let width = img.width();
let height = img.height();
let lut = &*SRGB_TO_LINEAR_LUT;
let mut linear = Image3F::from_pool_dirty(width, height, pool);
let (out_r, out_g, out_b) = linear.planes_mut();
for (y, row) in img.rows().enumerate() {
let row_r = out_r.row_mut(y);
let row_g = out_g.row_mut(y);
let row_b = out_b.row_mut(y);
for (x, px) in row.iter().enumerate() {
row_r[x] = lut[px.r as usize];
row_g[x] = lut[px.g as usize];
row_b[x] = lut[px.b as usize];
}
}
let xyb = opsin_dynamics_image(&linear, intensity_target, pool);
linear.recycle(pool);
xyb
}
#[allow(dead_code)]
pub(crate) fn imgref_linear_to_xyb(
img: ImgRef<RGB<f32>>,
intensity_target: f32,
pool: &BufferPool,
) -> Image3F {
let width = img.width();
let height = img.height();
let mut linear = Image3F::from_pool_dirty(width, height, pool);
let (out_r, out_g, out_b) = linear.planes_mut();
for (y, row) in img.rows().enumerate() {
let row_r = out_r.row_mut(y);
let row_g = out_g.row_mut(y);
let row_b = out_b.row_mut(y);
for (x, px) in row.iter().enumerate() {
row_r[x] = px.r;
row_g[x] = px.g;
row_b[x] = px.b;
}
}
let xyb = opsin_dynamics_image(&linear, intensity_target, pool);
linear.recycle(pool);
xyb
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gamma_positive() {
let result = gamma(1.0);
assert!(result.is_finite());
assert!(result > -30.0 && result < 30.0);
}
#[test]
fn test_gamma_zero() {
let result = gamma(0.0);
assert!(result.is_finite());
}
#[test]
fn test_opsin_absorbance_bias() {
let (out0, out1, out2) = opsin_absorbance(0.0, 0.0, 0.0, false);
assert!((out0 - MIXI3 as f32).abs() < 1e-6);
assert!((out1 - MIXI7 as f32).abs() < 1e-6);
assert!((out2 - MIXI11 as f32).abs() < 1e-6);
}
#[test]
fn test_opsin_absorbance_clamped() {
let (out0, out1, out2) = opsin_absorbance(-100.0, -100.0, -100.0, true);
assert!(out0 >= MIN_01);
assert!(out1 >= MIN_01);
assert!(out2 >= MIN_2);
}
#[test]
fn test_fast_log2f() {
for i in 1..100 {
let x = i as f32 * 0.1;
let fast = fast_log2f(x);
let exact = x.log2();
assert!(
(fast - exact).abs() < 0.1,
"fast_log2f({x}) = {fast}, expected {exact}"
);
}
}
}