├── vrProjectorCmd ├── images ├── back.png ├── front.png ├── left.png ├── right.png ├── top.png ├── bottom.png ├── equirectangular.png └── sidebysidefisheye.png ├── vrProjector ├── __init__.py ├── EquirectangularProjection.py ├── FisheyeProjection.py ├── SideBySideFisheyeProjection.py ├── CubemapProjection.py └── AbstractProjection.py ├── test.py ├── vrProjectorWrapper.py ├── README.md └── LICENSE.txt /vrProjectorCmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python vrProjectorWrapper.py "$@" 4 | -------------------------------------------------------------------------------- /images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/back.png -------------------------------------------------------------------------------- /images/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/front.png -------------------------------------------------------------------------------- /images/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/left.png -------------------------------------------------------------------------------- /images/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/right.png -------------------------------------------------------------------------------- /images/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/top.png -------------------------------------------------------------------------------- /images/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/bottom.png -------------------------------------------------------------------------------- /images/equirectangular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/equirectangular.png -------------------------------------------------------------------------------- /images/sidebysidefisheye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhautikj/vrProjector/HEAD/images/sidebysidefisheye.png -------------------------------------------------------------------------------- /vrProjector/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | from .AbstractProjection import AbstractProjection 13 | from .EquirectangularProjection import EquirectangularProjection 14 | from .SideBySideFisheyeProjection import SideBySideFisheyeProjection 15 | from .CubemapProjection import CubemapProjection 16 | from .FisheyeProjection import FisheyeProjection -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import vrProjector 2 | 3 | eq = vrProjector.EquirectangularProjection() 4 | eq.loadImage("images/equirectangular.png") 5 | # eq.set_use_bilinear(True) 6 | cb = vrProjector.CubemapProjection() 7 | cb.initImages(256,256) 8 | cb.reprojectToThis(eq) 9 | cb.saveImages("front.png", "right.png", "back.png", "left.png", "top.png", "bottom.png") 10 | 11 | 12 | # 13 | # sbs = SideBySideFisheyeProjection() 14 | # sbs.initImage(2048, 1024) 15 | ## sbs.reprojectToThisThreaded(eq, 8) 16 | # sbs.reprojectToThis(eq) 17 | # sbs.saveImage("foo.png") 18 | # 19 | # sbs2 = SideBySideFisheyeProjection() 20 | # sbs2.loadImage("foo.png") 21 | # 22 | # eq2 = EquirectangularProjection() 23 | # eq2.initImage(2048,1024) 24 | # eq2.reprojectToThis(sbs2) 25 | # eq2.saveImage("foo2.png") 26 | 27 | # eq = EquirectangularProjection() 28 | # eq.loadImage("cuber.jpg") 29 | # eq.set_use_bilinear(True) 30 | # cb = CubemapProjection() 31 | # cb.initImages(256,256) 32 | # cb.reprojectToThis(eq) 33 | # cb.saveImages("front.png", "right.png", "back.png", "left.png", "top.png", "bottom.png") 34 | # 35 | 36 | # cb2 = CubemapProjection() 37 | # cb2.loadImages("front.png", "right.png", "back.png", "left.png", "top.png", "bottom.png") 38 | # eq2 = EquirectangularProjection() 39 | # eq2.initImage(2048,1024) 40 | # eq2.reprojectToThis(cb2) 41 | # eq2.saveImage("foo.png") 42 | -------------------------------------------------------------------------------- /vrProjector/EquirectangularProjection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | from .AbstractProjection import AbstractProjection 14 | import math 15 | 16 | class EquirectangularProjection(AbstractProjection): 17 | def __init__(self): 18 | AbstractProjection.__init__(self) 19 | 20 | def set_angular_resolution(self): 21 | self.angular_resolution = math.pi/self.imsize[1] 22 | 23 | def _pixel_value(self, angle): 24 | theta = angle[0] 25 | phi = angle[1] 26 | if theta is None or phi is None: 27 | return (0,0,0) 28 | # theta: -pi..pi -> u: 0..1 29 | u = 0.5+0.5*(theta/math.pi) 30 | # phi: -pi/2..pi/2 -> v: 0..1 31 | v = 0.5+(phi/math.pi) 32 | return self.get_pixel_from_uv(u,v, self.image) 33 | 34 | @staticmethod 35 | def angular_position(texcoord): 36 | u = texcoord[0] 37 | v = texcoord[1] 38 | # theta: u: 0..1 -> -pi..pi 39 | theta = math.pi*2.0*(u-0.5) 40 | # phi: v: 0..1 - > -pi/2..pi/2 41 | phi = math.pi*(v-0.5) 42 | return (theta,phi) 43 | -------------------------------------------------------------------------------- /vrProjector/FisheyeProjection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | from .AbstractProjection import AbstractProjection 14 | import math 15 | 16 | class FisheyeProjection(AbstractProjection): 17 | def __init__(self): 18 | AbstractProjection.__init__(self) 19 | 20 | def set_angular_resolution(self): 21 | self.angular_resolution = math.pi/self.imsize[1] 22 | 23 | def _pixel_value(self, angle): 24 | FOV = math.pi 25 | 26 | theta = angle[0] * 0.5 27 | phi = angle[1] 28 | if theta is None or phi is None: 29 | return (0,0,0) 30 | 31 | 32 | # phi: -pi/2..pi/2 33 | # theta: -pi..pi 34 | 35 | # using convention from http://paulbourke.net/dome/fish2/ 36 | pt = self.point_on_sphere(theta, phi) 37 | p_y = pt[0] 38 | p_x = pt[1] 39 | p_z = pt[2] 40 | 41 | theta_l = math.atan2(p_z,p_x); 42 | phi_l = math.atan2(math.sqrt(p_x*p_x+p_z*p_z),p_y); 43 | r = phi_l / FOV; 44 | 45 | u = 0.5 + r * math.cos(theta_l); 46 | v = 0.5 + r * math.sin(theta_l); 47 | 48 | return self.get_pixel_from_uv(u,v, self.image) 49 | 50 | @staticmethod 51 | def angular_position(texcoord): 52 | u = texcoord[0] 53 | v = texcoord[1] 54 | # theta: u: 0..1 -> -pi..pi 55 | theta = math.pi*2.0*(u-0.5) 56 | # phi: v: 0..1 - > -pi/2..pi/2 57 | phi = math.pi*(v-0.5) 58 | return (theta,phi) 59 | -------------------------------------------------------------------------------- /vrProjector/SideBySideFisheyeProjection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | from .AbstractProjection import AbstractProjection 14 | import math 15 | 16 | class SideBySideFisheyeProjection(AbstractProjection): 17 | def __init__(self): 18 | AbstractProjection.__init__(self) 19 | 20 | def set_angular_resolution(self): 21 | self.angular_resolution = math.pi/self.imsize[1] 22 | 23 | def _pixel_value(self, angle): 24 | theta = angle[0] 25 | phi = angle[1] 26 | if theta is None or phi is None: 27 | return (0,0,0) 28 | 29 | r = math.cos(phi) 30 | # z is elevation in this case 31 | sphere_pnt = self.point_on_sphere(theta, phi) 32 | 33 | # sphere_pnt.x: [-1..1] 34 | u = 0.5+(sphere_pnt[0]*-0.5) 35 | if theta>=0: 36 | u = u*0.5 + 0.5 37 | else: 38 | u = (1.0-u)*0.5 39 | 40 | #sphere_pnt.z: -1..1 -> v: 0..1 41 | v = 0.5+(sphere_pnt[2]*0.5) 42 | 43 | return self.get_pixel_from_uv(u,v, self.image) 44 | 45 | @staticmethod 46 | def angular_position(texcoord): 47 | up = texcoord[0] 48 | v = texcoord[1] 49 | # correct for hemisphere 50 | if up>=0.5: 51 | u = 2.0*(up-0.5) 52 | else: 53 | u = 2.0*up 54 | 55 | # ignore points outside of circles 56 | if ((u-0.5)*(u-0.5) + (v-0.5)*(v-0.5))>0.25: 57 | return None, None 58 | 59 | # v: 0..1-> vp: -1..1 60 | phi = math.asin(2.0*(v-0.5)) 61 | 62 | # u = math.cos(phi)*math.cos(theta) 63 | # u: 0..1 -> upp: -1..1 64 | u = 1.0-u 65 | theta = math.acos( 2.0*(u-0.5) / math.cos(phi) ) 66 | 67 | if up<0.5: 68 | theta = theta-math.pi 69 | 70 | return (theta,phi) -------------------------------------------------------------------------------- /vrProjectorWrapper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | import argparse 14 | 15 | import vrProjector 16 | 17 | def main(): 18 | parser = argparse.ArgumentParser(description='Reproject photospheres') 19 | parser.add_argument('--sourceProjection', required=True, help='Type of source projection. Valid values are: Equirectangular, Cubemap, SideBySideFisheye') 20 | parser.add_argument('--sourceImage', required=True, help='Source image[s]. List multiple images in double quotes like so "front.png right.png back.png left.png top.png bottom.png"') 21 | parser.add_argument('--useBilnear', required=False, help='Use bilinear interpolation when reprojecting. Valid values are true and false.') 22 | parser.add_argument('--outProjection', required=True, help='Type of output projection. Valid values are: Equirectangular, Cubemap, SideBySideFisheye, Fisheye') 23 | parser.add_argument('--outImage', required=True, help='output image[s]. List multiple images in double quotes like so "front.png right.png back.png left.png top.png bottom.png"') 24 | parser.add_argument('--outWidth', required=True, help='output image[s] width in pixels') 25 | parser.add_argument('--outHeight', required=True, help='output image[s] height in pixels') 26 | 27 | args = parser.parse_args() 28 | 29 | source = None 30 | if args.sourceProjection.lower() == "Equirectangular".lower(): 31 | source = vrProjector.EquirectangularProjection() 32 | source.loadImage(args.sourceImage) 33 | elif args.sourceProjection.lower() == "SideBySideFisheye".lower(): 34 | source = vrProjector.SideBySideFisheyeProjection() 35 | source.loadImage(args.sourceImage) 36 | elif args.sourceProjection.lower() == "Cubemap".lower(): 37 | source = vrProjector.CubemapProjection() 38 | imageList = args.sourceImage.split(' ') 39 | source.loadImages(imageList[0], imageList[1], imageList[2], imageList[3], imageList[4], imageList[5]) 40 | elif args.sourceProjection.lower() == "Fisheye".lower(): 41 | source = vrProjector.FisheyeProjection() 42 | source.loadImage(args.sourceImage) 43 | 44 | else: 45 | print("Quitting because unsupported source projection type: ", args.sourceProjection) 46 | return 47 | 48 | if args.useBilnear is not None: 49 | if args.useBilnear.lower() == "true": 50 | source.set_use_bilinear(True) 51 | 52 | out = None 53 | if args.outProjection.lower() == "Equirectangular".lower(): 54 | out = vrProjector.EquirectangularProjection() 55 | out.initImage(int(args.outWidth), int(args.outHeight)) 56 | elif args.outProjection.lower() == "SideBySideFisheye".lower(): 57 | out = vrProjector.SideBySideFisheyeProjection() 58 | out.initImage(int(args.outWidth), int(args.outHeight)) 59 | elif args.outProjection.lower() == "Cubemap".lower(): 60 | out = vrProjector.CubemapProjection() 61 | out.initImages(int(args.outWidth), int(args.outHeight)) 62 | elif args.outProjection.lower() == "Fisheye".lower(): 63 | out = vrProjector.FisheyeProjection() 64 | out.initImage(int(args.outWidth), int(args.outHeight)) 65 | else: 66 | print("Quitting because unsupported output projection type: ", args.outProjection) 67 | return 68 | 69 | out.reprojectToThis(source) 70 | # out.reprojectToThisThreaded(source, 16) 71 | 72 | if args.outProjection.lower() == "Cubemap".lower(): 73 | imageList = args.outImage.split(' ') 74 | out.saveImages(imageList[0], imageList[1], imageList[2], imageList[3], imageList[4], imageList[5]) 75 | else: 76 | out.saveImage(args.outImage) 77 | 78 | if __name__ == "__main__": 79 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vrProjector, by Bhautik Joshi 2 | 3 | vrProjector is a python library and command-line tool to convert from one type of spherical projection to another. Currently it supports converting between Equirectangular, Cubemaps and Side-by-Side fisheye projections. 4 | 5 | ### Prerequisites 6 | 7 | You'll need the python imaging library; I've found it's best to get it via Pillow: 8 | 9 | ```sh 10 | $ pip install Pillow 11 | ``` 12 | 13 | You'll also need numpy: 14 | 15 | ```sh 16 | $ pip install numpy 17 | ``` 18 | 19 | ### Running vrProjector on the command line 20 | 21 | You can run vrProjector simply by running the ```vrProjectorCmd``` shell script on the command-line. You'll need to specify the source and output projection types as well as the source/output images and paramaters themselves. For example, use this to turn an equirectangular image into a set of 128x128 pixel cubemap faces: 22 | 23 | ```sh 24 | $ ./vrProjectorCmd --sourceProjection Equirectangular --sourceImage images/equirectangular.png --sourceProjection Equirectangular --outProjection CubeMap --outImage "front.png right.png back.png left.png top.png bottom.png" --outWidth 128 --outHeight 128 25 | ``` 26 | 27 | This converts an input equirectangular image into a side-by-side fisheye projection: 28 | 29 | ```sh 30 | $ ./vrProjectorCmd --sourceProjection Equirectangular --sourceImage images/equirectangular.png --sourceProjection Equirectangular --outProjection SideBySideFisheye --outImage foo.png --outWidth 256 --outHeight 128 31 | ``` 32 | 33 | You can access the full set of available commands via the ```-h``` switch: 34 | 35 | ```sh 36 | $ ./vrProjectorCmd -h 37 | usage: vrProjectorWrapper.py [-h] --sourceProjection SOURCEPROJECTION 38 | --sourceImage SOURCEIMAGE 39 | [--useBilnear USEBILNEAR] --outProjection 40 | OUTPROJECTION --outImage OUTIMAGE --outWidth 41 | OUTWIDTH --outHeight OUTHEIGHT 42 | 43 | Reproject photospheres 44 | 45 | optional arguments: 46 | -h, --help show this help message and exit 47 | --sourceProjection SOURCEPROJECTION 48 | Type of source projection. Valid values are: 49 | Equirectangular, Cubemap, SideBySideFisheye 50 | --sourceImage SOURCEIMAGE 51 | Source image[s]. List multiple images in double quotes 52 | like so "front.png right.png back.png left.png top.png 53 | bottom.png" 54 | --useBilnear USEBILNEAR 55 | Use bilinear interpolation when reprojecting. Valid 56 | values are true and false. 57 | --outProjection OUTPROJECTION 58 | Type of output projection. Valid values are: 59 | Equirectangular, Cubemap, SideBySideFisheye 60 | --outImage OUTIMAGE output image[s]. List multiple images in double quotes 61 | like so "front.png right.png back.png left.png top.png 62 | bottom.png" 63 | --outWidth OUTWIDTH output image[s] width in pixels 64 | --outHeight OUTHEIGHT 65 | output image[s] height in pixels 66 | ``` 67 | 68 | ### Running vrProjector in python 69 | 70 | First thing to do is to import the vrProjector package: 71 | 72 | ```python 73 | import vrProjector 74 | ``` 75 | 76 | Now load up your source projection - you'd do this for equirectangular: 77 | 78 | ```python 79 | source = vrProjector.EquirectangularProjection() 80 | source.loadImage("images/equirectangular.png") 81 | ``` 82 | 83 | or this for a set of cubemap images: 84 | 85 | ```python 86 | source = vrProjector.CubemapProjection() 87 | source.loadImages("front.png", "right.png", "back.png", "left.png", "top.png", "bottom.png") 88 | ``` 89 | 90 | If you want, you can set up the reprojection to bilinearly sample across the surface of the sphere. This improves the quality of low-resolution images a little but leads to a 4x increase in run-time: 91 | 92 | ```python 93 | source.set_use_bilinear(True) 94 | ``` 95 | 96 | Now create the output projection - in this case side-by-side fisheye - and save the result: 97 | 98 | ```python 99 | out = vrProjector.SideBySideFisheyeProjection() 100 | out.initImage(2048,1024) 101 | out.reprojectToThis(source) 102 | out.saveImage("sidebysidefisheye.png") 103 | ``` 104 | 105 | Cubemaps are almost the same: 106 | 107 | ```python 108 | out = vrProjector.CubemapeProjection() 109 | out.initImages(1024,1024) 110 | out.reprojectToThis(source) 111 | out.saveImages("front.png", "right.png", "back.png", "left.png", "top.png", "bottom.png") 112 | ``` 113 | -------------------------------------------------------------------------------- /vrProjector/CubemapProjection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | from .AbstractProjection import AbstractProjection 14 | from PIL import Image 15 | import math 16 | 17 | class CubemapProjection(AbstractProjection): 18 | def __init__(self): 19 | AbstractProjection.__init__(self) 20 | 21 | def set_angular_resolution(self): 22 | # imsize on a face: covers 90 degrees 23 | # |\ 24 | # 0.5| \ 25 | # | \ 26 | # ----- 27 | # 1/self.imsize[0] 28 | # angular res ~= arctan(1/self.imsize[0], 0.5) 29 | self.angular_resolution = math.atan2(1/self.imsize[0], 0.5) 30 | 31 | def loadImages(self, front, right, back, left, top, bottom): 32 | self.front, self.imsize = self._loadImage(front) 33 | self.right, self.imsize = self._loadImage(right) 34 | self.back, self.imsize = self._loadImage(back) 35 | self.left, self.imsize = self._loadImage(left) 36 | self.top, self.imsize = self._loadImage(top) 37 | self.bottom, self.imsize = self._loadImage(bottom) 38 | self.set_angular_resolution() 39 | 40 | def initImages(self, width, height): 41 | self.imsize = (width, height) 42 | self.front = self._initImage(width, height) 43 | self.right = self._initImage(width, height) 44 | self.back = self._initImage(width, height) 45 | self.left = self._initImage(width, height) 46 | self.top = self._initImage(width, height) 47 | self.bottom = self._initImage(width, height) 48 | self.set_angular_resolution() 49 | 50 | def saveImages(self, front, right, back, left, top, bottom): 51 | self._saveImage(self.front, self.imsize, front) 52 | self._saveImage(self.right, self.imsize, right) 53 | self._saveImage(self.back, self.imsize, back) 54 | self._saveImage(self.left, self.imsize, left) 55 | self._saveImage(self.top, self.imsize, top) 56 | self._saveImage(self.bottom, self.imsize, bottom) 57 | 58 | def _pixel_value(self, angle): 59 | theta = angle[0] 60 | phi = angle[1] 61 | if theta is None or phi is None: 62 | return (0,0,0) 63 | 64 | sphere_pnt = self.point_on_sphere(theta, phi) 65 | x = sphere_pnt[0] 66 | y = sphere_pnt[1] 67 | z = sphere_pnt[2] 68 | 69 | eps = 1e-6 70 | 71 | if math.fabs(x)>eps: 72 | if x>0: 73 | t = 0.5/x 74 | u = 0.5+t*y 75 | v = 0.5+t*z 76 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 77 | return self.get_pixel_from_uv(u, v, self.front) 78 | elif x<0: 79 | t = 0.5/-x 80 | u = 0.5+t*-y 81 | v = 0.5+t*z 82 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 83 | return self.get_pixel_from_uv(u, v, self.back) 84 | 85 | if math.fabs(y)>eps: 86 | if y>0: 87 | t = 0.5/y 88 | u = 0.5+t*-x 89 | v = 0.5+t*z 90 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 91 | return self.get_pixel_from_uv(u, v, self.right) 92 | elif y<0: 93 | t = 0.5/-y 94 | u = 0.5+t*x 95 | v = 0.5+t*z 96 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 97 | return self.get_pixel_from_uv(u, v, self.left) 98 | 99 | if math.fabs(z)>eps: 100 | if z>0: 101 | t = 0.5/z 102 | u = 0.5+t*y 103 | v = 0.5+t*-x 104 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 105 | return self.get_pixel_from_uv(u, v, self.bottom) 106 | elif z<0: 107 | t = 0.5/-z 108 | u = 0.5+t*y 109 | v = 0.5+t*x 110 | if u>=0.0 and u<=1.0 and v>=0.0 and v<=1.0: 111 | return self.get_pixel_from_uv(u, v, self.top) 112 | 113 | return None 114 | 115 | def get_theta_phi(self, _x, _y, _z): 116 | dv = math.sqrt(_x*_x + _y*_y + _z*_z) 117 | x = _x/dv 118 | y = _y/dv 119 | z = _z/dv 120 | theta = math.atan2(y, x) 121 | phi = math.asin(z) 122 | return theta, phi 123 | 124 | @staticmethod 125 | def angular_position(texcoord): 126 | u = texcoord[0] 127 | v = texcoord[1] 128 | return None 129 | 130 | def reprojectToThis(self, sourceProjection): 131 | halfcubeedge = 1.0 132 | 133 | for x in range(self.imsize[0]): 134 | for y in range(self.imsize[1]): 135 | u = 2.0*(float(x)/float(self.imsize[0])-0.5) 136 | v = 2.0*(float(y)/float(self.imsize[1])-0.5) 137 | 138 | # front 139 | theta, phi = self.get_theta_phi(halfcubeedge, u, v) 140 | pixel = sourceProjection.pixel_value((theta, phi)) 141 | self.front[y,x] = pixel 142 | 143 | # right 144 | theta, phi = self.get_theta_phi(-u, halfcubeedge, v) 145 | pixel = sourceProjection.pixel_value((theta, phi)) 146 | self.right[y,x] = pixel 147 | 148 | # left 149 | theta, phi = self.get_theta_phi(u, -halfcubeedge, v) 150 | pixel = sourceProjection.pixel_value((theta, phi)) 151 | self.left[y,x] = pixel 152 | 153 | # back 154 | theta, phi = self.get_theta_phi(-halfcubeedge, -u, v) 155 | pixel = sourceProjection.pixel_value((theta, phi)) 156 | self.back[y,x] = pixel 157 | 158 | # bottom 159 | theta, phi = self.get_theta_phi(-v, u, halfcubeedge) 160 | pixel = sourceProjection.pixel_value((theta, phi)) 161 | self.bottom[y,x] = pixel 162 | 163 | # top 164 | theta, phi = self.get_theta_phi(v, u, -halfcubeedge) 165 | pixel = sourceProjection.pixel_value((theta, phi)) 166 | self.top[y,x] = pixel 167 | -------------------------------------------------------------------------------- /vrProjector/AbstractProjection.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Bhautik J Joshi 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # Unless required by applicable law or agreed to in writing, software 7 | # distributed under the License is distributed on an "AS IS" BASIS, 8 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | # See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | 12 | 13 | # imports 14 | from PIL import Image 15 | import math 16 | import abc 17 | import numpy as np 18 | 19 | from multiprocessing.dummy import Pool as ThreadPool 20 | 21 | class AbstractProjection: 22 | __metaclass__ = abc.ABCMeta 23 | 24 | def __init__(self): 25 | self.use_bilinear = False 26 | pass 27 | 28 | def set_use_bilinear(self, val): 29 | self.use_bilinear = val 30 | 31 | def get_pixel_from_uv(self, u, v, image): 32 | x = int(self.imsize[0]*u) 33 | y = int(self.imsize[1]*v) 34 | x = min(x,self.imsize[0]-1) 35 | y = min(y,self.imsize[1]-1) 36 | pix = image[y,x] 37 | return pix 38 | 39 | @staticmethod 40 | def _loadImage(imageFile): 41 | img = Image.open(imageFile) 42 | imsize = img.size 43 | #parsed = Image.new("RGB", imsize, (255, 255, 255)) 44 | #bands = img.split() 45 | #parsed.paste(img, mask=(bands[3] if len(bands) == 4 else None)) 46 | #npimage = np.array(parsed.getdata(), np.uint8).reshape(img.size[1], img.size[0], 3) 47 | parsed = Image.new("RGBA", imsize, (255, 255, 255, 0)) 48 | parsed.paste(img) 49 | npimage = np.array(parsed.getdata(), np.uint8).reshape(img.size[1], img.size[0], 4) 50 | return npimage, imsize 51 | 52 | def loadImage(self, imageFile): 53 | self.image, self.imsize = self._loadImage(imageFile) 54 | self.set_angular_resolution() 55 | 56 | @staticmethod 57 | def _initImage(width, height): 58 | image = np.ndarray((height, width, 4), dtype=np.uint8) 59 | return image 60 | 61 | def initImage(self, width, height): 62 | self.image = self._initImage(width, height) 63 | self.imsize = (width, height) 64 | self.set_angular_resolution() 65 | 66 | @staticmethod 67 | def _saveImage(img, imgsize, destFile): 68 | mode = 'RGBA' 69 | arr = img.reshape(img.shape[0]*img.shape[1], img.shape[2]) 70 | if len(arr[0]) == 3: 71 | arr = np.c_[arr, 255*np.ones((len(arr),1), np.uint8)] 72 | img = Image.frombuffer(mode, imgsize, arr.tostring(), 'raw', mode, 0, 1) 73 | img.save(destFile) 74 | 75 | def saveImage(self, destFile): 76 | self._saveImage(self.image, self.imsize, destFile) 77 | 78 | # this isn't any faster because of the GIL on the image object 79 | def reprojectToThisThreaded(self, sourceProjection, numThreads): 80 | uvList = [] 81 | fx = float(self.imsize[0]) 82 | fy = float(self.imsize[1]) 83 | 84 | angleList = [self.angular_position((float(i)/fx,float(j)/fy)) for i in range(self.imsize[0]) for j in range(self.imsize[1])] 85 | 86 | poolAngles = ThreadPool(numThreads) 87 | image = poolAngles.map(sourceProjection.pixel_value, angleList) 88 | poolAngles.close() 89 | poolAngles.join() 90 | 91 | idx = 0 92 | for x in range(self.imsize[0]): 93 | for y in range(self.imsize[1]): 94 | pixel = image[idx] 95 | if pixel is None: 96 | print(x,y) 97 | else: 98 | self.image[y,x] = pixel 99 | idx = idx + 1 100 | 101 | 102 | def reprojectToThis(self, sourceProjection): 103 | for x in range(self.imsize[0]): 104 | for y in range(self.imsize[1]): 105 | u = float(x)/float(self.imsize[0]) 106 | v = float(y)/float(self.imsize[1]) 107 | theta, phi = self.angular_position((u,v)) 108 | if theta is None or phi is None: 109 | pixel = (0,0,0) 110 | else: 111 | pixel = sourceProjection.pixel_value((theta, phi)) 112 | if len(pixel) == 3: 113 | pixel = (pixel[0],pixel[1], pixel[2], 255) 114 | self.image[y,x] = pixel 115 | 116 | def point_on_sphere(self, theta, phi): 117 | r = math.cos(phi) 118 | return (r*math.cos(theta), r*math.sin(theta), math.sin(phi)) 119 | 120 | def pixel_value(self, angle): 121 | if self.use_bilinear: 122 | return self._pixel_value_bilinear_interpolated(angle) 123 | else: 124 | return self._pixel_value(angle) 125 | 126 | @abc.abstractmethod 127 | def _pixel_value(self, angle): 128 | return None 129 | 130 | @abc.abstractmethod 131 | def angular_position(self, texcoord): 132 | return None 133 | 134 | @abc.abstractmethod 135 | def set_angular_resolution(self): 136 | return None 137 | 138 | @staticmethod 139 | def bilinear_interpolation(x, y, points): 140 | '''Interpolate (x,y) from values associated with four points. 141 | 142 | The four points are a list of four triplets: (x, y, value). 143 | The four points can be in any order. They should form a rectangle. 144 | 145 | >>> bilinear_interpolation(12, 5.5, 146 | ... [(10, 4, 100), 147 | ... (20, 4, 200), 148 | ... (10, 6, 150), 149 | ... (20, 6, 300)]) 150 | 165.0 151 | 152 | ''' 153 | # See formula at: http://en.wikipedia.org/wiki/Bilinear_interpolation 154 | 155 | points = sorted(points) # order points by x, then by y 156 | (x1, y1, q11), (_x1, y2, q12), (x2, _y1, q21), (_x2, _y2, q22) = points 157 | 158 | if x1 != _x1 or x2 != _x2 or y1 != _y1 or y2 != _y2: 159 | raise ValueError('points do not form a rectangle') 160 | if not x1 <= x <= x2 or not y1 <= y <= y2: 161 | raise ValueError('(x, y) not within the rectangle') 162 | 163 | return (q11 * (x2 - x) * (y2 - y) + 164 | q21 * (x - x1) * (y2 - y) + 165 | q12 * (x2 - x) * (y - y1) + 166 | q22 * (x - x1) * (y - y1) 167 | ) / ((x2 - x1) * (y2 - y1) + 0.0) 168 | 169 | def _pixel_value_bilinear_interpolated(self, angle): 170 | angleeps = self.angular_resolution/8.0 171 | pixelA = self._pixel_value((angle[0]-angleeps, angle[1]-angleeps)) 172 | pixelB = self._pixel_value((angle[0]-angleeps, angle[1]+angleeps)) 173 | pixelC = self._pixel_value((angle[0]+angleeps, angle[1]-angleeps)) 174 | pixelD = self._pixel_value((angle[0]+angleeps, angle[1]+angleeps)) 175 | 176 | oR = self.bilinear_interpolation(0,0, [(-1,-1, pixelA[0]), (-1,1, pixelB[0]), (1,-1, pixelC[0]), (1,1, pixelD[0])]) 177 | oG = self.bilinear_interpolation(0,0, [(-1,-1, pixelA[1]), (-1,1, pixelB[1]), (1,-1, pixelC[1]), (1,1, pixelD[1])]) 178 | oB = self.bilinear_interpolation(0,0, [(-1,-1, pixelA[2]), (-1,1, pixelB[2]), (1,-1, pixelC[2]), (1,1, pixelD[2])]) 179 | oA = self.bilinear_interpolation(0,0, [(-1,-1, pixelA[3]), (-1,1, pixelB[3]), (1,-1, pixelC[3]), (1,1, pixelD[3])]) 180 | 181 | return (int(oR), int(oG), int(oB), int(oA)) 182 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | --------------------------------------------------------------------------------