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:カスタム ビジョン ✔️ ✔️

注意

このコースでは主に HoloLens に焦点を当てていますが、このコースで学習した内容を Windows Mixed Reality イマーシブ (VR) ヘッドセットにも適用できます。 イマーシブ (VR) ヘッドセットにはアクセス可能なカメラがないため、外部カメラが PC に接続されている必要があります。 このコースの中では、イマーシブ (VR) ヘッドセットをサポートするために必要な変更がある場合に、その変更が注意事項として記載されています。

前提条件

Note

このチュートリアルは、Unity と C# の基本的な使用経験がある開発者を対象としています。 また、このドキュメント内の前提条件や文章による説明は、執筆時 (2018 年 7 月) にテストおよび検証された内容であることをご了承ください。 ツールのインストールの記事に記載されているように、お客様は最新のソフトウェアを自由に使用できます。ただし、このコースの情報は、以下に記載されているものよりも新しいソフトウェアでの設定や結果と完全に一致するとは限りません。

このコースでは、次のハードウェアとソフトウェアをお勧めします。

開始する前に

  1. このプロジェクトをビルドする際の問題を避けるために、このチュートリアルで紹介するプロジェクトをルートまたはルートに近いフォルダーに作成することを強くお勧めします (フォルダー パスが長いと、ビルド時に問題が発生する可能性があります)。
  2. HoloLens を設定してテストします。 HoloLens の設定でサポートが必要な場合は、HoloLens セットアップに関する記事にアクセスしてください
  3. 新しい HoloLens アプリの開発を開始するときは、調整とセンサーのチューニングを実行することをお勧めします (ユーザーごとにこれらのタスクを実行すると役立つ場合があります)。

調整の詳細については、この HoloLens の調整に関する記事へのリンクを参照してください。

センサー チューニングの詳細については、HoloLens センサー チューニングに関する記事へのリンクを参照してください。

第 1 章 - Custom Vision サービス ポータル

Azure でCustom Vision サービスを使用するには、アプリケーションで使用できるようにサービスのインスタンスを構成する必要があります。

  1. まず、Custom Vision サービスのメイン ページに移動します

  2. [Get Started] (開始) ボタンをクリックします。

    Custom Vision サービスの概要

  3. Custom Vision サービス ポータルにサインインします。

    ポータルにサインインする

    Note

    まだ Azure アカウントをお持ちでない方は、作成する必要があります。 このチュートリアルを教室やラボで受講している場合は、インストラクターや監督者に新しいアカウントの設定方法を質問してください。

  4. 初めてログインすると、[サービス利用規約] パネルが表示されます。 利用規約に同意するチェック ボックスをオンにします。 次に、[同意する] をクリックします。

    サービス利用規約

  5. 利用規約に同意すると、ポータルの [プロジェクト] セクションに自動的に移動します。 [新しいプロジェクト] をクリックします。

    新しいプロジェクトの作成

  6. 右側にタブが表示され、プロジェクトに関するいくつかのフィールドを指定するように求められます。

    1. プロジェクトの [名前] を挿入します。

    2. プロジェクトの [説明] を挿入します (省略可能)。

    3. [リソース グループ] を選択するか、新規に作成します。 リソース グループは、Azure アセットのコレクションの監視、アクセス制御、プロビジョニング、課金管理を行う方法を提供します。 1 つのプロジェクト (たとえば、これらのコースなど) に関連するすべての Azure サービスを共通のリソース グループに保持することをお勧めします。

    4. [プロジェクトの種類][分類] に設定します

    5. [ドメイン][全般] に設定します。

      ドメインを設定する

      Azure リソース グループの詳細については、リソース グループに関する記事をご覧ください

  7. 完了したら、[プロジェクトの作成] をクリックします。Custom Vision サービスのプロジェクト ページにリダイレクトされます。

第 2 章 - Custom Vision プロジェクトのトレーニング

Custom Vision ポータルにアクセスする主な目的は、画像内の特定のオブジェクトを認識するようにプロジェクトをトレーニングすることです。 アプリケーションで認識するオブジェクトごとに、少なくとも 5 個の画像が必要ですが、10 個が推奨されます。 このコースで既に提供されている画像 (コンピューターのマウスとキーボード) を使用できます

