Skip to content

Instantly share code, notes, and snippets.

@ImagingSolution
Last active March 15, 2023 00:11
Show Gist options
  • Save ImagingSolution/bf7d9b348a2cc31c300ebb080171150b to your computer and use it in GitHub Desktop.
Save ImagingSolution/bf7d9b348a2cc31c300ebb080171150b to your computer and use it in GitHub Desktop.
python image viewer
import tkinter as tk # ウィンドウ作成用
from tkinter import filedialog # ファイルを開くダイアログ用
from PIL import Image, ImageTk # 画像データ用
import numpy as np # アフィン変換行列演算用
import os # ディレクトリ操作用
class Application(tk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.pack()
self.pil_image = None # 表示する画像データ
self.my_title = "Image Viewer" # タイトル
self.back_color = "#008B8B" # 背景色
# ウィンドウの設定
self.master.title(self.my_title) # タイトル
self.master.geometry("500x400") # サイズ
self.create_menu() # メニューの作成
self.create_widget() # ウィジェットの作成
def menu_open_clicked(self, event=None):
# ファイル→開く
filename = tk.filedialog.askopenfilename(
filetypes = [("Image file", ".bmp .png .jpg .tif"), ("Bitmap", ".bmp"), ("PNG", ".png"), ("JPEG", ".jpg"), ("Tiff", ".tif") ], # ファイルフィルタ
initialdir = os.getcwd() # カレントディレクトリ
)
# 画像ファイルを設定する
self.set_image(filename)
def menu_quit_clicked(self):
# ウィンドウを閉じる
self.master.destroy()
# create_menuメソッドを定義
def create_menu(self):
self.menu_bar = tk.Menu(self) # Menuクラスからmenu_barインスタンスを生成
self.file_menu = tk.Menu(self.menu_bar, tearoff = tk.OFF)
self.menu_bar.add_cascade(label="File", menu=self.file_menu)
self.file_menu.add_command(label="Open", command = self.menu_open_clicked, accelerator="Ctrl+O")
self.file_menu.add_separator() # セパレーターを追加
self.file_menu.add_command(label="Exit", command = self.menu_quit_clicked)
self.menu_bar.bind_all("<Control-o>", self.menu_open_clicked) # ファイルを開くのショートカット(Ctrol-Oボタン)
self.master.config(menu=self.menu_bar) # メニューバーの配置
def create_widget(self):
'''ウィジェットの作成'''
# ステータスバー相当(親に追加)
self.statusbar = tk.Frame(self.master)
self.mouse_position = tk.Label(self.statusbar, relief = tk.SUNKEN, text="mouse position") # マウスの座標
self.image_position = tk.Label(self.statusbar, relief = tk.SUNKEN, text="image position") # 画像の座標
self.label_space = tk.Label(self.statusbar, relief = tk.SUNKEN) # 隙間を埋めるだけ
self.image_info = tk.Label(self.statusbar, relief = tk.SUNKEN, text="image info") # 画像情報
self.mouse_position.pack(side=tk.LEFT)
self.image_position.pack(side=tk.LEFT)
self.label_space.pack(side=tk.LEFT, expand=True, fill=tk.X)
self.image_info.pack(side=tk.RIGHT)
self.statusbar.pack(side=tk.BOTTOM, fill=tk.X)
# Canvas
self.canvas = tk.Canvas(self.master, background= self.back_color)
self.canvas.pack(expand=True, fill=tk.BOTH) # この両方でDock.Fillと同じ
# マウスイベント
self.master.bind("<Motion>", self.mouse_move) # MouseMove
self.master.bind("<B1-Motion>", self.mouse_move_left) # MouseMove(左ボタンを押しながら移動)
self.master.bind("<Button-1>", self.mouse_down_left) # MouseDown(左ボタン)
self.master.bind("<Double-Button-1>", self.mouse_double_click_left) # MouseDoubleClick(左ボタン)
self.master.bind("<MouseWheel>", self.mouse_wheel) # MouseWheel
def set_image(self, filename):
''' 画像ファイルを開く '''
if not filename:
return
# PIL.Imageで開く
self.pil_image = Image.open(filename)
# 画像全体に表示するようにアフィン変換行列を設定
self.zoom_fit(self.pil_image.width, self.pil_image.height)
# 画像の表示
self.draw_image(self.pil_image)
# ウィンドウタイトルのファイル名を設定
self.master.title(self.my_title + " - " + os.path.basename(filename))
# ステータスバーに画像情報を表示する
self.image_info["text"] = f"{self.pil_image.format} : {self.pil_image.width} x {self.pil_image.height} {self.pil_image.mode}"
# カレントディレクトリの設定
os.chdir(os.path.dirname(filename))
# -------------------------------------------------------------------------------
# マウスイベント
# -------------------------------------------------------------------------------
def mouse_move(self, event):
''' マウスの移動時 '''
# マウス座標
self.mouse_position["text"] = f"mouse(x, y) = ({event.x: 4d}, {event.y: 4d})"
if self.pil_image == None:
return
# 画像座標
mouse_posi = np.array([event.x, event.y, 1]) # マウス座標(numpyのベクトル)
mat_inv = np.linalg.inv(self.mat_affine) # 逆行列(画像→Cancasの変換からCanvas→画像の変換へ)
image_posi = np.dot(mat_inv, mouse_posi) # 座標のアフィン変換
x = int(np.floor(image_posi[0]))
y = int(np.floor(image_posi[1]))
if x >= 0 and x < self.pil_image.width and y >= 0 and y < self.pil_image.height:
# 輝度値の取得
value = self.pil_image.getpixel((x, y))
self.image_position["text"] = f"image({x: 4d}, {y: 4d}) = {value}"
else:
self.image_position["text"] = "-------------------------"
def mouse_move_left(self, event):
''' マウスの左ボタンをドラッグ '''
if self.pil_image == None:
return
self.translate(event.x - self.__old_event.x, event.y - self.__old_event.y)
self.redraw_image() # 再描画
self.__old_event = event
def mouse_down_left(self, event):
''' マウスの左ボタンを押した '''
self.__old_event = event
def mouse_double_click_left(self, event):
''' マウスの左ボタンをダブルクリック '''
if self.pil_image == None:
return
self.zoom_fit(self.pil_image.width, self.pil_image.height)
self.redraw_image() # 再描画
def mouse_wheel(self, event):
''' マウスホイールを回した '''
if self.pil_image == None:
return
if (event.delta < 0):
# 上に回転の場合、縮小
self.scale_at(0.8, event.x, event.y)
else:
# 下に回転の場合、拡大
self.scale_at(1.25, event.x, event.y)
self.redraw_image() # 再描画
# -------------------------------------------------------------------------------
# 画像表示用アフィン変換
# -------------------------------------------------------------------------------
def reset_transform(self):
'''アフィン変換を初期化(スケール1、移動なし)に戻す'''
self.mat_affine = np.eye(3) # 3x3の単位行列
def translate(self, offset_x, offset_y):
''' 平行移動 '''
mat = np.eye(3) # 3x3の単位行列
mat[0, 2] = float(offset_x)
mat[1, 2] = float(offset_y)
self.mat_affine = np.dot(mat, self.mat_affine)
def scale(self, scale:float):
''' 拡大縮小 '''
mat = np.eye(3) # 単位行列
mat[0, 0] = scale
mat[1, 1] = scale
self.mat_affine = np.dot(mat, self.mat_affine)
def scale_at(self, scale:float, cx:float, cy:float):
''' 座標(cx, cy)を中心に拡大縮小 '''
# 原点へ移動
self.translate(-cx, -cy)
# 拡大縮小
self.scale(scale)
# 元に戻す
self.translate(cx, cy)
def zoom_fit(self, image_width, image_height):
'''画像をウィジェット全体に表示させる'''
# キャンバスのサイズ
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if (image_width * image_height <= 0) or (canvas_width * canvas_height <= 0):
return
# アフィン変換の初期化
self.reset_transform()
scale = 1.0
offsetx = 0.0
offsety = 0.0
if (canvas_width * image_height) > (image_width * canvas_height):
# ウィジェットが横長(画像を縦に合わせる)
scale = canvas_height / image_height
# あまり部分の半分を中央に寄せる
offsetx = (canvas_width - image_width * scale) / 2
else:
# ウィジェットが縦長(画像を横に合わせる)
scale = canvas_width / image_width
# あまり部分の半分を中央に寄せる
offsety = (canvas_height - image_height * scale) / 2
# 拡大縮小
self.scale(scale)
# あまり部分を中央に寄せる
self.translate(offsetx, offsety)
# -------------------------------------------------------------------------------
# 描画
# -------------------------------------------------------------------------------
def draw_image(self, pil_image):
if pil_image == None:
return
self.canvas.delete("all")
# キャンバスのサイズ
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
# キャンバスから画像データへのアフィン変換行列を求める
#(表示用アフィン変換行列の逆行列を求める)
mat_inv = np.linalg.inv(self.mat_affine)
# PILの画像データをアフィン変換する
dst = pil_image.transform(
(canvas_width, canvas_height), # 出力サイズ
Image.AFFINE, # アフィン変換
tuple(mat_inv.flatten()), # アフィン変換行列(出力→入力への変換行列)を一次元のタプルへ変換
Image.NEAREST, # 補間方法、ニアレストネイバー
fillcolor= self.back_color
)
# 表示用画像を保持
self.image = ImageTk.PhotoImage(image=dst)
# 画像の描画
self.canvas.create_image(
0, 0, # 画像表示位置(左上の座標)
anchor='nw', # アンカー、左上が原点
image=self.image # 表示画像データ
)
def redraw_image(self):
''' 画像の再描画 '''
if self.pil_image == None:
return
self.draw_image(self.pil_image)
if __name__ == "__main__":
root = tk.Tk()
app = Application(master=root)
app.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment