在Swift中从CVPixelBufferRef获取像素值

20

我该如何从CVPixelBufferRef获取RGB(或其他格式)像素值?我尝试了许多方法,但还没有成功。

func captureOutput(captureOutput: AVCaptureOutput!,
                   didOutputSampleBuffer sampleBuffer: CMSampleBuffer!,
                   fromConnection connection: AVCaptureConnection!) {
  let pixelBuffer: CVPixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
                CVPixelBufferLockBaseAddress(pixelBuffer, 0)
  let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)

  //Get individual pixel values here

  CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)
}

核心视频像素缓冲区不仅包含关于单个像素的信息,而是包含从场景中捕获到的所有像素的像素信息(即像素矩阵位图)。您的意思是想获取缓冲区内每个像素的RGB值吗? - Ayan Sengupta
4个回答

24

baseAddress是一个不安全的可变指针,更准确地说是UnsafeMutablePointer<Void>。一旦将指针从Void转换为更具体的类型,您就可以轻松访问内存:

// Convert the base address to a safe pointer of the appropriate type
let byteBuffer = UnsafeMutablePointer<UInt8>(baseAddress)

// read the data (returns value of type UInt8)
let firstByte = byteBuffer[0]

// write data
byteBuffer[3] = 90
确保使用正确的类型(8、16或32位无符号整数),这取决于视频格式。最可能是8位。

缓冲区格式更新:

您可以在初始化AVCaptureVideoDataOutput实例时指定格式。基本上,您有以下选择:

  • BGRA:单个平面,其中蓝色、绿色、红色和 Alpha 值分别存储在 32 位整数中
  • 420YpCbCr8BiPlanarFullRange:两个平面,第一个平面包含每个像素的 Y(亮度)值的字节,第二个平面包含用于一组像素的 Cb 和 Cr(色度)值
  • 420YpCbCr8BiPlanarVideoRange:与420YpCbCr8BiPlanarFullRange相同,但Y值限制在16-235范围内(由于历史原因)

如果您对颜色值感兴趣并且速度(或最大帧率)不是问题,则选择更简单的BGRA格式。否则,选择更高效的本机视频格式之一。

如果有两个平面,则必须获取所需平面的基址(请参见视频格式示例):

视频格式示例

let pixelBuffer: CVPixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
CVPixelBufferLockBaseAddress(pixelBuffer, 0)
let baseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)
let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
let byteBuffer = UnsafeMutablePointer<UInt8>(baseAddress)

// Get luma value for pixel (43, 17)
let luma = byteBuffer[17 * bytesPerRow + 43]

CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)

BGRA示例

let pixelBuffer: CVPixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
CVPixelBufferLockBaseAddress(pixelBuffer, 0)
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let int32PerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let int32Buffer = UnsafeMutablePointer<UInt32>(baseAddress)

// Get BGRA value for pixel (43, 17)
let luma = int32Buffer[17 * int32PerRow + 43]

CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)

让luma = int32Buffer [17 * int32Buffer + 43]无法编译。 "二进制运算符“*”不能应用于类型为“Int”和“UnsafeMutablePointer <UInt32>”的操作数。我一直遇到类似的问题。如果我找到正确的转换方法,我会更新的。 - scord
对不起,打错字了。已修正。 - Codo
如何从整个CMSampleBuffer获取UInt8数组? CMSampleBuffer转换为[UInt8] - omarojo
1
@codo 将 let int32Buffer = UnsafeMutablePointer<UInt32>(baseAddress) 转换为 Swift4 的写法应该是 let int32Buffer = baseAddress.assumingMemoryBound(to: UInt32.self)。但是这种写法似乎不起作用!相反,baseAddress.assumingMemoryBound(to: UInt8.self) 却可以正常工作。 我无法理解原因,因为我的像素缓冲区格式是 kCVPixelFormatType_32BGRA。有什么线索吗? - Martin
2
/!\ 在BGRA示例中有一个拼写错误:let luma = int32Buffer[17 * int32PerRow + 43] 应该是 **let bgra = int32Buffer[17 * int32PerRow + 43*4]**,因为每个像素有4个值(B,G,R,A),所以水平偏移应该是x4。 - Martin

