2022/04/06(水) [n年前の日記]
#1 [python] tkinterで画像の拡大縮小表示を試しているところ
Python + tkinter + Pillow を使って、画像の拡大縮小表示を試しているところ。マウスホイールの上下で画像が拡大縮小したり、「-」「+」ボタンのクリックで拡大縮小できたらいいなと。
以下、参考ページ。
_【Python/tkinter】Canvasに画像を表示する | イメージングソリューション
_Pythonの文法メモ: 【Pillow】ImageOpsモジュールによる画像拡大縮小・トリミング・パディング
_Tkinterの使い方 : スクロールバー(Scrollbar)の使い方 | だえうホームページ
_Python, Pillowで画像を一括リサイズ(拡大・縮小) | note.nkmk.me
_【Python/tkinter】マウスホイールで画像のリサイズを行う | だえうホームページ
動作確認環境は以下。
一応、動作してるっぽい感じのスクリプトは書けたのだけど…。ちょっと問題があるというか…。
_04_canvas_zoom4.py
使用画像は以下。
_tex_1024.png
py 04_canvas_zoom4.py で実行。以下のような感じになった。
ただ、いくつかの問題があって…。
以下、参考ページ。
_【Python/tkinter】Canvasに画像を表示する | イメージングソリューション
_Pythonの文法メモ: 【Pillow】ImageOpsモジュールによる画像拡大縮小・トリミング・パディング
_Tkinterの使い方 : スクロールバー(Scrollbar)の使い方 | だえうホームページ
_Python, Pillowで画像を一括リサイズ(拡大・縮小) | note.nkmk.me
_【Python/tkinter】マウスホイールで画像のリサイズを行う | だえうホームページ
動作確認環境は以下。
- Windows10 x64 21H2 + Python 3.9.12 64bit + Pillow 9.0.1
- Windows10 x64 21H2 + Python 2.7.18 32bit + Pillow 6.2.2
一応、動作してるっぽい感じのスクリプトは書けたのだけど…。ちょっと問題があるというか…。
_04_canvas_zoom4.py
import sys import platform if sys.version_info.major == 2: # Python 2.7 import Tkinter as tk else: # Python 3.x import tkinter as tk from PIL import Image, ImageTk, ImageOps class App(tk.Frame, object): def __init__(self, master=None): try: # Python 3.x super().__init__(master) except Exception: # Python 2.7 # tk.Frame.__init__(self, master) super(App, self).__init__(master) self.master.title("Canvas zoom sample") # self.master.geometry("512x512") self.rootfrm = tk.Frame(self.master) self.rootfrm.pack(expand=1, fill=tk.BOTH) self.rootfrm.rowconfigure(1, weight=1) self.rootfrm.columnconfigure(0, weight=1) # create Button self.btnfrm = tk.Frame(self.rootfrm) self.btnfrm.grid(row=0, column=0, sticky=tk.W) b0 = tk.Button(self.btnfrm, text="Fit", command=lambda: self.set_zoom_fit()) b1 = tk.Button(self.btnfrm, text="-", command=lambda: self.dec_ratio()) b2 = tk.Button(self.btnfrm, text="1:1", command=lambda: self.set_zoom_full()) b3 = tk.Button(self.btnfrm, text="+", command=lambda: self.inc_ratio()) b0.pack(side=tk.LEFT, padx=4, ipadx=8) b1.pack(side=tk.LEFT, padx=4, ipadx=8) b2.pack(side=tk.LEFT, padx=4, ipadx=8) b3.pack(side=tk.LEFT, padx=4, ipadx=8) self.ratiostr = tk.StringVar() self.ratiostr.set("100%") self.ratiolbl = tk.Label(self.btnfrm, textvariable=self.ratiostr, relief=tk.GROOVE) self.ratiolbl.pack(side=tk.LEFT, padx=4, ipadx=8) # create Canvas self.cvsfrm = tk.Frame(self.rootfrm) self.cvsfrm.grid(row=1, column=0, sticky=tk.N + tk.S + tk.W + tk.E) self.cvsfrm.rowconfigure(0, weight=1) self.cvsfrm.columnconfigure(0, weight=1) self.canvas = tk.Canvas(self.cvsfrm, bg="#666666", width=512, height=256) self.canvas.grid(row=0, column=0, sticky=tk.N + tk.S + tk.W + tk.E) # create Scrollbar xbar = tk.Scrollbar(self.cvsfrm, orient=tk.HORIZONTAL, command=self.canvas.xview) ybar = tk.Scrollbar(self.cvsfrm, orient=tk.VERTICAL, command=self.canvas.yview) xbar.grid(row=1, column=0, sticky=tk.W + tk.E) ybar.grid(row=0, column=1, sticky=tk.N + tk.S) self.canvas.config(xscrollcommand=xbar.set) self.canvas.config(yscrollcommand=ybar.set) # set left button drag scroll self.canvas.bind("<ButtonPress-1>", lambda e: self.canvas.scan_mark(e.x, e.y)) self.canvas.bind("<B1-Motion>", lambda e: self.canvas.scan_dragto(e.x, e.y, gain=1)) # set middle button drag scroll self.canvas.bind("<ButtonPress-2>", lambda e: self.canvas.scan_mark(e.x, e.y)) self.canvas.bind("<B2-Motion>", lambda e: self.canvas.scan_dragto(e.x, e.y, gain=1)) # set mouse wheel event # .bind_all() ... Worked with Python 2.7 and Python 3.9 # .bind() ... It worked in Python 3.9, but not in Python 2.7. self.canvas.bind_all("<MouseWheel>", lambda e: self.event_wheel(e)) if platform.system() == "Linux": self.canvas.bind_all("<Button-4>", lambda e: self.dec_ratio()) self.canvas.bind_all("<Button-5>", lambda e: self.inc_ratio()) self.reset_ratio() self.zoomfit = False self.im = Image.open("tex_1024.png") self.set_image() def set_zoom_fit(self): self.zoomfit = True self.set_image() def set_zoom_full(self): self.zoomfit = False self.set_image() def reset_ratio(self): self.ratio = 100 def inc_ratio(self): # check 32bit, 64bit, Py27, Py3x is64bits = (sys.maxsize > 2**32) ispy3x = (sys.version_info.major >= 3) maxsize = 1024 * (8 if (is64bits and ispy3x) else 5) w, h = self.im.size maxratio = min([(maxsize * 100 / w), (maxsize * 100 / h)]) if self.ratio == maxratio: return self.ratio = self.ratio + (10 if self.ratio < 100 else 100) self.ratio = min([self.ratio, maxratio]) self.change_ratio() def dec_ratio(self): minratio = 10 if self.ratio == minratio: return self.ratio = self.ratio - (10 if self.ratio <= 100 else 100) self.ratio = max([self.ratio, minratio]) self.change_ratio() def event_wheel(self, e): if e.delta > 0: self.dec_ratio() elif e.delta < 0: self.inc_ratio() def change_ratio(self): w, h = self.im.size w = int(w * self.ratio / 100) h = int(h * self.ratio / 100) if w < 0 or h < 0: return self.canvas.delete("all") if self.ratio < 100: self.im_pad = self.im.resize((w, h), Image.LANCZOS) self.photo_image = ImageTk.PhotoImage(image=self.im_pad) elif self.ratio > 100: self.im_pad = self.im.resize((w, h), Image.NEAREST) self.photo_image = ImageTk.PhotoImage(image=self.im_pad) else: self.photo_image = ImageTk.PhotoImage(image=self.im) self.create_canvas_image() def set_image(self): self.update() cw = self.canvas.winfo_width() ch = self.canvas.winfo_height() self.canvas.delete("all") if self.zoomfit: # view fit self.im_pad = ImageOps.pad(self.im, (cw - 4, ch - 4), method=Image.LANCZOS) self.photo_image = ImageTk.PhotoImage(image=self.im_pad) ow, oh = self.im.size nw = self.photo_image.width() nh = self.photo_image.height() rw = float(nw) / float(ow) rh = float(nh) / float(oh) self.ratio = int(min([rw, rh]) * 100) else: # view 1:1 self.reset_ratio() self.photo_image = ImageTk.PhotoImage(image=self.im) self.create_canvas_image() def create_canvas_image(self): self.canvas.create_image(0, 0, image=self.photo_image, anchor=tk.NW) # set scroll region iw = self.photo_image.width() ih = self.photo_image.height() region = (0, 0, iw, ih) self.canvas.config(scrollregion=region) # update ratio label self.ratiostr.set("%d%%" % int(self.ratio)) def main(): root = tk.Tk() app = App(master=root) app.mainloop() if __name__ == '__main__': main()
使用画像は以下。
_tex_1024.png
py 04_canvas_zoom4.py で実行。以下のような感じになった。
ただ、いくつかの問題があって…。
◎ 問題点。 :
問題点その1。Python 2.7.18 32bit + Pillow 6.2.2 で実行すると、1024 x 1024 の画像を8倍(800%)に拡大しようとした際に ―― つまりは 8192 x 8192 の画像サイズに拡大して変換しようとした際に、MemoryError が発生してしまう。RAMは16GB積んでるのだけどな…。
以下の行でエラーが出ている模様。Pillow の Image を、tkinter で利用できるように、ImageTk.PhotoImage() を使って変換しようとしたタイミングでメモリエラーが発生している。
ちなみに、Python 3.9.12 64bit + Pillow 9.0.1 で実行すると、8倍(800%)に拡大してもメモリエラーは発生しない。
原因は何だろう…。Python 32bit版では発生するけど、64bit版では発生しないから、32bit版の制限なのだろうか。それとも、Python 2.7.18 32bit で動かしている Pillow は 6.2.2 とバージョンが古いのでバグを持っているのだろうか。
仕方ないので、Python 2.* 32bit で動かしている時だけ、最大拡大率を5倍までに制限してみた。ダサいけど、仕方ない。
問題点その2。画像を縮小するのはともかく、拡大時が重い…。処理時間が数秒かかってしまう…。
参考ページでは、キャンバスに描画されるはずの範囲だけ事前にクロップしてから拡大処理(Image.resize())すれば改善される、という対策が紹介されていたけれど。今回は、拡大状態でスクロールバー等を操作したら、キャンバス外の部分もスクロールして見れるようにしたいので、クロップしてしまうとよろしくない。
このあたりは諦めるしかないかな…。本来、こういった画像ビューアっぽいことをやりたいなら、例えば OpenGL等を使ったライブラリを利用するべきだろうか。単に画像を表示させるだけなら、CPUじゃなくてGPUを働かせるべき、かもしれない…。
余談。マウスホイールの取得でちょっとハマった。
Python 3.9.12 64bit の場合は、.bind() で動いたけれど、Python 2.7.18 32bit の場合は反応してくれなかった。.bind() ではなく、.bind_all() に変えてみたら、Python 2.7.18 32bit でも動くようになった。ただ、何が原因なのかは分かってない…。
> py -2 04_canvas_zoom4.py Exception in Tkinter callback Traceback (most recent call last): File "C:\Python\Python27\lib\lib-tk\Tkinter.py", line 1547, in __call__ return self.func(*args) File "04_canvas_zoom4.py", line 69, in <lambda> b3 = tk.Button(self.btnfrm, text="+", command=lambda: self.inc_ratio()) File "04_canvas_zoom4.py", line 147, in inc_ratio self.change_ratio() File "04_canvas_zoom4.py", line 178, in change_ratio self.photo_image = ImageTk.PhotoImage(image=self.im_pad) File "C:\Python\Python27\lib\site-packages\PIL\ImageTk.py", line 121, in __init__ self.paste(image) File "C:\Python\Python27\lib\site-packages\PIL\ImageTk.py", line 176, in paste block = image.new_block(self.__mode, im.size) MemoryError
以下の行でエラーが出ている模様。Pillow の Image を、tkinter で利用できるように、ImageTk.PhotoImage() を使って変換しようとしたタイミングでメモリエラーが発生している。
self.photo_image = ImageTk.PhotoImage(image=self.im_pad)
ちなみに、Python 3.9.12 64bit + Pillow 9.0.1 で実行すると、8倍(800%)に拡大してもメモリエラーは発生しない。
原因は何だろう…。Python 32bit版では発生するけど、64bit版では発生しないから、32bit版の制限なのだろうか。それとも、Python 2.7.18 32bit で動かしている Pillow は 6.2.2 とバージョンが古いのでバグを持っているのだろうか。
仕方ないので、Python 2.* 32bit で動かしている時だけ、最大拡大率を5倍までに制限してみた。ダサいけど、仕方ない。
def inc_ratio(self): # check 32bit, 64bit, Py27, Py3x is64bits = (sys.maxsize > 2**32) ispy3x = (sys.version_info.major >= 3) maxsize = 1024 * (8 if (is64bits and ispy3x) else 5)
問題点その2。画像を縮小するのはともかく、拡大時が重い…。処理時間が数秒かかってしまう…。
参考ページでは、キャンバスに描画されるはずの範囲だけ事前にクロップしてから拡大処理(Image.resize())すれば改善される、という対策が紹介されていたけれど。今回は、拡大状態でスクロールバー等を操作したら、キャンバス外の部分もスクロールして見れるようにしたいので、クロップしてしまうとよろしくない。
このあたりは諦めるしかないかな…。本来、こういった画像ビューアっぽいことをやりたいなら、例えば OpenGL等を使ったライブラリを利用するべきだろうか。単に画像を表示させるだけなら、CPUじゃなくてGPUを働かせるべき、かもしれない…。
余談。マウスホイールの取得でちょっとハマった。
Python 3.9.12 64bit の場合は、.bind() で動いたけれど、Python 2.7.18 32bit の場合は反応してくれなかった。.bind() ではなく、.bind_all() に変えてみたら、Python 2.7.18 32bit でも動くようになった。ただ、何が原因なのかは分かってない…。
self.canvas.bind("<MouseWheel>", lambda e: self.event_wheel(e)) ↓ self.canvas.bind_all("<MouseWheel>", lambda e: self.event_wheel(e))
◎ 2022/04/07追記。 :
Ubuntu Linux 20.04 LTS (64bit)上でも動作確認してみた。スクリプトを少し修正する必要があった。
詳細は、
_2022/04/07 の日記
にメモ。
[ ツッコむ ]
以上です。