動画に対して顔認識を試す (標準SDK〜CoreImageのCIDetectorを利用する)

エンジニアの德光です。 前回の静止画での顔認識を、インカメラで写しているものに対してリアルタイムに実施してみよ…

エンジニアの德光です。
前回の静止画での顔認識を、インカメラで写しているものに対してリアルタイムに実施してみようかなと。
これでSN◉W的なアプリも自作可能ですね!?
静止画を指定する代わりに、動画撮影の1フレーム分の画像に対して顔認識処理を呼ぶようにして実装します。

◆カメラ画像の利用

AVFoundationを用いて、AVCaptureConnectionを貼り「 func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!)」で渡されるsampleBufferから画像を生成し、それを入力として顔認識させることにします。
前回同様、今度はカメラ入力を利用したいので、「Privacy – Camera Library Usage Description」の設定などは行っておきましょう。

カメラデバイスの初期化

基本的にはAVFoundationが直接プレビュー表示をするため、適当なUIViewを渡すと、そこにカメラからの入力画像を表示してくれます。

    //=== UI関連
    @IBOutlet weak var vwCameraPreview: UIView!//カメラ連動のプレビューを表示させるため
    //=== カメラ画像関連
    var captureSession = AVCaptureSession()
    var videoDataOutput: AVCaptureVideoDataOutput?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.initCamera()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.startCamera()
    }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.stopCamera()
    }
extension TKCameraVC: AVCaptureVideoDataOutputSampleBufferDelegate {
    //======== カメラ画像処理関連 ========
    func initCamera() {
        var videoDevice: AVCaptureDevice! //利用するデバイスの保持
        let devicePosition: AVCaptureDevicePosition = .front //利用するカメラの位置(インカメラを使いたいので.front)
        let captureDevices: [AnyObject] = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as [AnyObject]
        for captureDevice in captureDevices { //使えるカメラを列挙して、その中から利用可能なものを取得する
            if let device = captureDevice as? AVCaptureDevice {
                if device.position == devicePosition {
                    videoDevice = device    // as! AVCaptureDevice
                }
            }
        }
        if let videoInput = try? AVCaptureDeviceInput.init(device: videoDevice) {
            captureSession.addInput(videoInput)
        }
        videoDataOutput = AVCaptureVideoDataOutput()
        videoDataOutput?.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable : Int(kCVPixelFormatType_32BGRA)] // ピクセルフォーマット(32bit BGRA)
        videoDataOutput?.alwaysDiscardsLateVideoFrames = true //処理落ちフレームの削除
        videoDataOutput?.setSampleBufferDelegate(self, queue: DispatchQueue.main) //フレームキャプチャの設定
        captureSession.addOutput(videoDataOutput)
        // カメラ画素数の設定(ただし1920x1080をインカメラ指定するなど、存在しないと即クラッシュなので注意(利用可能なの列挙させて選択すべき)
        //captureSession.sessionPreset = AVCaptureSessionPreset1920x1080//これはインカメラ死亡
        //captureSession.sessionPreset = AVCaptureSessionPreset1280x720
        captureSession.sessionPreset = AVCaptureSessionPreset640x480
        //captureSession.sessionPreset = AVCaptureSessionPreset352x288
        
        // プレビュー(この処理が記述されていないと、下記のcaptureOutputが呼ばれ続けない)
        if let videoLayer = AVCaptureVideoPreviewLayer.init(session: captureSession) {
            videoLayer.frame = vwCameraPreview.bounds
            videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
            vwCameraPreview.layer.addSublayer(videoLayer)
            vwCameraPreview.alpha = 0.3
        }
    }
    func startCamera() {
        self.captureSession.startRunning() // セッションの開始
    }
    func stopCamera() {
        self.captureSession.stopRunning() // セッションの停止
    }
    // 新しいキャプチャの追加で呼ばれる
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        print(".captureOutput.")
        connection.videoOrientation = .portrait //デバイスの向きを設定(これしないと、横できたものをそのまま加工するのでずれる)
        let image = imageFromSampleBuffer(
            sampleBuffer: sampleBuffer)
        self.ivCameraPreview.image = image
    }

とりあえずここまでで、UIViewで貼っておいた生プレビュー用のvwCameraPreviewにカメラからの画像が表示されるはずです。
これはOS側が制御して表示するので、手出しをできない代わりに遅延などもなく描画されます。
そして、自前で表示させているivCameraPreviewの方は、表示が始まるまでに時間がかかったり、カメラ解像度をあげたりするともたつくかもしれません。

フレームごとに画像加工すると…

