基于Firebase平臺開發(fā)(一) —— 基于ML Kit的iOS圖片中文字的識別(一)

版本記錄

版本號 時間
V1.0 2019.02.01 星期五

前言

Firebase是一家實時后端數(shù)據(jù)庫創(chuàng)業(yè)公司,它能幫助開發(fā)者很快的寫出Web端和移動端的應用。自2014年10月Google收購Firebase以來,用戶可以在更方便地使用Firebase的同時,結(jié)合Google的云服務。Firebase能讓你的App從零到一。也就是說它可以幫助手機以及網(wǎng)頁應用的開發(fā)者輕松構(gòu)建App。通過Firebase背后負載的框架就可以簡單地開發(fā)一個App,無需服務器以及基礎(chǔ)設(shè)施。接下來幾篇我們就一起看一下基于Firebase平臺的開發(fā)。

Firebase提供的服務

首先我們看一下Firebase目前提供的服務:

這里看一下ML Kit關(guān)于機器學習的部分還是BETA


開始

首先看下寫作環(huán)境

Swift 4.2, iOS 12, Xcode 10

在這個ML Kit教程中,您將學習如何利用GoogleML Kit來檢測和識別文本。

幾年前,有兩種類型的機器學習(ML)開發(fā)人員:高級開發(fā)人員和其他人。底層的ML水平可能很難,這是很多數(shù)學,它使用邏輯回歸(logistic regression),稀疏性(sparsity)和神經(jīng)網(wǎng)絡(neural nets)等詞語。但它并不一定要那么難。

您也可以成為ML開發(fā)人員! ML的核心很簡單。有了它,您可以通過訓練軟件模型來識別模式而不是硬編碼每種情況和您能想到各種case來解決問題。但是,開始這可能是令人生畏的,這是您可以依賴現(xiàn)有工具的地方。

1. Machine Learning and Tooling

就像iOS開發(fā)一樣,ML就是工具。你不會建立自己的UITableView,或者至少你不應該,你會使用一個框架,比如UIKit

它與ML的方式相同。 ML擁有蓬勃發(fā)展的工具生態(tài)系統(tǒng)。例如,Tensorflow簡化了訓練和運行模型。 TensorFlow LiteiOSAndroid設(shè)備提供模型支持。

這些工具中的每一個都需要一些ML的經(jīng)驗。如果您不是ML專家但想要解決特定問題怎么辦?對于這些情況,有ML Kit


ML Kit

ML Kit是一款移動SDK,可將GoogleML專業(yè)知識帶入您的應用。 ML Kit的API有兩個主要部分,用于常見用例和自定義模型(common use cases and custom models),無論經(jīng)驗如何都易于使用。

當前的API支持:

這些用例中的每一個都帶有一個預先訓練的模型,該模型包含在一個易于使用的API中。 是時候開始建設(shè)了!

在本教程中,您將構(gòu)建一個名為Extractor的應用程序。 你有沒有拍下一張標志或海報的圖片來寫下文字內(nèi)容? 如果一個應用程序可以將文本從標志上剝離并保存給您,隨時可以使用,那就太棒了。 例如,您可以拍攝尋址信封的照片并保存地址。 這正是你要對這個項目做的! 做好準備!

首先,打開本教程的項目材料,該項目使用CocoaPods來管理依賴項。


Setting Up ML Kit

每個ML Kit API都有一組不同的CocoaPods依賴項。 這很有用,因為您只需要捆綁應用程序所需的依賴項。 例如,如果您沒有識別地標,則在您的應用中不需要該模型。 在Extractor中,您將使用Text Recognition API

如果您要將Text Recognition API添加到您的應用程序,那么您需要將以下行添加到您的Podfile中,但您不必為啟動項目執(zhí)行此操作,因為Podfile中有已經(jīng)寫好 - 您可以檢查。

pod 'Firebase/Core' => '5.5.0'
pod 'Firebase/MLVision' => '5.5.0'
pod 'Firebase/MLVisionTextModel' => '5.5.0'

