图像处理相关(二) —— HEIC图像压缩(一)

版本记录

版本号 时间
V1.0 2019.10.20 星期日

前言

App中很多时候都需要进行图像处理,包括各种缩放、滤镜和压缩等处理,好的图像处理不仅可以提高App的性能,也会给用户带来耳目一新的感觉,这里重新开了一个专题,专门讲述对图像的各种处理。感兴趣的可以看下面几篇文章。
1. 图像处理相关(一) —— 图像深度相关处理简单示例(一)

开始

首先看下主要内容:

主要内容:在此HEIC图像压缩教程中,您将学习如何将图像转换为HEICJPEG格式,并比较它们的效率以获得最佳性能。这里是翻译原文地址

接着看下写作环境

Swift 5, iOS 13, Xcode 11

在当今的现代世界中,照片和视频通常会占用移动设备的大部分磁盘空间。 由于Apple继续在iPhone的摄像头上投入时间和金钱,因此使用iOS设备的人们将继续保持这种情况。 高质量的照片意味着更大的图像数据。 4K摄像机占用这么多空间是有原因的!

要存储大量图像数据,通过增加硬件存储大小,您可以做很多事情。 为了帮助最小化数据占用空间,发明了各种数据压缩算法。 数据压缩算法很多,没有一种适合所有解决方案的算法。 对于图像,Apple已采用HEIC图像压缩。 您将在本教程中了解有关此压缩的所有信息。

1. Formatting and HEIC Image Compression

JPEG一词通常用于描述图片的文件类型。尽管文件的扩展名.jpg.jpeg可能会引起误解,但JPEG实际上是一种压缩格式。通过JPEG压缩创建的最常见文件类型为JFIFEXIFF

HEIF(High Efficiency Image File Format)是一种新的图像文件格式,在许多方面都比其JPEG更好。该格式由MPEG在2013年开发,声称保存的数据量是JPEG的两倍,并支持多种类型的图像数据,包括:

  • Items
  • Sequences
  • Derivations
  • Metadata
  • Auxiliary image items

这些数据类型使HEIFJPEG可以存储的单个图像的数据更具灵活性。这使得非常实用的用例(例如存储图像的编辑)非常有效。您还可以存储最新iPhone上记录的图像深度数据。

MPEG规范中定义了一些文件扩展名。对于他们的HEIF文件,Apple决定使用.heic扩展名,即High Image Image Container。他们的选择表明使用了HEVC编解码器,但是Apple的设备也可以读取其他一些编解码器压缩的文件。

首先,打开已经下载的项目。

该项目是一个简单的示例应用程序,显示两个图像视图以及一个用于调整JPEGHEIC图像压缩级别的滑块。 在每个图像视图旁边是几个标签,用于显示有关所选图像的信息,这些标签目前全部都不起任何作用。

该应用程序的目的是通过显示图像压缩所需的时间以及HEIC文件的大小来显示使用HEICJPEG的优点。 它还显示了如何使用share sheet共享HEIC文件。


Saving As HEIC

在启动程序项目打开的情况下,构建并运行以查看实际应用程序的用户界面。

在开始压缩图像之前,您需要能够选择图像。 杰里米·托马斯Jeremy ThomasUnsplash上使用的默认图片效果不错,但最好看看它在您自己的内容上的效果。

MainViewController.swift内部,将以下内容添加到文件的底部:

extension MainViewController: UIImagePickerControllerDelegate, 
                              UINavigationControllerDelegate {
  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    // 1
    picker.dismiss(animated: true)
  }
  
  func imagePickerController(
    _ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo
    info: [UIImagePickerController.InfoKey : Any]
    ) {
    picker.dismiss(animated: true)
    
    // 2
    guard let image = info[.originalImage] as? UIImage else {
      return
    }
    
    // 3
    originalImage = image
    updateImages()
  }
  
}

这是UIImagePickerControllerDelegate的简单实现。 在这里:

  • 1) 当按下取消按钮时,关闭选择器。
  • 2) 从选择器中获取原始图像,以在此应用程序中获得最佳效果。
  • 3) 存储此图像并更新图像视图。

目前,updateImages()不执行任何操作。 接下来,将这些行添加到空的addButtonPressed()中:

let picker = UIImagePickerController()
picker.delegate = self

present(picker, animated: true)

这提供了一个图像选择器,使用户有机会选择自己的图像。 但是,您仍然需要更新图像视图以使此工作正常。

