【问题标题】:Making CIContext.render(CIImage, CVPixelBuffer) work with AVAssetWriter使 CIContext.render(CIImage, CVPixelBuffer) 与 AVAssetWriter 一起工作
【发布时间】:2019-09-24 20:59:33
【问题描述】:

我想使用 Core Image 处理一堆 CGImage 对象并将它们转换为 ma​​cOS 上的 QuickTime 电影。以下代码演示了需要什么,但output contains a lot of blank (black) frames

import AppKit
import AVFoundation
import CoreGraphics
import Foundation
import CoreVideo
import Metal

// Video output url.
let url: URL = try! FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("av.mov")
try? FileManager.default.removeItem(at: url)

// Video frame size, total frame count, frame rate and frame image.
let frameSize: CGSize = CGSize(width: 2000, height: 1000)
let frameCount: Int = 100
let frameRate: Double = 1 / 30
let frameImage: CGImage

frameImage = NSImage(size: frameSize, flipped: false, drawingHandler: {
    NSColor.red.setFill()
    $0.fill()
    return true
}).cgImage(forProposedRect: nil, context: nil, hints: nil)!

let pixelBufferAttributes: [CFString: Any]
let outputSettings: [String: Any]

pixelBufferAttributes = [
    kCVPixelBufferPixelFormatTypeKey: Int(kCVPixelFormatType_32ARGB),
    kCVPixelBufferWidthKey: Float(frameSize.width),
    kCVPixelBufferHeightKey: Float(frameSize.height),
    kCVPixelBufferMetalCompatibilityKey: true,
    kCVPixelBufferCGImageCompatibilityKey: true,
    kCVPixelBufferCGBitmapContextCompatibilityKey: true,
]

outputSettings = [
    AVVideoCodecKey: AVVideoCodecType.h264,
    AVVideoWidthKey: Int(frameSize.width),
    AVVideoHeightKey: Int(frameSize.height),
]

let writer: AVAssetWriter = try! AVAssetWriter(outputURL: url, fileType: .mov)
let input: AVAssetWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
let pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input, sourcePixelBufferAttributes: pixelBufferAttributes as [String: Any])

input.expectsMediaDataInRealTime = true

precondition(writer.canAdd(input))
writer.add(input)

precondition(writer.startWriting())
writer.startSession(atSourceTime: CMTime.zero)

let colorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
let context = CIContext(mtlDevice: MTLCreateSystemDefaultDevice()!)

Swift.print("Starting the render…")

// Preferred scenario: using CoreImage to fill the buffer from the pixel buffer adapter. Shows that
// CIImage + AVAssetWriterInputPixelBufferAdaptor are not working together.

for frameNumber in 0 ..< frameCount {
    var pixelBuffer: CVPixelBuffer?
    guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
    precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)

    precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
    defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }

    let ciImage = CIImage(cgImage: frameImage)
    context.render(ciImage, to: pixelBuffer!)

    // ???? This fails – the pixel buffer doesn't get filled. AT ALL! Why? How to make it work?
    let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
    precondition(bytes.contains(where: { $0 != 0 }))

    while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
    precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
}


// Unpreferred scenario: using CoreImage to fill the manually created buffer. Proves that CIImage 
// can fill buffer and working.

// for frameNumber in 0 ..< frameCount {
//     var pixelBuffer: CVPixelBuffer?
//     precondition(CVPixelBufferCreate(nil, frameImage.width, frameImage.height, kCVPixelFormatType_32ARGB, pixelBufferAttributes as CFDictionary, &pixelBuffer) == kCVReturnSuccess)
//
//     precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
//     defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
//
//     let ciImage = CIImage(cgImage: frameImage)
//     context.render(ciImage, to: pixelBuffer!)
//
//     // ✅ This passes.
//     let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
//     precondition(bytes.contains(where: { $0 != 0 }))
//
//     while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
//     precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
// }


// Unpreferred scenario: using CoreGraphics to fill the buffer from the pixel buffer adapter. Shows that
// buffer from pixel buffer adapter can be filled and working.

// for frameNumber in 0 ..< frameCount {
//     var pixelBuffer: CVPixelBuffer?
//     guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
//     precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)
//
//     precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
//     defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }
//
//     guard let context: CGContext = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer!), width: frameImage.width, height: frameImage.height, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { preconditionFailure() }
//     context.clear(CGRect(origin: .zero, size: frameSize))
//     context.draw(frameImage, in: CGRect(origin: .zero, size: frameSize))
//
//     // ✅ This passes.
//     let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
//     precondition(bytes.contains(where: { $0 != 0 }))
//
//     while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
//     precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
// }

let semaphore = DispatchSemaphore(value: 0)

input.markAsFinished()
writer.endSession(atSourceTime: CMTime(seconds: Double(frameCount) * frameRate, preferredTimescale: 600))
writer.finishWriting(completionHandler: { semaphore.signal() })

semaphore.wait()

Swift.print("Successfully finished rendering to \(url.path)")

但是,以下内容适用于 CGContext,但我需要 CIContext 才能使用 GPU。问题似乎出在AVAssetWriterInputPixelBufferAdaptor 的缓冲池提供的像素缓冲区上。将CIContext 渲染到单独创建的缓冲区并将它们附加到适配器可以工作,但是效率非常低。将CIContext 渲染到适配器池提供的缓冲区会导致没有数据写入缓冲区根本,它实际上包含全零,就好像两个不兼容一样!但是,使用CGImage 进行渲染是可行的,就像手动复制数据一样。

