初試 iOS 11 新框架:Vision Framework 讓文字檢測(cè)變得更容易

在 2017 年的 WWDC 中,Apple 釋出了許多新框架(frameworks)都弹,Vision Framework 便是其中一個(gè)哮翘。使用 Vision Framework 掐禁,你不需要高深的知識(shí)就可以很容易地在你的 App 中實(shí)作出電腦視覺技術(shù)(Vision Techniques)!Vision Framework 可以讓你的 App 執(zhí)行許多強(qiáng)大的功能夺姑,例如識(shí)別人臉范圍及臉部特徵(微笑墩邀、皺眉、左眼眉毛等等)盏浙、條碼檢測(cè)眉睹、分類出圖像中的場(chǎng)景荔茬、物件檢測(cè)及追蹤以及視距檢測(cè)。

或許那些已經(jīng)使用 Swift 開發(fā)程序一段時(shí)間的人會(huì)想知道既然已經(jīng)有了Core ImageAVFoundation竹海,為什么還要推出 Vision 呢慕蔚?如果我們看一下這張?jiān)?WWDC 演講中出現(xiàn)的表格,我們可以看到 Vision 的準(zhǔn)確度(Accuracy)是最好的斋配,同時(shí)也支持較多的平臺(tái)孔飒。不過 Vision 需要較多的處理時(shí)間以及電源消耗。

Difference between AVFoundation and Vision framework

圖片來源: Apple’s WWDC video – Vision Framework: Building on Core ML

在本次的教學(xué)中艰争,我們將會(huì)利用 Vision Framework 來作出文字檢測(cè)的功能坏瞄,并實(shí)作出一個(gè)能夠檢測(cè)出文字的 App ,不論字體甩卓、字型及顏色鸠匀。如下圖所示,Vision Framework 可以識(shí)別出印刷及手寫兩種文字逾柿。

Text Recognition Demo App

編者按:根據(jù)測(cè)試結(jié)果缀棍,Vision Framework 對(duì)中文支持有限。

為了節(jié)省你建置 UI 所花的時(shí)間好專注在學(xué)習(xí) Vision Framework 上机错,你可以下載 Starter Project 作為開始爬范。

請(qǐng)注意你需要 Xcode 9 來完成本次教學(xué),同時(shí)也需要一臺(tái) iOS 11 設(shè)備來測(cè)試弱匪。所有的代碼皆是以 Swift 4 撰寫青瀑。

建立即時(shí)影像

當(dāng)你打開項(xiàng)目時(shí),你可以看到視圖已經(jīng)為你設(shè)定好放在 Storyboard 上了萧诫。接著進(jìn)入 ViewController.swift 狱窘,你會(huì)發(fā)現(xiàn)由一些 outlet 及 function 所構(gòu)成的程序骨架。我們的第一步就是要建立一個(gè)即時(shí)影像來檢測(cè)文字财搁,在 imageView 底下宣告一個(gè) AVCaptureSession 屬性:

var  session  =  AVCaptureSession()

這樣就初始化了一個(gè)可以用來作即時(shí)(real-time)或非即時(shí)(offline)影音獲取的AVCaptureSession物件。而這個(gè)物件在你要對(duì)即時(shí)影像進(jìn)行操作時(shí)就會(huì)用上躬络。接著尖奔,我們需要把這個(gè) session 連接到我們的設(shè)備上。首先把下面的函數(shù)放入 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()

}

如果你曾經(jīng)用過 AVFoundation提茁,你會(huì)發(fā)覺這個(gè)代碼有點(diǎn)熟悉。如果你沒用過馁菜,別擔(dān)心茴扁。我們逐行的將代碼說明一遍。

  1. 我們首先修改 AVCaptureSession 的設(shè)定汪疮。然后我們?cè)O(shè)定 AVMediaType 為影片峭火,因?yàn)槲覀兿M羌磿r(shí)影像毁习,此外它應(yīng)該要一直持續(xù)地運(yùn)作。
  2. 接著卖丸,我們要定義設(shè)備的輸入及輸出纺且。輸入是指相機(jī)所看到的,而輸出則是指應(yīng)該顯示的影像稍浆。我們希望影像顯示為 kCVPixelFormatType_32BGRA 格式载碌。你可以從這里了解更多關(guān)于像素格式的類型。最后衅枫,我們把輸入及輸出加進(jìn)到 AVCaptureSession嫁艇。
  3. 最后,我們把含有影像預(yù)覽的 sublayer 加進(jìn)到 imageView 中弦撩,然后讓 session 開始運(yùn)作步咪。

調(diào)用在 viewWillAppear 方法里的這個(gè)函數(shù):

override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

}

因?yàn)樵?viewWillAppear() 中還沒決定 imageView 的范圍,所以覆寫 viewDidLayoutSubviews()方法來更新圖層的范圍孤钦。

