原理を理解するために全天球画像画像からの変換のPythonコードを色々作ってみましたが、整理して関数として使えるようにしました。理屈はいいから使いたい。という方はこちらをどうぞ。理屈が知りたい方は過去の記事をご参照ください。
OpenCVに装備されててもよさそうなものですが、どうもなさそうなので自作です。Pythonのライブラリはありそうですが。
全体像
全天球画像もしくはCubeMapから自由視点での画像切り出しを目的に作りました。一般的なビューアーで見られる画像は作れるのではないかと思います。
こんな全天球画像や

こんなCubeMapから

こんな画像を切り出すものです。

また全天球画像もしくはCubeMapへの書き出しもできるようにしました。
基本思想としては3次元座標x,y,zを介してマッピングをしています。全天球画像→(x,y,z)→2次元座標となるようにしています。視点はx,y,zから変換することで切り替えています。回転はこの3次元座標で行っています。

保存時に360度画像だよ~ってタグをつけてはいないのでそのあたりは何とかしてください。
コード
コピペしてください。OpenCVが必要です。moduleとして外だしするのがクールだと思いますがひとまずベタベタに羅列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | import numpy as np import cv2 ## # @brief 3次元座標群取得関数 # @details 360度画像の緯度経度が半径1の球の3次元座標のどこにマッピングされるか # @param img_w 360度画像の幅 # @param img_h 360度画像の高さ # @return x,y,zの座標群 def create_3dmap_for_equirectangular(img_w, img_h): h = np.linspace( - np.pi / 2 , np.pi / 2 , img_h, endpoint = False ) w = np.linspace( - np.pi, np.pi, img_w, endpoint = False ) # 配列を対称形にするためのオフセット h = h + (np.pi / 2 ) / img_h w = w + np.pi / img_w theta, phi = np.meshgrid(w, h) x = np.cos(phi) * np.cos(theta) y = np.cos(phi) * np.sin(theta) z = np.sin(phi) return x, y, z ## # @brief 3次元座標群切り出し関数 # @details とある視点からとある距離の平面を通る半径1の球の表面を結んだ交点座標群を切り出す # @param img_w 画像の幅(分解能) # @return x,y,zの座標群 def create_3dmap_for_cube(img_w): senser_w = 0.5 w = np.linspace( - senser_w, senser_w, img_w, endpoint = False ) h = np.linspace( - senser_w, senser_w, img_w, endpoint = False ) # 配列を対称形にするためのオフセット w = w + senser_w / img_w h = h + senser_w / img_w # センサの座標 ww, hh = np.meshgrid(w, h) # 直線の式 a1 = 2 * ww a2 = 2 * hh # 球面上の3次元座標 x = ( 1 / ( 1 + a1 * * 2 + a2 * * 2 )) * * ( 1 / 2 ) y = a1 * x z = a2 * x return x, y, z ## # @brief 3次元座標群切り出し関数 # @details とある視点からとある距離の平面を通る半径1の球の表面を結んだ交点座標群を切り出す # @param img_w 画像の幅(分解能) # @param img_h 画像の高さ(分解能) # @param senser_size センサに見立てた平面の大きさ(0以上の少数) # @param view_point 視点位置 # @param senser_point 視点から平面までの距離 # @return x,y,zの座標群 def create_3dmap_from_viewpoint(img_w, img_h, senser_size, view_point, senser_point): senser_w = senser_size senser_h = senser_size * img_h / img_w x1 = view_point # 視点の位置 x2 = view_point + senser_point # 撮像面の位置(必ず視点より前) w = np.linspace( - senser_w, senser_w, img_w, endpoint = False ) h = np.linspace( - senser_h, senser_h, img_h, endpoint = False ) # 配列を対称形にするためのオフセット w = w + senser_w / img_w h = h + senser_h / img_h # センサの座標 ww, hh = np.meshgrid(w, h) # 直線の式 a1 = ww / (x2 - x1) a2 = hh / (x2 - x1) b1 = - a1 * x1 b2 = - a2 * x1 a = 1 + a1 * * 2 + a2 * * 2 b = 2 * (a1 * b1 + a2 * b2) c = b1 * * 2 + b2 * * 2 - 1 d = (b * * 2 - 4 * a * c) * * ( 1 / 2 ) # 球面上の3次元座標 x = ( - b + d) / ( 2 * a) y = a1 * x + b1 z = a2 * x + b2 return x, y, z ## # @brief 3次元回転座標を取得する関数 # @details 3次元座標群を原点から指定された角度で回転させる # @param x x座標群 # @param y y座標群 # @param z z座標群 # @param roll roll角(度単位) # @param pitch pitch角(度単位) # @param yaw yaw角(度単位) # @return 回転後の3次元座標 def rotate_3dmap(x, y, z, roll, pitch, yaw): # 回転量 roll = np.deg2rad(roll) pitch = np.deg2rad(pitch) yaw = np.deg2rad(yaw) # 3次元の回転行列 mtx1 = np.array([[ 1 , 0 , 0 ], [ 0 , np.cos(roll), np.sin(roll)], [ 0 , - np.sin(roll), np.cos(roll)]]) mtx2 = np.array([[np.cos(pitch), 0 , - np.sin(pitch)], [ 0 , 1 , 0 ], [np.sin(pitch), 0 , np.cos(pitch)]]) mtx3 = np.array([[np.cos(yaw), np.sin(yaw), 0 ], [ - np.sin(yaw), np.cos(yaw), 0 ], [ 0 , 0 , 1 ]]) # 回転行列の積 mtx4 = np.dot(mtx3, np.dot(mtx2, mtx1)) # 座標の行列計算 xd = mtx4[ 0 ][ 0 ] * x + mtx4[ 0 ][ 1 ] * y + mtx4[ 0 ][ 2 ] * z yd = mtx4[ 1 ][ 0 ] * x + mtx4[ 1 ][ 1 ] * y + mtx4[ 1 ][ 2 ] * z zd = mtx4[ 2 ][ 0 ] * x + mtx4[ 2 ][ 1 ] * y + mtx4[ 2 ][ 2 ] * z return xd, yd, zd ## # @brief 緯度経度情報にしたがって画像を変換する関数 # @details 1:2で格納されている画像から、そのx,y座標を緯度経度と見立てて対応する座標変換を行う # @param img 元となる360度画像(1:2) # @param x x座標群 # @param y y座標群 # @param z z座標群 # @return 変換された画像 def remap_from_equirectangular(img, x, y, z, interpolation = cv2.INTER_CUBIC, borderMode = cv2.BORDER_WRAP): # 緯度・経度へ変換 phi = np.arcsin(z) theta = np.arcsin(np.clip(y / np.cos(phi), - 1 , 1 )) theta = np.where((x< 0 ) & (y< 0 ), - np.pi - theta, theta) theta = np.where((x< 0 ) & (y> 0 ), np.pi - theta, theta) img_h, img_w = img.shape[: 2 ] # 画像座標へ正規化(座標を画素位置に戻すため0.5オフセット) phi = (phi * img_h / np.pi + img_h / 2 ).astype(np.float32) - 0.5 theta = (theta * img_w / ( 2 * np.pi) + img_w / 2 ).astype(np.float32) - 0.5 return cv2.remap(img, theta, phi, interpolation, borderMode = borderMode) ## # @brief 全天球画像からキューブ画像を作る # @details 半径1の球の中の1辺1の立方体に画像をマッピングさせ、6面を展開図として張り付ける # @param img 元となる360度画像(1:2) # @param width 1面の幅 # @return 変換された画像 def equirectangular_to_cube(img, width, interpolation = cv2.INTER_CUBIC): # x, y, z = create_3dmap_from_viewpoint(width, width, 0.5, 0, 0.5) x, y, z = create_3dmap_for_cube(width) front = remap_from_equirectangular(img, x, y, z, interpolation) x1, y1, z1 = rotate_3dmap(x, y, z, 0 , 0 , - 90 ) # 球を左90度まわす right = remap_from_equirectangular(img, x1, y1, z1, interpolation) x1, y1, z1 = rotate_3dmap(x, y, z, 0 , 0 , 90 ) left = remap_from_equirectangular(img, x1, y1, z1, interpolation) x1, y1, z1 = rotate_3dmap(x, y, z, 0 , 0 , 180 ) back = remap_from_equirectangular(img, x1, y1, z1, interpolation) x1, y1, z1 = rotate_3dmap(x, y, z, 0 , 90 , 0 ) # 球を上90度まわす(正面に下が来る) bottom = remap_from_equirectangular(img, x1, y1, z1, interpolation) x1, y1, z1 = rotate_3dmap(x, y, z, 0 , - 90 , 0 ) up = remap_from_equirectangular(img, x1, y1, z1, interpolation) # 背景となる画像 canvas = np.zeros((width * 3 , width * 4 , img.shape[ 2 ]), dtype = img.dtype) canvas[width * 1 :width * 2 , :width * 1 ] = left canvas[width * 1 :width * 2 , width * 1 :width * 2 ] = front canvas[width * 1 :width * 2 , width * 2 :width * 3 ] = right canvas[width * 1 :width * 2 , width * 3 : ] = back canvas[ :width * 1 , width * 1 :width * 2 ] = up canvas[width * 2 : , width * 1 :width * 2 ] = bottom return canvas ## # @brief cube画像にのりしろをつける # @details 画素補間用にcube画像の境界部に2画素分反対側の画像を付与する # @param img cube画像 # @return のりしろ付き画像 def add_norishiro_to_cube(img): h,w,c = img.shape cw = w / / 4 # 周囲2画素大きい画像を用意 canvas = np.zeros((h + 4 , w + 4 , c), dtype = img.dtype) canvas[ 2 : - 2 , 2 : - 2 ,:] = img # 上下左右にのりしろをつける # up canvas[ 0 : 2 ,cw + 2 : 2 * cw + 2 ,:] = np.rot90(img[cw:cw + 2 , 3 * cw:,:], 2 ) # bottom canvas[ - 2 :,cw + 2 : 2 * cw + 2 ,:] = np.rot90(img[ 2 * cw - 2 : 2 * cw, 3 * cw:,:], 2 ) # left canvas[cw + 2 : 2 * cw + 2 , 0 : 2 ,:] = img[cw: 2 * cw, - 2 :,:] # right canvas[cw + 2 : 2 * cw + 2 , - 2 :,:] = img[cw: 2 * cw, 0 : 2 ,:] # 残りの折り返しの部分のコピー canvas[cw:cw + 2 ,:cw + 2 ,:] = np.rot90(canvas[:cw + 2 ,cw + 2 :cw + 4 ,:]) canvas[:cw + 2 ,cw:cw + 2 ,:] = np.rot90(canvas[cw + 2 :cw + 4 ,:cw + 2 ,:], 3 ) # canvas[ 2 * cw + 2 : 2 * cw + 4 ,:cw + 2 ,:] = np.rot90(canvas[ 2 * cw + 2 :,cw + 2 :cw + 4 ,:], 3 ) canvas[ 2 * cw + 2 :,cw:cw + 2 ,:] = np.rot90(canvas[ 2 * cw: 2 * cw + 2 ,:cw + 2 ,:]) # canvas[cw:cw + 2 , 2 * cw + 2 : 3 * cw + 2 ,:] = np.rot90(canvas[ 2 :cw + 2 , 2 * cw: 2 * cw + 2 ,:], 3 ) canvas[:cw + 2 , 2 * cw + 2 : 2 * cw + 4 :] = np.rot90(canvas[cw + 2 :cw + 4 , 2 * cw + 2 : 3 * cw + 4 ,:]) # canvas[ 2 * cw + 2 : 2 * cw + 4 , 2 * cw + 2 : 3 * cw + 2 ,:] = np.rot90(canvas[ 2 * cw + 2 : - 2 , 2 * cw: 2 * cw + 2 ,:]) canvas[ 2 * cw + 2 :, 2 * cw + 2 : 2 * cw + 4 ,:] = np.rot90(canvas[ 2 * cw: 2 * cw + 2 , 2 * cw + 2 : 3 * cw + 4 ,:], 3 ) # canvas[cw:cw + 2 , 3 * cw + 2 :,:] = canvas[ 3 : 1 : - 1 , 2 * cw + 1 :cw - 1 : - 1 ,:] canvas[ 2 * cw + 2 : 2 * cw + 4 , 3 * cw + 2 :,:] = canvas[ - 3 : - 5 : - 1 , 2 * cw + 1 :cw - 1 : - 1 ,:] return canvas ## # @brief キューブ画像を元に画像を変換する。 # @details 6面を展開図から変換 # @param img 元となるキューブ(展開図)画像 # @param x x座標群 # @param y y座標群 # @param z z座標群 # @return 変換された画像 def remap_from_cube(img, x, y, z, interpolation = cv2.INTER_CUBIC): width = img.shape[ 1 ] / / 4 w = 0.5 # front xx = w * y / x + w yy = w * z / x + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (x > 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width + width, 0 ) tmpy = np.where(mask, yy * width + width, 0 ) # back xx = w * y / x + w yy = - w * z / x + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (x < 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width + width * 3 , tmpx) tmpy = np.where(mask, yy * width + width, tmpy) #right xx = - w * x / y + w yy = w * z / y + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (y > 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width + width * 2 , tmpx) tmpy = np.where(mask, yy * width + width, tmpy) #left xx = - w * x / y + w yy = - w * z / y + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (y < 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width, tmpx) tmpy = np.where(mask, yy * width + width, tmpy) #up xx = - w * y / z + w yy = - w * x / z + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (z < 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width + width, tmpx) tmpy = np.where(mask, yy * width, tmpy) #bottom xx = w * y / z + w yy = - w * x / z + w mask = np.where((xx > 0 ) & (xx < 1 ) & (yy > 0 ) & (yy < 1 ) & (z > 0 ), 1 , 0 ) tmpx = np.where(mask, xx * width + width, tmpx) tmpy = np.where(mask, yy * width + width * 2 , tmpy) cube = add_norishiro_to_cube(img) # のりしろ2画素と座標を画素位置に戻すため0.5オフセット tmpx + = ( 2.0 - 0.5 ) tmpy + = ( 2.0 - 0.5 ) return cv2.remap(cube, tmpx.astype(np.float32), tmpy.astype(np.float32), interpolation) ## # @brief キューブ画像から全天球画像を作る # @details 6面を展開図から全天球画像へ変換する # @param img 元となるキューブ(展開図)画像 # @param width 全天球画像の幅(2の倍数である必要がある) # @return 変換された画像 def cube_to_equirectangular(img, width, interpolation = cv2.INTER_CUBIC): img_w = width img_h = width / / 2 width = img.shape[ 1 ] / / 4 x, y, z = create_3dmap_for_equirectangular(img_w, img_h) return remap_from_cube(img, x, y, z, interpolation) |
コメント込みでも350行。
使い方その1 視点の回転
首を左右に振る。上下に振る。首を傾げる。の3方向。rotate_3dmapに与える角度を変えるだけです。
ソース画像はこれ。もちろん全天球画像(エクイレクタングラー画像)でも同じように扱えます。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 | cube = cv2.imread( "color_cube.png" ) x, y, z = create_3dmap_for_equirectangular( 640 , 320 ) for i in range ( 10 ): ofile = os.path.join( "\ani" , 'cube_{0:03d}.png' . format (i + 1 )) out = remap_from_cube(cube, x, y, z) cv2.imwrite(ofile, out) x, y, z = rotate_3dmap(x, y, z, 5 , 0 , 0 ) for i in range ( 10 ): ofile = os.path.join( "\ani" , 'cube_{0:03d}.png' . format (i + 10 )) out = remap_from_cube(cube, x, y, z) cv2.imwrite(ofile, out) x, y, z = rotate_3dmap(x, y, z, - 5 , 0 , 0 ) |

