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

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

 1class CameraViewController : UIViewController {
 2    var photoOutput : AVCapturePhotoOutput!
 3
 4    override func viewDidLoad() {
 5        super.viewDidLoad()
 6
 7        // カメラ色々
 8        configureDevice()
 9    }
10
11    // 撮影ボタン
12    @IBAction func takePhoto(_ sender: Any) {
13        let settingsForMonitoring = AVCapturePhotoSettings()
14        settingsForMonitoring.isAutoStillImageStabilizationEnabled = true
15        photoOutput?.capturePhoto(with: settingsForMonitoring, delegate: self)
16    }
17}
18
19extension CameraViewController : AVCapturePhotoCaptureDelegate {
20    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
21        let imageData = photo.fileDataRepresentation()
22        let image = UIImage(data: imageData!)
23        // imageのimageOrientationは .right
24    }
25}

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

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

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

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

 1import Vision
 2
 3class FaceLandmarksDetector {
 4    open func highlightFaces(for source: UIImage, complete: @escaping (UIImage) -> Void) {
 5        var resultImage = source
 6        let detectFaceRequest = VNDetectFaceLandmarksRequest { (request, error) in
 7            // 略
 8            complete(resultImage)
 9        }
10
11        let vnImage = VNImageRequestHandler(cgImage: source.cgImage!, options: [:])
12        try? vnImage.perform([detectFaceRequest])
13    }
14}

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

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

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

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

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

 1extension UIImage {
 2
 3    func fixedOrientation() -> UIImage? {
 4
 5        guard imageOrientation != UIImageOrientation.up else {
 6            //This is default orientation, don't need to do anything
 7            return self.copy() as? UIImage
 8        }
 9
10        guard let cgImage = self.cgImage else {
11            //CGImage is not available
12            return nil
13        }
14
15        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 {
16            return nil //Not able to create CGContext
17        }
18
19        var transform: CGAffineTransform = CGAffineTransform.identity
20
21        switch imageOrientation {
22        case .down, .downMirrored:
23            transform = transform.translatedBy(x: size.width, y: size.height)
24            transform = transform.rotated(by: CGFloat.pi)
25            break
26        case .left, .leftMirrored:
27            transform = transform.translatedBy(x: size.width, y: 0)
28            transform = transform.rotated(by: CGFloat.pi / 2.0)
29            break
30        case .right, .rightMirrored:
31            transform = transform.translatedBy(x: 0, y: size.height)
32            transform = transform.rotated(by: CGFloat.pi / -2.0)
33            break
34        case .up, .upMirrored:
35            break
36        }
37
38        //Flip image one more time if needed to, this is to prevent flipped image
39        switch imageOrientation {
40        case .upMirrored, .downMirrored:
41            transform.translatedBy(x: size.width, y: 0)
42            transform.scaledBy(x: -1, y: 1)
43            break
44        case .leftMirrored, .rightMirrored:
45            transform.translatedBy(x: size.height, y: 0)
46            transform.scaledBy(x: -1, y: 1)
47        case .up, .down, .left, .right:
48            break
49        }
50
51        ctx.concatenate(transform)
52
53        switch imageOrientation {
54        case .left, .leftMirrored, .right, .rightMirrored:
55            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
56        default:
57            ctx.draw(self.cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
58            break
59        }
60
61        guard let newCGImage = ctx.makeImage() else { return nil }
62        return UIImage.init(cgImage: newCGImage, scale: 1, orientation: .up)
63    }
64}