我是不是必须锁定从 AVCaptureVideoDataOutput 生成的 CVPixelBuffer
Posted
技术标签:
【中文标题】我是不是必须锁定从 AVCaptureVideoDataOutput 生成的 CVPixelBuffer【英文标题】:Do I have to lock a CVPixelBuffer produced from AVCaptureVideoDataOutput我是否必须锁定从 AVCaptureVideoDataOutput 生成的 CVPixelBuffer 【发布时间】:2021-02-02 20:24:59 【问题描述】:我有一个 AVCaptureVideoDataOutput
产生 CMSampleBuffer
实例传递给我的 AVCaptureVideoDataOutputSampleBufferDelegate
函数。我想有效地将像素缓冲区转换为 CGImage
实例,以便在我的应用程序的其他地方使用。
我必须小心不要保留对这些像素缓冲区的任何引用,否则捕获会话将由于OutOfBuffers
的原因开始丢帧。此外,如果转换时间过长,则帧将被丢弃,原因是FrameWasLate
。
之前我尝试使用CIContext
来渲染CGImage
,但是当捕获超过 30 FPS 时这被证明太慢了,我想以 60 FPS 捕获。在开始掉帧之前,我测试并达到了 38 FPS。
现在我尝试使用CGContext
,结果更好。我仍在丢帧,但频率明显降低。
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection)
// Capture at 60 FPS but only process at 4 FPS, ignoring all other frames
let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
guard timestamp - lastTimestamp >= CMTimeMake(value: 1, timescale: 4) else return
// Extract pixel buffer
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else return
// Lock pixel buffer before accessing base address
guard kCVReturnSuccess == CVPixelBufferLockBaseAddress(imageBuffer, .readOnly) else return
defer CVPixelBufferUnlockBaseAddress(imageBuffer, .readOnly)
// Use CGContext to render CGImage from pixel buffer
guard let cgimage = CGContext(data: CVPixelBufferGetBaseAddress(imageBuffer),
width: CVPixelBufferGetWidth(imageBuffer),
height: CVPixelBufferGetHeight(imageBuffer),
bitsPerComponent: 8,
bytesPerRow: CVPixelBufferGetBytesPerRow(imageBuffer),
space: cgColorSpace,
bitmapInfo: cgBitmapInfo).makeImage() else return
// Do something with cgimage...
我很好奇,接下来在不锁定像素缓冲区基地址的情况下尝试了这个。 当我注释掉这两行时,我完全停止丢帧,没有任何明显的影响。 似乎锁定机制花费了很长时间以至于丢帧,移除该机制显着降低了函数的运行时间并允许处理所有帧。
Apple's documentation 明确指出在CVPixelBufferGetBaseAddress
之前需要调用CVPixelBufferLockBaseAddress
。然而,由于AVCaptureVideoDataOutput
正在为其样本缓冲区使用预定义的内存池,因此基地址可能不会像通常情况那样发生变化。
我可以跳过锁定基地址吗?如果我不锁定基地址在这种特定情况下,可能发生的最坏情况是什么?
【问题讨论】:
我刚刚找到***.com/a/30947533/1500134,这表明锁定像素缓冲区会迫使 CPU 参与其中,这可能就是丢帧的原因。通过避免锁定,使用了 GPU,这肯定可以解释加速。现在我想知道为什么文档已更改并且不再说明这一点。 出于好奇:之后你用CGImages
做什么?为什么需要 CPU 端的视频帧?通常建议在 GPU 上进行尽可能多的处理。
@FrankSchlegel 我们正在通过 Core Image 过滤器运行它们并将它们传递给 Vision 以进行 CoreML 推理。如果我们决定保留图像(取决于视觉观察),我们只需要它们在最后的 CPU 上。您是对的,我们宁愿尽可能将它们保留在 GPU 上。
【参考方案1】:
根据您的描述,您根本不需要转换为CGImage
。您可以在 Core Image + Vision 管道中进行所有处理:
-
使用
CIImage(cvPixelBuffer:)
从相机的像素缓冲区创建CIImage
。
对CIImage
应用过滤器。
使用CIContext
将过滤后的图像渲染为新的CVPixelBuffer
。为获得最佳性能,请使用 CVPixelBufferPool
创建这些目标像素缓冲区。
将像素缓冲区传递给 Vision 进行分析。
如果 Vision 决定保留图像,请使用相同的 CIContext
将像素缓冲区(再次像 1. 中一样将其包装成 CIImage
)渲染为您选择的目标格式,例如 context.writeHEIFRepresentation(of:...)
.
只有最终将图像数据传输到CPU端。
【讨论】:
感谢您的想法,我会尝试一下,看看它在性能方面的比较。我唯一担心的是,在第 1 步和第 3 步之间,我们将保留对原始像素缓冲区的引用,如果处理速度不够快,可能会因分配的内存池中的缓冲区用完而导致丢帧。我目前正在使用 CGImage 的原因是在执行任何其他操作之前立即转储该像素缓冲区引用。 这不应该是一个问题,我想。 1 和 2 基本上是立即发生的,因为直到 3 才执行渲染。在 3 中,像素缓冲区一用于渲染就被释放。当您创建像素数据的副本并且无法像新缓冲区一样快速处理它时,无论如何您都会很快耗尽内存。 奇怪的是,将 CIImage 渲染到 CVPixelBuffer 的时间比 createCGImage 还要长。在这个速度下,它肯定还在 CPU 上——我不确定它为什么不使用 GPU。 createCGImage 需要 37 毫秒,render:toCVPixelBuffer 需要 46 毫秒,使用 CVPixelBufferPool 需要 58 毫秒。我的 OP 中的 CGContext.makeImage 机制在基地址锁定的情况下需要 21 毫秒,如果我不锁定,则需要 0.6 毫秒。 您是否要求AVCaptureDataOutput
提供kCVPixelFormatType_32BGRA
中的帧?我想Core Image 在渲染到CVPixelBuffer
时也会转换格式,这就是延迟的原因。您的CGContext
代码非常快,因为它基本上只是将内存指针重新转换为CGImageRef
,而无需进行任何转换或渲染。
是的,我确实将输出格式设置为 kCVPixelFormatType_32BGRA。我将结束这个问题,因为我在技术上找到了答案 - 但我可能会发布一个后续问题以进行进一步的性能讨论,因为我仍在努力获得我想要的渲染/复制时间。感谢您的帮助,您的建议很有见地!【参考方案2】:
这个问题从一开始就没有根据,因为我忽略了测试跳过锁的实际图像结果。如问题中所述,当我在初始化 CGContext 之前锁定基地址时,makeImage
渲染大约需要 17 毫秒。如果我跳过锁定并直接进入 CGContext 则 makeImage
需要 0.3 毫秒。
我错误地将这种速度差异解释为在后一种情况下 GPU 正在加速渲染。然而,实际发生的是CVPixelBufferGetBaseAddress
正在返回nil
而makeImage
没有渲染任何数据——产生一个纯白色的CGImage。
所以,简而言之,我的问题的答案是肯定的。基地址必须被锁定。
现在我要弄清楚如何加快速度。我以 60 FPS 的速度捕获,这意味着我希望我的渲染时间尽可能少于 16 毫秒,以便在下一个到达之前删除 CMSampleBuffer 引用。
【讨论】:
以上是关于我是不是必须锁定从 AVCaptureVideoDataOutput 生成的 CVPixelBuffer的主要内容,如果未能解决你的问题,请参考以下文章