これまでの記事を元に、Pythonで全天球画像(equirectangular/CubeMap)を変換して表示するGUIを作ってみました。ちゃんとビューアーになりました。
こんな感じ。
コード
前回の記事で360度画像のコンバータを作りましたが、いまいちどんなパラメータを入れるとどんな画像に変換されるか直感的ではなかったので、GUIをつけることにしました。CubeMapからの変換もできます。
前回の記事のコードを呼んでやる形で作っただけなので、GUI部は基本的に表示系しか仕事をしていません。一応それっぽくぐりぐり動かせるようにしました。
前回の記事のコードをio360_converter.pyって名前で保存しておいて、そのわきに今回のコードを置いたイメージです。動きはコメント見てください…。
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の方がイメージしやすいです。
視点のイメージ。
いつまでたってもどっちがどの角度か覚えられない。
まとめ
やっとこさビューアーと呼べる代物になったかな。ただエラー処理の類は何もしていないのであしからず。
コメント