Extract document data using Vision
Machine Learning & AI 进阶 24m

使用 Vision 提取文档数据

Extract document data using Vision

2021年6月10日

在 Apple 官方观看视频

一句话判断

Vision 框架的文档分析能力在 iOS 15 终于从「能 OCR」进化到「能理解文档结构」——表格识别和结构化文本提取让发票、收据类 app 的开发成本大幅降低。

这场 Session 讲了什么

Vision 框架在文本识别方面的能力持续增强。iOS 15 引入了 VNRecognizeTextRequest 2.0,除了识别文字内容,还能分析文档的版面结构。

最大的新功能是结构化数据提取。VNDocumentSegmentationRequest 可以将文档中的文本按逻辑区域分割:标题、正文、表格、页眉页脚等。配合 VNRecognizeTextRequest,你可以把一张发票照片解析成结构化的 JSON 数据(商家名称、日期、金额、商品列表),而不仅仅是原始文字。

文本识别的精度也有提升。中文识别的支持在 iOS 15 得到了显著改善,特别是手写体中文的识别率。还新增了对日文和韩文的支持。

Session 还介绍了 VNDocumentCameraViewController(属于 VisionKit),这是一个系统级的文档扫描 UI 组件。你只需要 present 这个控制器,就能获得完整的拍照、裁切、透视矫正流程。

值得深挖的点

表格识别的技术细节

Vision 的表格识别能力基于行列检测算法。它会分析文档中的线条和文字对齐模式,推断出表格结构。识别结果包含单元格的边界框和行列索引,你可以据此重建整个表格数据。

但局限性在于:不规则的表格(合并单元格、嵌套表格)识别率很低。如果你的目标文档包含复杂表格结构,建议先用 VNRecognizeTextRequest 提取所有文字,然后用自定义逻辑解析。

自定义数据提取管线

Session 推荐的处理管线是:先用 VNDocumentSegmentationRequest 分割文档区域,然后对每个区域分别运行 VNRecognizeTextRequest。这样可以针对不同区域使用不同的后处理逻辑——比如对表格区域做行列解析,对标题区域做命名实体提取。

代码片段

// 结构化文档识别管线
import Vision

class DocumentScanner {
    // 步骤一:识别文本
    func recognizeText(image: CGImage) throws -> [VNRecognizedTextObservation] {
        let request = VNRecognizeTextRequest()
        request.recognitionLevel = .accurate
        request.recognitionLanguages = ["zh-Hans", "en"]  // 支持中英文混合
        request.usesLanguageCorrection = true
        
        let handler = VNImageRequestHandler(cgImage: image, options: [:])
        try handler.perform([request])
        return request.results ?? []
    }
    
    // 提取发票信息
    func extractInvoiceData(observations: [VNRecognizedTextObservation]) -> InvoiceData {
        var invoice = InvoiceData()
        
        for observation in observations {
            guard let text = observation.topCandidates(1).first?.string else { continue }
            
            // 根据关键词和位置提取字段
            if text.contains("金额") || text.contains("合计") {
                // 提取金额数字
                if let amount = extractNumber(from: text) {
                    invoice.totalAmount = amount
                }
            }
            
            if text.contains("日期") {
                if let date = extractDate(from: text) {
                invoice.date = date
                }
            }
            
            if text.contains("公司") || text.contains("有限公司") {
                invoice.companyName = text
            }
        }
        
        return invoice
    }
    
    func extractNumber(from text: String) -> Double? {
        let pattern = #"[\d,]+\.?\d*"#
        guard let match = text.range(of: pattern, options: .regularExpression) else { return nil }
        let numberStr = String(text[match]).replacingOccurrences(of: ",", with: "")
        return Double(numberStr)
    }
}

struct InvoiceData {
    var companyName: String = ""
    var date: String = ""
    var totalAmount: Double = 0
    var items: [(name: String, price: Double)] = []
}
// 使用 VisionKit 的文档扫描 UI
import VisionKit

class ScanViewController: UIViewController {
    func presentDocumentScanner() {
        // 检查设备是否支持文档扫描
        guard VNDocumentCameraViewController.isSupported else {
            print("设备不支持文档扫描")
            return
        }
        
        let scanner = VNDocumentCameraViewController()
        scanner.delegate = self
        present(scanner, animated: true)
    }
}

extension ScanViewController: VNDocumentCameraViewControllerDelegate {
    func documentCameraViewController(
        _ controller: VNDocumentCameraViewController,
        didFinishWith scan: VNDocumentCameraScan
    ) {
        // scan 包含用户拍摄的所有页面
        for pageIndex in 0..<scan.pageCount {
            let image = scan.imageOfPage(at: pageIndex)
            // 对每页进行 OCR 处理
            processPage(image: image)
        }
        controller.dismiss(animated: true)
    }
    
    func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
        controller.dismiss(animated: true)
    }
}

最佳实践

如果你的 app 需要扫描多种类型的文档(发票、名片、身份证),不要用一个通用的 OCR 管线。为每种文档类型设计专门的提取逻辑,利用该文档类型的已知结构(比如发票的金额通常在右下角,名片的名字通常在顶部)。

中文识别建议同时设置 recognitionLanguages["zh-Hans", "en"]。大多数中文文档会包含一些英文(公司名称、金额),设置双语言支持可以避免英文字母被错误识别为中文字符。

还有什么值得关注

  • VNRecognizeTextRequestminimumTextHeight 参数可以过滤掉太小的文字(比如水印、注释),提高识别精度。
  • VisionKit 的扫描结果会自动做透视矫正和增强,不需要自己实现 OpenCV 的文档矫正算法。
  • 如果你的 app 只需要简单的文字识别(不需要文档结构分析),用 UITextField 的 Live Text 功能就够了——iOS 15 支持在相机取景器中直接选取文字。
WWDC 2021