iOS Swift ML Kit Firebase

使用ML Kit辨識信用卡號(iOS/Swift)

Amy Lin 2019/11/11 17:52:44
285

2018年的Google I/O大會正式發表ML Kit以來,Google不斷的為行動平台開發者提供更方便快速的新功能,直至現在(2019/7月),除了最早的文字辨識、臉部偵測、條碼辨識、圖片標籤及地標辨識外,新增了偵測與追蹤物件、訓練圖像物件分類、識別文字語言與翻譯,更還有智慧回覆等應用(如圖一所示)。這些應用大多都在本機(On-device)執行,僅有文字辨識、圖片標籤與地標辨識提供雲端API,得到精確度更高的回應(如圖二)。

 

(圖一)Firebase官網目前所有ML Kit功能。

 


(圖二)目前僅有文字辨識與圖片標籤可以使用雲端API,而地標辨識則僅有雲端API

 

https://tpu.thinkpower.com.tw/tpu/articleDetails/1217 文章中,介紹了在實務中最常使用到的文字辨識技術,並比較在本機與雲端上實作時回傳的實測結果;本文同樣以信用卡卡號辨識為主題,分享Swift版的實作用法、實際應用結果及分析目前版本可能會遇到的問題。

 

請先建立一個XcodeProject,開發者應該非常熟悉這個步驟,這裡就不再贅述。另外,我們建立一個新的Firebase專案。


我將這個專案取名做ML Kit-Swift,並點擊建立專案。

 

進入專案主頁後,選擇iOS

 


這邊的iOS軟體包ID,指Xcode中的Bundle Identifier,請參考下圖位置,複製並貼至Firebase的頁面。


 

應用程式暱稱與App Store ID皆為選填,這邊因為不準備上架所以就先空著,直接註冊應用程式。

 



下載完畢後將設定檔拖曳至Xcode專案左邊方框中,建議勾選Copy items if needed,複製檔案至專案內,完成後回到Firebase頁面點擊繼續。

 


(如果過去未使用過CocoaPods,請先安裝CocoaPods再進行下列步驟)

 


打開終端機,並cd至剛剛建立的Xcode專案資料夾,輸入「pod init」,系統會自動生成一個Podfile的檔案。

 


將剛剛的Xcode專案關閉,以文字編輯程式開啟Podfile檔案,輸入:

Pod 'Firebase/Core'
pod 'Firebase/Analytics'
pod 'Firebase/MLVision'
# If using an on-device API:
pod 'Firebase/MLVisionTextModel'

本次以本機端為主,因此也需要將TextModel下載進來。

 

Podfile存檔後關閉,回到終端機輸入「pod install


需要等待幾分鐘的時間,直到出現下圖。


這樣就能將終端機關閉了,我們會在Xcode專案資料夾中發現多了幾個檔案,接下來切勿再開啟.xcodeproj的檔案,一切程式編輯請改用.xcworkspace


以上步驟完成後,回到Firebase頁面點擊繼續。

 


開啟.xcworkspace檔案,在AppDelegate.swift中,按照圖示範例加入

import Firebase」,以及「FirebaseApp.configure()」,接著回到Firebase頁面點擊下一步。

 


接著,我們以模擬器執行專案,系統會幫我們確認以上的步驟是否正確。

 


若有順利建置成功,應該會出現以上這個畫面,這樣Firebase的基本設定就完成了!

 

終於能開始程式的部分,這個APP的構想很單純,以辨識信用卡卡號為主(當然要辨識其他文件也沒問題,不過現在本機版的API只支援拉丁文字,若要辨識其他語言需使用雲端API),我簡單在Main.storyboard放了幾個元件。


 

ViewController.swift的程式如下:

 

import UIKit
import Firebase

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    @IBOutlet weak var pickedImageView: UIImageView!
    @IBOutlet weak var resultTextView: UITextView!
 
    let vision = Vision.vision()
    var textRecognizer: VisionTextRecognizer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationController?.isToolbarHidden = true
        textRecognizer = vision.onDeviceTextRecognizer()
        
    }
    
    @IBAction func scanCardButton(_ sender: UIButton) {
        let photoSourceRequestController = UIAlertController(title: "", message: "選擇相片來源", preferredStyle: .actionSheet)
        let cameraAction = UIAlertAction(title: "相機", style: .default, handler: { (action) in
            if UIImagePickerController.isSourceTypeAvailable(.camera) {
                let imagePicker = UIImagePickerController()
                imagePicker.allowsEditing = false
                imagePicker.sourceType = .camera
                imagePicker.delegate = self
                self.present(imagePicker, animated: true, completion:  nil)
            }
        })
        
        let photoLibraryAction = UIAlertAction(title: "相簿", style: .default, handler: { (action) in
            if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
                let imagePicker = UIImagePickerController()
                imagePicker.allowsEditing = true
                imagePicker.sourceType = .photoLibrary
                imagePicker.delegate = self
                self.present(imagePicker, animated: true, completion: nil)
            }
        })
        let cancelAction = UIAlertAction(title: "取消", style: .cancel, handler: nil)
        
        photoSourceRequestController.addAction(cameraAction)
        photoSourceRequestController.addAction(photoLibraryAction)
        photoSourceRequestController.addAction(cancelAction)
        
        present(photoSourceRequestController, animated: true, completion: nil)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            resultTextView.text = ""
            pickedImageView.contentMode = .scaleAspectFit
            pickedImageView.image = pickedImage
	pickedImageView.backgroundColor = .none
            self.navigationController?.isToolbarHidden = true
            
            let visionImage = VisionImage(image: pickedImage)
            textRecognizer?.process(visionImage, completion: { (result, error) in
                guard error == nil, let result = result else {
                    self.resultTextView.text = "辨識失敗,請再試一次"
                    self.dismiss(animated: true, completion: nil)
                    return
                }
                
                self.resultTextView.text = "信用卡:\n"
                for block in result.blocks {
              
                    self.resultTextView.text = self.resultTextView.text + block.text + "\n"
                }
            })
        }
        dismiss(animated: true, completion: nil)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
}

   