override  func  viewDidLayoutSubviews()  {

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

}

在執(zhí)行之前歧斟,要在 Info.plist 加入一個(gè)條目來說明為何你需要使用到相機(jī)功能。這自 Apple 發(fā)佈 iOS 10 后偏形,都是必須添加的步驟静袖。

text-detection-infoplist

現(xiàn)在即時(shí)影像應(yīng)該會(huì)如預(yù)期般的運(yùn)作。然而俊扭,因?yàn)槲覀冞€沒實(shí)作 Vision Framework队橙,所以還沒有文字檢測(cè)功能。而這就是我們接下來要完成的部份萨惑。

實(shí)作文字檢測(cè)

在我們實(shí)作文字檢測(cè)(Text Detection)之前捐康,我們需要了解 Vision Framework 是如何運(yùn)作的∮拱基本上解总,在你的 App 里實(shí)作 Vision 會(huì)有三個(gè)步驟,分別是:

  • Requests – Requests 是指當(dāng)你要求 Framework 為你檢測(cè)一些東西時(shí)姐仅。
  • Handlers – Handlers 是指當(dāng)你想要 Framework 在 Request 產(chǎn)生后執(zhí)行一些東西或處理這個(gè) Request 時(shí).
  • Observations – Observations 是指你想要用你提供的資料做什么花枫。
request-observation

現(xiàn)在,讓我們從 Request 開始吧掏膏。在初始化的變量 session 底下宣告另一個(gè)變量:

var  requests  =  [VNRequest]()

我們建立了一個(gè)含有一個(gè)通用類別 VNRequest 的陣列劳翰。接著,讓我們?cè)?ViewController 類別里建立一個(gè)函數(shù)來進(jìn)行文字檢測(cè)吧馒疹。

func  startTextDetection()  {

    let  textRequest  =  VNDetectTextRectanglesRequest(completionHandler:  self.detectTextHandler)

    textRequest.reportCharacterBoxes  =  true

    self.requests  =  [textRequest]

}

在這個(gè)函數(shù)里佳簸,我們建立一個(gè) VNDetectTextRectanglesRequest 的常數(shù) textRequest∮北洌基本上它是 VNRequest 的一個(gè)特定型態(tài)生均,只能尋找文字中的矩形听想。當(dāng) Framework 完成了這個(gè) Request,我們希望它調(diào)用 detectTextHandler 函數(shù)疯特。同時(shí)我們也想要知道 Framework 辨識(shí)出了什么哗魂,這也是為什么我們?cè)O(shè)定 reportCharacterBoxes 屬性為 true。最后漓雅,我們?cè)O(shè)定早先建立好的變量requeststextRequest录别。

現(xiàn)在,你應(yīng)該會(huì)得到一些錯(cuò)誤訊息邻吞。這是因?yàn)槲覀冞€沒定義應(yīng)該用來處理 Request 的函數(shù)组题。為了解決這些錯(cuò)誤,建立一個(gè)函數(shù)像:

func  detectTextHandler(request:  VNRequest,  error:  Error?)  {

    guard let  observations  =  request.results  else  {

        print("no result")

        return

    }

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

}

在上面的代碼抱冷,我們首先定義一個(gè)含有所有 VNDetectTextRectanglesRequest 結(jié)果的常數(shù) observations崔列。接著,我們定義另一個(gè)常數(shù) result旺遮,它將遍歷所有 Request 的結(jié)果然后轉(zhuǎn)換為 VNTextObservation 型態(tài)赵讯。

現(xiàn)在,更新 viewWillAppear() 方法:

override  func  viewWillAppear(_  animated:  Bool)  {

    startLiveVideo()

    startTextDetection()

}

如果你現(xiàn)在執(zhí)行你的 App耿眉,你不會(huì)看到任何的不同边翼。這是因?yàn)殡m然我們告訴 VNDetectTextRectanglesRequest 要回報(bào)字母方框,但是沒有告訴它該如何回報(bào)鸣剪。這將是我們接下來要完成的部份组底。

繪制方框

在我們的 App 中,我們會(huì)讓 Framework 繪制兩個(gè)方框:一個(gè)所檢測(cè)的每個(gè)字母筐骇,另一個(gè)則是整個(gè)單字债鸡。讓我們就從制作繪制每個(gè)單字的方框開始吧!

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)

}

我們一開始先在函數(shù)里定義一個(gè)常數(shù) boxes铛纬,他是由 Request 所找到的所有 characterBoxes 的組合厌均。然后,我們定義一些在視圖上的坐標(biāo)點(diǎn)來幫助我們定位方框告唆。最后莫秆,我們建立一個(gè)有給定范圍約束的 CALayer 并將它應(yīng)用在我們的 imageView 上。接下來悔详,就讓我們來為每個(gè)字母建立方框吧。

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 來定義約束條件茄螃,讓我們更容易地勾勒出方框。現(xiàn)在连锯,我們已經(jīng)設(shè)置好所有的函數(shù)了归苍。最后一步便是要連接所有的東西用狱。

