📷

【XREAL Light】ピースで写真が撮れるアプリを作る【写真撮影編】

2023/12/24に公開

この記事は Panda株式会社 Advent Calendar 2023 15日目の記事です。
Panda株式会社は東京大学松尾研究室・香川高専発のスタートアップで、AR技術とAI技術を駆使したシステム開発と研究に取り組んでいます。
このアドベントカレンダーでは、スタートアップとしての知見、AI・AR技術、バックエンドなど、さまざまな領域の記事を公開していきます。

自己紹介
Panda株式会社代表取締役の田貝奈央です。ARグラスの普及に夢を見ている人です。高専の卒業研究ではXREAL Lightを使って目の前の人の顔を認証しプロフィール情報を提示する研究を行っていました。たくさんの人にXREAL Lightでのアプリ開発に取り組んでほしいと思い、この記事を執筆しました。

概要

XREAL Lightを装着しピースのジェスチャーを行うと、XREAL Lightについているカメラの映像を撮影し、ローカルに保存するシステムを作成します。ハンドジェスチャーを認識してイベントを発火させる方法と、カメラの映像を撮影しローカルに保存する方法の二回に分けて説明を行います。

  1. 【XREAL Light】ピースで写真が撮れるアプリを作る【ハンドジェスチャー編】
  2. (本記事)【XREAL Light】ピースで写真が撮れるアプリを作る【写真撮影編】

カメラの映像を撮影する

XREAL Lightの全面にはRGBカメラが付いています。このカメラを用いて、外界の状況を撮影します。
こちらの記事を参考にRGBカメラを扱います。
NRSDKのサンプルシーンの中に、RGBカメラで撮影を行うシーンが入っています。(Assets/NRSDK/Demos/RGBCamera-Capture.unity)これを参考に、カメラの映像を撮影するスクリプトを書いていきます。
前回のコードに以下内容を追加していきます。

HandGesturePhotoCapture.cs 全体
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using NRKernal;
+ using System.IO;
+ using System.Linq;
+ using NRKernal.Record;

namespace NRKernal.NRExamples{
+    #if UNITY_ANDROID && !UNITY_EDITOR
+        using GalleryDataProvider = NativeGalleryDataProvider;
+    #else
+        using GalleryDataProvider = MockGalleryDataProvider;
+    #endif
	public class HandGesturePhotoCapture : MonoBehaviour
	{
	    private float victory_count;
	    private bool takeflag;
+	    private NRPhotoCapture m_PhotoCaptureObject;
+	    private Resolution m_CameraResolution;
+	    GalleryDataProvider galleryDataTool;
	    // Start is called before the first frame update
	    void Start()
	    {
		victory_count = 0f;
		takeflag = false;
	    }

