HoloLens(1세대) 및 Azure 310: 개체 검색

참고

Mixed Reality 아카데미 자습서는 HoloLens(1세대) 및 Mixed Reality 몰입형 헤드셋을 염두에 두고 설계되었습니다. 따라서 이러한 디바이스 개발에 대한 지침을 계속 찾고 있는 개발자를 위해 이러한 자습서를 그대로 두는 것이 중요합니다. 이러한 자습서는 HoloLens 2에 사용되는 최신 도구 집합 또는 상호 작용으로 업데이트되지 않습니다. 대신 지원되는 디바이스에서 계속 작동하도록 유지 관리됩니다. HoloLens 2 위해 개발하는 방법을 보여 줄 새로운 자습서 시리즈가 미래에 게시 될 예정입니다. 이 알림은 해당 자습서가 게시될 때 해당 자습서에 대한 링크로 업데이트됩니다.


이 과정에서는 혼합 현실 애플리케이션에서 Azure Custom Vision "개체 감지" 기능을 사용하여 제공된 이미지 내에서 사용자 지정 시각적 콘텐츠 및 해당 공간 위치를 인식하는 방법을 알아봅니다.

이 서비스를 사용하면 개체 이미지를 사용하여 기계 학습 모델을 학습시킬 수 있습니다. 그런 다음 학습된 모델을 사용하여 몰입형(VR) 헤드셋을 위해 PC에 연결하는 Microsoft HoloLens 카메라 캡처 또는 카메라 연결에서 제공하는 대로 유사한 개체를 인식하고 실제 세계에서 해당 위치를 근사화합니다.

과정 결과

Azure Custom Vision 개체 감지는 개발자가 사용자 지정 이미지 분류자를 빌드할 수 있는 Microsoft 서비스입니다. 그런 다음 이러한 분류자를 새 이미지와 함께 사용하여 이미지 자체 내에서 Box 경계 를 제공하여 새 이미지 내의 개체를 검색할 수 있습니다. 이 서비스는 이 프로세스를 간소화하기 위해 간단하고 사용하기 쉬운 온라인 포털을 제공합니다. 자세한 내용은 다음 링크를 참조하세요.

이 과정을 완료하면 다음을 수행할 수 있는 혼합 현실 애플리케이션이 제공됩니다.

  1. 사용자는 Azure Custom Vision Service, 개체 검색을 사용하여 학습한 개체를 응시할 수 있습니다.
  2. 사용자는 제스처를 사용하여 보고 있는 내용의 이미지를 캡처합니다.
  3. 앱은 이미지를 Azure Custom Vision Service로 보냅니다.
  4. 인식 결과를 세계 공간 텍스트로 표시하는 서비스의 회신이 있습니다. 이는 인식된 개체의 세계 위치를 이해하는 방법으로 Microsoft HoloLens 공간 추적을 활용한 다음 이미지에서 검색된 내용과 연결된 태그를 사용하여 레이블 텍스트를 제공하는 방식으로 수행됩니다.

또한 이 과정에서는 제출하는 이미지 내에서 경계 상자를 설정하여 이미지를 수동으로 업로드하고, 태그를 만들고, 서비스에서 다른 개체(제공된 예제에서 컵)를 인식하도록 학습하는 방법을 다룹니다.

중요

앱을 만들고 사용한 후 개발자는 Azure Custom Vision Service로 다시 이동하여 서비스에서 수행한 예측을 식별하고 서비스에서 누락된 항목에 태그를 지정하고 경계 상자를 조정하여 올바른지 여부를 결정해야 합니다. 그런 다음 서비스를 다시 학습할 수 있으므로 실제 개체를 인식할 가능성이 높아집니다.

이 과정에서는 Azure Custom Vision Service, 개체 감지에서 Unity 기반 샘플 애플리케이션으로 결과를 가져오는 방법을 설명합니다. 빌드할 수 있는 사용자 지정 애플리케이션에 이러한 개념을 적용하는 것은 사용자에게 달려 있습니다.

디바이스 지원

과정 HoloLens 몰입형 헤드셋
MR 및 Azure 310: 개체 감지 ✔️

사전 요구 사항

참고

