NokkunBlog

Keep technology simple.

【Swift】Visionフレームワークを使って顔検出をしてみた

こんにちは、のっくんです。

今日はSwiftで画像の顔検出をやっていこうと思います。

SwiftにはVisionフレームワークが用意されています。

iOS11(2017年9月リリース)で導入されたフレームワークですが、ざっとみただけでも以下のような機能があります。

  • Object Tracking、物体追跡
  • Face and Body Detection、顔、体検出
  • Animal Detection、動物検出
  • Machine-Learning Image Analysis、マシンラーニングの画像解析

参考:https://developer.apple.com/documentation/vision

スゴイですね。こんな高機能な物が用意されているなんて。

さすがはApple。

デベロッパーとしてはとても嬉しいですね。

早速使っていこうと思います。

画像を用意する

まずは顔の写った画像を用意します。

ネットで人が写っている画像を検索して撮影、Xcodeに取り込みます。

Assetsの中にドラッグAndドロップで取り込んで適当な名前をつけます。

imageviewに表示する

画像をimageviewに表示させます。

override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        guard let image = UIImage(named: "sample1") else { return }
        
        let imageView = UIImageView(image: image)
        
        imageView.contentMode = .scaleAspectFit
        
        let scaledHeight = view.frame.width / image.size.width * image.size.height
        
        imageView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: scaledHeight)
        
        imageView.backgroundColor = .blue
        
        view.addSubview(imageView)

ImageViewの高さと横幅を設定しています。

ImageViewの横幅はViewの横幅にします。

ImageViewの高さは「Viewの横幅/画像の横幅」で割合を求めて、その割合を画像の高さと掛け算して求めます。

顔検出をしたら四角形を表示する

続けて以下のコードを追加して顔検出をした後に顔に四角形を表示してみます。

クロージャに以下の処理を追加しましょう。

let request = VNDetectFaceRectanglesRequest{ (req,
            err) in
            
            if let err = err{
                print("Failed to detect:", err)
                return
            }
            
            req.results?.forEach({ (res) in
                guard let faceObservation = res as? VNFaceObservation else { return }
                
                print(faceObservation.boundingBox)
                
                let x = self.view.frame.width * faceObservation.boundingBox.origin.x
                
                let height = scaledHeight *
                                 faceObservation.boundingBox.height
                
                let y = scaledHeight * (1 - faceObservation.boundingBox.origin.y) - height
                
                let width = self.view.frame.width * faceObservation.boundingBox.width
                
                let redView = UIView()
                redView.backgroundColor = .red
                redView.frame = CGRect(x: x, y: y, width: width, height: height)
                redView.alpha = 0.4
                self.view.addSubview(redView)
            })
        }

`VNDetectFaceRectanglesRequests`をすると、顔の場所(boundingBox、x座標、y座標、高さ、横幅)が返って来ます。

しかし、`VNDetectFaceRectanglesRequests`で顔検出をしているのは元の画像(UIImage)に対してなので、iPhone上の座標とは異なります。

iPhone上の座標に四角形を表示させるために、x、y、高さ、横幅を再計算しています。

ハンドラーを実行する

以上で顔検出時に実行するクロージャ内の処理が出来上がったので、あとは実行するのみです。

`VNImageRequestHandler`の引数にcgImageを渡して実行します。

 guard let cgImage = image.cgImage else {
            return
        }
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        do{
            try handler.perform([request])
        }catch let reqErr{
            print("Failed to perform request", reqErr)
        }

実行結果

良い感じにできていますね。

コードをリファクタリング(2022/1/19追記)

上記のコードだと少し分かりづらかったので、SwiftUI用にリファクタリングしました。

3つの機能を各関数として分けています。

・顔検出を行うコード

    func faceDetection() {
        let request = VNDetectFaceRectanglesRequest { (request, error) in
            if error != nil { return }
            var image = self.originalImage
            
            if(request.results?.count == 0){
                alertMessage = "顔が検出されませんでした。"
                showingAlert = true
                return
            }
            
            for observation in request.results as! [VNFaceObservation] {
                if let drawn = self.drawFaceRectangle(image: image, observation: observation){
                    image = drawn
                }
            }
            
            self.processImage = image
        }

        if let cgImage = self.originalImage?.cgImage {
            let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
            try? handler.perform([request])
        }
    }

・黒い四角形を描画するコード

    private func drawFaceRectangle(image: UIImage?, observation: VNFaceObservation) -> UIImage?{
        let imageSize = image!.size
        
        UIGraphicsBeginImageContextWithOptions(imageSize, false, 0.0)
        let context = UIGraphicsGetCurrentContext()
        image?.draw(in: CGRect(origin: .zero, size: imageSize))
        context?.fill(observation.boundingBox.converted(to: imageSize))
        
        let drawnImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return drawnImage
    }

・座標変換を行うコード

extension CGRect {
    func converted(to size: CGSize) -> CGRect {
        return CGRect(x: self.minX * size.width,
                      y: (1 - self.maxY) * size.height,
                      width: self.width * size.width,
                      height: self.height * size.height)
    }
}

それぞれの機能ごとに関数を作成すると可読性が上がって良いですね。^^