	    // Update is called once per frame
	    void Update()
	    {
		// 手のトラッキングが実行中かどうかを確認
		if (NRInput.Hands.IsRunning)
		{
		    // 右手の状態を取得
		    HandState handState = NRInput.Hands.GetHandState(HandEnum.RightHand);

		    // 右手がトラッキングされているかどうか、ピースジェスチャーが行われているかをチェック
		    if (handState.isTracked && handState.isVictory)
		    {
			if (takeflag == false){
			    if (victory_count <= 50)
			    {
				victory_count++;
			    }
			    else{
				CapturePhoto();
			    }
			}
		    }
		    else
		    {
			takeflag = false;
			victory_count = 0;
		    }
		}
	    }
+	    private void CapturePhoto()
+	    {
+		if (takeflag)
+		{
+			return;
+		}
+		takeflag = true;
+		Debug.Log("写真をとります");
+		if (m_PhotoCaptureObject == null)
+		{
+			this.Create((capture) =>
+                {
+			capture.TakePhotoAsync(OnCapturedPhotoToMemory);
+                });
+		}
+		else
+		{
+			m_PhotoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);
+		}
+	    }
+	    void Create(Action<NRPhotoCapture> onCreated)
+	    {
+		    if (m_PhotoCaptureObject != null)
+		    {
+			return;
+		    }

+		    // Create a PhotoCapture object
+		    NRPhotoCapture.CreateAsync(false, delegate (NRPhotoCapture captureObject)
+		    {
+			m_CameraResolution = NRPhotoCapture.SupportedResolutions.OrderByDescending((res) => res.width * res.height).First();

+			if (captureObject == null)
+			{
+			    return;
+			}

+			m_PhotoCaptureObject = captureObject;

+			CameraParameters cameraParameters = new CameraParameters();
+			cameraParameters.cameraResolutionWidth = m_CameraResolution.width;
+			cameraParameters.cameraResolutionHeight = m_CameraResolution.height;
+			cameraParameters.pixelFormat = CapturePixelFormat.PNG;
+			cameraParameters.frameRate = NativeConstants.RECORD_FPS_DEFAULT;
+			cameraParameters.blendMode = BlendMode.Blend;

+			// Activate the camera
+			m_PhotoCaptureObject.StartPhotoModeAsync(cameraParameters, delegate (NRPhotoCapture.PhotoCaptureResult result)
+			{
+			    if (result.success)
+			    {
+				onCreated?.Invoke(m_PhotoCaptureObject);
+			    }
+			    else
+			    {
+				this.Close();
+			    }
+			}, true);
+		    });
+		}
+		void OnCapturedPhotoToMemory(NRPhotoCapture.PhotoCaptureResult result, PhotoCaptureFrame photoCaptureFrame)
+		{
+		    var targetTexture = new Texture2D(m_CameraResolution.width, m_CameraResolution.height);
+		    // Copy the raw image data into our target texture
+		    photoCaptureFrame.UploadImageDataToTexture(targetTexture);

+		    // Create a gameobject that we can apply our texture to
+		    GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
+		    Renderer quadRenderer = quad.GetComponent<Renderer>() as Renderer;
+		    quadRenderer.material = new Material(Resources.Load<Shader>("Record/Shaders/CaptureScreen"));

+		    var headTran = NRSessionManager.Instance.NRHMDPoseTracker.centerAnchor;
+		    quad.name = "picture";
+		    quad.transform.localPosition = headTran.position + headTran.forward * 3f;
+		    quad.transform.forward = headTran.forward;
+		    quad.transform.localScale = new Vector3(1.6f, 0.9f, 0);
+		    quadRenderer.material.SetTexture("_MainTex", targetTexture);
+		    SaveTextureAsPNG(photoCaptureFrame);

+		    SaveTextureToGallery(photoCaptureFrame);
+		    // Release camera resource after capture the photo.
+		    this.Close();
+		}
+		void SaveTextureAsPNG(PhotoCaptureFrame photoCaptureFrame)
+        {
+            if (photoCaptureFrame.TextureData == null)
+                return;
+            try
+            {
+                string filename = string.Format("Xreal_Shot_{0}.png", NRTools.GetTimeStamp().ToString());
+                string path = string.Format("{0}/XrealShots", Application.persistentDataPath);
+                string filePath = string.Format("{0}/{1}", path, filename);

+                byte[] _bytes = photoCaptureFrame.TextureData;
+                NRDebugger.Info("Photo capture: {0}Kb was saved to [{1}]",  _bytes.Length / 1024, filePath);
+                if (!Directory.Exists(path))
+                {
+                    Directory.CreateDirectory(path);
+                }
+                File.WriteAllBytes(string.Format("{0}/{1}", path, filename), _bytes);

+            }
+            catch (Exception e)
+            {
+                NRDebugger.Error("Save picture faild!");
+                throw e;
+            }
+        }

+        /// <summary> Closes this object. </summary>
+        void Close()
+        {
+            if (m_PhotoCaptureObject == null)
+            {
+                NRDebugger.Error("The NRPhotoCapture has not been created.");
+                return;
+            }
+            // Deactivate our camera
+            m_PhotoCaptureObject.StopPhotoModeAsync(OnStoppedPhotoMode);
+        }

+        /// <summary> Executes the 'stopped photo mode' action. </summary>
+        /// <param name="result"> The result.</param>
+        void OnStoppedPhotoMode(NRPhotoCapture.PhotoCaptureResult result)
+        {
+            // Shutdown our photo capture resource
+            m_PhotoCaptureObject?.Dispose();
+            m_PhotoCaptureObject = null;
+        }

+        /// <summary> Executes the 'destroy' action. </summary>
+        void OnDestroy()
+        {
+            // Shutdown our photo capture resource
+            m_PhotoCaptureObject?.Dispose();
+            m_PhotoCaptureObject = null;
+        }

+        public void SaveTextureToGallery(PhotoCaptureFrame photoCaptureFrame)
+        {
+            if (photoCaptureFrame.TextureData == null)
+                return;
+            try
+            {
+                string filename = string.Format("Xreal_Shot_{0}.png", NRTools.GetTimeStamp().ToString());
+                byte[] _bytes = photoCaptureFrame.TextureData;
+                NRDebugger.Info(_bytes.Length / 1024 + "Kb was saved as: " + filename);
+                if (galleryDataTool == null)
+                {
+                    galleryDataTool = new GalleryDataProvider();
+                }

+                galleryDataTool.InsertImage(_bytes, filename, "Screenshots");
+            }
+            catch (Exception e)
+            {
+                NRDebugger.Error("[TakePicture] Save picture faild!");
+                throw e;
+            }
+         }
	}
}

