この記事は何?
Appleによるサンプルコード「VisionとCore MLでの画像の分類」のソースコードを理解したかったので、整理しました。
環境
macOS 10.15.7
Xcode 12.1
Swift 5.3
ソースコード
ビューコントローラーのコード
実際にダウンロードできるプロジェクトは、現在(2020.10.29)のXcodeではそのままビルドできませんでした。
そのため、Xcodeによる最適化を実行した上で、いくつかの部分を修正しました。
以下は、実装部分を省略したものです。
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
キーワードに注目すること。つまり、アクセスされた時点で始めて値が計算される。
言い換えると、呼び出されるたびに「モデルのインスタンス作成 → リクエスト作成」が行われることになる。
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
型の画像に対して、前処理(向きや形式)を行う。
そして、リクエストハンドラを作成した後、ハンドラにリクエストを渡して実行する。
注目は、ハンドラの実行はバックグラウンドで処理していること。
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"
のような文字列で表示している。
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()メソッド
分類処理の対象となる画像を取得する。
アラート表示から、カメラかフォトライブラリをユーザーに選択させる。
@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:)メソッド
カメラかフォトライブラリを表示する。
func presentPhotoPicker(sourceType: UIImagePickerController.SourceType) {
let picker = UIImagePickerController()
picker.delegate = self
picker.sourceType = sourceType
present(picker, animated: true)
}
imagePickerController(_:didFinishPickingMediaWithInfo:)メソッド
ピッカーで選択された画像をメンバプロパティに設定する。
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)
}
手続きの流れを整理する
takePicture()
メソッド →presentPhotoPicker(_:didFinishPickerMediaWithInfo:)
メソッド
画像を取得する。imagePickerController(_:didFinishPickingMediaWithInfo:)
メソッド
分類の手続きを開始するため、updateClassifications(for:)
メソッドに「選択された画像」を渡す。updateClassifications(for:)
メソッド
リクエストハンドラを作成して、それを実行する。ハンドラはバックグランド・キューとして処理される。
このとき、ハンドラにclassificationRequest
プロパティを渡す。classificationRequest
プロパティ
updateClassifications(for:)
メソッドから参照されることでプロパティの計算が行われ、作成したリクエストを返す。
ここで、リクエストをprocessClassifications(for:_:)
メソッドに渡している。processClassifications(for:_:)
メソッド
分類結果の表示を、メインキューで非同期に処理する。