HoloLens (第 1 代) 和 Azure 302b:自定義視覺


注意

混合實境學院教學課程的設計是以 HoloLens (第 1 代) 和混合實境沉浸式頭戴裝置為準。 因此,對於仍在尋找這些裝置開發指引的開發人員而言,我們覺得這些教學課程很重要。 這些教學課程不會使用用於 HoloLens 2 的最新工具組或互動進行更新。 系統會保留這些資訊,以繼續在支援的裝置上運作。 未來將會張貼一系列新的教學課程,示範如何針對 HoloLens 2 進行開發。 此通知會在張貼時更新這些教學課程的連結。


在此課程中,您將瞭解如何使用混合實境應用程式中的 Azure 自訂視覺 功能,辨識所提供影像內的自定義視覺內容。

此服務可讓您使用物件影像來定型機器學習模型。 然後,您將使用定型的模型來辨識類似的物件,如相機擷取 Microsoft HoloLens 或連線到您的電腦的相機,以進行沉浸式 (VR) 頭戴式裝置。

課程成果

Azure 自訂視覺 是 Microsoft 認知服務,可讓開發人員建置自定義映像分類器。 然後,這些分類器可以與新的影像搭配使用,以辨識或分類該新影像中的物件。 此服務提供簡單、容易使用的在線入口網站,以簡化程式。 如需詳細資訊,請流覽 Azure 自訂視覺 服務頁面

完成本課程之後,您將擁有混合實境應用程式,其能夠以兩種模式運作:

  • 分析模式:藉由上傳影像、建立標籤及訓練服務手動設定 自訂視覺 服務,以在此案例中辨識不同的物件 (滑鼠和鍵盤) 。 然後,您將建立 HoloLens 應用程式,以使用相機擷取影像,並嘗試辨識真實世界中的物件。

  • 訓練模式:您將實作程式代碼,以在您的應用程式中啟用「訓練模式」。 定型模式可讓您使用 HoloLens 相機擷取影像、將擷取的影像上傳至服務,以及定型自定義視覺模型。

本課程將教導您如何將 自訂視覺 服務的結果取得至 Unity 型範例應用程式。 您必須將這些概念套用至您可能要建置的自定義應用程式。

裝置支援

課程 HoloLens 沉浸式頭戴裝置
MR 和 Azure 302b:自訂視覺 ✔️ ✔️

注意

雖然本課程主要著重於 HoloLens,但您也可以將此課程中學到的內容套用至 Windows Mixed Reality 沉浸式 (VR) 頭戴式裝置。 由於沉浸式 (VR) 頭戴式裝置沒有可存取的相機,因此您需要連線到計算機的外部相機。 隨著課程一起進行時,您會看到可能需要採用的任何變更附註,以支援沉浸式 (VR) 頭戴裝置。

必要條件

注意

本教學課程專為具備 Unity 和 C# 基本體驗的開發人員所設計。 另請注意,本檔中的必要條件和書面指示代表在撰寫 (2018 年 7 月) 時已經過測試和驗證的內容。 您可以自由使用最新的軟體,如 安裝工具 一文中所列,但不應假設本課程中的資訊完全符合您在較新軟體中找到的內容,而不是下面所列的內容。

針對本課程,我們建議使用下列硬體和軟體:

開始之前

  1. 若要避免建置此專案時發生問題,強烈建議您在根資料夾或近根資料夾中建立本教學課程中提及的專案, (長資料夾路徑可能會導致建置時間) 的問題。
  2. 設定及測試 HoloLens。 如果您需要設定 HoloLens 的支援, 請務必流覽 HoloLens 設定一文
  3. 在開始開發新的 HoloLens 應用程式時,最好先執行校正和感測器微調 (有時有助於針對每個使用者執行這些工作) 。

如需校正的說明,請遵循此 連結至 HoloLens 校正文章

如需感測器微調的說明,請遵循此 連結至 HoloLens 感測器微調文章

