程序员开发实例大全宝库

网站首页 > 编程文章 正文

使用 Core ML 的 Vision 和 Tesseract OCR 构建 iOS 相机计算器

zazugpt 2024-08-22 04:40:13 编程文章 17 ℃ 0 评论

数学可能很可怕,但它是日常生活的重要组成部分。如果我们可以构建一个应用程序,将手机的摄像头对准一个表情,然后让应用程序计算结果,那不是很酷吗?每当我需要使用数学时,我都希望这是可能的。现在,随着 iOS 中机器学习和视觉识别的进步,这是可行的。

在本指南中,我将提供一些在 iOS 中使用 Vision 的技术细节,以及我使用该技术的个人经验。

您可以在我的 GitHub (MathSolver) 上找到最终项目。您可以在阅读本指南时作为参考。该项目使用 Swift 4.1 和 iOS 11 中的基础 SDK。有 ViewController 包含和多个服务类来分解职责,因此您可以轻松跟进。

作为参考,OCR 代表光学字符识别 - 将图像转换为可读文本的过程。我们将在全文中使用这个缩写。

什么是愿景?

Vision 是 Apple 在 WWDC 2017 上为 iOS 11 推出的计算机视觉框架,作为 Apple 将机器学习引入 iOS 平台的努力的一部分。

Vision 构建在 Core ML 框架之上,提供用于处理视觉任务的算法和实用程序,例如面部和地标检测、图像识别、文本检测、条形码识别和一般特征跟踪。在推出 Vision 之前,开发人员可能需要使用 OpenCV 等一些第三方框架来实现这些功能,但现在我们已经拥有了 iOS 中计算机视觉任务所需的一切。

在 WWDC 2018 上,Apple 推出了 Vision 框架的增强功能,以及适用于 iOS 12 的 Core ML 2.0。但是,我在本文中选择以 iOS 11 为目标,原因有两个:支持更多设备,因为 iOS 12 仍处于测试阶段且版本不高。 iOS 11 中的 Vision 通过 VNDetectTextRectanglesRequest 很好地支持文本检测

首先,让我们使用 AVFoundation 设置我们的项目以进行相机跟踪。

使用 AVFoundation 捕获相机帧 首先,我们需要设置一个相机会话,因为我们需要捕获图片以进行文本识别。相机逻辑及其预览层封装在自定义视图控制器 CameraController 中。

private func setupAVSession() {
  captureSession.beginConfiguration()
  captureSession.sessionPreset = .high
  defer {
    captureSession.commitConfiguration()
  }  // input
  guard
    let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
    let input = try? AVCaptureDeviceInput(device: backCamera),
    captureSession.canAddInput(input)
  else {
    return
  }  captureSession.addInput(input)  // output
  let output = AVCaptureVideoDataOutput()  guard captureSession.canAddOutput(output) else {
    return
  }  captureSession.addOutput(output)
  output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "MyQueue"))
  output.alwaysDiscardsLateVideoFrames = true  // connection
  let connection = output.connection(with: .video)
  connection?.videoOrientation = .portrait  // preview layer
  cameraLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
  view.layer.addSublayer(cameraLayer)
  view.layer.addSublayer(overlayLayer)
}

这里我们选择相机作为默认的捕捉输入设备。请记住将 videoGravity 设置为 resizeAspectFill 以获得全屏预览图层。为了从相机获取捕获的缓冲区,我们的视图控制器需要符合 AVCaptureVideoDataOutputSampleBufferDelegate。

处理方向是最棘手的,因为 AVCaptureConnection、Vision 中的 VNDetectTextRectanglesRequest 和 UIImage 具有不同的方向枚举。为了简单起见,我们将 videoOrientation 设置为纵向,这个属性在我们设置 Vision 时就会变得清晰。

每个捕获的帧通过委托函数 func captureOutput(_output: AVCaptureOutput, didOutput SampleBuffer: CMSampleBuffer, from Connection: AVCaptureConnection) 报告缓冲区信息。

现在我们有了 CMSampleBuffer,让我们在 VisionService 类中使用 Vision。

使用视觉进行文本检测

