Image Resizing Techniques

图片尺寸调整技术

原文链接: Image Resizing Techniques

自有历史以来,iOS的开发者一直面临着一个困惑:“我要怎么调整一个图片的尺寸呢?”这个问题甚至导致了开发者与开发平台了相互不信任。网页搜索结果有数以千计,都声称自己才是真正的解决方案。

其实这个局面挺尴尬的。

本周的文章将试图基于经验深入了解各种实现方案的性能特性,而不是简单地认为谁是最佳方案。 最终在各种iOS平台上调整图片尺寸的方案中提供一个清晰的解释(对于OS X,适当地将UIImage变成NSImage)。

在继续阅读之前,请注意
在使用UIImageView时,大部分情况下手动调整图片的大小并不是很必要。我们可以通过给contentMode属性来实现大部分的需求,例举一下两个值:

  • .ScaleAspectFit 以等比例的形式将图片的frame都显示在UIImageView中。
  • .ScaleAspectFill 以等比例的形式将图片充满整个:
1
2
imageView.contentMode = .ScaleAspectFit
imageView.image = image

确定最终的尺寸

在所有工作之前,我们要先确定图片调整的目标大小。

以常量缩放

缩放一个图片最简单的方法就是设置一个常量。多数情况下,是希望通过除以一个整数来减少原来的尺寸(通过乘以整数来放大图片的需求比较少)。
单独为长宽分别计算最后得到一个新的CGSize:

1
let size = CGSizeMake(image.size.width / 2.0, image.size.height / 2.0)

或者使用一个CGAffineTransform:

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

等比例缩放

等比例缩放在很多场景下都非常有用。AVMakeRectWithAspectRatioInsideRect是一个AVFoundation框架中很有用的一个方法,它会帮你解决计算方面的问题

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

调整尺寸

iOS有一系列的方法来实现图片的尺寸调整,当然他们也有着不同的能力和性能特性。

UIGraphicsBeginImageContextWithOptions & UIImage -drawInRect:

这是我们在UIKit框架中能够找到的最高级别封装的API。指定一个UIImage,配合一个临时的图形上下文(graphics context)就可以用UIGraphicsBeginImageContextWithOptions()和UIGraphicsGetImageFromCurrentImageContext()来渲染一个缩放后的版本:

1
2
3
4
5
6
7
8
9
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()

UIGraphicsBeginImageContextWithOptions()创建了一个临时的渲染上下文(rendering context)到原始图片。第一个参数size是要调整到的尺寸。第二个参数isOpaque用来决定是否需要渲染alpha通道。如果将false赋给它然后作用于没有透明属性的图片可能会导致图片看起来有一种粉色色调的感觉。第三个参数scale是用于显示的缩放比例。如果设置为0.0,则会使用主屏幕的scale属性,对于Retina屏幕来说就是2.0或更高(iPhone 6 Plus则是3.0)

CGBitmapContextCreate & CGContextDrawImage

Core Graphics / Quartz 2D 提供了一个底层的API集合,它允许更多的高级设置。给定一个CGImage ,创建一个临时的位图上下文(bitmap context)用CGBitmapContextCreate()和CGBitmapContextCreateImage()来渲染缩放后的图片:

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

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

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

CGContextSetInterpolationQuality(context, kCGInterpolationHigh)

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

let scaledImage = UIImage(CGImage: CGBitmapContextCreateImage(context))

CGBitmapContextCreate需要几个参数来构造一个上下文,它定义了需求的尺寸和每一个给定色彩空间通道的内存大小。在示例中,这些值都是从CGImage获取到的。接着,CGContextSetInterpolationQuality允许上下文插入像素以实现不同程度的保真结果。在这个情况下,’kCGInterpolationHigh’会返回最好的结果。CGContextDrawImage允许在给定的大小和位置绘制图片,允许为图片裁切特定的边,或者是符合一些特性集合,例如脸部检测等等。最后CGBitmapContextCreateImage从上下文中创建一个CGImage

