HoloLens (第 1 世代) と Azure 302b: Custom Vision
Note
Mixed Reality Academy のチュートリアルは、HoloLens (第 1 世代) と Mixed Reality イマーシブ ヘッドセットを念頭に置いて編成されています。 そのため、それらのデバイスの開発に関するガイダンスを引き続き探している開発者のために、これらのチュートリアルをそのまま残しておくことが重要だと考えています。 これらのチュートリアルが、HoloLens 2 に使用されている最新のツールセットや操作に更新されることは "ありません"。 これらは、サポートされているデバイス上で継続して動作するように、保守されます。 今後、HoloLens 2 の開発方法を説明する新しいチュートリアルが公開される予定です。 この通知は、それらのチュートリアルが投稿されたときにリンクと共に更新されます。
このコースでは、Mixed Reality アプリケーションの Azure Custom Vision 機能を使用して、提供された画像内のカスタム ビジュアル コンテンツを認識する方法について説明します。
このサービスでは、オブジェクトの画像を使用して機械学習モデルをトレーニングできます。 その後、トレーニング済みのモデルを使用して、Microsoft HoloLens のカメラ キャプチャまたは PC に接続されたイマーシブ (VR) ヘッドセット用のカメラによって提供される類似のオブジェクトを認識します。
Azure Custom Vision は Microsoft コグニティブ サービスの 1 つで、開発者はこれを使用してカスタム画像分類器を構築できます。 その後、これらの分類器を新しい画像と共に使用して、その新しい画像内のオブジェクトを認識または分類できます。 このプロセスを効率化するため、このサービスにはシンプルで使いやすいオンライン ポータルが用意されています。 詳細については、Azure Custom Vision サービスのページをご覧ください。
このコースを完了すると、次の 2 つのモードで動作する Mixed Reality アプリケーションが完成します。
分析モード: 画像をアップロードし、タグを作成し、さまざまなオブジェクト (この場合はマウスとキーボード) を認識するようにサービスをトレーニングすることで、Custom Vision サービスを手動で設定します。 次に、カメラを使用して画像をキャプチャする HoloLens アプリを作成し、現実世界にあるそれらのオブジェクトを認識してみます。
トレーニング モード: "トレーニング モード" を有効にするコードをアプリに実装します。 トレーニング モードでは、HoloLens のカメラを使用して画像をキャプチャし、キャプチャした画像をサービスにアップロードして、Custom Vision モデルをトレーニングできます。
このコースでは、Custom Vision サービスから結果を取得して、Unity ベースのサンプル アプリケーションに取り込む方法について説明します。 作成中のカスタム アプリケーションがある場合に、これらの概念をそのアプリケーションで採用するかどうかはご自身でご判断ください。
デバイス サポート
コース | HoloLens | イマーシブ ヘッドセット |
---|---|---|
MR と Azure 302b: カスタム ビジョン | ✔️ | ✔️ |
Note
このコースでは主に HoloLens に焦点を当てていますが、このコースで学習した内容を Windows Mixed Reality イマーシブ (VR) ヘッドセットにも適用できます。 イマーシブ (VR) ヘッドセットにはアクセス可能なカメラが搭載されていないため、外部カメラが PC に接続されている必要があります。 このコースの中では、イマーシブ (VR) ヘッドセットをサポートするために必要な変更がある場合に、その変更が注意事項として記載されています。
前提条件
Note
このチュートリアルは、Unity と C# の基本的な使用経験がある開発者を対象としています。 また、このドキュメント内の前提条件や文章による説明は、執筆時 (2018 年 7 月) にテストおよび検証された内容であることをご了承ください。 ツールのインストールの記事に記載されているように、お客様は最新のソフトウェアを自由に使用できます。ただし、このコースの情報は、以下に記載されているものよりも新しいソフトウェアでの設定や結果と完全に一致するとは限りません。
このコースでは、次のハードウェアとソフトウェアをお勧めします。
- イマーシブ (VR) ヘッドセットの開発に必要な Windows Mixed Reality と互換性のある開発用 PC
- 開発者モードが有効になっている Windows 10 Fall Creators Update (またはそれ以降)
- 最新の Windows 10 SDK
- Unity 2017.4
- Visual Studio 2017
- 開発者モードが有効になっている Windows Mixed Reality イマーシブ (VR) ヘッドセットまたは Microsoft HoloLens
- 開発者の PC に接続されているカメラ (イマーシブ ヘッドセット開発の場合)
- Azure のセットアップと Custom Vision API の取得のためのインターネット アクセス
- Custom Vision サービスで認識するオブジェクトごとに、少なくとも 5 個の画像 (10 個を推奨)。 必要な場合は、このコースで既に提供されている画像 (コンピューターのマウスとキーボード) を使用できます。
開始する前に
- このプロジェクトをビルドする際の問題を避けるために、このチュートリアルで紹介するプロジェクトをルートまたはルートに近いフォルダーに作成することを強くお勧めします (フォルダー パスが長いと、ビルド時に問題が発生する可能性があります)。
- HoloLens を設定してテストします。 HoloLens の設定でサポートが必要な場合は、HoloLens セットアップに関する記事にアクセスしてください。
- 新しい HoloLens アプリの開発を開始するときは、調整とセンサーのチューニングを実行することをお勧めします (ユーザーごとにこれらのタスクを実行すると役立つ場合があります)。
調整の詳細については、この HoloLens の調整に関する記事へのリンクを参照してください。
センサー チューニングの詳細については、この HoloLens センサー チューニングに関する記事へのリンクを参照してください。
第 1 章 - Custom Vision サービス ポータル
Azure でCustom Vision サービスを使用するには、アプリケーションで使用できるようにサービスのインスタンスを構成する必要があります。
Get Started ボタンをクリックします。
Custom Vision サービス ポータルにサインインします。
Note
まだ Azure アカウントをお持ちでない方は、作成する必要があります。 このチュートリアルを教室やラボで受講している場合は、インストラクターや監督者に新しいアカウントの設定方法を質問してください。
初めてログインすると、[サービス利用規約] パネルが表示されます。 利用規約に同意するチェック ボックスをオンにします。 次に、[同意する] をクリックします。
利用規約に同意すると、ポータルの [プロジェクト] セクションに自動的に移動します。 [新しいプロジェクト] をクリックします。
右側にタブが表示され、プロジェクトに関するいくつかのフィールドを指定するように求められます。
プロジェクトの [名前] を挿入します。
プロジェクトの [説明] を挿入します (省略可能)。
[リソース グループ] を選択するか、新規に作成します。 リソース グループは、Azure アセットのコレクションの監視、アクセス制御、プロビジョニング、課金管理を行う方法を提供します。 1 つのプロジェクト (たとえば、これらのコースなど) に関連するすべての Azure サービスを共通のリソース グループに保持することをお勧めします。
[プロジェクトの種類] を [分類] に設定します
[ドメイン] を [全般] に設定します。
Azure リソース グループの詳細については、リソース グループに関する記事を参照してください。
完了したら、[プロジェクトの作成] をクリックします。Custom Vision サービスのプロジェクト ページにリダイレクトされます。
第 2 章 - Custom Vision プロジェクトをトレーニングする
Custom Vision ポータルにアクセスする主な目的は、画像内の特定のオブジェクトを認識するようにプロジェクトをトレーニングすることです。 アプリケーションで認識するオブジェクトごとに、少なくとも 5 個の画像が必要ですが、10 個が推奨されます。 このコースで既に提供されている画像 (コンピューターのマウスとキーボード) を使用できます。
Custom Vision サービス プロジェクトをトレーニングするには:
[タグ] の横にある + ボタンをクリックします。
認識するオブジェクトの [名前] を追加します。 [Save] をクリックします。
タグが追加されていることがわかります (タグを表示するために、ページの再読み込みが必要になる場合があります)。 新しいタグの横にあるチェック ボックスをオンにします (まだオンにしていない場合)。
ページの中央にある [画像の追加] をクリックします。
[ローカル ファイルの参照] をクリックしてから、アップロードする画像を検索して選択します (少なくとも 5 個)。 これらの画像のすべてに、トレーニング対象のオブジェクトが含まれている必要があります。
Note
一度に複数の画像を選択してアップロードできます。
タブに画像が表示されたら、[マイ タグ] ボックスで適切なタグを選択します。
[ファイルのアップロード] をクリックします。 ファイルのアップロードが開始されます。 アップロードの確認が完了したら、[完了] をクリックします。
同じプロセスを繰り返して、「Keyboard」という名前の新しいタグを作成し、適切な写真をアップロードします。 新しいタグを作成したら、必ず uncheck Mouse を実行し、 イメージの追加 ウィンドウを表示します。
両方のタグを設定したら、[トレーニング] をクリックすると、最初のトレーニング イテレーションの構築が開始されます。
ビルドが完了すると、[既定値にする] と [予測 URL] の 2 つのボタンが表示されます。 最初に [既定値にする] をクリックし、次に [予測 URL] をクリックします。
Note
このエンドポイント URL は、既定としてマークされているイテレーションに設定されます。 そのため、後で新しいイテレーションを作成し、それを既定として更新する場合は、コードを変更する必要はありません。
[予測 URL] をクリックしたら、"メモ帳" を開き、後でコードで必要になったときに取得できるように、URL と Prediction-Key をコピーして貼り付けます。
画面の右上にある歯車アイコンをクリックします。
トレーニング キーをコピーし、後で使用できるようにメモ帳に貼り付けます。
また、プロジェクト ID もコピーして、後で使用できるように "メモ帳" ファイルに貼り付けます。
第 3 章 - Unity プロジェクトの設定
次の設定は、複合現実での開発のための一般的な設定であるため、他のプロジェクトのテンプレートとして利用できます。
Unity を開き、[新規] をクリックします。
次に Unity のプロジェクト名を指定する必要があります。 AzureCustomVision を挿入します。プロジェクト テンプレートが 3D に設定されていることを確認します。 [場所] を適切な場所に設定します (ルート ディレクトリに近い方が適しています)。 [Create project]\(プロジェクトの作成\) をクリックします。
Unity を開いた状態で、既定のスクリプト エディターが Visual Studio に設定されているかどうか確認することをお勧めします。 [編集] > [環境設定] に移動し、新しいウィンドウで [外部ツール] に移動します。 [外部スクリプト エディター] を [Visual Studio 2017] に変更します。 [環境設定] ウィンドウを閉じます。
次に、[ファイル] > [ビルド設定] で、[ユニバーサル Windows プラットフォーム] を選択してから、[プラットフォームの切り替え] ボタンをクリックして選択を適用します。
引き続き [File] (ファイル) > [Build Settings] (ビルド設定) で、次のことを確認します。
[ターゲット デバイス] が [HoloLens] に設定されている
イマーシブ ヘッドセットの場合は、[ターゲット デバイス] を [任意のデバイス] に設定します。
[Build Type] (ビルドの種類) が [D3D] に設定されている
[SDK] が [最新のインストール] に設定されている。
[Visual Studio Version] (Visual Studio のバージョン) が [Latest installed] (最新のインストール) に設定されている
[Build and Run] (ビルドと実行) が [Local Machine] (ローカル マシン) に設定されている
シーンを保存し、ビルドに追加します。
これを行うには、[Add Open Scenes] (開いているシーンを追加) を選択します。 保存ウィンドウが表示されます。
これと、今後のシーン用の新しいフォルダーを作成し、[新しいフォルダー] ボタンを選択して、新しいフォルダーを作成し、「Scenes」という名前を付けます。
新しく作成した Scenes フォルダーを開いてから、[ファイル名:] テキスト フィールドに「CustomVisionScene」と入力して [保存] をクリックします。
Unity のシーンは Unity プロジェクトに関連付けられている必要があるため、Assets フォルダーに保存する必要があることに注意してください。 Unity プロジェクトの構築では、通常、Scenes フォルダー (とその他の類似フォルダー) を作成します。
[ビルド設定] の残りの設定は、ここでは既定値のままにしておきます。
Build Settings (ビルド設定) ウィンドウで、[プレーヤー設定] ボタンをクリックすると、"インスペクター" が配置されているスペースに関連パネルが表示されます。
このパネルでは、いくつかの設定を確認する必要があります。
[Other Settings] (その他の設定) タブで、次の内容を確認します。
[スクリプト ランタイム バージョン] が [試験段階 (.NET 4.6 と同等)] である (この場合、エディターの再起動が必要になります)。
[スクリプト バックエンド] が [.NET] である。
[API Compatibility Level]\(API 互換性レベル\) が [.NET 4.6] である。
[Publishing Settings]\(公開設定\) タブ内の [Capabilities]\(機能\) で、次の内容を確認します。
InternetClient
Web カメラ
マイク
さらに、パネルの下にある [XR Settings]\(XR 設定\) ([Publish Settings]\(公開設定\) の下) で、[Virtual Reality Supported]\(Virtual Reality サポート\) をオンにし、Windows Mixed Reality SDK が追加されていることを確認します。
Build 設定 Unity C# プロジェクトはグレー表示されなくなりました。この横にあるチェックボックスをオンにします。
[ビルド設定] ウィンドウを閉じます。
シーンとプロジェクトを保存します ([ファイル] > [シーン/ファイルの保存] > [プロジェクトの保存])。
第 4 章 - Unity での Newtonsoft DLL のインポート
重要
このコースのUnity のセットアップコンポーネントをスキップして、そのままコードに進みたい場合は、この Azure-MR-302b.unitypackage をダウンロードして、それをカスタム パッケージとしてご自分のプロジェクトにインポートしてから、第 6 章から続けてください。
このコースでは、アセットに DLL として追加できる Newtonsoft ライブラリを使用する必要があります。 このライブラリを含むパッケージは、こちらのリンクからダウンロードできます。 Newtonsoft ライブラリをプロジェクトにインポートするには、このコースに付属している Unity パッケージを使用します。
Assets>ImportPackage>CustomPackage メニュー オプションを使用して、.unitypackage を Unity に追加します。
ポップアップ表示される [Unity パッケージのインポート] ボックスで、[プラグイン] およびその下にあるすべての項目が選択されていることを確認します。
[Import]\(インポート\) ボタンをクリックして、各項目をプロジェクトに追加します。
[プロジェクト] ビューの [プラグイン] の下にある Newtonsoft フォルダーに移動して、Newtonsoft.Json プラグインを選択します。
"Newtonsoft.Json プラグイン" を選択した状態で、[任意のプラットフォーム] がオフになっていることを確認し、[WSAPlayer] もオフになっていることを確認してから、[適用] をクリックします。 これは単に、ファイルが正しく構成されていることを確認するための手順です。
Note
これらのプラグインにマークを付けると、それらは Unity エディターでのみ使用されるように構成されます。 プロジェクトが Unity からエクスポートされた後に使用される別のセットが WSA フォルダー内にあります。
次に、Newtonsoft フォルダー内にある WSA フォルダーを開く必要があります。 先ほど構成したものと同じファイルのコピーがあるのが分かります。 そのファイルを選択して、インスペクターで次のことを確認します
- [Any Platform](任意のプラットフォーム) チェック ボックスがオフである
- のみ WSAPlayer は チェックされます
- [Dont process](処理しない) がオンである
第 5 章 - カメラのセットアップ
[階層] パネルで、Main Camera を選択します。
選択すると、[Main Camera] (メイン カメラ) のすべてのコンポーネントが [Inspector] (インスペクター) パネルに表示されます。
カメラオブジェクトの名前は「Main Camera」である必要があります (スペルにご注意ください)。
Main Camera の [Tag] (タグ) が MainCamera に設定されている必要があります (スペルに注意してください)。
[Transform]\(変換\) の [Position]\(位置\) が 0、0、0 に設定されていることを確認します。
[Clear Flags] (クリア フラグ) を [Solid Color] (単色) に設定します (イマーシブ ヘッドセットの場合は無視してください)。
カメラ コンポーネントの [背景色] を黒、アルファ 0 (16 進コード: #00000000) に設定します (イマーシブ ヘッドセットの場合は無視してください)。
第 6 章 - CustomVisionAnalyser クラスの作成
この時点で、コードを記述する準備ができました。
最初に、CustomVisionAnalyser クラスを作成します。
Note
次に示すコードでは、Custom Vision REST API を使用して Custom Vision サービスを呼び出します。 これを使用して、この API を実装して使用する方法を確認します (自力で同様の機能を実装する方法を理解するのに役立ちます)。 Microsoft では、サービスを呼び出すために使用できる Custom Vision サービス SDK を用意しています。 詳細については、Custom Vision サービス SDK に関する記事をご覧ください。
このクラスでは次のことを行います。
キャプチャされた最新の画像をバイト配列として読み込みます。
分析のために Azure Custom Vision サービス インスタンスにバイト配列を送信します。
応答を JSON 文字列として受け取ります。
応答を逆シリアル化し、結果として得られる予測を応答の表示方法を制御する SceneOrganiser クラスに渡します。
このクラスを作成するには、次の手順を実行します。
[プロジェクト] パネルにある Asset フォルダーを右クリックし、[作成]> [フォルダー] の順にクリックします。 フォルダーに「Scripts」という名前を付けます。
先ほど作成したフォルダーをダブルクリックして開きます。
フォルダー内を右クリックし、[作成]>[C# スクリプト] をクリックします。 スクリプトに「CustomVisionAnalyser」という名前を付けます。
新しい CustomVisionAnalyser スクリプトをダブルクリックして Visual Studio で開きます。
ファイルの一番上にある名前空間を次のように更新します。
using System.Collections; using System.IO; using UnityEngine; using UnityEngine.Networking; using Newtonsoft.Json;
CustomVisionAnalyser クラスに次の変数を追加します。
/// <summary> /// Unique instance of this class /// </summary> public static CustomVisionAnalyser Instance; /// <summary> /// Insert your Prediction Key here /// </summary> private string predictionKey = "- Insert your key here -"; /// <summary> /// Insert your prediction endpoint here /// </summary> private string predictionEndpoint = "Insert your prediction endpoint here"; /// <summary> /// Byte array of the image to submit for analysis /// </summary> [HideInInspector] public byte[] imageBytes;
Note
必ず predictionKey 変数に予測キーを挿入し、predictionEndpoint 変数に予測エンドポイントを挿入してください。 これらは、前の手順でメモ帳にコピーしたものです。
ここで、インスタンス変数を初期化する Awake() のコードを追加する必要があります。
/// <summary> /// Initialises this class /// </summary> private void Awake() { // Allows this instance to behave like a singleton Instance = this; }
Start() および Update() メソッドを削除します。
次に、ImageCapture クラスによってキャプチャされた画像の分析結果を取得するコルーチン (その下に静的な GetImageAsByteArray() メソッドを含む) を追加します。
Note
AnalyseImageCapture コルーチンには、まだ作成していない SceneOrganiser クラスへの呼び出しが含まれています。 そのため、ここではそれらの行をコメントのままにしておきます。
/// <summary> /// Call the Computer Vision Service to submit the image. /// </summary> public IEnumerator AnalyseLastImageCaptured(string imagePath) { WWWForm webForm = new WWWForm(); using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(predictionEndpoint, webForm)) { // Gets a byte array out of the saved image imageBytes = GetImageAsByteArray(imagePath); unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream"); unityWebRequest.SetRequestHeader("Prediction-Key", predictionKey); // The upload handler will help uploading the byte array with the request unityWebRequest.uploadHandler = new UploadHandlerRaw(imageBytes); unityWebRequest.uploadHandler.contentType = "application/octet-stream"; // The download handler will help receiving the analysis from Azure unityWebRequest.downloadHandler = new DownloadHandlerBuffer(); // Send the request yield return unityWebRequest.SendWebRequest(); string jsonResponse = unityWebRequest.downloadHandler.text; // The response will be in JSON format, therefore it needs to be deserialized // The following lines refers to a class that you will build in later Chapters // Wait until then to uncomment these lines //AnalysisObject analysisObject = new AnalysisObject(); //analysisObject = JsonConvert.DeserializeObject<AnalysisObject>(jsonResponse); //SceneOrganiser.Instance.SetTagsToLastLabel(analysisObject); } } /// <summary> /// Returns the contents of the specified image file as a byte array. /// </summary> static byte[] GetImageAsByteArray(string imageFilePath) { FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read); BinaryReader binaryReader = new BinaryReader(fileStream); return binaryReader.ReadBytes((int)fileStream.Length); }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 7 章 - CustomVisionObjects クラスの作成
ここでは、CustomVisionObjects クラスを作成します。
このスクリプトには、Custom Vision サービスに対して行われた呼び出しをシリアル化および逆シリアル化するために他のクラスで使用されるいくつかのオブジェクトが含まれています。
警告
以下の JSON 構造は Custom Vision Prediction v2.0 で動作するように設定されているため、Custom Vision サービスによって提供されるエンドポイントをメモすることが重要です。 別のバージョンを使用する場合は、以下の構造を更新する必要があります。
このクラスを作成するには、次の手順を実行します。
Scripts フォルダー内で右クリックしてから、[作成] > [C# スクリプト] の順にクリックします。 スクリプトに「CustomVisionObjects」という名前を付けます。
新しい CustomVisionObjects スクリプトをダブルクリックして Visual Studio で開きます。
次の名前空間を ファイルの先頭に追加します。
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking;
CustomVisionObjects クラス内の Start() および Update() メソッドを削除します。これで、このクラスは空になります。
CustomVisionObjects クラスの外部に次のクラスを追加します。 これらのオブジェクトは、Newtonsoft ライブラリで応答データをシリアル化および逆シリアル化するために使用されます。
// The objects contained in this script represent the deserialized version // of the objects used by this application /// <summary> /// Web request object for image data /// </summary> class MultipartObject : IMultipartFormSection { public string sectionName { get; set; } public byte[] sectionData { get; set; } public string fileName { get; set; } public string contentType { get; set; } } /// <summary> /// JSON of all Tags existing within the project /// contains the list of Tags /// </summary> public class Tags_RootObject { public List<TagOfProject> Tags { get; set; } public int TotalTaggedImages { get; set; } public int TotalUntaggedImages { get; set; } } public class TagOfProject { public string Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int ImageCount { get; set; } } /// <summary> /// JSON of Tag to associate to an image /// Contains a list of hosting the tags, /// since multiple tags can be associated with one image /// </summary> public class Tag_RootObject { public List<Tag> Tags { get; set; } } public class Tag { public string ImageId { get; set; } public string TagId { get; set; } } /// <summary> /// JSON of Images submitted /// Contains objects that host detailed information about one or more images /// </summary> public class ImageRootObject { public bool IsBatchSuccessful { get; set; } public List<SubmittedImage> Images { get; set; } } public class SubmittedImage { public string SourceUrl { get; set; } public string Status { get; set; } public ImageObject Image { get; set; } } public class ImageObject { public string Id { get; set; } public DateTime Created { get; set; } public int Width { get; set; } public int Height { get; set; } public string ImageUri { get; set; } public string ThumbnailUri { get; set; } } /// <summary> /// JSON of Service Iteration /// </summary> public class Iteration { public string Id { get; set; } public string Name { get; set; } public bool IsDefault { get; set; } public string Status { get; set; } public string Created { get; set; } public string LastModified { get; set; } public string TrainedAt { get; set; } public string ProjectId { get; set; } public bool Exportable { get; set; } public string DomainId { get; set; } } /// <summary> /// Predictions received by the Service after submitting an image for analysis /// </summary> [Serializable] public class AnalysisObject { public List<Prediction> Predictions { get; set; } } [Serializable] public class Prediction { public string TagName { get; set; } public double Probability { get; set; } }
第 8 章 - VoiceRecognizer クラスの作成
このクラスでは、ユーザーの音声入力を認識します。
このクラスを作成するには、次の手順を実行します。
Scripts フォルダー内で右クリックしてから、[作成] > [C# スクリプト] の順にクリックします。 VoiceRecognizer スクリプトを呼び出します。
新しい VoiceRecognizer スクリプトをダブルクリックして Visual Studio で開きます。
VoiceRecognizer クラスの上に次の名前空間を入力します。
using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Windows.Speech;
次に、VoiceRecognizer クラス内の Start() メソッドの上に次の変数を追加します。
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static VoiceRecognizer Instance; /// <summary> /// Recognizer class for voice recognition /// </summary> internal KeywordRecognizer keywordRecognizer; /// <summary> /// List of Keywords registered /// </summary> private Dictionary<string, Action> _keywords = new Dictionary<string, Action>();
Awake() および Start() メソッドを追加します。後者では、タグを画像に関連付けするときに認識されるユーザーのキーワードを設定します。
/// <summary> /// Called on initialization /// </summary> private void Awake() { Instance = this; } /// <summary> /// Runs at initialization right after Awake method /// </summary> void Start () { Array tagsArray = Enum.GetValues(typeof(CustomVisionTrainer.Tags)); foreach (object tagWord in tagsArray) { _keywords.Add(tagWord.ToString(), () => { // When a word is recognized, the following line will be called CustomVisionTrainer.Instance.VerifyTag(tagWord.ToString()); }); } _keywords.Add("Discard", () => { // When a word is recognized, the following line will be called // The user does not want to submit the image // therefore ignore and discard the process ImageCapture.Instance.ResetImageCapture(); keywordRecognizer.Stop(); }); //Create the keyword recognizer keywordRecognizer = new KeywordRecognizer(_keywords.Keys.ToArray()); // Register for the OnPhraseRecognized event keywordRecognizer.OnPhraseRecognized += KeywordRecognizer_OnPhraseRecognized; }
Update() メソッドを削除します。
次のハンドラーを追加します。これは、音声入力が認識されるたびに呼び出されます。
/// <summary> /// Handler called when a word is recognized /// </summary> private void KeywordRecognizer_OnPhraseRecognized(PhraseRecognizedEventArgs args) { Action keywordAction; // if the keyword recognized is in our dictionary, call that Action. if (_keywords.TryGetValue(args.text, out keywordAction)) { keywordAction.Invoke(); } }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
Note
エラーを含む可能性があるコードについては、すぐにこれらを修正する追加のクラスを作成するので、気にしないでください。
第 9 章 - CustomVisionTrainer クラスの作成
このクラスでは、Custom Vision サービスをトレーニングする一連の Web 呼び出しをチェーンします。 各呼び出しについては、コードの直前で詳しく説明します。
このクラスを作成するには、次の手順を実行します。
Scripts フォルダー内で右クリックしてから、[作成] > [C# スクリプト] の順にクリックします。 CustomVisionTrainer スクリプトを呼び出します。
新しい CustomVisionTrainer スクリプトをダブルクリックして Visual Studio で開きます。
CustomVisionTrainer クラスの上に次の名前空間を入力します。
using Newtonsoft.Json; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; using UnityEngine.Networking;
次に、CustomVisionTrainer クラス内の Start() メソッドの上に次の変数を追加します。
Note
ここで使用するトレーニング URL は、Custom Vision Training 1.2 ドキュメント内で提供され、https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/{projectId}/ という構造になっています。
詳細については、Custom Vision Training v1.2 の API リファレンス をご覧ください。警告
(CustomVisionObjects クラス内で) 使用される JSON 構造は Custom Vision Training v1.2 で動作するように設定されているため、Custom Vision サービスのトレーニング モードで提供されるエンドポイントをメモすることが重要です。 別のバージョンを使用する場合は、オブジェクトの構造を更新する必要があります。
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static CustomVisionTrainer Instance; /// <summary> /// Custom Vision Service URL root /// </summary> private string url = "https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/"; /// <summary> /// Insert your prediction key here /// </summary> private string trainingKey = "- Insert your key here -"; /// <summary> /// Insert your Project Id here /// </summary> private string projectId = "- Insert your Project Id here -"; /// <summary> /// Byte array of the image to submit for analysis /// </summary> internal byte[] imageBytes; /// <summary> /// The Tags accepted /// </summary> internal enum Tags {Mouse, Keyboard} /// <summary> /// The UI displaying the training Chapters /// </summary> private TextMesh trainingUI_TextMesh;
重要
必ず前にメモしたサービス キー (トレーニング キー) の値とプロジェクト ID の値を追加してください。これらは、前の手順 (第 2 章の手順 10 以降) でポータルから収集した値です。
次の Start() および Awake() メソッドを追加します。 これらのメソッドは初期化時に呼び出され、UI を設定するための呼び出しを含んでいます。
/// <summary> /// Called on initialization /// </summary> private void Awake() { Instance = this; } /// <summary> /// Runs at initialization right after Awake method /// </summary> private void Start() { trainingUI_TextMesh = SceneOrganiser.Instance.CreateTrainingUI("TrainingUI", 0.04f, 0, 4, false); }
Update() メソッドを削除します。 このクラスには不要です。
RequestTagSelection() メソッドを追加します。 このメソッドは、画像をキャプチャしてデバイスに格納し、Custom Vision サービスに送信してトレーニングする準備ができたときに、初めて呼び出されます。 このメソッドでは、ユーザーがキャプチャされた画像をタグ付けするために使用できる一連のキーワードをトレーニング UI に表示します。 また、VoiceRecognizer クラスに対して、ユーザーの音声入力の聞き取りを開始するように通知します。
internal void RequestTagSelection() { trainingUI_TextMesh.gameObject.SetActive(true); trainingUI_TextMesh.text = $" \nUse voice command \nto choose between the following tags: \nMouse\nKeyboard \nor say Discard"; VoiceRecognizer.Instance.keywordRecognizer.Start(); }
VerifyTag() メソッドを追加します。 このメソッドでは、VoiceRecognizer クラスによって認識される音声入力を受け取り、その有効性を確認してから、トレーニング プロセスを開始します。
/// <summary> /// Verify voice input against stored tags. /// If positive, it will begin the Service training process. /// </summary> internal void VerifyTag(string spokenTag) { if (spokenTag == Tags.Mouse.ToString() || spokenTag == Tags.Keyboard.ToString()) { trainingUI_TextMesh.text = $"Tag chosen: {spokenTag}"; VoiceRecognizer.Instance.keywordRecognizer.Stop(); StartCoroutine(SubmitImageForTraining(ImageCapture.Instance.filePath, spokenTag)); } }
SubmitImageForTraining() メソッドを追加します。 このメソッドでは、Custom Vision サービスのトレーニング プロセスを開始します。 最初の手順として、有効性が確認されたユーザーからの音声入力に関連付けられているタグ ID をサービスから取得します。 その後、このタグ ID が画像と共にアップロードされます。
/// <summary> /// Call the Custom Vision Service to submit the image. /// </summary> public IEnumerator SubmitImageForTraining(string imagePath, string tag) { yield return new WaitForSeconds(2); trainingUI_TextMesh.text = $"Submitting Image \nwith tag: {tag} \nto Custom Vision Service"; string imageId = string.Empty; string tagId = string.Empty; // Retrieving the Tag Id relative to the voice input string getTagIdEndpoint = string.Format("{0}{1}/tags", url, projectId); using (UnityWebRequest www = UnityWebRequest.Get(getTagIdEndpoint)) { www.SetRequestHeader("Training-Key", trainingKey); www.downloadHandler = new DownloadHandlerBuffer(); yield return www.SendWebRequest(); string jsonResponse = www.downloadHandler.text; Tags_RootObject tagRootObject = JsonConvert.DeserializeObject<Tags_RootObject>(jsonResponse); foreach (TagOfProject tOP in tagRootObject.Tags) { if (tOP.Name == tag) { tagId = tOP.Id; } } } // Creating the image object to send for training List<IMultipartFormSection> multipartList = new List<IMultipartFormSection>(); MultipartObject multipartObject = new MultipartObject(); multipartObject.contentType = "application/octet-stream"; multipartObject.fileName = ""; multipartObject.sectionData = GetImageAsByteArray(imagePath); multipartList.Add(multipartObject); string createImageFromDataEndpoint = string.Format("{0}{1}/images?tagIds={2}", url, projectId, tagId); using (UnityWebRequest www = UnityWebRequest.Post(createImageFromDataEndpoint, multipartList)) { // Gets a byte array out of the saved image imageBytes = GetImageAsByteArray(imagePath); //unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream"); www.SetRequestHeader("Training-Key", trainingKey); // The upload handler will help uploading the byte array with the request www.uploadHandler = new UploadHandlerRaw(imageBytes); // The download handler will help receiving the analysis from Azure www.downloadHandler = new DownloadHandlerBuffer(); // Send the request yield return www.SendWebRequest(); string jsonResponse = www.downloadHandler.text; ImageRootObject m = JsonConvert.DeserializeObject<ImageRootObject>(jsonResponse); imageId = m.Images[0].Image.Id; } trainingUI_TextMesh.text = "Image uploaded"; StartCoroutine(TrainCustomVisionProject()); }
TrainCustomVisionProject() メソッドを追加します。 画像が送信され、タグ付けされると、このメソッドが呼び出されます。 これにより、サービスに送信された以前のすべての画像と直前にアップロードされた画像を使用してトレーニングされる新しいイテレーションが作成されます。 このメソッドでは、トレーニングが完了すると、新しく作成されたイテレーションを既定として設定するメソッドを呼び出します。これにより、分析に使用するエンドポイントが最新のトレーニングされたイテレーションになります。
/// <summary> /// Call the Custom Vision Service to train the Service. /// It will generate a new Iteration in the Service /// </summary> public IEnumerator TrainCustomVisionProject() { yield return new WaitForSeconds(2); trainingUI_TextMesh.text = "Training Custom Vision Service"; WWWForm webForm = new WWWForm(); string trainProjectEndpoint = string.Format("{0}{1}/train", url, projectId); using (UnityWebRequest www = UnityWebRequest.Post(trainProjectEndpoint, webForm)) { www.SetRequestHeader("Training-Key", trainingKey); www.downloadHandler = new DownloadHandlerBuffer(); yield return www.SendWebRequest(); string jsonResponse = www.downloadHandler.text; Debug.Log($"Training - JSON Response: {jsonResponse}"); // A new iteration that has just been created and trained Iteration iteration = new Iteration(); iteration = JsonConvert.DeserializeObject<Iteration>(jsonResponse); if (www.isDone) { trainingUI_TextMesh.text = "Custom Vision Trained"; // Since the Service has a limited number of iterations available, // we need to set the last trained iteration as default // and delete all the iterations you dont need anymore StartCoroutine(SetDefaultIteration(iteration)); } } }
SetDefaultIteration() メソッドを追加します。 このメソッドでは、以前に作成され、トレーニングされたイテレーションを既定として設定します。 完了したら、このメソッドでサービス内の既存のイテレーションを削除する必要があります。 このコースの執筆時点では、サービス内に同時に存在できるイテレーションは最大 10 個に制限されています。
/// <summary> /// Set the newly created iteration as Default /// </summary> private IEnumerator SetDefaultIteration(Iteration iteration) { yield return new WaitForSeconds(5); trainingUI_TextMesh.text = "Setting default iteration"; // Set the last trained iteration to default iteration.IsDefault = true; // Convert the iteration object as JSON string iterationAsJson = JsonConvert.SerializeObject(iteration); byte[] bytes = Encoding.UTF8.GetBytes(iterationAsJson); string setDefaultIterationEndpoint = string.Format("{0}{1}/iterations/{2}", url, projectId, iteration.Id); using (UnityWebRequest www = UnityWebRequest.Put(setDefaultIterationEndpoint, bytes)) { www.method = "PATCH"; www.SetRequestHeader("Training-Key", trainingKey); www.SetRequestHeader("Content-Type", "application/json"); www.downloadHandler = new DownloadHandlerBuffer(); yield return www.SendWebRequest(); string jsonResponse = www.downloadHandler.text; if (www.isDone) { trainingUI_TextMesh.text = "Default iteration is set \nDeleting Unused Iteration"; StartCoroutine(DeletePreviousIteration(iteration)); } } }
DeletePreviousIteration() メソッドを追加します。 このメソッドでは、以前の既定以外のイテレーションを見つけて削除します。
/// <summary> /// Delete the previous non-default iteration. /// </summary> public IEnumerator DeletePreviousIteration(Iteration iteration) { yield return new WaitForSeconds(5); trainingUI_TextMesh.text = "Deleting Unused \nIteration"; string iterationToDeleteId = string.Empty; string findAllIterationsEndpoint = string.Format("{0}{1}/iterations", url, projectId); using (UnityWebRequest www = UnityWebRequest.Get(findAllIterationsEndpoint)) { www.SetRequestHeader("Training-Key", trainingKey); www.downloadHandler = new DownloadHandlerBuffer(); yield return www.SendWebRequest(); string jsonResponse = www.downloadHandler.text; // The iteration that has just been trained List<Iteration> iterationsList = new List<Iteration>(); iterationsList = JsonConvert.DeserializeObject<List<Iteration>>(jsonResponse); foreach (Iteration i in iterationsList) { if (i.IsDefault != true) { Debug.Log($"Cleaning - Deleting iteration: {i.Name}, {i.Id}"); iterationToDeleteId = i.Id; break; } } } string deleteEndpoint = string.Format("{0}{1}/iterations/{2}", url, projectId, iterationToDeleteId); using (UnityWebRequest www2 = UnityWebRequest.Delete(deleteEndpoint)) { www2.SetRequestHeader("Training-Key", trainingKey); www2.downloadHandler = new DownloadHandlerBuffer(); yield return www2.SendWebRequest(); string jsonResponse = www2.downloadHandler.text; trainingUI_TextMesh.text = "Iteration Deleted"; yield return new WaitForSeconds(2); trainingUI_TextMesh.text = "Ready for next \ncapture"; yield return new WaitForSeconds(2); trainingUI_TextMesh.text = ""; ImageCapture.Instance.ResetImageCapture(); } }
このクラスに最後に追加するメソッドは、キャプチャした画像をバイト配列に変換するために Web 呼び出しで使用する GetImageAsByteArray() メソッドです。
/// <summary> /// Returns the contents of the specified image file as a byte array. /// </summary> static byte[] GetImageAsByteArray(string imageFilePath) { FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read); BinaryReader binaryReader = new BinaryReader(fileStream); return binaryReader.ReadBytes((int)fileStream.Length); }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
第 10 章 - SceneOrganiser クラスの作成
このクラスでは:
メイン カメラにアタッチする Cursor オブジェクトを作成します。
サービスによって現実世界のオブジェクトが認識されたときに表示される Label オブジェクトを作成します。
適切なコンポーネントをアタッチして Main Camera を設定します。
分析モードでは、実行時にメイン カメラの位置を基準として適切なワールド空間にラベルを生成し、Custom Vision サービスから受信したデータを表示します。
トレーニング モードでは、トレーニング プロセスのさまざまな段階を表示する UI を生成します。
このクラスを作成するには、次の手順を実行します。
Scripts フォルダー内で右クリックしてから、[作成] > [C# スクリプト] の順にクリックします。 スクリプトに「SceneOrganiser」という名前を付けます。
新しい SceneOrganiser スクリプトをダブルクリックして Visual Studio で開きます。
必要な名前空間は 1 つだけです。SceneOrganiser クラスの上にある他のものは削除します。
using UnityEngine;
次に、SceneOrganiser クラス内の Start() メソッドの上に次の変数を追加します。
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static SceneOrganiser Instance; /// <summary> /// The cursor object attached to the camera /// </summary> internal GameObject cursor; /// <summary> /// The label used to display the analysis on the objects in the real world /// </summary> internal GameObject label; /// <summary> /// Object providing the current status of the camera. /// </summary> internal TextMesh cameraStatusIndicator; /// <summary> /// Reference to the last label positioned /// </summary> internal Transform lastLabelPlaced; /// <summary> /// Reference to the last label positioned /// </summary> internal TextMesh lastLabelPlacedText; /// <summary> /// Current threshold accepted for displaying the label /// Reduce this value to display the recognition more often /// </summary> internal float probabilityThreshold = 0.5f;
Start() および Update() メソッドを削除します。
変数のすぐ下に、クラスを初期化してシーンを設定する Awake() メソッドを追加します。
/// <summary> /// Called on initialization /// </summary> private void Awake() { // Use this class instance as singleton Instance = this; // Add the ImageCapture class to this GameObject gameObject.AddComponent<ImageCapture>(); // Add the CustomVisionAnalyser class to this GameObject gameObject.AddComponent<CustomVisionAnalyser>(); // Add the CustomVisionTrainer class to this GameObject gameObject.AddComponent<CustomVisionTrainer>(); // Add the VoiceRecogniser class to this GameObject gameObject.AddComponent<VoiceRecognizer>(); // Add the CustomVisionObjects class to this GameObject gameObject.AddComponent<CustomVisionObjects>(); // Create the camera Cursor cursor = CreateCameraCursor(); // Load the label prefab as reference label = CreateLabel(); // Create the camera status indicator label, and place it above where predictions // and training UI will appear. cameraStatusIndicator = CreateTrainingUI("Status Indicator", 0.02f, 0.2f, 3, true); // Set camera status indicator to loading. SetCameraStatus("Loading"); }
次に、メイン カメラのカーソルを作成して配置する CreateCameraCursor() メソッドと分析ラベル オブジェクトを作成する CreateLabel() メソッドを追加します。
/// <summary> /// Spawns cursor for the Main Camera /// </summary> private GameObject CreateCameraCursor() { // Create a sphere as new cursor GameObject newCursor = GameObject.CreatePrimitive(PrimitiveType.Sphere); // Attach it to the camera newCursor.transform.parent = gameObject.transform; // Resize the new cursor newCursor.transform.localScale = new Vector3(0.02f, 0.02f, 0.02f); // Move it to the correct position newCursor.transform.localPosition = new Vector3(0, 0, 4); // Set the cursor color to red newCursor.GetComponent<Renderer>().material = new Material(Shader.Find("Diffuse")); newCursor.GetComponent<Renderer>().material.color = Color.green; return newCursor; } /// <summary> /// Create the analysis label object /// </summary> private GameObject CreateLabel() { // Create a sphere as new cursor GameObject newLabel = new GameObject(); // Resize the new cursor newLabel.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f); // Creating the text of the label TextMesh t = newLabel.AddComponent<TextMesh>(); t.anchor = TextAnchor.MiddleCenter; t.alignment = TextAlignment.Center; t.fontSize = 50; t.text = ""; return newLabel; }
カメラの状態を示すテキスト メッシュに送信されるメッセージを処理する SetCameraStatus() メソッドを追加します。
/// <summary> /// Set the camera status to a provided string. Will be coloured if it matches a keyword. /// </summary> /// <param name="statusText">Input string</param> public void SetCameraStatus(string statusText) { if (string.IsNullOrEmpty(statusText) == false) { string message = "white"; switch (statusText.ToLower()) { case "loading": message = "yellow"; break; case "ready": message = "green"; break; case "uploading image": message = "red"; break; case "looping capture": message = "yellow"; break; case "analysis": message = "red"; break; } cameraStatusIndicator.GetComponent<TextMesh>().text = $"Camera Status:\n<color={message}>{statusText}..</color>"; } }
PlaceAnalysisLabel() および SetTagsToLastLabel() メソッドを追加します。これらにより、Custom Vision サービスのデータが生成され、シーンに表示されます。
/// <summary> /// Instantiate a label in the appropriate location relative to the Main Camera. /// </summary> public void PlaceAnalysisLabel() { lastLabelPlaced = Instantiate(label.transform, cursor.transform.position, transform.rotation); lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>(); } /// <summary> /// Set the Tags as Text of the last label created. /// </summary> public void SetTagsToLastLabel(AnalysisObject analysisObject) { lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>(); if (analysisObject.Predictions != null) { foreach (Prediction p in analysisObject.Predictions) { if (p.Probability > 0.02) { lastLabelPlacedText.text += $"Detected: {p.TagName} {p.Probability.ToString("0.00 \n")}"; Debug.Log($"Detected: {p.TagName} {p.Probability.ToString("0.00 \n")}"); } } } }
最後に、CreateTrainingUI() メソッドを追加します。これにより、アプリケーションがトレーニングモードのときにトレーニング プロセスの複数のステージを表示する UI が生成されます。 このメソッドは、カメラの状態オブジェクトを作成するためにも利用されます。
/// <summary> /// Create a 3D Text Mesh in scene, with various parameters. /// </summary> /// <param name="name">name of object</param> /// <param name="scale">scale of object (i.e. 0.04f)</param> /// <param name="yPos">height above the cursor (i.e. 0.3f</param> /// <param name="zPos">distance from the camera</param> /// <param name="setActive">whether the text mesh should be visible when it has been created</param> /// <returns>Returns a 3D text mesh within the scene</returns> internal TextMesh CreateTrainingUI(string name, float scale, float yPos, float zPos, bool setActive) { GameObject display = new GameObject(name, typeof(TextMesh)); display.transform.parent = Camera.main.transform; display.transform.localPosition = new Vector3(0, yPos, zPos); display.SetActive(setActive); display.transform.localScale = new Vector3(scale, scale, scale); display.transform.rotation = new Quaternion(); TextMesh textMesh = display.GetComponent<TextMesh>(); textMesh.anchor = TextAnchor.MiddleCenter; textMesh.alignment = TextAlignment.Center; return textMesh; }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
重要
続行する前に、CustomVisionAnalyser クラスを開き、AnalyseLastImageCaptured() メソッド内で次の行の "コメントを解除" します。
AnalysisObject analysisObject = new AnalysisObject();
analysisObject = JsonConvert.DeserializeObject<AnalysisObject>(jsonResponse);
SceneOrganiser.Instance.SetTagsToLastLabel(analysisObject);
第 11 章 - ImageCapture クラスの作成
次に作成するクラスは ImageCapture クラスです。
このクラスでは次のことを行います。
HoloLens のカメラを使用して画像をキャプチャし、それを App フォルダーに格納します。
ユーザーの "タップ" ジェスチャを処理します。
アプリケーションが分析モードとトレーニングモードのどちらで動作するかを決定する Enum 値を管理します。
このクラスを作成するには、次の手順を実行します。
先ほど作成した Scripts フォルダーに移動します。
フォルダー内を右クリックし、[作成] > [C# スクリプト] の順にクリックします。 スクリプトに「ImageCapture」という名前を付けます。
新しい ImageCapture スクリプトをダブルクリックして Visual Studio で開きます。
ファイルの先頭にある名前空間を次のものに置き換えます。
using System; using System.IO; using System.Linq; using UnityEngine; using UnityEngine.XR.WSA.Input; using UnityEngine.XR.WSA.WebCam;
次に、ImageCapture クラス内の Start() メソッドの上に次の変数を追加します。
/// <summary> /// Allows this class to behave like a singleton /// </summary> public static ImageCapture Instance; /// <summary> /// Keep counts of the taps for image renaming /// </summary> private int captureCount = 0; /// <summary> /// Photo Capture object /// </summary> private PhotoCapture photoCaptureObject = null; /// <summary> /// Allows gestures recognition in HoloLens /// </summary> private GestureRecognizer recognizer; /// <summary> /// Loop timer /// </summary> private float secondsBetweenCaptures = 10f; /// <summary> /// Application main functionalities switch /// </summary> internal enum AppModes {Analysis, Training } /// <summary> /// Local variable for current AppMode /// </summary> internal AppModes AppMode { get; private set; } /// <summary> /// Flagging if the capture loop is running /// </summary> internal bool captureIsActive; /// <summary> /// File path of current analysed photo /// </summary> internal string filePath = string.Empty;
ここで、Awake() および Start() メソッドのコードを追加する必要があります。
/// <summary> /// Called on initialization /// </summary> private void Awake() { Instance = this; // Change this flag to switch between Analysis Mode and Training Mode AppMode = AppModes.Training; } /// <summary> /// Runs at initialization right after Awake method /// </summary> void Start() { // Clean up the LocalState folder of this application from all photos stored DirectoryInfo info = new DirectoryInfo(Application.persistentDataPath); var fileInfo = info.GetFiles(); foreach (var file in fileInfo) { try { file.Delete(); } catch (Exception) { Debug.LogFormat("Cannot delete file: ", file.Name); } } // Subscribing to the HoloLens API gesture recognizer to track user gestures recognizer = new GestureRecognizer(); recognizer.SetRecognizableGestures(GestureSettings.Tap); recognizer.Tapped += TapHandler; recognizer.StartCapturingGestures(); SceneOrganiser.Instance.SetCameraStatus("Ready"); }
タップ ジェスチャが発生したときに呼び出されるハンドラーを実装します。
/// <summary> /// Respond to Tap Input. /// </summary> private void TapHandler(TappedEventArgs obj) { switch (AppMode) { case AppModes.Analysis: if (!captureIsActive) { captureIsActive = true; // Set the cursor color to red SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red; // Update camera status to looping capture. SceneOrganiser.Instance.SetCameraStatus("Looping Capture"); // Begin the capture loop InvokeRepeating("ExecuteImageCaptureAndAnalysis", 0, secondsBetweenCaptures); } else { // The user tapped while the app was analyzing // therefore stop the analysis process ResetImageCapture(); } break; case AppModes.Training: if (!captureIsActive) { captureIsActive = true; // Call the image capture ExecuteImageCaptureAndAnalysis(); // Set the cursor color to red SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red; // Update camera status to uploading image. SceneOrganiser.Instance.SetCameraStatus("Uploading Image"); } break; } }
Note
分析モードでは、TapHandler メソッドは写真キャプチャのループを開始または停止するためのスイッチとして機能します。
トレーニングモードでは、カメラから画像をキャプチャします。
カーソルが緑色の場合は、カメラが画像を撮影できる状態であることを示します。
カーソルが赤色の場合は、カメラがビジー状態であることを示します。
アプリケーションで画像キャプチャ プロセスを開始して画像を格納するために使用するメソッドを追加します。
/// <summary> /// Begin process of Image Capturing and send To Azure Custom Vision Service. /// </summary> private void ExecuteImageCaptureAndAnalysis() { // Update camera status to analysis. SceneOrganiser.Instance.SetCameraStatus("Analysis"); // Create a label in world space using the SceneOrganiser class // Invisible at this point but correctly positioned where the image was taken SceneOrganiser.Instance.PlaceAnalysisLabel(); // Set the camera resolution to be the highest possible Resolution cameraResolution = PhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First(); Texture2D targetTexture = new Texture2D(cameraResolution.width, cameraResolution.height); // Begin capture process, set the image format PhotoCapture.CreateAsync(false, delegate (PhotoCapture captureObject) { photoCaptureObject = captureObject; CameraParameters camParameters = new CameraParameters { hologramOpacity = 0.0f, cameraResolutionWidth = targetTexture.width, cameraResolutionHeight = targetTexture.height, pixelFormat = CapturePixelFormat.BGRA32 }; // Capture the image from the camera and save it in the App internal folder captureObject.StartPhotoModeAsync(camParameters, delegate (PhotoCapture.PhotoCaptureResult result) { string filename = string.Format(@"CapturedImage{0}.jpg", captureCount); filePath = Path.Combine(Application.persistentDataPath, filename); captureCount++; photoCaptureObject.TakePhotoAsync(filePath, PhotoCaptureFileOutputFormat.JPG, OnCapturedPhotoToDisk); }); }); }
写真がキャプチャされたとき、および写真を分析する準備ができたときに呼び出されるハンドラーを追加します。 その後、コードの設定モードに応じて、結果が CustomVisionAnalyser または CustomVisionTrainer に渡されます。
/// <summary> /// Register the full execution of the Photo Capture. /// </summary> void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result) { // Call StopPhotoMode once the image has successfully captured photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode); } /// <summary> /// The camera photo mode has stopped after the capture. /// Begin the Image Analysis process. /// </summary> void OnStoppedPhotoMode(PhotoCapture.PhotoCaptureResult result) { Debug.LogFormat("Stopped Photo Mode"); // Dispose from the object in memory and request the image analysis photoCaptureObject.Dispose(); photoCaptureObject = null; switch (AppMode) { case AppModes.Analysis: // Call the image analysis StartCoroutine(CustomVisionAnalyser.Instance.AnalyseLastImageCaptured(filePath)); break; case AppModes.Training: // Call training using captured image CustomVisionTrainer.Instance.RequestTagSelection(); break; } } /// <summary> /// Stops all capture pending actions /// </summary> internal void ResetImageCapture() { captureIsActive = false; // Set the cursor color to green SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.green; // Update camera status to ready. SceneOrganiser.Instance.SetCameraStatus("Ready"); // Stop the capture loop if active CancelInvoke(); }
Unity に戻る前に、必ず Visual Studio で変更を保存してください。
すべてのスクリプトが完成したので、Unity Editor に戻ってから、Scripts フォルダーの SceneOrganiser クラスをクリックして、[階層] パネルの [Main Camera] オブジェクトにドラッグします。
第 12 章 - ビルドする前に
アプリケーションの完全なテストを実行するには、それを HoloLens にサイドロードする必要があります。
実行する前に、次のことを確認してください。
第 2 章に記載されている設定がすべて正しく設定されている。
[Main Camera] ([インスペクター] パネル) のすべてのフィールドが適切に割り当てられている。
SceneOrganiser スクリプトが Main Camera オブジェクトにアタッチされている。
predictionKey 変数に予測キーが挿入されている。
predictionEndpoint 変数に予測エンドポイントが挿入されている。
CustomVisionTrainer クラスの trainingKey 変数にトレーニング キーが挿入されている。
CustomVisionTrainer クラスの projectId 変数にプロジェクト ID が挿入されている。
第 13 章 - アプリケーションのビルドとサイドロード
ビルドプロセスを開始するには:
[ファイル] > [ビルド設定] に移動します。
[Unity C# プロジェクト] をオンにします。
[ビルド] をクリックします。 Unity によって [エクスプローラー] ウィンドウが起動されます。そこで、アプリのビルド先のフォルダーを作成して選択する必要があります。 そのフォルダーを作成して、「App」という名前を付けます。 次に、App フォルダーを選択した状態で [フォルダーの選択] をクリックします。
Unity で、App フォルダーに対してプロジェクトのビルドが開始されます。
Unity によるビルドが完了すると (多少時間がかかる場合があります)、[エクスプローラー] ウィンドウが開いて、ビルドの場所が表示されます (必ずしも最前面に表示されるとは限らないため、タスク バーを確認してください。新しいウィンドウが追加されたことがわかります)。
HoloLens にデプロイするには、次の手順を実行します。
HoloLens の IP アドレスが必要になります (リモート デプロイの場合)。また、HoloLens が開発者モードになっていることを確認する必要があります。 手順は次のとおりです。
HoloLens の装着中に、[設定] を開きます。
[ ネットワークとインターネット>Wi-Fi>Advanced オプションに移動します
IPv4 アドレスを書き留めます。
次に、 Settings に戻り、 Update と Security>For Developers に移動します。
[開発者モード] を [オン] に設定します。
新しいユニティビルド(アプリフォルダー)にナビゲートし、Visual Studioでソリューションファイルを開きます。
[ソリューション構成] で、[デバッグ] を選択します。
[ソリューション プラットフォーム] で、[X86]、[リモート コンピューター] を選択します。 リモート デバイスの (この場合は、メモした HoloLens の) IP アドレスを挿入するように求められます。
[ビルド] メニューに移動して [ソリューションの配置] をクリックして、アプリケーションを HoloLens にサイドロードします。
HoloLens にインストールされたアプリの一覧にアプリが表示され、起動できる状態になります。
Note
イマーシブ ヘッドセットにデプロイするには、[Solution Platform](ソリューション プラットフォーム)を[Local Machine](ローカル コンピューター)に設定し、[Configuration](構成)を [Debug](デバッグ) にし、プラットフォームとして x86 を設定します。 次に、[ビルド] メニューの [ソリューションの配置] を選択して、ローカル コンピューターにデプロイします。
アプリケーションを使用するには:
アプリの機能をトレーニングモードと予測モードの間で切り替えるには、ImageCapture クラスの Awake() メソッド内にある AppMode 変数を更新する必要があります。
// Change this flag to switch between Analysis mode and Training mode
AppMode = AppModes.Training;
または
// Change this flag to switch between Analysis mode and Training mode
AppMode = AppModes.Analysis;
トレーニングモードで:
マウスまたはキーボードを見て、タップ ジェスチャを使用します。
次に、タグを指定するように求めるテキストが表示されます。
「マウス」または「キーボード」のいずれかを言います。
予測モードで:
オブジェクトを見て、タップ ジェスチャを使用します。
確率 (正規化されている) が最も高い検出済みオブジェクトを示すテキストが表示されます。
第 14 章 - Custom Vision モデルの評価と改善
サービスの精度を高めるには、予測に使用するモデルを継続的にトレーニングする必要があります。 これを実現するには、新しいアプリケーションをトレーニングと予測の両方のモードで使用しますが、後者ではポータルにアクセスする必要があります。この章ではこれについて説明します。 モデルを継続的に改善するため、ポータルに何度もアクセスできるように準備してください。
もう一度 Azure Custom Vision ポータルに移動し、プロジェクトが表示されたら、(ページの上部中央にある) [予測] タブを選択します。
アプリケーションの実行中にサービスに送信されたすべての画像が表示されます。 画像にマウス ポインターを合わせると、その画像に対して行われた予測が表示されます。
いずれかの画像を選択して開きます。 開くと、その画像に対して行われた予測が右側に表示されます。 予測が適切であり、この画像をサービスのトレーニング モデルに追加する場合は、[マイ タグ] 入力ボックスをクリックし、関連付けるタグを選択します。 完了したら、右下にある [保存して閉じる] ボタンをクリックして、次の画像に進みます。
画像のグリッドに戻ると、タグを追加 (および保存) した画像が削除されているのがわかります。 タグ付けされた項目が含まれていないと思われる画像がある場合は、その画像のチェック ボックスをオンにし (複数の画像に対してこれを実行できます)、グリッド ページの右上隅にある [削除] をクリックしてそれらを削除できます。 次のポップアップで、[はい、削除します] をクリックして削除を確認するか、[いいえ] をクリックしてキャンセルします。
操作を続行する準備ができたら、右上にある緑色の [トレーニング] ボタンをクリックします。 現在提供されているすべての画像を使用してサービス モデルがトレーニングされます (精度が向上します)。 トレーニングが完了したら、[既定値にする] ボタンをもう一度クリックして、[予測 URL] でサービスの最新のイテレーションが引き続き使用されるようにしてください。
完成した Custom Vision API アプリケーション
これで、Azure Custom Vision API を利用して現実世界のオブジェクトを認識し、サービス モデルをトレーニングして、検出されたものの信頼度を表示する Mixed Reality アプリが構築されました。
ボーナス演習
演習 1
より多くのオブジェクトを認識できるように Custom Vision サービスをトレーニングします。
演習 2
学習した内容を拡張する手段として、次の演習を実行します。
物体が認識されたときにサウンドを再生します。
演習 3
API を使用して、アプリで分析しているのと同じ画像を使用してサービスを再トレーニングし、サービスの精度を高めます (予測とトレーニングの両方を同時に行います)。