이 자습서는 Unity 및 C#에 대한 기본 경험이 있는 개발자를 위해 설계되었습니다. 또한 이 문서의 필수 구성 요소와 서면 지침은 작성 당시 테스트 및 확인된 내용을 나타냅니다(2018년 7월). 이 과정의 정보가 아래에 나열된 것보다 최신 소프트웨어에서 찾을 수 있는 것과 완벽하게 일치한다고 가정해서는 안 되지만 , 도구 설치 문서에 나열된 대로 최신 소프트웨어를 자유롭게 사용할 수 있습니다.

이 과정에서는 다음 하드웨어 및 소프트웨어를 사용하는 것이 좋습니다.

시작하기 전에

  1. 이 프로젝트를 빌드하는 데 문제가 발생하지 않도록 하려면 이 자습서에 언급된 프로젝트를 루트 또는 루트에 가까운 폴더에 만드는 것이 좋습니다(긴 폴더 경로는 빌드 시 문제를 일으킬 수 있습니다).
  2. HoloLens를 설정하고 테스트합니다. 이에 대한 지원이 필요한 경우 HoloLens 설정 문서를 참조하세요.
  3. 새 HoloLens 앱 개발을 시작할 때 보정 및 센서 튜닝을 수행하는 것이 좋습니다(때로는 각 사용자에 대해 이러한 작업을 수행하는 데 도움이 될 수 있음).

보정에 대한 도움말은 HoloLens 보정 문서에 대한 링크를 따르세요.

센서 튜닝에 대한 도움말은 HoloLens 센서 튜닝 문서에 대한 링크를 따르세요.

챕터 1 - Custom Vision 포털

Azure Custom Vision Service를 사용하려면 애플리케이션에서 사용할 수 있도록 instance 구성해야 합니다.

  1. Custom Vision Service 기본 페이지로 이동합니다.

  2. 시작 클릭합니다.

    시작 단추를 강조 표시하는 스크린샷

  3. Custom Vision 포털에 로그인합니다.

    로그인 단추를 보여 주는 스크린샷

  4. Azure 계정이 아직 없는 경우 계정을 만들어야 합니다. 교실 또는 랩 상황에서 이 자습서를 따르는 경우 강사 또는 감독관 중 한 명에게 새 계정 설정에 대한 도움을 요청하세요.

  5. 처음으로 로그인하면 서비스 약관 패널이 표시됩니다. 확인란을 클릭하여 약관에 동의합니다. 그런 다음 동의함 을 클릭합니다.

    서비스 약관 패널을 보여 주는 스크린샷

  6. 조건에 동의했으므로 이제 내 프로젝트 섹션에 있습니다. 새 프로젝트를 클릭합니다.

    새 프로젝트를 선택할 위치를 보여 주는 스크린샷

  7. 탭이 오른쪽에 표시되며 프로젝트에 대한 일부 필드를 지정하라는 메시지가 표시됩니다.

    1. 프로젝트의 이름 삽입

    2. 프로젝트에 대한 설명 삽입(선택 사항)

    3. 리소스 그룹을 선택하거나 새 리소스 그룹을 만듭니다. 리소스 그룹은 Azure 자산 컬렉션에 대한 액세스를 모니터링, 제어, 프로비전 및 관리하는 방법을 제공합니다. 단일 프로젝트(예: 이러한 과정)와 연결된 모든 Azure 서비스를 공통 리소스 그룹 아래에 유지하는 것이 좋습니다.

      새 프로젝트에 대한 세부 정보를 추가할 위치를 보여 주는 스크린샷

    4. 프로젝트 형식개체 감지(미리 보기)로 설정합니다.

  8. 완료되면 프로젝트 만들기를 클릭하면 Custom Vision Service 프로젝트 페이지로 리디렉션됩니다.

2장 - Custom Vision 프로젝트 교육

Custom Vision Portal에서 주요 목표는 이미지의 특정 개체를 인식하도록 프로젝트를 학습시키는 것입니다.

애플리케이션에서 인식할 각 개체에 대해 15개 이상의 이미지가 필요합니다. 이 과정과 함께 제공되는 이미지(일련의 컵)를 사용할 수 있습니다.

