前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初试 iOS 11 新框架:Vision Framework 让文字检测变得更容易

初试 iOS 11 新框架:Vision Framework 让文字检测变得更容易

作者头像
iOSDevLog
发布2018-06-21 17:22:05
2.5K0
发布2018-06-21 17:22:05
举报
文章被收录于专栏:iOSDevLog

在 2017 年的 WWDC 中,Apple 释出了许多新框架(frameworks),Vision Framework 便是其中一个。使用 Vision Framework ,你不需要高深的知识就可以很容易地在你的 App 中实作出电脑视觉技术(Vision Techniques)!Vision Framework 可以让你的 App 执行许多强大的功能,例如识别人脸范围及脸部特徵(微笑、皱眉、左眼眉毛等等)、条码检测、分类出图像中的场景、物件检测及追踪以及视距检测。

或许那些已经使用 Swift 开发程序一段时间的人会想知道既然已经有了Core ImageAVFoundation,为什么还要推出 Vision 呢?如果我们看一下这张在 WWDC 演讲中出现的表格,我们可以看到 Vision 的准确度(Accuracy)是最好的,同时也支持较多的平台。不过 Vision 需要较多的处理时间以及电源消耗。

Difference between AVFoundation and Vision framework

图片来源: Apple’s WWDC video – Vision Framework: Building on Core ML

在本次的教学中,我们将会利用 Vision Framework 来作出文字检测的功能,并实作出一个能够检测出文字的 App ,不论字体、字型及颜色。如下图所示,Vision Framework 可以识别出印刷及手写两种文字。

Text Recognition Demo App

编者按:根据测试结果,Vision Framework 对中文支持有限。

为了节省你建置 UI 所花的时间好专注在学习 Vision Framework 上,你可以下载 Starter Project 作为开始。

请注意你需要 Xcode 9 来完成本次教学,同时也需要一台 iOS 11 设备来测试。所有的代码皆是以 Swift 4 撰写。

建立即时影像

当你打开项目时,你可以看到视图已经为你设定好放在 Storyboard 上了。接着进入 ViewController.swift ,你会发现由一些 outlet 及 function 所构成的程序骨架。我们的第一步就是要建立一个即时影像来检测文字,在 imageView 底下宣告一个 AVCaptureSession 属性:

代码语言:javascript
复制
var  session  =  AVCaptureSession()

这样就初始化了一个可以用来作即时(real-time)或非即时(offline)影音获取的AVCaptureSession物件。而这个物件在你要对即时影像进行操作时就会用上。接着,我们需要把这个 session 连接到我们的设备上。首先把下面的函数放入 ViewController.swift 吧。

代码语言:javascript
复制
func  startLiveVideo()  {

    //1

    session.sessionPreset  =  AVCaptureSession.Preset.photo

    let  captureDevice  =  AVCaptureDevice.default(for:  AVMediaType.video)

    //2

    let  deviceInput  =  try!  AVCaptureDeviceInput(device:  captureDevice!)

    let  deviceOutput  =  AVCaptureVideoDataOutput()

    deviceOutput.videoSettings  =  [kCVPixelBufferPixelFormatTypeKey as  String:  Int(kCVPixelFormatType_32BGRA)]

    deviceOutput.setSampleBufferDelegate(self,  queue:  DispatchQueue.global(qos:  DispatchQoS.QoSClass.default))

    session.addInput(deviceInput)

    session.addOutput(deviceOutput)

    //3

    let  imageLayer  =  AVCaptureVideoPreviewLayer(session:  session)

    imageLayer.frame  =  imageView.bounds

    imageView.layer.addSublayer(imageLayer)

    session.startRunning()

}

如果你曾经用过 AVFoundation,你会发觉这个代码有点熟悉。如果你没用过,别担心。我们逐行的将代码说明一遍。

  1. 我们首先修改 AVCaptureSession 的设定。然后我们设定 AVMediaType 为影片,因为我们希望是即时影像,此外它应该要一直持续地运作。
  2. 接着,我们要定义设备的输入及输出。输入是指相机所看到的,而输出则是指应该显示的影像。我们希望影像显示为 kCVPixelFormatType_32BGRA 格式。你可以从这里了解更多关于像素格式的类型。最后,我们把输入及输出加进到 AVCaptureSession
  3. 最后,我们把含有影像预览的 sublayer 加进到 imageView 中,然后让 session 开始运作。

调用在 viewWillAppear 方法里的这个函数:

代码语言:javascript
复制
override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

}

因为在 viewWillAppear() 中还没决定 imageView 的范围,所以覆写 viewDidLayoutSubviews()方法来更新图层的范围。

代码语言:javascript
复制
override  func  viewDidLayoutSubviews()  {

    imageView.layer.sublayers?[0].frame  =  imageView.bounds

}