为了指示 Vision 工作,我们需要将 VNDetectTextRectanglesRequest 传递给 VNImageRequestHandler,它可以处理图像分析请求。 VNImageRequestHandler 接受 CVPixelBuffer、CGImage 和图像数据。这里我们通过CVImageBuffer从CMSampleBuffer转换为CGImage。 VNDetectTextRectanglesRequest 只是一个用于查找图像中可见文本区域的请求对象。

func handle(buffer: CMSampleBuffer) {
  guard let pixelBuffer = CMSampleBufferGetImageBuffer(buffer) else {
    return
  }
  let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
  guard let image = ciImage.toUIImage() else {
    return
  }  makeRequest(image: image)
}
Note that the orientation parameter for VNImageRequestHandler is important — more info on this can be found [here](https://developer.apple.com/documentation/vision/detecting_objects_in_still_images).private func makeRequest(image: UIImage) {
  guard let cgImage = image.cgImage else {
    assertionFailure()
    return
  }  let handler = VNImageRequestHandler(
    cgImage: cgImage,
    orientation: CGImagePropertyOrientation.up,
    options: [VNImageOption: Any]()
  )  let request = VNDetectTextRectanglesRequest(completionHandler: { [weak self] request, error in
    DispatchQueue.main.async {
      self?.handle(image: image, request: request, error: error)
    }
  })  request.reportCharacterBoxes = true  do {
    try handler.perform([request])
  } catch {
    print(error as Any)
  }
}

捕获图像的方向取决于 videoOrientation 属性的方向,默认为 .landscapeRight。如果我们故意设置connection.videoOrientation = .landscapeRight,那么我们需要对VNImageRequestHandler使用CGImagePropertyOrientation.right。理想情况下,为了支持多方向,我们需要注意设备方向的变化并相应地调整 AVCaptureConnection 的方向。

结果应该是一个 VNTextObservation 数组,其中包含文本在图像中所处位置的区域信息。对于这个演示,我只选择了有足够信心的结果。让我们在 BoxService 中绘制检测到的文本区域。

绘制检测到的文本区域

我们可以使用drawRect进行绘制,但是具有自定义边框的CALayer应该更容易。让我们为每个检测到的文本区域创建一个 CALayer,并将其绘制到 CameraController 中的覆盖层上。

let layer = CALayer()
view.layer.addSublayer(layer)
layer.borderWidth = 2
layer.borderColor = UIColor.green.cgColor

VNTextObservation 有一个 VNRectangleObservation 类型的字符框数组,其中包含单个字符边界框的信息。这是为了进行细粒度控制,以防我们想要单独突出显示每个字母。但是,作为 VNTextObservation 的子类,它是 VNDetectedObjectObservation 的子类,它还具有属性boundingBox,它是整个文本的框。我们将使用此属性来计算 CALayer 的框架。

我们可以使用AVCaptureVideoPreviewLayer中的辅助方法layerRectConverted将boundingBox转换为视图矩形,但这似乎并没有按预期工作。

我们根据boundingBox手动计算一下。

iOS 坐标从左上角开始,而boundingBox 则从左下角开始,就像在 MacOS 中一样。我们需要翻转坐标,打开我们的BoxService.swift,并添加以下代码

private func normalise(box: VNTextObservation) -> CGRect {
  return CGRect(
    x: box.boundingBox.origin.x,
    y: 1 - box.boundingBox.origin.y - box.boundingBox.height,
    width: box.boundingBox.size.width,
    height: box.boundingBox.size.height
  )
}

由于boundingBox具有从0到1的标准化值,为了映射它以构建我们的矩形CALayer,我们只需将其乘以overlayLayer的大小即可。

private func drawBox(overlayLayer: CALayer, normalisedRect: CGRect) {
  let x = normalisedRect.origin.x * overlayLayer.frame.size.width
  let y = normalisedRect.origin.y * overlayLayer.frame.size.height
  let width = normalisedRect.width * overlayLayer.frame.size.width
  let height = normalisedRect.height * overlayLayer.frame.size.height
  let outline = CALayer()
  outline.frame = CGRect(x: x, y: y, width: width, height: height).scaleUp(scaleUp: 0.1)
  outline.borderWidth = 2.0
  outline.borderColor = UIColor.red.cgColor  overlayLayer.addSublayer(outline)
}
extension CGRect {
  func scaleUp(scaleUp: CGFloat) -> CGRect {
    let biggerRect = self.insetBy(
      dx: -self.size.width * scaleUp,
      dy: -self.size.height * scaleUp
    )    return biggerRect
  }
}

