これまでの記事を元に、Pythonで全天球画像(equirectangular/CubeMap)を変換して表示するGUIを作ってみました。ちゃんとビューアーになりました。

こんな感じ。
コード
前回の記事で360度画像のコンバータを作りましたが、いまいちどんなパラメータを入れるとどんな画像に変換されるか直感的ではなかったので、GUIをつけることにしました。CubeMapからの変換もできます。
前回の記事のコードを呼んでやる形で作っただけなので、GUI部は基本的に表示系しか仕事をしていません。一応それっぽくぐりぐり動かせるようにしました。
前回の記事のコードをio360_converter.pyって名前で保存しておいて、そのわきに今回のコードを置いたイメージです。動きはコメント見てください…。
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 | import tkinter as tk import tkinter.filedialog import cv2 from PIL import ImageTk, Image import numpy as np import io360_converter as io360 # 前回の記事のコード WIDTH = 960 HEIGHT = 540 SCALE = 1 # 表示用(2だったらWIDTH/HEIGHTを半分にして表示) # 表示用GUIクラス class laps_viewerGUI(tk.Frame): def __init__( self , master = None ): super ().__init__(master) self .img = np.zeros(( 32 , 64 , 3 ), np.uint8) # 初期表示用画像 # ----------------------# 上側のフレーム(画像表示部)#----------------------# fm_upper = tk.Frame(master) fm_upper.pack(fill = tk.X, side = tk.TOP) # 表示 self .canvas = tk.Canvas(fm_upper, width = WIDTH / / SCALE, height = HEIGHT / / SCALE) self .canvas.bind( "<Button-1>" , self .click) self .canvas.bind( "<B1-Motion>" , self .drag) self .canvas.bind( "<Control-MouseWheel>" , self .ctrl_wheel) self .canvas.pack(side = tk.LEFT) self .image_Tk = ImageTk.PhotoImage(Image.fromarray( self .img), master = self .canvas) # 表示画像の生成 # ----------------------# 真ん中のフレーム # ---------------------- # fm_mid = tk.Frame(master) fm_mid.pack(fill = tk.X, side = tk.TOP) # グリッド表示・非表示 self .Chkbox = tk.BooleanVar() self .Chkbox. set ( True ) chkbtn = tk.Checkbutton(fm_mid, variable = self .Chkbox, text = 'グリッドを表示する' , command = self .draw) chkbtn.pack(side = tk.RIGHT) # ----------------------# 下側のフレーム #----------------------# fm_ctrl = tk.Frame(master) fm_ctrl.pack(fill = tk.X, side = tk.TOP) # 拡大縮小・回転 self .Var_yow = tk.Scale(fm_ctrl, label = 'yaw' , orient = "v" , from_ = - 180 , to = 180 , command = self .angle_callback) self .Var_pitch = tk.Scale(fm_ctrl, label = 'pitch' , orient = "v" , from_ = - 180 , to = 180 , command = self .angle_callback) self .Var_roll = tk.Scale(fm_ctrl, label = 'roll' , orient = "v" , from_ = - 180 , to = 180 , command = self .angle_callback) self .Var_scale = tk.Scale(fm_ctrl, label = 'senser_size' , orient = "v" , from_ = 0.1 , to = 2.0 , resolution = 0.1 , command = self .scrl_callback) self .Var_point = tk.Scale(fm_ctrl, label = 'view_point' , orient = "v" , from_ = - 1.8 , to = 1.0 , resolution = 0.05 , command = self .scrl_callback) self .Var_focus = tk.Scale(fm_ctrl, label = 'senser_point' , orient = "v" , from_ = 0.01 , to = 0.81 , resolution = 0.05 , command = self .scrl_callback) self .Var_yow.pack(fill = tk.Y, side = tk.LEFT) self .Var_pitch.pack(fill = tk.Y, side = tk.LEFT) self .Var_roll.pack(fill = tk.Y, side = tk.LEFT) self .Var_scale.pack(fill = tk.Y, side = tk.LEFT) self .Var_point.pack(fill = tk.Y, side = tk.LEFT) self .Var_focus.pack(fill = tk.Y, side = tk.LEFT) self .Var_scale. set ( 0.7 ) self .Var_point. set ( - 1.0 ) self .Var_focus. set ( 0.2 ) # ----------------------# メニュー #----------------------# men = tk.Menu(master) menu_file = tk.Menu(master, tearoff = 0 ) men.add_cascade(label = 'ファイル' , menu = menu_file) menu_file.add_command(label = 'ファイルを開く' , command = self .openfile) menu_file.add_command(label = 'CubeMapを開く' , command = self .opencube) menu_file.add_separator() menu_file.add_command(label = '名前を付けて保存' ,command = self .savefile) master.config(menu = men) # メインキャンパスでのマウスダウン def click( self , event): self .posx = event.x self .posy = event.y # メインキャンパスでのマウスムーブ def drag( self , event): dx = event.x - self .posx dy = event.y - self .posy self .Var_yow. set ( self .Var_yow.get() + dx) self .Var_pitch. set ( self .Var_pitch.get() + dy) if self .Var_yow.get() = = 180 : self .Var_yow. set ( - 180 ) elif self .Var_yow.get() = = - 180 : self .Var_yow. set ( 180 ) self .click(event) # メインキャンパスでのマウスホイール(拡大縮小) def ctrl_wheel( self , event): if event.delta > 0 : # 向きのみ検出 self .Var_scale. set ( self .Var_scale.get() + 0.1 ) else : self .Var_scale. set ( self .Var_scale.get() - 0.1 ) # 表示画像の生成 def remap( self ): roll = self .Var_roll.get() pitch = self .Var_pitch.get() yaw = self .Var_yow.get() xd, yd, zd = io360.rotate_3dmap( self .x, self .y, self .z, roll, pitch, yaw) return io360.remap_from_equirectangular( self .img, xd, yd, zd) # , borderMode=cv2.BORDER_CONSTANT) # 描画 def draw( self ): w = WIDTH / / SCALE h = HEIGHT / / SCALE # 表示はリサイズをかける if SCALE ! = 1 : image_rgb = cv2.resize( self .remap(), (w, h)) else : image_rgb = self .remap() self .image_Tk = ImageTk.PhotoImage(Image.fromarray(image_rgb), master = self .canvas) self .canvas.create_image( 0 , 0 , image = self .image_Tk, anchor = 'nw' ) # グリッドの表示 if self .Chkbox.get(): self .canvas.create_line(w / 2 , 0 , w / 2 , h, fill = 'red' ) self .canvas.create_line( 0 , h / 2 , w, h / 2 , fill = 'red' ) # 角度が変化したときに呼ばれるコールバック関数 def angle_callback( self , val): self .draw() # 角度以外が変化したときに呼ばれるコールバック関数 def scrl_callback( self , val): img_w = WIDTH img_h = HEIGHT senser_size = self .Var_scale.get() view_point = self .Var_point.get() senser_point = self .Var_focus.get() self .x, self .y, self .z = io360.create_3dmap_from_viewpoint(img_w, img_h, senser_size, view_point, senser_point) self .draw() # ファイルを開く def openfile( self ): fileName = tk.filedialog.askopenfilename( title = "全天球画像を開く" , filetypes = [( "Image file" , ".bmp .png .jpg .tif" ), ( "Bitmap" , ".bmp" ), ( "PNG" , ".png" ), ( "JPEG" , ".jpg" ), ( "Tiff" , ".tif" ) ],) if fileName ! = None : self .img = cv2.cvtColor(cv2.imread(fileName), cv2.COLOR_BGR2RGB) self .draw() # CubeMapを開く def opencube( self ): fileName = tk.filedialog.askopenfilename( title = "CubeMapを開く" , filetypes = [( "Image file" , ".bmp .png .jpg .tif" ), ( "Bitmap" , ".bmp" ), ( "PNG" , ".png" ), ( "JPEG" , ".jpg" ), ( "Tiff" , ".tif" ) ],) if fileName ! = None : tmp = cv2.imread(fileName) width = tmp.shape[ 1 ] tmp = io360.cube_to_equirectangular(tmp, width) #面倒なのでいきなり変換 self .img = cv2.cvtColor(tmp, cv2.COLOR_BGR2RGB) self .draw() # 保存メニュー def savefile( self ): fileName = tk.filedialog.asksaveasfilename(title = "画像の保存" ) if fileName ! = None : tmp = self .remap() cv2.imwrite(fileName, cv2.cvtColor(tmp, cv2.COLOR_BGR2RGB)) # main if __name__ = = '__main__' : laps_viewerGUI(master = tk.Tk()).mainloop() |
こんな文字だけのリソースなしコードで、

こんなのできちゃうんですね。

ここのバーの値をそのまま前回の記事の関数に渡して描画しているだけです。どんな数字入れたらどんな画像になるかGUIの方がイメージしやすいです。
視点のイメージ。

いつまでたってもどっちがどの角度か覚えられない。
まとめ
やっとこさビューアーと呼べる代物になったかな。ただエラー処理の類は何もしていないのであしからず。
コメント