上記の captureOutput(〜) で、毎フレームのsampleBufferをimageFromSampleBufferでUIImageに変換しているので、これを顔認識フィルタに横流せば良いことになります。
ちょっと「PhotoFilter」として、いろいろやるクラスを作っています。「checkFaceInfo」で顔認識した結果が配列で返却されるようにし、その結果をあらためて「maskFaces」に渡すと、指定したフィルタを見つかった顔に対して適用した一枚絵の画像を返却するようにしています。
2段階に分けた理由は、認識結果だけを保存しておくのに都合が良かったり、画像加工フィルタを多種類用意して切り替え可能にするときに処理がわかりやすくなるからです。

    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        connection.videoOrientation = .portrait //デバイスの向きを設定(これしないと、横でくる)
        let image = imageFromSampleBuffer(sampleBuffer: sampleBuffer)
        let faces = PhotoFilter.sharedManager.checkFaceInfo(image: image, delegate: self) //顔認識の結果を配列で受け取っておき、
        let filterd = PhotoFilter.sharedManager.maskFaces(image: image, faceInfo: faces, faceMask: self.faceMask)//その結果をもとに、あらためて画像加工している
        self.ivCameraPreview.image = filterd
    }

さて、この処理をいれて表示させてみると……。
なんということでしょう、表示されなくなってしまいました。
悲しいことに処理落ちです。
画像フィルタの処理が重くて、毎フレームの処理に追いつかなくなってしまっているようです。
てなわけで、カウンタを用意して、適当に間引いて処理をさせることにします。
(画像処理を高速化するってのがスジだとも思いますが、今回は手抜き。画像処理部分はいろいろ試してみたいところですし)

    //=== カメラ画像関連
    var frameCounter: Int = 0 //全フレームで処理をするのは無理なので...
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        self.frameCounter = self.frameCounter + 1
        if frameCounter % 2 != 0 { return } // 適当に間引く
	(省略)

とりあえず、フレーム処理の2回に1度だけ処理することにしたら表示されました。
まぁ、もう少し余裕をみて「if frameCounter % 3 != 0 { return } // 適当に間引く」などと3回に1度くらいにしておいても良いかもしれませんね。
(おもい画像加工を入れてみたりもするつもりなので)

画像フィルタで何をやっているか

透明を含む画像を顔認識エリアに重ねることで、鼻メガネ加工とか少女漫画のような瞳を表示しています。
ここでは、個別に得られる左右の目の位置は利用せず、顔の矩形と傾きだけを利用して重ねているため、鼻メガネ画像の余白で目鼻口の位置を調整しています。
認識した顔画像いくつかを出力しておいて、位置合わせをすると良い感じに重なるんじゃないでしょうか。
このくらいの処理だと、別画像を重ねて表示するだけなのでさきほどの2フレームに1度でも追いつくと思いますが、切り出した顔画像をもとに画像加工してすげかえるなど、もっと処理が重くなるものを適用した場合に、3フレームに1度や4フレームに1度に変更する必要がでてくるかもしれません。

    func maskFaces(image: UIImage, faceInfo: [FaceInfo], faceMask: FaceMask) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(image.size, true, 0);
        let context: CGContext = UIGraphicsGetCurrentContext()!
        image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
        for face in faceInfo {
            switch faceMask {
            case .HanaMegane: //顔マスク画像を重ねる(鼻メガネ)
                if let mask = UIImage(named: "hanamegane") {
                    let mask2 = rotateImage(image: mask, angle: face.angle)
                    mask2.draw(in: face.rect)
                }
            case .Comic01: //顔マスク画像を重ねる(少女マンガな瞳)
                if let mask = UIImage(named: "comic") {
                    let mask2 = rotateImage(image: mask, angle: face.angle)
                    mask2.draw(in: face.rect)
                }
            case .Nothing: break
            }
        }
        if let outputImage = UIGraphicsGetImageFromCurrentImageContext() {
            UIGraphicsEndImageContext();
            return outputImage
        }
        UIGraphicsEndImageContext();
        return image
    }

カメラデバイスの設定を変更する場合

デバイスの設定を変更する場合には、いったんロックして設定を変更する必要があります。
たとえばいきなり下のようにフレームレートを1/30から1/20に変更しようとしてもクラッシュします。

        videoDevice?.activeVideoMinFrameDuration = CMTimeMake(1, 20)// フレームレート 標準:1/30秒
        //カメラ情報のもろもろ初期化
        do {
            try videoDevice?.lockForConfiguration()//ロックして設定を変更
        } catch {
            print("カメラデバイスのロックができなかった...")
        }
        videoDevice?.activeVideoMinFrameDuration = CMTimeMake(1, 20)// フレームレート 標準:1/30秒
        videoDevice?.unlockForConfiguration()//ロック解除して設定を反映

なんかSwift3から例外キャッチで書かなきゃいけなくなっていて、少しハマってました。
これでフレームレート設定を変更してもクラッシュしなくなりましたが、、、本当に変わっているかな?
なんか、1/10とかにする代わりに、さきほどのカウンタ「self.frameCounter」の間引く回数を減らしてみても処理落ちして表示されなかったりするんですけど。。。

なんていう課題を残しつつ、今回はここまで。