在执行之前,要在 Info.plist 加入一个条目来说明为何你需要使用到相机功能。这自 Apple 发佈 iOS 10 后,都是必须添加的步骤。

text-detection-infoplist

现在即时影像应该会如预期般的运作。然而,因为我们还没实作 Vision Framework,所以还没有文字检测功能。而这就是我们接下来要完成的部份。

实作文字检测

在我们实作文字检测(Text Detection)之前,我们需要了解 Vision Framework 是如何运作的。基本上,在你的 App 里实作 Vision 会有三个步骤,分别是:

  • Requests – Requests 是指当你要求 Framework 为你检测一些东西时。
  • Handlers – Handlers 是指当你想要 Framework 在 Request 产生后执行一些东西或处理这个 Request 时.
  • Observations – Observations 是指你想要用你提供的资料做什么。

request-observation

现在,让我们从 Request 开始吧。在初始化的变量 session 底下宣告另一个变量:

代码语言:javascript
复制
var  requests  =  [VNRequest]()

我们建立了一个含有一个通用类别 VNRequest 的阵列。接着,让我们在 ViewController 类别里建立一个函数来进行文字检测吧。

代码语言:javascript
复制
func  startTextDetection()  {

    let  textRequest  =  VNDetectTextRectanglesRequest(completionHandler:  self.detectTextHandler)

    textRequest.reportCharacterBoxes  =  true

    self.requests  =  [textRequest]

}

在这个函数里,我们建立一个 VNDetectTextRectanglesRequest 的常数 textRequest。基本上它是 VNRequest 的一个特定型态,只能寻找文字中的矩形。当 Framework 完成了这个 Request,我们希望它调用 detectTextHandler 函数。同时我们也想要知道 Framework 辨识出了什么,这也是为什么我们设定 reportCharacterBoxes 属性为 true。最后,我们设定早先建立好的变量requeststextRequest

现在,你应该会得到一些错误讯息。这是因为我们还没定义应该用来处理 Request 的函数。为了解决这些错误,建立一个函数像:

代码语言:javascript
复制
func  detectTextHandler(request:  VNRequest,  error:  Error?)  {

    guard let  observations  =  request.results  else  {

        print("no result")

        return

    }

    let  result  =  observations.map({$0  as?  VNTextObservation})

}

在上面的代码,我们首先定义一个含有所有 VNDetectTextRectanglesRequest 结果的常数 observations。接着,我们定义另一个常数 result,它将遍历所有 Request 的结果然后转换为 VNTextObservation 型态。

现在,更新 viewWillAppear() 方法:

代码语言:javascript
复制
override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

    startTextDetection()

}

如果你现在执行你的 App,你不会看到任何的不同。这是因为虽然我们告诉 VNDetectTextRectanglesRequest 要回报字母方框,但是没有告诉它该如何回报。这将是我们接下来要完成的部份。

绘制方框

在我们的 App 中,我们会让 Framework 绘制两个方框:一个所检测的每个字母,另一个则是整个单字。让我们就从制作绘制每个单字的方框开始吧!

代码语言:javascript
复制
func  highlightWord(box:  VNTextObservation)  {

    guard let  boxes  =  box.characterBoxes  else  {

        return

    }

    var  maxX:  CGFloat  =  9999.0

    var  minX:  CGFloat  =  0.0

    var  maxY:  CGFloat  =  9999.0

    var  minY:  CGFloat  =  0.0

    for  char  in  boxes  {

        if  char.bottomLeft.x  <  maxX  {

            maxX  =  char.bottomLeft.x

        }

        if  char.bottomRight.x  >  minX  {

            minX  =  char.bottomRight.x

        }

        if  char.bottomRight.y  <  maxY  {

            maxY  =  char.bottomRight.y

        }

        if  char.topRight.y  >  minY  {

            minY  =  char.topRight.y

        }

    }

    let  xCord  =  maxX  *  imageView.frame.size.width

    let  yCord  =  (1  -  minY)  *  imageView.frame.size.height

    let  width  =  (minX  -  maxX)  *  imageView.frame.size.width

    let  height  =  (minY  -  maxY)  *  imageView.frame.size.height

    let  outline  =  CALayer()

    outline.frame  =  CGRect(x:  xCord,  y:  yCord,  width:  width,  height:  height)

    outline.borderWidth  =  2.0

    outline.borderColor  =  UIColor.red.cgColor

    imageView.layer.addSublayer(outline)

}

我们一开始先在函数里定义一个常数 boxes,他是由 Request 所找到的所有 characterBoxes 的组合。然后,我们定义一些在视图上的坐标点来帮助我们定位方框。最后,我们建立一个有给定范围约束的 CALayer 并将它应用在我们的 imageView 上。接下来,就让我们来为每个字母建立方框吧。

