panorama equirectangular cubemap c++ opencv image-processing unity3d

c++ - cubemap - equirectangular converter



Conversión de un Cubemap en Equirectangular Panorama (2)

Quiero convertir el mapa del cubo [figura 1] en un panorama equirrectangular [figura 2].

Figura 1

Figura 2

Es posible pasar de Esférico a Cúbico (siguiendo: Convertir panorama equirectangular 2: 1 a mapa de cubo ), pero se pierde en cómo invertirlo.

La Figura 2 debe representarse en una esfera usando Unity.


Suponiendo que la imagen de entrada está en el siguiente formato de mapa de cubos:

El objetivo es proyectar la imagen al formato equirectangular de la siguiente manera:

El algoritmo de conversión es bastante directo. Para calcular la mejor estimación del color en cada píxel en la imagen equirrectangular dado un mapa de cubos con 6 caras:

  • En primer lugar, calcule las coordenadas polares que corresponden a cada píxel en la imagen esférica.
  • En segundo lugar, al usar las coordenadas polares se forma un vector y se determina en qué cara del mapa de cubos y en qué píxel de esa cara se encuentra el vector; al igual que un raycast desde el centro de un cubo golpearía uno de sus lados y un punto específico de ese lado.

Tenga en cuenta que hay varios métodos para estimar el color de un píxel en la imagen equirrectangular dada una coordenada normalizada (u, v) en una cara específica de un mapa de cubos. El método más básico, que es una aproximación muy cruda y se usará en esta respuesta por simplicidad, es redondear las coordenadas a un píxel específico y usar ese píxel. Otros métodos más avanzados podrían calcular un promedio de unos pocos píxeles vecinos.

La implementación del algoritmo variará según el contexto. Hice una implementación rápida en Unity3D C # que muestra cómo implementar el algoritmo en un escenario del mundo real. Se ejecuta en la CPU, hay mucho margen de mejora, pero es fácil de entender.

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; } }

Para utilizar la GPU, creé un sombreador que realiza la misma conversión. Es mucho más rápido que ejecutar la conversión píxel por píxel en la CPU, pero lamentablemente Unity impone limitaciones de resolución en los mapas de cubo, por lo que su utilidad es limitada en los escenarios cuando se va a utilizar una imagen de entrada de alta resolución.

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 }

La calidad de las imágenes resultantes se puede mejorar en gran medida empleando un método más sofisticado para estimar el color de un píxel durante la conversión o procesando posteriormente la imagen resultante (o ambas, en realidad). Por ejemplo, se podría generar una imagen de mayor tamaño para aplicar un filtro de desenfoque y luego reducir la resolución al tamaño deseado.

Creé un proyecto simple de Unity con dos asistentes de edición que muestran cómo utilizar correctamente el código C # o el sombreador que se muestra arriba. Obténgalo aquí: https://github.com/Mapiarz/CubemapToEquirectangular

Recuerde establecer las configuraciones de importación correctas en Unity para sus imágenes de entrada:

  • Filtrado de puntos
  • Formato Truecolor
  • Deshabilitar mipmaps
  • Non Power of 2: None (solo para 2DTextures)
  • Habilitar lectura / escritura (solo para texturas 2D)

cube2sphere automatiza todo el proceso. Ejemplo:

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