下面是我的 iPhone X 的屏幕截图,其中有一些用 Sketch 编写的文字。

我们现在正在处理肖像图像,因此数学非常简单。如果您的项目支持横向图像,那么您可能需要相应地翻转规范化矩形的坐标。

仍然在 BoxService 中,我们应该在检测到的矩形中裁剪图像以进行 OCR。 Vision检测到的区域的矩形与文本紧密匹配,因此我们需要增加一点矩形以容纳顶部和底部边缘,这使得文本识别更容易工作。

private func cropImage(image: UIImage, normalisedRect: CGRect) -> UIImage? {
    let x = normalisedRect.origin.x * image.size.width
    let y = normalisedRect.origin.y * image.size.height
    let width = normalisedRect.width * image.size.width
    let height = normalisedRect.height * image.size.height
    let rect = CGRect(x: x, y: y, width: width, height: height).scaleUp(scaleUp: 0.1)    guard let cropped = image.cgImage?.cropping(to: rect) else {
      return nil
    }    let croppedImage = UIImage(cgImage: cropped, scale: image.scale, orientation: image.imageOrientation)
    return croppedImage
  }

裁剪检测到的文本图像

视觉可以检测屏幕上的许多文本,但为了简单起见,让我们处理最大的图像,因为那通常包含首选表达方式。为了获得裁剪图像的框架,我们将标准化矩形的坐标与捕获的图像大小相乘。

croppedImage 应包含文本 - 您可以使用 Xcode 中的“快速查找”进行检查,方法是在底部表达式面板中选择croppedImage 变量,然后单击眼睛图标打开图像的“快速查找”。

使用 OCR 框架进行文本识别

我个人喜欢与 Swift 配合良好的解决方案,因此我首先尝试了 SwiftOCR。它纯粹用 Swift 编写,API 更容易上手。 。 README 中的基准测试甚至指出它的性能甚至比 Tesseract 还要好。

let instance = SwiftOCR()
private func handleWithSwiftOCR(image: UIImage) {
  instance.recognize(image, { string in
    DispatchQueue.main.async {
      self.delegate?.ocrService(self, didDetect: string)
    }
  })
}

由于某些原因,SwiftOCR 运行不佳。这可能是因为我在最初的草图中使用了“Lato”字体。 SwiftOCR允许对新字体进行自定义训练,但是因为我很懒,所以我尝试了Tesseract。

Tesseract“是一个适用于各种操作系统的光学字符识别引擎。它是免费软件,根据 Apache 许可证 2.0 版发布,自 2006 年以来一直由 Google 赞助开发”。

iOS 端口在 GitHub 上开源,并支持 CocoaPods。只需在 Podfile 中安装“TesseractOCRiOS”即可。

正如 README 和 TestsProject 中所解释的,需要 tessdata——它包含使 Tesseract 工作的语言信息。如果没有此 tessdata,TesseractOCR 框架将向您发出有关缺少 TESSDATA_PREFIX 的警告。

在此处下载 tessdata,并将其添加为您的 Xcode 项目的引用。蓝色表示该文件夹已作为参考添加。

您可能还需要将 libstdc++.dylibCoreImage.framework 添加到您的目标中。此外,该库与 Xcode 中的 Bitcode 不兼容,因此您需要在目标设置中禁用 Bitcode。

超立方体

使用 Tesseract 很简单。请记住导入 TesseractOCR,而不是 TesseractOCRiOS:

let tesseract = G8Tesseract(language: "eng")!
tesseract.engineMode = .tesseractCubeCombined
tesseract.pageSegmentationMode = .singleBlockprivate func handleWithTesseract(image: UIImage) {
  tesseract.image = image.g8_blackAndWhite()
  tesseract.recognize()
  let text = tesseract.recognizedText ?? ""
  delegate?.ocrService(self, didDetect: text)
}

g8_blackAndWhite 是一个方便的滤镜,可以增加图像的对比度以便于检测。对于 pageSegmentationMode,我使用 singleBlock,因为我们的数字应该位于统一的文本块中(您也可以尝试 singleLine 模式)。最后,我们将 engineMode 设置为 tesseractCubeCombined,这是最准确的,但可能需要一些时间。您可以设置为 tesseractOnly 或cubeOnly 以牺牲速度。在我的测试中,它可以很好地识别手写文本和我使用 Arial、Lato 和 Helvetica 等流行字体放在屏幕上的文本。