用以下内容替换compressJPGImage(with :)compressHEICImage(with :)的空实现:

private func compressJPGImage(with quality: CGFloat) {
  jpgImageView.image = originalImage
}

private func compressHEICImage(with quality: CGFloat) {
  heicImageView.image = originalImage
}

现在,两个图像视图都将显示选定的图像。 选定的图像是临时的,但会验证图像选择器是否正常工作。

现在,构建并运行该应用程序。 选择一个图像以查看它是否同时出现在两个图像视图中

选择图像后,您可以继续使用压缩滑块。 它什么也没做,但是最终它可以改变每种类型的压缩强度。

在模拟器上压缩图像比在设备上压缩图像慢得多。 要解决此问题,您需要一个条件来确定如何读取滑块的值。

首先在originalImage上方的MainViewController.swift顶部添加以下内容:

private var previousQuality: Float = 0

此属性将存储最后一个滑块值,您稍后将使用该值来根据滑块的值限制更新次数。

接下来,在MainViewController.swiftActions部分的末尾添加以下两个方法:

@objc private func sliderEndedTouch() {
  updateImages()
}

@objc private func sliderDidChange() {
  let diff = abs(compressionSlider.value - previousQuality)
  
  guard diff > 0.1 else {
    return
  }
  
  previousQuality = compressionSlider.value
  
  updateImages()
}

这两种方法都会更新屏幕上的图像。 唯一的区别是,底部方法根据滑块值的明显变化来限制更新次数。

viewDidLoad()的底部添加以下内容:

#if targetEnvironment(simulator)
compressionSlider.addTarget(
  self,
  action: #selector(sliderEndedTouch),
  for: [.touchUpInside, .touchUpOutside]
)
#else
compressionSlider.addTarget(
  self,
  action: #selector(sliderDidChange),
  for: .valueChanged
)
#endif

这会根据当前开发环境将目标操作注册到滑块,从而提高了在没有物理设备的情况下开发应用程序的质量。 这种设置很有用,因为虽然模拟器可以加快开发速度,但有时性能并不理想。

有了这些功能,终于可以开始压缩这些图像了。

MainViewController.swift的顶部添加以下属性:

private let compressionQueue = OperationQueue()

操作队列(operation queue)是减轻繁重工作负担的一种方法,可确保应用程序的其余部分响应。 使用队列还提供了取消任何活动的压缩任务的能力。 对于此示例,在开始新任务之前取消当前任务是有意义的。

updateImages()内对resetLabels()的调用之后,添加以下行:

compressionQueue.cancelAllOperations()

在添加新任务之前,这将取消当前队列中的所有操作。 如果没有此步骤,您可能会在视图中设置错误的压缩质量的图像。

接下来,将compressJPGImage(with :)的内容替换为以下内容:

// 1
jpgImageView.image = nil
jpgActivityIndicator.startAnimating()

// 2
compressionQueue.addOperation {
  // 3
  guard let data = self.originalImage.jpegData(compressionQuality: quality) else {
    return
  }
  
  // 4
  DispatchQueue.main.async {
    self.jpgImageView.image = UIImage(data: data)
    // TODO: Add image size here...
    // TODO: Add compression time here...
    // TODO: Disable share button here...
    
    UIView.animate(withDuration: 0.3) {
      self.jpgActivityIndicator.stopAnimating()
    }
  }
}

使用上面的代码,您:

  • 1) 删除旧图像并启动活动指示器(activity indicator)
  • 2) 将压缩任务添加到已定义的操作队列中。
  • 3) 使用质量参数压缩原始图像并将其转换为Data
  • 4) 从压缩数据创建UIImage并更新主线程上的图像视图。 请记住,UI操作应始终在主线程上进行。 您即将在此方法中添加更多代码。

就是使用JPEG编解码器压缩图像。 要添加HEIC图像压缩,请将compressHEICImage(with :)的内容替换为:

heicImageView.image = nil
heicActivityIndicator.startAnimating()

compressionQueue.addOperation {
  do {
    let data = try self.originalImage.heicData(compressionQuality: quality)
    
    DispatchQueue.main.async {
      self.heicImageView.image = UIImage(data: data)
      // TODO: Add image size here...
      // TODO: Add compression time here...
      // TODO: Disable share button here...
      
      UIView.animate(withDuration: 0.3) {
        self.heicActivityIndicator.stopAnimating()
      }
    }
  } catch {
    print("Error creating HEIC data: \(error.localizedDescription)")
  }
}

