HoloLens (第 1 代) 和 Azure 310:物件偵測

注意

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


在此課程中,您將瞭解如何使用混合實境應用程式中的 Azure 自訂視覺「物件偵測」功能,辨識自訂視覺內容及其空間位置。

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

course outcome

Azure 自訂視覺,物件偵測是 Microsoft 服務,可讓開發人員建置自訂映射分類器。 然後,這些分類器可以搭配新的影像使用,藉由在影像本身內提供 Box 界限 來偵測該新影像內的物件。 此服務提供簡單、容易使用的線上入口網站,以簡化此程式。 如需詳細資訊,請流覽下列連結:

完成本課程之後,您將擁有混合實境應用程式,其可以執行下列動作:

  1. 使用者將能夠注視他們已使用 Azure 自訂視覺服務、物件偵測定型的物件。
  2. 使用者將會使用 選手勢來擷取他們所查看專案的影像。
  3. 應用程式會將映射傳送至 Azure 自訂視覺服務。
  4. 服務會有回復,將辨識結果顯示為世界空間文字。 這可透過利用Microsoft HoloLens的空間追蹤來完成,做為瞭解已辨識物件世界位置的方法,然後使用與影像中偵測到的標記相關聯的卷標來提供標籤文字。

本課程也會涵蓋手動上傳影像、建立標記,以及訓練服務以辨識所提供範例中 (的不同物件,藉由在您所提交的影像中設定 界限方 塊來辨識不同的物件,) 杯。

重要

在建立和使用應用程式之後,開發人員應該巡覽回 Azure 自訂視覺服務,並識別服務所做的預測,並透過標記服務遺漏的任何專案來判斷它們是否正確 (,以及調整周框方塊) 。 接著,服務可以重新定型,這會增加辨識真實世界物件的可能性。

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

裝置支援

課程 HoloLens 沉浸式頭戴裝置
MR 和 Azure 310:物件偵測 ✔️

必要條件

注意

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

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

在您開始使用 Intune 之前

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

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

如需感應器微調的說明,請遵循此連結,以取得感應器微調HoloLens文章

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

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

  1. 流覽自訂視覺服務主頁面

  2. 按一下消費者入門

    Screenshot that highlights the Getting Started button.

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

    Screenshot that shows the Sign In button.

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

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

    Screenshot that shows the Terms of Service panel.

  6. 您已同意條款,您現在位於 [我的專案 ] 區段中。 按一下 [新增Project]。

    Screenshot that shows where to select New Project.

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

    1. 插入專案的名稱

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

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

      Screenshot that shows where to add details for the new project.

    4. Project類型設定為物件偵測 (預覽)

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

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

一旦在自訂視覺入口網站中,您的主要目標是將專案定型,以辨識影像中的特定物件。

您想要讓應用程式辨識的每個物件,至少需要 15 個 (15 個) 影像。 您可以使用本課程所提供的影像, (一系列的杯子) 。

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

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

    Screenshot that shows the + button next to Tags.

  2. 新增標記 的名稱 ,以用來將影像與 產生關聯。 在此範例中,我們會使用杯子影像進行辨識,因此已為此 Cup命名標記。 完成之後,按一下 [ 儲存 ]。

    Screenshot that shows where to add a name for the tag.

  3. 您會注意到您的 標籤 已新增 (您可能需要重載頁面,才能顯示) 。

    Screenshot that shows where your tag is added.

  4. 按一下頁面中央的 [ 新增影像 ]。

    Screenshot that shows where to add images.

  5. 按一下 [ 流覽本機檔案],然後流覽至您想要上傳一個物件的影像,至少為 15 (15) 。

    提示

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

    Screenshot that shows the images you can upload.

  6. 一旦您選取要訓練專案的所有影像,請按Upload檔案。 檔案將會開始上傳。 確認上傳之後,按一下 [完成]。

    Screenshot that shows the progress of the uploaded images.

  7. 此時,您的影像會上傳,但未加上標記。

    Screenshot that shows an untagged image.

  8. 若要標記您的影像,請使用滑鼠。 當您將滑鼠停留在影像上時,選取範圍醒目提示將協助您自動繪製物件周圍的選取範圍。 如果不正確,您可以自行繪製。 這可藉由按住滑鼠左鍵並拖曳選取區域來包含您的物件來完成。

    Screenshot that shows how to tag an image.

  9. 在影像中選取物件之後,小提示會要求您 新增區域標籤。 選取您先前建立的標籤 ('Cup',在上述範例中) ,或如果您要新增更多標籤,請在 中輸入 ,然後按一下 [+ (加) ] 按鈕。

    Screenshot that shows the tag that you added to the image.

  10. 若要標記下一個影像,您可以按一下刀鋒視窗右邊的箭號,或按一下刀鋒視窗右上角的 X 來關閉標籤刀鋒視窗 (,) 然後按一下下一個影像。 當您準備好下一個映射之後,請重複相同的程式。 請針對您上傳的所有影像執行此動作,直到標記它們為止。

    注意

    您可以在相同的影像中選取數個物件,如下所示:

    Screenshot that shows multiple objects in an image.

  11. 標記全部之後,請按一下畫面左側 的已標記 按鈕,以顯示已標記的影像。

    Screenshot that highlights the Tagged button.

  12. 您現在已準備好訓練您的服務。 按一下 [ 定型] 按鈕,第一個訓練反復專案將會開始。

    Screenshot that highlights the Train button.

    Screenshot that shows the first training iteration.

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

    Screenshot that highlights the Make default button.

    注意

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

  14. 按一下[預測 URL] 之後,請開啟[記事本],然後複製並貼上URL (也稱為預測端點) 和服務預測金鑰,以便稍後在程式碼中需要時加以擷取。

    Screenshot that shows the prediction endpoint and the predition key.

第 3 章 - 設定 Unity 專案

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

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

    Screenshot that highlights the New button.

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

    Screenshot that shows the project details and where to select Create project.

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

    Screenshot that shows where to change the External Script Editor to Visual Studio.

  4. 接下來,移至[檔案 > 建置] 設定,並將[平臺] 切換為[通用 Windows 平臺],然後按一下 [切換平臺] 按鈕。

    Screenshot that highlights the Switch Platform button.

  5. 在相同的[建置設定] 視窗中,確定已設定下列專案:

    1. 目標裝置設定為HoloLens

    2. 組建類型 設定為 D3D

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

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

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

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

      Screenshot that shows the Build Setting configuration options.

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

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

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

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

      2. 腳本後端 應該是 .NET

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

        Screenshot that shows the API Compatibility Level option set to .NET 4.6.

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

      1. InternetClient

      2. 網路攝影機

      3. SpatialPerception

        Screenshot that shows the top half of the Capabilities configuration options.Screenshot that shows the lower half of the Capabilities configuration options.

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

      Screenshot that shows that the Windows Mixed Reality SDK is added.

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

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

  10. 編輯器中,按一下 [編輯>Project 設定>Graphics]。

    Screenshot that shows the Graphics menu option selected.

  11. [偵測器] 面板中,將會開啟圖形設定。 向下捲動直到您看到名為 Always Include 著色器的陣列為止。 在此範例中,藉由將 Size 變數增加一個 (來新增位置,這是 8 個,因此我們將其設為 9) 。 新的位置會出現在陣列的最後一個位置,如下所示:

    Screenshot that highlights the Always Included Shaders array.

  12. 在位置中,按一下位置旁的小型目標圓形,以開啟著色器清單。 尋找 舊版著色器/透明/擴散 著色器,然後按兩下。

    Screenshot that highlights the Legacy Shaders/Transparent/Diffuse shader.

第 4 章 - 匯入 CustomVisionObjDetection Unity 套件

針對本課程,您會提供名為 Azure-MR-310.unitypackage 的 Unity 資產套件

[TIP]Unity 支援的任何物件,包括整個場景,都可以封裝成 .unitypackage 檔案,並在其他專案中匯出/匯入。 這是在不同 Unity 專案之間移動資產的最安全且最有效率的方式。

