iOSでの動画処理における「回転」「向き」の取り扱いでもう混乱したくない

AV Foundation を用いて動画処理を行う（＝カメラ入力をリアルタイムに処理する）プログラムを書いていると、回転・向きの取り扱いで混乱してしまうことが度々あります。

**関連しそうなプロパティやら何やらが多すぎてややこしい**、となんとなく思ってるけど洗い出してみればそうでもないのかも、とも思うので、ドキュメントに目を通しつつ、**コード書いて実機で挙動を確認**したりもしつつ、いったん整理みようと思った次第です。

##AVCaptureDevice の `position` による向きの違い

AVCaptureDevicePosition には `Back` と `Front` があって、要は**バックカメラ（背面カメラ）か、フロントカメラか**、の違い。

<img width="373" alt="camera.png" src="https://qiita-image-store.s3.amazonaws.com/0/3180/67ca92db-1650-08e4-ae78-e7beef84048f.png">

バックカメラの場合は、

```swift
return AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
```

フロントカメラの場合は、

```swift
guard let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as? [AVCaptureDevice] else {fatalError()}
for device in devices where device.position == .Front {
    return device
}
```

という感じで初期化できます。

で、下記のように AVCaptureConnection のプロパティのうち回転・向きに関係のありそうな `videoOrientation`, `videoMirrored`, `automaticallyAdjustsVideoMirroring`プロパティをそれぞれコンソールに出力してみると、

```swift
let videoDataOutput = AVCaptureVideoDataOutput()
captureSession.addOutput(videoDataOutput)
videoConnection = videoDataOutput.connectionWithMediaType(AVMediaTypeVideo)
print("orientation: \(videoConnection.videoOrientation.rawValue)")
print("mirrored: \(videoConnection.videoMirrored)")
print("auto mirroring: \(videoConnection.automaticallyAdjustsVideoMirroring)")
```

リアカメラの場合：

```swift
orientation: 3  // AVCaptureVideoOrientation.LandscapeRight.rawValue と同値
mirrored: false
auto mirroring: false
```

フロントカメラの場合：

```swift
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 の一覧が取得できます。

```swift
print("available formats: \(videoDevice.formats)")
```

これらをコンソール出力してみると、基本的にいずれも**横長**フォーマット。

> 'vide'/'420f' **3840x216**0, { 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 を取得するようにしてみても、

```swift
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」の[この記事](https://www.objc.io/issues/23-video/core-image-video/)のサンプルコードを追っていくと、「カメラ入力を90°回転させる」（LandscapeなものをPortraitにする）＆「フロントカメラの場合は水平方向に反転させる」という処理を

```swift
let input = CIImage(buffer: buffer).imageByApplyingTransform(transform)
```

とサンプルバッファを受け取った後の処理で、Core Image を用いて行っています。


で、「AVFoundation プログラミングガイド」には、『ビデオの向きを設定する』という項目がありまして、そこには、

> AVCaptureOutput に接続したとき、画像を出力する向きは AVCaptureConnection に設定します。

> videoOrientation プロパティで、出力ポートで画像をどの向きにしたい
か指定できます。

と明記されているので、カメラから取得した画像データを所望の向きに補正したい場合は、**AVCaptureConnection の `videoOrientation` を設定する** のが Apple としてのファイナルアンサーのようです。

たとえば Portrait にしたい場合は次のようにします。

```swift
videoConnection.videoOrientation = .Portrait
```

##プレビュー方法の違いによる混乱

カメラ入力をお手軽にプレビュー表示したい場合、

```swift
let layer = AVCaptureVideoPreviewLayer(session: captureSession)
```

こんな感じで AVCaptureVideoPreviewLayer を生成してそのレイヤーをどこかに貼っ付ければokです。

が、たとえば各ビデオフレームに対してGPUで高速に何らかの処理を行いたい場合に、Core Image で処理するとします。その処理結果を UIImage に変換して UIImageView に描画とかしてると、たぶん処理が追いつきません。

そういう場合に、GLKit の GLKView に描画したりします。

```swift
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` にしてみます。

```swift
videoConnection.videoMirrored = true
```

当たり前ですが**水平方向に画像が反転**しました。フロントカメラだとそれによって鏡で自分を見たときの状態になってちょうどいいのですが、背面カメラの場合にはおかしくなります。

![mirrored.jpg](https://qiita-image-store.s3.amazonaws.com/0/3180/290c32b8-2d90-7188-88e4-0d01c02477d8.jpeg)


###`automaticallyAdjustsVideoMirroring`

を `true` にしてみます。

```swift
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` が逆になる**ことぐらいです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/3180/8a449822-006b-73a6-ca5d-dec0ae518770.jpeg" width="360">

（左は `videoMirrored` が `true`、右は `false`。**同じ方向に頭を傾けて**フロントカメラからの入力について CIDetector でリアルタイム顔認識を行い、`leftEyePosition` を青で、`rightEyePosition` を緑で示した）

というわけで基本的にフロントカメラ利用時は `videoMirrored` を `true` としておくことで余計な混乱は避けられそうです。


##AVAssetTrack の `preferredTransform`

ビデオの編集に関してはいまのところ対象外で考えていたのですが、見かけたので書いておきます。

「AVFoundation プログラミングガイド」に、『ビデオの向きを調べる』という項目があります。

> ビデオトラックやオーディオトラックをコンポジションに追加した後、2本のビデオトラックの向きが正しいかどうか確認する必要があります。**デフォルトでは、ビデオトラックはすべて横長モードと想定**します。縦長モードの場合、エクスポートすると正しい向きになりません。また、縦長モードのビデオを横長モードのそれと組み合わせようとしても、エクスポートセッションで処理に失敗してしまいます。

で、その下にサンプルコードが貼ってあって、どうやっているかというと、

```objc
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動画処理における回転・向きを取り扱えそうだなと。

間違いなどあればぜひご指摘ください。