在任何iOS设备上复制CVPixelBuffer

7

我很难编写可靠地在任何iOS设备上复制CVPixelBuffer的代码。我的第一次尝试在iPad Pro上测试时工作正常:

extension CVPixelBuffer {
    func deepcopy() -> CVPixelBuffer? {
        let width = CVPixelBufferGetWidth(self)
        let height = CVPixelBufferGetHeight(self)
        let format = CVPixelBufferGetPixelFormatType(self)
        var pixelBufferCopyOptional:CVPixelBuffer?
        CVPixelBufferCreate(nil, width, height, format, nil, &pixelBufferCopyOptional)
        if let pixelBufferCopy = pixelBufferCopyOptional {
            CVPixelBufferLockBaseAddress(self, kCVPixelBufferLock_ReadOnly)
            CVPixelBufferLockBaseAddress(pixelBufferCopy, 0)
            let baseAddress = CVPixelBufferGetBaseAddress(self)
            let dataSize = CVPixelBufferGetDataSize(self)
            let target = CVPixelBufferGetBaseAddress(pixelBufferCopy)
            memcpy(target, baseAddress, dataSize)
            CVPixelBufferUnlockBaseAddress(pixelBufferCopy, 0)
            CVPixelBufferUnlockBaseAddress(self, kCVPixelBufferLock_ReadOnly)
        }
        return pixelBufferCopyOptional
    }
}

上述代码在 iPad Pro 上出现奔溃是因为 CVPixelBufferGetDataSize(self) 稍微大于 CVPixelBufferGetDataSize(pixelBufferCopy),因此 memcpy 函数写入了未分配的内存。因此我放弃使用上述代码并尝试了以下代码:
func copy() -> CVPixelBuffer?
{
    precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

    var _copy: CVPixelBuffer?

    CVPixelBufferCreate(
        nil,
        CVPixelBufferGetWidth(self),
        CVPixelBufferGetHeight(self),
        CVPixelBufferGetPixelFormatType(self),
        CVBufferGetAttachments(self, .shouldPropagate),
        &_copy)

    guard let copy = _copy else { return nil }

    CVPixelBufferLockBaseAddress(self, .readOnly)
    CVPixelBufferLockBaseAddress(copy, [])
    defer
    {
        CVPixelBufferUnlockBaseAddress(copy, [])
        CVPixelBufferUnlockBaseAddress(self, .readOnly)
    }

    for plane in 0 ..< CVPixelBufferGetPlaneCount(self)
    {
        let dest        = CVPixelBufferGetBaseAddressOfPlane(copy, plane)
        let source      = CVPixelBufferGetBaseAddressOfPlane(self, plane)
        let height      = CVPixelBufferGetHeightOfPlane(self, plane)
        let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(self, plane)

        memcpy(dest, source, height * bytesPerRow)
    }

    return copy
}

这在我的测试设备上都可以正常运行,但现在实际客户开始使用了,结果发现它会在iPad 6上崩溃(目前只有这个设备出现了问题)。这是对memcpy()的调用产生了EXC_BAD_ACCESS错误。

看起来很不可思议,因为似乎没有一个简单的API可以调用来解决这个问题,尽管让它可靠地工作如此艰难。或者我是在让事情变得更复杂了吗?感谢任何建议!


1
第二种实现看起来相当可靠。我能想象的唯一问题是,新像素缓冲区中的平面分配了不同的步幅长度(每行字节数)。步幅长度基于宽度×(每像素字节数),然后以未指定的方式向上舍入以实现最佳内存访问。因此,请检查CVPixelBufferGetBytesPerRowOfPlane(self, plane) == CVPixelBufferGetBytesPerRowOfPlane(copy, plane)。如果不是这样,请逐行复制像素平面。 - Codo
谢谢,你想把那个作为答案发布吗? - Nestor
它解决了崩溃问题吗? - Codo
很遗憾,只有在将其发布到应用商店后才能知道,因为它会在第六代iPad上崩溃,而其他设备则不会。 - Nestor
它运行正常!不再崩溃。 - Nestor
2个回答

11

这个问题和答案的组合非常有价值。我将进行一些微小的重构并添加控制流来处理没有平面的CVPixelBuffers

