共用方式為


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 月)。 您可以自由使用最新的軟體,如安裝工具文章中所列,不過不應該假設本課程中的資訊會完全符合您在較新的軟體中找到的內容,而不是下面所列的內容。

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

在您開始使用 Intune 之前

  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. 麥克風

      設定發佈設定

    3. 在面板的下方,在 [XR 設定] 中,勾選 [支援虛擬現實] 中的 [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 (十六進位代碼: #00000000000) (忽略此專案作為沉浸式頭戴式裝置)。

    設定相機元件屬性

第 6 章 - 建立 CustomVisionAnalyser 類別。

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

您將從 CustomVisionAnalyser 類別開始

注意

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

此類別負責:

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

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

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

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

若要建立此類別:

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

    建立 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;
    

    注意

    請務必將預測密鑰插入 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 類別所擷取影像分析的結果。

    注意

    分析ImageCapture 協同程式中,您尚未建立的 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

    警告

    請務必記下 自訂視覺 服務提供定型模式的端點,因為已設定使用 JSON 結構(在 CustomVisionObjects 類別內)來處理 自訂視覺 定型 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() 方法來建立和放置主相機游標,以及建立 Analysis Label 物件的 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() 方法,此方法會將 自訂視覺 服務中的數據繁衍並顯示到場景中。

        /// <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. 移至您先前建立的 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;
            }     
        }
    

    注意

    分析 模式中 ,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 將會開始將您的專案建置至 App 資料夾。

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

若要在 HoloLens 上部署:

  1. 您需要 HoloLens 的 IP 位址(適用於遠端部署),並確保 HoloLens 處於 開發人員模式。 若要這樣做:

    1. 使用 HoloLens 時,開啟 [ 設定]。

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

    3. 記下 IPv4 位址。

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

    5. 將開發人員模式設定 為開啟

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

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

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

    設定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;

訓練 模式中:

  • 檢視滑鼠或鍵盤,並使用點選手勢

  • 接下來,會出現文字,要求您提供標籤。

  • 說出 滑鼠鍵盤

在預測模式中

  • 查看物件並使用 Tap 手勢

  • 文字會顯示為偵測到的物件,且機率最高(這是正規化)。

第 14 章 - 評估和改善您的 自訂視覺 模型

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

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

    選取預測索引標籤

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

    預測影像清單

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

    選取要開啟的影像

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

    刪除映像

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

    開始訓練服務模型選取 [設為預設] 選項

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

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

已完成的專案範例

額外練習

練習 1

將 自訂視覺 服務定型,以辨識更多物件。

練習 2

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

辨識物件時播放音效。

練習 3

使用 API 以應用程式正在分析的相同影像重新定型服務,讓服務更精確(同時執行預測和定型)。