連接程序

有兩個(gè)主要的部分需要連接。第一個(gè)是處理 Request 的函數(shù)拼弃。我們先來完成個(gè)這個(gè)吧夏伊。像這樣更新 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)

                }

            }

        }

    }

}

我們從讓代碼非同步執(zhí)行開始。首先吻氧,我們移除 imageView 最底層的圖層(如果你有注意到溺忧,我們先前添加了許多圖層到 imageView 中。)接下來盯孙,我們從 VNTextObservation 的結(jié)果里確認(rèn)是否有區(qū)域范圍存在÷成現(xiàn)在,我們調(diào)用沿著范圍(或者說單字)繪制方框的函數(shù)振惰。然后我們確認(rèn)是否有字符方框在這個(gè)范圍里歌溉。如果有,我們調(diào)用方法來沿著字母繪上方框骑晶。

現(xiàn)在痛垛,連接所有東西的最后一個(gè)步驟就是以即時(shí)影像來執(zhí)行我們的 Vision Framework 代碼。我們需要做的是錄制影像并將其轉(zhuǎn)換為 CMSampleBuffer桶蛔。在 ViewController.swift 的擴(kuò)展(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)

    }

}

在那邊打住一下匙头。這是我們代碼的最后部分了。這個(gè)擴(kuò)展調(diào)用了 AVCaptureVideoDataOutputSampleBufferDelegate 協(xié)定羽圃∏海基本上這個(gè)函數(shù)所做的就是它確認(rèn) CMSampleBuffer 是否存在以及提供一個(gè) AVCaptureOutput。接著朽寞,我們建立一個(gè) VNImageOption型態(tài)的字典(Dictionary)變量 requestOptions识窿。VNImageOption 是一個(gè)結(jié)構(gòu)(struct)類型,它可以從相機(jī)中保持著資料及屬性脑融。最后我們建立一個(gè) VNImageRequestHandler 物件并執(zhí)行我們?cè)缦冉⒌奈淖?Request喻频。

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

text-detection-example

小結(jié)

Well甥温,接下來是個(gè)大工程呢!試著用不同字型妓布、大小姻蚓、字體、粗細(xì)等等來測(cè)試 App 吧匣沼≌玻看看是否你可以擴(kuò)展這個(gè) App 。你可以在下面的回應(yīng)中貼上你如何擴(kuò)展這個(gè)項(xiàng)目。你也可以結(jié)合 Vision Framework 及 Core ML加叁。想要更多關(guān)于 Core ML 的資訊倦沧,可以參閱先前撰寫的 Core ML 介紹教學(xué)。

你可以參考放在 GitHub 上的 完整項(xiàng)目它匕。

更多關(guān)于 Vision Framework 的細(xì)節(jié)可以參考 Vision Framework 官方文件展融。你也可以參考 WWDC 關(guān)于 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

簡(jiǎn)寶玉寫作群日更打卡第 28 天

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市豫柬,隨后出現(xiàn)的幾起案子告希,更是在濱河造成了極大的恐慌,老刑警劉巖轮傍,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暂雹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡创夜,警方通過查閱死者的電腦和手機(jī)杭跪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驰吓,“玉大人涧尿,你說我怎么就攤上這事∶史。” “怎么了姑廉?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)翁涤。 經(jīng)常有香客問我桥言,道長(zhǎng),這世上最難降的妖魔是什么葵礼? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任号阿,我火速辦了婚禮,結(jié)果婚禮上鸳粉,老公的妹妹穿的比我還像新娘扔涧。我一直安慰自己,他們只是感情好届谈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布枯夜。 她就那樣靜靜地躺著,像睡著了一般艰山。 火紅的嫁衣襯著肌膚如雪湖雹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天曙搬,我揣著相機(jī)與錄音摔吏,去河邊找鬼汤踏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舔腾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搂擦,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼稳诚,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了瀑踢?” 一聲冷哼從身側(cè)響起扳还,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橱夭,沒想到半個(gè)月后氨距,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡棘劣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年俏让,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茬暇。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡首昔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出糙俗,到底是詐尸還是另有隱情勒奇,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布巧骚,位于F島的核電站赊颠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏劈彪。R本人自食惡果不足惜竣蹦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粉臊。 院中可真熱鬧草添,春花似錦、人聲如沸扼仲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屠凶。三九已至驰后,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間矗愧,已是汗流浹背灶芝。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工郑原, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人夜涕。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓犯犁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親女器。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酸役,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容