web-dev-qa-db-ja.com

キューブマップを正距円筒パノラマに変換する

立方体マップ[図1]から正距円筒パノラマ[図2]に変換したい。

図1 - Figure1

図2 enter image description here

球形から立方体に移動することは可能です(次のようにして 2:1の正距円筒パノラマを立方体マップに変換 )。しかし、それを元に戻す方法がわかりません。

図2はUnityを使用して球にレンダリングされます。

15
Sach

入力画像が次のキューブマップ形式であると仮定します。

Cubemap image

目標は、次のように画像を正距円筒形式に投影することです。

Equirectangular image

変換アルゴリズムはかなり単純です。 6つの面を持つ立方体マップが与えられた場合の正距円筒画像の各ピクセルでの色の最良のestimateを計算するには:

  • まず、全天球画像の各ピクセルに対応する極座標を計算します。
  • 次に、極座標を使用してベクトルを形成し、キューブマップのどの面に、その面のどのピクセルがベクトルにあるかを決定します。ちょうど立方体の中心からのレイキャストがその側面の1つとその側面の特定の点に当たるように。

キューブマップの特定の面の正規化座標(u、v)が与えられた場合、正距円筒画像のピクセルの色を推定する方法は複数あることに注意してください。最も基本的な方法は非常に生の近似であり、簡単にするためにこの回答で使用されますが、座標を特定のピクセルに丸めてそのピクセルを使用することです。他のより高度な方法では、隣接するいくつかのピクセルの平均を計算できます。

アルゴリズムの実装は、コンテキストによって異なります。 Unity3D C#で簡単な実装を行い、実際のシナリオでアルゴリズムを実装する方法を示しました。 CPU上で動作しますので、改善の余地はありますが分かりやすいです。

using UnityEngine;

