You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何结合PDFKit与Vision OCR提取带结构化格式的单页PDF表格文本?

如何结合PDFKit与Vision OCR提取带结构化格式的单页PDF表格文本?

我太懂你这种折腾了好几天却卡在半路上的感觉了!PDF的“无结构化”特性确实是个大坑,用PDFKit直接拿string只能得到一堆乱序的文本,完全没法还原表格结构。你想到用Vision OCR是对的方向,但问题确实出在PDF转图片的质量和OCR的结构化处理上,咱们一步步来解决:

一、先搞定PDF转高清图片的问题

你当前的转图代码有几个影响质量的小问题:比如背景色用了lightText(偏灰),和文本对比度不够;渲染分辨率没做针对性提升;还有坐标系的处理可以更简洁。试试下面这个优化后的版本:

func convertPDFToHighResImage(url: URL) -> UIImage? {
    guard let pdfDocument = PDFDocument(url: url) else { return nil }
    guard let pdfPage = pdfDocument.page(at: 0) else { return nil }
    
    let mediaBox = pdfPage.bounds(for: .mediaBox)
    // 提高渲染分辨率,这里用2x(视网膜屏级别),可以根据需求调到3x
    let scale: CGFloat = UIScreen.main.scale * 2
    let scaledSize = CGSize(width: mediaBox.width * scale, height: mediaBox.height * scale)
    
    // 配置渲染器,优先用广色域,保证颜色对比度
    let rendererFormat = UIGraphicsImageRendererFormat()
    rendererFormat.preferredRange = .extended
    rendererFormat.scale = scale
    
    let renderer = UIGraphicsImageRenderer(size: scaledSize, format: rendererFormat)
    let image = renderer.image { ctx in
        // 用纯白色背景,和黑色文本形成最高对比度,利于OCR识别
        UIColor.white.setFill()
        ctx.fill(CGRect(origin: .zero, size: scaledSize))
        
        // 调整坐标系,不用手动翻转,直接用PDF的原生坐标系
        ctx.cgContext.concatenate(pdfPage.transform(for: .mediaBox))
        pdfPage.draw(with: .mediaBox, to: ctx.cgContext)
    }
    
    return image
}

关键改进点:

  • 纯白色背景替代浅灰色,最大化文本与背景的对比度,OCR识别准确率会提升很多
  • 基于屏幕分辨率再放大2倍(可按需调整到3x),保证图片足够清晰
  • pdfPage.transform(for: .mediaBox)直接处理坐标系,避免手动翻转出错
  • 开启广色域渲染,减少颜色失真

二、优化Vision OCR的识别与结构化处理

你当前的OCR请求太基础了,没有开启高精度识别,也没利用位置信息来还原表格结构。咱们调整请求参数,同时通过文本块的位置来重建表格的行列:

func extractStructuredTableText(from image: UIImage, completion: @escaping ([[String]]?) -> Void) {
    guard let cgImage = image.cgImage else {
        completion(nil)
        return
    }
    
    // 配置高精度OCR请求
    let request = VNRecognizeTextRequest { request, error in
        guard error == nil, let observations = request.results as? [VNRecognizedTextObservation] else {
            completion(nil)
            return
        }
        
        // 1. 把每个观察结果转换成「文本+位置」的元组,同时转换坐标系
        var textWithFrames: [(text: String, frame: CGRect)] = []
        let imageSize = CGSize(width: cgImage.width, height: cgImage.height)
        
        for observation in observations {
            guard let topCandidate = observation.topCandidates(1).first else { continue }
            // 把Vision的左下角原点坐标系,转换成UIKit的左上角原点
            let normalizedFrame = observation.boundingBox
            let convertedFrame = CGRect(
                x: normalizedFrame.minX * imageSize.width,
                y: (1 - normalizedFrame.minY - normalizedFrame.height) * imageSize.height,
                width: normalizedFrame.width * imageSize.width,
                height: normalizedFrame.height * imageSize.height
            )
            textWithFrames.append((text: topCandidate.string, frame: convertedFrame))
        }
        
        // 2. 按行分组:把Y坐标接近的文本块归为同一行
        textWithFrames.sort { $0.frame.minY > $1.frame.minY } // 从上到下排序
        var rows: [[(text: String, frame: CGRect)]] = []
        let rowThreshold: CGFloat = 15 // 可根据PDF字体大小调整,判断是否属于同一行的Y差阈值
        
        for textFrame in textWithFrames {
            if let lastRow = rows.last, let firstInLastRow = lastRow.first {
                let yDiff = abs(textFrame.frame.minY - firstInLastRow.frame.minY)
                if yDiff <= rowThreshold {
                    rows[rows.count - 1].append(textFrame)
                } else {
                    rows.append([textFrame])
                }
            } else {
                rows.append([textFrame])
            }
        }
        
        // 3. 每行内按X坐标排序(从左到右),还原表格列顺序
        let structuredTable = rows.map { row in
            let sortedRow = row.sorted { $0.frame.minX < $1.frame.minX }
            return sortedRow.map { $0.text }
        }
        
        completion(structuredTable)
    }
    
    // 配置OCR核心参数
    request.recognitionLevel = .accurate
    request.recognitionLanguages = ["en-US"] // 对应PDF的实际语言,中文用["zh-Hans"]
    request.usesLanguageCorrection = true // 开启语言纠错,提升文本准确率
    
    let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
    DispatchQueue.global(qos: .userInitiated).async {
        do {
            try handler.perform([request])
        } catch {
            print("OCR执行失败:\(error.localizedDescription)")
            completion(nil)
        }
    }
}

核心逻辑说明:

  • 先给每个OCR识别到的文本块绑定位置信息,这是还原表格的关键
  • 通过Y坐标的差值判断文本块是否属于同一行,聚类出表格的行
  • 每行内按X坐标从左到右排序,还原表格的列顺序,最终输出的[[String]]就是和原表格完全对应的二维数组

三、整合调用示例

把两个函数串起来,就能得到结构化的表格数据了:

func processPDFTable(url: URL) {
    guard let highResImage = convertPDFToHighResImage(url: url) else {
        print("PDF转高清图片失败")
        return
    }
    
    extractStructuredTableText(from: highResImage) { structuredTable in
        guard let table = structuredTable else {
            print("OCR结构化处理失败")
            return
        }
        
        // 打印结构化结果,也可以直接映射到业务模型中
        DispatchQueue.main.async {
            print("结构化表格结果:")
            for row in table {
                print(row.joined(separator: " | "))
            }
        }
    }
}

可微调的参数提示

  • 如果表格字体特别小,可以把转图的scale调到3x,进一步提升清晰度
  • rowThreshold的数值可以根据PDF的实际行间距调整,字体大的表格可以设到20
  • OCR的recognitionLanguages必须对应PDF的实际语言,否则识别准确率会暴跌

内容来源于stack exchange

火山引擎 最新活动