360度カメラの全天球画像ビューアーを作る その4

360度画像

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

 視点のイメージ。

 いつまでたってもどっちがどの角度か覚えられない。

まとめ

 やっとこさビューアーと呼べる代物になったかな。ただエラー処理の類は何もしていないのであしからず。

コメント

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