版本记录
版本号 | 时间 |
---|---|
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) 将
CIColorMatrix
的Core 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
的宽度和高度。您可以使用CVPixelBufferGetWidth
和CVPixelBufferGetHeight
来实现。但是,由于您要遍历实际数据,因此实际上最好使用每行字节数和总数据大小,以使您知道自己在分配的内存范围内进行操作。 - 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) 在主线程上读取预定义的
UISegmentedControl
的selectedSegmentIndex
属性,以避免访问后台线程上的UI元素。 - 4) 根据选择的段创建
VNGenerateAttentionBasedSaliencyImageRequest
或VNGenerateObjectnessBasedSaliencyImageRequest
。
在构建和运行之前,请在适当的时候使UISegmentedControl
可见。
找到handleTap(_ :)
并在方法顶部添加以下行:
saliencyControl.isHidden = false
然后,在.heatMap
的case
下面,添加这行
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的显著性分析,感兴趣的给个赞或者关注~~~