Custom Vision サービス プロジェクトをトレーニングするには:

  1. [タグ] の横にある + ボタンをクリックします。

    新しいタグを追加する

  2. 認識するオブジェクトの [名前] を追加します。 [Save] をクリックします。

    オブジェクト名を追加して保存する

  3. タグが追加されていることがわかります (タグを表示するために、ページの再読み込みが必要になる場合があります)。 新しいタグの横にあるチェック ボックスをオンにします (まだオンにしていない場合)。

    新しいタグを有効にする

  4. ページの中央にある [画像の追加] をクリックします。

    画像の追加

  5. [ローカル ファイルの参照] をクリックしてから、アップロードする画像を検索して選択します (少なくとも 5 個)。 これらの画像のすべてに、トレーニング対象のオブジェクトが含まれている必要があります。

    Note

    一度に複数の画像を選択してアップロードできます。

  6. タブに画像が表示されたら、[マイ タグ] ボックスで適切なタグを選択します。

    タグの選択

  7. [ファイルのアップロード] をクリックします。 ファイルのアップロードが開始されます。 アップロードの確認が完了したら、[完了] をクリックします。

    ファイルをアップロードする

  8. 同じプロセスを繰り返して、「Keyboard」という名前の新しいタグを作成し、適切な写真をアップロードします。 新しいタグを作成したら、[画像の追加] ウィンドウを表示するため、必ず [マウス]オフにしてください。

  9. 両方のタグを設定したら、[トレーニング] をクリックすると、最初のトレーニング イテレーションの構築が開始されます。

    トレーニングイテレーションを有効にする

  10. ビルドが完了すると、[既定値にする][予測 URL] の 2 つのボタンが表示されます。 最初に [既定値にする] をクリックし、次に [予測 URL] をクリックします。

    既定の URL と予測 URL を作成する

    Note

    このエンドポイント URL は、既定としてマークされているイテレーションに設定されます。 そのため、後で新しいイテレーションを作成し、それを既定として更新する場合は、コードを変更する必要はありません。

  11. [予測 URL] をクリックしたら、メモ帳を開き、後でコードで必要になったときに取得できるように、URLPrediction-Key をコピーして貼り付けます。

    URL と予測キーをコピーして貼り付ける

  12. 画面の右上にある歯車アイコンをクリックします。

    歯車アイコンをクリックして設定を開く

  13. トレーニング キーをコピーし、後で使用できるようにメモ帳に貼り付けます。

    トレーニング キーをコピーする

  14. また、プロジェクト ID もコピーして、後で使用できるようにメモ帳ファイルに貼り付けます。

    プロジェクト ID をコピーする

第 3 章 - Unity プロジェクトを設定する