您可以 在這裡找到您需要下載的 Azure-MR-310 套件

  1. 在 Unity 儀表板前面,按一下畫面頂端功能表中的 [ 資產 ],然後按一下 [ 匯入套件 > 自訂套件]。

    Screenshot that highlights the Custom Package menu option.

  2. 使用檔案選擇器來選取 Azure-MR-310.unitypackage 套件,然後按一下 [ 開啟]。 此資產的元件清單會顯示給您。 按一下 [ 入] 按鈕以確認匯入。

    Screenshot that shows the list of asset components that you want to import.

  3. 一旦完成匯入,您就會注意到套件中的資料夾現在已新增至 您的 Assets 資料夾。 這種資料夾結構通常適用于 Unity 專案。

    Screenshot that shows the contents of the Assets folder.

    1. [材質] 資料夾包含注視游標所使用的材質。

    2. Plugins資料夾包含程式碼用來還原序列化服務 Web 回應的 Newtonsoft DLL。 這兩個 (2) 資料夾和子資料夾中所包含的不同版本,都必須允許 Unity 編輯器和 UWP 組建同時使用和建置程式庫。

    3. Prefabs資料夾包含場景中所包含的預製專案。 這些是:

      1. GazeCursor,這是應用程式中使用的資料指標。 將搭配 SpatialMapping 預製專案一起運作,以能夠在實體物件之上的場景中放置。
      2. Label,這是 UI 物件,用來在必要時在場景中顯示物件標籤。
      3. SpatialMapping,這是可讓應用程式使用Microsoft HoloLens空間追蹤建立虛擬地圖的物件。
    4. 目前包含本課程預先建置場景的 Scenes 資料夾。

  4. [Project面板] 中開啟Scenes資料夾,然後按兩下ObjDetectionScene,以載入您將用於本課程的場景。

    Screenshot that shows the ObjDetectionScene in the Scenes folder.

    注意

    未包含任何程式碼,您將遵循此課程撰寫程式碼。

第 5 章 - 建立 CustomVisionAnalyser 類別。

此時,您已準備好撰寫一些程式碼。 您將從 CustomVisionAnalyser 類別開始。

注意

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

此類別負責:

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

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

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

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

若要建立此類別:

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

    Screenshot that shows how to create the Scripts folder.

  2. 按兩下新建立的資料夾,以開啟它。

  3. 在資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 將腳本命名為 CustomVisionAnalyser。

  4. 按兩下新的CustomVisionAnalyser腳本,以使用 Visual Studio加以開啟。

  5. 請確定您在檔案頂端參考了下列命名空間:

    using Newtonsoft.Json;
    using System.Collections;
    using System.IO;
    using UnityEngine;
    using UnityEngine.Networking;
    
  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>
        /// Bite array of the image to submit for analysis
        /// </summary>
        [HideInInspector] public byte[] imageBytes;
    

    注意

    請務必將 服務預測金鑰 插入 predictionKey 變數,並將 您的 Prediction-Endpoint 插入 predictionEndpoint 變數。 您已在先前的步驟 2 的步驟 14 中,將這些複製到記事本。

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

        /// <summary>
        /// Initializes this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. 將協同程式 (新增至其下方的靜態 GetImageAsByteArray () 方法) ,這會取得 ImageCapture 類別所擷取之影像分析的結果。

    注意

    AnalysisImageCapture 協同程式中,您尚未建立 的 SceneOrganiser 類別呼叫。 因此,請保留這些行的批註。

        /// <summary>
        /// Call the Computer Vision Service to submit the image.
        /// </summary>
        public IEnumerator AnalyseLastImageCaptured(string imagePath)
        {
            Debug.Log("Analyzing...");
    
            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;
    
                Debug.Log("response: " + jsonResponse);
    
                // Create a texture. Texture size does not matter, since
                // LoadImage will replace with the incoming image size.
                //Texture2D tex = new Texture2D(1, 1);
                //tex.LoadImage(imageBytes);
                //SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex);
    
                // The response will be in JSON format, therefore it needs to be deserialized
                //AnalysisRootObject analysisRootObject = new AnalysisRootObject();
                //analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse);
    
                //SceneOrganiser.Instance.FinaliseLabel(analysisRootObject);
            }
        }
    
        /// <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);
        }
    
  9. 刪除 Start () Update () 方法,因為不會使用這些方法。

  10. 在返回Unity之前,請務必先將變更儲存在Visual Studio中。

重要

如先前所述,別擔心可能會有錯誤的程式碼,因為您很快就會提供進一步的類別,這會修正這些類別。