Custom Vision 프로젝트를 학습하려면 다음을 수행합니다.

  1. 태그 옆에 있는 + 단추를 클릭합니다.

    태그 옆에 있는 + 단추를 보여 주는 스크린샷

  2. 이미지를 연결하는 데 사용할 태그 의 이름을 추가합니다. 이 예제에서는 인식을 위해 컵 이미지를 사용하므로 이 Cup에 대한 태그의 이름을 지정했습니다. 완료되면 저장 을 클릭합니다.

    태그의 이름을 추가할 위치를 보여 주는 스크린샷

  3. 태그가 추가되었음을 알 수 있습니다(표시하려면 페이지를 다시 로드해야 할 수 있음).

    태그가 추가되는 위치를 보여 주는 스크린샷

  4. 페이지 가운데에서 이미지 추가 를 클릭합니다.

    이미지를 추가할 위치를 보여 주는 스크린샷

  5. 로컬 파일 찾아보기를 클릭하고 한 개체에 대해 업로드하려는 이미지를 찾습니다. 최소값은 15개(15)입니다.

    업로드할 이미지를 한 번에 여러 개 선택할 수 있습니다.

    업로드할 수 있는 이미지를 보여 주는 스크린샷

  6. 프로젝트를 학습시킬 모든 이미지를 선택한 후 파일 업로드 를 누릅니다. 파일 업로드가 시작됩니다. 업로드를 확인했으면 완료를 클릭합니다.

    업로드된 이미지의 진행률을 보여 주는 스크린샷

  7. 이 시점에서 이미지는 업로드되지만 태그는 지정되지 않습니다.

    태그가 지정되지 않은 이미지를 보여 주는 스크린샷

  8. 이미지에 태그를 지정하려면 마우스를 사용합니다. 이미지 위로 마우스를 가져가면 선택 영역 강조 표시가 개체 주위에 선택 영역을 자동으로 그리는 데 도움이 됩니다. 정확하지 않으면 직접 그릴 수 있습니다. 마우스 왼쪽 단추를 누른 채 선택 영역을 끌어 개체를 포괄하는 작업을 수행합니다.

    이미지에 태그를 지정하는 방법을 보여 주는 스크린샷

  9. 이미지 내에서 개체를 선택한 후 작은 프롬프트에서 지역 태그 추가를 요청합니다. 이전에 만든 태그(위의 예제에서 'Cup')를 선택하거나 태그를 더 추가하는 경우 에 를 입력하고 + (더하기) 단추를 클릭합니다.

    이미지에 추가한 태그를 보여 주는 스크린샷

  10. 다음 이미지에 태그를 지정하려면 블레이드 오른쪽에 있는 화살표를 클릭하거나 블레이드의 오른쪽 위 모서리에 있는 X 를 클릭하여 태그 블레이드를 닫은 다음 다음 이미지를 클릭할 수 있습니다. 다음 이미지가 준비되면 동일한 절차를 반복합니다. 모든 이미지에 태그가 지정될 때까지 업로드한 모든 이미지에 대해 이 작업을 수행합니다.

    참고

    아래 이미지와 같이 동일한 이미지에서 여러 개체를 선택할 수 있습니다.

    이미지의 여러 개체를 보여 주는 스크린샷

  11. 모두 태그를 지정했으면 화면 왼쪽에 있는 태그가 지정된 단추를 클릭하여 태그가 지정된 이미지를 표시합니다.

    태그가 지정된 단추를 강조 표시하는 스크린샷

  12. 이제 서비스를 학습시킬 준비가 되었습니다. 학습 단추를 클릭하면 첫 번째 학습 반복이 시작됩니다.

    학습 단추를 강조 표시하는 스크린샷.

    첫 번째 학습 반복을 보여 주는 스크린샷

  13. 빌드되면 기본값 만들기예측 URL이라는 두 개의 단추를 볼 수 있습니다. 기본 값으로 지정 을 먼저 클릭한 다음 예측 URL을 클릭합니다.

    기본값으로 설정 단추를 강조 표시하는 스크린샷.

    참고

    이 에서 제공되는 엔드포인트는 기본값으로 표시된 반복 으로 설정됩니다. 따라서 나중에 새 반복 을 만들고 기본값으로 업데이트하는 경우 코드를 변경할 필요가 없습니다.

  14. 예측 URL을 클릭한 후 메모장을 열고 URL(예측 엔드포인트라고도 함) 및 서비스 예측 키를 복사하여 붙여넣어 코드의 뒷부분에서 필요할 때 검색할 수 있습니다.

    예측 엔드포인트 및 사전 입력 키를 보여 주는 스크린샷.

