将JPEG UIIMAGE RAM使用减少50%
#编程 #教程 #ios

在2013年,苹果从过去的详细质地设计转变为填充了基本形状和矢量图标的更简单的样式。但是,即使在现代的直线和普通梯度的现代时代,栅格图形仍然没有过时。在iOS上尤其如此,在iOS上,对快速向量形状渲染的支持有限。绘制复杂用户界面的最有效方法是将静态零件转换为简单的图像,即iOS Compositor脱颖而出的区域。除了UI元素外,栅格图形还在非UI元素(例如照片)中起着必不可少的作用,这些元素无法仅使用矢量图形来充分代表。

渲染栅格图像可以以非显而易见的方式极大地影响设备的内存。这是因为图像所使用的内存由其像素的数量而不是文件大小决定。在本文中,我们将利用在Apple平台上渲染此类图像的更晦涩的技术之一。

RGB与Yuv

在iOS中,我们通常使用表示每个像素的Argb值数组的栅格图像(其中A架代表Alpha):

let pixels: [UInt8] = [
//  a     r     g     b
    0xff, 0xff, 0x00, 0x00,
    ...
]

在此表示形式中,一个像素需要4个字节的存储。从角度看,iPhone 14 Pro Max的分辨率为2796 x 1290,需要分配2796 * 1290 * 4 = 14427360字节(或RAM 14.4兆字节)的RAM以填充整个屏幕。为了进行比较,相同大小的平均JPEG照片将占据磁盘空间的1兆字节左右。

jpegs使用许多技巧来减少图像数据的大小,其中之一是对YUV的巧妙重组。 RGB数据可以无误地转换为YUV表示:

func rgbToYuv(r: Double, g: Double, b: Double) -> (y: Double, u: Double, v: Double) {
    let luminance = (0.299 * r) + (0.587 * g) + (0.114 * b)
    let u = (1.0 / 1.772) * (b - luminance)
    let v = (1.0 / 1.402) * (r - luminance)
    return (luminance, u, v)
}

和背面:

func yuvToRgb(y: Double, u: Double, v: Double) -> (r: Double, g: Double, b: Double) {
    let r = 1.402 * v + y
    let g = (y - (0.299 * 1.402 / 0.587) * v - (0.114 * 1.772 / 0.587) * u)
    let b = 1.772 * u + y
    return (r, g, b)
}

在YUV表示中,第一个组件(y)表示像素的亮度,而其他两个编码颜色。

但是,简单地将我们的RGB数据转换为YUV格式不会导致任何数据存储节省。为了实际压缩数据,我们需要丢弃一些信息,从而使颜色转换损失。

色度子采样

当涉及像素亮度的变化时,人眼非常敏感,但对颜色的看法较低。事实证明,我们可以将颜色数据存储在分辨率的一半,而不会在视觉清晰度上损失太多,尤其是在更高的分辨率和自然的摄影图像下。这被称为4:2:0子采样。

上图说明了在视觉上显而易见的色度子采样的极端情况 - 两个鲜艳的对比对象重叠。在天然发生的图像中,效果不太明显。

4:2:0采样的图像需要少50%的存储字节,因为每四个Yuv像素共享相同的颜色值:

4 RGB pixels = 4 * (4 * 3) = 12 bytes
4 YUV pixels = 4 * 1 + 1 + 1 = 6 bytes

uiimage和yuv

不幸的是,iOS不提供任何使我们可以将未压缩的YUV 4:2:0数据存储在UIImage中的API。但是,有几种方法可以解决此问题。

首先,可以使用AVSampleBufferDisplayLayer显示静态图像内容。 AVSampleBufferDisplayLayer可以显示ARGB内容,但是为了实现RAM空间的节省,我们将使用vImage
将图像转换为YUV格式

