Tesseract OCR iOS 教程

原文:Tesseract OCR Tutorial for iOS
作者:Lyndsey Scott
译者:kmyhy

更新说明:本教程由 Lyndsey Scott 更新为 Swift 4、iOS 11 和 Xcode 9。原文作者是 Lyndsey Scott。

你肯定知道 OCR……它通常用于处理扫描文档,手写文稿,以及在 Google 的 Translate app 所用的实景翻译技术。今天你将学习如何在你自己的 app 中利用 Tesseract 来实现它。听起来很不错,是吗?

但是……什么是 OCR?

光学文字识别(OCR)是一种从图片中抽取数字化字符的过程。当抽取完成后,用户就可以将这些文字用于编辑文档、文字搜索、压缩等等。

在本教程中,你将用 OCR 去追求你的真爱。你将使用一个由 Google 维护的开源 OCR 引擎 Tesseract 创建一个名为 Love In A Snap 的 app。这个 app 允许你用一首爱情诗的图片作为素材,将原作者的女神/男神替换为你想追求的对象。好棒!准备让人们大吃一惊吧。

开始

从这里下载开始项目,并解压缩。

这里面有几个文件夹:

  • LoveInASnap: 开始项目。
  • Images:爱情诗图片。
  • tessdata: Tesseract 的语言包。

打开 LoveInASnap\LoveinASnap.xcodeproj,build & run,随意点点,感受一下 UI。目前的 app 很简单,但你会在选中或反选文本框时看到会上移下移。这是为了防止键盘遮住文字框和按钮。

Tesseract OCR iOS 教程_第1张图片

开始编写代码

打开 ViewController.swift 看一下代码。你会看到几个 @IBOutlet 属性和 @IBAction 方法已经连接到了 Main.storyboard。在这些 @IBAction 中,view.endEditing(true) 用于释放键盘。在 sharePoen(_:) 方法中这样做是因为当键盘弹出时,分享按钮会被遮挡住。

在这些 @IBAction 之后,你会看到一个 performImageRecognition(_:)。这是 Tesseract 进行图片识别的地方。

下面两个函数用于将视图上移、下移:

func moveViewUp() {
  if topMarginConstraint.constant != originalTopMargin {
    return
  }  
  topMarginConstraint.constant -= 135
  UIView.animate(withDuration: 0.3) {
    self.view.layoutIfNeeded()
  }
}

func moveViewDown() {
  if topMarginConstraint.constant == originalTopMargin {
    return
  }
  topMarginConstraint.constant = originalTopMargin
  UIView.animate(withDuration: 0.3) {
    self.view.layoutIfNeeded()
  }
}

当键盘弹出时,moveViewUp 将 View controller 的 view 的 top 约束向上移。当键盘收起,moveViewDown 将控制器视图的 top 约束设置回原来的值。

在故事板中,UITextField 的委托设置为 ViewController。在 UITextFieldDelegate 扩展中有这几个方法:

// MARK: - UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
  func textFieldDidBeginEditing(_ textField: UITextField) {
    moveViewUp()
  }

  func textFieldDidEndEditing(_ textField: UITextField) {
    moveViewDown()
  }
}

当用户开始编辑 text field 时,调用 moveViewUp。当用户结束编辑 text field时,调用 moveViewDown。

尽管上述函数对于 app UX 来说必不可少,但跟本文毫不相关。因为它们是已经写好的,我们可以直接从真正感兴趣的代码入手。

Tesseract 的限制

Tesseract OCR 非常强大,但也有一些限制:

  • 和别的 OCR 引擎不同(比如美国邮政服务用于整理邮件的 OCR),Tesseract 无法识别手写体。实际上,它总共只支持 64 中字体。
  • 可以通过对图像进行预处理来提升 Tesseract 的性能。你必须通过对图片进行缩放、增加颜色对比度、对文本水平对齐来优化处理结果。
  • 最后,Tesseract OCR 只支持 Linux、Windows、和 Mac OS X。

呃?有 Linux、Windows 和 Mac OS X,没有 iOS?幸运的是,gali8 对 Tesseract OCR 进行了一个 O-C 的封装,你可以在 Swift 和 iOS 中使用。

嘁!:]

安装 Tesseract

根据 Joshua Greene 写的一篇教程如何在 Swift 中使用 CocoaPods 所描述的,你可以用以下步骤安装 CocoaPods 和 Tesseract 框架。

要安装 CocoaPods,可以在终端中使用命令:

sudo gem install cocoapods

当问到计算机密码时,请输入正确的密码。

要在项目中安装 Tesseract,用 cd 命令转到 LoveInASnap 项目所在的目录。例如,如果你的开始项目位于桌面,请使用:

cd ~/Desktop/OCR_Tutorial_Resources/LoveInASnap

然后,用下列命令在这个文件夹下生成一个 Podfile 文件:

pod init

用文本编辑器打开 Podfile 文件,编辑内容为:

use_frameworks!
platform :ios, '11.0'

target 'LoveInASnap' do
  use_frameworks!
  pod 'TesseractOCRiOS'
end

这会告诉 CocoaPods 你想在项目中使用 TesseractOCRiOS 框架。最后,保存、关闭 Podfiel,进入终端,保持之前的工作目录不变,输入命令:

pod install

就是这样!当一段长长的输出之后,然后你会看到 “Please close any current Xcode sessions and use ‘LoveInASnap.xcworkspace’ for this project from now on.” 。关闭 LoveinASnap.xcodeproj,在 Xcode 中打开OCR_Tutorial_Resources\LoveInASnap\LoveinASnap.xcworkspace 。

在 Xcode 中设置 Tesseract

将 tessdata 文件夹,也就是 Tesseract 的语言包,从 Finder 中拖进 Xcode 项目的 Supporting Files 文件夹下。确认勾选 Copy items 选项,和 Create folder 选项,然后勾上 LoveInASnap,点击 Finish。

Tesseract OCR iOS 教程_第2张图片

注意:确认在 Build Phases 的 Copy Bundlle Resources 下面有 tessdata 一项,否者运行时会报错,说在 tessdata 的父目录中未设置 TESSDATA_PREFIX 环境变量。

返回项目导航器,点击 LoveInASnap 项目文件,在 Targets 下面,选择 LoveInASnap,打开 General 标签页,找到 Linked Frameworkds and Libraries 选项。

这里只应该有一个文件存在:Pods_LoveInASnap.framework,也就是你刚刚添加的那个 pod。点击 + 按钮,添加 libstadc++.dylib、CoreImage.framework 和 TesseractOCR.framework。

Tesseract OCR iOS 教程_第3张图片

之后,你的 Linked Frameworks and Libraries 应该变成:

Tesseract OCR iOS 教程_第4张图片

差不多了!还剩一个步骤,我们就可以开始编写代码了……

在 LoveInASnap target 的 Build Settings 中,找到 C++ Standard Library,将它设置为 Compiler Default。然后找到 Enable Bitcode,将它设置为 NO。

类似地,回到左边的项目导航器中,选择 Pods 项目,找到 TesseractOCRiOS target 的 Build Settings,找到 C++ Standard Library 将它设置为 Compiler Default。然后找到 Enable Bitcoe 将它设置为 NO。

就是这样了!Build & run,确保能够编译。你会在左边的 issue 导航器中看到一些警告,但不要理它们。

好了没有?现在你终于可以开始有意思的部分了!

创建 Image Picker

打开 ViewController.swift 在类定义之后添加扩展:

// 1
// MARK: - UINavigationControllerDelegate
extension ViewController: UINavigationControllerDelegate {
}

// MARK: - UIImagePickerControllerDelegate
extension ViewController: UIImagePickerControllerDelegate {
  func presentImagePicker() {
    // 2
    let imagePickerActionSheet = UIAlertController(title: "Snap/Upload Image",
                                                   message: nil, preferredStyle: .actionSheet)
    // 3
    if UIImagePickerController.isSourceTypeAvailable(.camera) {
      let cameraButton = UIAlertAction(title: "Take Photo",
                                       style: .default) { (alert) -> Void in
                                        let imagePicker = UIImagePickerController()
                                        imagePicker.delegate = self
                                        imagePicker.sourceType = .camera
                                        self.present(imagePicker, animated: true)
      }
      imagePickerActionSheet.addAction(cameraButton)
    }
    // Insert here
  }
}

代码解释如下:

  1. 将 ViewController 声明为实现 UINavigationControllerDelegate 和 UIImagePickerController 协议,这是使用 UIImagePickerController 时必须实现的两个协议。
  2. 在 presentImagePicker() 方法中,创建一个 UIAlertController 用于向用户显示一个 action sheet 以便获取用户的选择。
  3. 如果设备拥有摄像头,在 imagePickerActionSheet 中添加一个 Take Photo 按钮。这个按钮会用 .camera 作为 sourceType 来创建和呈现 UIImagePickerController。

