MediaPipeのpre/post-processメモ

GoogleのMediaPipe良いですね。コア部分はTensorFlowLiteでできていて、Pre/Postプロセス部分をUnityで書き直すことで移植できました。まだ勉強中ですがメモ。

github.com

↓ こんな感じでUnity上で動作してます。

Face Tracking and Blaze Face

Blaze Pose

Hand Tracking

全体

MediaPipeのBlaze Face, Balze Pose, Hand Trackingは細かい違いはあれど、かなり似ていて。大きく分けて2つTensorFlow Liteのモデルが動いています。

  1. SSD Detection: オブジェクトの矩形エリアを推定します。
  2. Landmark Detection: オブジェクトの画像からランドマーク(骨格など)を推定します。

MediaPipeはWebで処理を見ることができます。以下はBlaze Faceの例です。Max/MSPのノードのような感じで全体を俯瞰できるので、まずここから概要を掴みました。

f:id:asus4:20200904221738p:plain
MediaPipe Web Capture
https://viz.mediapipe.dev/demo/face_detection

その他のモデルもこちらから見れます。

それぞれのTFLiteについて見ていきます。

1. SSD Detection

SSD(Single Shot Detetor)でオブジェクトの矩形エリアを返します。

Pre Process

とくに難しいことはなく、入力画像のサイズにトリムしてfloatの配列に変換するだけです。

がPC上のWebカメラでやるときは簡単ですが、スマートフォンではWebcamTextureは回転とフリップしてるんですよね…。とても面倒くさいです。私の場合は入力画像とWebカメラの回転の正規化を一度にするシェーダーを用意しました。

Post Process

とりあえず重なる部分も含めて多めに返して、CPU側でNon Max Suppressionというアルゴリズムを使って精度を上げています。Courseraの動画で概要を理解しました。最近他のライブラリにも追加されてるの見ました。

通常のSSDモデルは推定された矩形エリアを返すだけですが。これはキーポイントと呼ばれる特徴点も返します。これは次のプロセスで使います。

2. Landmark Detection

Pre Process

カメラ画像から対象のオブジェクト部分をトリムします。トリムするときに、キーポイントの情報をもとに回転を直します。これは頭良いなーと。回転を直してからLandmarkを検出するモデルに画像を入力することで精度が上がるとのこと。当初バグで、間違った回転をした画像をLandmark Detectionのモデルに入力したのですが、精度が全然違いました。

youtu.be

緑色が矩形エリアとキーポイント。赤が回転とクロップする範囲。右上のちっこい手が実際のLandmark Detectionへの入力画像です。手が横向きになっても入力画像は上向きなのがわかると思います。

Post Process

FaceMeshとHand Landmarkは出力されたランドマークをそのまま表示しています。BlazePoseはすこし値が暴れるのかRelative Velocity Filterというものを使っています。移動が少ないときは強めのローパスフィルタ。移動が多いときはフィルタを弱めるというもののようです。

返されるランドマークは、入力画像に対しての値です。それを、2段階の画像入力で使ったMatrix4x4の逆行列をかけて、Unityの座標に戻しています。この変換も結構ややこしかったです。

まとめ

MediaPipeの処理はかなり似ていて、一度作るとHandTracking, BlazePose, BlazeFaceも大体同じプログラムが適用できました。機械学習の勉強のために初めてTensorFlow LiteのUnity移植ですが、共通する処理が結構出てきて。自分のためになっている気がします。

参考

TensorFlow Lite界隈は日本人が多く活躍していますね。@PINTO03091さん、@terrykyさんのTwitter, GitHubをよく参考にさせていただいております。

NeumorphismなUIをUnity uGUI上につくる

DrribleやPinterestでよく見るデザイントレンドになっているNeumorphism。 とてもおしゃれに見えますが、みんなMaterial Designに飽きてきて、目新しさ目当てで流行ってるだけちゃうのという意見もちらほら目にします。私も懐疑派でした。

一方でこんな意見も。

note.com

