この記事は iOS#2 Advent Calendar 2019 15日目の記事です。
初めて Advent Calendar に参加するので若干の緊張があります笑
よろしくお願いします!※Qiita界隈に愛嬌を振りまいていくスタンス
今回は、「Create MLとCore MLを使って、カメラに写った人が嵐のメンバーの誰かをリアルタイムで判定する」という題でやっていきます。Core MLに興味ある方は、私の他にも @cthxn77r さんや @takashico さんが今年のアドベントカレンダーに投稿されていましたので、そちらもどうぞ!※Qiita界隈に"全力で"愛嬌を振りまいていくスタンス
#やったこと
以下の動画のようなことができるようになります。
何番煎じだよと思いましたが、Core ML使ってみるならこういうことしたいなーと考えていたので、それが実現できて嬉しかったです。題材に嵐を選んだのは、人数が少なくて正解しやすそうだなと思ったからです。
以下に、これを実装した手順を書いていきます。
#教師データの作成
API を叩かずに Google から画像収集をする
を使用させていただきました。メンバー1人あたり500枚ほど集めました。そこから顔部分を切り出してデータを集めるのですが、
PyTorchを使って日向坂46の顔分類をしよう!
を参考にさせていただきました。
今回はモデルの作成にCreate MLを使おうと思っていたので、画像のサイズは300*300を指定しております。※画像分類モデルの作成
あと、どこかで教師データにはノイズが入っていたほうが良いと聞いたことがあったので、「他人」というラベルの画像も同じくらい用意しました。
#モデルの作成
XcodeのOpen Developer Toolをクリックすると、Create MLがあります。そこをクリックし、Create MLのアプリを起動します。
メニューの File > New Project を選択します。今回は画像から正しいラベルをつけるモデルを作るため、 Image Classifierを選択しましょう。
そしてプロジェクト名を決めれば、教師データとテストデータを選択する画面になります。あとは教師データとテストデータのフォルダを選択し、Trainボタンを押すだけで簡単にモデルが作れてしまいます。このとき、教師データのフォルダ内にある画像フォルダの名前がそのままラベルになるので、テストフォルダの名前と一致するように気をつけてください。
このモデルを使いたかったら、生成されたモデルをドラッグ&ドロップでXcodeのプロジェクト内に引っ張ってくるだけ!簡単ですね(Uber EatsのCM風に)
#サンプルアプリの実装
以下に、サンプルアプリのプログラムを貼っておきます。コードは
Keras + iOS11 CoreML + Vision Framework による、ももクロ顔識別アプリの開発
Swiftでアイマスの画像認識やってみる
を大変参考にさせていただきました。ありがとうございました。
カメラを使用するので、info.plistでカメラの使用するための確認文言を入れておいてください。
import UIKit
import AVFoundation
import Vision
import CoreML
class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
@IBOutlet weak var cameraView: UIView!
@IBOutlet weak var faceImageView: UIImageView!
@IBOutlet weak var resultLabel: UILabel!
var ciImage: CIImage?
var captureLayer: AVCaptureVideoPreviewLayer?
override func viewDidLoad() {
super.viewDidLoad()
setupCamera()
}
override func viewDidLayoutSubviews() {
captureLayer?.frame = cameraView.bounds
}
func setupCamera() {
let session = AVCaptureSession()
captureLayer = AVCaptureVideoPreviewLayer(session: session)
cameraView.layer.addSublayer(captureLayer!)
guard let device = AVCaptureDevice.default(for: .video) else { return }
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
let output = AVCaptureVideoDataOutput()
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera"))
session.addInput(input)
session.addOutput(output)
session.startRunning()
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
if connection.videoOrientation != .portrait {
connection.videoOrientation = .portrait
return
}
guard let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
ciImage = CIImage(cvImageBuffer: buffer)
faceDetection(buffer)
}
func faceDetection(_ buffer: CVImageBuffer) {
let request = VNDetectFaceRectanglesRequest { (request, error) in
guard let results = request.results as? [VNFaceObservation] else { return }
if let image = self.ciImage, let result = results.first {
let face = self.getFaceCGImage(image: image, face: result)
if let cg = face {
self.showPreview(cgImage: cg)
self.scanImage(cgImage: cg)
}
}
}
let handler = VNImageRequestHandler(cvPixelBuffer: buffer, options: [:])
try? handler.perform([request])
}
func getFaceCGImage(image: CIImage, face: VNFaceObservation) -> CGImage? {
let imageSize = image.extent.size
let box = face.boundingBox.scaledForCropping(to: imageSize)
guard image.extent.contains(box) else {
return nil
}
let size = CGFloat(300.0)
let transform = CGAffineTransform(
scaleX: size / box.size.width,
y: size / box.size.height
)
let faceImage = image.cropped(to: box).transformed(by: transform)
let ctx = CIContext()
guard let cgImage = ctx.createCGImage(faceImage, from: faceImage.extent) else {
assertionFailure()
return nil
}
return cgImage
}
private func showPreview(cgImage: CGImage) {
let uiImage = UIImage(cgImage: cgImage)
DispatchQueue.main.async {
self.faceImageView.image = uiImage
}
}
func scanImage(cgImage: CGImage) {
let image = CIImage(cgImage: cgImage)
guard let model = try? VNCoreMLModel(for: ArashiClassifier().model) else { return }
let request = VNCoreMLRequest(model: model) { request, error in
guard let results = request.results as? [VNClassificationObservation] else { return }
guard let mostConfidentResult = results.first else { return }
DispatchQueue.main.async {
self.resultLabel.text = mostConfidentResult.identifier
}
}
let requestHandler = VNImageRequestHandler(ciImage: image, options: [:])
try? requestHandler.perform([request])
}
}
extension CGRect {
func scaledForCropping(to size: CGSize) -> CGRect {
return CGRect(
x: self.origin.x * size.width,
y: self.origin.y * size.height,
width: (self.size.width * size.width),
height: (self.size.height * size.height)
)
}
}
注意した部分は、ただカメラの撮影した画像をモデルに読み込ませるのではなく、Vision.Frameworkで顔部分を探索してから判定を行うようにしたことですね。CIImage型のプロパティをつくりカメラで読み込んだ画像を一時保存しておくことで実現しています。
#結果
実際に使っているところは上に貼った動画をご覧ください。
正直、使い物になるとは言い難い…!!ぶっちゃけ、Create MLで作ってる最中でテストの正解率が50%でした。
個人的な感覚ですが、実用的になるには80〜90%まではいかないとと思っているので、もっとデータを集めるなり、Create MLがもっといろいろなパラメータをいじれるようになるのを待つ必要はあるなと思いました。
ちなみに、私の顔写真をカメラで写してみると見事に「他人」と判定されました。
私の顔がジャニーズ判定されないのはおかしいのでもっと改良の余地がありますね。
#感想
アプリに機械学習を取り込むこと自体はすごい簡単にできて、その容易さに驚きました。そして、冷静に考えるとメンバー5人(プラス他人)の選択肢がある中で、5割の正答率を誇るのは割とすごい気がしました。(Pythonで作られた他モデルの精度には目をつぶりながら)
さらにこの後やったのですが、撮影した動画の1枚1枚ごとの結果を保存して比較的頻繁に出てくる名前を出力するようにすると、正答率は上がった気がします(体感ですが)。
このような、モデルに通す前にあらかじめ顔部分を調べるとか、一瞬の結果を保存して一番多い答えを出すことで正答率を上げるとか、そこらへんがアプリエンジニアが機械学習と向き合っていく際に使うノウハウなのかな、って気がしました。
明日は @S_Shimotori さんで、ダークモードについてのお話みたいですね。
私が働いている会社のアプリが今ダークモードに対応するか否かの真っ只中なので、楽しみです!※Qiita界隈に(ry
以上、ありがとうございましたー!