上下(ピッチ)

左右(ヨー)

首傾げ(ロール)
ヨー・ピッチ・ロールの意味合いとしてはこんな感じ。

使い方その2 ズームと前進
とある視点から見えている画像をズームする。とある視点から前進する。似てますが微妙に効果が違います。
ズーム
1 2 3 4 5 6 | cube = cv2.imread("color_cube.png") for i in range(40): x, y, z = create_3dmap_from_viewpoint(640, 480, 0.5 - i/100, -2.0, 0.5) ofile = os.path.join("\ani", 'cube_{0:03d}.png'.format(i+1)) out = remap_from_cube(cube, x, y, z) cv2.imwrite(ofile, out) |
前進
1 2 3 4 5 6 | cube = cv2.imread( "color_cube.png" ) for i in range ( 40 ): x, y, z = create_3dmap_from_viewpoint( 640 , 480 , 0.5 , - 2.0 + i / 20 , 0.5 ) ofile = os.path.join( "\ani" , 'cube_{0:03d}.png' . format (i + 1 )) out = remap_from_cube(cube, x, y, z) cv2.imwrite(ofile, out) |
結果

ズーム

前進
正面の正方形のゆがみ方が微妙に異なります。左右の壁の見え方も変わります。ズームはただ拡大しているだけですね。
まとめ
理屈はさておき360度画像を自在に操作できる気がしてきました。色々遊べそうです。
原理が知りてー。って方は
1.360度写真の平面投影
2.視点の変更と背面の投影
3.視点の回転
CubeMapの扱いは
1.全天球画像からCubeMap
2.CubeMapから全天球画像
原理はなんとなくわかったので頑張ればCでも書けそうです。
コメント