[iOS][UIKit] 實作 VisionKit 跟 Vision 來掃描圖片跟識別文字

VisionKit & Vision

在 iOS 13 中,蘋果發佈了一個新的套件,名叫 VisionKit,它讓開發者能在 App 裡利用原生的系統掃瞄器去實作文件掃描,它能有效地讓開法者去調整掃描的參數,例如:哪些物件不希望被掃瞄到或是需要倒轉\裁切,如此一來就能更自由的去將它應用到不同的產品需求上。

在 iOS 13 裡,Vision 就已經開始支援 OCR (Optical Character Recognition),也就是常被用來作偵測或是識別文件上的文字的技術。

今天這篇就來跟大家分享在 UIKit 裡面如何製作一個簡單的文件掃瞄器,之後會再有一篇 SwiftUI 的教程,有需要的朋友們可以關注一下這個部落格,謝謝!

第一步 - 建立 project

首先,因為我們只需要時做一個單純的 VisionKit + Vision 的App,因此我們在 Xcode 內建立一個 “Single View App”,並且我們也不需要引入任何第三方套件。

但有個需要特別注意的地方,因為掃瞄器在使用時是需要啟動相機的,因此我們需要在 App 裡面設定相機權限,否則你的 App 會一直爆 crash 給你看 XD,那要怎麼做呢?很簡單,只需要在 project 的 Info.plist 裡面增加一個 key 值叫做 “ NSCameraUsageDescription”,並在後面 value的部分給上權限視窗裡需要告知使用者的文字即可。


掃描文件

接著,在實作掃描器之前,我們必須先知道 VisionKit 提供了哪些應用層面,以便我們後續開發上的速度。

VisionKit 內提供的一個自己的 View Controller,名為 “VNDocumentCameraViewController” 讓使用者可以透過系統的相機介面來獲取單/多張頁面,並生成圖片(UIImge)物件,讓我們可以做後續的處理。

在我們得到了這些圖片物件之後,意味著我們就有了包含文字的圖片,這時我們就可以用 Vision 框架來幫我們完成文字的截取,Vision 預設的 input 格式為圖片,然後執行識別操作來產生純文字的 output。另外,Vision 也提供一些參數來調整辨識的精準度跟速度,這在後續的實作中會提到。

Delegates

使用 VisionKit 提供的 View Controller 時,我們必須讓我們自訂的 View Controller 繼承 VCDocumentCameraViewControllerDelegate 來執行使用者取消/完成掃描後獲取物件的 callbacks。

  func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan)
 
- 表示使用者成功將掃描到的資料儲存成物件。
  func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error)
 
- 表示掃瞄器在啟用時發生錯誤。
  func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController)
 
- 表示使用者取消的掃瞄器的操作。

第二步 - 建立 View Controller

接著,我們建立剛剛上面提到的 VNDocumentCameraViewController :

  
   let scanVC = VNDocumentCameraViewController()
    scanVC.delegate = self
    present(scanVC, animated: true)
 

第三步 - 繼承上 delegates

接下來我們需要繼承剛剛提到的 VNDocumentCameraViewControllerDelegate,來接收我們操作的 callbacks。

只需要在宣告後面將上 delegate 即可:

   extension ViewController: VNDocumentCameraViewControllerDelegate {
        ...
    }
 
並且對每個 delegate function 實作各自的功能,但有一點需要特別注意,就是每個callbacks 被接收之後,表示掃瞄器的功能也相應的結束了,因此我們必須也同時實作 dismiss 的部分:
1.
        func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
    // available for a document only
     guard scan.pageCount >= 1 else {
         controller.dismiss(animated: true)
         return
     }
     
     // Process the scanned page
     scanImageView.image = scan.imageOfPage(at: 0)
     processImage(scan.imageOfPage(at: 0))
     controller.dismiss(animated: true)
 }
 
當使用者成功掃描到物件並且點擊“儲存”後,這個 callback function 會被調用。VNDocumentCameraScan 會包含掃描的物件 (圖片內的文字等),之後需要可以對物件作處理,比方說文字的截取。最後要記得 dismiss 掉 controller。

2. 
         func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) {
     //Handle properly error, you can print it or do something else
     controller.dismiss(animated: true)
 } 
 
當 VNDocumentCameraViewController 因為錯誤 (例如用戶關閉相機的使用權限)導致掃描無法成功執行時,這個 callback 就會被調用。

這時你需要針對 error 做後續的處理,例如跳出相應的跳窗,並執行後續的操作,並且 dismiss 掉 controller。

3. 
   func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) {
     controller.dismiss(animated: true)
 }
 
當使用者點擊“取消”的時候,這個 callback 會被調用,但不需要跳出任何跳窗或是後續的行為,但仍然要記得 dismiss 掉 controller。

第四步 - 從物件中識別出文字