代码语言:javascript
复制
func  highlightLetters(box:  VNRectangleObservation)  {

    let  xCord  =  box.topLeft.x  *  imageView.frame.size.width

    let  yCord  =  (1  -  box.topLeft.y)  *  imageView.frame.size.height

    let  width  =  (box.topRight.x  -  box.bottomLeft.x)  *  imageView.frame.size.width

    let  height  =  (box.topLeft.y  -  box.bottomLeft.y)  *  imageView.frame.size.height

    let  outline  =  CALayer()

    outline.frame  =  CGRect(x:  xCord,  y:  yCord,  width:  width,  height:  height)

    outline.borderWidth  =  1.0

    outline.borderColor  =  UIColor.blue.cgColor

    imageView.layer.addSublayer(outline)

}

跟我们前面所撰写的代码相似,我们使用 VNRectangleObservation 来定义约束条件,让我们更容易地勾勒出方框。现在,我们已经设置好所有的函数了。最后一步便是要连接所有的东西。

连接程序

有两个主要的部分需要连接。第一个是处理 Request 的函数。我们先来完成个这个吧。像这样更新 detectTextHandler 方法:

代码语言:javascript
复制
func  detectTextHandler(request:  VNRequest,  error:  Error?)  {

    guard let  observations  =  request.results  else  {

        print("no result")

        return

    }

    let  result  =  observations.map({$0  as?  VNTextObservation})

    DispatchQueue.main.async()  {

        self.imageView.layer.sublayers?.removeSubrange(1...)

        for  region  in  result  {

            guard let  rg  =  region  else  {

                continue

            }

            self.highlightWord(box:  rg)

            if  let  boxes  =  region?.characterBoxes  {

                for  characterBox  in  boxes  {

                    self.highlightLetters(box:  characterBox)

                }

            }

        }

    }

}

我们从让代码非同步执行开始。首先,我们移除 imageView 最底层的图层(如果你有注意到,我们先前添加了许多图层到 imageView 中。)接下来,我们从 VNTextObservation 的结果里确认是否有区域范围存在。现在,我们调用沿着范围(或者说单字)绘制方框的函数。然后我们确认是否有字符方框在这个范围里。如果有,我们调用方法来沿着字母绘上方框。

现在,连接所有东西的最后一个步骤就是以即时影像来执行我们的 Vision Framework 代码。我们需要做的是录制影像并将其转换为 CMSampleBuffer。在 ViewController.swift 的扩展(Extension)中插入下面的代码:

代码语言:javascript
复制
func  captureOutput(_  output:  AVCaptureOutput,  didOutput sampleBuffer:  CMSampleBuffer,  from connection:  AVCaptureConnection)  {

    guard let  pixelBuffer  =  CMSampleBufferGetImageBuffer(sampleBuffer)  else  {

        return

    }

    var  requestOptions:[VNImageOption  :  Any]  =  [:]

    if  let  camData  =  CMGetAttachment(sampleBuffer,  kCMSampleBufferAttachmentKey_CameraIntrinsicMatrix,  nil)  {

        requestOptions  =  [.cameraIntrinsics:camData]

    }

    let  imageRequestHandler  =  VNImageRequestHandler(cvPixelBuffer:  pixelBuffer,  orientation:  6,  options:  requestOptions)

    do  {

        try  imageRequestHandler.perform(self.requests)

    }  catch  {

        print(error)

    }

}

在那边打住一下。这是我们代码的最后部分了。这个扩展调用了 AVCaptureVideoDataOutputSampleBufferDelegate 协定。基本上这个函数所做的就是它确认 CMSampleBuffer 是否存在以及提供一个 AVCaptureOutput。接着,我们建立一个 VNImageOption型态的字典(Dictionary)变量 requestOptionsVNImageOption 是一个结构(struct)类型,它可以从相机中保持着资料及属性。最后我们建立一个 VNImageRequestHandler 物件并执行我们早先建立的文字 Request。

Build 及 Run 你的 App,看看你得到什么!

text-detection-example

小结

Well,接下来是个大工程呢!试着用不同字型、大小、字体、粗细等等来测试 App 吧。看看是否你可以扩展这个 App 。你可以在下面的回应中贴上你如何扩展这个项目。你也可以结合 Vision Framework 及 Core ML。想要更多关于 Core ML 的资讯,可以参阅先前撰写的 Core ML 介绍教学。

你可以参考放在 GitHub 上的 完整项目

更多关于 Vision Framework 的细节可以参考 Vision Framework 官方文件。你也可以参考 WWDC 关于 Vision Framework 的演讲:

Vision Framework: Building on Core ML

Advances in Core Image: Filters, Metal, Vision, and More

原文Using Vision Framework for Text Detection in iOS 11

简宝玉写作群日更打卡第 28 天

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.05.18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 建立即时影像
  • 实作文字检测
  • 绘制方框
  • 连接程序
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档