原文:Image Resizing Techniques
作者:Mattt Thompson

古往今来,iOS 开发者被一个简单的问题所困惑:“如何调整图像大小?”。由于开发者和平台之间相互不信任,导致这个问题令人费解。网页搜索结果中有许许多多的代码示例,都声称是正确的方案,其他的都是错误的。

这真的好尴尬 😂。

本周文章通过对每个在 iOS (和 MacOS,UIImage -> NSImage 进行合适的转换)上的图像大小调整方法进行实验证明提供性能特点,尽力对每个方法提供清晰的解释,而不是简单的用一个方法应用所有场景。

在进一步阅读之前,请注意以下事项:

当在 UIImageView 中设置 UIImage 时,在大多数使用场景中不必手动调整图像大小。可以简单地设置 contentMode 属性为 .scaleAspectFit 确保整张图像显示在 image viewframe 中,或者设置为 .scaleAspectFill 让整张图像通过中间裁剪来填充整个 image view

1
2
imageView.contentMode = .scaleAspectFit
imageView.image = image

确定缩放大小

在做任何图像大小调整时,首先必须确定缩放的目标大小。

按系数缩放

通过常量因子进行图像缩放是最简单的方式。通常来说,这涉及到除以整数以至于减少原来的大小(而不是乘以整数以至于放大原来的大小)。

新的 CGSize 能够通过单独缩放宽度和高度计算得到:

1
let size = CGSize(width: image.size.width / 2, height: image.size.height / 2)

…或者应用一个 CGAffineTransform:

1
let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5))

按长宽比缩放

在不改变原来的长宽比的情况下缩放原来的大小以适合矩形时非常有用。AVFoundation 框架中的 AVMakeRectWithAspectRatioInsideRect 是个有用的函数可以帮助你计算:

1
2
import AVFoundation
let rect = AVMakeRectWithAspectRatioInsideRect(image.size, imageView.bounds)

图像大小调整

这里有一系列不同的方法去调整图像大小,每个都有不同能力和性能特点。

UIGraphicsBeginImageContextWithOptions & UIImage -drawInRect:

图像大小调整的最高级 API 能够在 UIKit 框架中找到。给定 UIImage,通过 UIGraphicsBeginImageContextWithOptions()UIGraphicsGetImageFromCurrentImageContext(),临时图形上下文能够用来渲染成缩放版本的图像:

1
2
3
4
5
6
7
8
9
10
11
let image = UIImage(contentsOfFile: self.URL.absoluteString!)

let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen

UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.drawInRect(CGRect(origin: CGPointZero, size: size))

let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

UIGraphicsBeginImageContextWithOptions() 创建原始绘制的临时渲染上下文。第一个入参 size 是缩放后的图像大小。第二个入参 isOpaque 是用来确定是否需要渲染 alpha 通道。对于不透明(即 alpha 通道)的图像,将其设置为 false 可能会导致粉红色的图像。第三个入参 scale 是显示缩放系数。当设置为 0.0 时,将使用 main screen 的缩放系数,对于 Retina 显示屏,其缩放系数为 2.0 或更高(iPhone 6 Plus 为 3.0)。

CGBitmapContextCreate & CGContextDrawImage

Core Graphics / Quartz 2D 提供了一套较低级的 API,允许进行更高级的配置。给定 CGImage,通过 CGBitmapContextCreate()CGBitmapContextCreateImage(),临时位图上下文能够用来渲染缩放的图像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let cgImage = UIImage(contentsOfFile: self.URL.absoluteString!).CGImage

let width = CGImageGetWidth(cgImage) / 2
let height = CGImageGetHeight(cgImage) / 2
let bitsPerComponent = CGImageGetBitsPerComponent(cgImage)
let bytesPerRow = CGImageGetBytesPerRow(cgImage)
let colorSpace = CGImageGetColorSpace(cgImage)
let bitmapInfo = CGImageGetBitmapInfo(cgImage)

let context = CGBitmapContextCreate(nil, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo.rawValue)

CGContextSetInterpolationQuality(context, kCGInterpolationHigh)

CGContextDrawImage(context, CGRect(origin: CGPointZero, size: CGSize(width: CGFloat(width), height: CGFloat(height))), cgImage)

