これまでの記事を元に、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の方がイメージしやすいです。
視点のイメージ。

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