基于Firebase平台开发(一) —— 基于ML Kit的iOS图片中文字的识别(一)

版本记录

版本号 时间
V1.0 2019.02.01 星期五

前言

Firebase是一家实时后端数据库创业公司,它能帮助开发者很快的写出Web端和移动端的应用。自2014年10月Google收购Firebase以来,用户可以在更方便地使用Firebase的同时,结合Google的云服务。Firebase能让你的App从零到一。也就是说它可以帮助手机以及网页应用的开发者轻松构建App。通过Firebase背后负载的框架就可以简单地开发一个App,无需服务器以及基础设施。接下来几篇我们就一起看一下基于Firebase平台的开发。

Firebase提供的服务

首先我们看一下Firebase目前提供的服务:

这里看一下ML Kit关于机器学习的部分还是BETA


开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

在这个ML Kit教程中,您将学习如何利用GoogleML Kit来检测和识别文本。

几年前,有两种类型的机器学习(ML)开发人员:高级开发人员和其他人。底层的ML水平可能很难,这是很多数学,它使用逻辑回归(logistic regression),稀疏性(sparsity)和神经网络(neural nets)等词语。但它并不一定要那么难。

您也可以成为ML开发人员! ML的核心很简单。有了它,您可以通过训练软件模型来识别模式而不是硬编码每种情况和您能想到各种case来解决问题。但是,开始这可能是令人生畏的,这是您可以依赖现有工具的地方。

1. Machine Learning and Tooling

就像iOS开发一样,ML就是工具。你不会建立自己的UITableView,或者至少你不应该,你会使用一个框架,比如UIKit

它与ML的方式相同。 ML拥有蓬勃发展的工具生态系统。例如,Tensorflow简化了训练和运行模型。 TensorFlow LiteiOSAndroid设备提供模型支持。

这些工具中的每一个都需要一些ML的经验。如果您不是ML专家但想要解决特定问题怎么办?对于这些情况,有ML Kit


ML Kit

ML Kit是一款移动SDK,可将GoogleML专业知识带入您的应用。 ML Kit的API有两个主要部分,用于常见用例和自定义模型(common use cases and custom models),无论经验如何都易于使用。

当前的API支持:

这些用例中的每一个都带有一个预先训练的模型,该模型包含在一个易于使用的API中。 是时候开始建设了!

在本教程中,您将构建一个名为Extractor的应用程序。 你有没有拍下一张标志或海报的图片来写下文字内容? 如果一个应用程序可以将文本从标志上剥离并保存给您,随时可以使用,那就太棒了。 例如,您可以拍摄寻址信封的照片并保存地址。 这正是你要对这个项目做的! 做好准备!

首先,打开本教程的项目材料,该项目使用CocoaPods来管理依赖项。


Setting Up ML Kit

每个ML Kit API都有一组不同的CocoaPods依赖项。 这很有用,因为您只需要捆绑应用程序所需的依赖项。 例如,如果您没有识别地标,则在您的应用中不需要该模型。 在Extractor中,您将使用Text Recognition API

如果您要将Text Recognition API添加到您的应用程序,那么您需要将以下行添加到您的Podfile中,但您不必为启动项目执行此操作,因为Podfile中有已经写好 - 您可以检查。

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的工作区文件,该文件是您在Xcode中打开的文件。 不要打开项目文件,因为它不包含编译应用程序所需的其他CocoaPods项目。

该项目包含以下重要文件:

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

Setting Up a Firebase Account

首先要创建一个账户,这个会在后面单独分出来一篇进行详细介绍。

一般的想法是:

  • 1) 创建一个帐户。
  • 2) 创建一个项目。
  • 3) 将iOS应用添加到项目中。
  • 4) 将GoogleService-Info.plist拖到您的项目中。
  • 5) 在AppDelegate中初始化Firebase

这是一个简单的过程,但如果您遇到任何障碍,后面我会单独进行讲解说明。

注意:您需要设置Firebase并为最终和初始项目创建自己的GoogleService-Info.plist

构建并运行应用程序,您将看到它看起来像这样:

除了允许您通过右上角的操作按钮共享硬编码文本之外,它不会执行任何操作。 您将使用ML Kit将此应用程序变为现实。


Detecting Basic Text

准备好第一次文本检测! 您可以首先向用户演示如何使用该应用程序。

一个很好的演示是在应用程序首次启动时扫描示例图像。 在资源文件夹中包含了一个名为scanning-text的图像,该图像当前是视图控制器的UIImageView中显示的默认图像。 您将使用它作为示例图像。

但首先,您需要一个文本检测器来检测图像中的文本。

1. Creating a Text Detector

创建名为ScaledElementProcessor.swift的文件并添加以下代码:

import Firebase