第 6 章 - 建立 CustomVisionObjects 類別

您將會建立的類別現在是 CustomVisionObjects 類別。

此腳本包含其他類別用來序列化和還原序列化對 自訂視覺 Service 發出的呼叫的一些物件。

若要建立此類別:

  1. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 呼叫 CustomVisionObjects 腳本。

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

  3. 請確定您在檔案頂端參考了下列命名空間:

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. 刪除CustomVisionObjects類別內的Start () Update () 方法,此類別現在應該是空的。

    警告

    請務必仔細遵循下一個指示。 如果您在 CustomVisionObjects 類別中放置新的類別宣告,您將會在 第 10 章中收到編譯錯誤,指出找不到 AnalysisRootObjectBoundingBox

  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
    /// Includes Bounding Box
    /// </summary>
    public class AnalysisRootObject
    {
        public string id { get; set; }
        public string project { get; set; }
        public string iteration { get; set; }
        public DateTime created { get; set; }
        public List<Prediction> predictions { get; set; }
    }
    
    public class BoundingBox
    {
        public double left { get; set; }
        public double top { get; set; }
        public double width { get; set; }
        public double height { get; set; }
    }
    
    public class Prediction
    {
        public double probability { get; set; }
        public string tagId { get; set; }
        public string tagName { get; set; }
        public BoundingBox boundingBox { get; set; }
    }
    
  6. 在返回Unity之前,請務必先將變更儲存在Visual Studio中。

第 7 章 - 建立 SpatialMapping 類別

這個類別會在場景中設定 空間對應碰撞器 ,以便能夠偵測虛擬物件與實際物件之間的衝突。

若要建立此類別:

  1. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 呼叫 Script SpatialMapping。

  2. 按兩下新的SpatialMapping腳本,以使用Visual Studio開啟它。

  3. 請確定您在 SpatialMapping 類別上方參考了下列命名空間:

    using UnityEngine;
    using UnityEngine.XR.WSA;
    
  4. 然後,在 SpatialMapping 類別中,于 Start () 方法上方新增下列變數:

        /// <summary>
        /// Allows this class to behave like a singleton
        /// </summary>
        public static SpatialMapping Instance;
    
        /// <summary>
        /// Used by the GazeCursor as a property with the Raycast call
        /// </summary>
        internal static int PhysicsRaycastMask;
    
        /// <summary>
        /// The layer to use for spatial mapping collisions
        /// </summary>
        internal int physicsLayer = 31;
    
        /// <summary>
        /// Creates environment colliders to work with physics
        /// </summary>
        private SpatialMappingCollider spatialMappingCollider;
    
  5. 新增 Awake () Start ()

        /// <summary>
        /// Initializes this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
        /// <summary>
        /// Runs at initialization right after Awake method
        /// </summary>
        void Start()
        {
            // Initialize and configure the collider
            spatialMappingCollider = gameObject.GetComponent<SpatialMappingCollider>();
            spatialMappingCollider.surfaceParent = this.gameObject;
            spatialMappingCollider.freezeUpdates = false;
            spatialMappingCollider.layer = physicsLayer;
    
            // define the mask
            PhysicsRaycastMask = 1 << physicsLayer;
    
            // set the object as active one
            gameObject.SetActive(true);
        }
    
  6. 刪除 Update () 方法。

  7. 在返回Unity之前,請務必先將變更儲存在Visual Studio中。

第 8 章 - 建立 GazeCursor 類別

這個類別負責使用在上一章中建立的 SpatialMappingCollider,在真實空間的正確位置設定游標。