第 1 章 - 自訂視覺 服務入口網站

若要在 Azure 中使用 自訂視覺 服務,您必須設定服務實例,才能提供給您的應用程式使用。

  1. 首先,流覽至 自訂視覺 服務主頁面

  2. 按兩下 [ 開始使用] 按鈕。

    開始使用 自訂視覺 服務

  3. 登入 自訂視覺 服務入口網站。

    登入入口網站

    注意

    如果您還沒有 Azure 帳戶,則必須建立一個帳戶。 如果您在教室或實驗室案例中遵循本教學課程,請詢問講師或其中一個專業人員,以協助設定新的帳戶。

  4. 第一次登入之後,系統會提示您輸入 服務條款 面板。 按兩下複選框以同意條款。 然後按兩下 [我同意]。

    服務條款

  5. 同意條款之後,您將流覽至入口網站的 [ 專案 ] 區段。 按兩下 [ 新增專案]。

    建立新專案

  6. 索引標籤會出現在右側,這會提示您指定專案的一些欄位。

    1. 插入項目 的名稱

    2. 插入專案的 [描述 ] (選擇性) 。

    3. 選擇 資源群組 或建立新的群組。 資源群組提供一種方式來監視、控制存取、布建和管理 Azure 資產集合的計費。 建議將所有與單一專案相關聯的 Azure 服務保持 (,例如這些課程) 在通用資源群組底下) 。

    4. 項目類型 設定為 分類

    5. [網域 ] 設定為 [一般]。

      設定網域

      如果您想要深入瞭解 Azure 資源群組,請 瀏覽資源群組一文

  7. 完成後,按兩下 [建立專案],系統會將您重新導向至 [自訂視覺 服務] 專案頁面。

第 2 章 - 訓練您的 自訂視覺 專案

一旦在 自訂視覺 入口網站中,您的主要目標是將專案定型,以辨識影像中的特定物件。 您至少需要五個 (5 個) 影像,但建議使用十個 (10 個) ,以便讓應用程式辨識的每個物件。 您可以使用本課程所提供的影像, (電腦滑鼠和鍵盤)

若要訓練您的 自訂視覺 服務專案:

  1. 按兩下 [標記] 旁的+按鈕。

    新增標籤

  2. 新增您想要辨識的物件 名稱 。 按一下 [ 儲存]。

    新增物件名稱並儲存

  3. 您會注意到您的 標籤 已新增 (您可能需要重載頁面,才能顯示) 。 如果尚未核取該複選框,請按下新標記旁的複選框。

    啟用新標籤

  4. 按兩下頁面中央的[ 新增映像 ]。

    新增影像

  5. 按兩下 [ 瀏覽本機檔案],然後搜尋,然後選取您想要上傳的影像,至少為5 (5) 。 請記住,這些影像應該包含您要定型的物件。

    注意

    您可以一次選取數個影像來上傳。

  6. 一旦您可以在索引標籤中看到影像,請在 [ 我的卷 標] 方塊中選取適當的標籤。

    選取標籤

  7. 按兩下 [上傳檔案]。 檔案將會開始上傳。 確認上傳之後,按兩下 [ 完成]。

    上傳檔案

  8. 重複相同的程式,以建立名為 Keyboard 的新標籤,併為其上傳適當的相片。 建立新標記之後,請務必 取消核取滑鼠 ,以便顯示 [ 新增影像 ] 視窗。

  9. 設定兩個標籤之後,按兩下 [ 型],第一個訓練反覆專案就會開始建置。

    啟用定型反覆專案

  10. 建置之後,您將可以看到兩個稱為 [建立預設 ] 和 [ 預測 URL] 的按鈕。 按兩下 [ 先設定預設值 ],然後按兩下 [ 預測URL]。

    進行預設和預測 URL

    注意

    從這個提供的端點 URL 會設定為已標示為預設值的 反覆專案 。 因此,如果您稍後進行新的 反覆 專案並將其更新為預設值,則不需要變更程序代碼。

  11. 單擊 [預測 URL] 之後,請開啟 [記事本],然後複製並貼上 URLPrediction-Key,以便稍後在程式代碼中需要時加以擷取。

    複製並貼上 URL 和預測金鑰

  12. 按兩下畫面右上方的 齒輪

    按兩下齒輪圖示以開啟設定

  13. 複製 訓練金鑰 並將它貼到 記事本,以供稍後使用。

    複制定型金鑰

  14. 同時複製 您的項目識別碼,並將它貼到記 事本 檔案中,以供稍後使用。

    複製項目識別碼

