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


注意

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


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

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

course outcome

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

完成本課程後,您將擁有混合實境應用程式,其可在兩種模式中運作:

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

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

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

裝置支援

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

注意

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

必要條件

注意

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

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

在您開始使用 Intune 之前

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

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

如需感應器微調的說明,請遵循此連結來HoloLens感應器微調一文

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

若要在 Azure 中使用自訂視覺服務,您必須將服務的實例設定為可供您的應用程式使用。

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

  2. 按一下[入門] 按鈕。

    Get started with Custom Vision Service

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

    Sign in to portal

    注意

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

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

    Terms of service

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

    Create new project

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

    1. 插入專案的 [名稱 ]。

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

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

    4. [Project類型] 設定為[分類]

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

      Set the domains

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

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

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

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

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

  1. 按一下 [標籤] 旁的 + 按鈕。

    Add new tag

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

    Add object name and save

  3. 您會注意到您的 標籤 已新增 (您可能需要重載頁面,使其出現在) 。 如果尚未核取核取方塊,請按一下新標籤旁的核取方塊。

    Enable new tag

  4. 按一下頁面中央的 [ 新增映射 ]。

    Add images

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

    注意

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

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

    Select tags

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

    Upload files

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

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

    Enable training iteration

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

    Make default and prediction URL

    注意

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

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

    Copy and paste URL and Prediction-Key

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

    Click on cog icon to open settings

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

    Copy training key

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

    Copy project id

第 3 章 - 設定 Unity 專案

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

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

    Create new Unity project

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

    Configure project settings

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

    Configure external tools

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

    Configure build settings

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

    1. 目標裝置設定為HoloLens

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

    2. 組建類型 設定為 D3D

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

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

    5. [建置並執行 ] 設定為 [ 本機電腦]

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

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

        Add open scene to build list

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

        Create new scene folder

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

        Name new scene file

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

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

      Default build settings

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

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

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

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

      2. 腳本後端 應該是 .NET

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

      Set API compantiblity

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

      1. InternetClient

      2. 網路攝影機

      3. 麥克風

      Configure publishing settings

    3. 此外,在XR 設定 (中,于 [發佈設定) ]、[支援虛擬實境] 底下找到,確定已新增Windows Mixed Reality SDK

    Configure XR settings

  8. 回到[建置設定Unity C# 專案] 不再呈現灰色;請勾選此旁的核取方塊。

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

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

第 4 章 - 在 Unity 中匯入 Newtonsoft DLL

重要

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

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

  1. 使用AssetsImportPackageCustomPackage >功能表選項,將 .unitypackage>新增至 Unity。

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

    Import all package items

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

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

    Select Newtonsoft plugin

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

    Configure Newtonsoft plugin

    注意

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

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

    • 核取任何平臺
    • onlyWSAPlayer已核取
    • 核取進程

    Configure Newtonsoft plugin platform settings

第 5 章 - 相機設定

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

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

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

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

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

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

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

    Configure Camera component properties

第 6 章 - 建立 CustomVisionAnalyser 類別。

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

您將從 CustomVisionAnalyser 類別開始。

注意

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

此類別負責:

  • 載入擷取為位元組陣列的最新映射。

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

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

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

若要建立此類別:

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

    Create scripts folder

  2. 按兩下剛才建立的資料夾,將其開啟。

  3. 在資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 將腳本命名為 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. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下 [CreateC># 腳本]。 呼叫 腳本 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. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下 [CreateC># 腳本]。 呼叫 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. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下 [CreateC># 腳本]。 呼叫 腳本 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;
    

    重要

    請確定您已將服務金鑰新增 (訓練金鑰) 值和Project識別碼值,如先前記下;這些值是您稍早在課程 (第 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. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下 [CreateC># 腳本]。 將腳本命名為 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 () 方法,這個方法會在應用程式處於訓練模式時,繁衍顯示訓練程式的多個階段。 此方法也會被利用以建立相機狀態物件。

        /// <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 會附加至 主要相機 物件。

  • 請務必將 預測金鑰 插入 predictionKey 變數。

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

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

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

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

若要開始 建置 程式:

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

  2. 刻度 Unity C# 專案

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

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

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

若要在HoloLens上部署:

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

    1. 在戴上您的HoloLens時,請開啟設定

    2. 移至網路 & 網際網路>Wi-FiAdvanced>選項

    3. 請注意 IPv4 位址。

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

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

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

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

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

    Set IP address

  5. 移至 [建置] 功能表,然後按一下 [部署方案] 以側載應用程式至您的HoloLens。

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

注意

若要部署至沉浸式頭戴式裝置,請將 [解決方案平臺 ] 設定為 [ 本機電腦],並將 [組 ] 設定為 [ 錯],並將 x86 設定為 [平臺]。 然後使用 [ 置] 功能表項目,選取 [ 部署解決方案],部署至本機電腦。

若要使用應用程式:

若要在訓練模式與預測模式之間切換應用程式功能,您需要更新AppMode變數,其位於ImageCapture類別內的Awake () 方法中。

        // 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 自訂視覺入口網站,一旦您進入您的專案,請從頁面頂端 (選取 [預測] 索引標籤,) :

    Select predictions tab

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

    List of prediction images

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

    Select image to open

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

    Delete images

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

    Start training service modelSelect make default option

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

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

Finished project example

額外練習

練習 1

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

練習 2

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

辨識物件時播放音效。

練習 3

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