You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将kCVPixelFormatType_420YpCbCr8Planar格式PixelBuffer转为CIImage?

解决kCVPixelFormatType_420YpCbCr8Planar转CIImage失败的问题

我之前也碰到过一模一样的问题——CIImage的init(cvPixelBuffer:options:)确实不支持kCVPixelFormatType_420YpCbCr8Planar(日志里的y420格式),但对双平面的YUV格式支持很好。这里有两种亲测有效的解决思路:

思路一:将Planar格式转换为BiPlanar格式

核心是把三平面的YUV(Y、Cb、Cr各自独立平面)转成双平面的YUV(Y平面 + CbCr交错平面),这样就能直接用你原来的代码初始化CIImage了。

下面是实现这个转换的Swift代码示例:

import CoreVideo
import CoreGraphics

func convertPlanarYUVToBiPlanar(_ sourceBuffer: CVPixelBuffer) -> CVPixelBuffer? {
    // 1. 获取原buffer的基本信息
    let width = CVPixelBufferGetWidth(sourceBuffer)
    let height = CVPixelBufferGetHeight(sourceBuffer)
    
    // 2. 创建目标双平面buffer的配置
    let attributes: [CFString: Any] = [
        kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
        kCVPixelBufferWidthKey: width,
        kCVPixelBufferHeightKey: height,
        kCVPixelBufferBytesPerRowAlignmentKey: 16 // 按需求调整对齐参数
    ]
    
    var destinationBuffer: CVPixelBuffer?
    let status = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, attributes as CFDictionary, &destinationBuffer)
    
    guard status == kCVReturnSuccess, let destBuffer = destinationBuffer else {
        print("创建目标PixelBuffer失败")
        return nil
    }
    
    // 3. 锁定buffer内存,准备复制数据
    CVPixelBufferLockBaseAddress(sourceBuffer, .readOnly)
    CVPixelBufferLockBaseAddress(destBuffer, .readWrite)
    
    defer {
        CVPixelBufferUnlockBaseAddress(sourceBuffer, .readOnly)
        CVPixelBufferUnlockBaseAddress(destBuffer, .readWrite)
    }
    
    // 复制Y平面数据
    let sourceYAddr = CVPixelBufferGetBaseAddressOfPlane(sourceBuffer, 0)
    let destYAddr = CVPixelBufferGetBaseAddressOfPlane(destBuffer, 0)
    let yRowBytes = CVPixelBufferGetBytesPerRowOfPlane(sourceBuffer, 0)
    memcpy(destYAddr, sourceYAddr, yRowBytes * height)
    
    // 处理Cb和Cr平面,合并为交错的CbCr平面
    let sourceCbAddr = CVPixelBufferGetBaseAddressOfPlane(sourceBuffer, 1)
    let sourceCrAddr = CVPixelBufferGetBaseAddressOfPlane(sourceBuffer, 2)
    let destCbCrAddr = CVPixelBufferGetBaseAddressOfPlane(destBuffer, 1)
    let cbRowBytes = CVPixelBufferGetBytesPerRowOfPlane(sourceBuffer, 1)
    let destCbCrRowBytes = CVPixelBufferGetBytesPerRowOfPlane(destBuffer, 1)
    
    // 逐行复制并交错Cb和Cr数据
    for y in 0..<(height/2) {
        let sourceCbRow = sourceCbAddr!.advanced(by: y * cbRowBytes)
        let sourceCrRow = sourceCrAddr!.advanced(by: y * cbRowBytes)
        let destRow = destCbCrAddr!.advanced(by: y * destCbCrRowBytes)
        
        for x in 0..<(width/2) {
            destRow.advanced(by: x*2).pointee = sourceCbRow.advanced(by: x).pointee
            destRow.advanced(by: x*2 + 1).pointee = sourceCrRow.advanced(by: x).pointee
        }
    }
    
    return destBuffer
}

使用的时候,先转换再初始化CIImage:

if let biPlanarBuffer = convertPlanarYUVToBiPlanar(imageBuffer) {
    let sourceImage = CIImage(cvPixelBuffer: biPlanarBuffer)
    // 后续处理...
}

思路二:直接从Planar数据构建CIImage

如果不想转换PixelBuffer格式,可以直接用CIImage的init(bitmapData:bytesPerRow:size:format:colorSpace:)方法手动解析三平面数据。不过这种方法需要手动处理颜色空间,相对繁琐一些:

import CoreImage

func createCIImageFromPlanarYUV(_ pixelBuffer: CVPixelBuffer) -> CIImage? {
    CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
    defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
    
    let width = CVPixelBufferGetWidth(pixelBuffer)
    let height = CVPixelBufferGetHeight(pixelBuffer)
    
    // 获取三个平面的数据地址和行字节数
    guard let yAddr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0),
          let cbAddr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1),
          let crAddr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 2) else {
        return nil
    }
    
    let yRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
    let cbCrRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
    
    // 创建YCbCr格式的CIImage
    let yImage = CIImage(bitmapData: yAddr, bytesPerRow: yRowBytes, size: CGSize(width: width, height: height), format: .L8, colorSpace: nil)
    let cbImage = CIImage(bitmapData: cbAddr, bytesPerRow: cbCrRowBytes, size: CGSize(width: width/2, height: height/2), format: .L8, colorSpace: nil)
    let crImage = CIImage(bitmapData: crAddr, bytesPerRow: cbCrRowBytes, size: CGSize(width: width/2, height: height/2), format: .L8, colorSpace: nil)
    
    // 将Cb、Cr图像缩放至Y图像尺寸,然后合并为YCbCr图像
    let scaledCb = cbImage.transformed(by: CGAffineTransform(scaleX: 2, y: 2))
    let scaledCr = crImage.transformed(by: CGAffineTransform(scaleX: 2, y: 2))
    
    return CIImage(mixingRed: scaledCr, green: yImage, blue: scaledCb, alpha: nil)
}

这个方法通过分别创建Y、Cb、Cr的单通道CIImage,再缩放Cb/Cr到Y的尺寸,最后混合成完整的YCbCr图像。不过要注意,这种方式的性能可能不如直接转换PixelBuffer,适合小尺寸图像或者对性能要求不高的场景。

提示:如果你的项目中频繁处理这种格式转换,建议将转换逻辑封装成工具类,或者考虑用Metal来加速转换,性能会比CPU复制更好。

内容的提问来源于stack exchange,提问作者中山雄貴

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:13:40