Sample Code

Adjusting the Hue of an Image

Convert an RGB image to L*a*b* color space and apply hue adjustment.

Download

Overview

This sample code project allows you to adjust the hue of an image by treating the chrominance information as 2D coordinates, and transforming those values with a rotation matrix. You can convert an RGB image—with its pixels represented as red, green, and blue values—to L*a*b*, where luminance and chrominance are stored discretely. The L* in L*a*b* refers to the lightness, and the a* and b* refer to the red-green, and blue-yellow values, respectively.

The image below shows an approximation of a L*a*b* color chart. The a* value transitions horizontally (left to right) from negative, through zero, to positive, and the b* transitions vertically (bottom to top) from negative, through zero, to positive. Because this sample code focuses on color rather than lightness, the image doesn’t consider L*:

alttext

This sample walks you through these steps:

  1. Derive RGB image format from source image.

  2. Create L*a*b* image format.

  3. Create RGB-to-L*a*b* and L*a*b*-to-RGB converters.

  4. Create a vImage buffer from the source image.

  5. Convert RGB to L*a*b*.

  6. Convert the interleaved L*a*b* buffer to planar buffers.

  7. Apply the hue adjustment.

  8. Convert the planar L*a*b* buffers to an interleaved buffer.

  9. Convert L*a*b* to RGB.

The following image shows four photographs, from left to right, with a hue adjustment of -90º, 0º (an unchanged hue), 90º, and 180º:

alttext

Derive RGB Image Format from Source Image

The converter that the sample uses to convert the RGB pixels to L*a*b* color space requires two vImage_CGImageFormat structures that describe the source and destination images. Use the init(cgImage:) initializer to create the RGB format from the source image:

var rect = CGRect(origin: .zero,
                  size: image.size)

guard
    let sourceCGImage = image.cgImage(forProposedRect: &rect,
                                      context: nil,
                                      hints: nil),
    let rgbImageFormat = vImage_CGImageFormat(cgImage: sourceCGImage) else {
        return nil
}

Create L*a*b* Image Format

To create the image format for the L*a*b* color space, use the genericLab system-defined CGColorSpace:

var labImageFormat = vImage_CGImageFormat(bitsPerComponent: 8,
                                          bitsPerPixel: 8 * 3,
                                          colorSpace: CGColorSpace(name: CGColorSpace.genericLab)!,
                                          bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
                                          renderingIntent: .defaultIntent)!

On return, labImageFormat describes the interleaved L*a*b* pixels over which this sample works. The first channel in each pixel is the lightness, and the second and third channels are the a* and b* respectively.

Create RGB-to-L*a*b* and L*a*b*-to-RGB Converters

Use the RGB and L*a*b* image formats to create vImageConverter instances to convert between the two color spaces:

private let rgbToLab: vImageConverter
private let labToRGB: vImageConverter
rgbToLab = try vImageConverter.make(sourceFormat: rgbImageFormat,
                                    destinationFormat: labImageFormat)

labToRGB = try vImageConverter.make(sourceFormat: labImageFormat,
                                    destinationFormat: rgbImageFormat)

To learn more about vImage’s convert-any-to-any functionality, see Building a Basic Conversion Workflow.

Create a vImage Buffer from the Source Image

Declare the vImage buffer, argbSourceBuffer, to store the source image, and use the init(cgImage:format:flags:) initializer to populate it with the Core Graphics image:

let argbSourceBuffer = try vImage_Buffer(cgImage: sourceCGImage,
                                         format: rgbImageFormat)
defer {
    argbSourceBuffer.free()
}

On return, argbSourceBuffer contains the image.

Convert RGB to L*a*b*

Initialize a vImage buffer that’s the same size as the source image and the L*a*b* image format’s bitsPerPixel:

labSource = try vImage_Buffer(width: Int(image.size.width),
                              height: Int(image.size.height),
                              bitsPerPixel: labImageFormat.bitsPerPixel)

The converter’s convert(source:destination:flags:) function performs the conversion:

try rgbToLab.convert(source: argbSourceBuffer,
                     destination: &labSource)

On return, the labSource contains the L*a*b* representation of the source image.

Convert the Interleaved L*a*b* to Planar Buffers.