次の設定は、複合現実での開発のための一般的な設定であるため、他のプロジェクトのテンプレートとして利用できます。

  1. Unity を開き、[New] (新規) をクリックします。

    新しい Unity プロジェクトを作成する

  2. 次に Unity のプロジェクト名を指定する必要があります。 AzureCustomVision を挿入します。プロジェクト テンプレートが 3D に設定されていることを確認します。 [場所] を適切な場所に設定します (ルート ディレクトリに近い方が適しています)。 次に、[プロジェクトの作成] をクリックします。

    プロジェクトの設定を構成する

  3. Unity を開いた状態で、既定のスクリプト エディターVisual Studio に設定されているかどうか確認することをお勧めします。 [環境設定編集]> に移動し、新しいウィンドウから [外部ツール] に移動します。 [外部スクリプト エディター][Visual Studio 2017] に変更します。 [環境設定] ウィンドウを閉じます。

    外部ツールの構成

  4. 次に、[ファイル] > [ビルド設定] に移動して [ユニバーサル Windows プラットフォーム] を選び、[プラットフォームの切り替え] ボタンをクリックして選択を適用します。

    ビルド設定を構成する

  5. 引き続き [ファイル] > [ビルド設定] で、次のことを確認します。

    1. [ターゲット デバイス][HoloLens] に設定されている

      イマーシブ ヘッドセットの場合は、[ターゲット デバイス][任意のデバイス] に設定します。

    2. [Build Type] (ビルドの種類)[D3D] に設定されている

    3. [SDK][Latest installed] (最新のインストール) に設定されている

    4. [Visual Studio Version] (Visual Studio のバージョン)[Latest installed] (最新のインストール) に設定されている

    5. [Build and Run] (ビルドと実行)[Local Machine] (ローカル マシン) に設定されている

    6. シーンを保存し、ビルドに追加します。

      1. これを行うには、[Add Open Scenes] (開いているシーンを追加) を選択します。 保存ウィンドウが表示されます。

        開いているシーンをビルド リストに追加する

      2. これと、今後のシーン用の新しいフォルダーを作成し、[新しいフォルダー] ボタンを選択して、新しいフォルダーを作成し、「Scenes」という名前を付けます。

        新しいシーン フォルダーを作成する

      3. 新しく作成した Scenes フォルダーを開いてから、[ファイル名:] テキスト フィールドに「CustomVisionScene」と入力して [保存] をクリックします。

        新しいシーン ファイルに名前を付けます

        Unity のシーンは Unity プロジェクトに関連付けられている必要があるため、Assets フォルダーに保存する必要があることに注意してください。 Unity プロジェクトの構築では、通常、Scenes フォルダー (とその他の類似フォルダー) を作成します。

    7. [ビルド設定] の残りの設定は、ここでは既定値のままにしておきます。

      既定のビルド設定

  6. [ビルド設定] ウィンドウで、[プレーヤー設定] ボタンをクリックすると、[インスペクター] が配置されているスペースに関連パネルが表示されます。

  7. このパネルでは、いくつかの設定を確認する必要があります。

    1. [その他の設定] タブで:

      1. [スクリプト ランタイム バージョン][試験段階 (.NET 4.6 と同等)] である (この場合、エディターの再起動が必要になります)

      2. [スクリプト バックエンド][.NET] である

      3. [API Compatibility Level] (API 互換性レベル)[.NET 4.6] である

      API のコンバンティブルを設定する

    2. [公開設定] タブ内の [機能] で、次の内容を確認します。

      1. InternetClient

      2. Web カメラ

      3. マイク

      発行の設定を構成する

    3. さらに、パネルの下にある [XR 設定] ([公開設定] の下) で、[Virtual Reality サポート] をオンにし、Windows Mixed Reality SDK が追加されていることを確認します。

    XR 設定を構成する

  8. [ビルド設定] に戻ると、Unity C# プロジェクトに適用されていた灰色表示が解除されています。その横にあるチェック ボックスをオンにします。

  9. [ビルド設定] ウィンドウを閉じます。

  10. シーンとプロジェクトを保存します ([ファイル] > [シーン/ファイルの保存] > [プロジェクトの保存])。

第 4 章 - Unity での Newtonsoft DLL のインポート

重要

このコースのUnity のセットアップコンポーネントをスキップして、そのままコードに進みたい場合は、この Azure-MR-302b.unitypackage をダウンロードして、それをカスタム パッケージとしてご自分のプロジェクトにインポートしてから、第 6 章から続けてください。

