版本记录
版本号 | 时间 |
---|---|
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
教程中,您将学习如何利用Google
的ML Kit
来检测和识别文本。
几年前,有两种类型的机器学习(ML)开发人员:高级开发人员和其他人。底层的ML水平可能很难,这是很多数学,它使用逻辑回归(logistic regression)
,稀疏性(sparsity)
和神经网络(neural nets)
等词语。但它并不一定要那么难。
您也可以成为ML开发人员! ML的核心很简单。有了它,您可以通过训练软件模型来识别模式而不是硬编码每种情况和您能想到各种case来解决问题。但是,开始这可能是令人生畏的,这是您可以依赖现有工具的地方。
1. Machine Learning and Tooling
就像iOS开发一样,ML
就是工具。你不会建立自己的UITableView
,或者至少你不应该,你会使用一个框架,比如UIKit
。
它与ML的方式相同。 ML拥有蓬勃发展的工具生态系统。例如,Tensorflow
简化了训练和运行模型。 TensorFlow Lite
为iOS
和Android
设备提供模型支持。
这些工具中的每一个都需要一些ML的经验。如果您不是ML专家但想要解决特定问题怎么办?对于这些情况,有ML Kit
。
ML Kit
ML Kit是一款移动SDK,可将Google
的ML
专业知识带入您的应用。 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
有一个接收VisionImage
的process
方法,它以传递给闭包的参数的形式返回一个文本结果数组。 - 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
用作frame
和shape 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×369
。 ML 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,图像比例和方向。
要了解有关Firebase
和ML Kit
的更多信息,请查看official documentation。
后记
本篇主要讲述了基于ML Kit的iOS图片中文字的识别,感兴趣的给个赞或者关注~~~