3장 - Unity 프로젝트 설정

다음은 혼합 현실로 개발하기 위한 일반적인 설정이며, 따라서 다른 프로젝트에 적합한 템플릿입니다.

  1. Unity를 열고 새로 만들기를 클릭합니다.

    새로 만들기 단추를 강조 표시하는 스크린샷

  2. 이제 Unity 프로젝트 이름을 제공해야 합니다. CustomVisionObjDetection을 삽입합니다. 프로젝트 형식이 3D로 설정되어 있는지 확인하고 위치를 적절한 위치로 설정합니다(루트 디렉터리에 가까울수록 좋습니다). 그런 다음 프로젝트 만들기를 클릭합니다.

    프로젝트 세부 정보 및 프로젝트 만들기를 선택할 위치를 보여 주는 스크린샷

  3. Unity를 열면 기본 스크립트 편집 기가 Visual Studio로 설정되어 있는지 확인할 필요가 있습니다. 기본 설정편집>으로 이동한 다음 새 창에서 외부 도구로 이동합니다. 외부 스크립트 편집기를 Visual Studio로 변경합니다. 기본 설정 창을 닫습니다.

    외부 스크립트 편집기를 Visual Studio로 변경할 위치를 보여 주는 스크린샷

  4. 다음으로 파일 > 빌드 설정으로 이동하여 플랫폼을유니버설 Windows 플랫폼 전환한 다음 플랫폼 전환 단추를 클릭합니다.

    플랫폼 전환 단추를 강조 표시하는 스크린샷.

  5. 동일한 빌드 설정 창에서 다음이 설정되었는지 확인합니다.

    1. 대상 디바이스HoloLens로 설정됩니다.

    2. 빌드 유형D3D로 설정됨

    3. SDK최신 설치됨으로 설정됨

    4. Visual Studio 버전최신 설치됨으로 설정됨

    5. 빌드 및 실행로컬 컴퓨터로 설정됩니다.

    6. 빌드 설정의 나머지 설정은 현재 기본값으로 남아 있어야 합니다.

      빌드 설정 구성 옵션을 보여 주는 스크린샷

  6. 동일한 빌드 설정 창에서 플레이어 설정 단추를 클릭하면 Inspector 가 있는 공간에서 관련 패널이 열립니다.

  7. 이 패널에서 몇 가지 설정을 확인해야 합니다.

    1. 기타 설정 탭에서 다음을 수행합니다.

      1. 런타임 버전 스크립팅 은 편집기를 다시 시작해야 하는 실험적 버전(.NET 4.6 등가)이어야 합니다.

      2. 백 엔드 스크립팅.NET이어야 합니다.

      3. API 호환성 수준은.NET 4.6이어야 합니다.

        .NET 4.6으로 설정된 API 호환성 수준 옵션을 보여 주는 스크린샷

    2. 게시 설정 탭의 기능에서 다음을 검사.

      1. InternetClient

      2. 웹캠

      3. SpatialPerception

        기능 구성 옵션의 상위 절반을 보여 주는 스크린샷.기능 구성 옵션의 하반부를 보여 주는 스크린샷

    3. 패널 아래쪽의 XR 설정(게시 설정 아래에 있음)에서 Virtual Reality Supported를 선택한 다음 Windows Mixed Reality SDK가 추가되었는지 확인합니다.

      Windows Mixed Reality SDK가 추가되었음을 보여 주는 스크린샷

  8. 빌드 설정으로 돌아가면 Unity C# 프로젝트가 더 이상 회색으로 표시되지 않습니다. 옆의 확인란을 선택합니다.

  9. 빌드 설정 창을 닫습니다.

  10. 편집기에서프로젝트 설정> 그래픽 편집> 클릭합니다.

    선택한 그래픽 메뉴 옵션을 보여 주는 스크린샷.

  11. 검사기 패널에서 그래픽 설정이 열립니다. 항상 셰이더 포함이라는 배열이 표시될 때까지 아래로 스크롤합니다. Size 변수를 하나씩 늘려 슬롯을 추가 합니다 (이 예제에서는 8이므로 9로 설정). 아래와 같이 배열의 마지막 위치에 새 슬롯이 표시됩니다.

    항상 포함된 셰이더 배열을 강조 표시하는 스크린샷.

  12. 슬롯에서 슬롯 옆에 있는 작은 대상 원을 클릭하여 셰이더 목록을 엽니다. 레거시 셰이더/투명/확산 셰이더를 찾아 두 번 클릭합니다.

    레거시 셰이더/투명/확산 셰이더를 강조 표시하는 스크린샷