少しコードの解説をします。
大きく追加したのは以下の関数です。

  • CapturePhoto
    • ユーザがピースをしたときに写真を撮影する関数
  • Create
    • 写真を撮影するためのNRPhotoCaptureオブジェクトを非同期的に作成する関数
  • OnCapturedPhotoToMemory
    • 写真撮影が終了した後に写真データを処理するための関数
  • SaveTextureAsPNG
    • 撮影された写真をPNG形式のファイルとして保存する関数
  • Close
    • 撮影プロセスを終了し、使用中のリソースを適切に開放する関数
  • OnStoppedPhotoMode
    • 写真撮影モードを停止する際に呼び出されるコールバック関数
  • OnDestroy
    • クラスが使っているリソースを適切に開放する関数
  • SaveTextureToGallery
    • 撮影した写真をスマートフォンのギャラリーに保存する関数

CapturePhoto関数は、ユーザがピースをしたときに写真を撮影する関数です。
まず、takeflagというブール変数の確認を行います。このフラグがtrueの場合、写真を撮影するプロセスがすでに進行・終了していることを示し、関数を終了させます。これにより、ピースをした際に複数の写真が撮影されることを防ぎます。takeflagtrueの場合、フラグをtrueにし、撮影プロセスを行います。
次に、m_PhotoCaptureObjectがすでに存在するかどうかを確認します。m_PhotoCaptureObjectはです。このオブジェクトが存在しない場合はm_PhotoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);を使って、非同期的にm_PhotoCaptureObjectを作成します。m_PhotoCaptureObjectが存在する場合は、そのオブジェクトのTakePhotoAsyncを直接呼び出して写真を撮影します。
写真が撮影されると、OnCapturedPhotoToMemory関数がコールバックとして呼び出され、撮影された写真がメモリにアップロードされます。

private void CapturePhoto()
{
    if (takeflag)
    {
	return;
    }

    takeflag = true;
    Debug.Log("写真をとります");
    if (m_PhotoCaptureObject == null)
    {
	this.Create((capture) =>
	{
	    capture.TakePhotoAsync(OnCapturedPhotoToMemory);
	});
    }
    else
    {
	m_PhotoCaptureObject.TakePhotoAsync(OnCapturedPhotoToMemory);
    }
}

実行しピースジェスチャーをするとカメラの景色を撮影できるようになりました。
https://youtu.be/qpDMmn6n1o4

おわりに

今回は「【XREAL Light】ピースで写真が撮れるアプリを作る【写真撮影編】」というテーマでPanda株式会社 Advent Calendar 2023 15日目を執筆させていただきました。
本記事では、XREAL Lightを用いた外界の撮影・保存方法を紹介しました。
前回の記事を合わせ、ピースをすると写真が撮れるアプリケーションが作れるようになったと思います。

明日の記事も私、Panda株式会社代表取締役の田貝奈央による「LumaAIのtext-to-3Dで作ったモデルをUnityで使う」です。お楽しみに!

Panda株式会社

Discussion