如果您需要支持更多语言和字体,请访问 Training Tesseract wiki 页面以了解更多信息。我还希望苹果提供合适的 OCR 模型来与 Vision 和 Core ML 一起使用,因为文本识别是移动应用程序的一项流行任务。

通过上面捕获的图像,Tesseract 应该能够识别字符串“(1+2)*3”。让我们将其验证为正确的数学表达式并尝试解决它。

验证表达式 有时,Tesseract 在结果中包含换行符或一些其他格式错误的字符。因此,您应该正确验证该表达式。现在让我们执行一个简单的验证。在演示中,我们支持对 0…9 的数字和数学运算符 +、-、*、/ 进行简单计算。您可以基于代码构建更复杂的表达式,如幂、对数、西格玛。

public func validate(expression: String) -> String {
  let set = Set("0123456789()+-*/")
  return expression
    .replacingOccurrences(of: "/n", with: "")
    .filter({ set.contains($0) })
}

求解表达式

现在到了有趣的部分——数学。为了解决这个表达式,我们需要使用逆波兰表示法。根据维基百科,

逆波兰表示法 (RPN) 也称为波兰后缀表示法或简称为后缀表示法,是一种数学表示法,其中运算符位于其操作数之后,而波兰表示法 (PN) 则运算符位于其操作数之前。

基本上,像 (1+2)3 这样的中缀表达式需要转换为后缀表达式,即 12+3 。下面是伪代码:

该算法非常简单。从字符串 12+3* 的开头开始,我们首先将 1 和 2 压入堆栈。然后我们遇到运算符+,将1和2从堆栈中弹出,并计算为1+2。结果3被推回堆栈。然后我们将下一个操作数 3 压入堆栈。最后,当我们遍历到运算符 * 时,我们从堆栈中弹出值 3 的 2 个操作数,计算 3*3,得到结果 9。

但是我们如何将中缀表达式转换为后缀表达式呢?有一种称为 Shunting-yard 的算法可以解决这个问题。

现在,让我们使用 NSString-Reverse-Polish-Notation 中的一个片段。它是在 Objective C 中,但我们只需要添加一个 MathSolver-Bridging-Header.h,因为 Swift 和 Objective C 是兼容的。

当我们运行该应用程序时,它会识别屏幕上所有可能的文本,但我们只需要关注我们的表情,即最大的文本。将摄像头对准表情,应用程序应该能够检测、识别并解决表情问题。如果您仍然有疑问,表达式 (1+2)*3/5 的结果是 1.8

当然,我们可以添加对更多运算符的支持,例如幂、正弦、余弦、西格玛,甚至预定义函数。

虽然这个特定的应用程序用于演示如何完成此过程,但它可以修改为更实际的用途,例如检查会议室可用性、电话号码跟踪、邮局的订单扫描。

从这往哪儿走

我希望本教程提供了一些关于 Vision 和 Tesseract 的有价值的见解。这里还有一些链接可以帮助您开始 iOS 上的文本检测之旅:

  • Tesseract OCR iOS 教程:学习如何在 iOS 中使用 Tesseract 框架,详细介绍了使用过程中可能遇到的一些问题。
  • 通过 iOS 的 ML 框架在手掌中利用机器学习:如何将 Vision 与 SwiftOCR 结合使用。 tesseract.js:用 Javascript 实现的 Tesseract。它与 iOS 无关,但在其他一些平台上有 Tesseract 的重要性是件好事。
  • 视觉中的对象跟踪:WWDC 2018 上 iOS 中的 Vision 发生了有趣的变化。对象跟踪和自定义模型训练方面有很多改进。
  • 检测静态图像中的对象:Apple 官方示例代码,用于使用 Vision 框架定位和划分图像中的矩形、面部、条形码和文本。
  • 在 iOS 中集成 Google ML Kit 以实现人脸检测、文本识别等:谷歌在今年的 Google IO 上推出了 ML Kit,它也擅长文本识别。该框架支持 iOS 和 Android。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表