Vision框架详细解析(二) —— 基于Vision的人脸识别(一)

版本记录

版本号 时间
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检测单个文本字符周围的边界框。但请注意,这不识别字符是什么,它只检测它们。
  • Horizo​​n:使用VNDetectHorizo​​nRequest,您可以确定图像中水平线的角度。
  • Barcodes:您可以使用VNDetectBarcodesRequest检测和识别多种条形码。请在此处here查看完整列表。
  • Objects:通过将Vision框架与CoreML相结合,您可以使用VNCoreMLRequest检测和分类特定对象。
  • Image alignment:使用VNTranslationalImageRegistrationRequestVNHomographicImageRegistrationRequest,您可以对齐具有重叠内容的两个图像。

太棒了吧?

嗯,使用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的规范化点转换为与预览图层一起使用的坐标。 如果一切顺利,您将这些转换的点分配给FaceViewleftEye属性。

其余看起来很熟悉,因为你已经在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是寻找leftright的词,并猜测它们的意思。 每次都不一样!

真棒! 如果你试图睁大眼睛或闭上眼睛,你应该看到眼睛略微改变形状,尽管没有那么多。

这是一个了不起的里程碑。 你现在可能想要快速休息一下,因为你将一举添加所有其他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添加绘图代码。 注意,leftEyebrowrightEyebrownosefaceContour不需要关闭他们的路径。 否则,他们看起来很有趣。

现在,再次打开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) 从结果中得到yawyaw是一个数字,告诉你你的脸转了多少。如果它是负面的,那么你就是向左看。如果是正值,那么你就是正确的。
  • 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的人脸识别,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容