S1TA が 2025年01月27日16時05分06秒 に編集
コメント無し
本文の変更
# 0.はじめに はじめまして、Unity/XRエンジニアの高原と申します。 業務でUnityを使ってXR系のアプリ開発をしています。(最近はAppleVisionProで色々やってます。) 普段ハードウェア系の開発には携わっていないのですが、今回弊社でSPRESENSE™ 活用コンテストに参加するというお話を伺ったため、 Unity/XRエンジニアとしてコンテンツ開発に協力することになりました。 本記事ではコンテストに応募するために制作した「空間監視カメラ」について解説をしていこうと思います。 # 1.空間監視カメラとは?
[](https://vimeo.com/1050641183 "空間監視カメラ")
Todo:埋め込み動画
空間監視カメラはSPRESENSE(Spresense HDRカメラボード)を活用し、通常は2Dでしか見られない監視カメラの映像を3D空間内の情報として確認することができる監視カメラVRビューワーアプリです。(Quest3スタンドアロンVRアプリ) 通常の監視カメラではカメラのある地点からの映像しか確認することができませんが、空間監視カメラは物理的なカメラの設置場所に関係なく、3D空間内の自由な視点から監視が可能となります。 仕組みとしては、まず監視対象となる空間をあらかじめ3Dスキャンして3Dモデルを作成します。 そしてSpresense HDRカメラボードのリアルタイムの監視映像から動体を検出し、3D空間上に反映します。 この時、動体の深度情報も計測することで3D空間内の適切な位置に配置することが可能となります。VRアプリとしているため、ユーザーは任意の視点から空間を自由に確認することが可能です。 また、今回ビューワーにカメラ映像を表示するだけでなく、画像を生成AIモデルであるClaude Sonnet 3.5に入力し、現在カメラに写っている映像について文章で説明する機能も実装しました。   # 2.全体処理構成  # 3.カメラハードウェア側構成   現実空間に配置したカメラに使用した部品は下記になります。 | 部品名 | 個数 | 役割 | | - | - | - | | SPRESENSEメインボード[CXD5602PWBMAIN1] | 1 | 制御用メインボード | | SPRESENSEカメラボード [CXD5602PWBCAM1] | 1 | カメラモジュール | | ToFセンサーボード(SPRESENSE用)(MM-S50MV) | 1 | 深度情報取得 | | SPRESENSE Wi-Fi Add-onボード iS110B | 1 | 画像、深度情報送信用 | | 筐体 | 1 | 3Dプリンタで作成 | | Raspberry Pi 4 | 1 | WEBサーバー用 | # 4.Unityソフトウェア側構成 VRアプリはUnity6000.0.23f1 URPで作成しています。 VR機能面はMeta XR Core SDKを利用しています。 ## シーン構成 シーン内にはあらかじめ3Dスキャンを行った空間モデルを配置しています。 かつ現実空間においてあるカメラ位置をVRアプリ内でも把握し、 動体画像の表示位置の指定に使っています。  ## スクリプト詳細 ### VR操作機能 VRでの操作はシンプルにコントローラを使った移動と回転のみ実装しています。 ```
using UnityEngine;
public class VRLocomotion : MonoBehaviour { [SerializeField] private Transform cameraRig;
[SerializeField] private Transform head;
[SerializeField] private float moveSpeed = 2f; [SerializeField] private float rotationAngle = 45f; private bool canRotate = true; private void Update() { // 左スティックで移動 HandleMovement(); // 右スティックで回転 HandleRotation(); } private void HandleMovement() { Vector2 leftStick = OVRInput.Get(OVRInput.RawAxis2D.LThumbstick); Vector3 forward = new Vector3(head.forward.x, 0f, head.forward.z).normalized; Vector3 right = new Vector3(head.right.x, 0f, head.right.z).normalized; Vector3 moveDirection = forward * leftStick.y + right * leftStick.x; cameraRig.position += moveDirection * moveSpeed * Time.deltaTime; } private void HandleRotation() { Vector2 rightStick = OVRInput.Get(OVRInput.RawAxis2D.RThumbstick); bool isRotating = Mathf.Abs(rightStick.x) > 0.5f; if (isRotating && canRotate) { cameraRig.Rotate(0f, rotationAngle * Mathf.Sign(rightStick.x), 0f); canRotate = false; } if (!isRotating) { canRotate = true; } } }
```
### カメラ画像の受信
カメラ画像はWebサーバーとなっているRaspberry Pi からHTTPS通信でダウンロードしています。 ``` using UnityEngine; using UnityEngine.Networking; using Cysharp.Threading.Tasks; using static Constants; using System; public class ImageDownloader { public async UniTask<(ImageResult, bool)> DownloadImageAsync() { var result = new ImageResult(); try { string url = $"{MonitorSSDUrl}?time={DateTime.UtcNow.Ticks}"; using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url)) { await request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"画像のダウンロードに失敗しました: {request.error}"); return (result, false); } result.Image = DownloadHandlerTexture.GetContent(request); return (result, true); } } catch (Exception ex) { Debug.LogError($"例外が発生しました: {ex.Message}"); return (result, false); } } } ``` ### 深度情報の受信 動体の深度情報も同様にWebサーバーとなっているRaspberry Pi からHTTPS通信でダウンロードしています。 ``` using System; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using Newtonsoft.Json; public static class DepthInfoDownloader { // 差異の深度情報(JSON)をダウンロードする public static async UniTask<(DepthResult depthResult, bool isSuccess)> FetchDepthInfoAsync() { DepthResult depthResult = null; try { using (var request = UnityWebRequest.Get(Constants.MonitorTOFUrl)) { await request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { Debug.LogError($"Depth info download failed: {request.error}"); return (depthResult, false); } string jsonText = request.downloadHandler.text; depthResult = JsonConvert.DeserializeObject<DepthResult>(jsonText); bool success = depthResult != null && depthResult.distance != null; return (depthResult, success); } } catch (Exception ex) { Debug.LogError($"Exception during depth info download: {ex.Message}"); return (depthResult, false); } } } ``` 実際に送られてきている深度Json情報は下記になります。 「511.000000」は測定できなかった時の値のため、それ以外の値が入っている場合、 そのブロックに紐づいたカメラ画像の領域に動体が存在する判定になります。 ``` { "distance": [ [ "511.000000", "511.000000", "511.000000", "511.000000" ], [ "511.000000", "511.000000", "511.000000", "511.000000" ], [ "511.000000", "511.000000", "511.000000", "511.000000" ], [ "511.000000", "511.000000", "511.000000", "511.000000" ], [ "3.809655", "3.866323", "3.679776", "511.000000" ], [ "511.000000", "3.692517", "3.667131", "3.683865" ], [ "3.556268", "3.670649", "3.556743", "3.667796" ], [ "2.890043", "3.223393", "3.033804", "3.564159" ] ] } ```
# 5.おわりに