我目前正在为网站制作简单的3D全景查看器。出于移动设备原因,我使用three.js
CSS3 renderer。这需要一个立方体贴图,分成6个单个图像。
我使用Google相册应用程序在iPhone上录制图像,或创建2:1 equirectangular全景图像的类似应用程序。然后,我使用此网站调整大小并将其转换为立方体贴图:http://gonchar.me/panorama/(Flash)
首先,我想自己进行转换,或者在three.js中动态,如果可能的话,或者在Photoshop中。我找到了Andrew Hazelden的Photoshop动作,它们似乎很接近,但没有直接转换。是否有一种数学方法来转换这些,或某种类型的脚本?如果可能的话,我想避免像Blender这样的3D应用程序。
也许这是一个很长的镜头,但我想我会问。我对javascript有很好的体验,但我对three.js
很新。我也对依赖WebGL功能犹豫不决,因为它似乎在移动设备上看起来很慢或有问题。支持仍然不稳定。
答案 0 :(得分:73)
如果你想做服务器端,有很多选择。 http://www.imagemagick.org/有一堆命令行工具可以将图像分割成碎片。您可以将命令放入脚本中,并在每次有新图像时运行它。
很难说清楚程序中使用了什么算法。我们可以通过在程序中输入正方形网格来尝试逆向工程。我使用了grid from wikipedia
这给出了这给了我们一个关于如何构造盒子的线索。
成像球体,其中包含纬度和经度线,以及围绕它的立方体。现在,从球体中心点开始的项目会在立方体上产生扭曲的网格。
对于球体r = 1,0,
以数学方式取极坐标r,θ,ø。 θ< π,-π/ 4< ø< 7π/ 4
将这些内容集中投影到多维数据集。首先,我们通过纬度-π/ 4 <4分成四个区域。 ø&lt; π/ 4,π/ 4&lt; ø&lt; 3π/4,3π/ 4&lt; ø&lt; 5π/4,5π/ 4&lt; ø&lt; 7π/ 4。这些将投射到顶部或底部的四个侧面之一。
假设我们处于第一侧-π/ 4&lt; ø&lt; π/ 4。中心投影 (sinθcosø,sinθsinø,cosθ)将是(asinθcosø,sinθsinø,cosθ),它在<= p>时击中x = 1平面
所以
,投影点是
如果| cotθ/cosø| &LT; 1这将在正面。否则,它将被投影在顶部或底部,您将需要一个不同的投影。对顶部的更好测试使用cosø的最小值将是cosπ/ 4 = 1 /√2的事实,因此如果cotθ/(1 /√2)>,则投影点总是在顶部。 1或tanθ<1。 1 /√2。这可以作为θ&lt; 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。 cubeToImg采用(x,y,z)坐标并将它们转换为输出图像坐标。
上述算法似乎使用我们得到的image of buckingham palace来确定几何图形 这似乎得到了正确铺设的大部分线条。
我们正在获得一些图像文物。这是因为没有1比1的像素图。我们需要做的是使用逆变换。而不是遍历源中的每个像素并找到目标中的相应像素,我们循环通过目标图像并找到最接近的相应源像素。
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()
结果为
答案 1 :(得分:11)
鉴于优秀的答案,我想基于 OpenCV 添加相应的 c ++实现。
对于那些不熟悉OpenCV的人,请将Mat
视为图像。我们首先构造两个从equirectangular图像重映射到我们相应的立方体贴图面的贴图。然后,我们使用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提供。
答案 2 :(得分:9)
我编写了一个脚本,将生成的立方体贴图剪切成单个文件(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", ""]]
转换后的文件是:
答案 3 :(得分:7)
这是Salix Alba's absolutely fantastic answer的一个(天真)修改版本,它一次转换一个面,吐出六个不同的图像并保留原始图像的文件类型。
除了大多数用例可能需要六个单独的图像之外,一次转换一个面的主要优点是它使得处理大图像的内存密集程度更低。
transition.redirect('/login');
答案 4 :(得分:6)
发现了这个问题,尽管答案很好,但我认为还有一些问题尚未解决,所以这是我的两分钱。
首先:除非您真的必须自己转换图像(即由于某些特定的软件要求),不要。
原因是,即使在equirectangular投影和立方投影之间有一个非常简单的映射,区域之间的映射也不简单:当你建立一个特定点之间的对应关系时目标图像和源中具有基本计算的点,只要您通过舍入将两个点转换为像素,就会执行非常原始逼近,而不考虑像素的大小,图像的质量必然很低。
第二:即使您需要在运行时进行转换,您确定需要进行转换吗?除非存在一些非常严格的性能问题,否则如果你只需要一个天空盒,就要创建一个非常大的球体,在它上面拼接等长的纹理,然后离开你。据我记得,三个JS已经提供了球体; - )
第三:NASA提供了一种工具,可以在所有可以想象的投影之间进行转换(我刚刚发现,测试它,并且像魅力一样工作)。你可以在这里找到它:G.Projector — Global Map Projector
我觉得有理由认为这些人知道他们在做什么; - )
希望这有帮助
更新:事实证明“ guys ”知道他们在某些方面做了什么:生成的立方体贴图有一个可怕的边框,这使得转换不那么容易...
UPDATE 2:找到了equirectangular to cubemap转换的权威工具,它被称为erect2cubic
。
这是一个小实用程序,它以这种方式生成一个脚本供给hugin:
$ erect2cubic --erect=input.png --ptofile=cube.pto
$ nona -o cube_prefix cube.pto
(从Vinay's Hacks页面获取的信息)
将生成所有6个立方体贴图面。我正在将它用于我的项目,就像一个魅力!
这种方法的唯一缺点是脚本erect2cubit
它不在标准的Ubuntu发行版中(我正在使用它),我不得不求助于此链接中的说明:
Blog describing how to install and use erect2cubic
了解如何安装它。
完全值得!
答案 5 :(得分:1)
cmft 工作室支持conversion/filtering
对HDR/LDR
的各种cubemaps
投影{。}}。
http://grokbase.com/t/perl/beginners/096pcz62bk/redirecting-stderr-with-io-tee
答案 6 :(得分:0)
环境地图有各种表示形式。这是一个很好的概述。
如果您使用Photosphere(或任何全景应用程序),您很可能已经拥有水平latitude / longitude表示。 然后,您可以简单地绘制纹理three.js SphereGeometry。这是一个关于如何渲染地球的教程。
Tutorial - How to Make the Earth in WebGL?
祝你好运:)。
答案 7 :(得分:0)
一个非常简单的C ++应用程序,它根据Salix Alba =&gt;的答案将equirectangular全景转换为立方体贴图。 https://github.com/denivip/panorama
答案 8 :(得分:0)
这是Benjamn Dobell代码的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 img coords
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); // coord of pixel to bottom left
var vi = Math.floor(vf);
var u2 = ui+1; // coords 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 coords from out image pixels coords
// i,j are pixel coords
// 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) {...}
答案 9 :(得分:0)
我使用OpenGL为这个问题创建了一个解决方案,并围绕它创建了一个命令行工具。它适用于图像和视频,它是我在那里发现的最快的工具。
Convert360 - 关于GitHub的项目。
OpenGL Shader - 用于重新投影的片段着色器。
用法非常简单:
$ pip install convert360
$ convert360 -i ~/Pictures/Barcelona/sagrada-familia.jpg -o example.png -s 300 300
得到这样的东西:
答案 10 :(得分:0)
也许我在这里遗漏了一些东西。但似乎大多数(如果不是全部)呈现的转换代码可能有些不正确。它们采用球形全景(equirectangular ---水平360度和垂直180度)并且似乎使用笛卡尔&lt; - &gt;转换为立方体面。圆柱形变换。如果他们不使用笛卡儿&lt; - &gt;球形变换。见http://mathworld.wolfram.com/SphericalCoordinates.html
我认为,只要他们将计算反转为从立方体面到全景,那么它应该可以解决。但是,当使用球形变换时,立方体面的图像可能略有不同。
如果我从这个equirectangular(球形全景)开始:
然后,如果我使用圆柱形变换(我目前不是100%确定是正确的),我得到这个结果:
但如果我使用球形变换,我得到这个结果:
他们不一样。但是我的球形转换结果似乎与Danke Xie的结果相符,但他的链接并没有显示他正在使用的那种转换,我最好能读懂它。
我是否误解了该主题的许多贡献者所使用的代码?
答案 11 :(得分:0)
kubi 可以将等距柱状图转换为立方体面。我写它是为了快速和灵活。它提供了选择输出布局的选项(默认为 6 个单独的图像)并决定重采样方法。