为了完成这个函数,请将 // Insert here 替换为:

// 1
let libraryButton = UIAlertAction(title: "Choose Existing",
  style: .default) { (alert) -> Void in
    let imagePicker = UIImagePickerController()
    imagePicker.delegate = self
    imagePicker.sourceType = .photoLibrary
    self.present(imagePicker, animated: true)
}
imagePickerActionSheet.addAction(libraryButton)
// 2
let cancelButton = UIAlertAction(title: "Cancel", style: .cancel)
imagePickerActionSheet.addAction(cancelButton)
// 3
present(imagePickerActionSheet, animated: true)

代码解释:

  1. 在 imagePickerActionSheet 中添加 Choose Existing 按钮。这个按钮用 .photoLibrary 作为 sourceType 来创建和呈现 UIImagePickerController。
  2. 添加一个 Cancel 按钮。
  3. 呈现 UIAlertController。

然后,在 takePhoto(_:) 方法中添加:

presentImagePicker()

当你点击 Snap/Upload Image 时,这会显示一个 Image picker。

如果你用真机编译,并企图拍照,app 会崩溃。因为 app 没有向用户获取到相机访问权限,因此你还需要添加必要的权限声明字段。

声明访问相册权限

在项目导航器中,找到 LoveInASnap 的 Info.plist 文件。在 Information Property List 上面点击 + 按钮,添加 Privacy – Photo Library Usage Description 和 Privacy – Camera Usage Description 两个 key。将它们的值填写为要显示给用户的内容。

Tesseract OCR iOS 教程_第5张图片

Build & run。点击 Snap/Upload Image,你将看到 UIAlertController 显示出来:

Tesseract OCR iOS 教程_第6张图片

注意:如果你使用模拟器,因为没有真实摄像头,你将无法看到 Take Photo 这个选项。

如果你点击 Take Photo,然后授权 app 访问相机,你就可以进行拍照。如果你选择 Choose Existing 然后授权 app 访问相册,你就可以从中选择一张图片。

选择图片之后,app 目前是不会做任何动作的。你还需要在 Tesseract 处理图片之前做一些准备工作。

在 Tesseract 的限制中提到,为了优化 OCR 的结果,你必须将图片尺寸限制在一定大小。如果图片太大或者太小,Tesseract 可能返回错误结果或者出现 EXC_BAD_ACCESS 而崩溃。

因此你必须写一个修改图片大小但宽高比保持不变的方法。

维持纵横比缩放图片

图片的纵横比是指它的宽度和高度的比例。因此,在减少图片的尺寸同时不改变纵横比,你必须将这个宽高比作为一个常量。

如果你知道原始图片的宽和高,那么只要你知道最终图片的宽或高中的任意一个,就能够应用下面的纵横比公式:

Tesseract OCR iOS 教程_第7张图片

因此,height2 = height1/width1 * width2,width2 = width1/height1 * height2。你可以在缩放方法中用这两个公式来保持图片的纵横比不变。

打开 ViewController.swift 在 UIImage 扩展中添加方法:

// MARK: - UIImage extension
extension UIImage {
  func scaleImage(_ maxDimension: CGFloat) -> UIImage? {

    var scaledSize = CGSize(width: maxDimension, height: maxDimension)

    if size.width > size.height {
      let scaleFactor = size.height / size.width
      scaledSize.height = scaledSize.width * scaleFactor
    } else {
      let scaleFactor = size.width / size.height
      scaledSize.width = scaledSize.height * scaleFactor
    }

    UIGraphicsBeginImageContext(scaledSize)
    draw(in: CGRect(origin: .zero, size: scaledSize))
    let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return scaledImage
  }
}

scaleImage(_:) 方法会获取图片的高或者宽——比较两者的较大者为准——然后将它的大小设置为 maxDimension 参数。然后,为了维持图片的纵横比,根据需要缩放另一边即可。然后将原图重新在新 frame 中重绘。最后,返回缩放后的图片。

现在,你必须写一个方法获取用户选择的图片。

获取图片

在 UIImagePickerControllerDelegate 扩展中在 presentImagePicker() 下面添加方法:

// 1
func imagePickerController(_ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info: [String : Any]) {
  // 2
  if let selectedPhoto = info[UIImagePickerControllerOriginalImage] as? UIImage, 
    let scaledImage = selectedPhoto.scaleImage(640) {
    // 3
    activityIndicator.startAnimating()
    // 4
    dismiss(animated: true, completion: {
      self.performImageRecognition(scaledImage)
    })
  }
}

这个方法解释如下:

  1. imagePickerController(_:didFinishPickingMediaWithInfo:) 是 UIImagePickerControllerDelegate 协议中的方法。当用户选择好图片,这个方法会在一个 info 字典中返回这张图片的信息。
  2. 将图片通过 UIImagePickerControllerOriginalImage 键从 info 字典中取出。将图片缩放至宽高小于 640。(根据经验,640 的识别结果最佳)同时对缩放后的图片进行解包操作。
  3. 让 activity indicator 开始显示,表示 Tesseract 正在工作。
  4. 解散 UIImagePicker,将图片传递给 performImageRecognition 方法处理。

Build & run,点击 Snap/upload Image,选择一张图片。activity indicator 将开始旋转。

别被它迷花了眼!还有更多代码要写。

我们显示了 activity indicator,但它到底代表什么意思?闲话少说(请来点掌声),你终于可以开始使用 Tesseract OCR 了!

使用 Tesseracdt OCR

打开 ViewController.swift 在 import UIKit 下添加:

import TesseractOCR

这将导入 Tesseract 框架,并允许你在这个文件中使用它。

然后,在 performImageRecognition(_:) 方法一开始添加:

// 1
if let tesseract = G8Tesseract(language: "eng+fra") {
  // 2
  tesseract.engineMode = .tesseractCubeCombined
  // 3
  tesseract.pageSegmentationMode = .auto
  // 4
  tesseract.image = image.g8_blackAndWhite()
  // 5
  tesseract.recognize()
  // 6
  textView.text = tesseract.recognizedText
}
// 7
activityIndicator.stopAnimating()

OCR 开始发挥作用了!整个方法分为以下几个部分:

  1. 创建一个 G8Tesseract 对象,传入 eng+fra 参数,即英语和法语语言包。本教程中所用的诗中用到了一些法语(罗曼蒂克),因此添加法语能让 Tesseract 认识其中的法语单词,并形成合体的字符。
  2. 有 3 个 OCR 模式:.tesseractOnly 最快,但准确率是最差的。.cubeOnly,稍慢但准确率更高,因为它使用了更多的人工智能。.tesseractdCubeCombined 集合了 .tesseractOnly 和 .cubeOnly,这也是其中最慢的一种模式。在本教程中,使用了.tesseractCubeCombined,因为它的准确率最高。
  3. Tesseract 默认要处理的文本处于同一文本块中。因为例子中使用的诗包含了段落换行符,它不是同一文本块。将 pageSegmentationMode 设置为 .auto 允许 Tesseract 自动识别出段落之间的分隔。
  4. 当文本和背景之间的对比度越高,识别的结果越好。用 Tesseract 内置的 g8_blackAndWhite 滤镜降低颜色饱和度,增加对比度,降低曝光度。
  5. 进行光学文字识别。
  6. 将识别出的文字放到 textview 里。
  7. 移除 activity indicator,表示 OCR 过程结束。

是时候测试一下代码,看看什么结果了!

处理第一张图片

在示例图片中有这样一张图片 OCR_Tutorial_Resources\Images\Lenore.png:

Tesseract OCR iOS 教程_第8张图片

Lenore.png 包含了一首爱情诗,是寄给 “Lenore” 的,但只需要稍微编辑下,就能用于送给你的女神/男神!:]

如果在有相机的设备上运行 app,你可以拍下这首诗,然后进行 OCR。但出于本文演示目的,将图片添加到设备的相机胶卷中,你就能够上传它了。这样,你可以避免光源不均匀、文字倾斜、打印不清晰等问题。

如果你使用模拟器,将图片文件拖进模拟器,即可将它添加到你的相机胶卷。

Build & run,选择 Snap/Upload Image,然后选择 Choose Existing。同意 app 访问你的相册,然后选择这张图片。

然后……看到了吗!几秒钟之后,文字就识别出来并显示到了 text view 中。

