用苹果官方 API 实现 iOS 备忘录的扫描文稿功能
访问我的博客 www.fatbobman.com[1] ,可以获得更好的阅读体验
iOS 系统自带的备忘录(Notes)在其质朴名称下提供了众多强大的功能,扫描文稿是我使用较多的功能之一。很早前便想在【健康笔记[2]】之中提供类似的功能,但考虑到其涉及的知识点较多,迟迟没有下手。最近在空闲时,将近年 WWDC 中涉及该功能实现的专题梳理、学习了一遍,受益匪浅。苹果官方早已为我们准备了所需的一切工具。本文将介绍如何通过 VisionKit、Vision、NaturalLanguage、CoreSpotlight 等系统框架实现与备忘录扫描文稿类似的功能。
用 VisionKit 拍摄适合识别的图片
VisionKit 介绍
VisionKit 是一个小框架,可以让你的应用程序使用系统的文档扫描仪。使用 VNDocumentCameraViewController 呈现覆盖整个屏幕的相机视图。通过在视图控制器中实现 VNDocumentCameraViewControllerDelegate,接收来自文档相机的回调,例如完成扫描。
通过同备忘录(Notes)一致的文档扫描外观,让开发者获得拍摄及图片处理能力(透视变换、颜色处理等)。
IMG_1938
VisionKit 使用方法
VisionKit 框架目标明确、无需配置,使用异常简单。
在 app 中申请相机的使用权限
在 info 中添加 NSCameraUsageDescription 键,填写使用相机的原因。
image-20211109184955837
创建 VNDocumentCameraViewController
VNDocumentCameraViewController 并没有提供任何的配置选项,只需要声明一个它的实例便可使用。
下面的代码为在 SwiftUI 中使用的方式:
代码语言:javascript复制import VisionKit struct VNCameraView: UIViewControllerRepresentable { @Binding var pages:[ScanPage] @Environment(.dismiss) var dismiss typealias UIViewControllerType = VNDocumentCameraViewController func makeUIViewController(context: Context) -> VNDocumentCameraViewController { let controller = VNDocumentCameraViewController() controller.delegate = context.coordinator return controller } func updateUIViewController(_ uiViewController: VNDocumentCameraViewController, context: Context) {} func makeCoordinator() -> VNCameraCoordinator { VNCameraCoordinator(pages: $pages,dismiss: dismiss) }} struct ScanPage: Identifiable { let id = UUID() let image: UIImage}
实现 VNDocumentCameraViewControllerDelegate
VNDocumentCameraViewControllerDelegate 提供了三个回调方法
•documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan)告诉委托,用户已成功从文档相机保存扫描的文档•documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController)告诉委托,用户已从文档扫描仪相机中取消。•documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error)告诉委托,当相机视图控制器处于活动状态时,文档扫描失败。
代码语言:javascript复制final class VNCameraCoordinator: NSObject, VNDocumentCameraViewControllerDelegate { @Binding var pages:[ScanPage] var dismiss:DismissAction func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { for i in 0..<scan.pageCount{ let scanPage = ScanPage(image: scan.imageOfPage(at: i)) pages.append(scanPage) } dismiss() } func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { dismiss() } func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) { dismiss() } init(pages:Binding<[ScanPage]>,dismiss:DismissAction) { self._pages = pages self.dismiss = dismiss }}
VisionKit 允许使用者连续扫描图片。通过 pageCount 可以查询图片数量,并用 imageOfPage 分别获取。
用户应将扫描图片的方向调整到正确的显示状态,便于下一步的文字识别。
在视图中调用
代码语言:javascript复制struct ContentView: View { @State var scanPages = [ScanPage]() @State var scan = false var body: some View { VStack { Button("Scan") { scan.toggle() } List { ForEach(scanPages, id: .id) { page in HStack{ Image(uiImage: page.image) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 100) } } } .fullScreenCover(isPresented: $scan) { VNCameraView(pages: $scanPages) .ignoresSafeArea() } } }}
至此,你已经获得了同 Notes 完全一致的拍摄扫描图片的功能。
用 Vision 进行文字识别
Vision 介绍
相较 VisionKit 的小巧,Vision 则是一个功能强大、使用范围广泛的大型框架。它应用了计算机视觉算法,对输入的图像和视频执行各种任务。
Vision 框架可以执行人脸和人脸特征点检测、文本检测、条形码识别、图像配准和目标跟踪。Vision 还允许使用自定义的 Core ML 模型来完成分类或物体检测等任务。
在本例中,我们仅需使用 Vision 提供的文本检测(text detection)功能。
如何使用 Vision 进行文字识别
Vision 能够检测和识别图像中的多语言文本,识别过程完全在设备本地进行,保证了用户的隐私。Vision 提供了两种文本的检测路径(算法),分别为 Fast(快速)和 Accurate(精确)。快速非常适合实时读取号码之类的场景,在本例中,由于我们需要对整个文档进行文字处理,选择使用神经网络算法的精确路径更加合适。
在 Vision 中无论进行哪个种类的识别计算,大致的流程都差不太多。
•为 Vision 准备输入图像Vision 使用 VNImageRequestHandler 处理基于图像的请求,并假定图像是直立的,所以在传递图像时要考虑到方向。在本例中,我们将使用 VNDocumentCameraViewController 提供的图像进行处理。•创建 Vision Request首先使用要处理的图像创建一个 VNImageRequestHandler 对象。接下来创建 VNImageBasedRequest 提出识别需求(request)。针对每种识别类型都有对应的 VNImageBasedRequest 子类,本例中,识别文本对应的 request 为 VNRecognizeTextRequest。可以对同一张图片提出多个 request,只需创建并捆绑所有的请求到 VNImageRequestHandler 的实例即可。•解释检测结果可以通过两种方式访问检测结果:一、调用 perform 后检查 results 属性。二、在创建 request 对象时,设置回调方法检索识别信息。回调结果可能包含多个观察结果(observations),需要循环观察数组以处理每个观察结果。
大概的代码如下:
代码语言:javascript复制import Vision func processImage(image: UIImage) -> String { guard let cgImage = image.cgImage else { fatalError() } var result = "" let request = VNRecognizeTextRequest { request, _ in guard let observations = request.results as? [VNRecognizedTextObservation] else { return } let recognizedStrings = observations.compactMap { observation in observation.topCandidates(1).first?.string } result = recognizedStrings.joined(separator: " ") } request.recognitionLevel = .accurate // 采用精确路径 request.recognitionLanguages = ["zh-Hans", "en-US"] // 设置识别的语言 let requestHandler = VNImageRequestHandler(cgImage: cgImage) do { try requestHandler.perform([request]) } catch { print("error:(error)") } return result}
每个被识别的文本段可能包含多个识别结果,通过 topCandidates(n) 设置最多返回几个候选结果。
recognitionLanguages 定义了语言处理和文本识别过程中语言的使用顺序,识别中文时,需将中文设置在首位。
需要识别的文档:
截屏 2021-11-09 下午 4.37.28
此类文档并不适合进行自然语言处理(除非进行大量的深度学习),但健康笔记中将主要保存的此种类型的内容。
识别结果:
代码语言:javascript复制InBody 身体水分 TinBody770) ID 15904113359 身高 年