func imageToYUVCVPixelBuffer(image: UIImage) -> CVPixelBuffer? {
    // 1
    guard let image = image.preparingForDisplay(), let cgImage = image.cgImage, let data = cgImage.dataProvider?.data, let bytes = CFDataGetBytePtr(data), let colorSpace = cgImage.colorSpace, case .rgb = colorSpace.model, cgImage.bitsPerPixel / cgImage.bitsPerComponent == 4 else {
        return nil
    }

    let width = cgImage.width
    let height = cgImage.width

    // 2
    var pixelBuffer: CVPixelBuffer? = nil
    let _ = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, [
        kCVPixelBufferIOSurfacePropertiesKey: NSDictionary()
    ] as CFDictionary, &pixelBuffer)
    guard let pixelBuffer else {
        return nil
    }

    CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    defer {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
    }
    guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)?.assumingMemoryBound(to: CVPlanarPixelBufferInfo_YCbCrBiPlanar.self) else {
        return nil
    }

    // 3
    var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 0, CbCr_bias: 128, YpRangeMax: 255, CbCrRangeMax: 255, YpMax: 255, YpMin: 0, CbCrMax: 255, CbCrMin: 0)
    var info = vImage_ARGBToYpCbCr()
    if vImageConvert_ARGBToYpCbCr_GenerateConversion(kvImage_ARGBToYpCbCrMatrix_ITU_R_709_2, &pixelRange, &info, kvImageARGB8888, kvImage420Yp8_Cb8_Cr8, vImage_Flags(kvImageDoNotTile)) != kvImageNoError {
        return nil
    }

    // 4
    var srcBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bytes), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: cgImage.bytesPerRow)
    var dstBufferY = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddress).advanced(by: Int(CFSwapInt32BigToHost(UInt32(baseAddress.pointee.componentInfoY.offset)))), height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(CFSwapInt32BigToHost(baseAddress.pointee.componentInfoY.rowBytes)))
    var dstBufferCbCr = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: baseAddress).advanced(by: Int(CFSwapInt32BigToHost(UInt32(baseAddress.pointee.componentInfoCbCr.offset)))), height: vImagePixelCount(height / 2), width: vImagePixelCount(width / 2), rowBytes: Int(CFSwapInt32BigToHost(baseAddress.pointee.componentInfoCbCr.rowBytes)))

    // 5
    let permuteMap: [UInt8] = [3, 0, 1, 2]
    if vImageConvert_ARGB8888To420Yp8_CbCr8(&srcBuffer, &dstBufferY, &dstBufferCbCr, &info, permuteMap, vImage_Flags(kvImageDoNotTile)) != kvImageNoError {
        return nil
    }

    return pixelBuffer
}

1-确保将源图像完全加载到存储器中并以适当的格式加载。
2-创建一个CVImagePixelBuffer,将以kCVPixelFormatType_420YpCbCr8BiPlanarFullRange格式存储数据。
3-为rgb准备YUV转换的vimage。
4-填充源和目的地vimage缓冲区结构。
5-执行实际转换。

要在AVSampleBufferDisplayLayer中显示CVPixelBuffer,需要将其封装在CMSampleBuffer中:

public func makeCMSampleBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
    var sampleBuffer: CMSampleBuffer?

    var videoInfo: CMVideoFormatDescription? = nil
    CMVideoFormatDescriptionCreateForImageBuffer(allocator: nil, imageBuffer: pixelBuffer, formatDescriptionOut: &videoInfo)
    guard let videoInfo else {
        return nil
    }

    var timingInfo = CMSampleTimingInfo(
        duration: CMTimeMake(value: 1, timescale: 30),
        presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
        decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
    )
    CMSampleBufferCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: videoInfo, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer)

    guard let sampleBuffer else {
        return nil
    }

    let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray
    let dict = attachments[0] as! NSMutableDictionary
    dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String)

    return sampleBuffer
}

现在我们最终可以在屏幕上显示YUV图像:

let videoLayer = AVSampleBufferDisplayLayer()
videoLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 70.0), size: CGSize(width: 200.0, height: 200.0))
videoLayer.enqueue(makeCMSampleBuffer(pixelBuffer: imageToYUVCVPixelBuffer(image: image)!)!)
view.layer.addSublayer(videoLayer)

更有效的替代方案

AVSampleBufferDisplayLayer是一个相对重型的UI元素,在后台进行广泛的隐藏状态管理。初始化此类型的一层所需的时间通常是不可预测的,并且可能偶尔会导致动画延迟。尽管AVSampleBufferDisplayLayer擅长显示YUV视频内容,但其高级功能对于显示简单的图像并不是必需的。取而代之的是,我们可以利用Uikit的晦涩功能,使我们可以将iOsurface支持的CVPixelBuffer直接分配给CALayer.contents

let directLayer = CALayer()
directLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 70.0), size: CGSize(width: 200.0, height: 200.0))
directLayer.contents = imageToYUVCVPixelBuffer(image: image)!
view.layer.addSublayer(directLayer)

如果您在模拟器中运行此代码,则不会看到任何图像。它仅适用于实际的iOS设备和本机MacOS应用。

结论

该技术可以琐碎地扩展以管理YUV+Alpha映像(使用kCVPixelFormatType_420YpCbCr8BiPlanarFullRange并根据需要调整转换例程)。

A 4:2:0编码的YUV图像比等效的RGB图像所需的RAM空间少50%。出于说明目的,本文中的代码将现有的RGB图像转换为YUV格式,从而导致该应用程序在转换过程中使用了150%的RGB等效RAM。为了避免这种峰,建议直接以Yuv格式来摘录图像。可以使用能够直接输出yuv数据的libjpeg衍生的库来实现这一点。