public extension CVPixelBuffer {
    func copy() throws -> CVPixelBuffer {
        precondition(CFGetTypeID(self) == CVPixelBufferGetTypeID(), "copy() cannot be called on a non-CVPixelBuffer")

        var _copy: CVPixelBuffer?

        let width = CVPixelBufferGetWidth(self)
        let height = CVPixelBufferGetHeight(self)
        let formatType = CVPixelBufferGetPixelFormatType(self)
        let attachments = CVBufferGetAttachments(self, .shouldPropagate)

        CVPixelBufferCreate(nil, width, height, formatType, attachments, &_copy)

        guard let copy = _copy else {
            throw PixelBufferCopyError.allocationFailed
        }

        CVPixelBufferLockBaseAddress(self, .readOnly)
        CVPixelBufferLockBaseAddress(copy, [])

        defer {
            CVPixelBufferUnlockBaseAddress(copy, [])
            CVPixelBufferUnlockBaseAddress(self, .readOnly)
        }

        let pixelBufferPlaneCount: Int = CVPixelBufferGetPlaneCount(self)


        if pixelBufferPlaneCount == 0 {
            let dest = CVPixelBufferGetBaseAddress(copy)
            let source = CVPixelBufferGetBaseAddress(self)
            let height = CVPixelBufferGetHeight(self)
            let bytesPerRowSrc = CVPixelBufferGetBytesPerRow(self)
            let bytesPerRowDest = CVPixelBufferGetBytesPerRow(copy)
            if bytesPerRowSrc == bytesPerRowDest {
                memcpy(dest, source, height * bytesPerRowSrc)
            }else {
                var startOfRowSrc = source
                var startOfRowDest = dest
                for _ in 0..<height {
                    memcpy(startOfRowDest, startOfRowSrc, min(bytesPerRowSrc, bytesPerRowDest))
                    startOfRowSrc = startOfRowSrc?.advanced(by: bytesPerRowSrc)
                    startOfRowDest = startOfRowDest?.advanced(by: bytesPerRowDest)
                }
            }

        }else {
            for plane in 0 ..< pixelBufferPlaneCount {
                let dest        = CVPixelBufferGetBaseAddressOfPlane(copy, plane)
                let source      = CVPixelBufferGetBaseAddressOfPlane(self, plane)
                let height      = CVPixelBufferGetHeightOfPlane(self, plane)
                let bytesPerRowSrc = CVPixelBufferGetBytesPerRowOfPlane(self, plane)
                let bytesPerRowDest = CVPixelBufferGetBytesPerRowOfPlane(copy, plane)

                if bytesPerRowSrc == bytesPerRowDest {
                    memcpy(dest, source, height * bytesPerRowSrc)
                }else {
                    var startOfRowSrc = source
                    var startOfRowDest = dest
                    for _ in 0..<height {
                        memcpy(startOfRowDest, startOfRowSrc, min(bytesPerRowSrc, bytesPerRowDest))
                        startOfRowSrc = startOfRowSrc?.advanced(by: bytesPerRowSrc)
                        startOfRowDest = startOfRowDest?.advanced(by: bytesPerRowDest)
                    }
                }
            }
        }
        return copy
    }
}

使用 Swift 5 有效。提供一些更多的背景...有许多格式可以在 AVCaptureVideoDataOutput.videoSettings 属性中使用。并非所有格式都有平面,特别是 ML 模型可能需要的那些。


7
第二个实现看起来非常可靠。我唯一能想象到的问题是,新像素缓冲区中的平面使用不同的步幅长度(每行字节数)分配。步幅长度基于宽度×(每像素字节数),然后以未指定的方式向上舍入,以实现最佳内存访问。
所以请检查:
CVPixelBufferGetBytesPerRowOfPlane(self, plane) == CVPixelBufferGetBytesPerRowOfPlane(copy, plane

如果不行,逐行复制像素平面:
for plane in 0 ..< CVPixelBufferGetPlaneCount(self)
{
    let dest            = CVPixelBufferGetBaseAddressOfPlane(copy, plane)
    let source          = CVPixelBufferGetBaseAddressOfPlane(self, plane)
    let height          = CVPixelBufferGetHeightOfPlane(self, plane)
    let bytesPerRowSrc  = CVPixelBufferGetBytesPerRowOfPlane(self, plane)
    let bytesPerRowDest = CVPixelBufferGetBytesPerRowOfPlane(copy, plane)

    if bytesPerRowSrc == bytesPerRowDest {
        memcpy(dest, source, height * bytesPerRowSrc)
    } else {
        var startOfRowSrc = source
        var startOfRowDest = dest
        for _ in 0..<height {
            memcpy(startOfRowDest, startOfRowSrc, min(bytesPerRowSrc, bytesPerRowDest))
            startOfRowSrc += bytesPerRowSrc
            startOfRowDest += bytesPerRowDest
        }
    }
}

在第六代iPad上运作良好。但是似乎你在第7行打错了字:bytesPerRowDest应该使用copy而不是self,否则你将始终进入if测试的第一个块。 - dgmz

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