静止画像の顔認識を試す (標準SDK〜CoreImageのCIDetectorを利用する)

エンジニアの德光です。 OpenCVで遊んでみるの続きとして、顔認識でもやってみようかなーと思ったものの、標準…

エンジニアの德光です。
OpenCVで遊んでみるの続きとして、顔認識でもやってみようかなーと思ったものの、標準SDKで顔認識がサポートされてるんだっけと、まずは試してみることにしました。
まずはお手軽に静止画での顔認識を。
もう、プロジェクト作成などの基本的な手順はすっとばしていきます。
ってか、前回のに画像読み込み用の処理を追加し、フィルタ処理の代わりに顔認識処理を呼ぶようにしておきます。

◆画像の読み込み処理

【Info.plist】に「Privacy – Photo Library Usage Description」を追加して適当な文言を設定。
これをやっておかないと、画像を読み込ませようとした時点でクラッシュします。
この文言は、画像読み込みの初回起動時に表示され、許可することでフォトライブラリからの画像読み込みが可能になります。

    @IBAction func actImageLoad(_ sender: UIButton) {
        if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { //利用可能か調べて
            let picker = UIImagePickerController()
            picker.modalPresentationStyle = .popover
            picker.delegate = self
            picker.sourceType = .photoLibrary
            self.present(picker, animated: false, completion: {
                print("#[\(#line)][\(#function)] ピッカー表示の完了時")
            })
        }
    }
//=== 画像選択処理デリゲートの記述
extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    //=== 選択された時の処理
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
       	print("#[\(#line)][\(#function)] 画像選択時")
        self.dismiss(animated: true, completion: { () -> Void in //もしも画面遷移が絡むなら、dismissのcompletionで記述した方が良いのです
            if let image = info[UIImagePickerControllerOriginalImage] as? UIImage {
                self.ivOrig.image = image
            }
        })
    }
    //=== 選択キャンセルされた時の処理(optionalなので、そもそも記述を省けば良いが、もしも書いたならdismiss処理も書かないと消えなくなるので注意)
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
       	print("#[\(#line)][\(#function)] 画像選択キャンセル時")
        self.dismiss(animated: true, completion: nil)
    }
}

◆画像のフィルタ処理

今後のサンプル拡張も踏まえて、いちいちボタンを増やしていくのも面倒なので、セグメントで複数種類の切り分け可能にしておきます。
てなわけで、UISegmentedControlをOutletでつないでおき、actSegChangedを「Value Changed」で呼び出すようにして、selectedSegmentIndexをswitchで振り分けて処理させておきます。

    @IBOutlet weak var segCtrl: UISegmentedControl!
    @IBAction func actSegChanged(_ sender: UISegmentedControl) {
        switch sender.selectedSegmentIndex {
        case 0: //これ前回のなごり
            if let img = self.ivOrig.image {
                let openCV = OpenCVWrapper()
                let imgConv = openCV.convGrayscale(img)
                self.ivConv.image = imgConv
            }
        case 1:
            self.faceDetect()
        default: break
        }
    }
    func faceDetect() {
        print("#[\(#line)][\(#function)] 顔認識させる")
    }