主要观察结果是CIContext.render 似乎异步工作,或者在缓冲区被填满和数据被写入视频流之间出现问题。换句话说,当缓冲区被刷新时,缓冲区中没有数据。以下是指向该方向的一种:

  1. 删除缓冲区锁定会导致几乎所有帧都被写入,除了前几帧,上面的代码实际上会生成一个correct output,但对于实际数据,行为与描述相同。
  2. 使用不同的编解码器(如 ProRes422)会导致几乎所有帧都正确写入,只有几个空白 - 上面的代码也会生成 correct output,但较大和复杂的图像会导致跳帧。

这段代码有什么问题,正确的处理方法是什么?

附:大多数 iOS 示例使用几乎相同的实现,并且似乎工作得非常好。我发现了一个hint,它可能与 macOS 不同,但看不到任何官方文档。

【问题讨论】:

  • 你需要一个自动释放池围绕主循环的帧,你当前的代码只会消耗所有内存,直到设备崩溃。
  • 另外,不要使用 kCVPixelFormatType_32ARGB,您应该使用 kCVPixelFormatType_32BGRA。

标签: avfoundation core-graphics metal core-image


【解决方案1】:

对于您的用例,最好使用pull-style APIsAVAssetWriterInput,因为您不需要实时处理任何媒体(就像从相机捕捉时一样)。

因此,与其在输入未准备好时暂停线程,不如等待它拉下一帧。在这种情况下,请记住还要将 expectsMediaDataInRealTime 设置为 false

我认为您当前方法的主要问题是,当作者尚未准备好时,您会暂停正在发生视频处理的线程。

(顺便说一句:你可以直接用纯色创建CIImages(CIImage(color:));不需要先创建CGImage。)

【讨论】:

  • 嘿弗兰克!欣赏输入。 1.我实际上是在实时捕获部分屏幕和特定窗口以对返回的CGImage进行后期处理,因此该部分是必要的。 2. 正确,但是当您取消注释“工作”部分时,同样不适用于CGImage,这是为了方便有人决定使用代码而留下的。顺便说一句,关于拉与推的好点,我对此也有同样的想法。虽然这两种方法似乎都有效,但拉动需要大量重写,我宁愿避免这样做,除非有支持证据表明它可以解决问题。
  • 您可以尝试新的CIRenderDestination API:从像素缓冲区创建一个CIRenderDestination 并在CIContext 上调用let task = context.startTask(toRender: image, to: destination)。然后,您可以在任务对象上显式调用 waitUntilCompleted() 以确保 Core Image 在您尝试将像素缓冲区写入视频文件之前完成处理。
  • 这看起来很有希望,不幸的是产生了同样的结果……该死!在挖掘 CoreImage 反汇编时遇到了一些日志记录语句。你知道是否有任何环境变量来触发详细日志记录吗?
  • 我不知道,但这太棒了……您是否在附加像素缓冲区的位置设置了一个断点并使用 QuickLook 检查它们?如果那些已经是黑色的会很有趣。
  • 是的,刚刚尝试过 + 几个选项。 CIContext.render 产生黑色像素并且没有输出。整个像素缓冲区在渲染后就为零。直接从CGImage 数据创建新的像素缓冲区会产生损坏的帧,就好像它的数据被部分释放并且部分变成黑色一样。最后memcpying CGImage 数据直接进入缓冲区就像CGContext 一样,没有伪影,但速度稍快一些,而且颜色平面都混在一起了。
【解决方案2】:

在与 Apple 开发者技术支持交谈后,似乎:

Core Image 延迟渲染,直到客户端请求访问帧缓冲区,即CVPixelBufferLockBaseAddress

所以,解决方法就是在调用CIContext.render之后做CVPixelBufferLockBaseAddress,如下所示:

for frameNumber in 0 ..< frameCount {
    var pixelBuffer: CVPixelBuffer?
    guard let pixelBufferPool: CVPixelBufferPool = pixelBufferAdaptor.pixelBufferPool else { preconditionFailure() }
    precondition(CVPixelBufferPoolCreatePixelBuffer(nil, pixelBufferPool, &pixelBuffer) == kCVReturnSuccess)

    let ciImage = CIImage(cgImage: frameImage)
    context.render(ciImage, to: pixelBuffer!)

    precondition(CVPixelBufferLockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess)
    defer { precondition(CVPixelBufferUnlockBaseAddress(pixelBuffer!, []) == kCVReturnSuccess) }

    let bytes = UnsafeBufferPointer(start: CVPixelBufferGetBaseAddress(pixelBuffer!)!.assumingMemoryBound(to: UInt8.self), count: CVPixelBufferGetDataSize(pixelBuffer!))
    precondition(bytes.contains(where: { $0 != 0 }))

    while !input.isReadyForMoreMediaData { Thread.sleep(forTimeInterval: 10 / 1000) }
    precondition(pixelBufferAdaptor.append(pixelBuffer!, withPresentationTime: CMTime(seconds: Double(frameNumber) * frameRate, preferredTimescale: 600)))
}

【讨论】:

    猜你喜欢
    • 2013-02-25
    • 1970-01-01
    • 2017-12-04
    • 1970-01-01
    • 2017-03-01
    • 2013-10-03
    • 2011-11-13
    • 2012-09-13
    • 2015-07-23
    相关资源
    最近更新 更多