How to remove alpha channel from CVPixelBuffer and get its Data in Swift

Viewed 858

My CVPixelBuffer comes in as kCVPixelFormatType_32BGRA and I'm trying to get the Data of the frame without the Alpha channel, in BGR format. Here's what I tried to do (as an extension CVPixelBuffer)

func bgrData(byteCount: Int) -> Data? {
    CVPixelBufferLockBaseAddress(self, .readOnly)
    defer { CVPixelBufferUnlockBaseAddress(self, .readOnly) }
    guard let sourceData = CVPixelBufferGetBaseAddress(self) else {
        return nil
    }
    
    let width = CVPixelBufferGetWidth(self)
    let height = CVPixelBufferGetHeight(self)
    let sourceBytesPerRow = CVPixelBufferGetBytesPerRow(self)
    let destinationBytesPerRow = 3 * width
    
    // Assign input image to `sourceBuffer` to convert it.
    var sourceBuffer = vImage_Buffer(
        data: sourceData,
        height: vImagePixelCount(height),
        width: vImagePixelCount(width),
        rowBytes: sourceBytesPerRow
    )
    
    // Make `destinationBuffer` and `destinationData` for its data to be assigned.
    guard let destinationData = malloc(height * destinationBytesPerRow) else {
        os_log("Error: out of memory", type: .error)
        return nil
    }
    defer { free(destinationData) }
    var destinationBuffer = vImage_Buffer(
        data: destinationData,
        height: vImagePixelCount(height),
        width: vImagePixelCount(width),
        rowBytes: destinationBytesPerRow)
    
    // Return `Data` with bgr image.
    return imageByteData = Data(
        bytes: sourceBuffer.data, count: destinationBuffer.rowBytes * height)
}

But the obtained buffer doesn't seem to be correct. What is the best way to achieve this? Thanks in advance

3 Answers

As you have access to your CVPixelBuffer, you can directly use Accelerate framework to do the convertion for you.

I am not checking for any errors, try/catch statements, no guards, etc, in this code. You will need to make sure that the code is error proofed.

Lets first define our BGRA color format. As we have 4 channels, we need 32 bits per pixel. We are also defining that our alpha channel is the last bit.

var bgraSourceFormat = vImage_CGImageFormat(
  bitsPerComponent: 8,
  bitsPerPixel: 32,
  colorSpace: Unmanaged.passRetained(CGColorSpaceCreateDeviceRGB()),
  bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue),
  version: 0,
  decode: nil,
  renderingIntent: .defaultIntent
)

Now we can define a BGR format. We need 3 channels, so 24 bits per pixel is enough. We are also defining that this format will not have an alpha channel.

var bgrDestinationFormat = vImage_CGImageFormat(
  bitsPerComponent: 8,
  bitsPerPixel: 24,
  colorSpace: nil,
  bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue),
  version: 0,
  decode: nil,
  renderingIntent: .defaultIntent
)

And create the converter...

let bgraToRgbConverter = vImageConverter_CreateWithCGImageFormat(
  &bgraSourceFormat,
  &bgrDestinationFormat,
  nil,
  vImage_Flags(kvImagePrintDiagnosticsToConsole),
  nil
)

let converter = bgraToRgbConverter!.takeRetainedValue()

Now we need to create a read buffer from our existing pixel data, and a write buffer for copying what we need. To create a read buffer from CVPixelBuffer, we can do something like this:

var bgraBuffer  = vImage_Buffer()
let imageFormat = vImageCVImageFormat_CreateWithCVPixelBuffer(cvPixelBuffer).takeRetainedValue()
vImageCVImageFormat_SetColorSpace(imageFormat, CGColorSpaceCreateDeviceRGB())
vImageBuffer_InitWithCVPixelBuffer(
  &bgraBuffer,
  &bgraSourceFormat,
  cvPixelBuffer,
  imageFormat,
  nil,
  vImage_Flags(kvImageNoFlags)
)

And create the empty write buffer...

var bgrBuffer = vImage_Buffer()
vImageBuffer_Init(
  &bgrBuffer,
  bgraBuffer.height,
  bgraBuffer.width,
  bgrDestinationFormat.bitsPerPixel,
  vImage_Flags(kvImageNoFlags)
)

