版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.03.12 星期二 |
前言
ios 11+
和macOS 10.13+
新出了Vision
框架,提供了人脸识别、物体检测、物体跟踪等技术,它是基于Core ML的。可以说是人工智能的一部分,接下来几篇我们就详细的解析一下Vision框架。感兴趣的看下面几篇文章。
1. Vision框架详细解析(一) —— 基本概览(一)
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
在本教程中,您将学习如何使用Vision框架:
- 创建面部检测和检测面部标志的请求。
- 处理这些请求。
- 在相机Feed上叠加结果,以获得实时的视觉反馈。
打开入门项目并探索您的内心。
注意:启动器项目使用摄像头,这意味着如果您尝试在模拟器中运行它,您将会崩溃。 确保在实际设备上运行本教程,以便您可以看到您可爱的脸!
目前,Face Lasers
应用程序并没有做很多事情。 好吧,它确实向你展示了你的美丽杯子!
底部还有一个标签,上面写着Face
。 您可能已经注意到,如果点击屏幕,此标签将更改为Lasers
。
真令人兴奋! 除了似乎没有任何激光器。 那不太令人兴奋。 别担心 - 在本教程结束时,你会像Super(wo)男人一样从你的眼睛中射出激光!
您还会注意到一些有用的Core Graphics
扩展。 您将在整个教程中使用这些来简化代码。
Vision Framework Usage Patterns
所有Vision
框架API都使用三种结构:
- 1) 请求
(Request)
:请求定义要检测的事物的类型以及将处理结果的完成处理程序。 这是VNRequest的子类。 - 2) 请求处理程序
(Request handler)
:请求处理程序在提供的像素缓冲区上执行请求(假如:图像)。 这将是用于单次一次性检测的VNImageRequestHandler
或用于处理一系列图像的VNSequenceRequestHandler
。 - 3) 结果
(Results)
:结果将附加到原始请求并传递给创建请求时定义的完成处理程序。 它们是VNObservation
的子类
简单吧?
Writing Your First Face Detector
打开FaceDetectionViewController.swift
并在类的顶部添加以下属性:
var sequenceHandler = VNSequenceRequestHandler()
这将定义您将从相机Feed中提供图像的请求处理程序。 您正在使用VNSequenceRequestHandler
,因为您将对一系列图像执行面部检测请求,而不是单个静态图像。
现在滚动到文件的底部,您将在其中找到空的captureOutput(_:didOutput:from :)
委托方法。 使用以下代码填写:
// 1
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
// 2
let detectFaceRequest = VNDetectFaceRectanglesRequest(completionHandler: detectedFace)
// 3
do {
try sequenceHandler.perform(
[detectFaceRequest],
on: imageBuffer,
orientation: .leftMirrored)
} catch {
print(error.localizedDescription)
}
使用此代码,您:
- 1) 从传入的样本缓冲区中获取图像缓冲区。
- 2) 创建面部检测请求以检测面部边界框并将结果传递给完成处理程序。
- 3) 使用先前定义的序列请求处理程序在图像上执行面部检测请求。
orientation
参数告诉请求处理程序输入图像的方向是什么。
现在你可能想知道:但detectedFace(request:error:)
在哪里呢?
你现在就定义它。
将detectFace(request:error :)
的以下代码添加到FaceDetectionViewController
类中,或者你喜欢的任何地方:
func detectedFace(request: VNRequest, error: Error?) {
// 1
guard
let results = request.results as? [VNFaceObservation],
let result = results.first
else {
// 2
faceView.clear()
return
}
// 3
let box = result.boundingBox
faceView.boundingBox = convert(rect: box)
// 4
DispatchQueue.main.async {
self.faceView.setNeedsDisplay()
}
}
在这种方法中你:
- 1) 从面部观察结果数组中提取第一个结果。
- 2) 如果出现问题或未检测到面部,请清除
FaceView
。 - 3) 将边界框设置为在从
VNFaceObservation
中的坐标转换后在FaceView
中绘制。 - 4) 调用
setNeedsDisplay()
以确保重绘FaceView
。
结果的边界框坐标在输入图像的0.0和1.0之间归一化,原点位于左下角。 这就是为什么你需要将它们转换为有用的东西。
不幸的是,这个函数不存在。 幸运的是,你是一个才华横溢的程序员!
在上面放置detectedFace(request:error:)
方法定义的位置,添加以下方法定义:
func convert(rect: CGRect) -> CGRect {
// 1
let origin = previewLayer.layerPointConverted(fromCaptureDevicePoint: rect.origin)
// 2
let size = previewLayer.layerPointConverted(fromCaptureDevicePoint: rect.size.cgPoint)
// 3
return CGRect(origin: origin, size: size.cgSize)
}
在这里你:
- 1) 使用
AVCaptureVideoPreviewLayer
中的方便方法将标准化原点转换为预览图层的坐标系。 - 2) 然后使用相同的方便方法以及一些漂亮的Core Graphics扩展来将标准化大小转换为预览图层的坐标系。
- 3) 使用新的原点和大小创建
CGRect
。
你可能很想建立并运行它。 如果你这样做,你会很失望,除了你自己的脸,屏幕上什么都看不到,可悲的是没有激光。
目前FaceView
有一个draw(_:)
方法。 如果你想在屏幕上看到某些东西,你需要填写它!
切换到FaceView.swift
并添加以下代码到draw(_:)
:
// 1
guard let context = UIGraphicsGetCurrentContext() else {
return
}
// 2
context.saveGState()
// 3
defer {
context.restoreGState()
}
// 4
context.addRect(boundingBox)
// 5
UIColor.red.setStroke()
// 6
context.strokePath()
使用此代码,您:
- 1) 获取当前图形上下文。
- 2) 将当前图形状态推入堆栈。
- 3) 退出此方法时恢复图形状态。
- 4) 向上下文添加描述边界框的路径。
- 5) 将颜色设置为红色。
- 6) 绘制步骤4中描述的实际路径。
唷! 你已经编写了相当长的一段时间。 终于到了!
继续构建并运行您的应用程序。
已经可以检测到人脸了。
What Else Can You Detect?
除了面部检测之外,Vision
框架还具有可用于检测各种事物的API。
-
Rectangles
:使用VNDetectRectanglesRequest
,您可以检测摄像机输入中的矩形,即使它们因透视而失真。 -
Text
:您可以使用VNDetectTextRectanglesRequest
检测单个文本字符周围的边界框。但请注意,这不识别字符是什么,它只检测它们。 -
Horizon
:使用VNDetectHorizonRequest
,您可以确定图像中水平线的角度。 -
Barcodes
:您可以使用VNDetectBarcodesRequest
检测和识别多种条形码。请在此处here查看完整列表。 -
Objects
:通过将Vision
框架与CoreML
相结合,您可以使用VNCoreMLRequest
检测和分类特定对象。 -
Image alignment
:使用VNTranslationalImageRegistrationRequest
和VNHomographicImageRegistrationRequest
,您可以对齐具有重叠内容的两个图像。
太棒了吧?
嗯,使用Vision
框架可以检测到另外一件非常重要的事情。您可以使用它来检测face landmarks
!由于本教程是关于面部检测的,因此您将在下一部分中进行此操作。
Detecting Face Landmarks
您需要做的第一件事是更新您的Vision
请求以检测face landmarks
。 要做到这一点,打开FaceDetectionViewController.swift
并在captureOutput(_:didOutput:from :)
中用以下代码替换你定义detectFaceRequest
的行:
let detectFaceRequest = VNDetectFaceLandmarksRequest(completionHandler: detectedFace)
如果您现在要构建并运行,您将看不到与之前的任何差异。 你仍然会在脸上看到一个红色的边框。
为什么?
因为VNDetectFaceLandmarksRequest
将首先检测图像中的所有面部,然后再分析面部特征。
接下来,您将需要定义一些辅助方法。 右下方convert(rect:)
,添加以下代码:
// 1
func landmark(point: CGPoint, to rect: CGRect) -> CGPoint {
// 2
let absolute = point.absolutePoint(in: rect)
// 3
let converted = previewLayer.layerPointConverted(fromCaptureDevicePoint: absolute)
// 4
return converted
}
使用此代码,您:
- 1) 定义一种方法,将
landmark
点转换为可在屏幕上绘制的内容。 - 2) 使用
CoreGraphicsExtensions.swift
中定义的Core Graphics
扩展来计算规范化点的绝对位置。 - 3) 将点转换为预览图层的坐标系。
- 4) 返回转换后的点。
在该方法下面添加以下内容:
func landmark(points: [CGPoint]?, to rect: CGRect) -> [CGPoint]? {
return points?.compactMap { landmark(point: $0, to: rect) }
}
此方法采用这些landmark
点的数组并将它们全部转换。
接下来,您将重构一些代码,以便更轻松地使用和添加功能。 在两个新的辅助方法下面添加以下方法:
func updateFaceView(for result: VNFaceObservation) {
defer {
DispatchQueue.main.async {
self.faceView.setNeedsDisplay()
}
}
let box = result.boundingBox
faceView.boundingBox = convert(rect: box)
guard let landmarks = result.landmarks else {
return
}
if let leftEye = landmark(
points: landmarks.leftEye?.normalizedPoints,
to: result.boundingBox) {
faceView.leftEye = leftEye
}
}
这里唯一新的东西是函数中的第一个if
语句。if
使用新的辅助方法将组成leftEye
的规范化点转换为与预览图层一起使用的坐标。 如果一切顺利,您将这些转换的点分配给FaceView
的leftEye
属性。
其余看起来很熟悉,因为你已经在detectedFace(request:error:)
中写了它,所以,你现在应该清理一下。
在·detectedFace(request:error :)·中,替换以下代码:
let box = result.boundingBox
faceView.boundingBox = convert(rect: box)
DispatchQueue.main.async {
self.faceView.setNeedsDisplay()
}
用下面
updateFaceView(for: result)
这将调用您新定义的方法来处理更新FaceView
。
在您尝试代码之前还有最后一步。 打开FaceView.swift
并将以下代码添加到draw(_ :)
的末尾,紧跟在现有语句context.strokePath()
之后:
// 1
UIColor.white.setStroke()
if !leftEye.isEmpty {
// 2
context.addLines(between: leftEye)
// 3
context.closePath()
// 4
context.strokePath()
}
在这里你:
- 1) 将描边颜色设置为白色,以区别于红色边框。
- 2) 如果有任何点,则在定义
leftEye
的点之间添加线。 - 3) 关闭路径,使眼睛形状很好。
- 4) 描边路径,使其可见。
是时候建立和运行了!
注意:您已添加代码来注释左眼,但这意味着什么? 使用
Vision
,您应该看到的轮廓不是在左眼上绘制的,而是在图像左侧的眼睛上绘制的。一个有趣的游戏与计算机视觉API是寻找
left
,right
的词,并猜测它们的意思。 每次都不一样!
真棒! 如果你试图睁大眼睛或闭上眼睛,你应该看到眼睛略微改变形状,尽管没有那么多。
这是一个了不起的里程碑。 你现在可能想要快速休息一下,因为你将一举添加所有其他face landmarks
。
回来了吗? 你很勤奋! 是时候添加其他landmarks
了。
当你仍然打开FaceView.swift
时,在左眼代码之后将以下内容添加到draw(_ :)
的末尾:
if !rightEye.isEmpty {
context.addLines(between: rightEye)
context.closePath()
context.strokePath()
}
if !leftEyebrow.isEmpty {
context.addLines(between: leftEyebrow)
context.strokePath()
}
if !rightEyebrow.isEmpty {
context.addLines(between: rightEyebrow)
context.strokePath()
}
if !nose.isEmpty {
context.addLines(between: nose)
context.strokePath()
}
if !outerLips.isEmpty {
context.addLines(between: outerLips)
context.closePath()
context.strokePath()
}
if !innerLips.isEmpty {
context.addLines(between: innerLips)
context.closePath()
context.strokePath()
}
if !faceContour.isEmpty {
context.addLines(between: faceContour)
context.strokePath()
}
在这里,您要为剩余的face landmarks
添加绘图代码。 注意,leftEyebrow
,rightEyebrow
,nose
和faceContour
不需要关闭他们的路径。 否则,他们看起来很有趣。
现在,再次打开FaceDetectionViewController.swift
。 在updateFaceView(for :)
的末尾,添加以下内容:
if let rightEye = landmark(
points: landmarks.rightEye?.normalizedPoints,
to: result.boundingBox) {
faceView.rightEye = rightEye
}
if let leftEyebrow = landmark(
points: landmarks.leftEyebrow?.normalizedPoints,
to: result.boundingBox) {
faceView.leftEyebrow = leftEyebrow
}
if let rightEyebrow = landmark(
points: landmarks.rightEyebrow?.normalizedPoints,
to: result.boundingBox) {
faceView.rightEyebrow = rightEyebrow
}
if let nose = landmark(
points: landmarks.nose?.normalizedPoints,
to: result.boundingBox) {
faceView.nose = nose
}
if let outerLips = landmark(
points: landmarks.outerLips?.normalizedPoints,
to: result.boundingBox) {
faceView.outerLips = outerLips
}
if let innerLips = landmark(
points: landmarks.innerLips?.normalizedPoints,
to: result.boundingBox) {
faceView.innerLips = innerLips
}
if let faceContour = landmark(
points: landmarks.faceContour?.normalizedPoints,
to: result.boundingBox) {
faceView.faceContour = faceContour
}
使用此代码,您可以将剩余的面部landmarks
添加到FaceView
,就是这样! 你准备建立并运行!
很好的工作!
Using Detected Faces
人脸检测是您最近可能会看到的更多内容。 当你想真正让图像中的人发光时,它对图像处理特别有用。
但是你会做一些比这更酷的事情。 你要从你眼中射出激光!
是时候开始了。
仍然在FaceDetectionViewController.swift
中,在updateFaceView(for:)
的正下方,添加以下方法:
// 1
func updateLaserView(for result: VNFaceObservation) {
// 2
laserView.clear()
// 3
let yaw = result.yaw ?? 0.0
// 4
if yaw == 0.0 {
return
}
// 5
var origins: [CGPoint] = []
// 6
if let point = result.landmarks?.leftPupil?.normalizedPoints.first {
let origin = landmark(point: point, to: result.boundingBox)
origins.append(origin)
}
// 7
if let point = result.landmarks?.rightPupil?.normalizedPoints.first {
let origin = landmark(point: point, to: result.boundingBox)
origins.append(origin)
}
}
这是相当多的代码。这是你用它做的:
- 1) 定义将更新
LaserView
的新方法。这有点像updateFaceView(for :)
。 - 2) 清除
LaserView
。 - 3) 从结果中得到
yaw
。yaw
是一个数字,告诉你你的脸转了多少。如果它是负面的,那么你就是向左看。如果是正值,那么你就是正确的。 - 4) 如果
yaw
为0.0,则返回。如果你直视前方,没有面部激光。 😞 - 5) 创建一个数组来存储激光的原点。
- 6) 根据左瞳孔添加激光原点。
- 7) 根据右瞳孔添加激光原点。
注意:尽管
Vision
框架在检测到的面部标志中包括左右瞳孔,但事实证明这些只是眼睛的几何中心。他们实际上并没有检测到瞳孔。如果你要保持头部不动,但向左或向右看,VNFaceObservation
中返回的瞳孔将不会移动。
好吧,你还没完成那种方法。你已经确定了激光的原点。但是,您仍然需要添加逻辑来确定激光器的聚焦位置。
在新创建的updateLaserView(for :)
的末尾,添加以下代码:
// 1
let avgY = origins.map { $0.y }.reduce(0.0, +) / CGFloat(origins.count)
// 2
let focusY = (avgY < midY) ? 0.75 * maxY : 0.25 * maxY
// 3
let focusX = (yaw.doubleValue < 0.0) ? -100.0 : maxX + 100.0
// 4
let focus = CGPoint(x: focusX, y: focusY)
// 5
for origin in origins {
let laser = Laser(origin: origin, focus: focus)
laserView.add(laser: laser)
}
// 6
DispatchQueue.main.async {
self.laserView.setNeedsDisplay()
}
在这里你:
- 1) 计算激光原点的平均
y
坐标。 - 2) 根据原点的平均
y
确定激光焦点的y坐标。 如果你的瞳孔高于屏幕中间,你就会击落。 否则,你会开枪。 你在viewDidLoad()
中计算了midY
。 - 3) 根据
yaw
计算激光焦点的x坐标。 如果你向左看,你应该向左侧射激光。 - 4) 从两个焦点坐标创建一个
CGPoint
。 - 5) 生成一些激光并将它们添加到
LaserView
。 - 6) 告诉
iPhone
应该重绘LaserView
。
现在你需要从某个地方调用这个方法。detectedFace(request:error:)
是完美的地方! 在该方法中,使用以下内容替换对updateFaceView(for :)
的调用:
if faceViewHidden {
updateLaserView(for: result)
} else {
updateFaceView(for: result)
}
此逻辑根据是否隐藏FaceView
选择要调用的更新方法。
目前,如果你要建造和运行,你只会从你的眼睛射出隐形激光。 虽然这听起来很酷,看到激光不是更好吗?
要解决这个问题,你需要告诉iPhone
如何绘制激光。
打开LaserView.swift
并找到draw(_ :)
方法。 它应该是完全空的。 现在添加以下代码:
// 1
guard let context = UIGraphicsGetCurrentContext() else {
return
}
// 2
context.saveGState()
// 3
for laser in lasers {
// 4
context.addLines(between: [laser.origin, laser.focus])
context.setStrokeColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5)
context.setLineWidth(4.5)
context.strokePath()
// 5
context.addLines(between: [laser.origin, laser.focus])
context.setStrokeColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.8)
context.setLineWidth(3.0)
context.strokePath()
}
// 6
context.restoreGState()
使用此绘图代码,您:
- 1) 获取当前图形上下文。
- 2) 将当前图形状态推入堆栈。
- 3) 循环遍历阵列中的激光。
- 4) 在激光方向画一条较粗的白线。
- 5) 然后在白线上画一条略微更细的红线,使其具有很酷的激光效果。
- 6) 从堆栈弹出当前图形上下文以将其恢复到其原始状态。
而已。 建立和运行时间!
点击屏幕上的任意位置以切换到激光模式。
当然,还有很多其他可以使用的Vision APIs。 现在你已经掌握了如何使用它们的基础知识,你可以探索它们!
后记
本篇主要讲述了基于Vision的人脸识别,感兴趣的给个赞或者关注~~~