第 3 章 - 設定 Unity 專案

以下是使用混合實境進行開發的一般設定,因此是其他專案的良好範本。

  1. 開啟 Unity ,然後按兩下 [ 新增]。

    建立新的 Unity 專案

  2. 您現在必須提供 Unity 項目名稱。 插入 AzureCustomVision。 請確定項目範本已設定為 3D。 將 [位置 ] 設定為適合您 (記住,較接近根目錄的較佳) 。 然後按兩下 [ 建立專案]。

    設定專案設定

  3. 開啟 Unity 時,值得檢查預設 腳本編輯器 已設定為 Visual Studio。 移至 [編輯>喜好 設定],然後從新視窗流覽至 [外部工具]。 將 外部腳本編輯器 變更為 Visual Studio 2017。 關閉 [喜好設定] 視窗。

    設定外部工具

  4. 接下來,移至 [檔案>建置設定],然後選取 [通用 Windows 平台],然後按兩下 [切換平臺] 按鈕以套用您的選取專案。

    設定組建設定

  5. 仍在 檔案 > 建置設定 中,並確定:

    1. 目標裝置 已設定為 HoloLens

      針對沉浸式頭戴裝置,將 [目標裝置 ] 設定為 [任何裝置]。

    2. 組建類型 設定為 D3D

    3. SDK 已設定為 [最新安裝]

    4. Visual Studio 版本 設定為 [最新安裝]

    5. [建置並執行 ] 設定為 [ 本機計算機]

    6. 儲存場景,並將其新增至組建。

      1. 選取 [ 新增開啟場景] 來執行此動作。 隨即會出現儲存視窗。

        新增開啟場景以建置清單

      2. 為此建立新的資料夾,以及任何未來的場景,然後選取 [ 新增資料夾 ] 按鈕,以建立新的資料夾,並將其命名為 Scenes

        建立新的場景資料夾

      3. 開啟新建立的 Scenes 資料夾,然後在 [ 檔名: 文字] 字段中輸入 CustomVisionScene,然後按兩下 [ 儲存]。

        為新的場景檔案命名

        請注意,您必須將 Unity 場景儲存在 Assets 資料夾中,因為它們必須與 Unity 專案相關聯。 建立場景資料夾 (和其他類似的資料夾) 是建構 Unity 專案的一般方式。

    7. 建置 設定中的其餘設定現在應該保留為預設值。

      預設組建設定

  6. 在 [ 建置設定 ] 視窗中,按兩下 [ 播放程序設定 ] 按鈕,這會在 Inspector 所在的空間中開啟相關的面板。

  7. 在此面板中,必須驗證一些設定:

    1. 在 [ 其他設定] 索引 標籤中:

      1. 腳本運行時間版本 應該是 實驗性 (.NET 4.6 對等) ,這會觸發重新啟動編輯器的需求。

      2. 腳本後端 應該是 .NET

      3. API 相容性層級 應該是 .NET 4.6

      設定 API 相容性

    2. 在 [ 發佈設定] 索引標籤的 [ 功能] 底下,檢查:

      1. InternetClient

      2. 網路攝影機

      3. Microphone (麥克風)

      設定發佈設定

    3. 進一步在面板下方的 [XR 設定] (中找到 [發佈設定]) ,勾選 [支援的虛擬實境],確定已新增 Windows Mixed Reality SDK

    設定 XR 設定

  8. 回到 組建設定Unity C# 專案 不再呈現灰色;勾選此旁的複選框。

  9. 關閉 [建置設定] 視窗。

  10. 儲存場景和專案 (檔案 > 儲存場景/檔案 > 儲存專案) 。

