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 很简单,但你会在选中或反选文本框时看到会上移下移。这是为了防止键盘遮住文字框和按钮。
开始编写代码
打开 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。
注意:确认在 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。
之后,你的 Linked Frameworks and Libraries 应该变成:
差不多了!还剩一个步骤,我们就可以开始编写代码了……
在 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
}
}
代码解释如下:
- 将 ViewController 声明为实现 UINavigationControllerDelegate 和 UIImagePickerController 协议,这是使用 UIImagePickerController 时必须实现的两个协议。
- 在 presentImagePicker() 方法中,创建一个 UIAlertController 用于向用户显示一个 action sheet 以便获取用户的选择。
- 如果设备拥有摄像头,在 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)
代码解释:
- 在 imagePickerActionSheet 中添加 Choose Existing 按钮。这个按钮用 .photoLibrary 作为 sourceType 来创建和呈现 UIImagePickerController。
- 添加一个 Cancel 按钮。
- 呈现 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。将它们的值填写为要显示给用户的内容。
Build & run。点击 Snap/Upload Image,你将看到 UIAlertController 显示出来:
注意:如果你使用模拟器,因为没有真实摄像头,你将无法看到 Take Photo 这个选项。
如果你点击 Take Photo,然后授权 app 访问相机,你就可以进行拍照。如果你选择 Choose Existing 然后授权 app 访问相册,你就可以从中选择一张图片。
选择图片之后,app 目前是不会做任何动作的。你还需要在 Tesseract 处理图片之前做一些准备工作。
在 Tesseract 的限制中提到,为了优化 OCR 的结果,你必须将图片尺寸限制在一定大小。如果图片太大或者太小,Tesseract 可能返回错误结果或者出现 EXC_BAD_ACCESS 而崩溃。
因此你必须写一个修改图片大小但宽高比保持不变的方法。
维持纵横比缩放图片
图片的纵横比是指它的宽度和高度的比例。因此,在减少图片的尺寸同时不改变纵横比,你必须将这个宽高比作为一个常量。
如果你知道原始图片的宽和高,那么只要你知道最终图片的宽或高中的任意一个,就能够应用下面的纵横比公式:
因此,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)
})
}
}
这个方法解释如下:
- imagePickerController(_:didFinishPickingMediaWithInfo:) 是 UIImagePickerControllerDelegate 协议中的方法。当用户选择好图片,这个方法会在一个 info 字典中返回这张图片的信息。
- 将图片通过 UIImagePickerControllerOriginalImage 键从 info 字典中取出。将图片缩放至宽高小于 640。(根据经验,640 的识别结果最佳)同时对缩放后的图片进行解包操作。
- 让 activity indicator 开始显示,表示 Tesseract 正在工作。
- 解散 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 开始发挥作用了!整个方法分为以下几个部分:
- 创建一个 G8Tesseract 对象,传入 eng+fra 参数,即英语和法语语言包。本教程中所用的诗中用到了一些法语(罗曼蒂克),因此添加法语能让 Tesseract 认识其中的法语单词,并形成合体的字符。
- 有 3 个 OCR 模式:.tesseractOnly 最快,但准确率是最差的。.cubeOnly,稍慢但准确率更高,因为它使用了更多的人工智能。.tesseractdCubeCombined 集合了 .tesseractOnly 和 .cubeOnly,这也是其中最慢的一种模式。在本教程中,使用了.tesseractCubeCombined,因为它的准确率最高。
- Tesseract 默认要处理的文本处于同一文本块中。因为例子中使用的诗包含了段落换行符,它不是同一文本块。将 pageSegmentationMode 设置为 .auto 允许 Tesseract 自动识别出段落之间的分隔。
- 当文本和背景之间的对比度越高,识别的结果越好。用 Tesseract 内置的 g8_blackAndWhite 滤镜降低颜色饱和度,增加对比度,降低曝光度。
- 进行光学文字识别。
- 将识别出的文字放到 textview 里。
- 移除 activity indicator,表示 OCR 过程结束。
是时候测试一下代码,看看什么结果了!
处理第一张图片
在示例图片中有这样一张图片 OCR_Tutorial_Resources\Images\Lenore.png:
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
这段代码很简单,让我们来简单过一下:
- 判断 textView、findTextField 和 replaceTextField 中内容不为空时,才调用交换方法。
- 在 text view 中,将 findTextField 中指定的文本替换为 replaceTextField 中的内容。
- 替换完成,清除 findTextField 和 replaceTextField 中的内容。
Build & run,再次上传示例图片,让 Tesseract 开始工作。当文字显示出来后,在 Find this … 中输入 Lenore ,在 Repace with … 中输入你的男神/女神的名字(注意,查找替换是大小写敏感的)。点击 swap 按钮,完成替换。
变,变,变-你创作了一首为情人量身定制的爱情诗!
你还可以替换其它单词,以迸发出你自己的艺术火花!
太好了!这么有诗意和勇气的作品不应该只呆在你的手机里。你还需要一个方法将你的大作分享给全世界。
分享成果
要分享你的诗,请在 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)
分别解释如下:
- 如果 text view 是空的,返回。
- 否则,用 text view 中的文本初始化一个 UIActivityViewController。
- UIActivityViewController 的 activity 类型默认是一个很长的数组。这里我们将不相关的类型全部排除。
- 呈现 UIActivityViewController 允许用户将他们的创作按照希望的方式进行分析。
再次 build & run。上传示例图片,查找替换文字。然后欣赏完你的诗作之后,点击信封,会显示分享选项,然后将你的诗歌按照你想要的方式分享出去。
这样你的 Love In A Snap 就完成了——你肯定能够获得对方的青睐。
你可以像我一样,将 Lenore 换掉,将诗歌发送到你的收件箱,然后独自饮下一杯葡萄酒,带着惺忪的眼神,假装这封 email 是来自于女王陛下,为了一次特别大气的、充满浪漫、舒适、美妙和神秘的夜晚……但还是只有我一个……
接下来做什么
从这里下载完成后的开始项目。
你可以看看 GitHub 上的 Tesseract 的 iOS 封装:https://github.com/gali8/Tesseract-OCR-iOS。在 Google 的 Tesseract OCR 网站可以下载其他语言包(请使用 3.03 版本以上的语言包,以便和当前框架兼容)。
在后续研究 OCR 的时候,记住这点:“输入的质量越差,输出的结果就越差。”最简单的提升输出质量的方法是改善输入的质量,比如:
- 对图片进行预处理。
- 对图片反复进行滤镜处理,比较结果上的差异,然后得到最准确的输出。
- 创建自己的 AI 逻辑,比如神经网络。
- 用 Tesseract 自己的训练工具帮助你的程序从错误中的学习,并即时改进成功率。
通过联合多种策略,你会得到最好的结果,因此请尝试各种手段并找出最佳工作方式。
最后,如果你对本文、Tesseract 或者 OCR 有任何问题或建议,请在下面留言。