美文网首页视频图像多媒体
Vision框架详细解析(二) —— 基于Vision的人脸识别

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

作者: 刀客传奇 | 来源:发表于2019-03-12 20:46 被阅读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框架:

    • 创建面部检测和检测面部标志的请求。
    • 处理这些请求。
    • 在相机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的人脸识别,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

        本文标题:Vision框架详细解析(二) —— 基于Vision的人脸识别

        本文链接:https://www.haomeiwen.com/subject/rjinpqtx.html