4장 - CustomVisionObjDetection Unity 패키지 가져오기

이 과정에서는 Azure-MR-310.unitypackage라는 Unity 자산 패키지가 제공됩니다.

[TIP] 전체 장면을 포함하여 Unity에서 지원하는 모든 개체는 .unitypackage 파일로 패키지하고 다른 프로젝트에서 내보내고 가져올 수 있습니다. 서로 다른 Unity 프로젝트 간에 자산을 이동하는 가장 안전하고 효율적인 방법입니다.

여기에서 다운로드해야 하는 Azure-MR-310 패키지를 찾을 수 있습니다.

  1. Unity dashboard 앞에 있는 화면 맨 위에 있는 메뉴에서 자산을 클릭한 다음 패키지 사용자 지정 패키지 > 가져오기를 클릭합니다.

    사용자 지정 패키지 메뉴 옵션을 강조 표시하는 스크린샷

  2. 파일 선택기를 사용하여 Azure-MR-310.unitypackage 패키지를 선택하고 열기를 클릭합니다. 이 자산의 구성 요소 목록이 표시됩니다. 가져오기 단추를 클릭하여 가져오기 를 확인합니다.

    가져올 자산 구성 요소 목록을 보여 주는 스크린샷

  3. 가져오기가 완료되면 패키지의 폴더가 이제 Assets 폴더에 추가된 것을 알 수 있습니다. 이러한 종류의 폴더 구조는 Unity 프로젝트에 일반적입니다.

    Assets 폴더의 내용을 보여 주는 스크린샷

    1. Material 폴더에는 응시 커서에서 사용하는 재질이 포함되어 있습니다.

    2. Plugins 폴더에는 코드에서 서비스 웹 응답을 역직렬화하는 데 사용하는 Newtonsoft DLL이 포함되어 있습니다. 폴더와 하위 폴더에 포함된 두 가지 버전과 하위 폴더는 Unity 편집기와 UWP 빌드 모두에서 라이브러리를 사용하고 빌드할 수 있도록 하는 데 필요합니다.

    3. Prefabs 폴더에는 장면에 포함된 프리팹이 포함되어 있습니다. 해당 항목은 다음과 같습니다.

      1. 애플리케이션에 사용되는 커서인 GazeCursor입니다. 실제 개체 위에 장면에 배치할 수 있도록 SpatialMapping 프리팹과 함께 작동합니다.
      2. 필요한 경우 장면에서 개체 태그를 표시하는 데 사용되는 UI 개체인 Label입니다.
      3. SpatialMapping은 애플리케이션이 Microsoft HoloLens 공간 추적을 사용하여 가상 맵을 만들 수 있도록 하는 개체입니다.
    4. 현재 이 과정의 미리 빌드된 장면이 포함된 Scenes 폴더입니다.

  4. 프로젝트 패널에서 Scenes 폴더를 열고 ObjDetectionScene을 두 번 클릭하여 이 과정에 사용할 장면을 로드합니다.

    Scenes 폴더의 ObjDetectionScene을 보여 주는 스크린샷

    참고

    코드가 포함되지 않습니다. 이 과정을 따라 코드를 작성합니다.

5장 - CustomVisionAnalyser 클래스를 만듭니다.

이 시점에서 코드를 작성할 준비가 된 것입니다. CustomVisionAnalyser 클래스로 시작합니다.

참고

아래 표시된 코드에서 만든 Custom Vision 서비스에 대한 호출은 Custom Vision REST API를 사용하여 수행됩니다. 이 API를 사용하여 이 API를 구현하고 사용하는 방법을 확인할 수 있습니다(비슷한 항목을 직접 구현하는 방법을 이해하는 데 유용함). Microsoft는 서비스를 호출하는 데 사용할 수 있는 Custom Vision SDK를 제공합니다. 자세한 내용은 Custom Vision SDK 문서를 참조하세요.

