Skip to content

Commit 603e97d

Browse files
committed
Implement color-space-aware pixel format conv
In contrast to the intra-color-space conversion this one only requires coefficients (in non-constant luminance) which we can do without problems for all DynamicImage buffers. This also does not require setting up any tables or tone matching functions. The only pain point is that we can not support the highly generic `DynamicImage::to` due to its output pixel generic being too unconstrained to pass it any information on the underlying color space. This will always be sRGB. Problematically this is not even `'static` so we can't due type-id hacks..
1 parent f189e06 commit 603e97d

File tree

4 files changed

+458
-97
lines changed

4 files changed

+458
-97
lines changed

src/images/buffer.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ use std::ops::{Deref, DerefMut, Index, IndexMut, Range};
66
use std::path::Path;
77
use std::slice::{ChunksExact, ChunksExactMut};
88

9-
use crate::color::{FromColor, Luma, LumaA, Rgb, Rgba};
9+
use crate::color::{FromColor, FromPrimitive, Luma, LumaA, Rgb, Rgba};
1010
use crate::error::{
1111
ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, UnsupportedErrorKind,
1212
};
1313
use crate::flat::{FlatSamples, SampleLayout};
1414
use crate::math::Rect;
15-
use crate::metadata::cicp::{CicpApplicable, CicpRgb};
15+
use crate::metadata::cicp::{CicpApplicable, CicpPixelCast, CicpRgb, ColorComponentForCicp};
1616
use crate::traits::{EncodableLayout, Pixel, PixelWithColorType};
1717
use crate::utils::expand_packed;
1818
use crate::{
@@ -1546,6 +1546,35 @@ where
15461546
SelfPixel: PixelWithColorType,
15471547
C: Deref<Target = [SelfPixel::Subpixel]> + DerefMut,
15481548
{
1549+
/// Convert the color data to another pixel type, the color space.
1550+
///
1551+
/// This method is supposed to be called by exposed monomorphized methods, not directly by
1552+
/// users. In particular it serves to implement `DynamicImage`'s casts that go beyond those
1553+
/// offered by `PixelWithColorType` and include, e.g., `LumaAlpha<f32>`.
1554+
///
1555+
/// Before exposing this method, decide if we want a design like [`DynamicImage::to`] (many
1556+
/// trait parameters) with color space aware `FromColor` or if we want a design that takes a
1557+
/// `ColorType` parameter / `PixelWithColorType`. The latter is not quite as flexible but
1558+
/// allows much greater internal changes that do not tie in with the _external_ stable API.
1559+
pub(crate) fn cast_in_color_space<IntoPixel>(
1560+
&self,
1561+
) -> ImageBuffer<IntoPixel, Vec<IntoPixel::Subpixel>>
1562+
where
1563+
SelfPixel: Pixel,
1564+
IntoPixel: Pixel,
1565+
IntoPixel: CicpPixelCast<SelfPixel>,
1566+
SelfPixel::Subpixel: ColorComponentForCicp,
1567+
IntoPixel::Subpixel: ColorComponentForCicp + FromPrimitive<SelfPixel::Subpixel>,
1568+
{
1569+
let vec = self
1570+
.color
1571+
.cast_pixels::<SelfPixel, IntoPixel>(self.inner_pixels(), &|| [0.2126, 0.7152, 0.0722]);
1572+
let mut buffer = ImageBuffer::from_vec(self.width, self.height, vec)
1573+
.expect("cast_pixels returned the right number of pixels");
1574+
buffer.copy_color_space_from(self);
1575+
buffer
1576+
}
1577+
15491578
/// Copy pixel data from one buffer to another, calculating equivalent color representations
15501579
/// for the target's color space.
15511580
///

src/images/dynimage.rs

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ impl DynamicImage {
224224
}
225225

226226
/// Encodes a dynamic image into a buffer.
227+
///
228+
/// **WARNING**: Conversion between RGB and Luma is not aware of the color space and always
229+
/// uses sRGB coefficients to determine a non-constant luminance from an RGB color (and
230+
/// conversely).
231+
///
232+
/// This unfortunately owes to the public bounds of `T` which does not allow for passing a
233+
/// color space as a parameter. This function will likely be deprecated and replaced.
227234
#[inline]
228235
#[must_use]
229236
pub fn to<
@@ -247,73 +254,103 @@ impl DynamicImage {
247254
/// Returns a copy of this image as an RGB image.
248255
#[must_use]
249256
pub fn to_rgb8(&self) -> RgbImage {
250-
self.to()
257+
match self {
258+
DynamicImage::ImageRgb8(x) => x.clone(),
259+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
260+
}
251261
}
252262

253263
/// Returns a copy of this image as an RGB image.
254264
#[must_use]
255265
pub fn to_rgb16(&self) -> Rgb16Image {
256-
self.to()
266+
match self {
267+
DynamicImage::ImageRgb16(x) => x.clone(),
268+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
269+
}
257270
}
258271

259272
/// Returns a copy of this image as an RGB image.
260273
#[must_use]
261274
pub fn to_rgb32f(&self) -> Rgb32FImage {
262-
self.to()
275+
match self {
276+
DynamicImage::ImageRgb32F(x) => x.clone(),
277+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
278+
}
263279
}
264280

265281
/// Returns a copy of this image as an RGBA image.
266282
#[must_use]
267283
pub fn to_rgba8(&self) -> RgbaImage {
268-
self.to()
284+
match self {
285+
DynamicImage::ImageRgba8(x) => x.clone(),
286+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
287+
}
269288
}
270289

271290
/// Returns a copy of this image as an RGBA image.
272291
#[must_use]
273292
pub fn to_rgba16(&self) -> Rgba16Image {
274-
self.to()
293+
match self {
294+
DynamicImage::ImageRgba16(x) => x.clone(),
295+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
296+
}
275297
}
276298

277299
/// Returns a copy of this image as an RGBA image.
278300
#[must_use]
279301
pub fn to_rgba32f(&self) -> Rgba32FImage {
280-
self.to()
302+
match self {
303+
DynamicImage::ImageRgba32F(x) => x.clone(),
304+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
305+
}
281306
}
282307

283308
/// Returns a copy of this image as a Luma image.
284309
#[must_use]
285310
pub fn to_luma8(&self) -> GrayImage {
286-
self.to()
311+
match self {
312+
DynamicImage::ImageLuma8(x) => x.clone(),
313+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
314+
}
287315
}
288316

289317
/// Returns a copy of this image as a Luma image.
290318
#[must_use]
291319
pub fn to_luma16(&self) -> Gray16Image {
292-
self.to()
320+
match self {
321+
DynamicImage::ImageLuma16(x) => x.clone(),
322+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
323+
}
293324
}
294325

295326
/// Returns a copy of this image as a Luma image.
296327
#[must_use]
297328
pub fn to_luma32f(&self) -> ImageBuffer<Luma<f32>, Vec<f32>> {
298-
self.to()
329+
dynamic_map!(self, ref p, p.cast_in_color_space())
299330
}
300331

301332
/// Returns a copy of this image as a `LumaA` image.
302333
#[must_use]
303334
pub fn to_luma_alpha8(&self) -> GrayAlphaImage {
304-
self.to()
335+
match self {
336+
DynamicImage::ImageLumaA8(x) => x.clone(),
337+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
338+
}
305339
}
306340

307341
/// Returns a copy of this image as a `LumaA` image.
308342
#[must_use]
309343
pub fn to_luma_alpha16(&self) -> GrayAlpha16Image {
310-
self.to()
344+
match self {
345+
DynamicImage::ImageLumaA16(x) => x.clone(),
346+
x => dynamic_map!(x, ref p, p.cast_in_color_space()),
347+
}
311348
}
312349

313350
/// Returns a copy of this image as a `LumaA` image.
314351
#[must_use]
315352
pub fn to_luma_alpha32f(&self) -> ImageBuffer<LumaA<f32>, Vec<f32>> {
316-
self.to()
353+
dynamic_map!(self, ref p, p.cast_in_color_space())
317354
}
318355

319356
/// Consume the image and returns a RGB image.
@@ -662,7 +699,7 @@ impl DynamicImage {
662699
dynamic_map!(
663700
*self,
664701
ref image_buffer,
665-
bytemuck::cast_slice(image_buffer.as_raw().as_ref())
702+
bytemuck::cast_slice(image_buffer.as_raw())
666703
)
667704
}
668705

@@ -1112,7 +1149,12 @@ impl DynamicImage {
11121149
// Try to no-op this transformation, we may be lucky..
11131150
if self.color_space() == other.color_space() {
11141151
// Nothing to transform, just rescale samples and type cast.
1115-
dynamic_map!(self, ref mut p, *p = other.to());
1152+
dynamic_map!(
1153+
self,
1154+
ref mut p,
1155+
*p = dynamic_map!(other, ref o, o.cast_in_color_space())
1156+
);
1157+
11161158
return Ok(());
11171159
}
11181160

@@ -1951,6 +1993,42 @@ mod test {
19511993
assert_eq!(target[(0, 0)], Rgb([234u8, 51, 35]));
19521994
}
19531995

1996+
#[test]
1997+
fn into_luma_is_color_space_aware() {
1998+
let mut buffer = super::DynamicImage::ImageRgb16({
1999+
ImageBuffer::from_fn(128, 128, |_, _| Rgb([u16::MAX, 0, 0]))
2000+
});
2001+
2002+
buffer.set_rgb_primaries(Cicp::DISPLAY_P3.primaries);
2003+
buffer.set_transfer_function(Cicp::DISPLAY_P3.transfer);
2004+
2005+
let luma8 = buffer.clone().into_luma8();
2006+
assert_eq!(luma8[(0, 0)], Luma([58u8]));
2007+
2008+
buffer.set_rgb_primaries(Cicp::SRGB.primaries);
2009+
2010+
let luma8 = buffer.clone().into_luma8();
2011+
assert_eq!(luma8[(0, 0)], Luma([54u8]));
2012+
}
2013+
2014+
#[test]
2015+
fn from_luma_is_color_space_aware() {
2016+
let mut buffer = super::DynamicImage::ImageLuma16({
2017+
ImageBuffer::from_fn(128, 128, |_, _| Luma([u16::MAX]))
2018+
});
2019+
2020+
buffer.set_rgb_primaries(Cicp::DISPLAY_P3.primaries);
2021+
buffer.set_transfer_function(Cicp::DISPLAY_P3.transfer);
2022+
2023+
let rgb8 = buffer.clone().into_rgb8();
2024+
assert_eq!(rgb8[(0, 0)], Rgb([58u8, 176u8, 20u8]));
2025+
2026+
buffer.set_rgb_primaries(Cicp::SRGB.primaries);
2027+
2028+
let rgb8 = buffer.clone().into_rgb8();
2029+
assert_eq!(rgb8[(0, 0)], Rgb([54u8, 182u8, 18u8]));
2030+
}
2031+
19542032
#[test]
19552033
fn convert_color_space_coverage() {
19562034
const TYPES: [ColorType; 10] = [

0 commit comments

Comments
 (0)