class ScaledElementProcessor {

}

很好! 你们都完成了! 开个玩笑。 在类中创建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是否实际包含图像。 如果没有,只需返回。 但是,理想情况下,您可以抛出或提供优雅的失败提示。
  • 2) ML Kit使用特殊的VisionImage类型。 它很有用,因为它可以包含ML Kit处理图像的特定元数据,例如图像的方向。
  • 3) textRecognizer有一个接收VisionImageprocess方法,它以传递给闭包的参数的形式返回一个文本结果数组。
  • 4) 结果可能是nil,在这种情况下,您将要为回调返回一个空字符串。
  • 5) 最后,触发回调以中继识别的文本。

2. Using the Text Detector

打开ViewController.swift,在类主体顶部的outlets之后,添加一个ScaledElementProcessor实例作为属性:

let processor = ScaledElementProcessor()

然后,在viewDidLoad()的底部添加以下代码,以在UITextView中显示检测到的文本:

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

这个小block调用process(in:),传递主imageView并将识别的文本分配给回调中的scansText属性。

运行该应用程序,您应该在图像正下方看到以下文本:

Your
SCanned
text
will
appear
here 

您可能需要滚动文本视图以显示最后几行。

注意扫描的“S”“C”是大写的。 有时,使用特定字体时,可能会出现错误的情况。 这就是为什么文本显示在UITextView中的原因,因此用户可以手动编辑以修复检测错误。

3. Understanding the Classes

注意:您不必复制本节中的代码,它只是有助于解释概念。您将在下一部分中向应用添加代码。

VisionText

您是否注意到ScaledElementProcessor中的textRecognizer.process(in :)的回调在result参数中返回了一个对象而不是普通的文本?这是VisionText的一个实例,它包含许多有用的信息,例如识别的文本。但是你想做的不仅仅是获取文本。描绘出每个识别的文本元素的每个frame不是很酷吗?

ML Kit以类似于树的结构提供结果。您需要遍历叶元素以获取包含已识别文本的frame的位置和大小。如果对树结构的引用让您感觉到困难,请不要太担心。以下部分应阐明正在发生的事情。

VisionTextBlock

使用已识别的文本时,您可以从VisionText对象开始 - 这是一个对象(称为树),它可以包含多个文本块(如树中的分支)。您遍历每个分支,这是块(blocks)数组中的VisionTextBlock对象,如下所示:

for block in result.blocks {

}

VisionTextElement

VisionTextBlock只是一个对象,包含一系列文本(如树枝上的叶子),每个文本都由一个VisionTextElement实例表示。 这个对象的嵌套允许您查看已识别文本的层次结构。

循环遍历每个对象如下所示:

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

    }
  }
}

此层次结构中的所有对象都包含文本所在的frame。 但是,每个对象包含不同级别的粒度(granularity)。 块可以包含多个行,一行可以包含多个元素,并且元素可以包含多个符号。

在本教程中,您将使用元素(elements)作为粒度级别。 元素通常对应于单词。 这将允许您绘制每个单词并向用户显示每个单词在图像中的位置。

最后一个循环遍历文本块的每一行中的元素。 这些元素包含frame,一个简单的CGRect。 使用此frame,您可以在图像上的单词周围绘制边框。


Highlighting the Text Frames

1. Detecting Frames

要在图像上绘制,您需要使用文本元素的frame创建CAShapeLayer。 打开ScaledElementProcessor.swift并将以下struct添加到文件的顶部:

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

这个struct是便利实现。 它可以更轻松地将frame和CAShapeLayer分组到控制器。 现在,您需要一个辅助方法来从元素的frame创建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构造UIBezierPath并将形状图层的path设置为UIBezierPath
  • 2) 颜色和宽度的可视属性通过常量枚举设置。
  • 3) 这个枚举有助于保持着色和宽度一致。

现在,用以下代码替换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) 除了识别的文本之外,回调现在还会获取一系列ScaledElement实例。
  • 2) scaledElements用作frameshape layer的集合。
  • 3) 正如上面所概述的,代码使用for循环来获取每个元素的frame。
  • 4) 最里面的for循环从元素的frame创建shape layer,然后用于构造新的ScaledElement实例。
  • 5) 将新创建的实例添加到scaledElements

2. Drawing

上面的代码是把你的铅笔放在一起。 现在,是时候画了! 打开ViewController.swift,在viewDidLoad()中,用以下代码替换对process(in :)的调用:

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

构建并运行。 看看你的艺术作品!

哦。 那是什么? 看起来你更像毕加索而不是莫奈。 这里发生了什么? 好吧,现在可能是谈论scale的时候了。


Understanding Image Scaling