let scaledImage = CGBitmapContextCreateImage(context).flatMap { UIImage(CGImage: $0) }

CGBitmapContextCreate 需要几个参数构建具有所需的尺寸和给定的颜色空间中每个通道的内存量的上下文。在这个例子中,这些值都是从 CGImage 中获取的。接下来,CGContextSetInterpolationQuality 允许上下文以各种保真度水平内插像素。在这种情况下,传递 kCGInterpolationHigh 以获得最佳结果。CGContextDrawImage 允许图像以给定的大小和位置绘制,允许图像在特定的边上被裁剪或者适合一组图像特征,例如面部。最后,CGBitmapContextCreateImage 从上下文中创建 CGImage

CGImageSourceCreateThumbnailAtIndex

Image I/O 是一个功能强大但鲜为人知的用于处理图像的框架。独立于 Core Graphics,它能读写很多不同的格式,访问照片的元数据,执行常见的图像处理操作。该框架提供了平台上最快的图像编码器和解码器,具有先进的缓存机制,甚至可以增量加载图像。

CGImageSourceCreateThumbnailAtIndex 提供了一个简洁的API,其中包含的选项与在等价的 Core Graphics 调用中找到的选项不同:

1
2
3
4
5
6
7
8
9
10
import ImageIO

if let imageSource = CGImageSourceCreateWithURL(self.URL, nil) {
let options: [NSString: NSObject] = [
kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) / 2.0,
kCGImageSourceCreateThumbnailFromImageAlways: true
]

let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options).flatMap { UIImage(CGImage: $0) }
}

给定一个 CGImageSource 和一组选项,CGImageSourceCreateThumbnailAtIndex 创建了一个缩略图。通过 kCGImageSourceThumbnailMaxPixelSize 完成调整大小。指定最大尺寸除以常数系数可以在保持原始长宽比的同时对图像进行缩放。通过指定 kCGImageSourceCreateThumbnailFromImageIfAbsent 或者 kCGImageSourceCreateThumbnailFromImageAlways,Image I/O 将会自动缓存缩放结果给后续的调用。

Core Image 的 Lanczos 重采样

Core Image 通过 CILanczosScaleTransform 过滤器提供了一个内置的 Lanczos 重采样 功能。尽管可以说是比 UIKit 更高级别的 API,但在 Core Image 中普遍使用键值编码(KVO)使其变得笨拙。

也就是说,至少这种模式是一致的。创建变换过滤器,配置它并渲染成输出图像的过程就像任何其他 Core Image 工作流程一样:

1
2
3
4
5
6
7
8
9
10
let image = CIImage(contentsOfURL: self.URL)

let filter = CIFilter(name: "CILanczosScaleTransform")!
filter.setValue(image, forKey: "inputImage")
filter.setValue(0.5, forKey: "inputScale")
filter.setValue(1.0, forKey: "inputAspectRatio")
let outputImage = filter.valueForKey("outputImage") as! CIImage

let context = CIContext(options: [kCIContextUseSoftwareRenderer: false])
let scaledImage = UIImage(CGImage: self.context.createCGImage(outputImage, fromRect: outputImage.extent()))

CILanczosScaleTransform 接收 inputImageinputScale,和 inputAspectRatio,所有这些都是不言自明的。由于 UIImage(CIImage:) 并不像预期的那样工作,因此需要 CIContext 通过 CGImageRef 中间表示来创建 UIImage

创建一个 CIContext 是一个昂贵的操作,因此应该总是使用缓存的上下文来重复调整大小。CIContext 可以使用 GPU 或者 CPU(慢得多)来创建 CIContext 进行渲染 - 使用选项字典中的 kCIContextUseSoftwareRenderer 键来指定哪一个。

Accelerate 中的 vImage

Accelerate 框架 包含一套 vImage 图像处理功能,并具有一组缩放图像缓冲区的功能。这些较低级别的 API 保证以较低的功耗实现高性能,但是要以自己管理缓冲区为代价。以下是 GitHub 上 Nyx0uf 提供的 Swift 版本的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let cgImage = UIImage(contentsOfFile: self.URL.absoluteString!).CGImage