您必須打開終端應用程序,切換到項目文件夾并運行以下命令來安裝項目中使用的CocoaPods

pod install

安裝CocoaPods后,在Xcode中打開Extractor.xcworkspace

注意:您可能會注意到項目文件夾包含名為Extractor.xcodeproj的項目文件和名為Extractor.xcworkspace的工作區(qū)文件,該文件是您在Xcode中打開的文件。 不要打開項目文件,因為它不包含編譯應用程序所需的其他CocoaPods項目。

該項目包含以下重要文件:

  • ViewController.swift:此項目中唯一的控制器。
  • + UIImage.swift:用于修復圖像方向的UIImage extension

Setting Up a Firebase Account

首先要創(chuàng)建一個賬戶,這個會在后面單獨分出來一篇進行詳細介紹。

一般的想法是:

  • 1) 創(chuàng)建一個帳戶。
  • 2) 創(chuàng)建一個項目。
  • 3) 將iOS應用添加到項目中。
  • 4) 將GoogleService-Info.plist拖到您的項目中。
  • 5) 在AppDelegate中初始化Firebase

這是一個簡單的過程,但如果您遇到任何障礙,后面我會單獨進行講解說明。

注意:您需要設(shè)置Firebase并為最終和初始項目創(chuàng)建自己的GoogleService-Info.plist

構(gòu)建并運行應用程序,您將看到它看起來像這樣:

除了允許您通過右上角的操作按鈕共享硬編碼文本之外,它不會執(zhí)行任何操作。 您將使用ML Kit將此應用程序變?yōu)楝F(xiàn)實。


Detecting Basic Text

準備好第一次文本檢測! 您可以首先向用戶演示如何使用該應用程序。

一個很好的演示是在應用程序首次啟動時掃描示例圖像。 在資源文件夾中包含了一個名為scanning-text的圖像,該圖像當前是視圖控制器的UIImageView中顯示的默認圖像。 您將使用它作為示例圖像。

但首先,您需要一個文本檢測器來檢測圖像中的文本。

1. Creating a Text Detector

創(chuàng)建名為ScaledElementProcessor.swift的文件并添加以下代碼:

import Firebase

class ScaledElementProcessor {

}

很好! 你們都完成了! 開個玩笑。 在類中創(chuàng)建text-detector屬性:

let vision = Vision.vision()
var textRecognizer: VisionTextRecognizer!
  
init() {
  textRecognizer = vision.onDeviceTextRecognizer()
}

textRecognizer是可用于檢測圖像中文本的主要對象。 您將使用它來識別UIImageView當前顯示的圖像中包含的文本。 將以下檢測方法添加到類中:

func process(in imageView: UIImageView, 
  callback: @escaping (_ text: String) -> Void) {
  // 1
  guard let image = imageView.image else { return }
  // 2
  let visionImage = VisionImage(image: image)
  // 3
  textRecognizer.process(visionImage) { result, error in
    // 4
    guard 
      error == nil, 
      let result = result, 
      !result.text.isEmpty 
      else {
        callback("")
        return
    }
    // 5
    callback(result.text)
  }
}

花一點時間來理解這段代碼:

  • 1) 在這里,您檢查imageView是否實際包含圖像。 如果沒有,只需返回。 但是,理想情況下,您可以拋出或提供優(yōu)雅的失敗提示。
  • 2) ML Kit使用特殊的VisionImage類型。 它很有用,因為它可以包含ML Kit處理圖像的特定元數(shù)據(jù),例如圖像的方向。
  • 3) textRecognizer有一個接收VisionImageprocess方法,它以傳遞給閉包的參數(shù)的形式返回一個文本結(jié)果數(shù)組。
  • 4) 結(jié)果可能是nil,在這種情況下,您將要為回調(diào)返回一個空字符串。
  • 5) 最后,觸發(fā)回調(diào)以中繼識別的文本。

