原文:Tesseract OCR Tutorial
"起初我写这篇教程是在情人节,OCR可以带给你一整年的爱"。
你之前肯定已经见过,OCR技术被应用于在平板电脑上将扫描文件处理成手写字迹,还被应用于谷歌最近添加到他们的Translate app上的"Word Len"功能。现在你将学习在自己的iPhone app上使用OCR,非常酷,不是吗?
那么…OCR是什么?
什么是OCR
光学字符识别,或着说OCR,是指用电子的方式从图片中取出文字然后重用于其他领域,如文档编辑、自由文本搜索,或文本比对。
本教程中,你将学习怎样使用Tesseract,谷歌维护的一个开源OCR引擎。
Tesseract介绍
Tesseract十分强大,但有以下几点局限性:
不像其他OCR引擎(例如美国邮政业用于分类邮件的),Tesseract不能识别手写,而且只能识别一共大约64中字体的文本。
Tesseract需要一些处理来改善OCR结果,图像需要被放缩,图像有非常多的差异,另外还有水平排布的文字。
最后,Tesseract仅仅支持Liuux,Windows,Mac OS X。
怎么样在iOS上使用它呢?幸运的是,有一套Tesseract OCR的Objective-C封装,也可以被用于swift和iOS上,适配swift版本的已经被包含在我们的示例工程中了:]
我们的app:一见钟情
你相信我们Ray Wenderlich团队不会让你在这即将到来的情人节失望,对吗?当然不会!我们支持你。我们成功找到一个有效的办法来打动你的真爱的心,你将通过编写一个app来实现这一点。
U + OCR = LUV
本教程中,你将学习怎样使用Tesseract--谷歌维护的一个开源OCR引擎。你将编写"一见钟情"应用,可以针对一张浪漫爱情诗的照片,将原诗人密切思念的名字替换为你自己喜欢的人的名字,从而将这首诗变成"你自己的"。非常好,准备好去感动你的Ta吧。
开始
从这里下载开始工程,解压到方便的地方。
压缩包中包含以下文件夹:
LoveInASnap:本教程的Xcode起步工程
Tesseract Resources:Tesseract框架和语言数据
Image Resources:样例图片,包含着一会你将用到的文字。
看一下现在的LoveinASnap.xcodeproj,可以注意到一个预先准备好的ViewController.swift和Main.storyboard界面,以及几个连接到控制器上的@IBOutlet和空的@IBAction方法。
在这些空方法下,可以看到两个已经写好的函数,用来处理展示和隐藏视图的active indicator:
1
2
3
4
5
6
7
8
9
10
11
12
|
func addActivityIndicator() {
activityIndicator = UIActivityIndicatorView(frame: view.bounds)
activityIndicator.activityIndicatorViewStyle = .WhiteLarge
activityIndicator.backgroundColor = UIColor(white: 0, alpha: 0.25)
activityIndicator.startAnimating()
view.addSubview(activityIndicator)
}
func removeActivityIndicator() {
activityIndicator.removeFromSuperview()
activityIndicator = nil
}
|
接下来还有一些方法来移动视图,防止键盘挡住text fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func moveViewUp() {
if
topMarginConstraint.constant != originalTopMargin {
return
}
topMarginConstraint.constant -= 135
UIView.animateWithDuration(0.3, animations: { () -> Void
in
self.view.layoutIfNeeded()
})
}
func moveViewDown() {
if
topMarginConstraint.constant == originalTopMargin {
return
}
topMarginConstraint.constant = originalTopMargin
UIView.animateWithDuration(0.3, animations: { () -> Void
in
self.view.layoutIfNeeded()
})
}
|
最后,剩下的方法根据用户的操作来触发键盘操作、调用moveViewUp()和moveViewDown():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@IBAction func backgroundTapped(sender: AnyObject) {
view.endEditing(
true
)
moveViewDown()
}
func textFieldDidBeginEditing(textField: UITextField) {
moveViewUp()
}
@IBAction func textFieldEndEditing(sender: AnyObject) {
view.endEditing(
true
)
moveViewDown()
}
func textViewDidBeginEditing(textView: UITextView) {
moveViewDown()
}
|
尽管app的用户体验很重要,但这些方法与本教程基本无关,因此实现为你准备好了,为了让你立刻开始真正的有趣的编程。
但是在你写第一行代码之前,编译并运行起步工程,到处点击一下app,感受一下UI。目前text view是不可编辑的,点击text filed只会简单地调出或关闭键盘,你的工作是补全完善这款app。
添加Tesseract框架
在你解压好的压缩包中有一个Tesseract Resources文件夹,包含着Tesseract框架和tessdata文件夹,里面有英语和法语识别数据。
在Finder中打开这一文件夹,将Tesseract.framework拖到Xcode的项目导航栏中,添加该框架,注意选中Copy items if needed。
最后点击Finish结束添加。
接下来你需要将tessdata文件夹作为"引用文件夹(referenced folder)"添加以便维持整个文件结构。将tessdata文件夹从Finder中拖到项目导航栏中Supporting Files组中。
同样,确保Copy items if needed选中,同时将Adding Folders选项设置为Create folder references。
Adding tessdata as a referenced folder
最后,点击Finish将数据文件添加到工程中,可以看到在项目导航栏中出现了一个蓝色的tessdata文件夹,蓝色说明这个文件夹是引用而不是Xcode中的group。
由于Tesseract需要libstdc++.6.0.9.dylib 和 CoreImage.framework,你还需要把这些库连接到项目中来。
选中LoveInASnap项目文件,选择LoveInASnap目标,在General栏中找到Linked Frameworks and Libraries。
现在这里应该只有一个文件:TesseractOCR.framework,就是你刚刚添加的,点击列表下面的+按钮,找到libstdc++.dylib和CoreImage.framework,把它们添加到你的工程中。
接下来在顶部菜单栏的Build Phases旁边,点击Build Settings,通过列表顶部的搜索栏可以方便地找到Other Linker Flags,在Other Linker Flags的所有已有的key后面添加-lstdc++,然后依旧是在Build Settings中,找到C++ Standard Library并选择"Compiler Default"。
差不多了,只剩最后一步了…
Wipe away those happy tears, Champ! Almost there! One step to go…
最后,因为Tesseract是一个Objective-C库,你需要创建"Objective-C桥接头文件"(Objective-C bridging header)来在你的swift app中使用该库。
创建桥接头文件并且使其符合所有项目配置的最简单的方法是把任意Objective-C文件添加到你的工程中。
选择File\New\File…,选中iOS\Source\Cocoa Touch Class然后点击Next,键入FakeObjectiveCClass作为类名,选择NSObject作为其超类,当然,确保语言选择为Objective-C!点击Next,然后点击Create。
当提示Would you like to configure an Objective-C bridging header时选择YES。
你已经成功创建了Objective-C桥接头文件,现在你可以从工程中删除FakeObjectiveCClass.m和FakeObjectiveCClass.h了,因为你只需要桥接头文件。:]
Toss out those Objective-c classes!
接下来把Tesseract库导入你新建的桥接头文件中,在项目导航栏中找到LoveInASnap-Bridging-Header.h,打开并添加下面这一行:
现在你可以通过你的工程访问Tesseract库了,编译并运行你的项目,确保一切都能正常通过编译。
一切正常吗?那么现在你可以开始做有趣的事了!
加载图像
当然,在你的OCR app中首先需要一个能从程序中加载一张图片的机制,最简单的方式是使用UIImagePickerController实例从相机或图片库中选择一张图片。
打开ViewController.swift,找到takePhoto(),将该方法的实现替换为下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
// 1
view.endEditing(
true
)
moveViewDown()
// 2
let imagePickerActionSheet = UIAlertController(title:
"Snap/Upload Photo"
,
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.presentViewController(imagePicker,
animated:
true
,
completion: nil)
}
imagePickerActionSheet.addAction(cameraButton)
}
// 4
let libraryButton = UIAlertAction(title:
"Choose Existing"
,
style: .Default) { (alert) -> Void
in
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .PhotoLibrary
self.presentViewController(imagePicker,
animated:
true
,
completion: nil)
}
imagePickerActionSheet.addAction(libraryButton)
// 5
let cancelButton = UIAlertAction(title:
"Cancel"
,
style: .Cancel) { (alert) -> Void
in
}
imagePickerActionSheet.addAction(cancelButton)
// 6
presentViewController(imagePickerActionSheet, animated:
true
,
completion: nil)
|
这段代码给用户展现两个或三个选项,这取决于他们设备的能力,下面是这段代码的具体讲解:
1、如果你当前正在编辑textView或者textField,那么关闭键盘并将视图移动到原来的位置。
2、使用actionSheet样式创建一个UIAlertController来为用户提供一组选项。
3、如果用户的设备有相机,那么在imagePickerActionSheet上添加Take Photo按钮,选中该按钮时将创建并展示一个UIImagePickerController实例,并且类型为sourceType .Camera。
4、为imagePickerActionSheet添加Choose Existing按钮,选中该按钮时将创建并展示一个UIImagePickerController实例,并且类型为sourceType .PhotoLibrary。
5、为imagePickerActionSheet添加Cancel按钮,设置其样式为.Cancel后,即使你不指定任何actions,选中该按钮依旧会关闭UIImagePickerController(译者注:个人感觉这里作者的意思是想说关闭UIAlertController)。
6、最后,展示UIAlertController实例。
编译并运行你的app,点击Snap/Upload a picture of your Poem按钮,你将看到你新添加的UIAlertController,如下图:
如果你运行在虚拟机上,那么没有可以用的物理相机,所以你不会看到Take Photo选项。
正如之前讲Tesseract的局限时提到的那样,为优化OCR结果,图片必须有一定的大小限制。如果一张图片太大或者太小,Tesseract可能返回一个错误的结果,甚至直接使整个程序崩掉并抛出EXC_BAD_ACCESS错误。
因此,你需要一个方法来重新设定图片大小,当然了不能改变图片的整体比例,这是为了使图片尽可能不失真。
保持比例放缩图片
图像的整体比例(aspect ratio)是指它的宽高比,从数学定义上来说,为了减小原图像的大小而不改变整体比例,你必须维持宽高比为一常数。
当你知道原图像的宽和高,以及最终图像的目标宽度或者高度时,你可以像下面一样重新组织整体比例的等式:
这样就有了两个公式:Height1/Width1 * width2 = height2和Width1/Height1 * height2 = width2。你将在你的缩放方法中使用这两个公式来维持图片的整体比例。
依然是在ViewController.swift中,为该类添加下面的工具方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func scaleImage(image: UIImage, maxDimension: CGFloat) -> UIImage {
var
scaledSize = CGSize(width: maxDimension, height: maxDimension)
var
scaleFactor: CGFloat
if
image.size.width > image.size.height {
scaleFactor = image.size.height / image.size.width
scaledSize.width = maxDimension
scaledSize.height = scaledSize.width * scaleFactor
}
else
{
scaleFactor = image.size.width / image.size.height
scaledSize.height = maxDimension
scaledSize.width = scaledSize.height * scaleFactor
}
UIGraphicsBeginImageContext(scaledSize)
image.drawInRect(CGRectMake(0, 0, scaledSize.width, scaledSize.height))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return
scaledImage
}
|
给定MaxDimension,该方法找出图像的宽度和高度的较大者,将较大者设置为MaxDimension参数的值,接下来基于原整体比例适当地放缩另一边,重绘原图像到新计算的frame中,最后将新创建的放缩好了的图片返回给调用者。
呼~
既然我们已经得到了我们需要的一切(此处应有掌声…),接下来可以开始实现Tesseract部分了。
实现Tesseract OCR
在ViewController.swift的底部找到UIImagePickerControllerDelegate扩展,在扩展中添加下面的方法:
1
2
3
4
5
6
7
8
9
10
11
|
func imagePickerController(picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
let selectedPhoto = info[UIImagePickerControllerOriginalImage] as! UIImage
let scaledImage = scaleImage(selectedPhoto, maxDimension: 640)
addActivityIndicator()
dismissViewControllerAnimated(
true
, completion: {
self.performImageRecognition(scaledImage)
})
}
|
imagePickerController(_:didFinishPickingMediaWithInfo:) 是 UIImagePickerDelegate的一个代理方法,在info字典对象中返回选中图片的信息。你可以从info字典中使用UIImagePickerControllerOriginalImage键来获取选中图片,然后使用scaleImage(_:maxDimension:)来放缩图片。
通过调用addActivityIndicator()来在Tesseract工作时打断用户交互,向用户展示活动指示器。接下来关闭UIImagePicker视图控制器,将图片传给performImageRecognition()(稍后你将实现这一方法)来处理。
下面,在类的主声明中添加下面方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
func performImageRecognition(image: UIImage) {
// 1
let tesseract = G8Tesseract()
// 2
tesseract.language =
"eng+fra"
// 3
tesseract.engineMode = .TesseractCubeCombined
// 4
tesseract.pageSegmentationMode = .Auto
// 5
tesseract.maximumRecognitionTime = 60.0
// 6
tesseract.image = image.g8_blackAndWhite()
tesseract.recognize()
// 7
textView.text = tesseract.recognizedText
textView.editable =
true
// 8
removeActivityIndicator()
}
|
这里就是OCR变魔法的地方!因为这是本教程中的核心部分,接下来是每一部分的详细讲解:
Your poem vil impress vith French! Ze language ov love! *Haugh* *Haugh* *Haugh*
1、初始化tesseract为一个新的G8Tesseract对象。
2、Tesseract将从.traineddata文件中寻找你在该参数中指定的语言,指定为eng和fra将从"eng.traineddata" 和 "fra.traineddata"包含的数据中分别检测英文和法文,法语转换数据(trained data)已经被包含到该工程中了,因为本教程中你将使用的示例诗词中包含一部分法语(Très romantique!),法语中的重读符号不在英语字母集中,因此为了能展示出这些重读符号,你需要连接法语的.traineddata文件。将法语数据包含进来也是很好的,因为.traineddata中有一部分涉及到了语言词汇。
3、你可以指定三种不同的OCR工作模式:.TesseractOnly是最快但最不精确的方法;.CubeOnly要慢一些,但更精确,因为它使用了更多的人工智能;.TesseractCubeCombined同时使用.TesseractOnly和.CubeOnly来提供最精确的结果,不过这也导致了它成为三种工作方式中最慢的一种。
4、Tesseract假定处理的文字是均匀的一段文字,但是你的样例诗中分了多段。Tesseract的pageSegmentationMode可以让它知道文字是怎么样被划分的。所以这里设置pageSegmentationMode为.Auto来支持自动页划分(automatic page segmentation),这样Tesseract就有能力识别段落分割了。
5、这里你通过设定maximumRecognitionTime来限制Tesseract识别图片的时间为一有限的时间。不过这样设定以后,只有Tesseract引擎被限制了,如果你正在使用.CubeOnly 或 .TesseractCubeCombined工作模式,那么即使Tesseract已经达到了maximumRecognitionTime指定的时间,立体引擎(Cube engine)依然会继续处理。
6、如果文字和背景相差很大,那么你将得到Tesseract处理的最好结果。Tesseract有一个内置的滤镜,g8_blackAndWhite(),降低图片颜色的饱和度,增加对比度,减少亮度。这里你在Tesseract图像识别过程开始之前,将滤镜处理后的图像赋值给Tesseract对象的image属性。
7、需要注意的是,图像识别是一个同步的过程,所以此时识别后的文本已经是可用的了。你将识别出来的文本填充到textView中,同时设置textView为可编辑,这样你的用户就可以照他(她)喜欢来编辑了。
8、最后,移除活动指示器来表明OCR已经完成识别图像的过程,可以让用户编辑他们的诗词了。
现在是时候检测一下你写的这段代码,看看会发生些什么了。
处理你的第一张图像
本教程的示例图像,可以从Image Resources\Lenore.png中找到。找到下面这一张图片:
Lenore.png图片包含一首写给"Lenore"的情诗--不过稍作修改后你可以得到一首足以引起你喜欢的人的注意的诗了!:]
尽管你可以手动打出这张图片的一份拷贝,然后使用app来截一张图后执行OCR,不过何不简化一下呢?把图片添加到设备的相册中,以减少其中潜在的人为错误、光照变换、文本扭曲、打印瑕疵等问题。如果你使用模拟器,仅仅把图片文件拖到模拟器上即可。
编译并运行你的app,选择Snap/Upload a picture of your Poem 然后选择Choose Existing从图片库中选取示例图片并开始Tesseract解析,首次运行时你需要允许app访问图片库,你将看到在你选择完图片之后活动指示器开始旋转。
然后…成了!最终破译后的文本出现在textView中--看上去Tesseract的OCR成功了。
但是如果你的爱人不叫Lenore,他或她可能不会很感谢你的这首诗,并且他们可能想要知道这个Lenore是谁!:]
考虑的Lenore频繁地出现在扫描的文本中,把这首诗自定成你爱人喜欢地那样可能要花上一番功夫…
你会问,那怎么办?当然了,你可以实现一个节省时间的方法来找到并替换这些单词!非常棒的主意!下一部分将向你展示怎么样做到这一点。
找到并替换文本
既然OCR引擎已经把图像转换为textView中的文本,你可以将其当做其他普通字符串一样对待。
打开ViewController.swift,可以看到已经为你准备好了一个swapText()方法,这个方法被连接到Swap按钮上,非常方便。:]
将swapText()的实现替换为下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@IBAction func swapText(sender: AnyObject) {
// 1
if
textView.text.isEmpty {
return
}
// 2
textView.text =
textView.text.stringByReplacingOccurrencesOfString(findTextField.text,
withString: replaceTextField.text, options: nil, range: nil)
// 3
findTextField.text = nil
replaceTextField.text = nil
// 4
view.endEditing(
true
)
moveViewDown()
}
|
上面的代码非常直接,接下来花一点时间一步一步地看一遍代码:
1、如果textView为空,说明没有需要替换的文本,那么简单地跳出该方法即可。
2、否则,在textView中找到你在findTextField中键入的字符串,将它们替换为你在replaceTextField中输入的字符串。
3、接下来, 一旦替换完成,清空findTextField 和 replaceTextField里的值。
4、最后,关闭键盘,将视图移回到原来的位置,就像之前在takePhoto()中一样,确保当键盘关闭后视图被正确放置。
注意:点击背景也会关闭键盘并将视图移动到原来的位置,这是借由在界面上其他元素之下的一个UIButton实现的,该按钮会触发ViewController.swift中的backgroundTapped()方法。
编译并运行app,再次选中示例图片,让Tesseract工作。一旦文本出现后,在Find this…输入框中输入Lenore(注意待替换文本是大小写敏感的),然后在Replace with…输入框中输入你爱人的名字,点击Swap按钮完成替换。
说变就变--你已经创造了为你的甜心量身定做、私人订制的一首情诗。
随便试着替换一下其他需要替换的单词和名字,完成之后--呃,完成之后接下来做些什么呢?如此一首有创造性的、勇敢的、充满艺术气息的诗不应该孤零零地在你的设备上,你需要一种方式来与世界分享你的杰作。
分享最终作品
在最后的这一部分,你将创建一个UIActivityViewController来允许你的用户分享他们的新作品。
在ViewController.swift中,将sharePoem()方法的实现替换为下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@IBAction func sharePoem(sender: AnyObject) {
// 1
if
textView.text.isEmpty {
return
}
// 2
let activityViewController = UIActivityViewController(activityItems:
[textView.text], applicationActivities: nil)
// 3
let excludeActivities = [
UIActivityTypeAssignToContact,
UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList,
UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo]
activityViewController.excludedActivityTypes = excludeActivities
// 4
presentViewController(activityViewController, animated:
true
,
completion: nil)
}
|
依次看一下上面的数字注释:
1、如果textView是空的,那就不分享任何东西。
2、否则,创建一个UIActivityViewController的实例,将textView中的文本放在数组中作为要被分享的项目。
3、UIActivityViewController有一长串内置的活动类型,你可以不包含UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll, UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr, 和 UIActivityTypePostToVimeo,因为这里它们没有什么意义。
4、最后,展示你的UIActivityViewController来让用户分享他们的作品。
再次编译并运行app,通Tesseract识别图片,如果你想,你可以再次执行查找和替换过程。当你对文本满意之后,点击share按钮。
好了,你的"一见钟情"app已经完成了--肯定可以赢得你爱慕的人的心。
如果你像我一样,你可能会把Lenore的名字替换为自己的名字,用其他账号把这首诗发到自己的邮件箱中,独自一人度过情人节的夜晚。吃一碗拌饭,喝一杯红酒,视线逐渐模糊,然后假装这封信是来自英国的女王,来邀请你去豪华的,充满浪漫、舒适、神秘和惊喜的夜晚。不过可能也只有我了…
何去何从
你可以从这里下载工程的最终版本。
你可以在github上找到Tesseract的iOS版的压缩包,地址是https://github.com/gali8/Tesseract-OCR-iOS,你也可以从谷歌的Tesseract OCR网站上下载更多的语言包,使用版本3.02及以上来保证与当前库的兼容性。
使用其他的诗、歌以及文本片段来试一下;试着使用你的照相机照几张相;或者使用你的图片库中的图片。你将看到Tesseract在不同的图片中拥有怎样不同的表现。
Examples of potentially problematic image inputs that can be corrected for improved results. Source: Google’s Tesseract OCR site
记住:"错误的输入必然导致错误的输出"。提高输出质量的最简单的方式是提高输入的质量。正如谷歌在他们的Tesseract网站上列出的那样,有很多方式可以提高你的输入质量:黑暗或者微弱的光照,图片干扰,歪曲文本方向,厚厚的黑边框都会导致低质量的结果。
你可以查找图片预处理的相关资料,或者实现自己的人工智能逻辑,例如神经网络或者充分利用Tesseract的优化工具,来帮助你的程序纠正错误,提高成功几率。又或者,因为即使是很小的图片亮度、颜色、对比度、曝光等的变化都会导致输出的不同,你可以将图片经过多种滤镜处理,然后对比结果来决定最精确的输出。通过使用以上一个或几个策略,你可能得到最好的输出结果,所以试一下这些方法看一下哪一个对你的应用最好。
Tesseract非常强大,但是OCR的潜能是无限的,当你在你的软件中使用Tesseract,提高Tesseract OCR的性能时,时刻思考你是否能通过你的眼睛、耳朵甚至指尖来破译这些字符。你已经是一个字符识别的专家了,完全可以教你的电脑更多它还不知道的东西。