Newer
Older
waypoint_navigation / waypoint_manager / scripts / devel_GUI.py
#import rospy
import tkinter as tk
import numpy as np
import ruamel.yaml
from PIL import Image, ImageTk
from pathlib import Path


#===== Applicationクラスの定義 tk.Frameクラスを継承 =====#
class Application(tk.Frame):

	"""
	+++++ コンストラクタ +++++
	 プログラムを実行したときに最初にすべき処理を全て記述する
	"""
	def __init__(self, master):
		super().__init__(master)   # スーパークラスのコンストラクタを実行
		self.master.title("Waypoints Manager")

		## 画面上部のメニューを作成
		self.menu_bar = tk.Menu(self)   # メニューバーを配置
		self.file_menu = tk.Menu(self.menu_bar, tearoff=tk.OFF)   # バーに追加するメニューを作成
		self.menu_bar.add_cascade(label="File", menu=self.file_menu)   # Fileメニューとしてバーに追加
		self.file_menu.add_command(label="Save",   # FileメニューにSaveコマンドを追加
								command=self.menu_save_clicked,   #コールバック関数を設定
								accelerator="Ctrl+S"# 右側に表示するキーボードショートカット
								)   
		self.file_menu.add_command(label="Save As",   # 同様にSave Asコマンドを追加
								command=self.menu_saveas_clicked,
								accelerator="Ctrl+Shift+S"
								)
		self.bind_all("<Control-s>", self.menu_save_clicked)   #キーボードショートカットを設定
		self.bind_all("<Control-Shift-S>", self.menu_saveas_clicked)
		self.master.config(menu=self.menu_bar)   # 大元に作成したメニューバーを設定

		## 右クリックしたときに表示するポップアップメニューを作成
		self.popup_menu = tk.Menu(self, tearoff=tk.OFF)
		self.popup_menu.add_command(label="add waypoint", command=self.add_waypoint)
		self.right_click_coord = None   # 右クリックしたときの座標を保持する変数

		## map.pgm, map.yaml, waypoints.yaml の読み込み 3つの変数は再代入禁止
		self.__map_img_pil, self.__map_yaml = self.get_map_info()
		self.__waypoints = self.get_waypoints()
		self.current_waypoints = self.__waypoints   # 編集中のウェイポイント情報を保持する

		## canvasを配置
		self.canvas = tk.Canvas(self.master, background="#a0a0a0")   # 画像を描画するcanvas
		self.canvas.pack(expand=True, fill=tk.BOTH)   # canvasを配置
		self.update()   # 情報の更新をする(canvasのサイズなどの情報が更新される)

		## 画像をcanvasのサイズにフィッティングして描画
		self.canv_w = self.canvas.winfo_width()   # canvasの幅を取得
		self.canv_h = self.canvas.winfo_height()   # canvasの高さを取得
		self.mat_affine = np.eye(3)   # 同次変換行列の初期化
		scale = 1
		offset_x = 0
		offset_y = 0
		if (self.canv_w / self.canv_h) > (self.__map_img_pil.width / self.__map_img_pil.height):
			# canvasの方が横長 画像をcanvasの縦に合わせてリサイズ
			scale = self.canv_h / self.__map_img_pil.height
			offset_x = (self.canv_w - self.__map_img_pil.width*scale) / 2
		else:
			# canvasの方が縦長 画像をcanvasの横に合わせてリサイズ
			scale = self.canv_w / self.__map_img_pil.width
			offset_y = (self.canv_h - self.__map_img_pil.height*scale) / 2
		
		self.scale_mat(scale)   # 同次変換行列を更新
		self.translate_mat(offset_x, offset_y)   # 同次変換行列を更新
		self.draw_img_tk = ImageTk.PhotoImage(image=self.__map_img_pil)	# 描画する画像を保持する変数を初期化
		self.draw_image()   # 画像を描画

		## map.yamlから原点と解像度を取得し、地図上に原点を示す円を描画
		origin = self.__map_yaml['origin'] # originは地図上の原点から画像の左下までの距離[x,y](m)
		self.map_resolution = self.__map_yaml['resolution']
		self.img_origin = [-origin[0]/self.map_resolution, self.__map_img_pil.height+origin[1]/self.map_resolution, 1]
		self.plot_origin()

		## waypoints.yamlからウェイポイント情報を取得し、画像にポイントを描画
		self.plot_waypoints()

		## マウスイベントに対するコールバックを設定
		self.master.bind("<MouseWheel>", self.mouse_wheel)
		self.master.bind("<B1-Motion>", self.left_click_move)
		self.master.bind("<Button-1>", self.left_click)
		self.master.bind("<ButtonRelease-1>", self.left_click_release)
		self.master.bind("<Button-3>", self.right_click)
		## ウィンドウに関するコールバック
		self.master.bind("<Configure>", self.window_resize_callback)

		self.old_click_point = None
		return



	"""
	+++++ mapのファイルパスを受け取り、map画像とmapの設定ファイルを読み込む +++++
	--- 今はパスを直接指定して読み込んでいるので、rospy.get_param()を使って読み込めるように ---
	"""
	def get_map_info(self):
		map_path = Path('..','..','waypoint_nav','maps','map_gakunai')   # .pgmと.yamlの手前までのパス
		map_img_pil = Image.open(Path(str(map_path)+'.pgm'))   # .pgmをplillowで読み込む
		with open(str(map_path)+'.yaml') as file:   # .yamlを読み込む
			map_yaml = ruamel.yaml.YAML().load(file)
		
		return map_img_pil, map_yaml    # この2つの変数を戻り値とする
	


	"""
	+++++ waypointsのパスを受け取り読み込んだデータを返す +++++
	--- これもget_param()でパスを受け取り、読み込めるようにする ---
	"""
	def get_waypoints(self):
		file_path = Path('..','..','waypoint_nav','param','waypoints_gakunai.yaml')
		with open(file_path) as file:
			waypoints = ruamel.yaml.YAML().load(file)
		return waypoints



	"""
	+++++ 地図上の原点に円を描画する +++++
	"""
	def plot_origin(self):
		origin_affine = np.dot(self.mat_affine, self.img_origin) # キャンバス上の座標に変換
		r = 10   # 円の半径(ピクセル)
		x1 = origin_affine[0] - r
		y1 = origin_affine[1] - r
		x2 = origin_affine[0] + r + 1
		y2 = origin_affine[1] + r + 1
		if self.canvas.find_withtag("origin"):
			self.canvas.moveto("origin", x1, y1)
		else:
			self.canvas.create_oval(x1, y1, x2, y2, tags="origin", fill='cyan', outline='blue')
		return



	"""
	+++++ 地図上にウェイポイントを示す円を描画する +++++
	"""
	def plot_waypoints(self):
		return




	"""
	+++++ 左クリックされたときに実行されるコールバック関数 +++++
	"""
	def left_click(self, event):
		self.popup_menu.unpost()   # 右クリックで出るポップアップメニューを非表示
		# クリックした座標の近くにあるオブジェクトを取得
		clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20)
		if clicked_obj:   # オブジェクトがクリックされた場合
			tag = self.canvas.gettags(clicked_obj[0])[0]
			print("Clicked " + tag)
		return



	"""
	+++++ マウスを左クリックしながらドラッグしたときのコールバック関数 +++++
	"""
	def left_click_move(self, event):
		if not self.old_click_point is None:
			delta_x = event.x-self.old_click_point[0]
			delta_y = event.y-self.old_click_point[1]
			if (np.linalg.norm([delta_x, delta_y]) < 300):
				self.translate_mat(event.x-self.old_click_point[0], event.y-self.old_click_point[1])
				self.draw_image()
				self.canvas.move("origin", delta_x, delta_y)   # self.plot_origin()
				self.plot_waypoints()
		self.old_click_point = [event.x, event.y]
		return
	


	"""
	+++++ マウスの左クリックを離したときのコールバック関数 +++++
	"""
	def left_click_release(self, event):
		self.old_click_point = None
		return



	"""
	+++++ 右クリックしたときのコールバック関数 +++++
	"""
	def right_click(self, event):
		# クリックした座標の近くにあるオブジェクトを取得
		clicked_obj = self.canvas.find_enclosed(event.x-20, event.y-20, event.x+20, event.y+20)
		if clicked_obj:   # 何かオブジェクトがクリックされていた場合
			return
		# クリックされた座標 => 元画像の座標 の変換
		mat_inv = np.linalg.inv(self.mat_affine)
		img_xy = np.dot(mat_inv, [event.x, event.y, 1])
		# 変換後の元画像の座標がサイズから外れている場合(地図画像の外をクリックしている)
		if (img_xy[0] < 0) or (img_xy[1] < 0) or \
			(img_xy[0] > self.__map_img_pil.width) or (img_xy[1] > self.__map_img_pil.height):
			return
		self.popup_menu.post(event.x_root, event.y_root)   # メニューをポップアップ
		self.right_click_coord = img_xy[0:2]   # クリックされた元画像上の座標を変数に格納
		return



	"""
	+++++ 右クリックしてポップアップメニューのadd waypointをクリックしたときのコールバック関数 +++++
	"""
	def add_waypoint(self):
		print("Clicked \"add waypoint\"", self.right_click_coord)
		pix_xy = np.array(self.right_click_coord, np.uint32)
		print("value=",self.__map_img_pil.getpixel((pix_xy[0], pix_xy[1])))
		return
	


	"""
	+++++ マウスホイールを回転したとき(タッチパッドをドラッグしたとき)のコールバック関数 +++++
	--- docker コンテナ上だとtkinterでマウスホイールイベントが拾えないっぽいので、これは使えないかも ---
	"""
	def mouse_wheel(self, event):
		self.translate_mat(-event.x, -event.y)
		if event.delta > 0:
			# 上に回転(タッチパッドなら下にドラッグ)=> 拡大
			self.scale_mat(1.1)
		else:
			# 下に回転(タッチパッドなら上にドラッグ)=> 縮小
			self.scale_mat(0.9)
		self.translate_mat(event.x, event.y)
		self.draw_image()
		self.plot_origin()
		self.plot_waypoints()
		return
	


	"""
	+++++ Fileメニューの"Save"がクリックされたときに実行されるコールバック関数 +++++
	"""
	def menu_save_clicked(self, event=None):
		print("Clicked \"Save\"")
		return
	


	"""
	+++++  Fileメニューの"Save As"がクリックされたときに実行されるコールバック関数 +++++
	"""
	def menu_saveas_clicked(self, event=None):
		print("Clicked \"Save As\"")
		return
	



	"""
	+++++ 引数の同次変換行列(mat_affine)にx,yの平行移動を加えた同次変換行列を返す +++++
	"""
	def translate_mat(self, x, y):
		mat = np.eye(3)
		mat[0, 2] = float(x)
		mat[1, 2] = float(y)
		self.mat_affine = np.dot(mat, self.mat_affine)
		return
	


	"""
	+++++ 引数のmat_affineにscale倍のリサイズを加えた同次変換行列を返す +++++
	"""
	def scale_mat(self, scale):
		mat = np.eye(3)
		mat[0, 0] = scale
		mat[1, 1] = scale
		self.mat_affine = np.dot(mat, self.mat_affine)
		return

	

	"""
	+++++ 元画像をaffne変換して描画、その画像を返す +++++
	"""
	def draw_image(self):
		mat_inv = np.linalg.inv(self.mat_affine)
		img = self.__map_img_pil.transform(
				(self.canv_w, self.canv_w),
				Image.Transform.AFFINE, tuple(mat_inv.flatten()),
				Image.Resampling.NEAREST,
				fillcolor = 160
				)
		self.draw_img_tk = ImageTk.PhotoImage(image=img)   # 描画する画像を変数に保持
		if not self.canvas.find_withtag("map_image"):   # 初めて画像を描画するとき
			self.canvas.create_image(0, 0, anchor='nw', image=self.draw_img_tk, tags="map_image")   # 画像の描画
		else:
			self.canvas.itemconfig("map_image", image=self.draw_img_tk)   # 既に描画された画像を差し替える
		return



	"""
	+++++ ウィンドウサイズが変更されたとき、情報を更新する +++++
	"""
	def window_resize_callback(self, event):
		cw = self.canvas.winfo_width()
		ch = self.canvas.winfo_height()
		if (self.canv_w != cw) or (self.canv_h != ch):
			self.canv_w = cw
			self.canv_h = ch
		return




#===== メイン処理 プログラムはここから実行される =====#
if __name__ == "__main__":
	#rospy.init_node("manager_GUI")
	root = tk.Tk()   # 大元になるウィンドウ
	w, h = root.winfo_screenwidth(), root.winfo_screenheight()
	root.geometry("%dx%d+0+0" % (w, h))
	app = Application(master=root)   # tk.Frameを継承したApplicationクラスのインスタンス
	app.mainloop()