2. Using the Text Detector

打開ViewController.swift,在類主體頂部的outlets之后,添加一個ScaledElementProcessor實例作為屬性:

let processor = ScaledElementProcessor()

然后,在viewDidLoad()的底部添加以下代碼,以在UITextView中顯示檢測到的文本:

processor.process(in: imageView) { text in
  self.scannedText = text
}

這個小block調(diào)用process(in:),傳遞主imageView并將識別的文本分配給回調(diào)中的scansText屬性。

運行該應用程序,您應該在圖像正下方看到以下文本:

Your
SCanned
text
will
appear
here 

您可能需要滾動文本視圖以顯示最后幾行。

注意掃描的“S”“C”是大寫的。 有時,使用特定字體時,可能會出現(xiàn)錯誤的情況。 這就是為什么文本顯示在UITextView中的原因,因此用戶可以手動編輯以修復檢測錯誤。

3. Understanding the Classes

注意:您不必復制本節(jié)中的代碼,它只是有助于解釋概念。您將在下一部分中向應用添加代碼。

VisionText

您是否注意到ScaledElementProcessor中的textRecognizer.process(in :)的回調(diào)在result參數(shù)中返回了一個對象而不是普通的文本?這是VisionText的一個實例,它包含許多有用的信息,例如識別的文本。但是你想做的不僅僅是獲取文本。描繪出每個識別的文本元素的每個frame不是很酷嗎?

ML Kit以類似于樹的結(jié)構(gòu)提供結(jié)果。您需要遍歷葉元素以獲取包含已識別文本的frame的位置和大小。如果對樹結(jié)構(gòu)的引用讓您感覺到困難,請不要太擔心。以下部分應闡明正在發(fā)生的事情。

VisionTextBlock

使用已識別的文本時,您可以從VisionText對象開始 - 這是一個對象(稱為樹),它可以包含多個文本塊(如樹中的分支)。您遍歷每個分支,這是塊(blocks)數(shù)組中的VisionTextBlock對象,如下所示:

for block in result.blocks {

}

VisionTextElement

VisionTextBlock只是一個對象,包含一系列文本(如樹枝上的葉子),每個文本都由一個VisionTextElement實例表示。 這個對象的嵌套允許您查看已識別文本的層次結(jié)構(gòu)。

循環(huán)遍歷每個對象如下所示:

for block in result.blocks {
  for line in block.lines {
    for element in line.elements {

    }
  }
}

此層次結(jié)構(gòu)中的所有對象都包含文本所在的frame。 但是,每個對象包含不同級別的粒度(granularity)。 塊可以包含多個行,一行可以包含多個元素,并且元素可以包含多個符號。

在本教程中,您將使用元素(elements)作為粒度級別。 元素通常對應于單詞。 這將允許您繪制每個單詞并向用戶顯示每個單詞在圖像中的位置。

最后一個循環(huán)遍歷文本塊的每一行中的元素。 這些元素包含frame,一個簡單的CGRect。 使用此frame,您可以在圖像上的單詞周圍繪制邊框。


Highlighting the Text Frames

1. Detecting Frames

要在圖像上繪制,您需要使用文本元素的frame創(chuàng)建CAShapeLayer。 打開ScaledElementProcessor.swift并將以下struct添加到文件的頂部:

struct ScaledElement {
  let frame: CGRect
  let shapeLayer: CALayer
}

這個struct是便利實現(xiàn)。 它可以更輕松地將frame和CAShapeLayer分組到控制器。 現(xiàn)在,您需要一個輔助方法來從元素的frame創(chuàng)建CAShapeLayer

將以下代碼添加到ScaledElementProcessor的末尾:

private func createShapeLayer(frame: CGRect) -> CAShapeLayer {
  // 1
  let bpath = UIBezierPath(rect: frame)
  let shapeLayer = CAShapeLayer()
  shapeLayer.path = bpath.cgPath
  // 2
  shapeLayer.strokeColor = Constants.lineColor
  shapeLayer.fillColor = Constants.fillColor
  shapeLayer.lineWidth = Constants.lineWidth
  return shapeLayer
}

