iOS音视频高级分享—图像调整技术

自远古以来,iOS开发人员一直困惑于一个单一的问题:

“你如何调整图像大小?”

由于对开发人员和平台的相互不信任,这是一个迷惑清晰度的问题。无数的代码样本垃圾堆栈溢出,每个声称是一个真正的解决方案™ - 所有其他,仅仅是伪装者。

在本文章中,我们将介绍5种不同的技术,用于在iOS上调整图像大小(和macOS,进行适当的UIImage→ NSImage转换)。但是,我们不会针对每种情况规定单一方法,而是将人体工程学与性能基准进行权衡,以便更好地了解何时使用一种方法而不是另一种方法。

您可以通过下载,构建和运行此示例代码项目来自行尝试这些图像大小调整技术 。

点击此处进交流群 有技术的来闲聊 没技术的来学习


何时以及为何缩放图像

在我们走得太远之前,让我们先确定为什么你需要调整图像大小。毕竟, 根据[属性指定的行为自动缩放和裁剪图像 。而在绝大多数情况下, ,,或 正是为您提供需要的行为。UIImageView content .scaleAspectFit .scaleAspect .scaleToFill

imageView.contentMode = .scaleAspectFit
imageView.image = image


那么什么时候调整图像大小是有意义的?
当它明显大于显示它的图像视图。


从美国国家航空航天局的可见地球图像目录中考虑这个令人惊叹的地球图像:国家航空和航天局

在其全分辨率下,该图像尺寸为12,000像素,重达20 MB。考虑到今天的硬件,你可能不会想到几兆字节,但这只是它的压缩尺寸。要显示它,需要首先将JPEG解码为位图。如果您按原样在图像视图上设置此全尺寸图像,则应用程序的内存使用量将增加到 数百兆字节的内存 ,对用户没有任何明显的好处(毕竟,屏幕只能显示如此多的像素) 。UIImageView

通过在设置其image属性之前简单地将该图像调整为图像视图的大小,您可以使用一个数量级更少的RAM:

内存使用量*(MB)*
没有下采样220.2
随着下采样23.7

这种技术称为下采样,可以在这种情况下显着提高应用程序的性能。如果您对有关下采样和其他图像和图形最佳实践的更多信息感兴趣,请参阅 WWDC 2018的这个优秀会议

现在,一些应用程序会不断尝试加载图像这个大…但它不是从一些我已经得到了从设计师回资产为期不远。 (说真的,一个3MB的PNG用于颜色渐变?) 所以考虑到这一点,让我们来看看你可以采用的各种方法来调整图像的大小和下采样。

这应该不言而喻,但是从URL加载图像的所有示例都是针对本地文件的。请记住,在应用程序的主线程上同步进行网络连接永远不是一个好主意。


图像调整技术

调整图像大小有许多不同的方法,每种方法都具有不同的功能和性能特征。我们在本文中看到的示例涵盖了低级和高级的框架,从Core Graphics,vImage和Image I / O到Core Image和UIKit:

  1. 绘制到UIGraphicsImageRenderer
  2. 绘制到核心图形上下文
  3. 使用图像I / O创建缩略图
  4. Lanczos重新采样核心图像
  5. 使用vImage进行图像缩放

为保持一致性,以下每种技术都共享一个通用接口:

func resizedImage(at url: URL, for size: CGSize) -> UIImage? { ... }

imageView.image = resizedImage(at: url, for: size)

这里size是点大小的度量,而不是像素大小。为了计算等效像素尺寸为您的尺寸调整的图像,由缩放图像视图帧的尺寸scale的主要的UIScreen

let scaleFactor = UIScreen.main.scale
let scale = CGAffineTransform(scaleX: scaleFactor, y: scaleFactor)
let size = imageView.bounds.size.applying(scale)

如果要异步加载大图像,请在图像视图上设置时使用转换使图像淡入。例如:

class ViewController: UIViewController {
    @IBOutlet var imageView: UIImageView!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let url = Bundle.main.url(forResource: "Blue Marble West",
                                withExtension: "tiff")!

        DispatchQueue.global(qos: .userInitiated).async {
            let image = resizedImage(at: url, for: self.imageView.bounds.size)

            DispatchQueue.main.sync {
                UIView.transition(with: self.imageView,
                                duration: 1.0,
                                options: [.curveEaseOut, .transitionCrossDissolve],
                                animations: {
                                    self.imageView.image = image
                                })
            }
        }
    }
}


技巧1:绘制到UIGraphicsImageRenderer

用于图像大小调整的最高级API可在UIKit框架中找到。给定a UIImage,您可以绘制上下文以呈现该图像的缩小版本:UIGraphicsImageRenderer

import UIKit

// Technique #1
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
    guard let image = UIImage(contentsOfFile: url.path) else {
        return nil
    }

    let renderer = UIGraphicsImageRenderer(size: size)
    return renderer.image { (context) in
        image.draw(in: CGRect(origin: .zero, size: size))
    }
}

UIGraphicsImageRenderer 是一个相对较新的API,在iOS 10中引入以替换旧的 / API。您可以通过指定一个点来构造一个。该方法接受一个闭包参数,并返回一个由执行传递的闭包产生的位图。在这种情况下,结果是缩小原始图像以在指定范围内绘制。UIGraphicsBeginImageContextWithOptions UIGraphicsEndImageContext UIGraphicsImageRenderer size image

在不改变原始宽高比的情况下缩放原始大小以适合帧内通常很有用。 是AVFoundation框架中的一个方便的函数,它为您负责计算:AVMakeRect(aspectRatio:insideRect:)

import func AVFoundation.AVMakeRect
let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)

技术2:绘制到核心图形上下文

Core Graphics / Quartz 2D提供了一组较低级别的API,允许更高级的配置。

给定a CGImage,使用临时位图上下文来渲染缩放图像,使用以下draw(_:in:)方法:

import UIKit
import CoreGraphics

// Technique #2
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
    else {
        return nil
    }

    let context = CGContext(data: nil,
                            width: Int(size.width),
                            height: Int(size.height),
                            bitsPerComponent: image.bitsPerComponent,
                            bytesPerRow: image.bytesPerRow,
                            space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
                            bitmapInfo: image.bitmapInfo.rawValue)
    context?.interpolationQuality = .high
    context?.draw(image, in: CGRect(origin: .zero, size: size))

    guard let scaledImage = context?.makeImage() else { return nil }

    return UIImage(cgImage: scaledImage)
}

CGContext初始化程序需要多个参数来构造上下文,包括所需的维度和给定颜色空间内每个通道的内存量。在此示例中,从CGImage对象获取这些参数。接下来,设置属性以 指示上下文以f保真度级别插值像素。该方法以给定的大小和位置绘制图像,允许在特定边缘上裁剪图像或者适合一组图像特征,例如面部。最后,该方法从上下文中捕获信息并将其呈现为一个值(然后用于构造对象)。interpolationQuality .high draw(_:in:) makeImage() CGImage UIImage

技巧3:使用图像I / O创建缩略图

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

重要的 提供了一个简洁的API,其中包含的选项与等效的Core Graphics调用中的选项不同:CGImageSourceCreateThumbnailAtIndex

import ImageIO

// Technique #3
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
    let options: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
    ]

    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary)
    else {
        return nil
    }

    return UIImage(cgImage: image)
}

给定一组和一组选项,该函数创建图像的缩略图。调整大小由选项完成,该选项指定用于以原始宽高比缩放图像的最大尺寸。通过设置 或 选项,Image I / O会自动缓存后续调用的缩放结果。CGImageSource CGImageSourceCreateThumbnailAtIndex(_:_:_:) kCGImageSourceThumbnailMaxPixelSize kCGImageSourceCreateThumbnailFromImageIfAbsent kCGImageSourceCreateThumbnailFromImageAlways

技术4:Lanczos重新采样核心图像

Core Image 通过同名过滤器提供内置的Lanczos重采样功能。尽管可以说是比UIKit更高级的API,但在Core Image中普遍使用键值编码使其变得难以处理。CILanczosScaleTransform

也就是说,至少这种模式是一致的。

创建变换过滤器,配置变换过滤器和渲染输出图像的过程与任何其他Core Image工作流程没有区别:

import UIKit
import CoreImage

let sharedContext = CIContext(options: [.useSoftwareRenderer : false])

// Technique #4
func resizedImage(at url: URL, scale: CGFloat, aspectRatio: CGFloat) -> UIImage? {
    guard let image = CIImage(contentsOf: url) else {
        return nil
    }

    let filter = CIFilter(name: "CILanczosScaleTransform")
    filter?.setValue(image, forKey: kCIInputImageKey)
    filter?.setValue(scale, forKey: kCIInputScaleKey)
    filter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)

    guard let outputCIImage = filter?.outputImage,
        let outputCGImage = sharedContext.createCGImage(outputCIImage,
                                                        from: outputCIImage.extent)
    else {
        return nil
    }

    return UIImage(cgImage: outputCGImage)
}

名为的Core Image过滤器 接受一个,一个和一个参数,每个参数都是不言自明的。CILanczosScaleTransform inputImage inputScale inputAspectRatio