默认的scanning-text.png图像为654×999 (width x height);但是,UIImageView具有“Aspect Fit”“Content Mode”,可以在视图中将图像缩放到375×369ML Kit接收图像的实际大小,并根据该大小返回元素frame。 然后根据缩放的大小绘制来自实际大小的frame,这会产生令人困惑的结果。

在上图中,请注意缩放大小和实际大小之间的差异。 您可以看到frame与实际大小匹配。 要获取正确的frame,您需要计算图像与视图的比例。

公式相当简单(👀):

  • 1) 计算视图和图像的分辨率。
  • 2) 通过比较分辨率确定比例。
  • 3) 通过将它们乘以scale来计算高度,宽度和原点x和y。
  • 4) 使用这些数据点创建新的CGRect

如果这听起来令人困惑,那就没关系! 当你看到代码时,你会明白的。


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)
  }

这是代码中发生的事情:

  • 1) 此方法接受CGRects的原始图像大小,显示的图像大小和UIImageView的frame。
  • 2) 图像和视图的分辨率分别通过它们的高度和宽度之比来计算。
  • 3) 比例由哪个分辨率更大来确定。如果视图较大,则按高度缩放;否则,你按宽度缩放。
  • 4) 此方法计算宽度和高度。frame的宽度和高度乘以比例以计算缩放的宽度和高度。
  • 5) frame的原点也必须缩放,否则,即使尺寸正确,它也会偏离错误的位置。
  • 6) 通过将x和y点scale添加到未缩放的原点乘以scale来计算新原点。
  • 7) 返回缩放的CGRect,使用计算的原点和大小进行配置。

既然你有一个缩放的CGRect,你可以从涂鸦到sgraffito

转到ScaledElementProcessor.swift中的process(in:callback :)并修改最里面的for循环以使用以下代码:

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)
}

新添加的行创建一个缩放frame,代码用于创建正确的位置shape layer

建立并运行。 您应该看到在正确的位置绘制的frame。 你是一位大师级画家!

足够的默认照片;是时候使用其他资源进行测试了!


Taking Photos with the Camera

该项目已经在ViewController.swift底部的扩展中设置了相机和库选取器代码。 如果您现在尝试使用它,您会注意到没有任何frame匹配。 那是因为它仍在使用预装图像中的旧frame! 拍摄或选择照片时,您需要删除它们并绘制新的。

将以下方法添加到ViewController

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

此方法使用for循环从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和回调,以便您知道它何时完成。
  • 2) 在处理新图像之前会自动删除frame。
  • 3) 一切都完成后触发完成回调。

现在,用以下代码替换viewDidLoad()中对processor.process(in:callback :)的调用:

drawFeatures(in: imageView)

向下滚动到类扩展并找到imagePickerController(_:didFinishPickingMediaWithInfo :);在imageView.image = pickedImage之后,将这行代码添加到if块的末尾:

drawFeatures(in: imageView)

拍摄或选择新照片时,此代码可确保删除旧frame并替换为新照片中的frame。

构建并运行。 如果您使用的是真实设备(不是模拟器),请拍下印刷文字。 你可能会看到奇怪的东西:

这里发生了什么?

您将在一秒钟内解决图像方向,因为上面是方向问题。


Dealing With Image Orientations

此应用程序以纵向方向锁定。 在设备旋转时重绘frame是很棘手的,因此现在更容易限制用户。

此限制要求用户拍摄竖屏照片。 UICameraPicker将竖屏照片在幕后旋转90度。 您没有看到旋转,因为UIImageView会为您旋转它。 但是,detector得到的是旋转的UIImage

这导致一些令人困惑的结果。 ML Kit允许您在VisionMetadata对象中指定照片的方向。 设置正确的方向将返回正确的文本,但将为旋转的照片绘制frame。

因此,您需要将照片方向固定为始终处于“向上”位置。 该项目包含一个名为+ UIImage.swift的扩展。 此扩展为UIImage添加了一种方法,可将任何照片的方向更改为向上位置。 一旦照片处于正确的方向,一切都将顺利进行!

打开ViewController.swift,在imagePickerController(_:didFinishPickingMediaWithInfo :)中,用以下内容替换imageView.image = pickedImage

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

下面详细说明:

  • 1) 新选择的图像pickedImage将旋转回向上位置。
  • 2) 然后,将旋转的图像分配给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)
}

这是一个简单的两步过程。 创建一个包含扫描文本和图像的UIActivityViewController。 然后调用present()并让用户完成其余的工作。

在本教程中,您学习了:

  • 通过构建文本检测照片应用程序了解ML Kit的基础知识。
  • ML Kit文本识别API,图像比例和方向。

要了解有关FirebaseML Kit的更多信息,请查看official documentation

后记

本篇主要讲述了基于ML Kit的iOS图片中文字的识别,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容