若要建立此類別:

  1. [腳本] 資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 呼叫 腳本 GazeCursor

  2. 按兩下新的GazeCursor腳本,以使用 Visual Studio開啟它。

  3. 請確定您在 GazeCursor 類別上方參考了下列命名空間:

    using UnityEngine;
    
  4. 然後在 GazeCursor 類別中,于 Start () 方法上方新增下列變數。

        /// <summary>
        /// The cursor (this object) mesh renderer
        /// </summary>
        private MeshRenderer meshRenderer;
    
  5. 使用下列程式碼更新 Start () 方法:

        /// <summary>
        /// Runs at initialization right after the Awake method
        /// </summary>
        void Start()
        {
            // Grab the mesh renderer that is on the same object as this script.
            meshRenderer = gameObject.GetComponent<MeshRenderer>();
    
            // Set the cursor reference
            SceneOrganiser.Instance.cursor = gameObject;
            gameObject.GetComponent<Renderer>().material.color = Color.green;
    
            // If you wish to change the size of the cursor you can do so here
            gameObject.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);
        }
    
  6. 使用下列程式碼 更新 Update () 方法:

        /// <summary>
        /// Update is called once per frame
        /// </summary>
        void Update()
        {
            // Do a raycast into the world based on the user's head position and orientation.
            Vector3 headPosition = Camera.main.transform.position;
            Vector3 gazeDirection = Camera.main.transform.forward;
    
            RaycastHit gazeHitInfo;
            if (Physics.Raycast(headPosition, gazeDirection, out gazeHitInfo, 30.0f, SpatialMapping.PhysicsRaycastMask))
            {
                // If the raycast hit a hologram, display the cursor mesh.
                meshRenderer.enabled = true;
                // Move the cursor to the point where the raycast hit.
                transform.position = gazeHitInfo.point;
                // Rotate the cursor to hug the surface of the hologram.
                transform.rotation = Quaternion.FromToRotation(Vector3.up, gazeHitInfo.normal);
            }
            else
            {
                // If the raycast did not hit a hologram, hide the cursor mesh.
                meshRenderer.enabled = false;
            }
        }
    

    注意

    別擔心找不到 SceneOrganiser 類別的錯誤,您將在下一章中加以建立。

  7. 在返回Unity之前,請務必先將變更儲存在Visual Studio中。

第 9 章 - 建立 SceneOrganiser 類別

此類別將會:

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

  • 偵測到物件時,它會負責計算其在真實世界中的位置,並使用適當的標籤名稱標籤放在其附近。

