extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
#[allow(unused_imports)]
use num_traits::Float;
use crate::oklab::srgb_to_oklab_l_fast;
pub fn compute_masking_weights(pixels: &[rgb::RGB<u8>], width: usize, height: usize) -> Vec<f32> {
let luminance = extract_luminance(pixels);
let contrast = compute_local_contrast(&luminance, width, height);
let block_w = width.div_ceil(4);
let block_h = height.div_ceil(4);
let block_masking = erode_to_blocks(&contrast, width, height, block_w, block_h);
let per_pixel = upscale_bilinear(&block_masking, block_w, block_h, width, height);
masking_to_weights(&per_pixel)
}
pub fn compute_masking_weights_from_labs(
labs: &[crate::oklab::OKLab],
width: usize,
height: usize,
) -> Vec<f32> {
let luminance: Vec<f32> = labs.iter().map(|lab| lab.l).collect();
let contrast = compute_local_contrast(&luminance, width, height);
let block_w = width.div_ceil(4);
let block_h = height.div_ceil(4);
let block_masking = erode_to_blocks(&contrast, width, height, block_w, block_h);
let per_pixel = upscale_bilinear(&block_masking, block_w, block_h, width, height);
masking_to_weights(&per_pixel)
}
pub fn compute_masking_weights_rgba(
pixels: &[rgb::RGBA<u8>],
width: usize,
height: usize,
) -> Vec<f32> {
let luminance = extract_luminance_rgba(pixels);
let contrast = compute_local_contrast(&luminance, width, height);
let block_w = width.div_ceil(4);
let block_h = height.div_ceil(4);
let block_masking = erode_to_blocks(&contrast, width, height, block_w, block_h);
let per_pixel = upscale_bilinear(&block_masking, block_w, block_h, width, height);
masking_to_weights(&per_pixel)
}
fn extract_luminance(pixels: &[rgb::RGB<u8>]) -> Vec<f32> {
pixels
.iter()
.map(|p| srgb_to_oklab_l_fast(p.r, p.g, p.b))
.collect()
}
fn extract_luminance_rgba(pixels: &[rgb::RGBA<u8>]) -> Vec<f32> {
pixels
.iter()
.map(|p| srgb_to_oklab_l_fast(p.r, p.g, p.b))
.collect()
}
fn compute_local_contrast(luminance: &[f32], width: usize, height: usize) -> Vec<f32> {
let mut contrast = vec![0.0f32; luminance.len()];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
let center = luminance[idx];
let mut sum = 0.0f32;
let mut count = 0u32;
if x > 0 {
sum += luminance[idx - 1];
count += 1;
}
if x + 1 < width {
sum += luminance[idx + 1];
count += 1;
}
if y > 0 {
sum += luminance[idx - width];
count += 1;
}
if y + 1 < height {
sum += luminance[idx + width];
count += 1;
}
let avg = if count > 0 {
sum / count as f32
} else {
center
};
let diff = center - avg;
contrast[idx] = (diff * diff).min(0.2);
}
}
contrast
}
fn erode_to_blocks(
contrast: &[f32],
width: usize,
height: usize,
block_w: usize,
block_h: usize,
) -> Vec<f32> {
const WEIGHTS: [f32; 4] = [0.40, 0.25, 0.20, 0.15];
let mut blocks = vec![0.0f32; block_w * block_h];
for by in 0..block_h {
for bx in 0..block_w {
let mut values = [0.0f32; 16];
let mut count = 0usize;
let y_start = by * 4;
let x_start = bx * 4;
let y_end = (y_start + 4).min(height);
let x_end = (x_start + 4).min(width);
for y in y_start..y_end {
for x in x_start..x_end {
values[count] = contrast[y * width + x];
count += 1;
}
}
if count == 0 {
continue;
}
values[..count]
.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let n = count.min(4);
let mut weighted_sum = 0.0f32;
let mut weight_sum = 0.0f32;
for i in 0..n {
weighted_sum += values[i] * WEIGHTS[i];
weight_sum += WEIGHTS[i];
}
blocks[by * block_w + bx] = weighted_sum / weight_sum;
}
}
blocks
}
fn upscale_bilinear(
blocks: &[f32],
block_w: usize,
block_h: usize,
width: usize,
height: usize,
) -> Vec<f32> {
let mut output = vec![0.0f32; width * height];
for y in 0..height {
for x in 0..width {
let bx_f = (x as f32 - 2.0) / 4.0;
let by_f = (y as f32 - 2.0) / 4.0;
let bx0 = (bx_f.floor() as isize).max(0) as usize;
let by0 = (by_f.floor() as isize).max(0) as usize;
let bx1 = (bx0 + 1).min(block_w - 1);
let by1 = (by0 + 1).min(block_h - 1);
let fx = (bx_f - bx0 as f32).clamp(0.0, 1.0);
let fy = (by_f - by0 as f32).clamp(0.0, 1.0);
let v00 = blocks[by0 * block_w + bx0];
let v10 = blocks[by0 * block_w + bx1];
let v01 = blocks[by1 * block_w + bx0];
let v11 = blocks[by1 * block_w + bx1];
let top = v00 * (1.0 - fx) + v10 * fx;
let bot = v01 * (1.0 - fx) + v11 * fx;
output[y * width + x] = top * (1.0 - fy) + bot * fy;
}
}
output
}
fn masking_to_weights(masking: &[f32]) -> Vec<f32> {
const K: f32 = 8.0;
masking
.iter()
.map(|&m| {
let w = 0.1 + 0.9 / (1.0 + K * m.sqrt());
w.clamp(0.1, 1.0)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_image(r: u8, g: u8, b: u8, width: usize, height: usize) -> Vec<rgb::RGB<u8>> {
vec![rgb::RGB { r, g, b }; width * height]
}
#[test]
fn flat_image_high_weights() {
let pixels = flat_image(128, 128, 128, 16, 16);
let weights = compute_masking_weights(&pixels, 16, 16);
assert_eq!(weights.len(), 256);
for &w in &weights {
assert!(w > 0.95, "expected high weight for flat image, got {w}");
}
}
#[test]
fn checkerboard_low_weights() {
let mut pixels = Vec::with_capacity(16 * 16);
for y in 0..16 {
for x in 0..16 {
if (x + y) % 2 == 0 {
pixels.push(rgb::RGB { r: 0, g: 0, b: 0 });
} else {
pixels.push(rgb::RGB {
r: 255,
g: 255,
b: 255,
});
}
}
}
let weights = compute_masking_weights(&pixels, 16, 16);
assert_eq!(weights.len(), 256);
let mean: f32 = weights.iter().sum::<f32>() / weights.len() as f32;
assert!(
mean < 0.5,
"expected low mean weight for checkerboard, got {mean}"
);
}
#[test]
fn weights_in_valid_range() {
let mut pixels = Vec::with_capacity(32 * 32);
for i in 0..(32 * 32) {
let v = (i % 256) as u8;
pixels.push(rgb::RGB { r: v, g: v, b: v });
}
let weights = compute_masking_weights(&pixels, 32, 32);
for &w in &weights {
assert!(
(0.1..=1.0).contains(&w),
"weight {w} out of range [0.1, 1.0]"
);
}
}
}