From 0836ce69517d22799e31e92277329fa88cb77ce5 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Sun, 24 May 2026 12:54:04 +0200 Subject: [PATCH 1/3] Optimize RGB to Luma conversion --- src/color.rs | 54 +++++++++++++------------------------ src/primitive_sealed.rs | 60 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/color.rs b/src/color.rs index e1c705c92d..e5d8fadd85 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,10 +1,11 @@ use std::ops::{Index, IndexMut}; -use num_traits::{NumCast, Zero}; +use num_traits::Zero; use crate::{ error::TryFromExtendedColorError, - traits::{Enlargeable, Pixel, Primitive}, + primitive_sealed::RgbToLuma, + traits::{Pixel, Primitive}, }; /// An enumeration over supported color types and bit depths @@ -515,11 +516,11 @@ define_colors! { /// /// For the purpose of color conversion, as well as blending, the implementation of `Pixel` /// assumes an `sRGB` color space of its data. - pub struct Rgb([T; 3, 0]) = "RGB"; + pub struct Rgb([T; 3, 0]) = "RGB"; /// Grayscale colors. pub struct Luma([T; 1, 0]) = "Y"; /// RGB colors + alpha channel - pub struct Rgba([T; 4, 1]) = "RGBA"; + pub struct Rgba([T; 4, 1]) = "RGBA"; /// Grayscale colors + alpha channel pub struct LumaA([T; 2, 1]) = "YA"; } @@ -637,18 +638,6 @@ where } } -/// Coefficients to transform from sRGB to a CIE Y (luminance) value. -const SRGB_LUMA: [u32; 3] = [2126, 7152, 722]; -const SRGB_LUMA_DIV: u32 = 10000; - -#[inline] -fn rgb_to_luma(rgb: &[T]) -> T { - let l = ::from(SRGB_LUMA[0]).unwrap() * rgb[0].to_larger() - + ::from(SRGB_LUMA[1]).unwrap() * rgb[1].to_larger() - + ::from(SRGB_LUMA[2]).unwrap() * rgb[2].to_larger(); - T::clamp_from(l / ::from(SRGB_LUMA_DIV).unwrap()) -} - // `FromColor` for Luma impl FromColor> for Luma where @@ -670,26 +659,23 @@ where } } -impl FromColor> for Luma +impl FromColor> for Luma where T: FromPrimitive, { fn from_color(&mut self, other: &Rgb) { - let gray = self.channels_mut(); - let rgb = other.channels(); - gray[0] = T::from_primitive(rgb_to_luma(rgb)); + let [r, g, b] = other.0; + self.0[0] = T::from_primitive(RgbToLuma::rgb_to_luma(r, g, b)); } } -impl FromColor> for Luma +impl FromColor> for Luma where T: FromPrimitive, { fn from_color(&mut self, other: &Rgba) { - let gray = self.channels_mut(); - let rgb = other.channels(); - let l = rgb_to_luma(rgb); - gray[0] = T::from_primitive(l); + let [r, g, b, _a] = other.0; + self.0[0] = T::from_primitive(RgbToLuma::rgb_to_luma(r, g, b)); } } @@ -707,27 +693,25 @@ where } } -impl FromColor> for LumaA +impl FromColor> for LumaA where T: FromPrimitive, { fn from_color(&mut self, other: &Rgb) { - let gray_a = self.channels_mut(); - let rgb = other.channels(); - gray_a[0] = T::from_primitive(rgb_to_luma(rgb)); - gray_a[1] = T::DEFAULT_MAX_VALUE; + let [r, g, b] = other.0; + self.0[0] = T::from_primitive(RgbToLuma::rgb_to_luma(r, g, b)); + self.0[1] = T::DEFAULT_MAX_VALUE; } } -impl FromColor> for LumaA +impl FromColor> for LumaA where T: FromPrimitive, { fn from_color(&mut self, other: &Rgba) { - let gray_a = self.channels_mut(); - let rgba = other.channels(); - gray_a[0] = T::from_primitive(rgb_to_luma(rgba)); - gray_a[1] = T::from_primitive(rgba[3]); + let [r, g, b, a] = other.0; + self.0[0] = T::from_primitive(RgbToLuma::rgb_to_luma(r, g, b)); + self.0[1] = T::from_primitive(a); } } diff --git a/src/primitive_sealed.rs b/src/primitive_sealed.rs index 98e1a57eac..fe256f3b7a 100644 --- a/src/primitive_sealed.rs +++ b/src/primitive_sealed.rs @@ -7,7 +7,10 @@ use crate::imageops::fast_blur::BlurAccumulator; /// This trait is `pub` but not exported, so it cannot be implemented outside /// this crate. #[allow(private_bounds)] -pub trait PrimitiveSealed: Sized + NearestFrom + WithBlurAcc + BgraSwizzle {} +pub trait PrimitiveSealed: + Sized + NearestFrom + WithBlurAcc + BgraSwizzle + RgbToLuma +{ +} impl PrimitiveSealed for usize {} impl PrimitiveSealed for u8 {} @@ -179,3 +182,58 @@ macro_rules! impl_with_blur_acc_f32 { } impl_with_blur_acc_f32!(u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64); + +/// Coefficients to transform from sRGB to a CIE Y (luminance) value. +/// +/// The coefficients are 2126/10000, 7152/10000, and 722/10000 for R, G, and B respectively. +const SRGB_LUMA: [u32; 3] = [2126, 7152, 722]; +const SRGB_LUMA_DIV: u32 = 10000; +pub(crate) trait RgbToLuma: Copy + Sized + crate::traits::Enlargeable { + #[inline] + fn rgb_to_luma(r: Self, g: Self, b: Self) -> Self { + use num_traits::NumCast; + + let l = ::from(SRGB_LUMA[0]).unwrap() * r.to_larger() + + ::from(SRGB_LUMA[1]).unwrap() * g.to_larger() + + ::from(SRGB_LUMA[2]).unwrap() * b.to_larger(); + Self::clamp_from(l / ::from(SRGB_LUMA_DIV).unwrap()) + } +} +impl RgbToLuma for usize {} +impl RgbToLuma for u8 { + fn rgb_to_luma(r: u8, g: u8, b: u8) -> u8 { + // The following constants give the same results as: + // ((r as u32 * 2126 + g as u32 * 7152 + b as u32 * 722 + 5000) / 10000) as u8 + // Note that results are correctly rounded to the nearest integer. + const W_R: u32 = ((1_u64 << 24) * 2126).div_ceil(10000) as u32; + const W_G: u32 = ((1_u64 << 24) * 7152).div_ceil(10000) as u32; + const W_B: u32 = ((1_u64 << 24) * 722).div_ceil(10000) as u32; + ((r as u32 * W_R + g as u32 * W_G + b as u32 * W_B + 0x800000) >> 24) as u8 + } +} +impl RgbToLuma for u16 {} +impl RgbToLuma for u32 {} +impl RgbToLuma for u64 {} +impl RgbToLuma for isize {} +impl RgbToLuma for i8 {} +impl RgbToLuma for i16 {} +impl RgbToLuma for i32 {} +impl RgbToLuma for i64 {} +impl RgbToLuma for f32 { + #[inline] + fn rgb_to_luma(r: f32, g: f32, b: f32) -> f32 { + const SCALE_R: f32 = SRGB_LUMA[0] as f32 / SRGB_LUMA_DIV as f32; + const SCALE_G: f32 = SRGB_LUMA[1] as f32 / SRGB_LUMA_DIV as f32; + const SCALE_B: f32 = SRGB_LUMA[2] as f32 / SRGB_LUMA_DIV as f32; + SCALE_R * r + SCALE_G * g + SCALE_B * b + } +} +impl RgbToLuma for f64 { + #[inline] + fn rgb_to_luma(r: f64, g: f64, b: f64) -> f64 { + const SCALE_R: f64 = SRGB_LUMA[0] as f64 / SRGB_LUMA_DIV as f64; + const SCALE_G: f64 = SRGB_LUMA[1] as f64 / SRGB_LUMA_DIV as f64; + const SCALE_B: f64 = SRGB_LUMA[2] as f64 / SRGB_LUMA_DIV as f64; + SCALE_R * r + SCALE_G * g + SCALE_B * b + } +} From cdaf9a037ee2ed61ee75ef9c4024d8a9457c3ea9 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Mon, 25 May 2026 19:13:44 +0200 Subject: [PATCH 2/3] Expand luma cast bench --- benches/convert.rs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/benches/convert.rs b/benches/convert.rs index 55adacf4e6..2c571933a2 100644 --- a/benches/convert.rs +++ b/benches/convert.rs @@ -103,19 +103,46 @@ pub fn bench_cast_intra_colorspace(c: &mut Criterion) { } pub fn bench_cast_with_coefficient(c: &mut Criterion) { - let source = - DynamicImage::ImageRgba8(ImageBuffer::from_pixel(256, 256, Rgba([0u8, 0, 0, 255]))); + let rgba8 = DynamicImage::new(256, 256, image::ColorType::Rgb8); c.bench_function("cast_dynamic_rgba8_l8", |b| { - b.iter(|| black_box(&source).to_luma8()); + b.iter(|| black_box(&rgba8).to_luma8()); }); c.bench_function("cast_dynamic_rgba8_l16", |b| { - b.iter(|| black_box(&source).to_luma16()); + b.iter(|| black_box(&rgba8).to_luma16()); }); c.bench_function("cast_dynamic_rgba8_la16", |b| { - b.iter(|| black_box(&source).to_luma_alpha16()); + b.iter(|| black_box(&rgba8).to_luma_alpha16()); + }); + + let rgba16 = DynamicImage::new(256, 256, image::ColorType::Rgb16); + + c.bench_function("cast_dynamic_rgba16_l8", |b| { + b.iter(|| black_box(&rgba16).to_luma8()); + }); + + c.bench_function("cast_dynamic_rgba16_l16", |b| { + b.iter(|| black_box(&rgba16).to_luma16()); + }); + + c.bench_function("cast_dynamic_rgba16_la16", |b| { + b.iter(|| black_box(&rgba16).to_luma_alpha16()); + }); + + let rgba32f = DynamicImage::new(256, 256, image::ColorType::Rgba32F); + + c.bench_function("cast_dynamic_rgba32f_l8", |b| { + b.iter(|| black_box(&rgba32f).to_luma8()); + }); + + c.bench_function("cast_dynamic_rgba32f_l16", |b| { + b.iter(|| black_box(&rgba32f).to_luma16()); + }); + + c.bench_function("cast_dynamic_rgba32f_la16", |b| { + b.iter(|| black_box(&rgba32f).to_luma_alpha16()); }); } From d71b210269223fa0d8b33e61fd57903f0a8e0cc2 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Mon, 25 May 2026 19:22:01 +0200 Subject: [PATCH 3/3] Minor changes --- src/primitive_sealed.rs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/primitive_sealed.rs b/src/primitive_sealed.rs index fe256f3b7a..4d71e11e65 100644 --- a/src/primitive_sealed.rs +++ b/src/primitive_sealed.rs @@ -183,16 +183,16 @@ macro_rules! impl_with_blur_acc_f32 { impl_with_blur_acc_f32!(u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64); -/// Coefficients to transform from sRGB to a CIE Y (luminance) value. -/// -/// The coefficients are 2126/10000, 7152/10000, and 722/10000 for R, G, and B respectively. -const SRGB_LUMA: [u32; 3] = [2126, 7152, 722]; -const SRGB_LUMA_DIV: u32 = 10000; +/// Converts sRGB to luma using standard coefficients for all primitives. pub(crate) trait RgbToLuma: Copy + Sized + crate::traits::Enlargeable { #[inline] fn rgb_to_luma(r: Self, g: Self, b: Self) -> Self { use num_traits::NumCast; + /// Coefficients to transform from sRGB to a CIE Y (luminance) value. + const SRGB_LUMA: [u32; 3] = [2126, 7152, 722]; + const SRGB_LUMA_DIV: u32 = 10000; + let l = ::from(SRGB_LUMA[0]).unwrap() * r.to_larger() + ::from(SRGB_LUMA[1]).unwrap() * g.to_larger() + ::from(SRGB_LUMA[2]).unwrap() * b.to_larger(); @@ -222,18 +222,10 @@ impl RgbToLuma for i64 {} impl RgbToLuma for f32 { #[inline] fn rgb_to_luma(r: f32, g: f32, b: f32) -> f32 { - const SCALE_R: f32 = SRGB_LUMA[0] as f32 / SRGB_LUMA_DIV as f32; - const SCALE_G: f32 = SRGB_LUMA[1] as f32 / SRGB_LUMA_DIV as f32; - const SCALE_B: f32 = SRGB_LUMA[2] as f32 / SRGB_LUMA_DIV as f32; - SCALE_R * r + SCALE_G * g + SCALE_B * b - } -} -impl RgbToLuma for f64 { - #[inline] - fn rgb_to_luma(r: f64, g: f64, b: f64) -> f64 { - const SCALE_R: f64 = SRGB_LUMA[0] as f64 / SRGB_LUMA_DIV as f64; - const SCALE_G: f64 = SRGB_LUMA[1] as f64 / SRGB_LUMA_DIV as f64; - const SCALE_B: f64 = SRGB_LUMA[2] as f64 / SRGB_LUMA_DIV as f64; + const SCALE_R: f32 = 2126. / 10000.; + const SCALE_G: f32 = 7152. / 10000.; + const SCALE_B: f32 = 722. / 10000.; SCALE_R * r + SCALE_G * g + SCALE_B * b } } +impl RgbToLuma for f64 {}