12

这是一种从BGRA像素缓冲区获取单个rgb值的方法。注意:在调用此方法之前,必须锁定缓冲区。

func pixelFrom(x: Int, y: Int, movieFrame: CVPixelBuffer) -> (UInt8, UInt8, UInt8) {
    let baseAddress = CVPixelBufferGetBaseAddress(movieFrame)
    
    let bytesPerRow = CVPixelBufferGetBytesPerRow(movieFrame)
    let buffer = baseAddress!.assumingMemoryBound(to: UInt8.self)
    
    let index = x*4 + y*bytesPerRow
    let b = buffer[index]
    let g = buffer[index+1]
    let r = buffer[index+2]
    
    return (r, g, b)
}

2
宽度以像素数给出,BGRA像素缓冲区的每个像素由4个字节表示。因此,index应该是4*x + y*bytesPerRow - J.E.K

7

Swift3 更新:

let pixelBuffer: CVPixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer)!
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0));
let int32Buffer = unsafeBitCast(CVPixelBufferGetBaseAddress(pixelBuffer), to: UnsafeMutablePointer<UInt32>.self)
let int32PerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
// Get BGRA value for pixel (43, 17)
let luma = int32Buffer[17 * int32PerRow + 43]

CVPixelBufferUnlockBaseAddress(pixelBuffer, 0)

你怎么从CMSampleBuffer中获取一个包含整个图像的UInt8数组?将CMSampleBuffer转换为[UInt8]。 - omarojo
3
如果我使用kCVPixelFormatType_14Bayer_RGGB原始格式,如何获取像素值? - Matt Colliss
警告:如果您不解开对CVPixelBufferGetBaseAddress的调用,您可能会错过重要的警告,从而导致未定义的行为。有关更多信息,请参见此答案 - Senseful

3

Swift 5

我曾经也遇到过同样的问题,最终得出了以下解决方案。我的 CVPixelBuffer 有尺寸为 68 x 68,可以通过以下方法进行检查:

CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
print(CVPixelBufferGetWidth(pixelBuffer))
print(CVPixelBufferGetHeight(pixelBuffer))

您还需要了解每行的字节数:

print(CVPixelBufferGetBytesPerRow(pixelBuffer))

在我的情况下,它是320。

此外,您需要了解像素缓冲区的数据类型,对我来说是Float32

然后,我构建了一个字节缓冲区,并按以下顺序连续读取字节(请记住锁定基地址,如上所示):

var byteBuffer = unsafeBitCast(CVPixelBufferGetBaseAddress(pixelBuffer), to: UnsafeMutablePointer<Float32>.self)
var pixelArray: Array<Array<Float>> = Array(repeating: Array(repeating: 0, count: 68), count: 68)
for row in 0...67{
    for col in 0...67{
        pixelArray[row][col] = byteBuffer.pointee
        byteBuffer = byteBuffer.successor()    
    }
    byteBuffer = byteBuffer.advanced(by: 12)
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))

你可能会对 byteBuffer = byteBuffer.advanced(by: 12) 这部分感到困惑。我们之所以要这么做,是因为:我们知道每行有320字节。但是,我们的缓冲区宽度为68,数据类型为Float32,例如每个值4个字节。这意味着我们每行实际上只有272字节,其余部分都是填充零。这些填充零可能是由于内存布局原因造成的。
因此,我们必须跳过每行的最后48个字节,这通过byteBuffer = byteBuffer.advanced(by: 12)来完成(12*4 = 48)。
这种方法与其他解决方案略有不同,因为我们使用指向下一个byteBuffer的指针。但我认为这种方法更容易理解和直观。

let advance = (bytesPerRow - bufferWidth) / MemoryLayout<Float32>.size - emrahgunduz
2
警告:如果您不解开对CVPixelBufferGetBaseAddress的调用,您可能会错过重要的警告,从而导致未定义的行为。有关更多信息,请参见此答案 - Senseful

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接