sai10の雑記

センサー類, Python, Unity(C#), 機械学習とかについてのメモ

HoloLens2でWindows.AI.MachineLearningを使って手の動作認識を行う

はじめに

昨年7月にHoloLensMeetupで発表した手の動作認識では、機械学習のライブラリとしてOpenCVForUnityを用いていました。 画像処理の機能が使える反面、OpenCVForuUnityを使用する場合だとライセンス料がかかってしまう、という欠点があります。

そこで、HoloLens2でWindows.AI.MachineLearningを使って手の動作認識をしてみました。 UWP標準の機能を用いることで、特別にライセンス料などのコストをかけることがなく機械学習を用いることができます。

youtu.be

コードはGithubに上げてあります。 ポイントをいくつか挙げて解説していきます。

Github github.com

Windows.AI.MachineLearningとは?

https://docs.microsoft.com/ja-jp/windows/ai/

Microsoft公式のWindowsアプリに機械学習を実装を行うことができます。UWPアプリにも対応しています。

動作認識モデル(onnxファイル)の準備

モデルファイルは下記を参考にしてください stopengin0012.hatenablog.com

HoloLens上で動作認識モデル(onnxファイル)をロードする

Capabilityの設定

アップしたコード上では、HoloLens実機のピクチャフォルダ以下を参照するようにしています。

        private string dirName = "HandGestureModel";
        private string fileName = "handgesture_v001.onnx";
        private async Task LoadModelAsync()
        {
#if WINDOWS_UWP
            var dir = await KnownFolders.PicturesLibrary.GetFolderAsync(dirName);
            var file = await dir.GetFileAsync(fileName);
            // モデルのロード
            handGestureModelGen = await HandGestureModel.CreateFromStreamAsync(file as IRandomAccessStreamReference);
            if (handGestureModelGen == null)
            {
                Debug.LogError("モデルデータの読み込みに失敗しました. ");
            }
#endif
        }

したがって、下記図に示すように、ビルド時にCapabilityを設定してあげる必要があります。

f:id:stopengin0012:20210210031731p:plain

.onnxファイルを有効にする

また、(結構忘れそうな事項として)onnx拡張子のファイルへの参照を有効にします。 下記の図のように、サポートされるファイルの種類に「.onnx」を指定してください。

f:id:stopengin0012:20210210031839p:plain

ロードしたモデルを利用する

基本的には、先日の記事で実行確認した時と同じです。 入力を合わせることに注意してください。

... (using参照)
#if WINDOWS_UWP
using Windows.AI.MachineLearning;
using Windows.Storage;
using Windows.Storage.Streams;
#endif
...

        private async Task<string> RecognizeGesture()
        {
#if WINDOWS_UWP
            ...

            float[] data = ReadToFloatArray(handJointController.handGestureData);
            handGestureInput.Input120 = TensorFloat.CreateFromArray(new long[] { 1, columns * frameCount }, data);

            //Evaluate the model
            var handGestureOutput = await handGestureModelGen.EvaluateAsync(handGestureInput);
            IList<string> stringList = handGestureOutput.label.GetAsVectorView().ToList();
            //Debug.Log($"判定された動作ラベル:{stringList[0]}"); //IL2CPP 確認用
            var label = stringList[0];
            return label;
#endif
        }

おわりに

HoloLens2でWindows.AI.MachineLearningを使って手の動作認識を実装しました。 その上で気をつけるポイントをいくつかまとめました。

追記

昨年7月のスライドです

www.slideshare.net

sklearnで作成したRansomForestモデルをONNXに変換してwinML(UWP)で利用する

はじめに

sklearnで作成したRansomForestモデルをONNXファイルに変換し、Windows Machine Learningで利用する際に気を付けることを書いていきます。

サンプルに、HoloLens2取得したHandGestureを分類するコードをGithubに挙げました。 github.com

環境(python)

  • python 3.6 (winmltoolsがpython3.7以降をサポートしていないため)
  • winmltools 1.5.1
  • skl2onnx 1.7.1 (whlファイルからインポートしてください)
  • その他 (pandas, sklearnなど)

環境(uwp)

1. sklearnで学習 -> ONNXファイルに変換する時

1-1. zipmapの削除

Windows Machine LearningのUWPアプリでONNXモデルを利用する際、どうやらモデルに型不明な経路(?)があると、ロードに失敗します。 f:id:stopengin0012:20210207141018p:plain

これを防ぐために、convert_sklearn関数でinitial_typesやfinal_typesを指定します。 ここでfinal_types指定の際、zipmap出力の型(map(string,tensor(float))の指定方法がわからず、方法を模索していたところ、zipmap変換をしない方法にたどり着きました。

options = {id(pipeline): {'zipmap': False}}

上記のoptionsをconvert_sklearnの引数に指定します。

from skl2onnx import convert_sklearn
from winmltools.convert.common.data_types import FloatTensorType, StringTensorType, Int64TensorType
options = {id(pipeline): {'zipmap': False}}
rf_onnx = convert_sklearn(pipeline,
                            target_opset=7,
                            options=options,
                            name = 'RandomForestClassifier',
                            initial_types=[('input', FloatTensorType([1, 120]))],
                            )

出力されたONNXモデルは以下のようになります。 f:id:stopengin0012:20210207141445p:plain

1-2. convert_sklearnはskl2onnxのものを使う

ONNXのバージョンに気をつける必要はありますが、とりあえずwinmltoolsのコンバータを使用せず、最新のskl2onnxを使うことができました。

from skl2onnx import convert_sklearn

2. Windows Machine Learning(UWP)で利用する時

2-1. 入力・出力をONNXと一致させる

当然といえば当然ですが、入力・出力の型を合わせる必要があります。skleanrからのコンバート時にinitial_typesを指定するなども、その一環として行っています。

...
# データ入力時
handGestureInput input = new handGestureInput();
input.Input120 = TensorFloat.CreateFromArray(new long[] { 1 , 120 }, feature);
...

public sealed class handGestureInput
{
    public TensorFloat Input120;
}

2-2. TensorStringの出力の仕方

TensorString型の取り出しには、一癖あるなぁと感じます。 取り出し方としては、TensorString.GetAsVectorView().ToList()で、string型のListに変換し、その要素を取り出します。

//Evaluate the model
var handGestureOutput = await handGestureModelGen.EvaluateAsync(input);
IList<string> stringList = handGestureOutput.label.GetAsVectorView().ToList();
Debug.WriteLine($"判定された動作ラベル:{stringList[0]}");

終わりに

sklearnで作成したRansomForestモデルをONNXファイルに変換し、Windows Machine Learningで利用する際に気を付けることを書きました。 (HoloLens2上で実行できたらいいな....)

HoloLens 2のResearchModeのStreamRecorderを試す

HoloLens 2のResearchMode とは?

簡単にいうと、HoloLens2に内蔵されているセンサー類のストリームデータを可視化・保存できるモードのことです。 もちろん、研究用途のためなので、動作保証とかないです。(なので、お仕事で使う場合は気をつけてください) また、Preview版なので、変更される可能性が高いです。 詳しくは、下記ドキュメントをご覧になってください。 docs.microsoft.com

HoloLens 2のOSをInsiderPreviewにする

ドキュメント通りやれば大丈夫なはず。 OSのversionが19041.1356 以降となってれば使えるはずです。 また、デバイスポータルでReserchmodeを有効にすることを忘れずに。

ResearchModeのサンプルコードを試す

公式にサンプルコードが公開されています. https://github.com/microsoft/HoloLens2ForCV

本記事では、Samples/StreamRecorder以下を参照します。 ここには、記録するためのプロジェクト(C++)と、保存したデータをロードするコード類(Python)が入っています - StreamRecorderApp : 記録するためのプロジェクト(C++) - StreamRecorderConverter : 保存したデータをロードするコード類(Python)

1. StreamRecorderを試す

まず、ソリューションファイルStreamRecorderApp/StreamRecorder.sln を開きます。

1-1. StreamRecorderのコードを変更する

(20200912時点)現在のコードだと、RGBカメラの情報やEyeTrackingの情報を記録するモードになっていません。 これを有効にするために、以下の手順を踏んでください。

AppMain.cppファイルのコードを少し変えます。 //Set streams to capture となっている部分に以下を追記(20200912時点)してください。

std::vector<ResearchModeSensorType> AppMain::kEnabledRMStreamTypes = { ResearchModeSensorType::DEPTH_LONG_THROW };
std::vector<StreamTypes> AppMain::kEnabledStreamTypes = { StreamTypes::PV , StreamTypes::EYE };

1-2. 実行

Debugビルド、ARM64で実行すると、Start・Stopボタンが表示されます。 Start押し、適当な時間の後Stopを押して記録が完了するのを待ちます。 記録が完了すると、以下の図ようにHandの位置などが可視化された状態になります。

f:id:stopengin0012:20200911175840j:plain

2.StreamRecorderConverterを試す

2-1.Anaconda3, opencv-python. open3dのインストール

仮想環境を作成し、python3, numpy, opencv-python. open3dをインストールしてください。 この辺は省略します。

2-2.実機から保存したデータをタウンロードする

  • <output_folder> : 出力先のフォルダを指定してください
  • user : デバイスポータルに入る際に入力するuserを指定してください
  • password : デバイスポータルに入る際に入力するpasswordを指定してください
python StreamRecorderConverter/recorder_console.py --workspace_path <output_folder>  --dev_portal_username <user> --dev_portal_password <password>

すると、下記のようなコンソール画面になります。download_allを入力してEnterを押すと、実機からダウンロードされます。

f:id:stopengin0012:20200912104924p:plain

データには、HandやGazeのデータがCSV形式になっていたり、またCameraパラメータなどもあるようです。

2-3.データを読み込むコードを動かす

下記コマンドで、ダウンロードしたデータを、いくつかのスクリプトで読み込める形にできます。 - <path_to_capture_folder> : 実機からダウンロードしたフォルダのパスを指定してください("2020-09-11-175712"のように日付の形式になっているフォルダです)

python process_all.py --recording_path <path_to_capture_folder>

色付きPointCloudの生成

python convert_images.py --recording_path <path_to_capture_folder>

f:id:stopengin0012:20200912103754p:plain

Hand・Eye追跡画像の生成

python project_hand_eye_to_pv.py --recording_path <path_to_capture_folder>

青がGaze, 緑がHandかな??

f:id:stopengin0012:20200912104350p:plain

空間Meshの生成

  • <path_to_pinhole_projected_camera> : <path_to_capture_folder>以下にある、pinhole_projectionフォルダを指定してください
python tsdf-integration.py --pinhole_path <path_to_pinhole_projected_camera>

tsdf-mesh.plyが作成されます.

f:id:stopengin0012:20200912104206p:plain

Vuforia知見共有会(社内)で発表しました

あけましておめでとうございます。 去年の話になりますが、2018年12月28日、社内でのVuforia知見共有会にて、発表をしました。 (あくまでも知見の共有のため、Vuforiaとは何か、ExtendTrackingとは何か、について省いておきます。) 以下がその時のスライドになります。

実演中、手順の中で「なぜここはこうしているの?」「私だったらこうするんだけどな」というような質問や意見が飛び交い、 活発に議論をしていました。(研究室のゼミのような雰囲気でしたw)

スライドにあること以外でも、例えば、以下のようなことについて議論しました。

  • ①マーカーにQRをなぜ使うのか
  • ②マーカーを拡大して印刷した場合、どこの値を変更すればよいか
  • ③マーカー設定の際のwidth入力について(ARコンテンツを作成する際の手順を踏まえながら)

①マーカーにQRをなぜ使うのか

本来、マーカーに用いる画像の選定などはVuforia公式のガイドラインに沿うのが良いと思います。

ただ、QRコードをしばしば用いたりする理由としては、

  • お客様からの指定があった(情報技術感が出ているから等)
  • 白黒なので、環境光による色の変化の影響を受けにくい

といった理由が挙げられました。もちろん、ロゴなどの画像を用いる場合、よりコンテンツとして現実を意識したものになるため、QRコードが必ずしもコンテンツにとって最適解というわけではありません。

②マーカーを拡大して印刷した場合の変更点

作業を進めるなか、マーカーを大きめにして印刷した場合どうなるか?といった検証を行いたいことがあるようです(Unity内)。 この場合、inspectorにて、ImageTargetのScaleやImageTarget BehaviorのAdvancedオプションの値を変更すればよい。ただ、これはあくまでサッと検証したい場合での応急措置であり、製作する場合はきちんとマーカーの設定からやり直したほうがよいという結論に至りました。

③マーカー設定の際のwidth入力について

使いたいマーカーの画像サイズは、縦横比が1:1になっていない場合があります。この場合、VuforiaサイトのTargetManagerにて画像サイズを登録する場合、widthには何の値を入力すれば良いか、ということが議論になりました。

回答としては、縦長であろうと横長であろうと、登録する画像の横幅を入力すれば良い。。。のですが、 ARコンテンツを作成する際の手順を踏まえながら考えるのがより良いと考えます。

というのは、コンテンツを作成する際、ユーザーはどこから入ってきて、どのようにマーカーを見て、どのようにオブジェクトやエフェクトを表示するか、という演出を事前に考えるはずです。そして、「登録する画像の横幅」というのは、ユーザーがマーカーを見るときの向きを考慮したうえで、画像をを作成・登録し、その画像を印刷した時の横幅を入力するのが良いのではないかと考えます。

まとめ

VuforiaをUnityで使用する際、いくつかの手順を踏んでコンテンツを作ることになります。今回、社内での知見共有会にて議論した結果、新たに気をつけなくてはいけないポイントや、人によって手順や考えが異なる点(小さい違いではあるものの)などがあることが分かりました。そういった点を社内で共有することで、同じ技術を同じ期待値で製作することができるのではないか、とも考えられます。今後も、Vuforiaだけでなく、他の技術についても共有していきたいです。

Hololens RS5で akihiroさん(@akihiro01051)のwindowsML画像認識デモを動かす

はじめに

akihiroさん(@akihiro01051)のHololens上でのwindowsML画像認識デモを
RS5で動かす時のメモです。 akihiro-document.azurewebsites.net

RS5への変更に伴い、windows MLの名前空間は以下のようにPreviewが除かれました。
Windows.AI.MachineLearning.PreviewWindows.AI.MachineLearning
そして、コード面でもいくつか変更点があったようです。

実行環境

- Windows 10 Insider Preview 17763.1
- Windows 10 SDK Insider Preview 17763
- Visual Studio 2017
- Unity 2017.4.12f1
- Hololens RS5 preview 17720

事前準備

まず、以下のリンクのリポジトリからWindows-Machine-LearningのサンプルをCloneします。
https://github.com/Microsoft/Windows-Machine-Learning

また、Windows-Machine-Learning\Samples\SqueezeNetObjectDetection\UWP\cs\Assets 以下にある
- model.onnx
- Labels.json
の二つのファイルを確保しておいてください。

次に、本記事冒頭で述べたakihiroさんのプロジェクトをCloneします。
GitHub - akihiro0105/WindowsMLDemo_HoloLens_Unity: This Repository is WindowsML demo(ObjectDetection) with Unity in HoloLens

その後、確保しておいた二つのファイルを、Assets\StreamingAssets 以下に置いてください。
(Labels.jsonは上書きで構いません。また、元のSqueezeNet.onnxは削除して大丈夫です。)

そして、Unityからプロジェクトをビルドし、生成されたvisual studioプロジェクトの
WindowsML_Demo.csを変更していきます

コード変更

10行目 using参照を変更します。

//using Windows.AI.MachineLearning.Preview;  //削除
//追加
using Windows.AI.MachineLearning;  

28行目ファイル名を変更します。
SqueezeNet.onnx → model.onnx

67行目 Previewを外します。

//private LearningModelPreview _model = null;  //削除
//追加
private LearningModel _model = null;

68, 69行目 この辺がsessionに統一され、使いやすくなった感じはあります。

//private ImageVariableDescriptorPreview _inputImageDescription;  //削除
//private TensorVariableDescriptorPreview _outputTensorDescription;  //削除
//追加
private LearningModelSession _session = null;  

97行目 SqueezeNet.onnx → model.onnx

98行目Previewを外します。

//_model = await LearningModelPreview.LoadModelFromStorageFileAsync(modelFile);  //削除
//追加
_model = await LearningModel.LoadFromStorageFileAsync(modelFile);

100~103行目 sessionに統一され、コードもすっきり書くことができます。

//削除
//List<ILearningModelVariableDescriptorPreview> inputFeatures = _model.Description.InputFeatures.ToList();  
//List<ILearningModelVariableDescriptorPreview> outputFeatures = _model.Description.OutputFeatures.ToList();  
//_inputImageDescription = inputFeatures.FirstOrDefault(feature => feature.ModelFeatureKind == LearningModelFeatureKindPreview.Image) as ImageVariableDescriptorPreview;
//_outputTensorDescription = outputFeatures.FirstOrDefault(feature => feature.ModelFeatureKind == LearningModelFeatureKindPreview.Tensor) as TensorVariableDescriptorPreview;  
//追加
_session = new LearningModelSession(_model, new LearningModelDevice(LearningModelDeviceKind.Default));

146~148行目

// WindowsMLに入力,出力形式を設定する
//削除
//LearningModelBindingPreview binding = new LearningModelBindingPreview(_model as LearningModelPreview);
//binding.Bind(_inputImageDescription.Name, inputFrame);
//binding.Bind(_outputTensorDescription.Name, _outputVariableList);
//追加
LearningModelBinding binding = new LearningModelBinding(_session);
ImageFeatureValue imageTensor = ImageFeatureValue.CreateFromVideoFrame(inputFrame);
binding.Bind("data_0", imageTensor);

151~152行目

// Process the frame with the model
//削除
//LearningModelEvaluationResultPreview results = await _model.EvaluateAsync(binding, "test");
//List<float> resultProbabilities = results.Outputs[_outputTensorDescription.Name] as List<float>;
//追加
var results = await _session.EvaluateAsync(binding, "");
var resultTensor = results.Outputs["softmaxout_1"] as TensorFloat;
var resultVector = resultTensor.GetAsVectorView();

157, 164行目 resultProbabilities → resultVector

結果

結果としては以下のような動画になります。
画面に近づいた時には猫が、 ノートPCを写している時はノートPCとして認識されているのがわかるかと思います。 最後にブルームしたときのUIは、RS5ならではのUIです。 youtu.be

また、猫の画像はこちらを使用させていただきました。

free2photo2.blogspot.com

まとめ

・Hololens RS5でakihiroさん(@akihiro01051)のwindowsML画像認識デモを動かした
・その際の変更点などを示した

Hololensのカメラ画像をOpenCVforUnityで加工して透過表示するメモ

はじめに

OpenCVforUnityのサンプルを利用します。
(ライセンス関係に抵触する可能性があるため、ソースコードを用いた説明は省きます。)
(また、ある程度OpenCVを知っていることを前提とします。Mat形式とか。)
具体的には、Hololensのカメラ画像をOpenCVforUnityで加工して透過表示するサンプルとして、
OpenCVforUnityのHoloLensComicFilterExample.csを編集していきます。
まずは、以下の動画をご覧ください。


Comic Filter Sample of OpenCVforUnity with HoloLens

少しわかりづらいかもしれませんが、
視線のある一定範囲に漫画の線が重なるような表示が透過されているのが確認できます。
このように、Hololensのカメラ映像をOpenCVforUnityで加工して、透過表示することができます。

カメラ画像の特徴量を透過表示

次に、HoloLensComicFilterExample.csを編集し、
カメラ画像の特徴量を表示するように変更します。以下の動画をご覧ください。


ORB Feature Visualization Test of OpenCVforUnity with HoloLens

コミックフィルターをかける代わりに、
ORB特徴量として複数の小さい円が表示されているのが確認できます。
(右の画像は、静止画でORB特徴を算出した例です。)
これは、HoloLensComicFilterExample.csのOnFrameMatAcquired関数を変更しています。

OnFrameMatAcquired関数での流れメモ

流れを説明する前に、ちょっとポイントがあります。 これらのサンプルでは、実際のカメラの範囲より少し狭めになるよう、表示する画像を変換しています。 (変換部分をわかりやすくするため、処理を軽くするため等が考えられます。)

そのとき、カメラのサイズそのままの画像加工用のdstMat、
少し狭めの表示領域となるdstMatClippingROIがあります。
確認が取れていないので、推測になりますが....
dstMatClippingROIは、その表示領域部分のみdstMatのメモリを参照しています。(のはず)
つまり、dstMatClippingROIのみ加工し、dstMatごと、テクスチャに貼るMatへコピーするという流れになります。
(ちょっと自信ない)


そして、以下がOnFrameMatAcquired関数での流れとなります。
① Hololensカメラ画像 → 変数bgraMat (BGRA32形式のMatデータ)として受け取る
② 変数bgraMatを表示サイズへ変換
(Mat bgraMatClipROI = new Mat(bgraMat, processingAreaRect);)
③ bgraMatClipROI → dstMatClippingROI (この時にしたい処理をする。今回は特徴量表示。)
④ dstMat → bgraMatへコピー

まとめ

  • OpenCVforUnityのHoloLensComicFilterExampleを紹介しました
  • HoloLensComicFilterExample.csを特徴量表示するよう変更しました
  • HoloLensComicFilterExample.csの流れのメモを記述しておきました

HololensでBackgroundでメディアを再生する様子と、二種類のプロセスについて(UWP)

はじめに

UWPには、アプリをBackgroundで実行するためのバックグラウンドタスクが用意されています。 まずは、以下の動画をご覧ください。BackgroundでMusicを再生しながら、Reseach Modeを呼んでいる様子です。 (ソースコードMicrosoftWindows-universal-samplesを利用しています。 https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/BackgroundMediaPlayback

(また、ResearchModeのアプリは@akihiroさんのブログ(http://akihiro-document.azurewebsites.net/post/hololens_researchmode2/)の UWP(C#)版を利用しています。)

www.youtube.com

バックグラウンドタスクの二種類のプロセス

さて、ここでバックグラウンドタスクにおける二種類のプロセスがあるそうです。 この二つのプロセスについて、まとめてみました。 (参考 : バックグラウンド タスクのガイドライン - UWP app developer | Microsoft Docs )

A. インプロセス
アプリとそのバックグラウンド プロセスが同じプロセスで実行されるプロセス。
今回のBackgroundMediaPlayer Sampleはこちらのプロセスを利用しています。

[利点]
インプロセス バックグラウンド タスクでは、プロセス間通信が不要のため、記述内容は複雑になりません。

[欠点]
インプロセス バックグラウンド タスクでは、DeviceUseTrigger、DeviceServicingTrigger、IoTStartupTask の各トリガーがサポートされていません。 (→センサ系のトリガーが使えない) また、アプリケーション内での VoIP バックグラウンド タスクのアクティブ化がサポートされていません。(→VoIPはアウトプロセス)

[特徴]
Application オブジェクトから EnteredBackground と LeavingBackground という 2 つの新しいイベントを使用できます。 EnteredBackground イベントは、アプリがバックグラウンドで実行されている間に処理されるコードを実行します。また、LeavingBackground イベントは、アプリがフォアグラウンドに移動したことを知るために処理します。 (シンプルに実装できる。)

B. アウトプロセス

[利点]
アウトプロセスタスクでのコードがクラッシュしても、フォアグラウンドでのコードはクラッシュしません。また、 DeviceUseTrigger、DeviceServicingTrigger、IoTStartupTask の各トリガーを使用できます。

[欠点]
プロセス間通信を利用するため、記述内容が複雑になりがちです

[特徴(というより、書くべき処理)]
- IBackgroundTask インターフェイスを実装するクラスを作ると、コードをバックグラウンドで実行できます。 このコードは、SystemTrigger や MaintenanceTrigger などを使って特定のイベントをトリガーすると実行されます。 - 実行するバックグラウンド タスクを登録します
- イベント ハンドラーでバックグラウンド タスクの完了を処理します
- アプリがバックグラウンド タスクを使うことをアプリ マニフェストで宣言します
(工程が多く複雑(?)。)

やりたいこと

- カメラセンサ情報 + Networkをbackgroundで実行、サーバー側にセンサ情報を投げ続ける。 この場合、アウトプロセスを利用する必要があると考えます。

まとめ

アプリをBackgroundで実行する様子と、そのために用意されたバックグラウンドタスクにおける二つのプロセスについてまとめました。 今回は公式ドキュメントの中で用意された二つのプロセスについて着目しましたが、実装面には触れていません。 Backgroundで何を処理させたいかで、どちらのプロセスを選択すべきかが変わる、といったところでしょうか。 今後は、Hololensのカメラセンサ等をBackgroundで処理していこうと思います。