見出し画像

[Swift]SemanticSegmentationMatteの使い方

すでに世はiOS14ですが。iOS13で追加されたSemanticSegmentationMatteについてです。ピクセルを肌・髮・歯に分類してくれる機能ですね。
使ってみたくてググってもあまり情報がなくて、自分で色々試した結果と併せてまとめです。
間違いやベストプラクティスなどあれば指摘をお願いします。

まずSemanticSegmentationMatteの大前提として、A11 Bionicチップ搭載端末(当然それ以前も)では動かないっぽいです。

とりあえずうちのiPhone8ではダメでした。
A13だとiPhoneSEでも動きます。
A12・A12X・A12Zは該当端末が手元にないのでわかりませんが多分いけそうです。

もちろんシミュレータでカメラは動かせないので実機が必要になります。
Appleはシミュレータにカメラのドライバをつけてくれてもいいと思う…。

開発環境

MacOS: 10.15.6
Xcode: 11.7
Swift: 5.2.4
iPhone実機: iPhoneSE2
iOS: 13.4.1

サンプルコード

さすがに長くなってきたのでベタ貼りはしません。
以前に作ったプレビュー回転のサンプルに以降に説明する内容を盛り込んだコードへのリンクのみ掲載します。

AVCapturePhotoOutputの設定

isDepthDataDeliveryEnabledとenabledSemanticSegmentationMatteTypesの設定が必要になります。

厳密にはこの2つのプロパティに設定していない値をAVCapturePhotoSettingsの同名プロパティに設定するとエラーになります。

AVCapturePhotoSettingsの設定

AVCapturePhotoOutputと同じくisDepthDataDeliveryEnabledとenabledSemanticSegmentationMatteTypesの設定が必要になります。

enabledSemanticSegmentationMatteTypesは必ずしもavailableSemanticSegmentationMatteTypesにしないでも、例えばhairしか必要ないよという人は[AVSemanticSegmentationMatte.MatteType.hair]のみで問題ありません。

AVSemanticSegmentationMatteの取得

デリゲートメソッドAVCapturePhotoCaptureDelegate内で、AVCapturePhotoのメソッドsemanticSegmentationMatte(for:)メソッドを呼びます。

パラメータに.skinを与えれば肌の、.hairを与えれば髮の、.teethを与えれば歯のAVSemanticSegmentationMatteが取得できます。

画像への変換

CIImageのinit(semanticSegmentationMatte:options:)イニシャライザにmatteを与えます。
オプションはskinとhairとteethで別になります。
オプションなしでも変換できてるっぽいですが一応つけることにしました。

CIImageにはinit(cvPixelBuffer:options:)イニシャライザもあるのでAVSemanticSegmentationMatteのmattingImageを与えてもいいのですが、結局mattingImageに対してすることが特にないので、直にCIImageでいいかなと思います。
人によってはmattingImageに対して方向の修正を入れてるようです。

var matte = photo.semanticSegmentationMatte(for: .hair)
if let orientation = photo.metadata[ String(kCGImagePropertyOrientation) ] as? UInt32 {
   matte = matte.applyingExifOrientation(CGImagePropertyOrientation(rawValue: orientation)!)
}

今回は写真と合成するので、方向についてはCIImageに変換してからでよいと判断しました。

CIFilterの適用

matteのCIImageを取得してもただのグレースケール画像に過ぎないので、CIFilterによる画像加工が必要になります。
ここからは各自の利用目的に応じて変わってきます。
今回のサンプルでは、lanczosScaleTransform()によるMatte画像の拡大(写真にスケールをあわせる)、colorClamp()による色・アルファ(透過度)変更、blendWithMask()による写真との合成を実行しています。

CIFilterを初めて使用する人はとりあえずCore Image Programming Guidの"Processing Images"だけでも目を通しておくことをおすすめします。

SwiftUIでCIImageを使用する場合の注意点

UIImageにはinit(ciImage:)イニシャライザがありますが、これで初期化したUIImageをImageクラスに渡しても画像が表示されないことがあります。というか私が試した範囲では全く表示されませんでした。

CIImageのリファレンスまわりを確認すると、描画のタイミングについての記述が色々あって、特に“Processing an Image Using Built-in Filtersの記事には具体的に注記があります。

Important
To optimize computation, Core Image does not actually render any intermediate CIImage result until you force the CIImage to display its content onscreen, as you might do using UIImageView.

回避策として一旦CGImageに変換してからinit(cgImage:)イニシャライザでUIImageを初期化することで、CIImageを描画させることができます。

サンプルでCIImageをoriented(.right)で決め打ちで回転しているのは、CGImagePropertyOrientationのOverviewに時計回りに90°回した方がよさげに書いてあるのを参考にしています。

When the user captures a photo while holding the device in portrait orientation, iOS writes an orientation value of CGImagePropertyOrientation.right in the resulting image file. Software displaying the image can then, after reading that value from the file's metadata, apply a 90° clockwise rotation to the image data so that the image appears in the photographer's intended orientation.

あるいはUIViewRepresentableでラップすることでSwiftUIでもUIImageViewを使用することができます。その場合はCGImageを経由する必要はありません。
ただビューのサイズ調整などが面倒そうだったので、CGImage経由にした方が単純かつ綺麗なコードになります。
ドキュメントを見た感じ、ベストパフォーマンスを追求するならUIImageViewを頑張るべきかもしれません。

補足事項

作成したサンプルであれこれ撮影してみて、セマンティックセグメンテーションの対象にできる物を調べてみた結果です。

○セグメンテーションできる
写真:被写界深度の設定を必要とする割にモニターに映した写真も綺麗にセグメンテーションできます
複数人数:複数人数OKです
帽子付き:適当な写真で試してみましたがOKです
眼鏡付き:適当な写真で試してみましたがOKです
肌:肌色率高めのお姉さん画像で試してみましたが全身OKです

△セグメンテーションできたりできなかったりする
マスク:鼻の露出率で変わるのかな?モデルの学習に期待です。

ハマったところ

AVSemanticSegmentationMatteやCIFilterは動かすだけなら簡単で困ることはなかったのですが、表示まわりでハマりました。
AVCaptureVideoPreviewLayerのvideoGravityをresizeAspectFillからresizeAspectに変えて(=Fit)、ボタンとVStackで縦に並べようとしたところ、初期表示時の位置関係が崩れるようになってました。
ネット上のサンプルはみんなresizeAspectFill+ZStackなので参考になるコードもなく…。
自分であれこれ試した結果、ステータスバーを非表示にして、AVCaptureVideoPreviewLayerとボタン等はZStackで重ねて頑張って細かく表示調整、というのが今の最終解です。
現状のSwiftUIは単体で完結できないのでそこら辺が弱点ですね。
VStackで綺麗に並べられる方法を知っている方がいたら教えてください。

悩んだら

SemanticSegmentationMatteは全体的に参考情報が少ないですが、AVSemanticSegmentationMatteを取得して以降はAVPortraitEffectsMatteと扱いが変わらないのでそちらも探してみると答えが見つかるかもしれません。 ■

いいなと思ったら応援しよう!