public static class CubemapConverter
{
    public static byte[] ConvertToEquirectangular(Texture2D sourceTexture, int outputWidth, int outputHeight)
    {
        Texture2D equiTexture = new Texture2D(outputWidth, outputHeight, TextureFormat.ARGB32, false);
        float u, v; //Normalised texture coordinates, from 0 to 1, starting at lower left corner
        float phi, theta; //Polar coordinates
        int cubeFaceWidth, cubeFaceHeight;

        cubeFaceWidth = sourceTexture.width / 4; //4 horizontal faces
        cubeFaceHeight = sourceTexture.height / 3; //3 vertical faces


        for (int j = 0; j < equiTexture.height; j++)
        {
            //Rows start from the bottom
            v = 1 - ((float)j / equiTexture.height);
            theta = v * Mathf.PI;

            for (int i = 0; i < equiTexture.width; i++)
            {
                //Columns start from the left
                u = ((float)i / equiTexture.width);
                phi = u * 2 * Mathf.PI;

                float x, y, z; //Unit vector
                x = Mathf.Sin(phi) * Mathf.Sin(theta) * -1;
                y = Mathf.Cos(theta);
                z = Mathf.Cos(phi) * Mathf.Sin(theta) * -1;

                float xa, ya, za;
                float a;

                a = Mathf.Max(new float[3] { Mathf.Abs(x), Mathf.Abs(y), Mathf.Abs(z) });

                //Vector Parallel to the unit vector that lies on one of the cube faces
                xa = x / a;
                ya = y / a;
                za = z / a;

                Color color;
                int xPixel, yPixel;
                int xOffset, yOffset;

                if (xa == 1)
                {
                    //Right
                    xPixel = (int)((((za + 1f) / 2f) - 1f) * cubeFaceWidth);
                    xOffset = 2 * cubeFaceWidth; //Offset
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight; //Offset
                }
                else if (xa == -1)
                {
                    //Left
                    xPixel = (int)((((za + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = 0;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else if (ya == 1)
                {
                    //Up
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((za + 1f) / 2f) - 1f) * cubeFaceHeight);
                    yOffset = 2 * cubeFaceHeight;
                }
                else if (ya == -1)
                {
                    //Down
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((za + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = 0;
                }
                else if (za == 1)
                {
                    //Front
                    xPixel = (int)((((xa + 1f) / 2f)) * cubeFaceWidth);
                    xOffset = cubeFaceWidth;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else if (za == -1)
                {
                    //Back
                    xPixel = (int)((((xa + 1f) / 2f) - 1f) * cubeFaceWidth);
                    xOffset = 3 * cubeFaceWidth;
                    yPixel = (int)((((ya + 1f) / 2f)) * cubeFaceHeight);
                    yOffset = cubeFaceHeight;
                }
                else
                {
                    Debug.LogWarning("Unknown face, something went wrong");
                    xPixel = 0;
                    yPixel = 0;
                    xOffset = 0;
                    yOffset = 0;
                }

                xPixel = Mathf.Abs(xPixel);
                yPixel = Mathf.Abs(yPixel);

                xPixel += xOffset;
                yPixel += yOffset;

                color = sourceTexture.GetPixel(xPixel, yPixel);
                equiTexture.SetPixel(i, j, color);
            }
        }

        equiTexture.Apply();
        var bytes = equiTexture.EncodeToPNG();
        Object.DestroyImmediate(equiTexture);

        return bytes;
    }
}

GPUを利用するために、同じ変換を行うシェーダーを作成しました。 CPUでピクセルごとに変換を実行するよりもはるかに高速ですが、残念ながらUnityはキューブマップに解像度の制限を課しているため、高解像度の入力画像を使用するシナリオでは、その有用性が制限されます。

Shader "Conversion/CubemapToEquirectangular" {
  Properties {
        _MainTex ("Cubemap (RGB)", CUBE) = "" {}
    }

    Subshader {
        Pass {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }      

            CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest
                //#pragma fragmentoption ARB_precision_hint_nicest
                #include "UnityCG.cginc"

                #define PI    3.141592653589793
                #define TWOPI 6.283185307179587

                struct v2f {
                    float4 pos : POSITION;
                    float2 uv : TEXCOORD0;
                };

                samplerCUBE _MainTex;

                v2f vert( appdata_img v )
                {
                    v2f o;
                    o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                    o.uv = v.texcoord.xy * float2(TWOPI, PI);
                    return o;
                }

                fixed4 frag(v2f i) : COLOR 
                {
                    float theta = i.uv.y;
                    float phi = i.uv.x;
                    float3 unit = float3(0,0,0);

                    unit.x = sin(phi) * sin(theta) * -1;
                    unit.y = cos(theta) * -1;
                    unit.z = cos(phi) * sin(theta) * -1;

                    return texCUBE(_MainTex, unit);
                }
            ENDCG
        }
    }
    Fallback Off
}

結果の画像の品質は、変換中にピクセルの色を推定するためのより高度な方法を採用するか、結果の画像を後処理する(または実際には両方)ことにより、大幅に改善できます。たとえば、大きなサイズの画像を生成してぼかしフィルターを適用し、それを希望のサイズにダウンサンプリングすることができます。

上記のC#コードまたはシェーダーを適切に使用する方法を示す2つのエディターウィザードを使用して、シンプルなUnityプロジェクトを作成しました。ここで入手: https://github.com/Mapiarz/CubemapToEquirectangular

Unityで入力画像に適切なインポート設定を行うことを忘れないでください:

  • ポイントフィルタリング
  • トゥルーカラー形式
  • ミップマップを無効にする
  • 2の非べき乗:なし(2DTexturesのみ)
  • 読み取り/書き込みを有効にする(2DTexturesのみ)
11
Bartosz

cube2sphere プロセス全体を自動化します。例:

$ cube2sphere front.jpg back.jpg right.jpg left.jpg top.jpg bottom.jpg -r 2048 1024 -fTGA -ostitched
1
13rac1