オブジェクトのエッジを強調したり、モデルベースの画像処理を行いたいときなどは、見ているシーンの深度画像が必要になります。(これは本当)
OpenCV に現在フレームの深度画像を渡したいときは float 配列としてカメラからのZ距離が得られると文句はないのです。
この記事ではその float 配列の取得方法を動作確認込みで示します。
Unity で深度画像を得る方法をいくつか存じておりますが、私が知る中で最速な方法を示したいと思います。
前回、前々回と触ってきた Compute Shader を使います。(以下の方法が具体的にイメージできない人は読み返してね)
Compute Shader を新規作成したら、次のコードを記述します。
#pragma kernel CSMain
float n_f;
float f;
Texture2D<float> _zBuffer;
RWTexture2D<float> _cameraZ;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
#if SHADER_API_GLES3
_cameraZ[id.xy] = 1.0f / (n_f * (1.0f - _zBuffer[id.xy]) + f);
#else
_cameraZ[id.xy] = 1.0f / (n_f * _zBuffer[id.xy] + f);
#endif
}
単なるシェーダーではなく Compute Shader であるため UNITY_REVERSED_Z による判定が行えません。
Android では UNITY_REVERSED_Z ではなかったので泣く泣く SHADER_API_GLES3 でコードを変更しています。
この Compute Shader を使うスクリプトは次の通り
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class VirtualCameraBehaviour : MonoBehaviour {
private int width;
private int height;
Camera _virtualCamera = null;
RenderTexture _color = null;
RenderTexture _zBuffer = null;
public RenderTexture _cameraZ = null;
public ComputeShader Depth;
public GameObject uiResult0;
public GameObject uiResult1;
public GameObject uiResult2;
void Start () {
width = Screen.width / 2;
height = Screen.height / 2;
_virtualCamera = GetComponent<Camera>();
_color = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
_zBuffer = new RenderTexture(width, height, 32, RenderTextureFormat.Depth);
_virtualCamera.SetTargetBuffers(_color.colorBuffer, _zBuffer.depthBuffer);
_cameraZ = new RenderTexture(width, height, 0, RenderTextureFormat.RFloat);
_cameraZ.enableRandomWrite = true;
_cameraZ.Create();
float n_inv = 1.0f / _virtualCamera.nearClipPlane;
float f_inv = 1.0f / _virtualCamera.farClipPlane;
Depth.SetFloat("n_f", n_inv - f_inv);
Depth.SetFloat("f", f_inv);
Depth.SetTexture(0, "_zBuffer", _zBuffer);
Depth.SetTexture(0, "_cameraZ", _cameraZ);
uiResult0.GetComponent<UnityEngine.UI.RawImage>().texture = _color;
uiResult1.GetComponent<UnityEngine.UI.RawImage>().texture = _cameraZ;
uiResult2.GetComponent<UnityEngine.UI.RawImage>().texture = _zBuffer;
}
void Update () {
}
private void OnPostRender()
{
Depth.Dispatch(0, _zBuffer.width / 8, _zBuffer.height / 8, 1);
}
}
オフスクリーンレンダリングで、解像度を指定して VirtualCamera 画像を作り、そのカラー画像や深度画像を float 32bit 深度 1チャンネル画像として取得しています。
CPU 側で float 配列を手に入れたい場合は次の一工夫を加えて完了です。
#pragma kernel CSMain
float n_f;
float f;
int width;
Texture2D<float> _zBuffer;
RWTexture2D<float> _cameraZ;
RWStructuredBuffer<float> _zArray;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
#if SHADER_API_GLES3
_cameraZ[id.xy] = 1.0f / (n_f * (1.0f - _zBuffer[id.xy]) + f);
#else
_cameraZ[id.xy] = 1.0f / (n_f * _zBuffer[id.xy] + f);
#endif
_zArray[id.x + id.y * width] = _cameraZ[id.xy];
}
以下の対応で CPU 側の float 配列に深度画像の画素値が得られることを確認しました。
これで OpenCV にこのデータを使ってもらうことにより、さまざまなモデルベース処理が行えるという夢が広がります。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class VirtualCameraBehaviour : MonoBehaviour {
private int width;
private int height;
Camera _virtualCamera = null;
RenderTexture _color = null;
RenderTexture _zBuffer = null;
public RenderTexture _cameraZ = null;
public ComputeShader Depth;
ComputeBuffer _zArray;
float[] _zArrayData;
public GameObject uiResult0;
public GameObject uiResult1;
public GameObject uiResult2;
void Start () {
width = Screen.width / 2;
height = Screen.height / 2;
_virtualCamera = GetComponent<Camera>();
_color = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
_zBuffer = new RenderTexture(width, height, 32, RenderTextureFormat.Depth);
_virtualCamera.SetTargetBuffers(_color.colorBuffer, _zBuffer.depthBuffer);
_cameraZ = new RenderTexture(width, height, 0, RenderTextureFormat.RFloat);
_cameraZ.enableRandomWrite = true;
_cameraZ.Create();
float n_inv = 1.0f / _virtualCamera.nearClipPlane;
float f_inv = 1.0f / _virtualCamera.farClipPlane;
Depth.SetFloat("n_f", n_inv - f_inv);
Depth.SetFloat("f", f_inv);
Depth.SetTexture(0, "_zBuffer", _zBuffer);
Depth.SetTexture(0, "_cameraZ", _cameraZ);
_zArrayData = new float[width * height];
_zArray = new ComputeBuffer(_zArrayData.Length, sizeof(float));
Depth.SetBuffer(0, "_zArray", _zArray);
Depth.SetInt("width", width);
uiResult0.GetComponent<UnityEngine.UI.RawImage>().texture = _color;
uiResult1.GetComponent<UnityEngine.UI.RawImage>().texture = _cameraZ;
uiResult2.GetComponent<UnityEngine.UI.RawImage>().texture = _zBuffer;
}
void Update () {
}
private void OnPostRender()
{
Depth.Dispatch(0, _zBuffer.width / 8, _zBuffer.height / 8, 1);
_zArray.GetData(_zArrayData);
float av = 0;
for (int i = 0; i < _zArrayData.Length; i++)
{
av += _zArrayData[i];
}
av /= _zArrayData.Length;
Debug.Log("average z = " + av.ToString("0.0000"));
}
}