若要建立此類別:

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

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

  3. 請確定您在 SceneOrganiser 類別上方參考了下列命名空間:

    using System.Collections.Generic;
    using System.Linq;
    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 Main Camera
        /// </summary>
        internal GameObject cursor;
    
        /// <summary>
        /// The label used to display the analysis on the objects in the real world
        /// </summary>
        public GameObject label;
    
        /// <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.8f;
    
        /// <summary>
        /// The quad object hosting the imposed image captured
        /// </summary>
        private GameObject quad;
    
        /// <summary>
        /// Renderer of the quad object
        /// </summary>
        internal Renderer quadRenderer;
    
  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 CustomVisionObjects class to this Gameobject
            gameObject.AddComponent<CustomVisionObjects>();
        }
    
  7. 新增 PlaceAnalysisLabel () 方法,此方法會將場景中的標籤 具現化 (此時使用者) 看不到該標籤。 它也會將四邊形 (放在影像的位置) 不可見,並與真實世界重迭。 這很重要,因為分析後從服務擷取的方塊座標會追蹤回這個四邊形,以判斷物件在真實世界中的近似位置。

        /// <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>();
            lastLabelPlacedText.text = "";
            lastLabelPlaced.transform.localScale = new Vector3(0.005f,0.005f,0.005f);
    
            // Create a GameObject to which the texture can be applied
            quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
            quadRenderer = quad.GetComponent<Renderer>() as Renderer;
            Material m = new Material(Shader.Find("Legacy Shaders/Transparent/Diffuse"));
            quadRenderer.material = m;
    
            // Here you can set the transparency of the quad. Useful for debugging
            float transparency = 0f;
            quadRenderer.material.color = new Color(1, 1, 1, transparency);
    
            // Set the position and scale of the quad depending on user position
            quad.transform.parent = transform;
            quad.transform.rotation = transform.rotation;
    
            // The quad is positioned slightly forward in font of the user
            quad.transform.localPosition = new Vector3(0.0f, 0.0f, 3.0f);
    
            // The quad scale as been set with the following value following experimentation,  
            // to allow the image on the quad to be as precisely imposed to the real world as possible
            quad.transform.localScale = new Vector3(3f, 1.65f, 1f);
            quad.transform.parent = null;
        }
    
  8. 新增 FinaliseLabel () 方法。 下列為其負責的項目:

    • 以最高信賴度設定具有預測卷標籤文字。
    • 在四邊形物件上呼叫 周框方塊 的計算,並置於先前的位置,並將標籤放在場景中。
    • 使用 Raycast 向 周框方塊調整標籤深度,這應該與真實世界中的物件碰撞。
    • 重設擷取程式,讓使用者擷取另一個映射。
        /// <summary>
        /// Set the Tags as Text of the last label created. 
        /// </summary>
        public void FinaliseLabel(AnalysisRootObject analysisObject)
        {
            if (analysisObject.predictions != null)
            {
                lastLabelPlacedText = lastLabelPlaced.GetComponent<TextMesh>();
                // Sort the predictions to locate the highest one
                List<Prediction> sortedPredictions = new List<Prediction>();
                sortedPredictions = analysisObject.predictions.OrderBy(p => p.probability).ToList();
                Prediction bestPrediction = new Prediction();
                bestPrediction = sortedPredictions[sortedPredictions.Count - 1];
    
                if (bestPrediction.probability > probabilityThreshold)
                {
                    quadRenderer = quad.GetComponent<Renderer>() as Renderer;
                    Bounds quadBounds = quadRenderer.bounds;
    
                    // Position the label as close as possible to the Bounding Box of the prediction 
                    // At this point it will not consider depth
                    lastLabelPlaced.transform.parent = quad.transform;
                    lastLabelPlaced.transform.localPosition = CalculateBoundingBoxPosition(quadBounds, bestPrediction.boundingBox);
    
                    // Set the tag text
                    lastLabelPlacedText.text = bestPrediction.tagName;
    
                    // Cast a ray from the user's head to the currently placed label, it should hit the object detected by the Service.
                    // At that point it will reposition the label where the ray HL sensor collides with the object,
                    // (using the HL spatial tracking)
                    Debug.Log("Repositioning Label");
                    Vector3 headPosition = Camera.main.transform.position;
                    RaycastHit objHitInfo;
                    Vector3 objDirection = lastLabelPlaced.position;
                    if (Physics.Raycast(headPosition, objDirection, out objHitInfo, 30.0f,   SpatialMapping.PhysicsRaycastMask))
                    {
                        lastLabelPlaced.position = objHitInfo.point;
                    }
                }
            }
            // Reset the color of the cursor
            cursor.GetComponent<Renderer>().material.color = Color.green;
    
            // Stop the analysis process
            ImageCapture.Instance.ResetImageCapture();        
        }
    
  9. 新增 CalculateBoundingBoxPosition () 方法,其裝載許多計算,以轉譯從服務擷取的 周框方塊 座標,並在四邊形上按比例重新建立這些座標。

        /// <summary>
        /// This method hosts a series of calculations to determine the position 
        /// of the Bounding Box on the quad created in the real world
        /// by using the Bounding Box received back alongside the Best Prediction
        /// </summary>
        public Vector3 CalculateBoundingBoxPosition(Bounds b, BoundingBox boundingBox)
        {
            Debug.Log($"BB: left {boundingBox.left}, top {boundingBox.top}, width {boundingBox.width}, height {boundingBox.height}");
    
            double centerFromLeft = boundingBox.left + (boundingBox.width / 2);
            double centerFromTop = boundingBox.top + (boundingBox.height / 2);
            Debug.Log($"BB CenterFromLeft {centerFromLeft}, CenterFromTop {centerFromTop}");
    
            double quadWidth = b.size.normalized.x;
            double quadHeight = b.size.normalized.y;
            Debug.Log($"Quad Width {b.size.normalized.x}, Quad Height {b.size.normalized.y}");
    
            double normalisedPos_X = (quadWidth * centerFromLeft) - (quadWidth/2);
            double normalisedPos_Y = (quadHeight * centerFromTop) - (quadHeight/2);
    
            return new Vector3((float)normalisedPos_X, (float)normalisedPos_Y, 0);
        }
    
  10. 請務必先將變更儲存在Visual Studio中,再返回Unity

    重要

    繼續之前,請先開啟 CustomVisionAnalyser 類別,然後在 [分析][LastImageCaptured] () 方法中 取消批註 下列幾行:

    // Create a texture. Texture size does not matter, since 
    // LoadImage will replace with the incoming image size.
    Texture2D tex = new Texture2D(1, 1);
    tex.LoadImage(imageBytes);
    SceneOrganiser.Instance.quadRenderer.material.SetTexture("_MainTex", tex);
    
    // The response will be in JSON format, therefore it needs to be deserialized
    AnalysisRootObject analysisRootObject = new AnalysisRootObject();
    analysisRootObject = JsonConvert.DeserializeObject<AnalysisRootObject>(jsonResponse);
    
    SceneOrganiser.Instance.FinaliseLabel(analysisRootObject);
    