更有趣的CIContext是,这里使用a来创建UIImage (通过中间表示),因为通常不会按预期工作。创建a 是一项昂贵的操作,因此缓存的上下文用于重复调整大小。CGImageRef UIImage(CIImage:) CIContext

CIContext可以使用GPU或CPU(慢得多)创建A 进行渲染。在初始化程序中指定选项以选择要使用的选项。(提示:使用更快的一个,也许?).useSoftwareRenderer

技术5:使用vImage进行图像缩放

最后,它是令人尊敬的Accelerate框架- 或者更具体地说,是图像处理子框架。vImage

vImage附带了一系列 用于缩放图像缓冲区的不同功能。这些低级API承诺高性能和低功耗,但是以自己管理缓冲区为代价(更不用说,显着地编写更多代码):

import UIKit
import Accelerate.vImage

// Technique #5
func resizedImage(at url: URL, for size: CGSize) -> UIImage? {
    // Decode the source image
    guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil),
        let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
        let imageWidth = properties[kCGImagePropertyPixelWidth] as? vImagePixelCount,
        let imageHeight = properties[kCGImagePropertyPixelHeight] as? vImagePixelCount
    else {
        return nil
    }

    // Define the image format
    var format = vImage_CGImageFormat(bitsPerComponent: 8,
                                      bitsPerPixel: 32,
                                      colorSpace: nil,
                                      bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.first.rawValue),
                                      version: 0,
                                      decode: nil,
                                      renderingIntent: .defaultIntent)

    var error: vImage_Error

    // Create and initialize the source buffer
    var sourceBuffer = vImage_Buffer()
    defer { sourceBuffer.data.deallocate() }
    error = vImageBuffer_InitWithCGImage(&sourceBuffer,
                                         &format,
                                         nil,
                                         image,
                                         vImage_Flags(kvImageNoFlags))
    guard error == kvImageNoError else { return nil }

    // Create and initialize the destination buffer
    var destinationBuffer = vImage_Buffer()
    error = vImageBuffer_Init(&destinationBuffer,
                              vImagePixelCount(size.height),
                              vImagePixelCount(size.width),
                              format.bitsPerPixel,
                              vImage_Flags(kvImageNoFlags))
    guard error == kvImageNoError else { return nil }

    // Scale the image
    error = vImageScale_ARGB8888(&sourceBuffer,
                                 &destinationBuffer,
                                 nil,
                                 vImage_Flags(kvImageHighQualityResampling))
    guard error == kvImageNoError else { return nil }

    // Create a CGImage from the destination buffer
    guard let resizedImage =
        vImageCreateCGImageFromBuffer(&destinationBuffer,
                                      &format,
                                      nil,
                                      nil,
                                      vImage_Flags(kvImageNoAllocate),
                                      &error)?.takeRetainedValue(),
        error == kvImageNoError
    else {
        return nil
    }

    return UIImage(cgImage: resizedImage)
}

这里使用的Accelerate API显然比目前讨论的任何其他调整大小方法都要低得多。但是,通过看起来不友好的类型和函数名称,你会发现这种方法相当简单。

  • 首先,从输入图像创建源缓冲区,
  • 然后,创建目标缓冲区以保存缩放图像
  • 接下来,将源缓冲区中的图像数据缩放到目标缓冲区,
  • 最后,从目标缓冲区中生成的图像数据创建图像。

绩效基准

那么这些不同的方法如何相互叠加?

以下是 在此项目中运行iOS 12.2的iPhone 7上执行的一些性能基准测试的结果。


以下数字显示了多次迭代的平均运行时间,用于 从以前加载,缩放和显示 地球的巨型大小图片

时间*(秒)*
技术1: UIKit0.1420
技术2:Core Graphics 10.1722
技术3: Image I/O0.1616
技术4:Core Image 22.4983
技术5: v<wbr style="box-sizing: border-box;">Image2.3126

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

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

结论

  • UIKitCore GraphicsImage I / O 在大多数图像上都可以很好地进行缩放操作。如果您必须选择一个(至少在iOS上), 通常是您最好的选择。UIGraphicsImageRenderer
  • Core Image的性能优于图像缩放操作。实际上,根据*“核心图像编程指南”的* Apple“性能最佳实践”部分,您应该使用Core Graphics或Image I / O函数来裁剪和下采样图像而不是Core Image。
  • 除非您已经在使用,否则在大多数情况下使用低级Accelerate API所需的额外工作可能是不合理的。vImage

小编这里有大量的书籍和面试资料哦(点击下载

原文转载地址:https://nshipster.com/image-resizing/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值