剛剛我們已經得到了我們圖片 (UIImage) 型別的物件,但我們還是沒辦法得到圖片中的文字內容,因此我們需要借助 Vision 的支援來協助識別文字。

我們會為每個圖片物件建立一個物件:VNImageRequestHandler 物件。這個物件主要是將圖片透過 Vision 建立一個請求 (request) 來執行辨識。

接者我們必須透過 VCRecognizeTextRequest ,來請求 Vision 實際執行識別動作。
以上的動作都會透過非同步的方式處理,完成後我們就會得到請求的結果:

- 識別錯誤時會回傳錯誤

- 請求成功時會回傳文字物件

這時我們就可以針對識別出的文字去做邏輯的處理了。

因此我們要先在 View Controller 一開始先創建 request 的物件,他會在每次掃描的時候被重複使用:

 private var scanRequest = VNRecognizeTextRequest(completionHandler: nil)
 

接著實作請求的部分:



scanRequest = VNRecognizeTextRequest { (request, error) in
            guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
                
            var ocrText = ""
            for observation in observations {
                guard let topCandidate = observation.topCandidates(1).first else { return }
                    
                ocrText += topCandidate.string + "\n"
            }
                
                
            DispatchQueue.main.async {
                self.scanTextView.text = ocrText
                self.scanButton.isEnabled = true
            }
        }
            
        scanRequest.recognitionLevel = .accurate
        scanRequest.recognitionLanguages = ["zh-Hant", "en-US"]
        scanRequest.usesLanguageCorrection = true
    }  
 

在這裏,我們創建了一個 VNRegconizeTextRequest 包含一個 completion handler 來處理完成的請求。我們利用 VNRegconizedObservation 來響應並得到我們的文字,接著我們把得到的文字拆分並賦予在 ocrText 裏面。

第五步 - 自定義選項

  • customWords:自訂文字,當你預期的文件中有特定的文字能夠被偵測到,但他可能不屬於任何語言或是詞彙時 (比方說:QQㄋㄟㄋㄟ好喝到咩噗茶),我們可以在 customWords 中加入我們希望被識別到的文字們。

  •   textRecognitionRequest.customWords = ["Levis", "Gucci"] // An array of strings
     
  • minimumTextHeight:最低文字高度,數值是0 ~ 1之間而官方預設是 0.03125,這個參數的是讓 Vision 識別時,可以針對字體大小大於 minimumTextHeight 的文字,這樣我們就可以針對特定範圍的文字作識別,也可以加快 Vision 識別的速度跟效率,也能降低為了儲存文字物件所消耗的記憶體容量。

  •   textRecognitionRequest.minimumTextHeight = 0.03125
     
  • recognitionLevel:一個包含 .fast 跟 .accurate 的 VNRequestTextRecognitionLevel enum,用來調整 Vision 在識別文字時的優先級別,如果你希望識別的速度快一點,則設定成 .fast
        如果希望識別更精準,則設定成 .accurate。
       textRecognitionRequest.recognitionLevel = .accurate
     
  • recognitionLanguages:這個陣列包含了 Vision 在識別文字時會依照的語言,並且會一到陣列的順序優先做識別,而陣列所吃的是 ISO 語言碼 (language codes)。
       textRecognitionRequest.recognitionLanguages = ["zh-Hant", "en-US"]
     
  • usesLanguageCorrection : 這個參數是個 Boolean 值,是用來校正識別出的文字,如果設為 true,當識別出的文字有誤時, Vision 會自動幫你做校正  ;  false 的話則將識別出的文字原封不動的產出。
  •    textRecognitionRequest.usesLanguageCorrection = true
     

第六步 - 建立 request handler

為了要正確執行 Vision 的 request,我們需要建立一個 request handler 去處理即將要被識別的圖片物件,而這個 handler 需要每次掃描時都被建立。

guard let cgImage = image.cgImage else { return }

        scanTextView.text = ""
        scanButton.isEnabled = false
            
        let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        do {
            try requestHandler.perform([self.scanRequest])
        } catch {
            print(error)
        }
 

建立 handler 是個很瑣碎的工作,但一定要記得它需要 CGImageCIImage 或是 URL 來運作,因此如果你只有從 VisionKit 那邊得到的 UIImage 物件,你得用圖片的 cgImage 來進行操作。

以上就是一個簡易的 VisionKit + Vision 掃瞄器,說是簡易,但其實蠻多東西需要設定集留意的,因此可能大多數人會在實作時遇到一些問題,因此在這附上我的 Demo Project 供大家取用跟參考。

希望大家可以多多關注,如果有任何問題歡迎到我的IG私訊我或在底下留言告訴我喔!

如果想給我一些支持,也歡迎買杯咖啡給我,謝謝大家!

留言

這個網誌中的熱門文章

[iOS] 16.1 Live Activities (即時動態) 快速上手教學 (上)

[iOS] 16.1 Live Activities (即時動態) 快速上手教學 (下)