このコースでは、アセットに DLL として追加できる Newtonsoft ライブラリを使用する必要があります。 このライブラリを含むパッケージは、こちらのリンクからダウンロードできます。 Newtonsoft ライブラリをプロジェクトにインポートするには、このコースに付属している Unity パッケージを使用します。

  1. .unitypackage を Unity に追加するには、[資産][パッケージのインポート] [カスタムパッケージ] メニュー オプションを使用します。

  2. ポップアップ表示される [Unity パッケージのインポート] ボックスで、[プラグイン] およびその下にあるすべての項目が選択されていることを確認します。

    すべてのパッケージ アイテムをインポートする

  3. [インポート] ボタンをクリックして、各項目をプロジェクトに追加します。

  4. [プロジェクト] ビューの [プラグイン] の下にある Newtonsoft フォルダーに移動して、Newtonsoft.Json プラグインを選択します。

    Newtonsoft プラグインを選択する

  5. Newtonsoft.Json プラグインを選択した状態で、[任意のプラットフォーム]オフになっていることを確認し、[WSAPlayer]オフになっていることを確認してから、[適用] をクリックします。 これは単に、ファイルが正しく構成されていることを確認するための手順です。

    Newtonsoft プラグインを構成する

    Note

    これらのプラグインにマークを付けると、それらは Unity エディターでのみ使用されるように構成されます。 プロジェクトが Unity からエクスポートされた後に使用される別のセットが WSA フォルダー内にあります。

  6. 次に、Newtonsoft フォルダー内にある WSA フォルダーを開く必要があります。 先ほど構成したものと同じファイルのコピーが表示されます。 そのファイルを選択して、インスペクターで次のことを確認します

    • [任意のプラットフォーム] チェック ボックスがオフである
    • [WSAPlayer]のみオンである
    • [処理しない]オンである

    Newtonsoft プラグイン プラットフォームの設定を構成する

第 5 章 - カメラのセットアップ

  1. [階層] パネルで、Main Camera を選択します。

  2. 選択すると、Main Camera のすべてのコンポーネントを [インスペクター] パネルに表示できるようになります。

    1. カメラオブジェクトの名前は「Main Camera」である必要があります (スペルにご注意ください)。

    2. メイン カメラの [タグ] は「MainCamera」に設定する必要があります (スペルにご注意ください)。

    3. [変換の位置]0、0、0 に設定されていることを確認します。

    4. [クリア フラグ][単色] に設定します (イマーシブ ヘッドセットの場合は無視してください)。

    5. カメラ コンポーネントの [背景色]黒、アルファ 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 クラスに渡します。

このクラスを作成するには:

  1. [プロジェクト] パネルにある Asset フォルダーを右クリックし、[作成]> [フォルダー] の順にクリックします。 フォルダーに「Scripts」という名前を付けます。

    scripts フォルダーを作成する

  2. 先ほど作成したフォルダーをダブルクリックして開きます。

  3. フォルダー内を右クリックし、[作成]>[C# スクリプト] の順にクリックします。 スクリプトに「CustomVisionAnalyser」という名前を付けます。

  4. 新しい CustomVisionAnalyser スクリプトをダブルクリックして Visual Studio で開きます。

  5. ファイルの一番上にある名前空間を次のように更新します。

    using System.Collections;
    using System.IO;
    using UnityEngine;
    using UnityEngine.Networking;
    using Newtonsoft.Json;
    
  6. 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 変数に予測エンドポイントを挿入してください。 これらは、前の手順でメモ帳にコピーしたものです。

  7. ここで、インスタンス変数を初期化する Awake() のコードを追加する必要があります。

        /// <summary>
        /// Initialises this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. Start() および Update() メソッドを削除します。

  9. 次に、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);
        }
    
  10. Unity に戻る前に、必ず Visual Studio で変更を保存してください。

第 7 章 - CustomVisionObjects クラスの作成

ここでは、CustomVisionObjects クラスを作成します。

このスクリプトには、Custom Vision サービスに対して行われた呼び出しをシリアル化および逆シリアル化するために他のクラスで使用されるいくつかのオブジェクトが含まれています。

警告

以下の JSON 構造は Custom Vision Prediction v2.0 で動作するように設定されているため、Custom Vision サービスによって提供されるエンドポイントをメモすることが重要です。 別のバージョンを使用する場合は、以下の構造を更新する必要があります。

