Improve your app's performance by converting image buffer formats from interleaved to planar.
Framework
- Accelerate
Overview
vImage operates on two image buffer formats:
Interleaved. Stores each pixel's color data consecutively in a single buffer. For example, the data that describes a 4-channel image (red, green, blue, and alpha) would be stored as RGBARGBARGBA…
Planar. Stores each color channel in separate buffers. For example, a 4-channel image would be stored as four individual buffers containing red, green, blue, and alpha data.

Because many vImage functions operate on a single color channel at a time—by converting an interleaved buffer to planar buffers—you can often improve your app's performance by doing this conversion manually. However, most vImage functions are available in both the interleaved and planar variants, so before you do the conversion, try both to see which works better in your context.
In some cases, you may not want to apply a vImage operation to all four channels of an image. For example, you may know beforehand that the alpha channel is irrelevant in the images that you’re dealing with, or perhaps all of your images are grayscale and you only need to operate on one channel. Using planar formats makes it possible to isolate and work with only the channels you need.
Review Interleaved Performance
Typically, your source imagery is in interleaved format and your default option will be to use the interleaved variant of a vImage function. For example, the following code populates an interleaved 8-bit ARGB destination buffer with the result of a table lookup transformation on an 8-bit ARGB source buffer:
var lookUpTable = (0...255).map {
return Pixel_8(($0 / 75) * 75)
}
vImageTableLookUp_ARGB8888(&sourceBuffer,
&destinationBuffer,
nil,
&lookUpTable,
&lookUpTable,
&lookUpTable,
vImage_Flags(kvImageNoFlags))
For information on logging CPU performance, see Logging.
Convert an Interleaved Source Buffer to Planar Buffers
First create a planar buffer for each color component of the source image you want to work with. The following code interrogates format
—the v
structure that represents the source image format—to calculate the number of components. (To learn more about Core Graphics image formats, see Creating a Core Graphics Image Format.)
let componentCount = format.componentCount
You can now create an array of initialized buffers for each color component. Note that each planar buffer's pixel
value is set to the source image's bits
value.
var argbSourcePlanarBuffers: [vImage_Buffer] = (0 ..< componentCount).map { _ in
guard let buffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
height: Int(sourceBuffer.height),
bitsPerPixel: format.bitsPerComponent) else {
fatalError("Error creating source buffers.")
}
return buffer
}
Assuming your source image contains four 8-bit channels, the v
function populates the planar buffers with the contents of the interleaved source image:
vImageConvert_ARGB8888toPlanar8(&sourceBuffer,
&argbSourcePlanarBuffers[0],
&argbSourcePlanarBuffers[1],
&argbSourcePlanarBuffers[2],
&argbSourcePlanarBuffers[3],
vImage_Flags(kvImageNoFlags))
Initialize the Destination Planar Buffers
Creating the destination buffers is similar to creating the source buffers:
var argbDestinationPlanarBuffers: [vImage_Buffer] = (0 ..< componentCount).map { _ in
guard let buffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
height: Int(sourceBuffer.height),
bitsPerPixel: format.bitsPerComponent) else {
fatalError("Error creating destination buffers.")
}
return buffer
}
Derive the Alpha Channel Index
Because you don't want to apply the lookup table transform to the alpha channel, you need to derive the index of the alpha channel from the source image's alpha
property. The following code populates the alpha
variable with the index of the alpha channel:
let alphaIndex: Int?
let littleEndian = cgImage.byteOrderInfo == .order16Little ||
cgImage.byteOrderInfo == .order32Little
switch cgImage.alphaInfo {
case .first, .noneSkipFirst, .premultipliedFirst:
alphaIndex = littleEndian ? componentCount - 1 : 0
case .last, .noneSkipLast, .premultipliedLast:
alphaIndex = littleEndian ? 0 : componentCount - 1
default:
alphaIndex = nil
}
Copy the Source Alpha to the Destination Alpha
Use the alpha
value to copy the alpha information directly from the appropriate source buffer to the appropriate destination buffer:
if let alphaIndex = alphaIndex {
do {
try argbSourcePlanarBuffers[alphaIndex].copy(destinationBuffer: &argbDestinationPlanarBuffers[alphaIndex])
} catch {
fatalError("Error copying alpha buffer: \(error.localizedDescription).")
}
}
Apply the Lookup Table to the Planar Buffers
To apply the lookup table transform to the color planar buffers, execute v
on each one:
for index in 0 ..< componentCount where index != alphaIndex {
vImageTableLookUp_Planar8(&argbSourcePlanarBuffers[index],
&argbDestinationPlanarBuffers[index],
&lookUpTable,
vImage_Flags(kvImageNoFlags))
}
Convert the Planar Buffers Back to an Interleaved Buffer
With the planar buffers populated with the transform operation results, you're ready to convert the buffers back to an interleaved image. Create an interleaved destination buffer that matches the source buffer:
guard var destinationBuffer = try? vImage_Buffer(width: Int(sourceBuffer.width),
height: Int(sourceBuffer.height),
bitsPerPixel: format.bitsPerPixel) else {
fatalError("Error creating destination buffers.")
}
The v
function interleaves the four planar buffers, writing the results to the destination buffer:
vImageConvert_Planar8toARGB8888(&argbDestinationPlanarBuffers[0],
&argbDestinationPlanarBuffers[1],
&argbDestinationPlanarBuffers[2],
&argbDestinationPlanarBuffers[3],
&destinationBuffer,
vImage_Flags(kvImageNoFlags))
Free the Buffer Memory
After you’re finished working with the buffers, it’s important that you free the memory allocated to them:
destinationBuffer.free()
for buffer in argbSourcePlanarBuffers {
buffer.free()
}
for buffer in argbDestinationPlanarBuffers {
buffer.free()
}