【XREAL Light】ピースで写真が撮れるアプリを作る【写真撮影編】
この記事は Panda株式会社 Advent Calendar 2023 15日目の記事です。
Panda株式会社は東京大学松尾研究室・香川高専発のスタートアップで、AR技術とAI技術を駆使したシステム開発と研究に取り組んでいます。
このアドベントカレンダーでは、スタートアップとしての知見、AI・AR技術、バックエンドなど、さまざまな領域の記事を公開していきます。
自己紹介
Panda株式会社代表取締役の田貝奈央です。ARグラスの普及に夢を見ている人です。高専の卒業研究ではXREAL Lightを使って目の前の人の顔を認証しプロフィール情報を提示する研究を行っていました。たくさんの人にXREAL Lightでのアプリ開発に取り組んでほしいと思い、この記事を執筆しました。
概要
XREAL Lightを装着しピースのジェスチャーを行うと、XREAL Lightについているカメラの映像を撮影し、ローカルに保存するシステムを作成します。ハンドジェスチャーを認識してイベントを発火させる方法と、カメラの映像を撮影しローカルに保存する方法の二回に分けて説明を行います。
カメラの映像を撮影する
XREAL Lightの全面にはRGBカメラが付いています。このカメラを用いて、外界の状況を撮影します。
こちらの記事を参考にRGBカメラを扱います。
NRSDKのサンプルシーンの中に、RGBカメラで撮影を行うシーンが入っています。(Assets/NRSDK/Demos/RGBCamera-Capture.unity
)これを参考に、カメラの映像を撮影するスクリプトを書いていきます。
前回のコードに以下内容を追加していきます。
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
の場合、写真を撮影するプロセスがすでに進行・終了していることを示し、関数を終了させます。これにより、ピースをした際に複数の写真が撮影されることを防ぎます。takeflag
がtrue
の場合、フラグを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);
}
}
実行しピースジェスチャーをするとカメラの景色を撮影できるようになりました。
おわりに
今回は「【XREAL Light】ピースで写真が撮れるアプリを作る【写真撮影編】」というテーマでPanda株式会社 Advent Calendar 2023 15日目を執筆させていただきました。
本記事では、XREAL Lightを用いた外界の撮影・保存方法を紹介しました。
前回の記事を合わせ、ピースをすると写真が撮れるアプリケーションが作れるようになったと思います。
明日の記事も私、Panda株式会社代表取締役の田貝奈央による「LumaAIのtext-to-3Dで作ったモデルをUnityで使う」です。お楽しみに!
Discussion