Loading [MathJax]/extensions/tex2jax.js
MENU

Pythonで360度画像を変換する

360度画像

 原理を理解するために全天球画像画像からの変換の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でも書けそうです。

コメント

タイトルとURLをコピーしました