// create a source buffer
var format = vImage_CGImageFormat(bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: nil,
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.First.rawValue),
version: 0, decode: nil, renderingIntent: CGColorRenderingIntent.RenderingIntentDefault)
var sourceBuffer = vImage_Buffer()
defer {
sourceBuffer.data.dealloc(Int(sourceBuffer.height) * Int(sourceBuffer.height) * 4)
}

var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &format, nil, cgImage, numericCast(kvImageNoFlags))
guard error == kvImageNoError else { return nil }

// create a destination buffer
let scale = UIScreen.mainScreen().scale
let destWidth = Int(image.size.width * 0.5 * scale)
let destHeight = Int(image.size.height * 0.5 * scale)
let bytesPerPixel = CGImageGetBitsPerPixel(image.CGImage) / 8
let destBytesPerRow = destWidth * bytesPerPixel
let destData = UnsafeMutablePointer<UInt8>.alloc(destHeight * destBytesPerRow)
defer {
destData.dealloc(destHeight * destBytesPerRow)
}
var destBuffer = vImage_Buffer(data: destData, height: vImagePixelCount(destHeight), width: vImagePixelCount(destWidth), rowBytes: destBytesPerRow)

// scale the image
error = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, numericCast(kvImageHighQualityResampling))
guard error == kvImageNoError else { return nil }

// create a CGImage from vImage_Buffer
let destCGImage = vImageCreateCGImageFromBuffer(&destBuffer, &format, nil, nil, numericCast(kvImageNoFlags), &error)?.takeRetainedValue()
guard error == kvImageNoError else { return nil }

// create a UIImage
let scaledImage = destCGImage.flatMap { UIImage(CGImage: $0, scale: 0.0, orientation: image.imageOrientation) }

这里使用的 Accelerate API 明显比其他调整大小的方法在更低的层次上运行。要使用这个方法,
首先通过 vImageBuffer_InitWithCGImage()vImage_CGImageFormat 从你的 CGImage 中创建源缓存区。目标缓冲区以所需的图像分辨率进行分配,然后 vImageScale_ARGB8888 完成调整图像大小的实际工作。当对大于应用程序内存限制的图像进行操作时需要管理自己的缓冲区,这是留给读者的一个练习。

性能基准

那么这些各种方法如何相互叠加呢?

以下是通过此项目在运行 iOS 8.4 的 iPhone 6 上完成的一组性能基准的结果:

JPEG

加载,缩放和显示来自美国宇航局 Visible Earth 的大尺寸高分辨率(12000 ⨉ 12000 px 20 MB JPEG)源图像在 1/10 的大小:

Operation Time (sec) σ
UIKit 0.612 14%
Core Graphics1 0.266 3%
Image I/O 0.255 2%
Core Image2 3.703 33%
vImage3

PNG

加载、缩放和显示一个相当大的(1024 ⨉ 1024 px 1MB PNG)渲染的 Postgres.app 图标在 1/10 的大小:

Operation Time (sec) σ
UIKit 0.044 30%
Core Graphics4 0.036 10%
Image I/O 0.038 11%
Core Image5 0.053 68%
vImage 0.050 25%

1, 4 结果在不同的 CGInterpolationQuality 值上是一致的,在性能基准上差异可以忽略不计。

3 美国宇航局 Visible Earth 图像的大小大于设备一次能够处理的大小。

2, 5CIContext 创建时所传的选项中设置 kCIContextUseSoftwareRenderer 为 true 会产生比基础结果慢一个数量级的结果。

结论

  • UIKitCore Graphics,和 Image I/O 在大多数图像上都可以很好地进行缩放操作。
  • Core Image 的表现优于图像缩放操作。实际上,Core Image 编程指南的性能最佳实践章节特别推荐使用 Core Graphics 或者 Image I/O 函数预先裁剪或者降低采样图像。
  • 对于一般的没有任何额外功能的图像缩放,UIGraphicsBeginImageContextWithOptions 可能是最好的选择。
  • 如果需要考虑图像质量,考虑使用 CGBitmapContextCreate 结合 CGContextSetInterpolationQuality
  • 当缩放图像的目的是显示缩略图时,CGImageSourceCreateThumbnailAtIndex 为渲染和缓存提供了一个引人注目的解决方案。
  • 除非你已经在使用 vImage,否则使用低级 Accelerate 框架进行大小调整的额外工作并不值得。