注意

別擔心 ImageCapture 類別 「找不到」訊息,您將在下一章中建立它。

第 10 章 - 建立 ImageCapture 類別

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

此類別負責:

  • 使用HoloLens相機擷取影像,並將其儲存在[應用程式] 資料夾中。
  • 處理使用者的 點選 手勢。

若要建立此類別:

  1. 移至您先前建立的 Scripts 資料夾。

  2. 在資料夾內按一下滑鼠右鍵,然後按一下[CreateC># 腳本]。 將腳本命名為 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>
        /// 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;
        }
    
        /// <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 Microsoft HoloLens API gesture recognizer to track user gestures
            recognizer = new GestureRecognizer();
            recognizer.SetRecognizableGestures(GestureSettings.Tap);
            recognizer.Tapped += TapHandler;
            recognizer.StartCapturingGestures();
        }
    
  7. 實作會在點選手勢發生時呼叫的處理常式:

        /// <summary>
        /// Respond to Tap Input.
        /// </summary>
        private void TapHandler(TappedEventArgs obj)
        {
            if (!captureIsActive)
            {
                captureIsActive = true;
    
                // Set the cursor color to red
                SceneOrganiser.Instance.cursor.GetComponent<Renderer>().material.color = Color.red;
    
                // Begin the capture loop
                Invoke("ExecuteImageCaptureAndAnalysis", 0);
            }
        }
    

    重要

    當游標為 綠色時,表示相機可用來拍攝影像。 當游標為 紅色時,表示相機忙碌中。

  8. 新增應用程式用來啟動映射擷取程式並儲存映射的方法:

        /// <summary>
        /// Begin process of image capturing and send to Azure Custom Vision Service.
        /// </summary>
        private void ExecuteImageCaptureAndAnalysis()
        {
            // Create a label in world space using the ResultsLabel 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(true, delegate (PhotoCapture captureObject)
            {
                photoCaptureObject = captureObject;
    
                CameraParameters camParameters = new CameraParameters
                {
                    hologramOpacity = 1.0f,
                    cameraResolutionWidth = targetTexture.width,
                    cameraResolutionHeight = targetTexture.height,
                    pixelFormat = CapturePixelFormat.BGRA32
                };
    
                // Capture the image from the camera and save it in the App internal folder
                captureObject.StartPhotoModeAsync(camParameters, delegate (PhotoCapture.PhotoCaptureResult result)
                {
                    string filename = string.Format(@"CapturedImage{0}.jpg", captureCount);
                    filePath = Path.Combine(Application.persistentDataPath, filename);          
                    captureCount++;              
                    photoCaptureObject.TakePhotoAsync(filePath, PhotoCaptureFileOutputFormat.JPG, OnCapturedPhotoToDisk);              
                });
            });
        }
    
  9. 新增將在擷取相片時呼叫的處理常式,以及何時準備好進行分析。 結果接著會傳遞至 CustomVisionAnalyser 進行分析。

        /// <summary>
        /// Register the full execution of the Photo Capture. 
        /// </summary>
        void OnCapturedPhotoToDisk(PhotoCapture.PhotoCaptureResult result)
        {
            try
            {
                // Call StopPhotoMode once the image has successfully captured
                photoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
            }
            catch (Exception e)
            {
                Debug.LogFormat("Exception capturing photo to disk: {0}", e.Message);
            }
        }
    
        /// <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;
    
            // Call the image analysis
            StartCoroutine(CustomVisionAnalyser.Instance.AnalyseLastImageCaptured(filePath)); 
        }
    
        /// <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;
    
            // Stop the capture loop if active
            CancelInvoke();
        }
    
  10. 請務必先將變更儲存在Visual Studio中,再返回Unity