第 4 章 - 在 Unity 中匯入 Newtonsoft DLL

重要

如果您想要略過本課程的 Unity 設定 元件,並直接進入程式碼,請隨意下載此 Azure-MR-302b.unitypackage,將其匯入您的專案作為 自定義套件,然後從 第 6 章繼續進行。

本課程需要使用 Newtonsoft 連結庫,您可以將它新增為資產的 DLL。 您可以從此連結下載包含此連結庫的套件。 若要將 Newtonsoft 連結庫匯入您的專案,請使用本課程隨附的 Unity 套件。

  1. 使用 [資產>套件自定義套件>] 功能表選項,將 .unitypackage 新增至 Unity。

  2. 在彈出視窗的 [ 匯入 Unity 套件 ] 方塊中,確定已選取 [ (] 底下的所有專案,並選取 [包含) 外掛程式 ]。

    匯入所有套件專案

  3. 按兩下 [ 匯入 ] 按鈕,將專案新增至您的專案。

  4. 移至項目檢視中 [外掛程式] 底下的 Newtonsoft 資料夾,然後選取 Newtonsoft.Json 外掛程式

    選取 Newtonsoft 外掛程式

  5. 選取 Newtonsoft.Json 外掛程式後,請確定未核[任何平臺],然後確定 WSAPlayer未核取,然後按兩下 [套用]。 這隻是為了確認檔案已正確設定。

    設定 Newtonsoft 外掛程式

    注意

    標記這些外掛程式會將這些外掛程式設定為只在 Unity 編輯器中使用。 WSA 資料夾中有一組不同的它們,將在專案從 Unity 匯出之後使用。

  6. 接下來,您必須在 Newtonsoft 資料夾中開啟 WSA 資料夾。 您會看到您剛才設定的相同檔案複本。 選取檔案,然後在偵測器中,確定

    • 核取任何平臺
    • 只會檢查WSAPlayer
    • 核取進程

    設定 Newtonsoft 外掛程式平台設定