CGImageSourceCreateThumbnailAtIndex

Image I/O框架很强大,但是相对比较少人了解。它独立于Core Graphics,可以在许多不同格式间读和写,获取照片元数据,执行一些常见的图片处理操作。这个框架提供了平台上最快的图片编码和解码速度,还提供高级的缓存机制,甚至能够增量地加载图片。
相比于同等效果的Core Graphics代码,CGImageSourceCreateThumbnailAtIndex提供了不同选项的简洁API

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

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

let scaledImage = UIImage(CGImage: CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options))
}

指定一个CGImageSource并设置选项,CGImageSourceCreateThumbnaiAtIndex就能创建了一个缩略图。尺寸调整由kCGImageSourceThumbnailMaxPixelSize决定。通过除以一个常量来以原图等比例指定一个最大的尺寸。通过指定kCGImageSourceCreateThumbnailFromImageIfAbsent或者kCGImageSourceCreateThumbnailFromImageAlways,Image I/O 能够自动为随后的调用缓存缩放结果。

用Core Image兰索斯重取样(Lanczos Resampling)

Core Image提供了一个内建的通过CILanczosScaleTranform实现的Lanczos Resampling功能。尽管是一个比UIKit更高级的API,但是Core Image中普遍使用的键值对编程使得他很笨重。
也就是说,至少模式是和其他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: nil)
let scaledImage = UIImage(CGImage: self.context.createCGImage(outputImage, fromRect: outputImage.extent()))

CILanczosScaleTransform接受一个inputImage, inputScale和inputAspectRatio,从变量名字上就能知道他们是干什么的。CIContext用来通过一个CGImageRef来生成UIImage,因为UIImage(CIImage:)经常不能按照期望的那样。

性能衡量

那么这些不同方法的性能如何呢?

以下是在一个运行iOS 8 GM版本的iPod Touch 5上,使用XCTestCase.measureBlock()得到了性能衡量结果:

JPEG

从NASA Visible Earth拿到一个12000*12000像素的图片,大约20MB,将其缩小到大约十分之一的大小。

Operation Time(sec) σ
UIKit 0.002 22%
Core Graphics1 0.006 9%
Image I/O2 0.001 121%
Core Image3, 4 0.011 7%

PNG

缩放Postgres.app图标至十分之一的大小,1024*1024像素,大约1MB大小。

Operation Time (sec) σ
UIKit 0.001 25%
Core Graphics5 0.005 12%
Image I/O6 0.001 82%
Core Image 0.234 43%
  • 1, 5使用了不同的CGInterpolationQuality,对于性能的影响几乎可以忽略不计。
  • 2相比同等Core Graphics函数的性能,一定程度的高度偏离的结果说明缓存缩略图的代价不菲。
  • 3创建一个CIContext是一个异常消耗资源的操作。在测试中使用的时间最多。使用一个缓存了的实例会减少平均运行时时间至UIGraphicsBeginImageContextWithOptions的水准。
  • 4, 7创建CIContext将参数kCIContextUseSoftwareRenderer设置为true使得相比于其他结果慢了许多。

结论

  • UIkit, Core Graphics 和 Image I/O 对大多数图片的缩放表现都良好
  • Core Image 在图片缩放的表现上并不佳。事实上,在官方的Core Image Programming Guide中的性能最佳实践这部分特别推荐使用Core Graphics或者Image I/O函数来裁切或事先降低取样图片。
  • 对于大部分的缩放图片,UIGraphicsBeginImageContextWithOptions应该是最佳选择。
  • 如果图片质量在考量范围之内,考虑使用CGBitmapContextCreate结合使用CGContextSetInterpolationQuality。
  • 当想缩放图片以显示缩略图的时候,CGImageSourceCreateThumbnailAtIndex提供了一个渲染和缓存的一站式解决方案

NSmutableHipster

在这里我想提醒大家,NSHipster的文章已经发布在了GitHub上,如果你想更正文章的错误或者有别的高见,请开启一个issue或是提交一个pull request。