Accelerate框架详细解析(二) —— 基于Accelerate 和 vImage的SwiftUI程序图像处理(一)

版本记录

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

前言

Accelerate框架进行大规模的数学计算和图像计算,针对高性能进行优化。接下来几篇我们就一起看一下这个框架。感兴趣的可以看上面几篇。
1. Accelerate框架详细解析(一) —— 基本概览(一)

开始

首先看下主要内容:

了解如何在SwiftUI应用程序中使用AcceleratevImage处理图像。内容来自翻译。

下面看下写作环境:

Swift 5, iOS 14, Xcode 12

现代计算最有价值的好处可能是能够快速,准确地进行复杂的数学运算。早期的计算机几乎专门用于自动化以前手工完成的繁琐且容易出错的计算。

如今,一部电话可以在一秒钟的时间内完成计算,相当于曾经花费一个团队几个星期甚至几个月的时间的工作量。这种增强的功能要求程序员向这些设备添加更复杂的计算,从而增加了电话的实用性。

Accelerate框架为应用程序开发人员提供了一个高效的高速库,可进行大规模数学或基于图像的计算。它使用现代专用CPU上的矢量处理功能来快速执行计算,同时保持高效的能源使用。

您需要首先了解什么是Accelerate以及将在本教程中使用的组件。因此,在深入研究代码之前,请看一下Accelerate的组件。

Accelerate包含几个相关的库,每个库都执行一种复杂的数学过程类型。这些库是:

  • BNNS:用于训练和运行神经网络
  • vImage:图像处理库
  • vDSP:数字信号处理功能库
  • vForce:对大量数字执行arithmetic and transcendental计算
  • Sparse Solvers, BLAS and LAPACK:用于线性代数计算。

苹果还将这些库用作其他框架的构建块。例如,CoreML建立在BNNS之上。archive and compression框架也建立在Accelerate的基础上。由于Apple广泛使用这些框架,因此您还将在所有当前的Apple平台上获得支持。

在本教程中,您将使用vImage库探索Accelerate框架。所有库的工作方式相似,vImage库提供了清晰的视觉示例,比诸如数字信号处理之类的更复杂的任务更容易理解。


Introducing vImage and vImage_Buffer

vImage为您提供了一种使用CPU的矢量指令集来处理大型图像的方法。与使用通用指令相比,这些指令使您可以编写可快速执行复杂图像计算的应用,同时减轻移动设备电池的压力。当您需要执行非常复杂的计算或处理实时视频,或者需要高精度时,vImage可以很好地工作。

Accelerate在较旧的Apple框架中有点独特,因为它已部分更新为Swift兼容性。与许多较早的框架不同,您会发现可以正常使用的功能像原生Swift库一样。但是您不能忽略该框架的较早起源,因为许多调用仍然期望并使用Swift之前的习惯用法和模式。在本教程的后面,您将看到如何管理它们。

要用vImage处理图像,必须首先将其转换为vImage的原生格式- vImage_Buffer。该缓冲区表示原始图像数据,vImage函数将其更多地视为一组数字而不是图像数据,与Accelerate的矢量处理范例保持一致。


Creating vImage Buffers

是时候开始编码了!

打开入门项目。打开启动程序项目,然后构建并运行。

Accelerate框架详细解析(二) —— 基于Accelerate 和 vImage的SwiftUI程序图像处理(一)_第1张图片

您会看到一个应用程序,可让您从相机胶卷中选择一张照片。 然后,它显示所选的图像。 在UIKitSwiftUI中,通常使用UIImage。 由于vImage无法识别这种格式,因此您首先需要将该UIImage转换为可以使用的格式。

创建一个名为VImageWrapper.swift的新Swift文件。 将文件内容替换为:

import UIKit
import Accelerate

struct VImageWrapper {
  // 1
  let vNoFlags = vImage_Flags(kvImageNoFlags)
}

extension VImageWrapper {
  // 2
  func printVImageError(error: vImage_Error) {
    let errDescription = vImage.Error(vImageError: error).localizedDescription
    print("vImage Error: \(errDescription)")
  }
}

该文件包含您将在本教程中创建的多个vImage流程的Swift包装器的开始。 首先导入UIKitAccelerate框架以访问UIImage。 其余代码将在以后有用:

  • 1) 大多数Accelerate函数期望使用flags参数来限制或提供该函数的上下文。 对于本教程,您无需提供它,而vNoFlags = vImage_Flags(kvImageNoFlags)为反映这一点的值提供了一个便携的常数。
  • 2) 即使与Swift进行了更好的集成,许多方法仍会返回Objective C样式值来指示该方法的状态。 此方法将返回的vImage_Error值转换为Swift友好的vImage.Error。 然后将描述打印到控制台以进行调试。 在本教程中,您将使用此方法来处理错误。

1. Converting UIImage to vImage

接下来,将以下代码添加到VImageWrapper结构中:

var uiImage: UIImage

init(uiImage: UIImage) {
  self.uiImage = uiImage
}

此代码创建一个UIImage属性以及一个接受UIImage的自定义初始化程序。

接下来,将以下方法添加到结构中。

func createVImage(image: UIImage) -> vImage_Buffer? {
  guard
    // 1
    let cgImage = uiImage.cgImage,
    // 2
    let imageBuffer = try? vImage_Buffer(cgImage: cgImage)
  else {
    // 3
    return nil
  }
  // 4
  return imageBuffer
}

请注意使用guard语句以确保每个步骤都有效。如果任何步骤失败,则else返回nil表示出了点问题。在本教程中,您将经常使用这种逻辑。

  • 1) Accelerate无法提供从UIImagevImage_Buffer的直接转换。它确实支持将CGImage转换为vImage_Buffer。因为UIImage(通常)在cgImage属性中包含CGImage,所以您尝试访问UIImageCGImage
  • 2) 使用CGImage,您尝试从中创建vImage_Buffer。创建此缓冲区可能会引发错误,因此您使用try?运算符,如果发生错误,运算符将得到nil
  • 3) 如果这些步骤中的任何一个失败,那么您将返回nil
  • 4) 否则,您将返回vImage_Buffer

到此结束了将图像转换为vImage可以处理的缓冲区所需的设置。在整个教程中,您将广泛使用它。

您还需要一种从vImage_Buffer转换回UIImage的方法。所以,现在就编写代码。


Converting vImage to UIImage

VImageWrapper结构的末尾添加以下方法:

func convertToUIImage(buffer: vImage_Buffer) -> UIImage? {
  guard
    // 1
    let originalCgImage = uiImage.cgImage,
    // 2
    let format = vImage_CGImageFormat(cgImage: originalCgImage),
    // 3
    let cgImage = try? buffer.createCGImage(format: format)
  else {
    return nil
  }
  let image = UIImage(cgImage: cgImage)
  return image
}

您可以看到,从vImage_Buffer转化为UIImage需要花费更多的工作:

  • 1) 首先,您将获得UIImageCGImage
  • 2) 如前所述,vImage_Buffer仅包含图像数据。 它没有有关缓冲区代表什么的信息,并且它需要知道用于图像数据的位的顺序和位数。 您可以从原始图像中获取此信息,这就是您在这里所做的。 您创建一个vImage_CGImageFormat对象,该对象包含解释原始图像的图像数据所需的信息。
  • 3) 您可以在缓冲区buffer上调用createCGImage(format :),以使用上一步中确定的格式将图像数据转换为图像。

现在,要查看您的工作,请将以下可选属性添加到VImageWrapper结构的属性列表的末尾:

var processedImage: UIImage?

然后,在init(uiImage :)的末尾添加以下代码:

if let buffer = createVImage(image: uiImage), 
  let converted = convertToUIImage(buffer: buffer) {
    processedImage = converted
}

上面的代码块尝试将图像转换为buffer,然后再转换回UIImage

接下来,打开ContentView.swift,并在现有ImageView之后添加以下代码:

if let image = processedImage {
  ImageView(
    title: "Processed",
    image: image)
  .padding(3)
}

当存在经过处理的图像时,应用程序将使用启动程序项目中定义的ImageView将其显示。 您需要再进行一次更改,才能看到自己的工作。

1. Instantiating the Wrapper

sheet(isPresented:onDismiss:content:)内,将print(“ Dismissed”)替换为以下内容:

let imageWrapper = VImageWrapper(uiImage: originalImage)
processedImage = imageWrapper.processedImage

这段代码用选定的图像实例化包装器,然后将对象的processedImage属性分配给视图的processedImage属性。 由于您尚未更改图片,因此您会看到处理后的图片与原始图片匹配。

构建并运行。 点击Select Photo,然后在模拟器中选择红色花朵以外的任何照片。 您会看到一切看起来不错。

但是,正如您可能从该限制中猜到的那样,存在一个问题。 点按Select Photo,然后选择那张红色花朵的照片,您会发现东西有点颠倒了。

处理后的照片看起来颠倒了。具体来说,它已与原图像旋转了180°。是什么导致图像旋转?现在,您将对其进行修复。


Managing Image Orientation

旋转图像的bug来自转换回UIImage这一阶段。请记住,早先vImage_Buffer仅包含图像的原始像素数据。这些像素的含义没有上下文。在转换回UIImage的过程中,您基于原始CGImage创建了vImage_CGImageFormat以提供该格式。那我们错过或者少了什么呢?

事实证明,与CGImage相比,UIImage包含有关图像的更多信息。这些属性之一是imageOrientation。此属性指定图像的预期显示方向。通常,此值为.up。但是对于此图像,它读.down。定义显示器应将图像从其原始像素数据方向旋转180°。

因为在将vImage_Buffer转换回UIImage时未设置此值,所以获得了默认值.up。简单的解决方法:进行转换时使用原始UIImage的信息。

打开VImageWrapper.swift并找到convertToUIImage(buffer :)。将let image = ...行更改为:

let image = UIImage(
  cgImage: cgImage,
  scale: 1.0,
  orientation: uiImage.imageOrientation
)

使用此构造函数,可以在创建图像时指定方向。 您可以使用原始图片的imageOrientation属性中的值。

构建并运行。 点击Select Photo,然后选择红色花朵的照片。 现在,您将看到处理后的图像看起来正确。


Image Processing

现在您可以将图像与vImage所需的格式进行相互转换,现在可以开始使用该库来处理图像了。 您将采用几种不同的方式来处理图像,并尝试使用它们的颜色。

1. Implementing Equalize Histogram

首先,您将实施均衡直方图过程。 此过程将转换图像,使其具有更均匀的直方图。 生成的图像应使每种颜色的强度几乎相等地出现。 结果通常比在视觉上更具吸引力。 但这在大多数图像上都有明显的视觉区别。

首先,打开VImageWrapper.swift。 将以下方法添加到结构的末尾:

// 1
mutating func equalizeHistogram() {
  guard
    // 2
    let image = uiImage.cgImage,
    var imageBuffer = createVImage(image: uiImage),
    // 3
    var destinationBuffer = try? vImage_Buffer(
      width: image.width,
      height: image.height,
      bitsPerPixel: UInt32(image.bitsPerPixel))
    else {
      // 4
      print("Error creating image buffers.")
      processedImage = nil
      return
  }
  // 5
  defer {
    imageBuffer.free()
    destinationBuffer.free()
  }
}

此代码设置图像更改所需的值。转换为缓冲区buffer应该看起来很熟悉,但是有一些新工作,因为您需要放置图像处理结果的位置:

  • 1) 使用struct时,您必须声明该方法为mutating,以便您可以更新processedImage属性。
  • 2) 您将获得UIImageCGImage以及用于原始图像的vImage_Buffer。您将在后面的步骤中看到为什么需要CGImage的原因。
  • 3) 一些处理函数会将结果放回原始缓冲区(original buffer)。大多数期望第二个destination buffer保存结果。使用此构造函数,您可以指定缓冲区的宽度和高度,以及图像中每个像素使用的位数。您可以从第二步中获得的CGImage中获得这些值。
  • 4) 如果这些步骤中的任何一个失败,则该方法将错误打印到控制台,将processedImage属性设置为nil并返回。
  • 5) 创建vImage_Buffer时,该库会动态分配内存。完成对象后,必须通过调用free()让库知道。您可以将此调用包装在defer中,以便在此方法返回时都将调用它。

2. Processing the Image

完成设置后,您现在就可以处理图像了。将以下代码添加到equalizeHistogram()的末尾:

// 1
let error = vImageEqualization_ARGB8888(
  &imageBuffer,
  &destinationBuffer,
  vNoFlags)

// 2
guard error == kvImageNoError else {
  printVImageError(error: error)
  processedImage = nil
  return
}

// 3
processedImage = convertToUIImage(buffer: destinationBuffer)

设置完成后,您可以看到实际的图像处理只需很少的代码:

  • 1) vImageEqualization_ARGB8888(_:_:_ :)采用三个参数。第一个和第二个是您创建的源缓冲区和目标缓冲区(source and destination buffers)。您传递之前定义的vNoFlags常量,因为您对此函数没有特殊说明。这是为您执行实际直方图均衡化的函数。
  • 2) 您检查以查看该函数中是否有error。如果是这样,则将error输出到控制台。之后,清除processedImage属性,然后从方法中返回。提醒一下,感谢defer关键字,现在可以执行free()
  • 3) 现在,您将缓冲区转换回UIImage并将其存储在processedImage属性中。如果该方法在这里完成,并且由于error而没有更早返回,那么现在是由于defer块而在缓冲区上调用free()的时候了。

请注意vImage上的_ARGB8888后缀。因为缓冲区仅指定没有上下文的图像数据,所以大多数vImage都具有多个后缀,这些后缀会将数据视为指示的格式。

此格式指定图像数据将针对每个像素按此顺序包含alpha,红色,绿色和蓝色通道。数字标识每个通道由每个像素的八位信息通道组成。

注意:在Image Processing in iOS Part 1: Raw Bitmap Modification,您将找到有关图像格式的更多详细信息。

3. Hooking it up to the UI

此模式适用于大多数vImage处理例程。您进行设置以创建适当的源缓冲区和目标缓冲区,使用后缀声明数据格式以调用所需的图像处理函数,然后检查是否有error并根据需要处理目标缓冲区。

现在是时候看到这一点了。打开ContentView.swift。在两个ImageViews之间,添加以下代码:

HStack {
  Button("Equalize Histogram") {
    var imageWrapper = VImageWrapper(uiImage: originalImage)
    imageWrapper.equalizeHistogram()
    processedImage = imageWrapper.processedImage
  }
}
.disabled(originalImage.cgImage == nil)

您添加了一个按钮,只有在包含有效``CGImage的图像加载后,该按钮才有效。 轻按后,该按钮将调用equalizeHistogram()并将结果放入视图的processedImage属性中。

构建并运行。 选择一张照片,然后点击Equalize Histogram按钮。 您会注意到巨大的变化。

这是一个很好的转换,但是现在该是另一个转换的时候了。

4. Implementing Image Reflection

直方图均衡化所遵循的步骤几乎适用于任何vImage图像处理功能。 现在,您将添加类似的代码以实现水平图像反射。

打开VImageWrapper.swift并将以下方法添加到该结构:

mutating func reflectImage() {
  guard
    let image = uiImage.cgImage,
    var imageBuffer = createVImage(image: uiImage),
    var destinationBuffer = try? vImage_Buffer(
      width: image.width,
      height: image.height,
      bitsPerPixel: UInt32(image.bitsPerPixel))
  else {
    print("Error creating image buffers.")
    processedImage = nil
    return
  }
  defer {
    imageBuffer.free()
    destinationBuffer.free()
  }

  let error = vImageHorizontalReflect_ARGB8888(
    &imageBuffer,
    &destinationBuffer,
    vNoFlags)

  guard error == kvImageNoError else {
    printVImageError(error: error)
    processedImage = nil
    return
  }

  processedImage = convertToUIImage(buffer: destinationBuffer)
}

此方法与equalizeHistogram()之间的唯一区别是,它将调用vImageHorizontalReflect_ARGB8888(_:_:_ :)而不是vImageEqualization_ARGB8888(_:_:_ :)

打开ContentView.swift,并在HStack末尾的Equalize Histogram按钮之后添加以下代码:

Spacer()
Button("Reflect") {
  var imageWrapper = VImageWrapper(uiImage: originalImage)
  imageWrapper.reflectImage()
  processedImage = imageWrapper.processedImage
}

构建并运行。 选择一张照片,然后点击新的Reflect按钮。 您会发现只需很小的改动就可以实现进一步的图像处理。

现在,您已经掌握了使用vImage的基本知识,接下来,您将探索一个更复杂的任务,该任务将需要您深入研究Objective-C模式。


Histograms

图像直方图表示图像中色调值的分布。 它将图像的色调分为沿水平轴显示。 每个点的直方图高度代表具有该色调的像素数。 一目了然,它使您可以了解照片的整体曝光和曝光平衡。 在图像处理中,直方图可以帮助进行边缘检测和分割任务。

在本部分中,您将了解如何获取图像的直方图数据。 在此过程中,您将学习与vImage库的更复杂的交互,包括使用仍基于Objective-C构建的模式。

1. Getting the Histogram

打开VImageWrapper.swift并在VImageWrapper结构之前添加以下代码:

enum WrappedImage {
  case original
  case processed
}

您将使用此枚举类型来区分原始图像或处理后的图像。 现在,将以下代码添加到VImageWrapper结构的末尾:

func getHistogram(_ image: WrappedImage) -> HistogramLevels? {
  guard
    // 1
    let cgImage =
      image == .original ? uiImage.cgImage : processedImage?.cgImage,
    // 2
    var imageBuffer = try? vImage_Buffer(cgImage: cgImage)
  else {
    return nil
  }
  // 3
  defer {
    imageBuffer.free()
  }
}

这里没有新内容:

  • 1) 您使用WrappedImage枚举的值来选择原始图像或已处理的图像。
  • 2) 然后,为图像创建一个vImage_Buffer
  • 3) 再次,在退出作用域时,使用defer调用free()

现在,在getHistogram(_ :)的末尾添加以下代码:

var redArray: [vImagePixelCount] = Array(repeating: 0, count: 256)
var greenArray: [vImagePixelCount] = Array(repeating: 0, count: 256)
var blueArray: [vImagePixelCount] = Array(repeating: 0, count: 256)
var alphaArray: [vImagePixelCount] = Array(repeating: 0, count: 256)

直方图的原始内容提供直方图中bin的像素数,其值为vImagePixelCount类型。 因此,您将创建四个256个元素的数组 - 每个颜色通道和一个alpha通道一个数组。 具有256个元素意味着一个具有256个直方图的数组,ARGB8888格式的每个通道的八位数据的不同值的数量保持不变。

2. Working with Pointers

现在,您来看看指针,您可能希望避免使用Swift! 在刚添加的数组定义之后添加以下代码:

// 1
var error: vImage_Error = kvImageNoError
// 2
redArray.withUnsafeMutableBufferPointer { rPointer in
  greenArray.withUnsafeMutableBufferPointer { gPointer in
    blueArray.withUnsafeMutableBufferPointer { bPointer in
      alphaArray.withUnsafeMutableBufferPointer { aPointer in
        // 3
        var histogram = [
          rPointer.baseAddress, gPointer.baseAddress,
          bPointer.baseAddress, aPointer.baseAddress
        ]
        // 4
        histogram.withUnsafeMutableBufferPointer { hPointer in
          // 5
          if let hBaseAddress = hPointer.baseAddress {
            error = vImageHistogramCalculation_ARGB8888(
              &imageBuffer,
              hBaseAddress,
              vNoFlags
            )
          }
        }
      }
    }
  }
}

尽管进行了使库更Swift-friendly友好的工作,但Accelerate框架的Objective-C根源贯穿于此。用于计算直方图的函数需要一个指向包含四个指针的数组的指针。这四个指针中的每一个都指向一个数组,该数组将接收一个通道的计数。几乎所有这些代码都将一组Swift数组更改为此Objective-C模式。

代码是这样的:

  • 1) 在闭包内部定义指针时,在Swift中使用指针变得更加舒适。因为您想将error参数设置在几个块深处,并且仍在块外使用,所以在开始闭包操作之前先对其进行定义。
  • 2) Swift提供了多种方法来访问诸如此类的遗留需求的指针。 withUnsafeMutableBufferPointer(_ :)创建一个可以在方法闭包内访问的指针。您为每个通道阵列制作一个。指针仅在传递给withUnsafeMutableBufferPointer(_ :)的闭包内有效。这就是为什么您必须嵌套这些调用的原因。
  • 3) 您创建一个新数组,其元素是这四个指针。注意这里的数组顺序。 baseAddress属性获得一个指向缓冲区第一个元素的指针,在本例中为每个通道的数组。至此,您已经建立了vImage期望直方图函数调用的结构。
  • 4) 您需要像创建通道数组一样指向刚创建的数组的指针,并在块内使用它。
  • 5) 您将指针解包到直方图histogram数组的第一个元素,然后调用vImageHistogramCalculation_ARGB8888传递图像缓冲区,解包后的指针,并且不再有特殊指令。

3. Finalizing the Histogram Data

最后,您可以返回正常的Swift-land,因为计算的值驻留在您先前创建的数组中,或者出现了问题。添加以下代码以完成该方法:

// 1
guard error == kvImageNoError else {
  printVImageError(error: error)
  return nil
}

// 2
let histogramData = HistogramLevels(
  red: redArray,
  green: greenArray,
  blue: blueArray,
  alpha: alphaArray
)

// 3
return histogramData

回到标准Swift很高兴,不是吗? 这是您在这里所做的事情:

  • 1) 检查是否有错误error,如果有,请打印出来。
  • 2) 根据原始直方图数据创建HistogramLevels结构体。
  • 3) 返回直方图数据。

HistogramLevels结构体是在项目中的HistogramView.swift内部定义的。 它为图像的每个通道提供一个数组。 随时打开文件,看看结构是如何定义的。

4. Visualizing the Histogram Data

现在,要可视化直方图数据,请打开ContentView.swift。 将以下内容添加到结构顶部的属性列表中:

@State private var originalHistogram: HistogramLevels?
@State private var processedHistogram: HistogramLevels?

然后,您需要将可选的直方图传递给视图。 将第一个ImageView调用更改为:

ImageView(
  title: "Original",
  image: originalImage,
  histogram: originalHistogram)

并把第二个修改为:

ImageView(
  title: "Processed",
  image: image,
  histogram: processedHistogram)
.padding(3)

接下来,在sheet(isPresented:onDismiss:content:)调用中,将onDismiss闭包的内容替换为以下内容:

let imageWrapper = VImageWrapper(uiImage: originalImage)
processedImage = nil
processedHistogram = nil
originalHistogram = imageWrapper.getHistogram(.original)

最后,在Equalize HistogramReflect按钮的动作结尾处添加以下代码。

processedHistogram = imageWrapper.getHistogram(.processed)

构建并运行。 选择与之前一样的照片,然后点按它,您会发现这样做可以打开或关闭直方图显示。 点击Equalize Histogram按钮,您将看到已处理图像的直方图所做的更改。 对于大多数照片来说,这是一个巨大的变化。

如您所料,Reflect不会更改直方图,因为它只会更改照片的方向,而不会更改图像的色调。

通过使用vImage库探索图像处理,您已经了解了Accelerate框架的基础知识。 Accelerate框架中的功能太多,本教程几乎无法触及它提供的功能。它应该为您奠定坚实的基础,帮助您理解在使用部分Swift友好的框架时会遇到的挑战。

Accelerate的文档documentation for Accelerate可能是所有Apple框架中最详尽的文档之一。这是学习框架可以做的一个很好的起点。主题topics部分提供了一些带有现代代码的示例。它通常更多地侧重于how而不是why,但是在学习本教程之后,您应该更好地了解框架背后的过程。

若要查看Accelerate如何适应Swift,请观看WWDC 2019中的Introducing Accelerate for Swift。如果您对信号处理感兴趣,即使许多代码有些过时,您也可以在WWDC 2018中的Using Accelerate and SIMD中找到有用的信息。因为它早于2019 Swift更新。

后记

本篇主要讲述了基于AcceleratevImageSwiftUI程序图像处理,感兴趣的给个赞或者关注~~~

Accelerate框架详细解析(二) —— 基于Accelerate 和 vImage的SwiftUI程序图像处理(一)_第2张图片

你可能感兴趣的:(Accelerate框架详细解析(二) —— 基于Accelerate 和 vImage的SwiftUI程序图像处理(一))