The function you use to apply the hue adjustment, vImageMatrixMultiply_Planar8(_:_:_:_:_:_:_:_:_:), operates on a set of planar buffers. To convert the interleaved L*a*b* buffer to planar buffers, initialize three buffers with a bitsPerPixel that is equal to labImageFormat.bitsPerComponent:

lDestination = try vImage_Buffer(width: Int(image.size.width),
                                 height: Int(image.size.height),
                                 bitsPerPixel: labImageFormat.bitsPerComponent)

aDestination = try vImage_Buffer(width: Int(image.size.width),
                                 height: Int(image.size.height),
                                 bitsPerPixel: labImageFormat.bitsPerComponent)

bDestination = try vImage_Buffer(width: Int(image.size.width),
                                 height: Int(image.size.height),
                                 bitsPerPixel: labImageFormat.bitsPerComponent)

Use vImageConvert_RGB888toPlanar8(_:_:_:_:_:) to populate the planar buffers with the contents of the interleaved buffer

vImageConvert_RGB888toPlanar8(&labSource,
                              &lDestination, &aDestination, &bDestination,
                              vImage_Flags(kvImageNoFlags))

To learn more about working with planar buffers, see Optimizing Image Processing Performance.

Apply the Hue Adjustment

Hue adjustment is achieved by rotating a two-element vector, described by a* and b*. To learn more about working with rotation matrices, see Working with Matrices.

The following visualizes a sample color (marked A) rotated by -90º (marked C) and 45º (marked B):

alttext

Use the following code to generate the rotation matrix based on hueAngle:

let divisor: Int32 = 0x1000

let rotationMatrix = [
    cos(hueAngle), -sin(hueAngle),
    sin(hueAngle),  cos(hueAngle)
    ].map {
        return Int16($0 * Float(divisor))
}

The preBias and postBias values effectively shift the a* and b* values from 0...255 to -128...127, so the rotation is centered where a* and b* are zero, which is represented by the UInt8 values as 128:

let preBias = [Int16](repeating: -128, count: 2)
let postBias = [Int32](repeating: 128 * divisor, count: 2)

vImageMatrixMultiply_Planar8(_:_:_:_:_:_:_:_:_:) multiplies each pixel in the source buffers by the matrix and writes the result to the destination buffers. In this example, the matrix multiplication is done in-place, so the source and destination point to the same buffers.

Use the following code to create the source and destination as UnsafeMutablePointer<UnsafePointer<vImage_Buffer>?> structures from the a* and b* planar buffers, and pass them to the matrix multiply function:

[bDestination, aDestination].withUnsafeBufferPointer { bufferPointer in
    
    var src: [UnsafePointer<vImage_Buffer>?] = (0...1).map {
        bufferPointer.baseAddress! + $0
    }
    
    var dst: [UnsafePointer<vImage_Buffer>?] = (0...1).map {
        bufferPointer.baseAddress! + $0
    }

    vImageMatrixMultiply_Planar8(&src,
                                 &dst,
                                 2, 2,
                                 rotationMatrix,
                                 divisor,
                                 preBias,
                                 postBias,
                                 0)
}

On return, aDestination and bDestination contain the hue adjusted a* and b* channels.

Convert L*a*b* to RGB

Finally, convert the hue adjusted buffers back to RGB, by converting the planar buffers back to a single, interleaved buffer:

vImageConvert_Planar8toRGB888(&lDestination, &aDestination, &bDestination,
                              &labDestination,
                              vImage_Flags(kvImageNoFlags))

Use the labToRGB converter to populate rgbDestination with the RGB representation of the hue-adjusted image:

try labToRGB.convert(source: labDestination,
                     destination: &rgbDestination)

See Also

Color and Tone Adjustment

Adjusting the Brightness and Contrast of an Image

Use a gamma function to apply a linear or exponential curve.

Adjusting Saturation and Applying Tone Mapping

Convert an RGB image to discrete luminance and chrominance channels, and apply color and contrast treatments.

Specifying Histograms with vImage

Calculate the histogram of one image and apply it to a second image.

Transform

Apply color transformations to images.

Histogram

Calculate and or manipulate an image's histogram.