cocoa

Vision框架详细解析(四) —— 在iOS中使用Vision

2019-07-23  本文已影响0人  刀客传奇

版本记录

版本号 时间
V1.0 2019.07.23 星期二

前言

iOS 11+macOS 10.13+ 新出了Vision框架,提供了人脸识别、物体检测、物体跟踪等技术,它是基于Core ML的。可以说是人工智能的一部分,接下来几篇我们就详细的解析一下Vision框架。感兴趣的看下面几篇文章。
1. Vision框架详细解析(一) —— 基本概览(一)
2. Vision框架详细解析(二) —— 基于Vision的人脸识别(一)
3. Vision框架详细解析(三) —— 基于Vision的人脸识别(二)

开始

首先看一下主要内容

主要内容:在本教程中,您将使用MetalVision框架从iOS中的图片中删除移动对象。 您将学习如何堆叠,对齐和处理多个图像,以便任何移动对象消失。

然后看一下写作环境

Swift 5, iOS 12, Xcode 10

下面我们就正式开始了。

什么是照片堆叠(Photo Stacking)?好吧,想象一下。你在度假,某个神奇的地方。你正在英国各地游览所有哈利波特拍摄地点!

现在是时候看到景点并拍摄最精彩的照片了。只有一个问题:人太多了。

啊!你拍的每一张照片都充满了它们。只要你能像哈利一样施放一个简单的咒语,并让所有这些人消失。 Evanesco!噗!他们走了。那太棒了。

也许你可以做些什么。照片堆叠是所有酷孩子都在谈论的新兴计算摄影(computational photography)趋势。你想知道如何使用它吗?

在本教程中,您将使用Vision框架来学习如何:

令人兴奋,对吗?那么,你还等什么呢?继续阅读!

打开入门项目并在您的设备上运行它。

注意:由于您需要在本教程中使用相机和Metal,因此您必须在实际设备而不是模拟器上运行它。

Evanesco startup screenshot

你应该看到一些看起来像一个简单的相机应用程序。 有一个红色的记录按钮,周围有一个白色的环,它显示了全屏的摄像头输入。

当然你已经注意到相机看起来有点jittery。 那是因为它设置为每秒五帧捕获。 要查看代码中定义的位置,请打开CameraViewController.swift并在configureCaptureSession()中找到以下两行:

camera.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: 5)
camera.activeVideoMinFrameDuration = CMTime(value: 1, timescale: 5)

第一行强制最大帧速率为每秒五帧。 第二行将最小帧速率定义为相同。 这两行一起要求摄像机以所需的帧速率运行。

如果点击录制按钮,您应该看到外部白色环顺时针填满。 但是,当它完成时,没有任何反应。

你现在必须要做些什么。


Saving Images to the Files App

为了帮助您在进行中调试应用程序,将您正在使用的图像保存到Files应用程序会很不错。 幸运的是,这比听起来容易得多。

将以下两个键添加到Info.plist

将它们的值都设置为YES。 完成后,文件应如下所示:

第一个密钥为Documents目录中的文件启用文件共享。 第二个让您的应用程序从文件提供程序file provider打开原始文档,而不是接收副本。 启用这两个选项后,存储在应用程序Documents目录中的所有文件都将显示在Files应用程序中。 这也意味着其他应用可以访问这些文件。

现在您已经获得了Files应用程序访问Documents目录的权限,现在可以在那里保存一些图像了。

与起始项目捆绑在一起的是一个名为ImageSaverhelper struct。 实例化时,它会生成通用唯一标识符(Universally Unique Identifier - UUID)并使用它在Documents目录下创建目录。 这是为了确保您不会覆盖以前保存的图像。 您将在应用中使用ImageSaver将图像写入文件。

CameraViewController.swift中,在类的顶部定义一个新变量,如下所示:

var saver: ImageSaver?

然后,滚动到recordTapped(_ :)并将以下内容添加到方法的末尾:

saver = ImageSaver()

每次点击录制按钮时,您都可以在此处创建新的ImageSaver,从而确保每个录制会话都将图像保存到新目录。

接下来,滚动到captureOutput(_:didOutput:from :)并在初始if语句后添加以下代码:

// 1
guard 
  let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer),
  let cgImage = CIImage(cvImageBuffer: imageBuffer).cgImage() 
  else {
    return
}
// 2
let image = CIImage(cgImage: cgImage)
// 3 
saver?.write(image)

使用此代码,您:

注意:为什么你必须将样本缓冲区转换为CIImage,然后转换为CGImage,最后再转换回CIImage? 这与谁拥有数据有关。 将样本缓冲区转换为CIImage时,图像会存储对样本缓冲区的强引用。 不幸的是,对于视频采集,这意味着在几秒钟之后,它将开始丢弃帧,因为它耗尽了分配给样本缓冲区的内存。 通过使用CIIContextCIImage渲染为CGImage,可以复制图像数据,并且可以释放样本缓冲区以便再次使用。

现在,构建并运行应用程序。 点击录制按钮,完成后,切换到Files应用。 在Evanesco文件夹下,您应该看到一个UUID命名的文件夹,其中包含20个项目。

如果您查看此文件夹,您将找到在录制的4秒内采集的20帧。

注意:如果您没有立即看到该文件夹​​,请使用Files应用程序顶部的搜索栏。

好的。那么你可以用20张几乎相同的图像做什么呢?


Photo Stacking

在计算摄影(computational photography)中,照片堆叠是一种采集,对齐和组合多个图像以产生不同所需效果的技术。

例如,通过拍摄不同曝光等级的若干图像并将每个图像的最佳部分组合在一起来获得HDR图像。这就是你如何在iOS中同时在阴影和明亮的天空中看到细节。

天文摄影(Astrophotography)也大量使用照片堆叠。图像曝光越短,传感器拾取的噪声越少。因此,天文摄影师通常会拍摄一堆短曝光图像并将它们叠加在一起以增加亮度。

在微距摄影(macro photography)中,难以立即获得整个图像。使用照片堆叠,摄影师可以拍摄不同焦距的一些图像并将它们组合在一起,以产生非常小的物体的极其清晰的图像。

要将图像组合在一起,首先需要对齐它们。怎么样? iOS提供了一些有趣的API,可以帮助您。


Using Vision to Align Images

Vision框架有两个用于对齐图像的不同API:VNTranslationalImageRegistrationRequestVNHomographicImageRegistrationRequest。 前者更容易使用,如果你认为应用程序的用户将相对静止地保持iPhone,它应该足够好。

为了使您的代码更具可读性,您将创建一个新类来处理采集图像的对齐和最终组合。

创建一个新的空Swift文件并将其命名为ImageProcessor.swift

删除任何提供的import语句并添加以下代码:

import CoreImage
import Vision

class ImageProcessor {
  var frameBuffer: [CIImage] = []
  var alignedFrameBuffer: [CIImage] = []
  var completion: ((CIImage) -> Void)?
  var isProcessingFrames = false

  var frameCount: Int {
    return frameBuffer.count
  }
}

在这里,您导入Vision框架并定义ImageProcessor类以及一些必要的属性:

接下来,将以下方法添加到ImageProcessor类:

func add(_ frame: CIImage) {
  if isProcessingFrames {
    return
  }
  frameBuffer.append(frame)
}

此方法将采集的帧添加到帧缓冲区,但前提是您当前未处理帧缓冲区中的帧。

仍然在类上,添加处理方法:

func processFrames(completion: ((CIImage) -> Void)?) {
  // 1
  isProcessingFrames = true  
  self.completion = completion
  // 2
  let firstFrame = frameBuffer.removeFirst()
  alignedFrameBuffer.append(firstFrame)
  // 3
  for frame in frameBuffer {
    // 4
    let request = VNTranslationalImageRegistrationRequest(targetedCIImage: frame)

    do {
      // 5      
      let sequenceHandler = VNSequenceRequestHandler()
      // 6
      try sequenceHandler.perform([request], on: firstFrame)
    } catch {
      print(error.localizedDescription)
    }
    // 7
    alignImages(request: request, frame: frame)
  }
  // 8
  cleanup()
}

这看着是很多步骤,但这种方法相对简单。添加完所有采集的帧后,您将调用此方法。它将处理每个帧并使用Vision框架对齐它们。具体来说,在此代码中,您:

准备解决alignImages(request:frame:)

processFrames(completion:)下面添加以下代码:

func alignImages(request: VNRequest, frame: CIImage) {
  // 1
  guard 
    let results = request.results as? [VNImageTranslationAlignmentObservation],
    let result = results.first 
    else {
      return
  }
  // 2
  let alignedFrame = frame.transformed(by: result.alignmentTransform)
  // 3
  alignedFrameBuffer.append(alignedFrame)
}

在这里你:

最后两种方法是您的应用程序所需的Vision代码的核心。 您执行请求,然后使用结果(results)来修改图像。 现在剩下的就是自己清理。

将以下方法添加到ImageProcessor类的末尾:

func cleanup() {
  frameBuffer = []
  alignedFrameBuffer = []
  isProcessingFrames = false
  completion = nil
}

cleanup()中,您只需清除两个帧缓冲区,重置标志以指示您不再处理帧并将完成处理程序设置为nil

在构建和运行应用程序之前,需要在CameraViewController中使用ImageProcessor

打开CameraViewController.swift。 在类的顶部,定义以下属性:

let imageProcessor = ImageProcessor()

接下来,找到captureOutput(_:didOutput:from :)。 您将对此方法进行两处小改动。

let image = ...行下方添加以下行:

imageProcessor.add(image)

在调用stopRecording()之后,仍然在if语句中添加:

imageProcessor.processFrames(completion: displayCombinedImage)

构建并运行您的应用程序......没有任何反应。 不用担心,波特先生。 您仍然需要将所有这些图像组合成一个杰作。 要了解如何做到这一点,你必须继续阅读!

注意:如果要查看对齐图像与原始采集的比较,可以在ImageProcessor中实例化ImageSaver。 这将允许您将对齐的图像保存到Documents文件夹,并在Files应用程序中查看它们。


How Photo Stacking works

将图像组合或堆叠在一起有几种不同的方法。 到目前为止,最简单的方法是将图像中每个位置的像素平均在一起。

例如,如果要堆叠20个图像,则可以将所有20个图像的坐标(13,37)处的像素平均在一起,以获得(13,37)处的堆叠图像的平均像素值。

Pixel stacking

如果对每个像素坐标执行此操作,则最终图像将是所有图像的平均值。您拥有的图像越多,平均值与背景像素值越接近。如果某些东西在相机前移动,它只会出现在几张图像中的相同位置,因此它对整体平均值的贡献不大。这就是移动物体消失的原因。

这就是您实现堆叠逻辑(stacking logic)的方法。


Stacking Images

现在来了真正有趣的部分!您将把所有这些图像组合成一个梦幻般的图像。您将使用Metal Shading Language(MSL)创建自己的Core Image kernel

您的简单内核将计算两个图像的像素值的加权平均值。当你将一堆图像平均在一起时,任何移动的物体都应该消失。背景像素将更频繁地出现并且支配平均像素值。

1. Creating a Core Image Kernel

您将从实际内核开始,该内核是用MSL编写的。 MSLC ++非常相似。

将新的Metal File添加到项目中,并将其命名为AverageStacking.metal。保留模板代码并将以下代码添加到文件末尾:

#include <CoreImage/CoreImage.h>

extern "C" { namespace coreimage {
  // 1
  float4 avgStacking(sample_t currentStack, sample_t newImage, float stackCount) {
    // 2
    float4 avg = ((currentStack * stackCount) + newImage) / (stackCount + 1.0);
    // 3
    avg = float4(avg.rgb, 1);
    // 4
    return avg;
  }
}}

使用此代码,您:

注意:理解为两个图像之间的每对相应像素调用此函数非常重要。 sample_t数据类型是来自图像的像素样本。

好了,既然你有一个内核函数,你需要创建一个CIFilter来使用它! 将新的Swift文件添加到项目中,并将其命名为AverageStackingFilter.swift。 删除import语句并添加以下内容:

import CoreImage

class AverageStackingFilter: CIFilter {
  let kernel: CIBlendKernel
  var inputCurrentStack: CIImage?
  var inputNewImage: CIImage?
  var inputStackCount = 1.0
}

在这里,您将定义新的CIFilter类以及它所需的一些属性。 注意三个输入变量如何对应于内核函数中的三个参数。 巧合?

到目前为止,Xcode可能会警告此类缺少初始化程序。 所以,是时候解决这个问题。 将以下内容添加到类中:

override init() {
  // 1
  guard let url = Bundle.main.url(forResource: "default", 
                                  withExtension: "metallib") else {
    fatalError("Check your build settings.")
  }
  do {
    // 2
    let data = try Data(contentsOf: url)
    // 3
    kernel = try CIBlendKernel(
      functionName: "avgStacking", 
      fromMetalLibraryData: data)
  } catch {
    print(error.localizedDescription)
    fatalError("Make sure the function names match")
  }
  // 4
  super.init()
}

使用此初始化程序,您:

等一下......你什么时候编译并链接你的Metal文件? 不幸的是,你还没有。 不过,好消息是你可以让Xcode为你做到!

2. Compiling Your Kernel

要编译和链接Metal文件,需要在Build Settings中添加两个标志。 所以前往那边。

搜索Other Metal Compiler Flags并添加-fcikernel

Metal compiler flag

接下来,单击+按钮并选择Add User-Defined Setting

Add user-defined setting

调用设置MTLLINKER_FLAGS并将其设置为-cikernel

Metal linker flag

现在,下次构建项目时,Xcode将编译您的Metal文件并自动链接它们。

但是,在您可以执行此操作之前,您仍需要对Core Image过滤器进行一些工作。

回到AverageStackingFilter.swift,添加以下方法:

func outputImage() -> CIImage? {
  guard 
    let inputCurrentStack = inputCurrentStack,
    let inputNewImage = inputNewImage
    else {
      return nil
  }
  return kernel.apply(
    extent: inputCurrentStack.extent,
    arguments: [inputCurrentStack, inputNewImage, inputStackCount])
}

这种方法非常重要。 也就是说,它会将您的内核函数应用于输入图像并返回输出图像! 如果没有这样做,它将是一个无用的过滤器。

呃,Xcode还在警告! 很好,将以下代码添加到类中以使其消失:

required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

您无需从unarchiver初始化此Core Image过滤器,因此您只需实现最低限度即可使Xcode满意。

3. Using Your Filter

打开ImageProcessor.swift并将以下方法添加到ImageProcessor

func combineFrames() {
  // 1
  var finalImage = alignedFrameBuffer.removeFirst()
  // 2
  let filter = AverageStackingFilter()
  //3 
  for (i, image) in alignedFrameBuffer.enumerated() {
    // 4
    filter.inputCurrentStack = finalImage
    filter.inputNewImage = image
    filter.inputStackCount = Double(i + 1)
    // 5
    finalImage = filter.outputImage()!
  }
  // 6
  cleanup(image: finalImage)
}

在这里你:

您可能已经注意到cleanup()不接受任何参数。 通过使用以下内容替换cleanup()来解决此问题:

func cleanup(image: CIImage) {
  frameBuffer = []
  alignedFrameBuffer = []
  isProcessingFrames = false
  if let completion = completion {
    DispatchQueue.main.async {
      completion(image)
    }
  }
  completion = nil
}

唯一的更改是新添加的参数和在主线程上调用完成处理程序的if语句。 其余的仍然保持原样。

processFrames(completion:)的底部,将对cleanup()的调用替换为:

combineFrames()

这样,图像处理器(image processor)将在对齐后将所有捕获的帧组合在一起,然后将最终图像传递给completion函数。

构建并运行这个应用程序,让那些人,汽车和任何在你的镜头中移动的东西消失!

为了更多的乐趣,当你使用该应用程序。其他人绝对不会认为你很奇怪。

恭喜!您已经完成了本教程中的许多概念。你现在已准备好在现实世界中发挥你的魔力!

但是,如果您想尝试改进您的应用,有几种方法可以做到:

后记

本篇主要讲述了在iOS中使用Vision和Metal进行照片堆叠,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读