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

在 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 属性:

var  session  =  AVCaptureSession()

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

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 方法里的这个函数:

override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

}

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

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 底下宣告另一个变量:

var  requests  =  [VNRequest]()

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

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 的函数。为了解决这些错误,建立一个函数像:

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() 方法:

override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

    startTextDetection()

}

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

绘制方框

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

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 上。接下来,就让我们来为每个字母建立方框吧。

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 方法:

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)中插入下面的代码:

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 天

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

推荐阅读更多精彩内容