将 2:1 个等距柱状全景图转换为立方体贴图
Convert 2:1 equirectangular panorama to cube map
我目前正在为一个网站开发一个简单的 3D 全景图查看器。出于移动性能原因,我使用 Three.js CSS 3 renderer。这需要一个立方体贴图,分成六个单独的图像。
我正在用 Google Photo Sphere, or similar apps that create 2:1 equirectangular panoramas. I then resize and convert these to a cubemap with this website: http://gonchar.me/panorama/ (Flash)
在 iPhone 上录制图像
最好,我想自己进行转换,可以在Three.js 中即时进行,也可以在Photoshop 中进行。我找到了 Andrew Hazelden 的 Photoshop 动作,它们看起来有点接近,但不能直接转换。有没有一种数学方法来转换这些,或者某种脚本呢?如果可能,我想避免使用像 Blender 这样的 3D 应用程序。
也许这不太可能,但我想我会问的。我对 JavaScript 的经验还不错,但我对 Three.js 还很陌生。我也对依赖 WebGL 功能犹豫不决,因为它在移动设备上看起来不是很慢就是有问题。支持也仍然参差不齐。
如果你想在服务器端做,有很多选择。 ImageMagick 有一堆命令行工具可以将您的图像切成碎片。您可以将执行此操作的命令放入脚本中,然后 运行 每次您有一个新图像时。
很难说清楚程序中使用了什么算法。我们可以尝试通过将正方形网格输入程序来对正在发生的事情进行逆向工程。我用过 grid from Wikipedia:
给出:
这为我们提供了有关盒子构造方式的线索。
想象一个上面有经纬线的球体,以及一个围绕着它的立方体。现在从球体中心的点进行投影会在立方体上产生扭曲的网格。
在数学上,取极坐标 r, θ, ø,对于球体 r=1, 0 < θ < π, -π/4 < ø < 7π/4
- x= r sin θ cos ø
- y= r sin θ sin ø
- z= 余弦θ
将这些集中投影到多维数据集。首先我们按纬度划分为四个区域-π/4 < ø < π/4, π/4 < ø < 3π/4, 3π/4 < ø < 5π/4, 5π/4 < ø < 7π/4。这些将投射到顶部或底部的四个侧面之一。
假设我们在第一边 -π/4 < ø < π/4。的中心投影
(sin θ cos ø, sin θ sin ø, cos θ) 将是 (a sin θ cos ø, a sin θ sin ø, a cos θ) 当
- a sin θ cos ø = 1
所以
- a = 1 / (sin θ cos ø)
投影点是
- (1, tan ø, cot θ / cos ø)
如果| cot θ / cos ø | < 1,这将在正面。否则,它将投影在顶部或底部,您将需要一个不同的投影。更好的顶部测试使用 cos ø 的最小值将是 cos π/4 = 1/√2 的事实,因此如果 cot θ / (1/√2) > 1 或投影点始终位于顶部tan θ < 1/√2。计算结果为 θ < 35º 或 0.615 弧度。
把这个放在一起 Python:
import sys
from PIL import Image
from math import pi,sin,cos,tan
def cot(angle):
return 1/tan(angle)
# Project polar coordinates onto a surrounding cube
# assume ranges theta is [0,pi] with 0 the north poll, pi south poll
# phi is in range [0,2pi]
def projection(theta,phi):
if theta<0.615:
return projectTop(theta,phi)
elif theta>2.527:
return projectBottom(theta,phi)
elif phi <= pi/4 or phi > 7*pi/4:
return projectLeft(theta,phi)
elif phi > pi/4 and phi <= 3*pi/4:
return projectFront(theta,phi)
elif phi > 3*pi/4 and phi <= 5*pi/4:
return projectRight(theta,phi)
elif phi > 5*pi/4 and phi <= 7*pi/4:
return projectBack(theta,phi)
def projectLeft(theta,phi):
x = 1
y = tan(phi)
z = cot(theta) / cos(phi)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Left",x,y,z)
def projectFront(theta,phi):
x = tan(phi-pi/2)
y = 1
z = cot(theta) / cos(phi-pi/2)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Front",x,y,z)
def projectRight(theta,phi):
x = -1
y = tan(phi)
z = -cot(theta) / cos(phi)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Right",x,-y,z)
def projectBack(theta,phi):
x = tan(phi-3*pi/2)
y = -1
z = cot(theta) / cos(phi-3*pi/2)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Back",-x,y,z)
def projectTop(theta,phi):
# (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,1)
a = 1 / cos(theta)
x = tan(theta) * cos(phi)
y = tan(theta) * sin(phi)
z = 1
return ("Top",x,y,z)
def projectBottom(theta,phi):
# (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,-1)
a = -1 / cos(theta)
x = -tan(theta) * cos(phi)
y = -tan(theta) * sin(phi)
z = -1
return ("Bottom",x,y,z)
# Convert coords in cube to image coords
# coords is a tuple with the side and x,y,z coords
# edge is the length of an edge of the cube in pixels
def cubeToImg(coords,edge):
if coords[0]=="Left":
(x,y) = (int(edge*(coords[2]+1)/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Front":
(x,y) = (int(edge*(coords[1]+3)/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Right":
(x,y) = (int(edge*(5-coords[2])/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Back":
(x,y) = (int(edge*(7-coords[1])/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Top":
(x,y) = (int(edge*(3-coords[1])/2), int(edge*(1+coords[2])/2) )
elif coords[0]=="Bottom":
(x,y) = (int(edge*(3-coords[1])/2), int(edge*(5-coords[2])/2) )
return (x,y)
# convert the in image to out image
def convert(imgIn,imgOut):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
edge = inSize[0]/4 # the length of each edge in pixels
for i in xrange(inSize[0]):
for j in xrange(inSize[1]):
pixel = inPix[i,j]
phi = i * 2 * pi / inSize[0]
theta = j * pi / inSize[1]
res = projection(theta,phi)
(x,y) = cubeToImg(res,edge)
#if i % 100 == 0 and j % 100 == 0:
# print i,j,phi,theta,res,x,y
if x >= outSize[0]:
#print "x out of range ",x,res
x=outSize[0]-1
if y >= outSize[1]:
#print "y out of range ",y,res
y=outSize[1]-1
outPix[x,y] = pixel
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convert(imgIn,imgOut)
imgOut.show()
projection
函数采用立方体中的 theta
和 phi
值以及每个方向上从 -1 到 1 的 returns 坐标。 cubeToImg 获取 (x,y,z) 坐标并将它们转换为输出图像坐标。
上述算法似乎使用 image of buckingham palace 获得了正确的几何形状。我们得到:
这似乎得到了铺路中的大部分线条。
我们得到了一些图像人工制品。这是因为没有一对一的像素映射。我们需要做的是使用逆变换。我们不是循环遍历源中的每个像素并找到目标中的对应像素,而是循环遍历目标图像并找到最接近的对应源像素。
import sys
from PIL import Image
from math import pi,sin,cos,tan,atan2,hypot,floor
from numpy import clip
# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# face is face number
# edge is edge length
def outImgToXYZ(i,j,face,edge):
a = 2.0*float(i)/edge
b = 2.0*float(j)/edge
if face==0: # back
(x,y,z) = (-1.0, 1.0-a, 3.0 - b)
elif face==1: # left
(x,y,z) = (a-3.0, -1.0, 3.0 - b)
elif face==2: # front
(x,y,z) = (1.0, a - 5.0, 3.0 - b)
elif face==3: # right
(x,y,z) = (7.0-a, 1.0, 3.0 - b)
elif face==4: # top
(x,y,z) = (b-1.0, a -5.0, 1.0)
elif face==5: # bottom
(x,y,z) = (5.0-b, a-5.0, -1.0)
return (x,y,z)
# convert using an inverse transformation
def convertBack(imgIn,imgOut):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
edge = inSize[0]/4 # the length of each edge in pixels
for i in xrange(outSize[0]):
face = int(i/edge) # 0 - back, 1 - left 2 - front, 3 - right
if face==2:
rng = xrange(0,edge*3)
else:
rng = xrange(edge,edge*2)
for j in rng:
if j<edge:
face2 = 4 # top
elif j>=2*edge:
face2 = 5 # bottom
else:
face2 = face
(x,y,z) = outImgToXYZ(i,j,face2,edge)
theta = atan2(y,x) # range -pi to pi
r = hypot(x,y)
phi = atan2(z,r) # range -pi/2 to pi/2
# source img coords
uf = ( 2.0*edge*(theta + pi)/pi )
vf = ( 2.0*edge * (pi/2 - phi)/pi)
# Use bilinear interpolation between the four surrounding pixels
ui = floor(uf) # coord of pixel to bottom left
vi = floor(vf)
u2 = ui+1 # coords of pixel to top right
v2 = vi+1
mu = uf-ui # fraction of way across pixel
nu = vf-vi
# Pixel values of four corners
A = inPix[ui % inSize[0],clip(vi,0,inSize[1]-1)]
B = inPix[u2 % inSize[0],clip(vi,0,inSize[1]-1)]
C = inPix[ui % inSize[0],clip(v2,0,inSize[1]-1)]
D = inPix[u2 % inSize[0],clip(v2,0,inSize[1]-1)]
# interpolate
(r,g,b) = (
A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )
outPix[i,j] = (int(round(r)),int(round(g)),int(round(b)))
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convertBack(imgIn,imgOut)
imgOut.save(sys.argv[1].split('.')[0]+"Out2.png")
imgOut.show()
结果是:
如果有人想倒车,请参阅this JS Fiddle page。
环境贴图有多种表现形式。这是一个很好的概述。
如果您使用 Photosphere(或与此相关的任何全景应用程序),您很可能已经拥有水平 latitude / longitude 表示。
然后你可以简单地绘制一个纹理 three.js SphereGeometry。这里有一个关于如何渲染地球的教程。
Tutorial - How to Make the Earth in WebGL?
祝你好运:).
我写了一个脚本将生成的立方体贴图切割成单独的文件(posx.png、negx.png、posy.png、negy.png、posz.png 和 negz.png).它还会将这 6 个文件打包成一个 .zip 文件。
来源在这里:https://github.com/dankex/compv/blob/master/3d-graphics/skybox/cubemap-cut.py
可以修改数组来设置图片文件:
name_map = [ \
["", "", "posy", ""],
["negz", "negx", "posz", "posx"],
["", "", "negy", ""]]
转换后的文件为:
首先:除非你真的必须自己转换图像(即,由于某些特定的软件要求),不要。
原因是,即使等角投影和立方投影之间有一个非常简单的映射,区域之间的映射并不简单:当你建立一个对应关系目标图像的特定点和源中的点进行基本计算,只要通过四舍五入将这两个点都转换为像素 你正在做 very raw不考虑像素大小的近似值,图像质量必然偏低
第二:即使你需要在运行时进行转换,你确定你真的需要进行转换吗?除非有一些非常严格的性能问题,否则如果你只需要一个天空盒,创建一个非常大的球体,在上面缝合 equirectangular 纹理,然后就可以了。据我所知,Three.js 已经提供了球体 ;-)
第三:NASA 提供了一种工具,可以在所有可以想象的投影之间进行转换(我刚刚发现并测试了它,并且非常有效)。你可以在这里找到它:
G.Projector — Global Map Projector
而且我认为这些人知道他们在做什么是有道理的;-)
事实证明,“伙计们”知道他们在某些时候做了什么:生成的立方体贴图有一个丑陋的边框,这使得转换不是那么容易...
我找到了将等距柱状图转换为立方体贴图的权威工具,它叫做 erect2cubic
。
这是一个小实用程序,可以生成一个脚本以提供给 hugin,以这种方式:
erect2cubic --erect=input.png --ptofile=cube.pto
nona -o cube_prefix cube.pto
(从 Vinay's Hacks 页面窃取的信息)
它会生成所有六个立方体贴图面。我将它用于我的项目,它 非常有用!
这种方法的唯一缺点是脚本 erect2cubit
不在标准 Ubuntu 发行版中(这是我正在使用的),我不得不求助于 a blog describing how to install and use erect2cubic 了解安装方法。
绝对值得!
鉴于优秀的接受答案,我想添加相应的 C++ implementation, based on OpenCV。
对于那些不熟悉 OpenCV 的人,请将 Mat
视为图像。我们首先构建两个贴图,从等距柱状图像重新映射到相应的立方体贴图面。然后,我们使用 OpenCV 进行繁重的工作(即使用插值重新映射)。
如果不关心可读性,代码可以做得更紧凑。
// Define our six cube faces.
// 0 - 3 are side faces, clockwise order
// 4 and 5 are top and bottom, respectively
float faceTransform[6][2] =
{
{0, 0},
{M_PI / 2, 0},
{M_PI, 0},
{-M_PI / 2, 0},
{0, -M_PI / 2},
{0, M_PI / 2}
};
// Map a part of the equirectangular panorama (in) to a cube face
// (face). The ID of the face is given by faceId. The desired
// width and height are given by width and height.
inline void createCubeMapFace(const Mat &in, Mat &face,
int faceId = 0, const int width = -1,
const int height = -1) {
float inWidth = in.cols;
float inHeight = in.rows;
// Allocate map
Mat mapx(height, width, CV_32F);
Mat mapy(height, width, CV_32F);
// Calculate adjacent (ak) and opposite (an) of the
// triangle that is spanned from the sphere center
//to our cube face.
const float an = sin(M_PI / 4);
const float ak = cos(M_PI / 4);
const float ftu = faceTransform[faceId][0];
const float ftv = faceTransform[faceId][1];
// For each point in the target image,
// calculate the corresponding source coordinates.
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
// Map face pixel coordinates to [-1, 1] on plane
float nx = (float)y / (float)height - 0.5f;
float ny = (float)x / (float)width - 0.5f;
nx *= 2;
ny *= 2;
// Map [-1, 1] plane coords to [-an, an]
// thats the coordinates in respect to a unit sphere
// that contains our box.
nx *= an;
ny *= an;
float u, v;
// Project from plane to sphere surface.
if(ftv == 0) {
// Center faces
u = atan2(nx, ak);
v = atan2(ny * cos(u), ak);
u += ftu;
} else if(ftv > 0) {
// Bottom face
float d = sqrt(nx * nx + ny * ny);
v = M_PI / 2 - atan2(d, ak);
u = atan2(ny, nx);
} else {
// Top face
float d = sqrt(nx * nx + ny * ny);
v = -M_PI / 2 + atan2(d, ak);
u = atan2(-ny, nx);
}
// Map from angular coordinates to [-1, 1], respectively.
u = u / (M_PI);
v = v / (M_PI / 2);
// Warp around, if our coordinates are out of bounds.
while (v < -1) {
v += 2;
u += 1;
}
while (v > 1) {
v -= 2;
u += 1;
}
while(u < -1) {
u += 2;
}
while(u > 1) {
u -= 2;
}
// Map from [-1, 1] to in texture space
u = u / 2.0f + 0.5f;
v = v / 2.0f + 0.5f;
u = u * (inWidth - 1);
v = v * (inHeight - 1);
// Save the result for this pixel in map
mapx.at<float>(x, y) = u;
mapy.at<float>(x, y) = v;
}
}
// Recreate output image if it has wrong size or type.
if(face.cols != width || face.rows != height ||
face.type() != in.type()) {
face = Mat(width, height, in.type());
}
// Do actual resampling using OpenCV's remap
remap(in, face, mapx, mapy,
CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));
}
给定以下输入:
生成了以下面孔:
图片由 Optonaut 提供。
cmft Studio 支持 conversion/filtering
各种 HDR/LDR
到 cubemaps
的投影。
更新 2:看起来其他人的 already built a far superior web application 比我自己的多。他们的转换在客户端运行,因此无需担心任何上传和下载。
我想如果您出于某种原因讨厌 JavaScript,或者正试图在您的手机上执行此操作,那么我下面的网络应用程序是可以的。
更新:我已经在 ZIP 文件中发布了 a simple web application where you can upload a panorama and have it return the six skybox 张图片。
源代码是以下内容的清理重新实现,是 available on GitHub。
该应用程序目前 运行 在单个免费层 Heroku dyno 上,但请不要尝试将其用作 API。如果你想要自动化,请自己部署; 单击部署到 Heroku 可用。
原始:这是的(天真)修改版本,一次转换一张脸,吐出六张不同的图像并保留原始图像的文件类型.
除了大多数用例可能需要六张单独的图像这一事实之外,一次转换一张脸的主要优点是它可以大大减少处理大图像的内存占用。
#!/usr/bin/env python
import sys
from PIL import Image
from math import pi, sin, cos, tan, atan2, hypot, floor
from numpy import clip
# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# faceIdx is face number
# faceSize is edge length
def outImgToXYZ(i, j, faceIdx, faceSize):
a = 2.0 * float(i) / faceSize
b = 2.0 * float(j) / faceSize
if faceIdx == 0: # back
(x,y,z) = (-1.0, 1.0 - a, 1.0 - b)
elif faceIdx == 1: # left
(x,y,z) = (a - 1.0, -1.0, 1.0 - b)
elif faceIdx == 2: # front
(x,y,z) = (1.0, a - 1.0, 1.0 - b)
elif faceIdx == 3: # right
(x,y,z) = (1.0 - a, 1.0, 1.0 - b)
elif faceIdx == 4: # top
(x,y,z) = (b - 1.0, a - 1.0, 1.0)
elif faceIdx == 5: # bottom
(x,y,z) = (1.0 - b, a - 1.0, -1.0)
return (x, y, z)
# convert using an inverse transformation
def convertFace(imgIn, imgOut, faceIdx):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
faceSize = outSize[0]
for xOut in xrange(faceSize):
for yOut in xrange(faceSize):
(x,y,z) = outImgToXYZ(xOut, yOut, faceIdx, faceSize)
theta = atan2(y,x) # range -pi to pi
r = hypot(x,y)
phi = atan2(z,r) # range -pi/2 to pi/2
# source img coords
uf = 0.5 * inSize[0] * (theta + pi) / pi
vf = 0.5 * inSize[0] * (pi/2 - phi) / pi
# Use bilinear interpolation between the four surrounding pixels
ui = floor(uf) # coord of pixel to bottom left
vi = floor(vf)
u2 = ui+1 # coords of pixel to top right
v2 = vi+1
mu = uf-ui # fraction of way across pixel
nu = vf-vi
# Pixel values of four corners
A = inPix[ui % inSize[0], clip(vi, 0, inSize[1]-1)]
B = inPix[u2 % inSize[0], clip(vi, 0, inSize[1]-1)]
C = inPix[ui % inSize[0], clip(v2, 0, inSize[1]-1)]
D = inPix[u2 % inSize[0], clip(v2, 0, inSize[1]-1)]
# interpolate
(r,g,b) = (
A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )
outPix[xOut, yOut] = (int(round(r)), int(round(g)), int(round(b)))
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
faceSize = inSize[0] / 4
components = sys.argv[1].rsplit('.', 2)
FACE_NAMES = {
0: 'back',
1: 'left',
2: 'front',
3: 'right',
4: 'top',
5: 'bottom'
}
for face in xrange(6):
imgOut = Image.new("RGB", (faceSize, faceSize), "black")
convertFace(imgIn, imgOut, face)
imgOut.save(components[0] + "_" + FACE_NAMES[face] + "." + components[1])
一个非常简单的 C++ 应用程序,它根据 :
的回答将等距柱状全景图转换为立方体贴图
这是 的 JavaScript 版本。 convertFace
需要传递两个 ìmageData
对象和一个人脸 ID (0-6)。
所提供的代码可以安全地用于 Web Worker,因为它没有依赖项。
// convert using an inverse transformation
function convertFace(imgIn, imgOut, faceIdx) {
var inPix = shimImgData(imgIn),
outPix = shimImgData(imgOut),
faceSize = imgOut.width,
pi = Math.PI,
pi_2 = pi/2;
for(var xOut=0; xOut<faceSize; xOut++) {
for(var yOut=0; yOut<faceSize; yOut++) {
var xyz = outImgToXYZ(xOut, yOut, faceIdx, faceSize);
var theta = Math.atan2(xyz.y, xyz.x); // range -pi to pi
var r = Math.hypot(xyz.x, xyz.y);
var phi = Math.atan2(xyz.z, r); // range -pi/2 to pi/2
// source image coordinates
var uf = 0.5 * imgIn.width * (theta + pi) / pi;
var vf = 0.5 * imgIn.width * (pi_2 - phi) / pi;
// Use bilinear interpolation between the four surrounding pixels
var ui = Math.floor(uf); // coordinate of pixel to bottom left
var vi = Math.floor(vf);
var u2 = ui + 1; // coordinates of pixel to top right
var v2 = vi + 1;
var mu = uf - ui; // fraction of way across pixel
var nu = vf - vi;
// Pixel values of four corners
var A = inPix.getPx(ui % imgIn.width, clip(vi, 0, imgIn.height-1));
var B = inPix.getPx(u2 % imgIn.width, clip(vi, 0, imgIn.height-1));
var C = inPix.getPx(ui % imgIn.width, clip(v2, 0, imgIn.height-1));
var D = inPix.getPx(u2 % imgIn.width, clip(v2, 0, imgIn.height-1));
// interpolate
var rgb = {
r:A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu + D[0]*mu*nu,
g:A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu + D[1]*mu*nu,
b:A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu + D[2]*mu*nu
};
rgb.r = Math.round(rgb.r);
rgb.g = Math.round(rgb.g);
rgb.b = Math.round(rgb.b);
outPix.setPx(xOut, yOut, rgb);
} // for(var yOut=0; yOut<faceSize; yOut++) {...}
} // for(var xOut=0;xOut<faceSize;xOut++) {...}
} // function convertFace(imgIn, imgOut, faceIdx) {...}
// get x, y, z coordinates from out image pixels coordinates
// i,j are pixel coordinates
// faceIdx is face number
// faceSize is edge length
function outImgToXYZ(i, j, faceIdx, faceSize) {
var a = 2 * i / faceSize,
b = 2 * j / faceSize;
switch(faceIdx) {
case 0: // back
return({x:-1, y:1-a, z:1-b});
case 1: // left
return({x:a-1, y:-1, z:1-b});
case 2: // front
return({x: 1, y:a-1, z:1-b});
case 3: // right
return({x:1-a, y:1, z:1-b});
case 4: // top
return({x:b-1, y:a-1, z:1});
case 5: // bottom
return({x:1-b, y:a-1, z:-1});
}
} // function outImgToXYZ(i, j, faceIdx, faceSize) {...}
function clip(val, min, max) {
return(val<min ? min : (val>max ? max : val));
}
function shimImgData(imgData) {
var w = imgData.width*4,
d = imgData.data;
return({
getPx:function(x, y) {
x = x*4 + y*w;
return([d[x], d[x+1], d[x+2]]);
},
setPx:function(x, y, rgb) {
x = x*4 + y*w;
d[x] = rgb.r;
d[x+1] = rgb.g;
d[x+2] = rgb.b;
d[x+3] = 255; // alpha
}
});
} // function shimImgData(imgData) {...}
我使用 OpenGL 为这个问题创建了一个解决方案,并围绕它制作了一个命令行工具。它适用于图像和视频,是我发现的最快的工具。
Convert360 - 在 GitHub.
上的项目
OpenGL Shader - 用于重新投影的片段着色器。
用法很简单:
pip install convert360
convert360 -i ~/Pictures/Barcelona/sagrada-familia.jpg -o example.png -s 300 300
要得到这样的东西:
也许我在这里遗漏了什么。但似乎大多数(如果不是全部)呈现的转换代码可能有些不正确。他们采用球形全景图(等距柱状图——水平 360 度和垂直 180 度)并且似乎使用笛卡尔 <-> 圆柱变换转换为立方体面。他们不应该使用笛卡尔 <-> 球面变换吗?
我想只要他们逆向计算从立方体面到全景图,就应该可以解决。但在使用球面变换时,立方体面的图像可能会略有不同。
如果我从这个 equirectangular(球面全景图)开始:
然后如果我使用圆柱变换(目前我还不能 100% 确定它是正确的),我会得到这个结果:
但是如果我使用球面变换,我会得到这个结果:
它们不一样。但是我的球形变换结果似乎匹配 ,但他的 link 没有显示他正在使用的那种变换,我能读懂它。
那么我是否误解了该主题的许多贡献者所使用的代码?
kubi 可以将等距柱状图像转换为立方体面。我写它是为了快速和灵活。它提供了选择输出布局(默认为六张单独的图像)和决定重采样方法的选项。
我目前正在为一个网站开发一个简单的 3D 全景图查看器。出于移动性能原因,我使用 Three.js CSS 3 renderer。这需要一个立方体贴图,分成六个单独的图像。
我正在用 Google Photo Sphere, or similar apps that create 2:1 equirectangular panoramas. I then resize and convert these to a cubemap with this website: http://gonchar.me/panorama/ (Flash)
在 iPhone 上录制图像最好,我想自己进行转换,可以在Three.js 中即时进行,也可以在Photoshop 中进行。我找到了 Andrew Hazelden 的 Photoshop 动作,它们看起来有点接近,但不能直接转换。有没有一种数学方法来转换这些,或者某种脚本呢?如果可能,我想避免使用像 Blender 这样的 3D 应用程序。
也许这不太可能,但我想我会问的。我对 JavaScript 的经验还不错,但我对 Three.js 还很陌生。我也对依赖 WebGL 功能犹豫不决,因为它在移动设备上看起来不是很慢就是有问题。支持也仍然参差不齐。
如果你想在服务器端做,有很多选择。 ImageMagick 有一堆命令行工具可以将您的图像切成碎片。您可以将执行此操作的命令放入脚本中,然后 运行 每次您有一个新图像时。
很难说清楚程序中使用了什么算法。我们可以尝试通过将正方形网格输入程序来对正在发生的事情进行逆向工程。我用过 grid from Wikipedia:
给出:
这为我们提供了有关盒子构造方式的线索。
想象一个上面有经纬线的球体,以及一个围绕着它的立方体。现在从球体中心的点进行投影会在立方体上产生扭曲的网格。
在数学上,取极坐标 r, θ, ø,对于球体 r=1, 0 < θ < π, -π/4 < ø < 7π/4
- x= r sin θ cos ø
- y= r sin θ sin ø
- z= 余弦θ
将这些集中投影到多维数据集。首先我们按纬度划分为四个区域-π/4 < ø < π/4, π/4 < ø < 3π/4, 3π/4 < ø < 5π/4, 5π/4 < ø < 7π/4。这些将投射到顶部或底部的四个侧面之一。
假设我们在第一边 -π/4 < ø < π/4。的中心投影 (sin θ cos ø, sin θ sin ø, cos θ) 将是 (a sin θ cos ø, a sin θ sin ø, a cos θ) 当
- a sin θ cos ø = 1
所以
- a = 1 / (sin θ cos ø)
投影点是
- (1, tan ø, cot θ / cos ø)
如果| cot θ / cos ø | < 1,这将在正面。否则,它将投影在顶部或底部,您将需要一个不同的投影。更好的顶部测试使用 cos ø 的最小值将是 cos π/4 = 1/√2 的事实,因此如果 cot θ / (1/√2) > 1 或投影点始终位于顶部tan θ < 1/√2。计算结果为 θ < 35º 或 0.615 弧度。
把这个放在一起 Python:
import sys
from PIL import Image
from math import pi,sin,cos,tan
def cot(angle):
return 1/tan(angle)
# Project polar coordinates onto a surrounding cube
# assume ranges theta is [0,pi] with 0 the north poll, pi south poll
# phi is in range [0,2pi]
def projection(theta,phi):
if theta<0.615:
return projectTop(theta,phi)
elif theta>2.527:
return projectBottom(theta,phi)
elif phi <= pi/4 or phi > 7*pi/4:
return projectLeft(theta,phi)
elif phi > pi/4 and phi <= 3*pi/4:
return projectFront(theta,phi)
elif phi > 3*pi/4 and phi <= 5*pi/4:
return projectRight(theta,phi)
elif phi > 5*pi/4 and phi <= 7*pi/4:
return projectBack(theta,phi)
def projectLeft(theta,phi):
x = 1
y = tan(phi)
z = cot(theta) / cos(phi)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Left",x,y,z)
def projectFront(theta,phi):
x = tan(phi-pi/2)
y = 1
z = cot(theta) / cos(phi-pi/2)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Front",x,y,z)
def projectRight(theta,phi):
x = -1
y = tan(phi)
z = -cot(theta) / cos(phi)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Right",x,-y,z)
def projectBack(theta,phi):
x = tan(phi-3*pi/2)
y = -1
z = cot(theta) / cos(phi-3*pi/2)
if z < -1:
return projectBottom(theta,phi)
if z > 1:
return projectTop(theta,phi)
return ("Back",-x,y,z)
def projectTop(theta,phi):
# (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,1)
a = 1 / cos(theta)
x = tan(theta) * cos(phi)
y = tan(theta) * sin(phi)
z = 1
return ("Top",x,y,z)
def projectBottom(theta,phi):
# (a sin θ cos ø, a sin θ sin ø, a cos θ) = (x,y,-1)
a = -1 / cos(theta)
x = -tan(theta) * cos(phi)
y = -tan(theta) * sin(phi)
z = -1
return ("Bottom",x,y,z)
# Convert coords in cube to image coords
# coords is a tuple with the side and x,y,z coords
# edge is the length of an edge of the cube in pixels
def cubeToImg(coords,edge):
if coords[0]=="Left":
(x,y) = (int(edge*(coords[2]+1)/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Front":
(x,y) = (int(edge*(coords[1]+3)/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Right":
(x,y) = (int(edge*(5-coords[2])/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Back":
(x,y) = (int(edge*(7-coords[1])/2), int(edge*(3-coords[3])/2) )
elif coords[0]=="Top":
(x,y) = (int(edge*(3-coords[1])/2), int(edge*(1+coords[2])/2) )
elif coords[0]=="Bottom":
(x,y) = (int(edge*(3-coords[1])/2), int(edge*(5-coords[2])/2) )
return (x,y)
# convert the in image to out image
def convert(imgIn,imgOut):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
edge = inSize[0]/4 # the length of each edge in pixels
for i in xrange(inSize[0]):
for j in xrange(inSize[1]):
pixel = inPix[i,j]
phi = i * 2 * pi / inSize[0]
theta = j * pi / inSize[1]
res = projection(theta,phi)
(x,y) = cubeToImg(res,edge)
#if i % 100 == 0 and j % 100 == 0:
# print i,j,phi,theta,res,x,y
if x >= outSize[0]:
#print "x out of range ",x,res
x=outSize[0]-1
if y >= outSize[1]:
#print "y out of range ",y,res
y=outSize[1]-1
outPix[x,y] = pixel
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convert(imgIn,imgOut)
imgOut.show()
projection
函数采用立方体中的 theta
和 phi
值以及每个方向上从 -1 到 1 的 returns 坐标。 cubeToImg 获取 (x,y,z) 坐标并将它们转换为输出图像坐标。
上述算法似乎使用 image of buckingham palace 获得了正确的几何形状。我们得到:
这似乎得到了铺路中的大部分线条。
我们得到了一些图像人工制品。这是因为没有一对一的像素映射。我们需要做的是使用逆变换。我们不是循环遍历源中的每个像素并找到目标中的对应像素,而是循环遍历目标图像并找到最接近的对应源像素。
import sys
from PIL import Image
from math import pi,sin,cos,tan,atan2,hypot,floor
from numpy import clip
# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# face is face number
# edge is edge length
def outImgToXYZ(i,j,face,edge):
a = 2.0*float(i)/edge
b = 2.0*float(j)/edge
if face==0: # back
(x,y,z) = (-1.0, 1.0-a, 3.0 - b)
elif face==1: # left
(x,y,z) = (a-3.0, -1.0, 3.0 - b)
elif face==2: # front
(x,y,z) = (1.0, a - 5.0, 3.0 - b)
elif face==3: # right
(x,y,z) = (7.0-a, 1.0, 3.0 - b)
elif face==4: # top
(x,y,z) = (b-1.0, a -5.0, 1.0)
elif face==5: # bottom
(x,y,z) = (5.0-b, a-5.0, -1.0)
return (x,y,z)
# convert using an inverse transformation
def convertBack(imgIn,imgOut):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
edge = inSize[0]/4 # the length of each edge in pixels
for i in xrange(outSize[0]):
face = int(i/edge) # 0 - back, 1 - left 2 - front, 3 - right
if face==2:
rng = xrange(0,edge*3)
else:
rng = xrange(edge,edge*2)
for j in rng:
if j<edge:
face2 = 4 # top
elif j>=2*edge:
face2 = 5 # bottom
else:
face2 = face
(x,y,z) = outImgToXYZ(i,j,face2,edge)
theta = atan2(y,x) # range -pi to pi
r = hypot(x,y)
phi = atan2(z,r) # range -pi/2 to pi/2
# source img coords
uf = ( 2.0*edge*(theta + pi)/pi )
vf = ( 2.0*edge * (pi/2 - phi)/pi)
# Use bilinear interpolation between the four surrounding pixels
ui = floor(uf) # coord of pixel to bottom left
vi = floor(vf)
u2 = ui+1 # coords of pixel to top right
v2 = vi+1
mu = uf-ui # fraction of way across pixel
nu = vf-vi
# Pixel values of four corners
A = inPix[ui % inSize[0],clip(vi,0,inSize[1]-1)]
B = inPix[u2 % inSize[0],clip(vi,0,inSize[1]-1)]
C = inPix[ui % inSize[0],clip(v2,0,inSize[1]-1)]
D = inPix[u2 % inSize[0],clip(v2,0,inSize[1]-1)]
# interpolate
(r,g,b) = (
A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )
outPix[i,j] = (int(round(r)),int(round(g)),int(round(b)))
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
imgOut = Image.new("RGB",(inSize[0],inSize[0]*3/4),"black")
convertBack(imgIn,imgOut)
imgOut.save(sys.argv[1].split('.')[0]+"Out2.png")
imgOut.show()
结果是:
如果有人想倒车,请参阅this JS Fiddle page。
环境贴图有多种表现形式。这是一个很好的概述。
如果您使用 Photosphere(或与此相关的任何全景应用程序),您很可能已经拥有水平 latitude / longitude 表示。 然后你可以简单地绘制一个纹理 three.js SphereGeometry。这里有一个关于如何渲染地球的教程。
Tutorial - How to Make the Earth in WebGL?
祝你好运:).
我写了一个脚本将生成的立方体贴图切割成单独的文件(posx.png、negx.png、posy.png、negy.png、posz.png 和 negz.png).它还会将这 6 个文件打包成一个 .zip 文件。
来源在这里:https://github.com/dankex/compv/blob/master/3d-graphics/skybox/cubemap-cut.py
可以修改数组来设置图片文件:
name_map = [ \
["", "", "posy", ""],
["negz", "negx", "posz", "posx"],
["", "", "negy", ""]]
转换后的文件为:
首先:除非你真的必须自己转换图像(即,由于某些特定的软件要求),不要。
原因是,即使等角投影和立方投影之间有一个非常简单的映射,区域之间的映射并不简单:当你建立一个对应关系目标图像的特定点和源中的点进行基本计算,只要通过四舍五入将这两个点都转换为像素 你正在做 very raw不考虑像素大小的近似值,图像质量必然偏低
第二:即使你需要在运行时进行转换,你确定你真的需要进行转换吗?除非有一些非常严格的性能问题,否则如果你只需要一个天空盒,创建一个非常大的球体,在上面缝合 equirectangular 纹理,然后就可以了。据我所知,Three.js 已经提供了球体 ;-)
第三:NASA 提供了一种工具,可以在所有可以想象的投影之间进行转换(我刚刚发现并测试了它,并且非常有效)。你可以在这里找到它:
G.Projector — Global Map Projector
而且我认为这些人知道他们在做什么是有道理的;-)
事实证明,“伙计们”知道他们在某些时候做了什么:生成的立方体贴图有一个丑陋的边框,这使得转换不是那么容易...
我找到了将等距柱状图转换为立方体贴图的权威工具,它叫做 erect2cubic
。
这是一个小实用程序,可以生成一个脚本以提供给 hugin,以这种方式:
erect2cubic --erect=input.png --ptofile=cube.pto
nona -o cube_prefix cube.pto
(从 Vinay's Hacks 页面窃取的信息)
它会生成所有六个立方体贴图面。我将它用于我的项目,它 非常有用!
这种方法的唯一缺点是脚本 erect2cubit
不在标准 Ubuntu 发行版中(这是我正在使用的),我不得不求助于 a blog describing how to install and use erect2cubic 了解安装方法。
绝对值得!
鉴于优秀的接受答案,我想添加相应的 C++ implementation, based on OpenCV。
对于那些不熟悉 OpenCV 的人,请将 Mat
视为图像。我们首先构建两个贴图,从等距柱状图像重新映射到相应的立方体贴图面。然后,我们使用 OpenCV 进行繁重的工作(即使用插值重新映射)。
如果不关心可读性,代码可以做得更紧凑。
// Define our six cube faces.
// 0 - 3 are side faces, clockwise order
// 4 and 5 are top and bottom, respectively
float faceTransform[6][2] =
{
{0, 0},
{M_PI / 2, 0},
{M_PI, 0},
{-M_PI / 2, 0},
{0, -M_PI / 2},
{0, M_PI / 2}
};
// Map a part of the equirectangular panorama (in) to a cube face
// (face). The ID of the face is given by faceId. The desired
// width and height are given by width and height.
inline void createCubeMapFace(const Mat &in, Mat &face,
int faceId = 0, const int width = -1,
const int height = -1) {
float inWidth = in.cols;
float inHeight = in.rows;
// Allocate map
Mat mapx(height, width, CV_32F);
Mat mapy(height, width, CV_32F);
// Calculate adjacent (ak) and opposite (an) of the
// triangle that is spanned from the sphere center
//to our cube face.
const float an = sin(M_PI / 4);
const float ak = cos(M_PI / 4);
const float ftu = faceTransform[faceId][0];
const float ftv = faceTransform[faceId][1];
// For each point in the target image,
// calculate the corresponding source coordinates.
for(int y = 0; y < height; y++) {
for(int x = 0; x < width; x++) {
// Map face pixel coordinates to [-1, 1] on plane
float nx = (float)y / (float)height - 0.5f;
float ny = (float)x / (float)width - 0.5f;
nx *= 2;
ny *= 2;
// Map [-1, 1] plane coords to [-an, an]
// thats the coordinates in respect to a unit sphere
// that contains our box.
nx *= an;
ny *= an;
float u, v;
// Project from plane to sphere surface.
if(ftv == 0) {
// Center faces
u = atan2(nx, ak);
v = atan2(ny * cos(u), ak);
u += ftu;
} else if(ftv > 0) {
// Bottom face
float d = sqrt(nx * nx + ny * ny);
v = M_PI / 2 - atan2(d, ak);
u = atan2(ny, nx);
} else {
// Top face
float d = sqrt(nx * nx + ny * ny);
v = -M_PI / 2 + atan2(d, ak);
u = atan2(-ny, nx);
}
// Map from angular coordinates to [-1, 1], respectively.
u = u / (M_PI);
v = v / (M_PI / 2);
// Warp around, if our coordinates are out of bounds.
while (v < -1) {
v += 2;
u += 1;
}
while (v > 1) {
v -= 2;
u += 1;
}
while(u < -1) {
u += 2;
}
while(u > 1) {
u -= 2;
}
// Map from [-1, 1] to in texture space
u = u / 2.0f + 0.5f;
v = v / 2.0f + 0.5f;
u = u * (inWidth - 1);
v = v * (inHeight - 1);
// Save the result for this pixel in map
mapx.at<float>(x, y) = u;
mapy.at<float>(x, y) = v;
}
}
// Recreate output image if it has wrong size or type.
if(face.cols != width || face.rows != height ||
face.type() != in.type()) {
face = Mat(width, height, in.type());
}
// Do actual resampling using OpenCV's remap
remap(in, face, mapx, mapy,
CV_INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 0, 0));
}
给定以下输入:
生成了以下面孔:
图片由 Optonaut 提供。
cmft Studio 支持 conversion/filtering
各种 HDR/LDR
到 cubemaps
的投影。
更新 2:看起来其他人的 already built a far superior web application 比我自己的多。他们的转换在客户端运行,因此无需担心任何上传和下载。
我想如果您出于某种原因讨厌 JavaScript,或者正试图在您的手机上执行此操作,那么我下面的网络应用程序是可以的。
更新:我已经在 ZIP 文件中发布了 a simple web application where you can upload a panorama and have it return the six skybox 张图片。
源代码是以下内容的清理重新实现,是 available on GitHub。
该应用程序目前 运行 在单个免费层 Heroku dyno 上,但请不要尝试将其用作 API。如果你想要自动化,请自己部署; 单击部署到 Heroku 可用。
原始:这是
除了大多数用例可能需要六张单独的图像这一事实之外,一次转换一张脸的主要优点是它可以大大减少处理大图像的内存占用。
#!/usr/bin/env python
import sys
from PIL import Image
from math import pi, sin, cos, tan, atan2, hypot, floor
from numpy import clip
# get x,y,z coords from out image pixels coords
# i,j are pixel coords
# faceIdx is face number
# faceSize is edge length
def outImgToXYZ(i, j, faceIdx, faceSize):
a = 2.0 * float(i) / faceSize
b = 2.0 * float(j) / faceSize
if faceIdx == 0: # back
(x,y,z) = (-1.0, 1.0 - a, 1.0 - b)
elif faceIdx == 1: # left
(x,y,z) = (a - 1.0, -1.0, 1.0 - b)
elif faceIdx == 2: # front
(x,y,z) = (1.0, a - 1.0, 1.0 - b)
elif faceIdx == 3: # right
(x,y,z) = (1.0 - a, 1.0, 1.0 - b)
elif faceIdx == 4: # top
(x,y,z) = (b - 1.0, a - 1.0, 1.0)
elif faceIdx == 5: # bottom
(x,y,z) = (1.0 - b, a - 1.0, -1.0)
return (x, y, z)
# convert using an inverse transformation
def convertFace(imgIn, imgOut, faceIdx):
inSize = imgIn.size
outSize = imgOut.size
inPix = imgIn.load()
outPix = imgOut.load()
faceSize = outSize[0]
for xOut in xrange(faceSize):
for yOut in xrange(faceSize):
(x,y,z) = outImgToXYZ(xOut, yOut, faceIdx, faceSize)
theta = atan2(y,x) # range -pi to pi
r = hypot(x,y)
phi = atan2(z,r) # range -pi/2 to pi/2
# source img coords
uf = 0.5 * inSize[0] * (theta + pi) / pi
vf = 0.5 * inSize[0] * (pi/2 - phi) / pi
# Use bilinear interpolation between the four surrounding pixels
ui = floor(uf) # coord of pixel to bottom left
vi = floor(vf)
u2 = ui+1 # coords of pixel to top right
v2 = vi+1
mu = uf-ui # fraction of way across pixel
nu = vf-vi
# Pixel values of four corners
A = inPix[ui % inSize[0], clip(vi, 0, inSize[1]-1)]
B = inPix[u2 % inSize[0], clip(vi, 0, inSize[1]-1)]
C = inPix[ui % inSize[0], clip(v2, 0, inSize[1]-1)]
D = inPix[u2 % inSize[0], clip(v2, 0, inSize[1]-1)]
# interpolate
(r,g,b) = (
A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu+D[0]*mu*nu,
A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu+D[1]*mu*nu,
A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu+D[2]*mu*nu )
outPix[xOut, yOut] = (int(round(r)), int(round(g)), int(round(b)))
imgIn = Image.open(sys.argv[1])
inSize = imgIn.size
faceSize = inSize[0] / 4
components = sys.argv[1].rsplit('.', 2)
FACE_NAMES = {
0: 'back',
1: 'left',
2: 'front',
3: 'right',
4: 'top',
5: 'bottom'
}
for face in xrange(6):
imgOut = Image.new("RGB", (faceSize, faceSize), "black")
convertFace(imgIn, imgOut, face)
imgOut.save(components[0] + "_" + FACE_NAMES[face] + "." + components[1])
一个非常简单的 C++ 应用程序,它根据
这是 convertFace
需要传递两个 ìmageData
对象和一个人脸 ID (0-6)。
所提供的代码可以安全地用于 Web Worker,因为它没有依赖项。
// convert using an inverse transformation
function convertFace(imgIn, imgOut, faceIdx) {
var inPix = shimImgData(imgIn),
outPix = shimImgData(imgOut),
faceSize = imgOut.width,
pi = Math.PI,
pi_2 = pi/2;
for(var xOut=0; xOut<faceSize; xOut++) {
for(var yOut=0; yOut<faceSize; yOut++) {
var xyz = outImgToXYZ(xOut, yOut, faceIdx, faceSize);
var theta = Math.atan2(xyz.y, xyz.x); // range -pi to pi
var r = Math.hypot(xyz.x, xyz.y);
var phi = Math.atan2(xyz.z, r); // range -pi/2 to pi/2
// source image coordinates
var uf = 0.5 * imgIn.width * (theta + pi) / pi;
var vf = 0.5 * imgIn.width * (pi_2 - phi) / pi;
// Use bilinear interpolation between the four surrounding pixels
var ui = Math.floor(uf); // coordinate of pixel to bottom left
var vi = Math.floor(vf);
var u2 = ui + 1; // coordinates of pixel to top right
var v2 = vi + 1;
var mu = uf - ui; // fraction of way across pixel
var nu = vf - vi;
// Pixel values of four corners
var A = inPix.getPx(ui % imgIn.width, clip(vi, 0, imgIn.height-1));
var B = inPix.getPx(u2 % imgIn.width, clip(vi, 0, imgIn.height-1));
var C = inPix.getPx(ui % imgIn.width, clip(v2, 0, imgIn.height-1));
var D = inPix.getPx(u2 % imgIn.width, clip(v2, 0, imgIn.height-1));
// interpolate
var rgb = {
r:A[0]*(1-mu)*(1-nu) + B[0]*(mu)*(1-nu) + C[0]*(1-mu)*nu + D[0]*mu*nu,
g:A[1]*(1-mu)*(1-nu) + B[1]*(mu)*(1-nu) + C[1]*(1-mu)*nu + D[1]*mu*nu,
b:A[2]*(1-mu)*(1-nu) + B[2]*(mu)*(1-nu) + C[2]*(1-mu)*nu + D[2]*mu*nu
};
rgb.r = Math.round(rgb.r);
rgb.g = Math.round(rgb.g);
rgb.b = Math.round(rgb.b);
outPix.setPx(xOut, yOut, rgb);
} // for(var yOut=0; yOut<faceSize; yOut++) {...}
} // for(var xOut=0;xOut<faceSize;xOut++) {...}
} // function convertFace(imgIn, imgOut, faceIdx) {...}
// get x, y, z coordinates from out image pixels coordinates
// i,j are pixel coordinates
// faceIdx is face number
// faceSize is edge length
function outImgToXYZ(i, j, faceIdx, faceSize) {
var a = 2 * i / faceSize,
b = 2 * j / faceSize;
switch(faceIdx) {
case 0: // back
return({x:-1, y:1-a, z:1-b});
case 1: // left
return({x:a-1, y:-1, z:1-b});
case 2: // front
return({x: 1, y:a-1, z:1-b});
case 3: // right
return({x:1-a, y:1, z:1-b});
case 4: // top
return({x:b-1, y:a-1, z:1});
case 5: // bottom
return({x:1-b, y:a-1, z:-1});
}
} // function outImgToXYZ(i, j, faceIdx, faceSize) {...}
function clip(val, min, max) {
return(val<min ? min : (val>max ? max : val));
}
function shimImgData(imgData) {
var w = imgData.width*4,
d = imgData.data;
return({
getPx:function(x, y) {
x = x*4 + y*w;
return([d[x], d[x+1], d[x+2]]);
},
setPx:function(x, y, rgb) {
x = x*4 + y*w;
d[x] = rgb.r;
d[x+1] = rgb.g;
d[x+2] = rgb.b;
d[x+3] = 255; // alpha
}
});
} // function shimImgData(imgData) {...}
我使用 OpenGL 为这个问题创建了一个解决方案,并围绕它制作了一个命令行工具。它适用于图像和视频,是我发现的最快的工具。
Convert360 - 在 GitHub.
上的项目OpenGL Shader - 用于重新投影的片段着色器。
用法很简单:
pip install convert360
convert360 -i ~/Pictures/Barcelona/sagrada-familia.jpg -o example.png -s 300 300
要得到这样的东西:
也许我在这里遗漏了什么。但似乎大多数(如果不是全部)呈现的转换代码可能有些不正确。他们采用球形全景图(等距柱状图——水平 360 度和垂直 180 度)并且似乎使用笛卡尔 <-> 圆柱变换转换为立方体面。他们不应该使用笛卡尔 <-> 球面变换吗?
我想只要他们逆向计算从立方体面到全景图,就应该可以解决。但在使用球面变换时,立方体面的图像可能会略有不同。
如果我从这个 equirectangular(球面全景图)开始:
然后如果我使用圆柱变换(目前我还不能 100% 确定它是正确的),我会得到这个结果:
但是如果我使用球面变换,我会得到这个结果:
它们不一样。但是我的球形变换结果似乎匹配
那么我是否误解了该主题的许多贡献者所使用的代码?
kubi 可以将等距柱状图像转换为立方体面。我写它是为了快速和灵活。它提供了选择输出布局(默认为六张单独的图像)和决定重采样方法的选项。