// MARK: - private
  
// 3
private enum Constants {
  static let lineWidth: CGFloat = 3.0
  static let lineColor = UIColor.yellow.cgColor
  static let fillColor = UIColor.clear.cgColor
}

這是代碼的作用:

  • 1) CAShapeLayer沒有接收CGRect的初始化程序。 因此,您使用CGRect構(gòu)造UIBezierPath并將形狀圖層的path設(shè)置為UIBezierPath
  • 2) 顏色和寬度的可視屬性通過常量枚舉設(shè)置。
  • 3) 這個枚舉有助于保持著色和寬度一致。

現(xiàn)在,用以下代碼替換process(in:callback :)

// 1
func process(
  in imageView: UIImageView, 
  callback: @escaping (_ text: String, _ scaledElements: [ScaledElement]) -> Void
  ) {
  guard let image = imageView.image else { return }
  let visionImage = VisionImage(image: image)
    
  textRecognizer.process(visionImage) { result, error in
    guard 
      error == nil, 
      let result = result, 
      !result.text.isEmpty 
      else {
        callback("", [])
        return
    }
  
    // 2
    var scaledElements: [ScaledElement] = []
    // 3
    for block in result.blocks {
      for line in block.lines {
        for element in line.elements {
          // 4
          let shapeLayer = self.createShapeLayer(frame: element.frame)
          let scaledElement = 
            ScaledElement(frame: element.frame, shapeLayer: shapeLayer)

          // 5
          scaledElements.append(scaledElement)
        }
      }
    }
      
    callback(result.text, scaledElements)
  }
}

下面進行詳細說明:

  • 1) 除了識別的文本之外,回調(diào)現(xiàn)在還會獲取一系列ScaledElement實例。
  • 2) scaledElements用作frameshape layer的集合。
  • 3) 正如上面所概述的,代碼使用for循環(huán)來獲取每個元素的frame。
  • 4) 最里面的for循環(huán)從元素的frame創(chuàng)建shape layer,然后用于構(gòu)造新的ScaledElement實例。
  • 5) 將新創(chuàng)建的實例添加到scaledElements

2. Drawing

上面的代碼是把你的鉛筆放在一起。 現(xiàn)在,是時候畫了! 打開ViewController.swift,在viewDidLoad()中,用以下代碼替換對process(in :)的調(diào)用:

processor.process(in: imageView) { text, elements in
  self.scannedText = text
  elements.forEach() { feature in
    self.frameSublayer.addSublayer(feature.shapeLayer)
  }
}

ViewController有一個frameSublayer屬性,附加到imageView。 在這里,您將每個元素的shape layer添加到子圖層,以便iOS將自動在圖像上繪制shape

構(gòu)建并運行。 看看你的藝術(shù)作品!

哦。 那是什么? 看起來你更像畢加索而不是莫奈。 這里發(fā)生了什么? 好吧,現(xiàn)在可能是談論scale的時候了。


Understanding Image Scaling

默認的scanning-text.png圖像為654×999 (width x height);但是,UIImageView具有“Aspect Fit”“Content Mode”,可以在視圖中將圖像縮放到375×369ML Kit接收圖像的實際大小,并根據(jù)該大小返回元素frame。 然后根據(jù)縮放的大小繪制來自實際大小的frame,這會產(chǎn)生令人困惑的結(jié)果。

在上圖中,請注意縮放大小和實際大小之間的差異。 您可以看到frame與實際大小匹配。 要獲取正確的frame,您需要計算圖像與視圖的比例。

公式相當簡單(??):

  • 1) 計算視圖和圖像的分辨率。
  • 2) 通過比較分辨率確定比例。
  • 3) 通過將它們乘以scale來計算高度,寬度和原點x和y。
  • 4) 使用這些數(shù)據(jù)點創(chuàng)建新的CGRect