只不过,如果你的女神/男神名字并不叫做 Lenore,他或者她并不会买账。因为在诗中,Lenore 的使用十分频繁,要将它替换成你的心上人是一个不小的工作。

你说什么?是的,你可以写一个函数,查找并替换这个词。这想法太妙了!下一节将告诉你怎么做。

查找替换文本

现在 OCR 引擎已经把图片转换成文字,你可以把它看成普通的字符串对待。

还记得吗?ViewController.swift 中有一个 swapText 函数,当 swap 按钮被点时会触发这个函数。这就简单了,是不?

找到 swapText(_:),在 view.endEditing(true) 一句下面添加:

// 1
guard let text = textView.text,
  let findText = findTextField.text,
  let replaceText = replaceTextField.text else {
    return
}

// 2
textView.text =
  text.replacingOccurrences(of: findText, with: replaceText)
// 3
findTextField.text = nil
replaceTextField.text = nil

这段代码很简单,让我们来简单过一下:

  1. 判断 textView、findTextField 和 replaceTextField 中内容不为空时,才调用交换方法。
  2. 在 text view 中,将 findTextField 中指定的文本替换为 replaceTextField 中的内容。
  3. 替换完成,清除 findTextField 和 replaceTextField 中的内容。

Build & run,再次上传示例图片,让 Tesseract 开始工作。当文字显示出来后,在 Find this … 中输入 Lenore ,在 Repace with … 中输入你的男神/女神的名字(注意,查找替换是大小写敏感的)。点击 swap 按钮,完成替换。

Tesseract OCR iOS 教程_第9张图片

变,变,变-你创作了一首为情人量身定制的爱情诗!

你还可以替换其它单词,以迸发出你自己的艺术火花!

太好了!这么有诗意和勇气的作品不应该只呆在你的手机里。你还需要一个方法将你的大作分享给全世界。

分享成果

要分享你的诗,请在 sharePeom() 中编写代码:

// 1
if textView.text.isEmpty {
  return
}
// 2
let activityViewController = UIActivityViewController(activityItems:
  [textView.text], applicationActivities: nil)
// 3
let excludeActivities:[UIActivityType] = [
  .assignToContact,
  .saveToCameraRoll,
  .addToReadingList,
  .postToFlickr,
  .postToVimeo]
activityViewController.excludedActivityTypes = excludeActivities
// 4
present(activityViewController, animated: true)

分别解释如下:

  1. 如果 text view 是空的,返回。
  2. 否则,用 text view 中的文本初始化一个 UIActivityViewController。
  3. UIActivityViewController 的 activity 类型默认是一个很长的数组。这里我们将不相关的类型全部排除。
  4. 呈现 UIActivityViewController 允许用户将他们的创作按照希望的方式进行分析。

再次 build & run。上传示例图片,查找替换文字。然后欣赏完你的诗作之后,点击信封,会显示分享选项,然后将你的诗歌按照你想要的方式分享出去。

这样你的 Love In A Snap 就完成了——你肯定能够获得对方的青睐。

你可以像我一样,将 Lenore 换掉,将诗歌发送到你的收件箱,然后独自饮下一杯葡萄酒,带着惺忪的眼神,假装这封 email 是来自于女王陛下,为了一次特别大气的、充满浪漫、舒适、美妙和神秘的夜晚……但还是只有我一个……

接下来做什么

从这里下载完成后的开始项目。

你可以看看 GitHub 上的 Tesseract 的 iOS 封装:https://github.com/gali8/Tesseract-OCR-iOS。在 Google 的 Tesseract OCR 网站可以下载其他语言包(请使用 3.03 版本以上的语言包,以便和当前框架兼容)。

Tesseract OCR iOS 教程_第10张图片

在后续研究 OCR 的时候,记住这点:“输入的质量越差,输出的结果就越差。”最简单的提升输出质量的方法是改善输入的质量,比如:

  • 对图片进行预处理。
  • 对图片反复进行滤镜处理,比较结果上的差异,然后得到最准确的输出。
  • 创建自己的 AI 逻辑,比如神经网络。
  • 用 Tesseract 自己的训练工具帮助你的程序从错误中的学习,并即时改进成功率。

通过联合多种策略,你会得到最好的结果,因此请尝试各种手段并找出最佳工作方式。

最后,如果你对本文、Tesseract 或者 OCR 有任何问题或建议,请在下面留言。

你可能感兴趣的:(OCR,Tesseract,iPhone开发)