UPDATE 2: It appears that another individual has already created a more advanced web application than my own. Their conversion process is done client-side, eliminating any concerns about uploads and downloads.
If you have a dislike for JavaScript or need to use this on your mobile device, then the web application I've provided below might be suitable.
UPDATE: I've introduced a basic web application where you can submit a panorama and receive back the six skybox images in a ZIP file. You can access it here.
This source code is a refined version of what's included here, and it's open-source on GitHub which you can find here.
The current operation of the application is reliant on a single free-tier Heroku dyno. However, please refrain from utilizing it as an API. If automation is necessary, deploy your own version; a one-click "Deploy to Heroku" option is available.
Original: This is a modified take on Salix Alba's fantastic answer which converts each face individually, producing six distinct images while preserving the original image format.
Aside from most scenarios expecting multiple images, processing one face at a time significantly reduces memory usage when working with large images.
#!/usr/bin/env python
import sys
from PIL import Image
from math import pi, sin, cos, tan, atan2, hypot, floor
from numpy import clip
# Function to transform output image pixel coordinates to x,y,z coords
# (i,j) are pixel coords
# faceIdx represents 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:
(x,y,z) = (-1.0, 1.0 - a, 1.0 - b)
elif faceIdx == 1:
(x,y,z) = (a - 1.0, -1.0, 1.0 - b)
elif faceIdx == 2:
(x,y,z) = (1.0, a - 1.0, 1.0 - b)
elif faceIdx == 3:
(x,y,z) = (1.0 - a, 1.0, 1.0 - b)
elif faceIdx == 4:
(x,y,z) = (b - 1.0, a - 1.0, 1.0)
elif faceIdx == 5:
(x,y,z) = (1.0 - b, a - 1.0, -1.0)
return (x, y, z)
# Convert using inverse transformation technique
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)
r = hypot(x,y)
phi = atan2(z,r)
uf = 0.5 * inSize[0] * (theta + pi) / pi
vf = 0.5 * inSize[0] * (pi/2 - phi) / pi
ui = floor(uf)
vi = floor(vf)
u2 = ui+1
v2 = vi+1
mu = uf-ui
nu = vf-vi
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)]
(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])