このクラスを作成するには:

  1. Scripts フォルダー内で右クリックしてから、[作成]>[C# スクリプト] の順にクリックします。 CustomVisionObjects スクリプトを呼び出します。

  2. 新しい CustomVisionObjects スクリプトをダブルクリックして Visual Studio で開きます。

  3. ファイルの先頭に次のステートメントを追加します。

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. CustomVisionObjects クラス内の Start() および Update() メソッドを削除します。これで、このクラスは空になります。

  5. 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 クラスの作成

このクラスでは、ユーザーの音声入力を認識します。

このクラスを作成するには:

  1. Scripts フォルダー内で右クリックしてから、[作成]>[C# スクリプト] の順にクリックします。 VoiceRecognizer スクリプトを呼び出します。

  2. 新しい VoiceRecognizer スクリプトをダブルクリックして Visual Studio で開きます。

  3. VoiceRecognizer クラスの上に次の名前空間を入力します。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.Windows.Speech;
    
  4. 次に、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>();
    
  5. 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;
        }
    
  6. Update() メソッドを削除します。

  7. 次のハンドラーを追加します。これは、音声入力が認識されるたびに呼び出されます。

        /// <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();
            }
        }
    
  8. Unity に戻る前に、必ず Visual Studio で変更を保存してください。

Note

エラーを含む可能性があるコードについては、すぐにこれらを修正する追加のクラスを作成するので、気にしないでください。

第 9 章 - CustomVisionTrainer クラスの作成

このクラスでは、Custom Vision サービスをトレーニングする一連の Web 呼び出しをチェーンします。 各呼び出しについては、コードの直前で詳しく説明します。

このクラスを作成するには:

  1. Scripts フォルダー内で右クリックしてから、[作成]>[C# スクリプト] の順にクリックします。 CustomVisionTrainer スクリプトを呼び出します。

  2. 新しい CustomVisionTrainer スクリプトをダブルクリックして Visual Studio で開きます。

  3. CustomVisionTrainer クラスの上に次の名前空間を入力します。

    using Newtonsoft.Json;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Text;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. 次に、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 以降) でポータルから収集した値です。

  5. 次の 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);
        }
    
  6. Update() メソッドを削除します。 このクラスには不要です。

  7. 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();
        }
    
  8. 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));
            }
        }
    
  9. 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());
        }
    
  10. 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)); 
                }
            }
        }
    
  11. 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));
                }
            }
        }
    
  12. 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();
            }
        }
    
  13. このクラスに最後に追加するメソッドは、キャプチャした画像をバイト配列に変換するために 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);
        }
    
  14. Unity に戻る前に、必ず Visual Studio で変更を保存してください。

第 10 章 - SceneOrganiser クラスの作成

このクラスでは:

  • メイン カメラにアタッチする Cursor オブジェクトを作成します。

  • サービスによって現実世界のオブジェクトが認識されたときに表示される Label オブジェクトを作成します。

  • 適切なコンポーネントをアタッチしてメイン カメラを設定します。

  • 分析モードでは、実行時にメイン カメラの位置を基準として適切なワールド空間にラベルを生成し、Custom Vision サービスから受信したデータを表示します。

  • トレーニング モードでは、トレーニング プロセスのさまざまな段階を表示する UI を生成します。

このクラスを作成するには:

  1. Scripts フォルダー内を右クリックして、[作成]>[C# スクリプト] をクリックします。 スクリプトに「SceneOrganiser」という名前を付けます。

  2. 新しい SceneOrganiser スクリプトをダブルクリックして Visual Studio で開きます。

  3. 必要な名前空間は 1 つだけです。SceneOrganiser クラスの上にある他のものは削除します。

    using UnityEngine;
    
  4. 次に、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;
    
  5. Start() および Update() メソッドを削除します。

  6. 変数のすぐ下に、クラスを初期化してシーンを設定する 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");
        }
    
  7. 次に、メイン カメラのカーソルを作成して配置する 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;
        }
    
  8. カメラの状態を示すテキスト メッシュに送信されるメッセージを処理する 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>";
            }
        }
    
  9. 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")}");
                    }
                }
            }
        }
    
  10. 最後に、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;
        }
    
  11. Unity に戻る前に、必ず Visual Studio で変更を保存してください。

重要

続行する前に、CustomVisionAnalyser クラスを開き、AnalyseLastImageCaptured() メソッド内で次の行のコメントを解除します。

  AnalysisObject analysisObject = new AnalysisObject();
  analysisObject = JsonConvert.DeserializeObject<AnalysisObject>(jsonResponse);
  SceneOrganiser.Instance.SetTagsToLastLabel(analysisObject);