이 클래스는 다음을 담당합니다.

  • 바이트 배열로 캡처된 최신 이미지 로드

  • 분석을 위해 바이트 배열을 Azure Custom Vision Service instance 보냅니다.

  • 응답을 JSON 문자열로 수신합니다.

  • 응답을 역직렬화하고 결과 예측을SceneOrganiser 클래스에 전달합니다. 이 클래스는 응답을 표시하는 방법을 처리합니다.

이 클래스를 만들려면 다음을 수행합니다.

  1. 프로젝트 패널에 있는 자산 폴더를 마우스 오른쪽 단추로 클릭한 다음폴더만들기>를 클릭합니다. 스크립트 폴더 를 호출합니다.

    Scripts 폴더를 만드는 방법을 보여 주는 스크린샷

  2. 새로 만든 폴더를 두 번 클릭하여 엽니다.

  3. 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음C# 스크립트만들기>를 클릭합니다. 스크립트 이름을 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;
    

    참고

    service Prediction-KeypredictionKey 변수에 삽입하고 Prediction-EndpointpredictionEndpoint 변수에 삽입해야 합니다. 앞서 2장, 14단계에서 메모장에 복사했습니다.

  7. 이제 Instance 변수를 초기화하려면 Awake() 에 대한 코드를 추가해야 합니다.

        /// <summary>
        /// Initializes this class
        /// </summary>
        private void Awake()
        {
            // Allows this instance to behave like a singleton
            Instance = this;
        }
    
  8. 아래에 정적 GetImageAsByteArray() 메서드를 사용하여 코루틴을 추가합니다. 이 메서드는 ImageCapture 클래스에서 캡처한 이미지 분석 결과를 가져옵니다.

    참고

    AnalyseImageCapture 코루틴에는 아직 만들지 않은 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 클래스입니다 .

이 스크립트에는 다른 클래스에서 Custom Vision 서비스에 대한 호출을 직렬화하고 역직렬화하는 데 사용하는 여러 개체가 포함되어 있습니다.

이 클래스를 만들려면 다음을 수행합니다.

  1. Scripts 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음C# 스크립트만들기>를 클릭합니다. CustomVisionObjects 스크립트를 호출합니다.

  2. CustomVisionObjects 스크립트를 두 번 클릭하여 Visual Studio에서 엽니다.

  3. 파일 맨 위에 다음 네임스페이스가 참조되어 있는지 확인합니다.

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Networking;
    
  4. CustomVisionObjects 클래스 내에서 Start()Update() 메서드를 삭제합니다. 이제 이 클래스는 비어 있어야 합니다.

    경고

    다음 지침을 주의 깊게 따르는 것이 중요합니다. 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. Scripts 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음C# 스크립트만들기>를 클릭합니다. SpatialMapping 스크립트를 호출합니다.

  2. SpatialMapping 스크립트를 두 번 클릭하여 Visual Studio에서 엽니다.

  3. SpatialMapping 클래스 위에 다음 네임스페이스가 참조되어 있는지 확인합니다.

    using UnityEngine;
    using UnityEngine.XR.WSA;
    
  4. 그런 다음, Start() 메서드 위에 SpatialMapping 클래스 내에 다음 변수를 추가합니다.

        /// <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. Scripts 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음C# 스크립트만들기>를 클릭합니다. 스크립트 GazeCursor 호출

  2. GazeCursor 스크립트를 두 번 클릭하여 Visual Studio에서 엽니다.

  3. GazeCursor 클래스 위에 다음 네임스페이스가 참조되어 있는지 확인합니다.

    using UnityEngine;
    
  4. 그런 다음 Start() 메서드 위에 GazeCursor 클래스 내에 다음 변수를 추가합니다.

        /// <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. Scripts 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음C# 스크립트만들기>를 클릭합니다. 스크립트 이름을 SceneOrganiser로 지정합니다.

  2. SceneOrganiser 스크립트를 두 번 클릭하여 Visual Studio에서 엽니다.

  3. SceneOrganiser 클래스 위에 다음 네임스페이스가 참조되어 있는지 확인합니다.

    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    
  4. 그런 다음, Start() 메서드 위에 SceneOrganiser 클래스 내에 다음 변수를 추가합니다.

        /// <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() 메서드를 추가합니다. IISConfigurator는 다음을 담당합니다.

    • 신뢰도가 가장 높은 예측의 태그를 사용하여 레이블 텍스트를 설정합니다.
    • 이전에 배치된 쿼드 개체에서 경계 상자 의 계산을 호출하고 장면에 레이블을 배치합니다.
    • 경계 상자를 향해 레이캐스트를 사용하여 레이블 깊이 조정합니다. 이는 실제 세계의 개체와 충돌해야 합니다.
    • 사용자가 다른 이미지를 캡처할 수 있도록 캡처 프로세스를 다시 설정합니다.
        /// <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. Unity로 돌아가기 전에 Visual Studio에서 변경 내용을 저장해야 합니다.

    중요

    계속하기 전에 CustomVisionAnalyser 클래스를 열고 , AnalyticLastImageCaptured() 메서드 내에서 다음 줄의 주석 처리를 제거 합니다.

    // 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. 폴더 내부를 마우스 오른쪽 단추로 클릭한 다음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. 그런 다음, Start() 메서드 위에 ImageCapture 클래스 내에 다음 변수를 추가합니다.

        /// <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. Unity로 돌아가기 전에 Visual Studio에서 변경 내용을 저장해야 합니다.

