6
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-28

この記事は何?

Appleによるサンプルコード「VisionとCore MLでの画像の分類」のソースコードを理解したかったので、整理しました。

環境

macOS 10.15.7
Xcode 12.1
Swift 5.3

ソースコード

ビューコントローラーのコード

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

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キーワードに注目すること。つまり、アクセスされた時点で始めて値が計算される。
言い換えると、呼び出されるたびに「モデルのインスタンス作成 → リクエスト作成」が行われることになる。

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型の画像に対して、前処理(向きや形式)を行う。
そして、リクエストハンドラを作成した後、ハンドラにリクエストを渡して実行する。
注目は、ハンドラの実行はバックグラウンドで処理していること。

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"のような文字列で表示している。

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()メソッド

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

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:)メソッド

カメラかフォトライブラリを表示する。

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

imagePickerController(_:didFinishPickingMediaWithInfo:)メソッド

ピッカーで選択された画像をメンバプロパティに設定する。

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:_:)メソッド
    分類結果の表示を、メインキューで非同期に処理する。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4