なるほど。今後xRなアプリケーションも同時に開発するようになったときには、ライティングや立体感が重要も再び重要になってくるのかもと。 まだ答えは出ていませんが、、。

静的にfigmaPhotoshopのエフェクトで作った立体感ではなく、動的にユーザーの動作に合わせて自然に変化すると面白いんじゃないかと…。個人的な実験の意味も込めて以下のようなデモを作って見ました。

gif

UnityのシェーダーでNeumorphismを再現して。リアルタイムに影や凹凸を変化させられるようにしています。更に画像をかなり拡大してもエッジはきれいなまま。

Neumorphismを解析する

Unityで実装を説明する前にまずNeumorphismがどうやって作られているかFigmaで公開してくれているサンプルを分析してみました。こちらを参考にしました。

dribbble.com

一番シンプルそうなパネルをFigmaで見てみます。(CSSで描くのは簡単だ…。)

f:id:asus4:20200811220919p:plain
figma-ui

  • 光の方向は左上から右下に。
  • 全体にうっすらとグラデーション
  • 右下に暗い色のドロップシャドウ
  • 左上に明るい色のドロップシャドウ
  • 縁を細いベベルでグラデーション

ざっくりこんな感じでしょうか。これを参考にしながらUnity上でできる範囲で作っていきます。

SDFで拡大に強くきれいな線を描く

Unityで作るスマホゲームでUIをピクセルパーフェクトで作ってるゲームってあるんでしょか?@1x @2x @3xなテクスチャを用意してるゲーム。実際 iPhone SE(640x1136)からiPad Pro 12.9(2048x2732)までサポートするようなゲームで無理…ですよね。FullHDでデザインして拡縮している現場が多いように思います。Vector Graphicsモジュールもありますが、UI全部に使ってしまうと重いですね。

今回はTextMesh Proでも使われているSDF(signed distance field)を使ってみました。SDFはパスからの距離をテクスチャに書き込むことでテクスチャ解像度が小さくてもきれいなエッジを再現できます。私が仕事で開発に参加してるLyric SpeakerでもSDFを使ってきれいなフォントを描画しています。

さらに今回はSDFの改良版アルゴリズムのMSDFを使ってみました。詳しくはGitHubリポジトリと、こちらの論文PDFが詳しいです。

github.com

https://user-images.githubusercontent.com/18639794/86908136-5fdd4780-c116-11ea-96c5-4f58a42043a4.pnghttps://user-images.githubusercontent.com/18639794/86908146-6370ce80-c116-11ea-87ee-95bfb699665c.pnghttps://user-images.githubusercontent.com/18639794/86908155-65d32880-c116-11ea-9583-1b45f806bbd9.png msdfgenより引用

簡単に言うとSDFではグレースケールでエッジとの距離を書き込んでいましたが、MSDFではRGBチャンネルそれぞれに角度ごとに分けた距離を書き込むことで、小さい解像度でも角がきれいに出るように改良したようです。

反面、テクスチャ圧縮をかけてしまうとmsdfはきれいに表示されないので、無圧縮に設定する必要があります。容量削減の意味では、効果は少ないです。

実際にshaderのコードを簡略化するとこんな感じになります。

inline float msdf(sampler2D tex, float2 uv)
{
    // MSDFのテクスチャ取得
    half3 c = tex2D(tex, uv);
    // RGB平均値の取得
    return max(min(c.r, c.g), min(max(c.r, c.g), c.b)) - 0.5;
}

fixed4 frag(v2f IN) : SV_Target
{
   // パスからの距離をfloatで
   float sdf = msdf(_MainTex, IN.texcoord);

   // UIのTint colorを取得
   half4 color = IN.color;

   // パスの境界線より外側のアルファを0に
   float2 sdfUnit = _PixelRange / _MainTex_TexelSize.zw;
   float clipSdf = sdf * max(dot(sdfUnit, 0.5 / fwidth(IN.texcoord)), 1);
   color.a *= saturate(clipSdf + 0.5);
   
   return color;
}