如果這聽起來令人困惑,那就沒關(guān)系! 當你看到代碼時,你會明白的。


Calculating the Scale

打開ScaledElementProcessor.swift并添加以下方法:

// 1
private func createScaledFrame(
  featureFrame: CGRect, 
  imageSize: CGSize, viewFrame: CGRect) 
  -> CGRect {
  let viewSize = viewFrame.size
    
  // 2
  let resolutionView = viewSize.width / viewSize.height
  let resolutionImage = imageSize.width / imageSize.height
    
  // 3
  var scale: CGFloat
  if resolutionView > resolutionImage {
    scale = viewSize.height / imageSize.height
  } else {
    scale = viewSize.width / imageSize.width
  }
    
  // 4
  let featureWidthScaled = featureFrame.size.width * scale
  let featureHeightScaled = featureFrame.size.height * scale
    
  // 5
  let imageWidthScaled = imageSize.width * scale
  let imageHeightScaled = imageSize.height * scale
  let imagePointXScaled = (viewSize.width - imageWidthScaled) / 2
  let imagePointYScaled = (viewSize.height - imageHeightScaled) / 2
    
  // 6
  let featurePointXScaled = imagePointXScaled + featureFrame.origin.x * scale
  let featurePointYScaled = imagePointYScaled + featureFrame.origin.y * scale
    
  // 7
  return CGRect(x: featurePointXScaled,
                y: featurePointYScaled,
                width: featureWidthScaled,
                height: featureHeightScaled)
  }

這是代碼中發(fā)生的事情:

  • 1) 此方法接受CGRects的原始圖像大小,顯示的圖像大小和UIImageView的frame。
  • 2) 圖像和視圖的分辨率分別通過它們的高度和寬度之比來計算。
  • 3) 比例由哪個分辨率更大來確定。如果視圖較大,則按高度縮放;否則,你按寬度縮放。
  • 4) 此方法計算寬度和高度。frame的寬度和高度乘以比例以計算縮放的寬度和高度。
  • 5) frame的原點也必須縮放,否則,即使尺寸正確,它也會偏離錯誤的位置。
  • 6) 通過將x和y點scale添加到未縮放的原點乘以scale來計算新原點。
  • 7) 返回縮放的CGRect,使用計算的原點和大小進行配置。

既然你有一個縮放的CGRect,你可以從涂鴉到sgraffito

轉(zhuǎn)到ScaledElementProcessor.swift中的process(in:callback :)并修改最里面的for循環(huán)以使用以下代碼:

for element in line.elements {
  let frame = self.createScaledFrame(
    featureFrame: element.frame,
    imageSize: image.size, 
    viewFrame: imageView.frame)
  
  let shapeLayer = self.createShapeLayer(frame: frame)
  let scaledElement = ScaledElement(frame: frame, shapeLayer: shapeLayer)
  scaledElements.append(scaledElement)
}

新添加的行創(chuàng)建一個縮放frame,代碼用于創(chuàng)建正確的位置shape layer

建立并運行。 您應該看到在正確的位置繪制的frame。 你是一位大師級畫家!

足夠的默認照片;是時候使用其他資源進行測試了!


Taking Photos with the Camera

該項目已經(jīng)在ViewController.swift底部的擴展中設(shè)置了相機和庫選取器代碼。 如果您現(xiàn)在嘗試使用它,您會注意到?jīng)]有任何frame匹配。 那是因為它仍在使用預裝圖像中的舊frame! 拍攝或選擇照片時,您需要刪除它們并繪制新的。

將以下方法添加到ViewController

private func removeFrames() {
  guard let sublayers = frameSublayer.sublayers else { return }
  for sublayer in sublayers {
    sublayer.removeFromSuperlayer()
  }
}

此方法使用for循環(huán)從frame sublayer中刪除所有子層。 這為您提供了下一張照片的干凈畫布。

要合并檢測代碼,請將以下新方法添加到ViewController

// 1
private func drawFeatures(
  in imageView: UIImageView, 
  completion: (() -> Void)? = nil
  ) {
  // 2
  removeFrames()
  processor.process(in: imageView) { text, elements in
    elements.forEach() { element in
      self.frameSublayer.addSublayer(element.shapeLayer)
    }
    self.scannedText = text
    // 3
    completion?()
  }
}

這是改變的地方:

  • 1) 此方法接受UIImageView和回調(diào),以便您知道它何時完成。
  • 2) 在處理新圖像之前會自動刪除frame。
  • 3) 一切都完成后觸發(fā)完成回調(diào)。

現(xiàn)在,用以下代碼替換viewDidLoad()中對processor.process(in:callback :)的調(diào)用:

drawFeatures(in: imageView)

向下滾動到類擴展并找到imagePickerController(_:didFinishPickingMediaWithInfo :);在imageView.image = pickedImage之后,將這行代碼添加到if塊的末尾:

drawFeatures(in: imageView)

拍攝或選擇新照片時,此代碼可確保刪除舊frame并替換為新照片中的frame。

構(gòu)建并運行。 如果您使用的是真實設(shè)備(不是模擬器),請拍下印刷文字。 你可能會看到奇怪的東西:

這里發(fā)生了什么?

您將在一秒鐘內(nèi)解決圖像方向,因為上面是方向問題。


Dealing With Image Orientations

此應用程序以縱向方向鎖定。 在設(shè)備旋轉(zhuǎn)時重繪frame是很棘手的,因此現(xiàn)在更容易限制用戶。

此限制要求用戶拍攝豎屏照片。 UICameraPicker將豎屏照片在幕后旋轉(zhuǎn)90度。 您沒有看到旋轉(zhuǎn),因為UIImageView會為您旋轉(zhuǎn)它。 但是,detector得到的是旋轉(zhuǎn)的UIImage

這導致一些令人困惑的結(jié)果。 ML Kit允許您在VisionMetadata對象中指定照片的方向。 設(shè)置正確的方向?qū)⒎祷卣_的文本,但將為旋轉(zhuǎn)的照片繪制frame。

因此,您需要將照片方向固定為始終處于“向上”位置。 該項目包含一個名為+ UIImage.swift的擴展。 此擴展為UIImage添加了一種方法,可將任何照片的方向更改為向上位置。 一旦照片處于正確的方向,一切都將順利進行!

打開ViewController.swift,在imagePickerController(_:didFinishPickingMediaWithInfo :)中,用以下內(nèi)容替換imageView.image = pickedImage

// 1
let fixedImage = pickedImage.fixOrientation()
// 2
imageView.image = fixedImage

下面詳細說明:

  • 1) 新選擇的圖像pickedImage將旋轉(zhuǎn)回向上位置。
  • 2) 然后,將旋轉(zhuǎn)的圖像分配給imageView

建立并運行。 再拍那張照片。 你應該在正確的地方看到一切。


Sharing the Text

最后一步不需要您采取任何措施,該應用程序與UIActivityViewController集成。 看看shareDidTouch()

@IBAction func shareDidTouch(_ sender: UIBarButtonItem) {
  let vc = UIActivityViewController(
    activityItems: [textView.text, imageView.image!], 
    applicationActivities: [])

  present(vc, animated: true, completion: nil)
}

這是一個簡單的兩步過程。 創(chuàng)建一個包含掃描文本和圖像的UIActivityViewController。 然后調(diào)用present()并讓用戶完成其余的工作。

在本教程中,您學習了:

  • 通過構(gòu)建文本檢測照片應用程序了解ML Kit的基礎(chǔ)知識。
  • ML Kit文本識別API,圖像比例和方向。

要了解有關(guān)FirebaseML Kit的更多信息,請查看official documentation

后記

本篇主要講述了基于ML Kit的iOS圖片中文字的識別,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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