第 11 章 - ImageCapture クラスの作成

次に作成するクラスは ImageCapture クラスです。

このクラスは次の役割を担います。

  • HoloLens のカメラを使用して画像をキャプチャし、それを App フォルダーに格納します。

  • ユーザーのタップ ジェスチャを処理します。

  • アプリケーションが分析モードとトレーニングモードのどちらで動作するかを決定する Enum 値を管理します。

このクラスを作成するには:

  1. 先ほど作成した Scripts フォルダーに移動します。

  2. フォルダー内を右クリックし、[作成] > [C# スクリプト] の順にクリックします。 スクリプトに「ImageCapture」という名前を付けます。

  3. 新しい ImageCapture スクリプトをダブルクリックして Visual Studio で開きます。

  4. ファイルの先頭にある名前空間を次のものに置き換えます。

    using System;
    using System.IO;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.XR.WSA.Input;
    using UnityEngine.XR.WSA.WebCam;
    
  5. 次に、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;
    
  6. ここで、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");
        }
    
  7. タップ ジェスチャが発生したときに呼び出されるハンドラーを実装します。

        /// <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 メソッドは写真キャプチャのループを開始または停止するためのスイッチとして機能します。

    トレーニングモードでは、カメラから画像をキャプチャします。

    カーソルが緑色の場合は、カメラが画像を撮影できる状態であることを示します。

    カーソルが赤色の場合は、カメラがビジー状態であることを示します。

  8. アプリケーションで画像キャプチャ プロセスを開始して画像を格納するために使用するメソッドを追加します。

        /// <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);              
                });
            });   
        }
    
  9. 写真がキャプチャされ、分析する準備ができたときに呼び出されるハンドラーを追加します。 その後、コードの設定モードに応じて、結果が 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();
        }
    
  10. Unity に戻る前に、必ず Visual Studio で変更を保存してください。

  11. すべてのスクリプトが完成したので、Unity Editor に戻ってから、Scripts フォルダーの SceneOrganiser クラスをクリックして、[階層] パネルの [Main Camera] オブジェクトにドラッグします。

第 12 章 - ビルドする前に

アプリケーションの完全なテストを実行するには、それを HoloLens にサイドロードする必要があります。

実行する前に、次のことを確認してください。

  • 第 2 章に記載されている設定がすべて正しく設定されている。

  • [Main Camera] ([インスペクター] パネル) のすべてのフィールドが適切に割り当てられている。

  • SceneOrganiser スクリプトが [Main Camera] オブジェクトにアタッチされている。

  • predictionKey 変数に予測キーが挿入されている。

  • predictionEndpoint 変数に予測エンドポイントが挿入されている。

  • CustomVisionTrainer クラスの trainingKey 変数にトレーニング キーが挿入されている。

  • CustomVisionTrainer クラスの projectId 変数にプロジェクト ID が挿入されている。

第 13 章 - アプリケーションのビルドとサイドロード