第 5 章 - 相機設定

  1. 在 [階層] 面板中,選取 [主相機]。

  2. 選取之後,您就可以在 [偵測器] 面板中看到主要相機的所有元件。

    1. 相機對象必須命名為主相機 (記下拼字!)

    2. 主相機 標籤 必須設定為 MainCamera (記下拼字!)

    3. 確定 [轉換位置 ] 設定為 0、0、0

    4. [清除旗標] 設定為 [純色 ], (針對沉浸式頭戴式裝置) 忽略此功能。

    5. 將相機元件 的背景 色彩設定為 黑色,Alpha 0 (十六進位代碼: #0000000000 ) (忽略沉浸式頭戴式裝置) 。

    設定相機元件屬性

第 6 章 - 建立 CustomVisionAnalyser 類別。

此時,您已準備好撰寫一些程序代碼。

您將從 CustomVisionAnalyser 類別開始。

注意

下列程式代碼中對 自訂視覺 服務的呼叫是使用 自訂視覺 REST API 來進行。 透過使用此方式,您將瞭解如何實作和使用此 API (有助於瞭解如何在自己的) 上實作類似的專案。 請注意,Microsoft 提供 自訂視覺 服務 SDK,也可用來呼叫服務。 如需詳細資訊,請參閱 自訂視覺 Service SDK 一文。

此類別負責:

  • 載入擷取為位元組數位的最新影像。

  • 將位元組數位送至 Azure 自訂視覺 服務實例進行分析。

  • 以 JSON 字串形式接收回應。

  • 將回應還原串行化,並將產生的 預測 傳遞至 SceneOrganiser 類別,這會負責處理響應的顯示方式。

若要建立此類別:

  1. 以滑鼠右鍵按兩下位於 [項目面板] 的 [資產資料夾],然後按兩下 [建立>資料夾]。 呼叫 腳本資料夾。

    建立文稿資料夾

  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;
    

    注意

    請務必將 預測密鑰 插入 predictionKey 變數,並將 預測端點 插入 predictionEndpoint 變數中。 您稍早在課程中將這些項目複製到 記事本

  7. 現在必須新增 Awake () 的程式代碼,才能初始化 Instance 變數:

        /// <summary>
        /// Initialises this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. 刪除 Start () Update () 方法。

  9. 接下來,將協同程式 (與其下方的靜態 GetImageAsByteArray () 方法) ,這會取得 ImageCapture 類別所擷取影像分析的結果。

    注意

    AnalysisImageCapture 協同程式中,有一個您尚未建立 的 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. 請務必先將變更儲存在 Visual Studio 中,再返回 Unity

第 7 章 - 建立 CustomVisionObjects 類別

您現在要建立的類別是 CustomVisionObjects 類別。

此腳本包含其他類別用來串行化和還原串行化對 自訂視覺 Service 所進行呼叫的一些物件。

警告

請務必記下 自訂視覺 服務所提供的端點,因為下列 JSON 結構已設定為使用 自訂視覺 Prediction v2.0。 如果您有不同的版本,您可能需要更新下列結構。

若要建立此類別:

  1. [腳本 ] 資料夾內按下滑鼠右鍵,然後按兩下 [ 建立>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. [腳本 ] 資料夾內按下滑鼠右鍵,然後按兩下 [ 建立>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. 請務必先將變更儲存在 Visual Studio 中,再返回 Unity

注意

別擔心可能會出現錯誤的程式代碼,因為您很快就會提供進一步的類別,這會修正這些類別。

第 9 章 - 建立 CustomVisionTrainer 類別

此類別會鏈結一系列的 Web 呼叫,以訓練 自訂視覺 服務。 每個呼叫都會在程式代碼上方詳細說明。

若要建立此類別:

  1. [腳本 ] 資料夾內按下滑鼠右鍵,然後按兩下 [ 建立>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 () 方法上方新增下列變數。

    注意

    此處使用的定型 URL 是在 自訂視覺 Training 1.2 檔中提供,其結構如下:https://southcentralus.api.cognitive.microsoft.com/customvision/v1.2/Training/projects/{projectId}/
    如需詳細資訊,請流覽 自訂視覺 訓練 v1.2 參考 API

    警告

    請務必記下 自訂視覺 服務提供給訓練模式的端點,因為已設定在 CustomVisionObjects 類別內 (使用的 JSON 結構,) 已設定為使用 自訂視覺 Training v1.2。 如果您有不同的版本,您可能需要更新 Objects 結構。

        /// <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;
    

    重要

    請確定您已將 服務金鑰 新增 (訓練金鑰) 值和 專案識別碼 值,您先前已注意到此值;這些是您 稍早在課程中從入口網站收集的值, (第 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 () 方法。 此方法是第一個在裝置中擷取並儲存映射時呼叫的方法,現在已準備好提交至 自訂視覺 服務進行定型。 此方法會在定型 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 () 方法。 這個方法會開始 自訂視覺 服務定型程式。 第一個步驟是從服務擷取 標記標識碼 ,此標識碼與使用者驗證的語音輸入相關聯。 接著,標籤 標識碼 會隨著影像一起上傳。

        /// <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 () 方法。 這個方法會將先前建立和定型的反覆項目設定為 Default。 完成後,此方法必須刪除服務中現有的先前反覆專案。 在撰寫本課程時,允許在服務中同時存在最多10個 (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. 在此類別中新增的最後一種方法是 GetImageAsByteArray () 方法,用於 Web 呼叫,以將擷取的影像轉換成位元組數位。

        /// <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. 請務必先將變更儲存在 Visual Studio 中,再返回 Unity

第 10 章 - 建立 SceneOrganiser 類別

此類別將會:

  • 建立 Cursor 物件以附加至主要相機。

  • 建立 Label 物件,當服務辨識真實世界物件時會出現。

  • 藉由將適當的元件附加至主要相機來設定主要相機。

  • 分析模式中,在運行時間繁衍標籤、相對於主要相機位置的適當世界空間,並顯示從 自訂視覺 服務接收的數據。

  • [定型模式] 中時,繁衍將顯示定型程式不同階段的UI。

若要建立此類別:

  1. [腳本 ] 資料夾內按下滑鼠右鍵,然後按兩下 [ 建立>C# 腳本]。 將腳本命名為 SceneOrganiser

  2. 按兩下新的 SceneOrganiser 腳稿,以使用 Visual Studio 加以開啟。

  3. 您只需要命名空間,請從 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 () 方法,此方法會建立 Analysis Label 物件。

        /// <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 () 方法新增並顯示來自 自訂視覺 服務的數據到場景中。

        /// <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. 請務必先將變更儲存在 Visual Studio 中,再返回 Unity

重要

在繼續之前,請開啟 CustomVisionAnalyser 類別,並在 分析LastImageCaptured () 方法中 取消批注 下列幾行:

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

第 11 章 - 建立 ImageCapture 類別

您要建立的下一個類別是 ImageCapture 類別。

此類別負責:

  • 使用 HoloLens 相機擷取影像,並將其儲存在 應用程式 資料夾中。

  • 處理使用者的點選手勢。

  • 維護 列舉 值,以判斷應用程式是否會在 分析 模式或 型模式中執行。

若要建立此類別:

  1. 移至您先前建立的 [腳稿 ] 資料夾。

  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;
            }     
        }
    

    注意

    分析 模式中, 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. 新增在擷取相片時所呼叫的處理程式,以及何時準備好進行分析。 結果接著會根據程式代碼設定的模式,傳遞至 CustomVisionAnalyserCustomVisionTrainer

        /// <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. 請務必先將變更儲存在 Visual Studio 中,再返回 Unity

  11. 現在所有腳本都已完成,請返回 Unity 編輯器,然後按兩下 [SceneOrganiser] 類別,從 [腳稿] 資料夾拖曳至 [階層面板] 中的 [主要相機] 物件。

第 12 章 - 建置之前

若要徹底測試您的應用程式,您必須將它側載至 HoloLens。

在您執行之前,請確定:

  • 第 2 章中所述的所有設定都會正確設定。

  • [主要相機]、[偵測器面板] 中的所有欄位都會正確指派。

  • 腳本 SceneOrganiser 會附加至 Main Camera 物件。

  • 請確定您將 預測金鑰 插入 predictionKey 變數中。

  • 您已將 預測端點 插入 predictionEndpoint 變數。

  • 您已將訓練金鑰插入 CustomVisionTrainer 類別的 trainingKey 變數中。

  • 您已將專案識別碼插入 CustomVisionTrainer 類別的 projectId 變數中。

第 13 章 - 建置和側載您的應用程式

若要開始 建置 程式:

  1. 移至 [檔案 > 建置設定]。

  2. 刻度 Unity C# 專案

  3. 按一下 [建置]。 Unity 會啟動 檔案總管 視窗,您需要在其中建立,然後選取要建置應用程式的資料夾。 立即建立該資料夾,並將它命名為 [應用程式]。 然後選取 [ 應用程式 ] 資料夾後,按兩下 [ 選取資料夾]。

  4. Unity 會開始將專案建置至 [應用程式 ] 資料夾。

  5. 一旦 Unity 完成建置 (可能需要一些時間) ,它會在組建的位置開啟 檔案總管 視窗, (檢查您的任務列,因為它可能不一定會出現在您的視窗上方,但會通知您新增視窗) 。

若要在 HoloLens 上部署:

  1. 您將需要 HoloLens (的 IP 位址以進行遠端部署) ,並確保 HoloLens 處於 開發人員模式。 作法如下:

    1. 在戴上 HoloLens 時,開啟 [ 設定]。

    2. 移至 網路 & 因特網>Wi-Fi>進階選項

    3. 請注意 IPv4 位址。

    4. 接下來,流覽回 [設定],然後流覽至 [更新開發人員的安全性 & 安全性>]

    5. 設定 [開啟開發人員模式]。

  2. 流覽至新的 Unity 組建, (應用程式 資料夾) ,並使用 Visual Studio 開啟方案檔。

  3. 在 [ 方案組態 ] 中,選取 [ 偵錯]。

  4. [解決方案平臺] 中,選取 [x86]、[遠端計算機]。 在此情況下,系統會提示您在 HoloLens (插入遠端裝置的 IP 位址 ,在此案例中,您) 。

    設定 IP 位址

  5. 移至 [建置 ] 功能表,然後按兩下 [ 部署解決方案 ] 將應用程式側載至 HoloLens。

  6. 您的應用程式現在應該會出現在 HoloLens 上安裝的應用程式清單中,並準備好啟動!

注意

若要部署至沉浸式頭戴式裝置,請將 [解決方案平臺 ] 設定為 [ 本機計算機],並將 [設定] 設定為 [ 偵錯 ],並將[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 章 - 評估並改善您的 自訂視覺 模型

若要讓您的服務更精確,您必須繼續定型用於預測的模型。 這可透過使用新的應用程式來完成,其中包含 訓練預測 模式,後者要求您造訪入口網站,也就是本章所涵蓋的內容。 準備好多次重新流覽您的入口網站,以持續改善您的模型。

  1. 再次前往 Azure 自訂視覺 入口網站,一旦您進入專案,請從頁面頂端中央選取 [預測] 索引卷標 () :

    選取預測索引標籤

  2. 您會在應用程式執行時看到傳送至服務的所有映像。 如果您將滑鼠停留在影像上,它們會為您提供針對該影像所做的預測:

    預測影像清單

  3. 選取其中一個影像加以開啟。 開啟之後,您會看到對右側該影像所做的預測。 如果您預測正確,而且您想要將此影像新增至服務的定型模型,請按兩下 [ 我的卷標 ] 輸入方塊,然後選取您想要建立關聯的標籤。 當您完成時,請按下右下角的 [ 儲存並關閉 ] 按鈕,然後繼續進行下一個影像。

    選取要開啟的影像

  4. 回到影像方格之後,您會發現已將標記新增至 (並儲存) 的影像將會遭到移除。 如果您發現任何您認為沒有標記專案的影像,您可以刪除它們,方法是單擊該影像上的刻度, (可以針對數個影像執行此動作,) 然後按兩下方格頁面右上角的 [ 刪除 ]。 在後續的彈出視窗中,您可以分別按兩下 [ 是]、[刪除 ] 或 [ ],以確認刪除或取消。

    刪除映像

  5. 當您準備好繼續時,請按下右上方的綠色 [訓練] 按鈕。 您的服務模型將會使用您現在提供的所有影像來定型, (,使其更精確) 。 定型完成後,請務必再按兩下 [ 建立預設] 按鈕,讓您的 預測URL 繼續使用您服務的最新反覆專案。

    開始定型服務模型選取預設選項

您已完成 自訂視覺 API 應用程式

恭喜,您已建置混合實境應用程式,利用 Azure 自訂視覺 API 來辨識真實世界物件、定型服務模型,以及顯示已看到內容的信賴度。

已完成的專案範例

額外練習

練習 1

訓練您的 自訂視覺 服務以辨識更多物件。

練習 2

若要擴充您學到的內容,請完成下列練習:

辨識物件時播放音效。

練習 3

使用 API 以應用程式正在分析的相同影像來重新定型您的服務,因此為了讓服務更精確, (同時進行預測和訓練) 。