肝の部分はRGB平均値を取得する部分と、クリッピングする部分だと思いますが、とてもシンプルで良いですね。

SDFでドロップシャドウを描く

Unity UI付属の公式Dropshadow エフェクトはただ同じ画像をコピーしてオフセットかけているだけですが、
SDFはきれいな線を描くだけではなく、パス境界との距離と、勾配がわかるので、ドロップシャドウなどのエフェクトにも使えます。TextMeshProのエフェクト設定動画がわかりやすいです。

www.youtube.com

Normalを取得するコードは以下のようになります。

// さっきと同じ平均値を返すだけ。
inline float msdf_median(float3 c)
{
    return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
}
 
// ノーマル画像を返す
inline float3 msdf_normal(sampler2D tex, float4 texel, float2 uv)
{
    texel *= 2;
    float left = msdf_median(tex2D(tex, float2(uv.x - texel.x, uv.y)));
    float right = msdf_median(tex2D(tex, float2(uv.x + texel.x, uv.y)));
    float bottom = msdf_median(tex2D(tex, float2(uv.x, uv.y - texel.y)));
    float top = msdf_median(tex2D(tex, float2(uv.x, uv.y + texel.y)));
    return float3(left - right, bottom - top, 0);
}

以上のように隣接ピクセルからの勾配情報をNormalテクスチャのような感じで取得します。これをもとにブラーやベベルを書き込んでいます。

Unity uGUI上に組み込む

実際にUnity uGUI上にNeumorphismデザインを実装してみます。

ドロップシャドウをuGUIのエフェクトとして使う。

github.com

UIEffectで行われているUIMeshの拡張を参考にしています。

同じ画像をコピーしても作れるのですが、一個の画像を作るのにレイヤー3枚…。などは避けたかったため。

すべてのNeumorphismコンポーネントの中ですべての頂点を一つにまとめ、uv2の中に、オフセット上をを埋め込み。Vertex shaderでDirectional Lightの方向にオフセットをかけています。

f:id:asus4:20200811222851p:plain
fig uv2

こんな感じです。

改善点

SDFでは実際にはテクスチャサイズより小さな領域が描画されます。配置のときに手動で直してましたが、ライブラリ側でサイズの計算を吸収できるのがベストです。

まとめ

このNeumorphism UIはGitHub上で公開しています。

github.com

またNeumorphismにしなくてもMSDFを使ったUIは使い所がありそうです。

ほっといても実践で使われることはなさそうなので、先日公開した自分のアプリでこのシェーダーを使っています。

asus4.hatenablog.com

おまけで:実験的に加速度センサでライティングの向きが変わる機能を搭載してたりします。 mobile.twitter.com

TensorFlow LiteとUnityでカメラアプリを作って公開しました。

こちらからダウンロードできます!

AnyFilter

AnyFilter

  • Koki Ibukuro
  • Photo & Video
  • Free
apps.apple.com

去年の年末頃からUnity上でTensorFlow Liteを動かして遊んでいます。その中でできたプロトタイプをTwitterで公開したところ↓思いの外評判が良かったので、誰でも試せるようにiOSアプリとして公開してみました。

コアの機能はGitHubで公開してます。 github.com

普段の受託案件では自分でUIデザインすることはまれですが、一人プロジェクトだったためUIもつくりました。
UnityでゲームぽくないUIをデザインするときに、Unityで複数の解像度に対応するためにpixel perfectをあきらめることが多かったため、UIを全部SDF(Signed Distance Field) Textureを使って作ってみました。更に実験でNeumorphismをuGUI上で再現しています。
加速度センサでドロップシャドウの向きが変わったりします。

といった感じで個人的な実験を公開してみました。良ければダウンロードしてみてください!

NativeArray<T>を高速にシリアライズ/デシリアライズ

高速化TIPSです。以前、ARKitの顔の頂点のデータをUnityEditorに送るプログラムを書きました。

mobile.twitter.com この様なことができます。

