视频图像多媒体

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

2019-03-12  本文已影响74人  刀客传奇

版本记录

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

前言

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

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

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

打开入门项目并探索您的内心。

注意:启动器项目使用摄像头,这意味着如果您尝试在模拟器中运行它,您将会崩溃。 确保在实际设备上运行本教程,以便您可以看到您可爱的脸!

目前,Face Lasers应用程序并没有做很多事情。 好吧,它确实向你展示了你的美丽杯子!

底部还有一个标签,上面写着Face。 您可能已经注意到,如果点击屏幕,此标签将更改为Lasers

真令人兴奋! 除了似乎没有任何激光器。 那不太令人兴奋。 别担心 - 在本教程结束时,你会像Super(wo)男人一样从你的眼睛中射出激光!

您还会注意到一些有用的Core Graphics扩展。 您将在整个教程中使用这些来简化代码。


Vision Framework Usage Patterns

所有Vision框架API都使用三种结构:

简单吧?


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)
}

使用此代码,您:

现在你可能想知道:但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()
  }
}

在这种方法中你:

结果的边界框坐标在输入图像的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)
}

在这里你:

你可能很想建立并运行它。 如果你这样做,你会很失望,除了你自己的脸,屏幕上什么都看不到,可悲的是没有激光。

目前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()

使用此代码,您:

唷! 你已经编写了相当长的一段时间。 终于到了!

继续构建并运行您的应用程序。

已经可以检测到人脸了。


What Else Can You Detect?

除了面部检测之外,Vision框架还具有可用于检测各种事物的API。

太棒了吧?

嗯,使用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
}

使用此代码,您:

在该方法下面添加以下内容:

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()
}

在这里你:

是时候建立和运行了!

注意:您已添加代码来注释左眼,但这意味着什么? 使用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)
  }
}

这是相当多的代码。这是你用它做的:

注意:尽管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()
}

在这里你:

现在你需要从某个地方调用这个方法。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()

使用此绘图代码,您:

而已。 建立和运行时间!

点击屏幕上的任意位置以切换到激光模式。

当然,还有很多其他可以使用的Vision APIs。 现在你已经掌握了如何使用它们的基础知识,你可以探索它们!

后记

本篇主要讲述了基于Vision的人脸识别,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读