カテゴリー
iOS Swift

iPhoneで録画したポートレート動画の描画

iPhoneでポートレート(portrait == 縦方向に長い画像)撮影した動画は、フレームを描画すると意図した角度から90°反時計回りに回転して描画されてしまいます。その理由についてAppleのこのコードを元に勉強してみました。作成したサンプルはこちら

iPhoneで撮影した動画は全てランドスケープ(landscape == 横方向に長い画像)で保存されるようです。その動画がポートレートかランドスケープかの判定にはAVAssetTrackのpreferredTransformというプロパティを用いるということのようです。

preferredTransformはCGAffineTransformというクラスのインスタンスです。Affine Transform(アフィン変換)を理解するには行列の知識が必要なのですが、この「さつき先生」の動画レクチャーがウルトラ分かりやすいのでおすすめします。

CGAffineTransformはこのような行列です。

iPhoneで撮影したランドスケープ動画のpreferredTransformの値は以下のようになっています。これはつまり単位行列(identity matrix)です。

affine transform a: 1.0
affine transform b: 0.0
affine transform c: 0.0
affine transform d: 1.0
affine transform tx: 0.0
affine transform ty: 0.0
3×3の単位行列

単位行列はベクトルや行列にかけても、そのベクトルや行列を変化させません。これは、グラフィックスでは、元の動画を変化させない(スケール、回転、並行移動をさせない)ということです。

iPhoneで撮影したポートレート動画のpreferredTransformの値は以下のようになっていました。単位行列ではなく、aとdの値が0.0, bが1.0, cは-1.0という値になっています。またtxの値が1080(画像の縦寸法)になっています。(txが画像の縦寸法の値になっている理由がいまいちよく分かっていません、、、分かり次第記事をアップデートします)

affine transform a: 0.0
affine transform b: 1.0
affine transform c: -1.0
affine transform d: 0.0
affine transform tx: 1080.0
affine transform ty: 0.0

この行列の逆行列は以下です。bとcが入れ替わり、txとtyも入れ替わっています。

affine transform inverted a: 0.0
affine transform inverted b: -1.0
affine transform inverted c: 1.0
affine transform inverted d: 0.0
affine transform inverted tx: -0.0
affine transform inverted ty: 1080.0

このa, b, c, dの部分はつまり、θ = 270°の回転行列です。回転行列式の仕組みも、このさつき先生のハイパー分かりやすい動画レクチャーをご覧ください。さつき先生の説明にある回転行列と比べるとCGAffineTransformはcとdの位置が入れ替わっています。これはこの行列を右から掛けるか左から掛けるかの違いから来ています。

つまりこの preferredTransformの値は、逆行列を用いることで、単位行列の時はそのまま単位行列を返し、回転したい場合は回転行列を返して利用するところがミソです。

var affineTransform: CGAffineTransform {
    return self.videoTrack.preferredTransform.inverted()
}

UIImageViewには以下のようにCVPixelBuffer -> CIImage -> CGImage -> UIImage と変換させ、描画させています。

func displayFrame(_ frame: CVPixelBuffer?, withAffineTransform transform: CGAffineTransform) {
    DispatchQueue.main.async { // updating the UI needs to happen on the main thread
        if let frame = frame {            
            let ciImage = CIImage(cvPixelBuffer: frame)
                .transformed(by: transform) // applying affineTransform here, otherwise a portrait image will be displayed in landscape.
            let ciContext = CIContext(options: nil)
            guard let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) else { return }
            let uiImage = UIImage(cgImage: cgImage)
            self.imageView.image = uiImage
        }
    }
}