HEIC图像压缩方法只有一个区别。 图像数据在UIImage + Additions.swift中的帮助器方法中压缩,该方法目前为空。

打开UIImage + Additions.swift,您会发现heicData(compressionQuality :)的空实现。 在添加方法的内容之前,您需要自定义错误类型。

在扩展的顶部添加以下内容:

enum HEICError: Error {
  case heicNotSupported
  case cgImageMissing
  case couldNotFinalize
}

Error枚举包含几种情况,以说明使用HEIC压缩图像时可能出错的情况。 并非所有的iOS设备都可以捕获HEIC内容,但是大多数运行iOS 11或更高版本的设备都可以读取和编辑此内容。

heicData(compressionQuality :)的内容替换为:

// 1
let data = NSMutableData()
guard let imageDestination =
  CGImageDestinationCreateWithData(
    data, AVFileType.heic as CFString, 1, nil
  )
  else {
    throw HEICError.heicNotSupported
}

// 2
guard let cgImage = self.cgImage else {
  throw HEICError.cgImageMissing
}

// 3
let options: NSDictionary = [
  kCGImageDestinationLossyCompressionQuality: compressionQuality
]

// 4
CGImageDestinationAddImage(imageDestination, cgImage, options)
guard CGImageDestinationFinalize(imageDestination) else {
  throw HEICError.couldNotFinalize
}

return data as Data

是时候分解一下:

  • 首先,您需要一个空的数据缓冲区。此外,您可以使用CGImageDestinationCreateWithData(_:_:_:_ :)HEIC编码的内容创建目标。此方法是Image I / O框架的一部分,用作一种容器,可以在添加图像数据之前添加图像数据并更新其属性。如果这里有问题,则设备上没有HEIC
  • 您需要确保有要处理的图像数据。
  • 使用键kCGImageDestinationLossyCompressionQuality来应用传递给该方法的参数。您正在使用NSDictionary类型,因为CoreGraphics需要它。
    最后,将图像数据和选项一起应用到目标。
  • CGImageDestinationFinalize(_ :)完成HEIC图像压缩,如果成功,则返回true

Build并运行。现在您应该看到图像将根据滑块的值而改变。底部的图像应该花更长的时间显示,因为HEIC图像压缩涉及更多的压缩过程,因为它可以节省更多磁盘空间。


Measuring Time

现在,您可能会认为整个HEIC事情并不十分令人印象深刻。 目前唯一清楚的是使用HEIC压缩图像的速度很慢。 好吧,接下来您将看到HEIC文件小了多少。

该项目中包含一个名为Data + Additions.swift的帮助文件,该文件包含一个计算属性,可以漂亮地打印pretty-prints Data对象的大小。 此属性使用Foundation框架的便捷ByteCountFormatter格式化字节大小。

MainViewController.swift中,替换TODO: Add image size here…compressJPGImage(with :)内为:

self.jpgSizeLabel.text = data.prettySize

JPEG方法一样,替换TODO: Add image size here…compressHEICImage(with :)内为:

self.heicSizeLabel.text = data.prettySize

这将更新size label以反映每张图像的尺寸。

Build并运行。 您应该立即看到使用HEIC可以节省多少空间,这将非常有用。

HEICJPEG之间进行选择时,要考虑的最后一个元素是时间。 压缩所需的时间是要考虑的关键数据。 如果您的应用程序需要速度大于空间,那么HEIC可能不是您的最佳选择。

MainViewController.swift的顶部添加以下内容:

private let numberFormatter = NumberFormatter()

格式化程序有助于使数字更具可读性。 此格式化程序将使读取精确的时间间隔更加容易。

viewDidLoad()的底部,在updateImages()之前,添加以下代码:

numberFormatter.maximumSignificantDigits = 1
numberFormatter.maximumFractionDigits = 3

这将格式化程序配置为限制其输出,因为两种压缩方法之间的差异非常明显。 如果两者更接近,那么更高的精确度将是有益的。

resetLabels()之后添加以下方法:

private func elapsedTime(from startDate: Date) -> String? {
  let endDate = Date()
  let interval = endDate.timeIntervalSince(startDate)
  let intervalNumber = NSNumber(value: interval)
  
  return numberFormatter.string(from: intervalNumber)
}

此方法使用您先前声明的格式化程序。 它输入一个开始日期,然后根据该日期计算持续时间,并使用数字格式化程序返回一个可选字符串。

compressJPGImage(with :)内部,将其添加到方法的顶部:

let startDate = Date()

接下来,替换TODO: Add compression time here…compressJPGImage(with:)内部:

if let time = self.elapsedTime(from: startDate) {
  self.jpgTimeLabel.text = "\(time) s"
}

立即记录开始日期,确保方法的所有部分都对计算的持续时间有所贡献。 然后,一旦在主队列上解码完成,便会设置时间标签。

与以前一样,您需要为HEIC图像压缩方法添加随附的逻辑。 将此添加到compressHEICImage(with :)的顶部:

let startDate = Date()

并替换TODO: Add compression time here…,其内容如下:

if let time = self.elapsedTime(from: startDate) {
  self.heicTimeLabel.text = "\(time) s"
}

键入或复制类似代码时,请确保设置正确的标签。 注意,此heicTimeLabel是在HEIC方法中设置的,而jpgTimeLabel是在JPG方法中设置的。

Build并运行

现在,您可以做出完全明智的决定。 JPG压缩非常快,但需要较大的图像。 相反,HEIC图像较小,但压缩速度较慢。

了解用户的设备是一件好事。 由于HEIC需要更长的时间,因此当电池电量不足时,您可能会节省空间。 您还可以检查设备的可用存储空间。 如果磁盘已满75%,则始终选择HEIC图像压缩。


Sharing HEIC

最后要考虑的一点是共享HEIC图像。 JPEG压缩算法多年来一直是Web上的标准,但是HEIC必须提供的灵活性和节省空间令人印象深刻。

许多设计良好的网站已经使用JPEG压缩其内容。 如果网站的文件已经很小,那么节省50%并不是很诱人。 但是节省一半高质量iPhone照片的空间更有意义。

虽然可能不是时候在网上全部使用HEIC,但某些网站提供了上传HEIC照片的支持。 在Apple生态系统内,其他应用在处理HEIC内容时应该没有问题。 共享内容时,最好选择共享的格式。

updateImages()下面添加以下方法:

private func shareImage(_ image: UIImage) {
  let avc = UIActivityViewController(
    activityItems: [image],
    applicationActivities: nil
  )
  present(avc, animated: true)
}

此方法共享以两种格式压缩的图像。 UIImage类负责处理其基础格式,因此您不必这样做。

要使用此功能,请在MainViewController.swift中将以下内容添加到shareButtonPressed()

let avc = UIAlertController(
  title: "Share",
  message: "How would you like to share?",
  preferredStyle: .alert
)

if let jpgImage = jpgImageView.image {
  avc.addAction(UIAlertAction(title: "JPG", style: .default) { _ in
    self.shareImage(jpgImage)
  })
}

if let heicImage = heicImageView.image {
  avc.addAction(UIAlertAction(title: "HEIC", style: .default) { _ in
    self.shareImage(heicImage)
  })
}

avc.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

present(avc, animated: true)

这设置了此应用程序的简单共享功能,因为图像视图中已经有两个压缩图像。 对于将所有图像存储为HEIC的真实应用,您需要在共享之前将图像转换为JPEG

要添加到示例应用程序中的最后一项功能是在没有可用图像时阻止共享。 对于较大的文件,HEIC图像压缩可能会失败或需要更长的时间。 在此期间,最好禁用共享按钮,以免造成混淆并维护良好的用户体验。

updateImages()内部添加:

navigationItem.leftBarButtonItem?.isEnabled = false

此行在开始压缩之前禁用共享按钮。

接下来,将每次出现的TODO: Disable share button here…替换为:

self.navigationItem.leftBarButtonItem?.isEnabled = true

每次压缩完成后,重新启用共享按钮。 如果HEIC图片压缩仍在处理中,则您只会在alert中看到JPG选项。 对于您的应用,等待两个选项均可用可能更有意义。

构建并运行。

恭喜,示例应用已完成!

您现在应该对HEIC,它的优点和缺点有一定的了解。 HEIC有很多用例,随着时间的推移,它只会越来越受欢迎。

回顾HEIC的好处:

  • JPEG相比,文件大小小50%
  • 包含许多图像项。
  • 图像派生,非破坏性编辑。
  • 图像序列,例如实时照片(Live Photos)
  • 用于存储深度或HDR数据的辅助图像项目。
  • 图像元数据,例如位置或相机信息。

有关Apple的HEIC的更多信息,请查看2017年关于 HEIF and HEVC的WWDC会议。

后记

本篇主要讲述了HEIC图像压缩,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容