AV Foundation を用いて動画処理を行う(=カメラ入力をリアルタイムに処理する)プログラムを書いていると、回転・向きの取り扱いで混乱してしまうことが度々あります。
関連しそうなプロパティやら何やらが多すぎてややこしい、となんとなく思ってるけど洗い出してみればそうでもないのかも、とも思うので、ドキュメントに目を通しつつ、コード書いて実機で挙動を確認したりもしつつ、いったん整理みようと思った次第です。
##AVCaptureDevice の position
による向きの違い
AVCaptureDevicePosition には Back
と Front
があって、要はバックカメラ(背面カメラ)か、フロントカメラか、の違い。
バックカメラの場合は、
return AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
フロントカメラの場合は、
guard let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as? [AVCaptureDevice] else {fatalError()}
for device in devices where device.position == .Front {
return device
}
という感じで初期化できます。
で、下記のように AVCaptureConnection のプロパティのうち回転・向きに関係のありそうな videoOrientation
, videoMirrored
, automaticallyAdjustsVideoMirroring
プロパティをそれぞれコンソールに出力してみると、
let videoDataOutput = AVCaptureVideoDataOutput()
captureSession.addOutput(videoDataOutput)
videoConnection = videoDataOutput.connectionWithMediaType(AVMediaTypeVideo)
print("orientation: \(videoConnection.videoOrientation.rawValue)")
print("mirrored: \(videoConnection.videoMirrored)")
print("auto mirroring: \(videoConnection.automaticallyAdjustsVideoMirroring)")
リアカメラの場合:
orientation: 3 // AVCaptureVideoOrientation.LandscapeRight.rawValue と同値
mirrored: false
auto mirroring: false
フロントカメラの場合:
orientation: 4 // AVCaptureVideoOrientation.LandscapeLeft.rawValue と同値
mirrored: false
auto mirroring: false
という結果となりました。
ここからわかること:
- **どちらのカメラでもデフォルトでは Landscape (横長)**である
- Right/Leftの違いはある
- リア/フロントのそれぞれのカメラが鏡合わせの関係にあることを考えると、違和感のない結果
-
videoMirrored
はどちらもfalse
- フロントカメラの場合は
true
になってるのかと思ってた
- フロントカメラの場合は
-
automaticallyAdjustsVideoMirroring
もどちらもfalse
- ドキュメント(ヘッダ)に書いてあることと違うような・・・!?
/*!
@property automaticallyAdjustsVideoMirroring
@abstract
// 略
@discussion
// 略
The default value is YES.
*/
##AVCaptureDevice の activeFormat
は回転・向きに影響するか?
AVCaptureDevice の formats
でそのデバイスで利用可能な AVCaptureDeviceFormat の一覧が取得できます。
print("available formats: \(videoDevice.formats)")
これらをコンソール出力してみると、基本的にいずれも横長フォーマット。
'vide'/'420f' 3840x2160, { 3- 30 fps}, fov:58.632, supports vis, max zoom:130.88 (upscales @1.00), AF System:2, ISO:23.0-736.0, SS:0.000013-0.333333
'vide'/'420f' 4032x3024, { 3- 30 fps}, fov:57.716, max zoom:189.00 (upscales @1.00), AF System:2, ISO:23.0-1840.0, SS:0.000013-0.333333
ちなみに、試しに下記のように activeFormat
の CMVideoDimensions を取得するようにしてみても、
let description = videoDevice.activeFormat.formatDescription
print(CMVideoFormatDescriptionGetDimensions(description))
print(CMVideoFormatDescriptionGetPresentationDimensions(description, true, true))
結果は同じでした。(printでもこの値を出力してくれている)
##ビデオの向きはどこで設定すべきか?
色んなOSSやサンプルコードを見ていると、カメラから取得した画像(動画のフレーム)の向きを補正する場合に、様々な流儀を見かけます。
- AVCaptureVideoPreviewLayer を回転
- CMSamplerBuffer -> CIImage ->
imageByApplyingTransform:
で CGAffineTransform を渡して回転 - AVCaptureConnection の
videoOrientation
で設定 - etc..
たとえば世界的に有名なブログ「objc.io」のこの記事のサンプルコードを追っていくと、「カメラ入力を90°回転させる」(LandscapeなものをPortraitにする)&「フロントカメラの場合は水平方向に反転させる」という処理を
let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
とサンプルバッファを受け取った後の処理で、Core Image を用いて行っています。
で、「AVFoundation プログラミングガイド」には、『ビデオの向きを設定する』という項目がありまして、そこには、
AVCaptureOutput に接続したとき、画像を出力する向きは AVCaptureConnection に設定します。
videoOrientation プロパティで、出力ポートで画像をどの向きにしたい
か指定できます。
と明記されているので、カメラから取得した画像データを所望の向きに補正したい場合は、AVCaptureConnection の videoOrientation
を設定する のが Apple としてのファイナルアンサーのようです。
たとえば Portrait にしたい場合は次のようにします。
videoConnection.videoOrientation = .Portrait
##プレビュー方法の違いによる混乱
カメラ入力をお手軽にプレビュー表示したい場合、
let layer = AVCaptureVideoPreviewLayer(session: captureSession)
こんな感じで AVCaptureVideoPreviewLayer を生成してそのレイヤーをどこかに貼っ付ければokです。
が、たとえば各ビデオフレームに対してGPUで高速に何らかの処理を行いたい場合に、Core Image で処理するとします。その処理結果を UIImage に変換して UIImageView に描画とかしてると、たぶん処理が追いつきません。
そういう場合に、GLKit の GLKView に描画したりします。
myContext.drawImage(image, inRect: destRect, fromRect: image.extent)
で、この GLKView に毎フレーム描画してプレビューするのと、AVCaptureVideoPreviewLayer でプレビューするのとで、向きに関して何か違いがあるのか、というと大アリで、
- AVCaptureVideoPreviewLayer の場合、AVCaptureConnection の
videoOrientation
と iOSデバイス(e.g.iPhone)の向きが合ってても合ってなくても正しい向きで描画 される - GLKView の場合、CIImage等への変換を行った上で描画するので、AVCaptureConnection の
videoOrientation
を適切に設定する必要がる
後者の挙動は考えてみれば当たり前で、Landscapeで撮影した画像をPortraitで表示しようとすれば90°回転してしまうのは当然です。
こうやって整理してみるまで気づいてなかったのは前者の挙動ですが、AVCaptureVideoPreviewLayer は向きを自動で調整してくれるのでしょうか?(リファレンスやドキュメントにそれらしい記述は見当たらず)
ちなみに AVCaptureVideoPreviewLayer ではフロントカメラ利用時の mirrored の調整も自動でやってくれてました。(その際に AVCaptureConnection の videoMirrored
の値は変わっていない)
##AVCaptureConnection のミラー系プロパティの挙動
「AVCaptureDevice の position
」の項で調べた通りデフォルトではどちらも false
だったわけですが、true
にセットするとどうなるのか見てみます。
※上述した通り AVCaptureVideoPreviewLayer を使うと勝手に調整されてしまうので GLKView に描画して試してみました。
###videoMirrored
を、true
にしてみます。
videoConnection.videoMirrored = true
当たり前ですが水平方向に画像が反転しました。フロントカメラだとそれによって鏡で自分を見たときの状態になってちょうどいいのですが、背面カメラの場合にはおかしくなります。
###automaticallyAdjustsVideoMirroring
を true
にしてみます。
videoConnection.automaticallyAdjustsVideoMirroring = true
print("mirrored: \(videoConnection.videoMirrored)")
フロントカメラのときだけ自動的に videoMirrored
を true
にしてくれる、という挙動を期待していたのですが、背面カメラでもフロントカメラでも、videoMirrored
は false
のまま、描画結果もそのままでした。。
もう一度ドキュメントを読んでみると、
When the value of this property is YES, the value of @"videoMirrored" may change depending on the configuration of the session, for example after switching to a different AVCaptureDeviceInput.
とあるので、カメラを前面 ↔ 背面と切り替えた際に整合性を取ってくれる、というもののようです。
##CIFaceFeature の座標・角度の取り扱い
CIDetector を用いて顔認識を行った場合、CIFaceFeature という構造体として認識結果を受け取れます。
Core Image における座標系は垂直方向(y軸)についてUIKitと反転している、ということはよく知られていますが、以前個人的によくわからなくなったのが、水平方向の取り扱いです。
背面カメラのときは問題ないとして、**フロントカメラのときは認識結果の矩形(bounds
)の座標や角度(faceAngle
)を水平方向に反転させて考えるべきなのか?**と。
結論からいうと、videoMirrored
が true
でも false
でも、水平方向に座標や角度を反転させる必要はありません。CIDetector は入力されてきたものを処理するだけなので、そりゃそうです。(以前混乱したときは、どこか変なところで水平方向への反転をかけていたのかなと)
注意点は、フロントカメラ利用時に videoConnection.videoMirrored = false
としているときは、leftEyePosition
と rightEyePosition
が逆になることぐらいです。
(左は videoMirrored
が true
、右は false
。同じ方向に頭を傾けてフロントカメラからの入力について CIDetector でリアルタイム顔認識を行い、leftEyePosition
を青で、rightEyePosition
を緑で示した)
というわけで基本的にフロントカメラ利用時は videoMirrored
を true
としておくことで余計な混乱は避けられそうです。
##AVAssetTrack の preferredTransform
ビデオの編集に関してはいまのところ対象外で考えていたのですが、見かけたので書いておきます。
「AVFoundation プログラミングガイド」に、『ビデオの向きを調べる』という項目があります。
ビデオトラックやオーディオトラックをコンポジションに追加した後、2本のビデオトラックの向きが正しいかどうか確認する必要があります。デフォルトでは、ビデオトラックはすべて横長モードと想定します。縦長モードの場合、エクスポートすると正しい向きになりません。また、縦長モードのビデオを横長モードのそれと組み合わせようとしても、エクスポートセッションで処理に失敗してしまいます。
で、その下にサンプルコードが貼ってあって、どうやっているかというと、
CGAffineTransform firstTransform = firstVideoAssetTrack.preferredTransform;
// Check the first video track's preferred transform to determine if it was recorded in portrait mode.
if (firstTransform.a == 0 && firstTransform.d == 0 && (firstTransform.b == 1.0 || firstTransform.b == -1.0) && (firstTransform.c == 1.0 || firstTransform.c == -1.0))
{
isFirstVideoPortrait = YES;
}
なんと、AVAssetTrack の preferredTransform
から取り出した CGAffineTransform、すなわちアフィン変換を行う行列を見て、ポートレートかどうかの判定を行っているようです。(ハードルが高い。。)
##まとめ
まだありそうですが、個人的に気になっていた部分は解消されたので、このへんにしておきます。
ごちゃごちゃと書いてますが、個人的な結論としては、
- AVFoundationを用いたカメラ入力は、デフォルトでは横長モード(Landscape)である
- AVCaptureVideoPreviewLayer は色々よしなにやってくれているのでその描画(プレビュー)結果を見て油断しないこと
- 向きを変更したい場合は AVCaptureConnection の
videoOrientation
を利用する - フロントカメラ利用時は
videoMirrored
をtrue
としておく
以上の点をおさえておけば混乱なくスッキリとiOS動画処理における回転・向きを取り扱えそうだなと。
間違いなどあればぜひご指摘ください。