ビルドプロセスを開始するには:

  1. [ファイル] > [ビルド設定] に移動します。

  2. [Unity C# プロジェクト] チェック ボックスをオンにします。

  3. [ビルド] をクリックします。 Unity によって [エクスプローラー] ウィンドウが起動されます。そこで、アプリのビルド先のフォルダーを作成して選択する必要があります。 そのフォルダーを作成して、「App」という名前を付けます。 次に、App フォルダーを選択した状態で [フォルダーの選択] をクリックします。

  4. Unity によって App フォルダーへのプロジェクトのビルドが開始されます。

  5. Unity によるビルドが完了すると (多少時間がかかる場合があります)、エクスプローラー ウィンドウが開いてビルドの場所が表示されます (必ずしも最前面に表示されるとは限らないため、タスク バーを確認してください。新しいウィンドウが追加されたことがわかります)。

HoloLens にデプロイするには:

  1. HoloLens の IP アドレスが必要になります (リモート デプロイの場合)。また、HoloLens が開発者モードになっていることを確認する必要があります。 手順は次のとおりです。

    1. HoloLens を装着した状態で、[設定] を開きます。

    2. [ネットワーク & インターネット>Wi-Fiの詳細オプション]> に移動します

    3. IPv4 アドレスを書き留めます。

    4. 次に、[設定] に戻り、[Update & Securityfor Developers]\(開発者向けセキュリティ>の更新\) に移動します

    5. [開発者モード] を [オン] に設定します。

  2. 新しい Unity ビルド (App フォルダー) に移動し、Visual Studio を使用してソリューション ファイルを開きます。

  3. [ソリューション構成] で、[デバッグ] を選択します。

  4. [ソリューション プラットフォーム] で、[X86]、[リモート コンピューター] を選択します。 リモート デバイスの (この場合は、メモした HoloLens の) IP アドレスを挿入するように求められます。

    IP アドレスの設定

  5. [ビルド] メニューに移動して [ソリューションの配置] をクリックし、アプリケーションを HoloLens にサイドロードします。

  6. HoloLens にインストールされたアプリの一覧にこのアプリが表示され、起動できる状態になります。

Note

イマーシブ ヘッドセットにデプロイするには、[ソリューション プラットフォーム][ローカル コンピューター] に設定し、[構成][デバッグ] に設定して、[プラットフォーム] として [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 モデルの評価と改善

サービスの精度を高めるには、予測に使用するモデルを継続的にトレーニングする必要があります。 これを実現するには、新しいアプリケーションをトレーニング予測の両方のモードで使用しますが、後者ではポータルにアクセスする必要があります。この章ではこれについて説明します。 モデルを継続的に改善するため、ポータルに何度もアクセスできるように準備してください。

  1. もう一度 Azure Custom Vision ポータルに移動し、プロジェクトが表示されたら、(ページの上部中央にある) [予測] タブを選択します。

    [予測の選択] タブ

  2. アプリケーションの実行中にサービスに送信されたすべての画像が表示されます。 画像にマウス ポインターを合わせると、その画像に対して行われた予測が表示されます。

    予測画像の一覧

  3. いずれかの画像を選択して開きます。 開くと、その画像に対して行われた予測が右側に表示されます。 予測が適切であり、この画像をサービスのトレーニング モデルに追加する場合は、[マイ タグ] 入力ボックスをクリックし、関連付けるタグを選択します。 完了したら、右下にある [保存して閉じる] ボタンをクリックして、次の画像に進みます。

    開く画像を選択する

  4. 画像のグリッドに戻ると、タグを追加 (および保存) した画像が削除されているのがわかります。 タグ付けされた項目が含まれていないと思われる画像がある場合は、その画像のチェック ボックスをオンにし (複数の画像に対してこれを実行できます)、グリッド ページの右上隅にある [削除] をクリックしてそれらを削除できます。 次のポップアップで、[はい、削除します] をクリックして削除を確認するか、[いいえ] をクリックしてキャンセルします。

    イメージを削除する

  5. 操作を続行する準備ができたら、右上にある緑色の [トレーニング] ボタンをクリックします。 現在提供されているすべての画像を使用してサービス モデルがトレーニングされます (精度が向上します)。 トレーニングが完了したら、[既定値にする] ボタンをもう一度クリックして、[予測 URL] でサービスの最新のイテレーションが引き続き使用されるようにしてください。

    トレーニング サービス モデルを開始する 既定のオプションを選択する

完成した Custom Vision API アプリケーション

これで、Azure Custom Vision API を利用して現実世界のオブジェクトを認識し、サービス モデルをトレーニングして、検出されたものの信頼度を表示する Mixed Reality アプリが構築されました。

完成したプロジェクトの例

ボーナス演習

演習 1

より多くのオブジェクトを認識できるように Custom Vision サービスをトレーニングします。

演習 2

学習した内容を拡張する手段として、次の演習を実行します。

オブジェクトが認識されたときにサウンドを再生します。

演習 3

API を使用して、アプリで分析する同じ画像を使用してサービスを再トレーニングし、サービスの精度を高めます (予測とトレーニングの両方を同時に行います)。