PIYO - Tech & Life -

AVCaptureOutputで撮影した写真のimageOrientationを修正する

iOS Swift

AVCaptureOutputで撮影した写真はUIImageViewに表示したり本体のフォトアルバムに保存する限りは問題ないのですが、別のソフトウェアやライブラリが向きを正しく解釈してくれなくてうまくいかないというケースが起こります。

↓は典型的なAVFoundationを用いた写真撮影のコードです。だいぶ端折って書いてますが、だいたいこんな感じになるはずです。ボタンを押すと写真を撮影できるというイメージです。

class CameraViewController : UIViewController {
    var photoOutput : AVCapturePhotoOutput!

    override func viewDidLoad() {
        super.viewDidLoad()

        // カメラ色々
        configureDevice()
    }

    // 撮影ボタン
    @IBAction func takePhoto(_ sender: Any) {
        let settingsForMonitoring = AVCapturePhotoSettings()
        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true
        photoOutput?.capturePhoto(with: settingsForMonitoring, delegate: self)
    }
}

extension CameraViewController : AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        let imageData = photo.fileDataRepresentation()
        let image = UIImage(data: imageData!)
        // imageのimageOrientationは .right
    }
}

didFinishProcessingPhotoの中で撮影結果がえられるので、Dataを経由してUIImageにしています。

これをUIImageViewに設定したりアルバムに保存したりしても画像の向きの問題は起こりません。

僕が気がついたのは、iOSで使えるVisionフレームワークを使った顔認識機能に画像を渡したときです。AVCapturePhotoOutputから得られたUIImageを使って顔認識をしようとしたところ、まっすぐ撮影した画像ではだめで、90度横にした場合に顔認識の結果が描画(目の位置や口の位置などのマーク)されたのです。

詳細は今度紹介しますが、コードは↓のようなイメージです。

import Vision

class FaceLandmarksDetector {
    open func highlightFaces(for source: UIImage, complete: @escaping (UIImage) -> Void) {
        var resultImage = source
        let detectFaceRequest = VNDetectFaceLandmarksRequest { (request, error) in
            // 略
            complete(resultImage)
        }

        let vnImage = VNImageRequestHandler(cgImage: source.cgImage!, options: [:])
        try? vnImage.perform([detectFaceRequest])
    }
}

顔認識の詳細はさておき、考えられるのはimageOrientationが考慮されていないということです。imageOrientation.upの状態で正しい向きになっている写真にしてから顔認識にかける必要が、どうやらありそうです。

現在のimageOrientationを元に元画像を回転させたうえで、新しいimageOrientation.upとして画像を再生成するような対応をすれば解決しそうです。

その手のコードはしっかり書こうとすると大変なんですが、同じようなことで困っている人はたくさんいるはずなので色々探してみたら良い感じのGistのコードを見つけました。

Extension to fix orientation of an UIImage (Sets orientation to portrait) · GitHub
Extension to fix orientation of an UIImage (Sets orientation to portrait) - UIImageFixedOrientationExtension.swift

このリンク先のGistにSwift 4 tested ( + handled some cases)というバージョンがあり、読んだ感じちゃんと動きそうだったので使ってみました。これによって無事顔認識が動いてくれました。

以下フルバージョンです。

extension UIImage {

    func fixedOrientation() -> UIImage? {

        guard imageOrientation != UIImageOrientation.up else {
            //This is default orientation, don't need to do anything
            return self.copy() as? UIImage
        }

        guard let cgImage = self.cgImage else {
            //CGImage is not available
            return nil
        }

        guard let colorSpace = cgImage.colorSpace, let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
            return nil //Not able to create CGContext
        }

        var transform: CGAffineTransform = CGAffineTransform.identity

        switch imageOrientation {
        case .down, .downMirrored:
            transform = transform.translatedBy(x: size.width, y: size.height)
            transform = transform.rotated(by: CGFloat.pi)
            break
        case .left, .leftMirrored:
            transform = transform.translatedBy(x: size.width, y: 0)
            transform = transform.rotated(by: CGFloat.pi / 2.0)
            break
        case .right, .rightMirrored:
            transform = transform.translatedBy(x: 0, y: size.height)
            transform = transform.rotated(by: CGFloat.pi / -2.0)
            break
        case .up, .upMirrored:
            break
        }

        //Flip image one more time if needed to, this is to prevent flipped image
        switch imageOrientation {
        case .upMirrored, .downMirrored:
            transform.translatedBy(x: size.width, y: 0)
            transform.scaledBy(x: -1, y: 1)
            break
        case .leftMirrored, .rightMirrored:
            transform.translatedBy(x: size.height, y: 0)
            transform.scaledBy(x: -1, y: 1)
        case .up, .down, .left, .right:
            break
        }

        ctx.concatenate(transform)

        switch imageOrientation {
        case .left, .leftMirrored, .right, .rightMirrored:
            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
        default:
            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
            break
        }

        guard let newCGImage = ctx.makeImage() else { return nil }
        return UIImage.init(cgImage: newCGImage, scale: 1, orientation: .up)
    }
}