UnityEditorには毎フレーム以下のデータを送っています。

public NativeArray<Vector3> vertices; 
public NativeArray<Vector3> normals; 
public NativeArray<int> indices; 
public NativeArray<Vector2> uvs;

頂点の数が多くシリアライズ、デシリアライズがパフォーマンスのネックになっていました。直接ポインタをゴニョゴニョすると行けるのではないかと調べているとNativeSliceにSliceConvertという型を変換できるメソッドがあることに気づきました。これを使うと以下の様なことができます。

gist.github.com

使い方です。

NativeArray<Vector3> vertices;

// シリアライズ
byte[] verticesBytes = vertices.ToRawBytes();

// デシリアライズ
NativeArray<Vector3> vertices2 = NativeArrayExtension.FromRawBytes<Vector3>(verticesBytes, Allocator.Temp);

Unity AR FoundationをEditor上でシミュレーションする方法

iOS13が公開されたことで、beta版をインストールしなくてもUnityでARKit3の新規のHuman Segmentaionなどが使えるようになりました。対応端末はiPhone X以降です。

実験としてこのようなアプリを作ってみました。

が、開発中、実機で確認するのがとても面倒でした。ARKitはiPhone搭載のカメラのセンサを使うため、実機じゃないと動きません。毎回Unityからビルド→Xcodeプロジェクトに書き出し→実機で確認。のステップを踏まなくてはいけません。毎回実機確認に5分くらいかかります。ARKit2まではARKit Remote for Unityで、Unity Editorとつなげてデバックができました。とところが、ARKit3の機能に対応しら新しいUnityライブラリである、AR Foundation (動作時点ではver 3.0.0-preview.3)ではリモートデバックができないようです。公式スレッドによれば開発中だけど難航しているようです。

なにかを作るときにはツールから作るのをモットーにしているので、Unity Editorにリモートで送信できるツールを作ってみました。現在はHuman Segmentation関係の機能しか用意していませんが、普通に動きそうです。

ソースコードはこちら。

github.com

仕組みはシンプルで、iPhoneからARに必要な情報を送ります。

  • NDI
    • RGBカメラ(YCbCr色空間) 映像
    • Depth 映像
    • Human Segmentation 映像
  • WebSocket
    • カメラmatrixとかの情報

NDIはWi-Fi経由だと遅いのですが、USB経由で接続すると0.3秒くらいの遅延で収まりました。

Hack C#でprivateプロパティだけなstructを作る

シミュレータを作るときに困った点を一つ。ARFoundationはコア部分はネイティブC++プラグイン。そちらで生成したstructをやり取りしているので、C#から見えるstructはプロパティが全部プライベートでした。

[StructLayout(LayoutKind.Sequential)]
public struct XRCameraFrame
{
    public long timestampNs
    {
        get { return m_TimestampNs; }
    }
    long m_TimestampNs;

    public Matrix4x4 projectionMatrix
    {
        get { return m_ProjectionMatrix; }
    }
    Matrix4x4 m_ProjectionMatrix;
    
    ...
}

↑このようなクラス構造。Unityでもunsafe{}を使えばこのようなstructでも作れますが、ライブラリ化を考えるとなるべくunsafe以外の方法でstructを作ったほうが良いため、以下の様な共用体をつくることで解決しました。c#で共用体って作れるんですね…。

[StructLayout(LayoutKind.Sequential)]
public struct MyCameraFrame
{
    public long timestampNs;
    public Matrix4x4 projectionMatrix;
    ...
}

[StructLayout(LayoutKind.Explicit)]
public struct CameraFrameUnion
{
    [FieldOffset(0)] public MyCameraFrame a;
    [FieldOffset(0)] public XRCameraFrame b;
}

void main()
{
    var union = new CameraFrameUnion()
    {
         a = new MyCameraFrame()
         {
               // これはいじれる!
               timestampeNS = 1000,
         }
    }
    
    // 共用体なので、aを作るとbもできる!
    XRCameraFrame b = union.b;
}