Convert an RGB image to discrete luminance and chrominance channels, and apply color and contrast treatments.
- macOS 10.15+
- Xcode 11.2+
This sample code project allows you to apply saturation adjustments to an image without affecting luminosity, and change the luminance response curve without affecting color.
Many image processing techniques, such as saturation adjustment and tone mapping, are simpler to implement when you can work on an image’s luminance data separately from its color data. This article explains how you can convert an RGB image—with its pixels represented as red, green, and blue values—to YCbCr, where luminance and chrominance are stored discretely. The Y in YCbCr refers to the luminance, and the Cb and Cr refer to the blue-luminance difference, and red-luminance difference respectively.
This sample walks you through the following steps to adjust the saturation of an image, and then apply tone mapping to it:
Create a vImage buffer from a Core Graphics image.
Create source YCbCr buffers.
Define the RGB to YCbCr conversion.
Perform the RGB to YCbCr conversion.
Create destination YCbCr Buffers.
Apply saturation adjustment to the image.
Multiply CbCr values to decrease saturation.
Apply gamma to CbCr to increase saturation.
Apply gamma to luminance to apply tone mapping.
Define the YCbCr to RGB conversion.
Perform the YCbCr to RGB conversion.
Correct gamma before applying operations.
The following images show two photographs with a range of saturation adjustments, that illustrate the kinds of color changes you can make using the methods shown here.
Create a vImage Buffer from a Core Graphics Image
Declare the vImage buffer,
argb, to store the source image, and use the
buffer function to populate it with the Core Graphics image:
buffer function accepts a Boolean parameter to specify whether to remap the image from sRGB to linear:
argb contains the image.
Create Source YCbCr Buffers
The conversion routine used in this sample creates a YCbCr result with a chroma of 4:2:0; this means there is one Cb and one Cr pixel for every four luminance pixels. That is, each chrominance buffer is half of the width, and half of the height of the luminance channel. Reducing the resolution for the chrominance channels is known as chroma subsampling, and relies on the fact that human vision is less sensitive to color than luminance to reduce an image’s size.
The image below shows that a 4 x 2 image is represented by a 4 x 2 luminance channel, but each chrominance channel is 2 x 1 pixels:
To support the 4:2:0 YCbCr representation of the source image, create a luminance buffer that’s the same size as the source buffer, but define the chrominance buffer’s height as half the source height, and the width as the source width. This size enables the chrominance buffer to store both the Cb and Cr data as interleaved pixels.
Define the RGB to YCbCr Conversion
v structure that defines the range and clamping information for the destination YCbCr format. The destination buffer is 8-bit, so set the minimum and maximum values for luminance and chrominance to
Cb specifies the middle of the CbCr range (that is, where the blue-luminance difference or red-luminance difference is zero), so set that to 128:
v` to generate the conversion from ARGB to YpCbCr. In this sample, the conversion of RGB values is calculated using the conversion matrix for ITU Recommendation BT.709-2.
Perform the RGB to YCbCr Conversion
v function to populate two vImage buffers—one containing luminance data, and one containing chrominance data—from the contents of a single ARGB buffer:
The following image shows the luminance result on the left, and the interleaved chrominance result on the right. Because the interleaved chrominance result contains both the Cb and Cr information, it’s half the height of the luminance channel, but has the same width.
Create Destination YCbCr Buffers
Before performing the image adjustments, create destination buffers to receive the processed result:
Apply Saturation Adjustment to the Image
This sample uses two techniques to adjust saturation:
Multiply CbCr values to decrease saturation.
Apply gamma to CbCr to increase saturation
The tone mapping is performed by applying gamma to the luminance channel:
Multiply CbCr Values to Decrease Saturation
You can easily adjust the color saturation of a YpCbCr image, without affecting its luminance, using the following formula:
You use the
v function to perform this math on the source chrominance buffer.
Pass the pre-bias (
–128), and then multiply by the divisor both the post-bias (
+128) and the saturation. The saturation is passed to the matrix multiply function as a single-element matrix, and the chrominance buffer is passed as the source and destination:
The following image shows two photographs, from left to right, with saturations of
1 (that is, the rightmost image has an unchanged saturation).
Apply Gamma to CbCr to Increase Saturation
The simple linear adjustment provided by
v is fine for desaturating an image, however, when increasing saturation, the CbCr values can clip, leading to areas of solid color. An alternative technique to increase saturation is to apply an exponential adjustment. Use
v to apply a gamma value to the CbCr values to increase saturation.
YCb function applies a gamma value to the chrominance buffer with the following steps:
vto create a gamma function object, defining its gamma as the reciprocal of the desired saturation.
Image Create Gamma Function(_: _: _:)
Populate the gamma destination buffer with the contents of the CbCr buffer. Pass a
1, and a
-1to remap the
UInt8CbCr values from
vto apply the gamma function to the buffer, and overwrite the CbCr buffer with the gamma adjusted values.
Image Gamma _Planar F(_: _: _: _:)
The following image shows two photographs, from left to right, with a saturation of 1.0 (that is, the leftmost image has an unchanged saturation), 1.5, and 2.0.
When decreasing the saturation, the gamma function is not appropriate because pixels with very saturated color will desaturate very little, or not at all.
Apply Gamma to Luminance to Apply Tone Mapping
You can adjust the contrast of an image, with a technique known as tone mapping, by applying a gamma adjustment to the luminance channel.
Adjusting contrast is discussed in Adjusting the Brightness and Contrast of an Image, however applying a gamma adjustment to red, green, and blue channels changes both the color and tonal values.
yp buffer, that contains the luminance data, is a planar buffer and can be operated on by
The following image shows two photographs, from left to right, with a gamma applied to the luminance channel of 2.5, 0.0 (that is, the center image is unchanged), and 0.5.
Define the YCbCr to RGB Conversion
Once you’ve completed working on the YCbCr representation, convert the YCbCr data to RGB. The process is very similar to the RGB to YCbCr conversion and uses the same pixel range, but the conversion generated by the
Perform the YCbCr to RGB Conversion
Create a buffer to recieve the YCbCr to RGB conversion result:
Perform the conversion using
argb contains the processed image in RGB color space.
Correct Gamma Before Applying Operations
Many vImage operations provide optimal results when working on images with a linear response curve. If you are working with nonlinear images—such as sRGB—consider converting them to a linear color space by applying a reciprocal gamma, performing the operation, and converting them back to their original domain by applying the original gamma.
vImage provides predefined gamma functions for converting from linear to sRGB, and from sRGB to linear. The following function is implemented as an extension to
v and remaps a buffer’s contents in-place in the specified direction:
Create a 32-bit-per-component buffer to receive the gamma result:
Create planar buffers that reference the interleaved buffers:
Convert to 8-bit source data to single-precision values and remap from
Perform the gamma correction on the single-precision planar buffer:
Finally, convert the gamma corrected single-precision values back to
UInt8, overwriting the contents of the source buffer: