Vision框架详细解析(六) —— 基于Vision的显著性分析(一)

版本记录

版本号 时间
V1.0 2019.12.04 星期三

前言

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

开始

首先看下主要内容

在本教程中,您将学习如何在iOS中使用Vision框架执行显着性分析,以及如何在实时视频Feed上创建效果。

接着看下写作环境

Swift 5, iOS 13, Xcode 11

您知道机器人的恐怖之处吗?您永远无法说出他们在看什么。他们就像戴着墨镜的人。他们在盯着你还是其他?

最后,苹果已经说够了。苹果为我们提供了一种技术,可以让人们看到iPhone的有趣之处。这就是所谓的显着性分析(saliency analysis),您也可以利用它的强大功能!

在本教程中,您将构建一个应用,该应用使用显着性分析来过滤来自iOS设备的相机feed,从而围绕有趣的对象创建聚光灯(spotlight)效果。

在此过程中,您将学习如何使用Vision框架执行以下操作:

  • 创建请求以执行显着性分析。
  • 使用返回的观测值生成热图(heat map)
  • 使用热图作为输入来过滤视频流。

注意:由于本教程使用iOS 13中引入的相机和API,因此您至少需要Xcode 11和运行iOS 13.0或更高版本的设备。您不能为此使用模拟器,因为您需要从物理摄像机实时直播视频。

准备通过iPhone的眼睛看世界!

打开入门项目并浏览代码,以了解其工作原理。

入门项目设置了摄像机并将其输出未经修改地显示在屏幕上。 此外,屏幕顶部还有一个标签,描述了屏幕输出。 最初,由于相机feed未更改,因此显示原始(Original)


Saliency Analysis

显着性分析使用算法来确定图像中人类感兴趣或重要的事物。 本质上是确定引起人们注意的图像的含义。

选出照片中的重要区域后,便可以使用此信息自动裁剪或提供突出显示它们的滤镜效果。

如果您对视频源实时执行显着性分析,则还可以使用这些信息来帮助重点关注关键区域。

Apple提供的Vision框架具有两种不同类型的显着性分析:基于注意力和基于对象(attention-based and object-based)

基于注意力的显着性试图确定一个人可能关注的领域。 另一方面,基于对象的显着性旨在突出显示整个感兴趣的对象。 尽管相关,但这两者是完全不同的。


Attention-Based Heat Maps

用于显着性分析的两种Vision API均会返回热图(heat map)。有多种方式可视化热图。Vision请求返回的那些是灰度的。此外,热图是在比照片或视频feed更粗糙的网格上定义的。根据Apple的文档documentation,根据您是否实时进行API调用,您将获得64 x 64或68 x 68像素的热图。

注意:尽管文档说在实时调用API时期望有64 x 64像素的热图,但是本教程中用于在视频源上执行Vision请求的代码仍然会产生80 x 68像素的热图。

用于返回CVPixelBuffer的宽度和高度的函数报告为68 x68。但是,CVPixelBuffer中包含的数据实际上为80 x68。这可能是一个错误,将来可能会更改。

CameraViewController.swift中,将以下代码添加到captureOutput(_:didOutput:from :)的末尾:

// 1
let req = VNGenerateAttentionBasedSaliencyImageRequest(
  completionHandler: handleSaliency)
    
do {
  // 2
  try sequenceHandler.perform(
    [req],
    on: imageBuffer,
    orientation: .up)
    
} catch {
  // 3
  print(error.localizedDescription)
}

使用此代码,您:

  • 1) 生成基于关注的显着性视觉请求。
  • 2) 使用VNSequenceRequestHandler对在方法开头创建的CVImageBuffer执行请求。
  • 3) 捕获并打印错误error(如果有的话)。

您迈向了解机器人iPhone的第一步!

您会注意到Xcode报错,并且似乎不知道handleSaliency是什么。 尽管他们在计算机视觉方面取得了长足的进步,但Apple仍未找到让Xcode为您编写代码的方法。

您需要编写handleSaliency,它将完成一个视觉请求,并对结果进行一些有用的处理。

在同一文件的末尾,添加新的扩展名以容纳与Vision相关的方法:

extension CameraViewController {
}

然后,在此扩展中,添加传递给Vision请求的handleSaliency完成处理程序:

func handleSaliency(request: VNRequest, error: Error?) {
  // 1
  guard
    let results = request.results as? [VNSaliencyImageObservation],
    let result = results.first
    else { return }

  // 2
  guard let targetExtent = currentFrame?.extent else {
    return
  }
  
  // 3
  var ciImage = CIImage(cvImageBuffer: result.pixelBuffer)

  // 4
  let heatmapExtent = ciImage.extent
  let scaleX = targetExtent.width / heatmapExtent.width
  let scaleY = targetExtent.height / heatmapExtent.height

  // 5
  ciImage = ciImage
    .transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
      
  // 6
  showHeatMap(with: ciImage)
}

在这里,您:

  • 1) 确保结果是VNSaliencyImageObservation对象,并从Vision请求返回的观察值数组中提取第一个结果。
  • 2) 从当前帧中抓取图像范围。 这实质上是当前帧的大小。
  • 3) 从CVPixelBuffer创建一个表示热图的CIImage
  • 4) 计算当前帧和热图之间的比例因子(scale factor)
  • 5) 将热图缩放为当前帧的大小。
  • 6) 使用showHeatMap显示热图。

现在,在handleSaliency(request:error :)上方,添加将显示热图的便捷帮助器方法:

func showHeatMap(with heatMap: CIImage) {
  // 1
  guard let frame = currentFrame else {
    return
  }
  
  let yellowHeatMap = heatMap
    // 2
    .applyingFilter("CIColorMatrix", parameters:
      ["inputBVector": CIVector(x: 0, y: 0, z: 0, w: 0),
       "inputAVector": CIVector(x: 0, y: 0, z: 0, w: 0.7)])
    // 3
    .composited(over: frame)

  // 4
  display(frame: yellowHeatMap)
}

使用此方法,您:

  • 1) 解包currentFrame可选。
  • 2) 将CIColorMatrixCore Image滤镜应用于热图。 您需要将蓝色分量归零,同时将每个像素的alpha分量乘以0.7。 这导致黄色的热图部分透明。
  • 3) 将黄色热图添加到原始帧的顶部。
  • 4) 使用提供的帮助方法显示结果图像。

在构建和运行该应用之前,您需要进行最后的更改。

返回captureOutput(_:didOutput:from :)并替换以下代码行:

display(frame: currentFrame)

替换为

if mode == .original {
  display(frame: currentFrame)
  return
}

此代码可确保您仅在Original模式下显示未过滤的帧。 它也会从方法中返回,因此您不会浪费宝贵的计算周期(和电池!)来执行任何Vision请求。

好吧,时间到了! 生成并运行该应用程序,然后点击以将其置于“热图”模式(Heat Map mode)


Improving the Heat Map

虽然您的热图非常酷,但是有两个问题:

  • 1) 如果算法对其结果不太有把握,则最亮的地方可能会很暗。
  • 2) 看起来像素化。

好消息是,这两个问题都是可以解决的。

1. Normalizing the Heat Map

您可以通过标准化热图来解决第一个问题。

CVPixelBufferExtension.swift中,将以下规范化方法添加到现有CVPixelBuffer扩展中:

func normalize() {
  // 1
  let bytesPerRow = CVPixelBufferGetBytesPerRow(self)
  let totalBytes = CVPixelBufferGetDataSize(self)

  let width = bytesPerRow / MemoryLayout<Float>.size
  let height = totalBytes / bytesPerRow
    
  // 2
  CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags(rawValue: 0))
  
  // 3
  let floatBuffer = unsafeBitCast(
    CVPixelBufferGetBaseAddress(self), 
    to: UnsafeMutablePointer<Float>.self)
  
  // 4  
  var minPixel: Float = 1.0
  var maxPixel: Float = 0.0
    
  // 5
  for i in 0 ..< width * height {
    let pixel = floatBuffer[i]
    minPixel = min(pixel, minPixel)
    maxPixel = max(pixel, maxPixel)
  }
    
  // 6
  let range = maxPixel - minPixel
    
  // 7
  for i in 0 ..< width * height {
    let pixel = floatBuffer[i]
    floatBuffer[i] = (pixel - minPixel) / range
  }
    
  // 8
  CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags(rawValue: 0))
}

那是很多代码。在这里:

  • 1) 提取CVPixelBuffer的宽度和高度。您可以使用CVPixelBufferGetWidthCVPixelBufferGetHeight来实现。但是,由于您要遍历实际数据,因此实际上最好使用每行字节数和总数据大小,以使您知道自己在分配的内存范围内进行操作。
  • 2) 锁定像素缓冲区的基地址。在使用CPU访问像素数据之前,这是必需的。
  • 3) 因为您知道热图数据是浮点数据,所以将CVPixelBuffer的基地址强制转换为Float指针。
  • 4) 初始化一些变量以跟踪找到的最小和最大像素值。
  • 5) 循环遍历CVPixelBuffer中的每个像素,并保存最小和最大值。由于CVPixelBuffer数据在内存中线性映射,因此您可以循环遍历缓冲区中的像素数。
  • 6) 计算像素值的范围。
  • 7) 再次遍历每个像素,并将其值归一化为介于0.0和1.0之间。
  • 8) 解锁像素缓冲区的基地址。

在尝试之前,您需要从Vision请求管道中调用normalize

打开CameraViewController.swift,然后再次找到handleSaliency(request:error :)。 在声明和初始化ciImage的行上方,添加以下行:

result.pixelBuffer.normalize()

normalize更新CVPixelBuffer时,请确保在其他地方使用result.pixelBuffer之前先调用它。

再次构建并运行该应用程序,以查看更突出的热图。

不是很坏,对吧?

2. Blurring the Heat Map

现在,该解决第二个问题了:像素化。 发生像素化的原因是热图为80 x 68,并且您正在将其缩放到视频feed的分辨率。

要解决此问题,请在按比例放大后对热图应用高斯模糊。 打开CameraViewController.swift并再次找到handleSaliency(request:error :)。 然后替换以下行:

ciImage = ciImage
  .transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))

使用

ciImage = ciImage
  .transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
  .applyingGaussianBlur(sigma: 20.0)
  .cropped(to: targetExtent)

在缩放热图并使用20.0的模糊半径后,您将直接应用高斯模糊。 由于模糊会导致图像每侧增加模糊半径,因此请将其裁剪为原始图像范围。

再次构建并运行,然后查看新的和改进的热图!


Object-Based Heat Maps

现在,您已经成为基于注意力的热图的专家,现在该是您尝试基于对象的热图的时候了。

基于对象的热图将尝试分割被认为有趣的整个对象。Vision框架将尝试使热图与对象的形状一致。

此外,您将以某种方式编写代码,以使您能够在基于注意的显着性和基于对象的显着性之间快速切换。 这样做将使您轻松了解两种显着性方法之间的区别。

再次打开CameraViewController.swift。 转到captureOutput(_:didOutput:from :)并找到在其中创建VNGenerateAttentionBasedSaliencyImageRequest的行。

用以下代码替换该行:

// 1
let req: VNImageBasedRequest

// 2
var selectedSegmentIndex = 0
    
// 3
DispatchQueue.main.sync {
  selectedSegmentIndex = saliencyControl.selectedSegmentIndex
}
    
// 4
switch selectedSegmentIndex {
case 0:
  req = 
    VNGenerateAttentionBasedSaliencyImageRequest(completionHandler: handleSaliency)
case 1:
  req = 
    VNGenerateObjectnessBasedSaliencyImageRequest(completionHandler: handleSaliency)
default:
      fatalError("Unhandled segment index!")
}