11장 - 장면에서 스크립트 설정

이제 이 프로젝트에 필요한 모든 코드를 작성했으므로 스크립트가 올바르게 작동하도록 장면 및 프리팹에서 스크립트를 설정해야 합니다.

  1. Unity 편집기 내의 계층 패널에서 기본 카메라를 선택합니다.

  2. 검사기 패널에서 주 카메라를 선택한 상태에서 구성 요소 추가를 클릭한 다음 SceneOrganiser 스크립트를 검색하고 두 번 클릭하여 추가합니다.

    SceneOrganizer 스크립트를 보여 주는 스크린샷

  3. 프로젝트 패널에서 Prefabs 폴더를 열고, 아래 이미지와 같이 기본 카메라에 방금 추가한 SceneOrganiser 스크립트의 레이블 빈 참조 대상 입력 영역으로 레이블 프리팹을 끕니다.

    주 카메라에 추가한 스크립트를 보여 주는 스크린샷.

  4. 계층 패널에서 주 카메라GazeCursor 자식 을 선택합니다.

  5. 검사기 패널에서 GazeCursor를 선택한 상태에서 구성 요소 추가를 클릭한 다음 GazeCursor 스크립트를 검색하고 두 번 클릭하여 추가합니다.

    GazeCursor 스크립트를 추가하는 위치를 보여 주는 스크린샷

  6. 다시 계층 구조 패널에서 주 카메라SpatialMapping 자식 을 선택합니다.

  7. 검사기 패널에서 SpatialMapping이 선택된 상태에서 구성 요소 추가를 클릭한 다음 SpatialMapping 스크립트를 검색하고 두 번 클릭하여 추가합니다.

    SpatialMapping 스크립트를 추가하는 위치를 보여 주는 스크린샷

설정하지 않은 나머지 스크립트는 런타임 중에 SceneOrganiser 스크립트의 코드에 의해 추가됩니다.

챕터 12 - 빌드 전

애플리케이션에 대한 철저한 테스트를 수행하려면 Microsoft HoloLens 테스트용으로 로드해야 합니다.

이렇게 하기 전에 다음을 확인합니다.

  • 챕터 3에 언급된 모든 설정이 올바르게 설정됩니다.

  • SceneOrganiser 스크립트는 Main Camera 개체에 연결됩니다.

  • GazeCursor 스크립트는 GazeCursor 개체에 연결됩니다.

  • SpatialMapping 스크립트는 SpatialMapping 개체에 연결됩니다.

  • 5장, 6단계:

    • 서비스 예측 키를predictionKey 변수에 삽입해야 합니다.
    • predictionEndpoint 클래스에 예측 엔드포인트를 삽입했습니다.

13장 - UWP 솔루션 빌드 및 애플리케이션 테스트용 로드

