【Vison・Core ML】画像分類を行うiOS機械学習アプリのコード解説


# この記事は何？
Appleによるサンプルコード「[VisionとCore MLでの画像の分類](https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml)」のソースコードを理解したかったので、整理しました。

###環境
macOS 10.15.7
Xcode 12.1
Swift 5.3

#ソースコード
###ビューコントローラーのコード
実際にダウンロードできるプロジェクトは、現在(2020.10.29)のXcodeではそのままビルドできませんでした。
そのため、Xcodeによる最適化を実行した上で、いくつかの部分を修正しました。
以下は、実装部分を省略したものです。

````swift:ImageClassificationViewController.swift
class ImageClassificationViewController: UIViewController {
    // IBOutlets
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var cameraButton: UIBarButtonItem!
    @IBOutlet weak var classificationLabel: UILabel!
    
    // Image Classification
    // モデルのセットアップ
    lazy var classificationRequest: VNCoreMLRequest = {
        let modelURL = Bundle.main.url(forResource: "MobileNetV2", withExtension: "mlmodelc")!
        let model = try! VNCoreMLModel(for: MobileNetV2(contentsOf: modelURL).model)
        
        let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
            self?.processClassifications(for: request, error: error)
        })
        request.imageCropAndScaleOption = .centerCrop
        return request
    }()
    
    // リクエストを実行する
    func updateClassifications(for image: UIImage) {...}
    
    /// 分類結果で画面表示を更新する
    // 分類を行う
    func processClassifications(for request: VNRequest, error: Error?) {...}
    }
    
    // 写真の手続き    
    @IBAction func takePicture() {...}    
    func presentPhotoPicker(sourceType: UIImagePickerController.SourceType) {...}
}

extension ImageClassificationViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    // イメージピッカーによる選択
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {...}
}
````

`ImageClassificationViewController`クラスには、「4つのプロパティ」と「5つのメソッド」がある。
実装は以下のとおり。

###classificationRequestプロパティ
`VNCoreMLRequest`型の「リクエスト」を作成して返す。
ここで、`lazy`キーワードに注目すること。つまり、アクセスされた時点で始めて値が計算される。
言い換えると、呼び出されるたびに「モデルのインスタンス作成 → リクエスト作成」が行われることになる。

````swift:classificationRequestプロパティ
lazy var classificationRequest: VNCoreMLRequest = {
    do {
        // Core MLが自動生成するMobileNetクラスを使用する
        // この部分を書き換えることで、任意のCore MLモデルを使用できる
        let modelURL = Bundle.main.url(forResource: "MobileNetV2", withExtension: "mlmodelc")
        let model = try VNCoreMLModel(for: MobileNetV2(contentsOf: modelURL!).model)
            
        let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
            self?.processClassifications(for: request, error: error)
        })
        request.imageCropAndScaleOption = .centerCrop
        return request
    } catch {
        fatalError("Failed to load Vision ML model: \(error)")
    }
}()
````

###updateClassifications(for:)メソッド
まず、受け取った`UIImage`型の画像に対して、前処理（向きや形式）を行う。
そして、リクエストハンドラを作成した後、ハンドラにリクエストを渡して実行する。
注目は、ハンドラの実行はバックグラウンドで処理していること。

````swift:updateClassifications()メソッド
func updateClassifications(for image: UIImage) {
    classificationLabel.text = "Classifying..."
    
    let orientation = CGImagePropertyOrientation(image.imageOrientation)
    guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }
    
    DispatchQueue.global(qos: .userInitiated).async {
        let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
        try! handler.perform([self.classificationRequest])
    }
}
````

###processClassifications(for:_:)メソッド
分類結果に対する処理を行う。UIを更新する手続きになるので、すべてのコードをメインキューで処理している。
UI画面において分類結果は、`"(0.37) cliff, drop, drop-off"`のような文字列で表示している。

````swift:processClassifications()メソッド
func processClassifications(for request: VNRequest, error: Error?) {
    DispatchQueue.main.async {
        guard let results = request.results else {
            self.classificationLabel.text = "Unable to classify image.\n\(error!.localizedDescription)"
            return
        }
        // resultsの型は常に、VNClassificationObservationのコレクション
        let classifications = results as! [VNClassificationObservation]
    
        if classifications.isEmpty {
            self.classificationLabel.text = "Nothing recognized."
        } else {
            // 確度が高い順に分類が表示する
            let topClassifications = classifications.prefix(2)
            let descriptions = topClassifications.map { classification in
               // 分類の表示形式: "(0.37) cliff, drop, drop-off"
               return String(format: "  (%.2f) %@", classification.confidence, classification.identifier)
            }
            self.classificationLabel.text = "Classification:\n" + descriptions.joined(separator: "\n")
        }
    }
}
````

###takePicture()メソッド
分類処理の対象となる画像を取得する。
アラート表示から、カメラかフォトライブラリをユーザーに選択させる。

````swift:takePicture()メソッド
@IBAction func takePicture() {
    // カメラが使用可能な場合のみ、ピッカーの選択を表示する
    guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
        presentPhotoPicker(sourceType: .photoLibrary)
        return
    }
    
    let photoSourcePicker = UIAlertController()
    let takePhoto = UIAlertAction(title: "Take Photo", style: .default) { [unowned self] _ in
        self.presentPhotoPicker(sourceType: .camera)
    }
    let choosePhoto = UIAlertAction(title: "Choose Photo", style: .default) { [unowned self] _ in
        self.presentPhotoPicker(sourceType: .photoLibrary)
    }
    
    photoSourcePicker.addAction(takePhoto)
    photoSourcePicker.addAction(choosePhoto)
    photoSourcePicker.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    
    present(photoSourcePicker, animated: true)
}
````

###presentPhotoPicker(_:didFinishPickerMediaWithInfo:)メソッド
カメラかフォトライブラリを表示する。

````swift:presentPhotoPicker()メソッド
func presentPhotoPicker(sourceType: UIImagePickerController.SourceType) {
    let picker = UIImagePickerController()
    picker.delegate = self
    picker.sourceType = sourceType
    present(picker, animated: true)
}
````

###imagePickerController(_:didFinishPickingMediaWithInfo:)メソッド
ピッカーで選択された画像をメンバプロパティに設定する。

````swift:imagePickerController()メソッド
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
   picker.dismiss(animated: true)
    
    // このメソッドが返す元画像を作成
    let image = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
    imageView.image = image
    updateClassifications(for: image)
}
````

## 手続きの流れを整理する

1. `takePicture()`メソッド → `presentPhotoPicker(_:didFinishPickerMediaWithInfo:)`メソッド
画像を取得する。

2. `imagePickerController(_:didFinishPickingMediaWithInfo:)`メソッド
分類の手続きを開始するため、`updateClassifications(for:)`メソッドに「選択された画像」を渡す。

3. `updateClassifications(for:)`メソッド
リクエストハンドラを作成して、それを実行する。ハンドラはバックグランド・キューとして処理される。
このとき、ハンドラに`classificationRequest`プロパティを渡す。

4. `classificationRequest`プロパティ
`updateClassifications(for:)`メソッドから参照されることでプロパティの計算が行われ、作成したリクエストを返す。
ここで、リクエストを`processClassifications(for:_:)`メソッドに渡している。

5. `processClassifications(for:_:)`メソッド
分類結果の表示を、メインキューで非同期に処理する。