とりあえず顔検出させてみる

    func faceDetect(_ image: UIImage) {
        //===CoreImageに存在する顔認識機能を利用する
        let ciImage = CIImage(cgImage: image.cgImage!) //UIImageからCIImageに変換するため
        let ciDetector = CIDetector(
            ofType: CIDetectorTypeFace
            ,context: nil
            ,options: [
                CIDetectorAccuracy: CIDetectorAccuracyHigh, // LowよりHighの方が認識率は上がるが処理が重くなる
                 //CIDetectorSmile: true, // 笑顔の検出
                 //CIDetectorEyeBlink: true, // ウィンクの検出
            ]//Trackingつけると認識率が落ちた
        )
        //=== CIImageに検出器をかけた結果を取得し表示する
        if let features = ciDetector?.features(in: ciImage) {
            if features.count == 0 {
                print("顔が見つからない")
            } else {
                print("\(features.count)人の顔を認識しました")
            }

これでいろんな画像を読み込ませてみて、とりあえず顔認識するかの確認はできるようになりました。
んが、これじゃつまらないので、せめて認識した顔の確認ぐらいはしたいと思います。
ついでに、ログ表示していたメッセージもちゃんと画面に表示させてやりましょう。
なので、適当にラベルを追加してやるとともに、これらの結果もちゃんと戻り値を返すようにしておきます。

顔検出結果の個別確認と、認識した(最後の)顔の切り抜き表示

    @IBOutlet weak var lblStatus: UILabel!
    @IBAction func actSegChanged(_ sender: UISegmentedControl) {
        guard let imgOrig = self.ivOrig.image else { return }
        switch sender.selectedSegmentIndex {
        case 0:
            let openCV = OpenCVWrapper()
            let imgConv = openCV.convGrayscale(imgOrig)
            self.ivConv.image = imgConv
        case 1:
            let (msg, img) = self.faceDetect(imgOrig) //顔検出結果を表示(なければnilを設定して画像を消す)
            self.lblStatus.text = msg
            self.ivConv.image = img
        default: break
        }
    }
    func faceDetect(_ image: UIImage) -> (String, UIImage?) {
        var burResult: String = ""
        var imgResult: UIImage? = nil
        //===CoreImageに存在する顔認識機能を利用する
        let ciImage = CIImage(cgImage: image.cgImage!) //UIImageからCIImageに変換するため
        let ciDetector = CIDetector(
            ofType: CIDetectorTypeFace
            ,context: nil
            ,options: [
                CIDetectorAccuracy: CIDetectorAccuracyHigh, // LowよりHighの方が認識率は上がるが処理が重くなる
                 CIDetectorSmile: true, // 笑顔の検出
                 CIDetectorEyeBlink: true, // ウィンクの検出
                 //CIDetectorTracking: true // これつけると認識率が落ちてしまう
            ]//Trackingつけると認識率が落ちた
        )
        //=== CIImageに検出器をかけた結果を取得し表示する
        if let features = ciDetector?.features(in: ciImage) {
            burResult = (features.count == 0) ? "顔が見つからない" : "\(features.count)人の顔を認識しました"
            //=== 認識結果を個別に確認
            for feature in features {
                if let ff = feature as? CIFaceFeature {
                    let rectFace = ff.bounds //顔認識した矩形
                    print(NSStringFromCGRect(rectFace))
                    if ff.hasSmile { print("笑ってた") }
                    print("\n◆"
                        + "[hasTrackingID:\(ff.hasTrackingID):\t\(ff.trackingID)]\t"
                        + "[hasSmile:\(ff.hasSmile)] "
                        + "[bounds:\(ff.bounds)]\t"
                        + "[hasFaceAngle:\(ff.hasFaceAngle): \(ff.faceAngle)]\t"
                        + "[hasMouthPosition:\(ff.hasMouthPosition): \(NSStringFromCGPoint(ff.mouthPosition))]\t"
                        + "[hasLeftEyePosition:\(ff.hasLeftEyePosition): \(NSStringFromCGPoint(ff.leftEyePosition))]\t"
                        + "[hasRightEyePosition:\(ff.hasRightEyePosition): \(NSStringFromCGPoint(ff.rightEyePosition))]\t"
                    )
                    //とりま見つけた顔だけ表示しちゃう
                    if let cripImageRef = image.cgImage!.cropping(to: convRect(rectFace, inCanvasSize: image.size)) {
                        let crippedImage = UIImage(cgImage: cripImageRef)
                        imgResult = crippedImage
                    }
                }
            }
        }
        return (burResult, imgResult) //手抜きして、結果のメッセージと画像をタプルで返す
    }

これでいくつかの画像を確認すると。。。
顔認識に成功した場合に、なんか予想と違う結果になったりしていませんか?
これは、UImageは左上座標系ですが、CIImageは左下座標系のためです。
なので、顔認識の結果のRectは左下座標系なので、そのままUIImageから切り出すのに用いているので、関係ない場所が切り抜かれているんですよね。
その補正のための変換関数を用意して、かませてやることにしましょう。

    func convRect(_ rect: CGRect, inCanvasSize: CGSize) -> CGRect {
        var rect = rect
        rect.origin.y = inCanvasSize.height - rect.origin.y - rect.size.height
        return rect
    }

そして、呼び出し時にこれをかますようにします。

                // if let cripImageRef = image.cgImage!.cropping(to: rectFace) {
                if let cripImageRef = image.cgImage!.cropping(to: convRect(rectFace, inCanvasSize: image.size)) {
                    let crippedImage = UIImage(cgImage: cripImageRef)
                    imgResult = crippedImage
                }

これで、顔認識に成功すると、とりあえず誰かの顔が表示されるようになったはずです。
認識結果として、顔の外接矩形と、あれば傾き具合。
右目、左目、口の位置や、笑っているかといった情報が取得できます。
あと、指定していればトラッキングIDも。(これは静止画だとあまり意味ないっすよね)

ところで、何種類かを試しても「hasSmile」が「true」になったのをみなかったのですが、何か勘違いしているのか??
長くなったので、今回はこれでおしまい。