이제 Microsoft HoloLens 배포할 수 있는 UWP 솔루션으로 애플리케이션을 빌드할 준비가 되었습니다. 빌드 프로세스를 시작하려면 다음을 수행합니다.

  1. 파일 > 빌드 설정으로 이동합니다.

  2. Unity C# 프로젝트를 선택합니다.

  3. 열린 장면 추가를 클릭합니다. 그러면 현재 열려 있는 장면이 빌드에 추가됩니다.

    열린 장면 추가 단추를 강조 표시하는 스크린샷.

  4. 빌드를 클릭한 다음 Unity는 파일 탐색기 창을 시작합니다. 여기서 앱을 빌드할 폴더를 만들어야 합니다. 이제 해당 폴더를 만들고 이름을 앱으로 지정 합니다. 그런 다음 폴더를 선택한 상태에서 폴더 선택을 클릭합니다.

  5. Unity는 App 폴더에 프로젝트 빌드를 시작합니다.

  6. Unity 빌드가 완료되면(다소 시간이 걸릴 수 있음) 빌드 위치에서 파일 탐색기 창이 열립니다(작업 표시줄을 검사 항상 창 위에 표시되지는 않지만 새 창이 추가되었음을 알려 줍니다).

  7. Microsoft HoloLens 배포하려면 해당 디바이스의 IP 주소(원격 배포의 경우)가 필요하며 개발자 모드도 설정되어 있는지 확인해야 합니다. 가상 하드 디스크 파일에 대한 중요 정보를 제공하려면

    1. HoloLens를 착용하는 동안 설정을 엽니다.

    2. 네트워크 & 인터넷>Wi-Fi>고급 옵션으로 이동합니다.

    3. IPv4 주소를 기록해 둡니다.

    4. 다음으로 설정으로 다시 이동한 다음개발자를 위한& 보안> 업데이트로 이동합니다.

    5. 개발자 모드를 설정합니다.

  8. 새 Unity 빌드( App 폴더)로 이동하여 Visual Studio를 사용하여 솔루션 파일을 엽니다.

  9. 솔루션 구성에서 디버그를 선택합니다.

  10. 솔루션 플랫폼에서 x86, 원격 머신을 선택합니다. 원격 디바이스의 IP 주소(이 경우 기록한 Microsoft HoloLens)를 삽입하라는 메시지가 표시됩니다.

    IP 주소를 삽입할 위치를 보여 주는 스크린샷

  11. 빌드 메뉴로 이동하여 솔루션 배포를 클릭하여 HoloLens에 애플리케이션을 테스트용으로 로드합니다.

  12. 이제 앱이 Microsoft HoloLens 설치된 앱 목록에 표시되고 시작할 준비가 되었습니다.

애플리케이션을 사용하려면 다음을 수행합니다.

  • Azure Custom Vision Service, 개체 감지를 사용하여 학습한 개체를 살펴보고 탭 제스처를 사용합니다.
  • 개체가 성공적으로 검색되면 태그 이름과 함께 세계 공간 레이블 텍스트 가 표시됩니다.

중요

사진을 캡처하여 서비스로 보낼 때마다 서비스 페이지로 돌아가서 새로 캡처된 이미지로 서비스를 다시 학습할 수 있습니다. 처음에는 경계 상자를 보다 정확하게 수정하고 서비스를 다시 학습해야 할 수도 있습니다.

참고

배치된 레이블 텍스트는 Microsoft HoloLens 센서 및/또는 Unity의 SpatialTrackingComponent가 실제 개체를 기준으로 적절한 충돌체를 배치하지 못하는 경우 개체 근처에 나타나지 않을 수 있습니다. 이 경우 다른 화면에서 애플리케이션을 사용해 보세요.

Custom Vision, 개체 감지 애플리케이션

축하합니다. 이미지에서 개체를 인식한 다음 3D 공간에서 해당 개체에 대한 대략적인 위치를 제공할 수 있는 Azure Custom Vision 개체 검색 API를 활용하는 혼합 현실 앱을 빌드했습니다.

Azure Custom Vision 개체 감지 API를 활용하는 혼합 현실 앱을 보여 주는 스크린샷

보너스 연습

연습 1

텍스트 레이블에 를 추가하여 반투명 큐브를 사용하여 실제 개체를 3D 경계 상자로 래핑합니다.

연습 2

Custom Vision Service를 학습하여 더 많은 개체를 인식합니다.

연습 3

개체가 인식될 때 소리를 재생합니다.

연습 4

API를 사용하여 앱이 분석하는 것과 동일한 이미지로 서비스를 다시 학습하여 서비스를 보다 정확하게 만듭니다(예측과 학습을 동시에 수행).