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のコードを見つけました。
このリンク先の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)
}
}