We are ready... Let's tell the accelerate framework to convert from one format to another

vImageConvert_AnyToAny(
  converter,
  &bgraBuffer,
  &bgrBuffer,
  nil,
  vImage_Flags(kvImagePrintDiagnosticsToConsole)
)

And that's all. Your BGRA is now converted to BGR as a vImage_Buffer. We can check if we accomplished what we wanted, by directly reading from the pixel data. First, we need to get access to the data:

let bgraData = bgraBuffer.data!.assumingMemoryBound(to: UInt8.self)
let bgrData  = bgrBuffer.data!.assumingMemoryBound(to: UInt8.self)

Now, we can print the first and second pixels

print(bgraData[0], bgraData[1], bgraData[2], bgraData[3])
print(bgrData[0], bgrData[1], bgrData[2])

print(bgraData[4], bgraData[5], bgraData[6], bgraData[7])
print(bgrData[3], bgrData[4], bgrData[5])

This is the output I'm seeing from a png image I used in playgrounds for testing:

249 244 234 255
249 244 234

251 242 233 255
251 242 233

As you can see, pixels are copied without the alpha channel. Be careful on your for loops, if you are going to use any, as we now have 3 channels.

If you are developing a game + doing this on each frame, try keeping your objects alive. Accelerate format definitions, write buffer and converter never change for same image size + format, so they can be created once and kept in memory for future use.

Looks like you are returning a Data object. You can convert UnsafeMutablePointer construct to whatever you need.

Or you can also convert vImage_Buffer back to CVPixelBuffer (if you need) using accelerate's vImageBuffer_CopyToCVPixelBuffer method. vImage_Buffer has a lot of converters, one definitely will suit your needs. Check this link for more information on how to use copy to pixel buffer method. This link has a great example on the usage.

Edit: Your CVPixelBuffer might be padded.

Because of the hardware requirements your image might have a padding to make sure that the buffer width and height is a multiple of 16. This will also result in a padding in your vImage_Buffer structures. If you need to loop, but only need to access/update single pixels, you can use Accelerate's functions for speed. Check this link for possible methods and there are great examples at the end of the page.

To read the data fully, you can write something like this:

var bgrData = bgrBuffer.data!.assumingMemoryBound(to: UInt8.self)
print(bgrBuffer.width, bgrBuffer.height, bgrBuffer.rowBytes)

for _ in 0 ..< Int(bgrBuffer.height) {
  for x in 0 ..< Int(bgrBuffer.width) {
    let b = (bgrData + x * 3 + 0).pointee
    let g = (bgrData + x * 3 + 1).pointee
    let r = (bgrData + x * 3 + 2).pointee
    print(b, g, r)
  }
  bgrData = bgrData.advanced(by: bgrBuffer.rowBytes)
}

This will make sure that you are reading full width pixels, but pass the padding at the end.

Your code does only allocation of the new data structure, but evidently it does no 'squeezing' of 4 bytes into 3 bytes by discarding the last Alpha byte.

So that the Alpha byte of the first pixel (in Source) becomes the Blue byte of the second pixel (in Destination) and so on:

S: BGRABGRABGRA...

D: BGRBGRBGRBGR...

vImage is great as described above. But the solution below is a bit shorter. outData will contain the buffer in BGR if inData is in BGRA. vImageConvert_ARGB8888toRGB888 doesn't care in which order the channels are as long as the alpha is first.

   guard let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer)  else { return }  
   var inBuff = vImage_Buffer(data: baseAddress, height: inHeight, width: inWidth, rowBytes: inRowbytes)     
   
   let outData = UnsafeMutableRawPointer.allocate(byteCount: outSize, alignment: MemoryLayout<UInt>.alignment)
   var outBuff = vImage_Buffer(data: outData, height: outHeight, width: outWidth, rowBytes: outRowbytes)

   vImagePermuteChannels_ARGB8888(&inBuff, &inBuff, [3, 0, 1, 2], vImage_Flags(kvImageNoFlags))
   vImageConvert_ARGB8888toRGB888(&inBuff, &outBuff, vImage_Flags(kvImageNoFlags))

   outData.deallocate()
Related