如何将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,提问作者中山雄貴
相关产品推荐
相关产品推荐