通过此代码更改,您:

  • 1) 声明一个常量VNImageBasedRequest。两种类型的显着性分析请求都从此类继承,因此您可以使用此常数来存储其中之一。另外,您可以使它成为常量而不是变量,因为您保证只分配一次。
  • 2) 初始化变量以存储来自UISegmentedControl的所选段的索引。除了声明它之外,还必须初始化它。否则,您将得到一个错误,指出该错误在被后续闭包捕获之前尚未初始化。
  • 3) 在主线程上读取预定义的UISegmentedControlselectedSegmentIndex属性,以避免访问后台线程上的UI元素。
  • 4) 根据选择的段创建VNGenerateAttentionBasedSaliencyImageRequestVNGenerateObjectnessBasedSaliencyImageRequest

在构建和运行之前,请在适当的时候使UISegmentedControl可见。

找到handleTap(_ :)并在方法顶部添加以下行:

saliencyControl.isHidden = false

然后,在.heatMapcase下面,添加这行

saliencyControl.isHidden = true

完成方法如下所示:

@IBAction func handleTap(_ sender: UITapGestureRecognizer) {
  saliencyControl.isHidden = false
    
  switch mode {
  case .original:
    mode = .heatMap
  case .heatMap:
    mode = .original
    saliencyControl.isHidden = true
  }
    
  modeLabel.text = mode.rawValue
}

您将使saliencyControl的默认状态可见,除非要转换为.original状态,此时要隐藏它。

构建并运行。 切换到热图模式。 您应该在屏幕底部看到一个分段控件,该控件使您可以在基于注意的显着性和基于对象的显着性之间进行切换。


Spotlight Effect Using the Saliency Heat Maps

显着性分析的一种用途是根据热图创建效果,以应用于图像或视频源。 您将要创建一个突出显示突出区域并使其他所有区域变暗的区域。

仍在showHeatMap(with :)下面的CameraViewController.swift中,添加以下方法:

func showFlashlight(with heatMap: CIImage) {
  // 1
  guard let frame = currentFrame else {
    return
  }
    
  // 2
  let mask = heatMap
    .applyingFilter("CIColorMatrix", parameters:
      ["inputAVector": CIVector(x: 0, y: 0, z: 0, w: 2)])

  // 3
  let spotlight = frame
    .applyingFilter("CIBlendWithMask", parameters: ["inputMaskImage": mask])

  // 4
  display(frame: spotlight)
}

使用此方法,您:

  • 1) 解包当前帧,即CIImage
  • 2) 使用Core Image滤镜将热图的Alpha通道乘以2,从而在热图中生成一个更亮且稍大的受热区域。
  • 3) 应用另一个Core Image滤镜以遮掩热图为黑色的帧中的所有像素。
  • 4) 显示过滤后的图像。

这种方法将成为您的特殊效果的主力军。 要启用它,请在文件顶部的ViewMode枚举中添加一个新的大小写:

case flashlight = "Spotlight"

Xcode现在应该在报错。 将handleTap中的switch语句替换为以下内容:

switch mode {
case .original:
  mode = .heatMap
case .heatMap:
  mode = .flashlight
case .flashlight:
  mode = .original
  saliencyControl.isHidden = true
}

这将添加新的.flashlight大小写,并将其添加为.heatMap之后的新模式。

最后,在handleSaliency(request:error :)的底部,用以下代码替换对showHeatMap(with :)的调用:

switch mode {
case .heatMap:
  showHeatMap(with: ciImage)
case .flashlight:
  showFlashlight(with: ciImage)
default:
  break
}

在这里,您可以根据应用所处的模式选择合适的显示方式。

构建并运行您的应用程序,并使用基于注意和基于对象的显着性检查聚光灯效果!

后记

本篇主要讲述了基于Vision的显著性分析,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容