我使用UIImagePickerController開啟內建的相機與相簿功能,別忘了在專案Info中加入:

Privacy - Camera Usage Description

Privacy - Photo Library Usage Description

取得使用者的權限,接下來就可以執行這個專案測試了,不過,雖然我是按照Firebase官方文件的步驟,但執行上卻出了一點問題,如果不幸地看到這個錯誤:


並在Log欄中看到了這些文字:


 

第一個很好解決,在Info.plist中手動新增FirebaseScreenReportingEnabled,並將類別設為boolean值並設成「NO」,但接下來就與我們前面的設定有些衝突了,我之前在試做時,將FirebaseApp.configure()放在application:didFinishLaunchingWithOptions: 中是沒問題的,但在這裡卻執行失敗。

不過沒關係,我們就依照指示放進

override init() {

        super.init()

        FirebaseApp.configure()

    }

並將application:didFinishLaunchingWithOptions: 中的改為註解:


這樣就可以順利執行了!

 

傳統的卡面設計通常是這樣:


分為凸面卡號與平面卡號(如VISA金融卡等),先以這兩張作為測試。

 

這是在模擬器執行後的畫面,我將兩張卡片放進去,看看結果。

 

成果滿不錯的,顯見若是在平面化過的文件上,辨識的成功率挺高,那麼接著來看一下使用實機拍攝信用卡的效果。

 


只要照片能拍得正,平面卡號上辨識也沒問題,那麼凸面卡號呢?

 


不太好:(

一般信用卡辨識工具,若卡號為凸面的話,會因為容易反光而更容易辨識,但在ML Kit中卻正好相反,特別因凸面字型的關係,會有5辨識成S6辨識成b7辨識成1的狀況等。
此外,若是與卡面的底色對比不強烈,更有可能發生這樣的情形:


卡號與日期幾乎辨識錯誤。

 

另一個問題是,現在卡面的設計相當多元,為求整體的美觀,越來越多銀行選擇將卡號等資訊放置在背面,甚至還有部分卡號設計為直式:


 

若僅是用來當作文字辨識可能還沒什麼問題:


(雖然在數字前後多了|)

 

但要是用在信用卡識別上,應需更精確的判定,比如說要判斷該數字是否為信用卡號。我在這邊使用正規表示式,將for block in result.blocks 裡面改為以下程式:

{

let regex = "^((?:4\\d{3})|(?:5[1-5]\\d{2})|(?:6011)|(?:3[68]\\d{2})|(?:30[012345]\\d))[ -]?(\\d{4})[ -]?(\\d{4})[ -]?(\\d{4}|3[4,7]\\d{13})$"

let predicate = NSPredicate(format: "SELF MATCHES %@", regex)

let cardNumber = predicate.evaluate(with: block.text)

if cardNumber == true {

    self.resultTextView.text = self.resultTextView.text + block.text

}

 

先使用較安全的平面化文件識別看看:


 

看起來很好,但若使用上方的直式排列卡面:


不意外地無法辨識(雖然這種設計在其他第三方工具也得要手動輸入才行)。

 

最後還有一個問題,當我在測試同一張卡片的時候,會發現因拍照角度的關係,會有不一樣的結果,若在拍攝時稍微有了傾斜,識別就可能變成這樣:


不僅無法識別數字是否為信用卡號,連排序都亂了。

 

做個總結。

雖然實測的結果好壞參半,但單純以文字辨識而非信用卡辨識的使用上,ML Kit還是挺優秀的,特別是在印刷文字(非特殊字體)上面的識別準確性相當高,且程式也不算複雜,運用在其他方面,如辨識傳單、看板等,更可以再搭配文字翻譯的API做使用,對開發者來說輕鬆多了。如果有興趣研究其他功能,請至官方文件https://firebase.google.com/docs/ml-kit ,製作更多有趣的APP吧。

 

相關連結參考:

https://firebase.google.com/docs/ml-kit/recognize-text

https://www.appcoda.com.tw/ml-kit/

 

Amy Lin