私は現在、ウェブサイト用のシンプルな3Dパノラマビューアを制作しています。モバイルのパフォーマンス上の理由から、私は three.js
CSS3 renderer を使用しています。これは、6つの単一画像に分割されたキューブマップを必要とします。
iPhoneのGoogle Photosphereアプリや、2:1の等緯経度パノラマを作成する同様のアプリで画像を記録しています。その後、このウェブサイトでリサイズしてキューブマップに変換しています。http://gonchar.me/panorama/ (Flash)
できれば、three.jsでその場で変換するか、Photoshopで変換したいのですが、可能でしょうか?Andrew HazeldenのPhotoshopアクションを見つけ、それらはある種近いように見えますが、直接の変換はできません。これらを変換する数学的な方法、あるいはそれを行うスクリプトのようなものはあるのでしょうか?できればBlenderのような3Dアプリを経由するのは避けたいのですが。
もしかしたら、これは遠回しに言っているのかもしれませんが、私は聞いてみようと思いました。javascriptの経験はそこそこありますが、three.js
はかなり初めてです。また、モバイルデバイスでは遅いかバグがあるように見えるので、WebGL機能に依存することを躊躇しています。また、サポートはまだ不安定です。
http://www.imagemagick.org/ には画像を断片化するためのコマンドラインツールがたくさんあります。これを行うコマンドをスクリプトに記述して、新しい画像を作成するたびにそれを実行すればよいのです。
このプログラムでどのようなアルゴリズムが使われているのかを知るのは難しいです。正方形のグリッドをプログラムに入力することで、何が起こっているのかをリバースエンジニアリングしてみることができます。私は、wikipediaのグリッドを使いました。
64×64のグリッド]2を使いました。
これは、箱がどのように構成されているかを知る手がかりになります。
緯線と経線を持つ球体と、それを取り囲む立方体をイメージしてください。球の中心の点から投影すると、立方体の上に歪んだ格子ができます。
数学的に極座標r, θ, øをとると、球面r=1, 0 < θ < π, -π/4 < ø < 7π/4 となる。
を中心にして立方体に投影する。まず、緯度 -π/4 < ø < π/4, π/4 < ø < 3π/4, 3π/4 < ø < 5π/4, 5π/4 < ø < 7π/4 で4つの領域に分割する。これらは、4つの側面、上面、下面のいずれかに突出することになります。
最初の辺-π/4 < ø < π/4にいると仮定します。の中心投影は (sin θ cos ø, sin θ sin ø, cos θ) は、(a sin θ cos ø, a sin θ sin ø, a cos θ) となり、x=1 平面にぶつかるのは、次のときです。
そこで
となり、投影点は
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 までの立方体上の座標を返します。cubeToImg は、 (x,y,z) 座標を受け取り、それを出力画像の座標に変換します。
上記のアルゴリズムは、バッキンガム宮殿の画像を使って正しいジオメトリを得るようです。 バッキンガム宮殿の立方体地図]5です。 これは、舗装の線のほとんどを正しく表示しているようです。
しかし、いくつかの画像の不具合が生じています。これは、ピクセルの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()
この結果は、逆変換を使用するです。
生成されたCubemapを個々のファイル(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][3]! 画像の説明を入力してください[4] [画像の説明を入力してください][4][[画像の説明を入力してください]]。 画像の説明を入力してください[5] [画像の説明を入力してください[[画像の説明を入力してください]]。 画像の説明を入力してください6 [!]
環境マップには様々な表現があります。ここでは、その概要を紹介します。
Photosphere(または他のパノラマアプリ)を使用している場合、ほとんどの場合、水平方向の緯度/経度の表現が既にあります。 そして、テクスチャ付きのthree.js SphereGeometryを描けばよいのです。地球のレンダリング方法についてのチュートリアルはこちらです。
幸運を祈ります :).