第 11 章 - 在場景中設定腳本

既然您已撰寫此專案所需的所有程式碼,就可以在場景中設定腳本,並在預製專案上設定腳本,讓它們正常運作。

  1. Unity 編輯器的 [ 階層] 面板中,選取 [主相機]。

  2. 在 [ 偵測器面板] 中,選取 [主要相機 ],按一下 [ 新增元件],然後搜尋 SceneOrganiser 腳本,然後按兩下以新增它。

    Screenshot that shows the SceneOrganizer script.

  3. [Project面板] 中,開啟Prefabs 資料夾,將 [標籤預製專案] 拖曳至 [標籤] 空白參考目標輸入區域,在您剛新增至主要相機SceneOrganiser腳本中,如下圖所示:

    Screenshot that shows the script that you added to the Main Camera.

  4. 在 [階層] 面板中,選取主相機GazeCursor子系。

  5. [偵測器] 面板中,選取 [GazeCursor ],按一下 [ 新增元件],然後搜尋 GazeCursor 腳本,然後按兩下以新增它。

    Screenshot that shows where you add the GazeCursor script.

  6. 同樣地,在[階層面板] 中,選取主相機SpatialMapping子系。

  7. [偵測器] 面板中,選取 SpatialMapping ,按一下 [ 新增元件],然後搜尋 SpatialMapping 腳本,然後按兩下以新增它。

    Screenshot that shows where you add the SpatialMapping script.

您尚未設定的其餘腳本將會在執行時間期間,由 SceneOrganiser 腳本中的程式碼新增。

第 12 章 - 建置之前

若要執行應用程式的完整測試,您必須將它側載至您的Microsoft HoloLens。

在您這麼做之前,請確定:

  • 第 3 章中所述的所有設定都已正確設定。

  • 腳本 SceneOrganiser 會附加至 主要相機 物件。

  • GazeCursor腳本會附加至GazeCursor物件。

  • SpatialMapping腳本會附加至SpatialMapping物件。

  • 在第 5 章的步驟 6:

    • 請確定您將 服務預測金鑰 插入 predictionKey 變數中。
    • 您已將 預測端點 插入 predictionEndpoint 類別。

第 13 章 - 建置 UWP 解決方案並側載您的應用程式

您現在已準備好將應用程式建置為 UWP 解決方案,您將能夠部署到Microsoft HoloLens。 若要開始建置程式:

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

  2. 刻度 Unity C# 專案

  3. 按一下 [ 新增開啟的場景]。 這會將目前開啟的場景新增至組建。

    Screenshot that highlights the Add Open Scenes button.

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

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

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

  7. 若要部署至Microsoft HoloLens,您需要該裝置的 IP 位址, (進行遠端部署) ,並確保其也已設定開發人員模式。 若要這樣做:

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

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

    3. 請注意 IPv4 位址。

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

    5. 設定開發人員 ModeOn

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

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

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

    Screenshot that shows where to insert the IP address.

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

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

若要使用應用程式:

  • 查看您已使用Azure 自訂視覺 服務、物件偵測訓練的物件,並使用點選手勢
  • 如果成功偵測到物件,則會以標籤名稱顯示世界空間卷 標文字

重要

每次擷取相片並將其傳送至服務時,您可以返回 [服務] 頁面,並使用新擷取的影像重新定型服務。 一開始,您可能也必須更正 周框方 塊,以更精確並重新定型服務。

注意

Microsoft HoloLens 當 Unity 中的感應器和/或 Unity 中的 SpatialTrackingComponent 無法放置適當的碰撞器,相對於真實世界物件時,放置的標籤文字可能不會出現在物件附近。 如果是這種情況,請嘗試在不同的介面上使用應用程式。

您的自訂視覺物件偵測應用程式

恭喜,您已建置混合實境應用程式,利用 Azure 自訂視覺、物件偵測 API,可從影像辨識物件,然後在 3D 空間中提供該物件的近似位置。

Screenshot that shows a mixed reality app that leverages the Azure Custom Vision, Object Detection API.

額外練習

練習 1

新增至文字標籤,使用半透明 Cube 將實際物件包裝在 3D 周框方塊中。

練習 2

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

練習 3

辨識物件時播放音效。

練習 4

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