2022/04/07(木) [n年前の日記]
#1 [python] tkinterのキャンバスで画像を拡大縮小させるスクリプトの動作確認中
_昨日作成した
Python + tkinter + Pillow を使って画像表示するスクリプトを、Ubuntu Linux 20.04 LTS (64bit)上でも動かしてみた。
◎ メモリエラーの問題。 :
Windows10 x64 21H2 + Python 2.7.18 32bit 上では、1024 x 1024の画像を8倍(8192 x 8192)まで拡大表示した際にメモリエラーが発生していたのだけど。
Ubuntu Linux 20.04 LTS上なら、Python 2.7.18 64bit、Python 3.8.10 64bit、どちらも8倍まで拡大表示できた。
つまり、この問題は、Python や Pillow のバージョンが原因では無く、32bit版か否かに起因する、ということになるのだろう…。
Ubuntu Linux 20.04 LTS上なら、Python 2.7.18 64bit、Python 3.8.10 64bit、どちらも8倍まで拡大表示できた。
つまり、この問題は、Python や Pillow のバージョンが原因では無く、32bit版か否かに起因する、ということになるのだろう…。
◎ Pillowのアルゴリズム指定が変更された。 :
Ubuntu Linux 20.04 LTS + Python 3.8.10 + Pillow 9.1.0 で動かした際、拡大縮小アルゴリズムの種類を示す Image.LANCZOS や Image.NEAREST という指定に対して警告が表示された。
Image.LANCZOS や Image.NEAREST は Pillow 10.x で削除される予定らしい…。Image.Resampleing.LANCZOS や Image.Resampling.NEAREST と記述せよとのこと。
しかし、新しい記述の仕方をしたら、例えば Python 2.7.x + Pillow 6.x.x で動かないスクリプトになりそうな気がする…。まあ、以下のような書き方をすれば動きそう。たぶん。
$ python3 04_canvas_zoom4.py 04_canvas_zoom4.py:173: DeprecationWarning: LANCZOS is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.LANCZOS instead. self.im_pad = self.im.resize((w, h), Image.LANCZOS) 04_canvas_zoom4.py:194: DeprecationWarning: LANCZOS is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.LANCZOS instead. self.im_pad = ImageOps.pad(self.im, (cw - 4, ch - 4), method=Image.LANCZOS) 04_canvas_zoom4.py:179: DeprecationWarning: NEAREST is deprecated and will be removed in Pillow 10 (2023-07-01). Use Resampling.NEAREST or Dither.NONE instead. self.im_pad = self.im.resize((w, h), Image.NEAREST)
Image.LANCZOS や Image.NEAREST は Pillow 10.x で削除される予定らしい…。Image.Resampleing.LANCZOS や Image.Resampling.NEAREST と記述せよとのこと。
しかし、新しい記述の仕方をしたら、例えば Python 2.7.x + Pillow 6.x.x で動かないスクリプトになりそうな気がする…。まあ、以下のような書き方をすれば動きそう。たぶん。
try: mkind = Image.Resampling.LANCZOS except Exception: mkind = Image.LANCZOS self.im_pad = self.im.resize((w, h), mkind)
◎ 修正版のスクリプト。 :
そんなわけで、スクリプトを少し修正。Windows10 x64 21H2 上でも、Ubuntu Linux 20.04 LTS 上でも、どちらでも動いてくれた。
_04_canvas_zoom4.py
使用画像は以下。
_tex_1024.png
_04_canvas_zoom4.py
使用画像は以下。
_tex_1024.png
◎ zoom や subsample というメソッドもあった。 :
tkinter の PhotoImage についてググってたら、zoom や subsample という、拡大や縮小に使えるメソッドがあると知った。
整数を指定することしかできないので、細かいリサイズはできないけれど、これらのメソッドを使えば、Pillow を使って画像をリサイズするのと比べて、メモリエラーが出にくいのではと思えてきた。Pillow の ImageTk.PhotoImage() に、リサイズ後の巨大な Image を渡してしまうとメモリエラーが発生するわけだけど、PhotoImage.zoom() を使って拡大処理をすれば、Pillow の ImageTk.PhotoImage() は元々の画像サイズで変換することになるので、メモリエラーを回避できるのでは、と…。
ということで、試してみた。
_01_photoimage_zoom.py
使用画像は以下。
_tex_1024.png
動作確認環境は以下。
py 01_photoimage_zoom.py で実行。以下のような感じで動作した。
Python 2.7.18 32bit + Pillow 6.2.2 で処理をした場合、1024 x 1024 の画像を8倍(8192 x 8192)まで拡大させるとメモリエラーが発生してしまうけれど。.zoom() を使うと、Python 2.7.18 32bit上で9倍(9216 x 9216)まで拡大してもメモリエラーは出なかった。
しかし、どこまでも拡大できるわけでもないようで…。11倍(11264 x 11264)まで拡大しようとしたらメモリエラーが発生した。
また、10倍(10240 x 10240)まで拡大してから、9倍に戻そうとしたら、そのタイミングでもメモリエラーが発生した。
ということで、.zoom() を使えば、Python 32bit版上でも、Pillow でリサイズ処理をする場合と比べて、もうちょっと大きく拡大表示ができることが分かった。ただ、拡大率を上げていくと、そのうちどこかでメモリエラーが発生するし、拡大すると処理時間が目に見えて遅くなるあたりはどちらも同じなので、五十歩百歩な感も否めない…。
余談。Pillow の ImageTk.PhotoImage() を使って得られた PhotoImage に対して、.zoom() や .subsample() を使いたい場合は、hoge._PhotoImage__photo.zoom(n) といった具合に記述するらしい。「._PhotoImage__photo.」を挿入することで、.zoom() 等が呼べるようになる模様。
_python - PhotoImage zoom - Stack Overflow
_Tkinter Photoimage | Delft Stack
- PhotoImage.zoom(n) : 整数倍(n倍)で拡大する。
- PhotoImage.subsample(n) : 整数分の一(1/n)で縮小する。
整数を指定することしかできないので、細かいリサイズはできないけれど、これらのメソッドを使えば、Pillow を使って画像をリサイズするのと比べて、メモリエラーが出にくいのではと思えてきた。Pillow の ImageTk.PhotoImage() に、リサイズ後の巨大な Image を渡してしまうとメモリエラーが発生するわけだけど、PhotoImage.zoom() を使って拡大処理をすれば、Pillow の ImageTk.PhotoImage() は元々の画像サイズで変換することになるので、メモリエラーを回避できるのでは、と…。
ということで、試してみた。
_01_photoimage_zoom.py
try: import Tkinter as tk except Exception: import tkinter as tk from PIL import Image, ImageTk class App(tk.Frame): def __init__(self, master=None): tk.Frame.__init__(self, master) self.frm = tk.Frame(self.master) self.frm.pack(expand=1, fill=tk.BOTH) self.frm.rowconfigure(1, weight=1) self.frm.columnconfigure(2, weight=1) self.btn0 = tk.Button(self.frm, text="-", command=lambda: self.dec_ratio()) self.btn1 = tk.Button(self.frm, text="+", command=lambda: self.inc_ratio()) self.zoomstr = tk.StringVar() self.zoomlbl = tk.Label(self.frm, textvariable=self.zoomstr) self.btn0.grid(row=0, column=0, ipadx=4) self.btn1.grid(row=0, column=1, ipadx=4) self.zoomlbl.grid(row=0, column=2) self.canvas = tk.Canvas(self.frm, bg="#666666") self.canvas.grid(row=1, column=0, columnspan=3, sticky=tk.N + tk.S + tk.W + tk.E) self.im = Image.open("tex_1024.png") self.photo_image = ImageTk.PhotoImage(image=self.im) self.ratio = 1 self.set_image() def set_image(self): self.canvas.delete("all") if self.ratio == 1: # 1:1 self.zoomstr.set("%d%%" % (self.ratio * 100)) self.zoom_image = self.photo_image elif self.ratio > 1: # zoom (x 2, 3, 4, ...) zm = int(self.ratio) self.zoomstr.set("%d%%" % (self.ratio * 100)) self.zoom_image = self.photo_image._PhotoImage__photo.zoom(zm) else: # subsample (/ 2, 3, 4, ...) zm = -self.ratio r = int(100 / zm) self.zoomstr.set("%d%%" % r) self.zoom_image = self.photo_image._PhotoImage__photo.subsample(zm) self.canvas.create_image(0, 0, image=self.zoom_image, anchor=tk.NW) def inc_ratio(self): maxratio = 16 if self.ratio < maxratio: self.ratio = self.ratio + 1 self.ratio = min([maxratio, self.ratio]) if -1 <= self.ratio <= 1: self.ratio = 1 self.set_image() def dec_ratio(self): minratio = -10 if self.ratio > minratio: self.ratio = self.ratio - 1 self.ratio = max([minratio, self.ratio]) if -1 <= self.ratio <= 0: self.ratio = -2 self.set_image() def main(): root = tk.Tk() app = App(master=root) app.mainloop() if __name__ == '__main__': main()
使用画像は以下。
_tex_1024.png
動作確認環境は以下。
- Windows10 x64 21H2 + Python 3.9.12 64bit + Pillow 9.1.0
- Windows10 x64 21H2 + Python 2.7.18 32bit + Pillow 6.2.2
py 01_photoimage_zoom.py で実行。以下のような感じで動作した。
Python 2.7.18 32bit + Pillow 6.2.2 で処理をした場合、1024 x 1024 の画像を8倍(8192 x 8192)まで拡大させるとメモリエラーが発生してしまうけれど。.zoom() を使うと、Python 2.7.18 32bit上で9倍(9216 x 9216)まで拡大してもメモリエラーは出なかった。
しかし、どこまでも拡大できるわけでもないようで…。11倍(11264 x 11264)まで拡大しようとしたらメモリエラーが発生した。
> py -2 01_photoimage_zoom.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 "01_photoimage_zoom.py", line 32, in <lambda> self.btn1 = tk.Button(self.frm, text="+", command=lambda: self.inc_ratio()) File "01_photoimage_zoom.py", line 75, in inc_ratio self.set_image() File "01_photoimage_zoom.py", line 58, in set_image self.zoom_image = self.photo_image._PhotoImage__photo.zoom(zm) File "C:\Python\Python27\lib\lib-tk\Tkinter.py", line 3400, in zoom self.tk.call(destImage, 'copy', self.name, '-zoom',x,y) TclError: not enough free memory for image buffer
また、10倍(10240 x 10240)まで拡大してから、9倍に戻そうとしたら、そのタイミングでもメモリエラーが発生した。
ということで、.zoom() を使えば、Python 32bit版上でも、Pillow でリサイズ処理をする場合と比べて、もうちょっと大きく拡大表示ができることが分かった。ただ、拡大率を上げていくと、そのうちどこかでメモリエラーが発生するし、拡大すると処理時間が目に見えて遅くなるあたりはどちらも同じなので、五十歩百歩な感も否めない…。
余談。Pillow の ImageTk.PhotoImage() を使って得られた PhotoImage に対して、.zoom() や .subsample() を使いたい場合は、hoge._PhotoImage__photo.zoom(n) といった具合に記述するらしい。「._PhotoImage__photo.」を挿入することで、.zoom() 等が呼べるようになる模様。
_python - PhotoImage zoom - Stack Overflow
_Tkinter Photoimage | Delft Stack
[ ツッコむ ]
#2 [python][wxpython] wxPython 3.0と4.0の違いを少し調べた
昔、wxPython 3.0.2.0 用に書いていたスクリプトが、wxPython 4.1.0 で動かないことに気が付いたので、どこらへんの仕様が変わったのか、ほんの少しだけ調べてみた。
ある程度は、以下のページで記述されてる気配はあるけれど…。
_wxPython Changelog | wxPython
確認した環境は以下。
気づいたところをいくつかメモ。
wx.EVT_PAINT(self, self.OnPaint) や wx.EVT_SIZE(self, self.OnSize) という書き方は非推奨。.Bind() を使って指定する。
wx.Menu の .Append() 内で text="..." という書き方はできなくなった。
他にもまだまだ変更点があるはずだけど…。
ある程度は、以下のページで記述されてる気配はあるけれど…。
_wxPython Changelog | wxPython
確認した環境は以下。
- Windows10 x64 21H2 + Python 2.7.18 32bit + wxPython 4.1.0
- Windows10 x64 21H2 + Python 3.9.12 64bit + wxPython 4.1.1
気づいたところをいくつかメモ。
- .Ok() は .IsOk() になった。昔はどちらも使えたけれど、wxPython 4.0 は .IsOk() のみになったらしい。
- wx.Rect() の .InsideXY(x, y) は .Contains(x, y) になった。
- .GetClientSizeTuple() は .GetClientSize().Get() になった。
- wx.SAVE は wx.FD_SAVE になった。
- wx.EmptyBitmap() は wx.Bitmap() になった。
- wx.FileDropTarget の .OnDropFiles() はbool値(True or False)を返すことを強制されるようになった。
- .BeginDrawing()、.EndDrawing() は呼ばなくていい。
- wx.NewId() は非推奨になった。代わりに wx.NewIdRef() を使う。
- wx.PySimpleApp() は非推奨になった。代わりに wx.App() を使う。
wx.EVT_PAINT(self, self.OnPaint) や wx.EVT_SIZE(self, self.OnSize) という書き方は非推奨。.Bind() を使って指定する。
wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) ↓ self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize)
wx.Menu の .Append() 内で text="..." という書き方はできなくなった。
item = file_menu.Append(wx.ID_EXIT, text="&Exit") ↓ item = file_menu.Append(wx.ID_EXIT, "&Exit")
他にもまだまだ変更点があるはずだけど…。
◎ 2022/04/08追記。 :
◎ 修正したスクリプト。 :
上記の変更を反映させて、以前書いたスクリプトを修正してみた。画像をウインドウ内にドラッグアンドドロップ(DnD)すると画像を表示して、その画像をドラッグすると位置を移動できるスクリプト。
_drag_image.py
py drag_image.py で実行。以下のような感じになった。
_drag_image.py
import wx USE_BUFFERED_DC = True class MyObj(): """マウスドラッグで移動できるオブジェクト用クラス""" def __init__(self, bmp, x=0, y=0): """初期化""" self.bmp = bmp # bitmapを記録 self.pos = wx.Point(x, y) # 表示位置を記録 self.diff_pos = wx.Point(0, 0) def HitTest(self, pnt): """座標とアタリ判定""" rect = self.GetRect() # 矩形領域を取得 # 座標が矩形内に入ってるか調べる try: r = rect.InsideXY(pnt.x, pnt.y) except: r = rect.Contains(pnt.x, pnt.y) return r def GetRect(self): """矩形領域を返す""" return wx.Rect(self.pos.x, self.pos.y, self.bmp.GetWidth(), self.bmp.GetHeight()) def SavePosDiff(self, pnt): """ マウス座標と自分の座標の相対値を記録。 この情報がないと、画像をドラッグした時の表示位置がしっくりこない """ self.diff_pos.x = self.pos.x - pnt.x self.diff_pos.y = self.pos.y - pnt.y def Draw(self, dc, select_enable): """与えられたDCを使って画像を描画""" try: have_bitmap = self.bmp.Ok() except: have_bitmap = self.bmp.IsOk() if not have_bitmap: return False r = self.GetRect() # 矩形領域を取得 # ペンを設定しないと何故か描画できない dc.SetPen(wx.Pen(wx.BLACK, 4)) dc.DrawBitmap(self.bmp, r.x, r.y, True) # 画像を描画 if select_enable: # 画像枠を描画 dc.SetBrush(wx.TRANSPARENT_BRUSH) # 透明塗り潰し dc.SetPen(wx.Pen(wx.RED, 1)) # 赤い線を指定 dc.DrawRectangle(r.x, r.y, r.width, r.height) # 矩形を描画 return True class MyFileDropTarget(wx.FileDropTarget): """ドラッグアンドドロップ担当クラス""" def __init__(self, obj): """初期化""" wx.FileDropTarget.__init__(self) self.obj = obj # ファイルのドロップ対象を覚えておく def OnDropFiles(self, x, y, filenames): """ファイルドロップ時""" self.obj.LoadImage(filenames) # 親?の画像読み込みメソッドを呼ぶ return True class MyFrame(wx.Frame): """ダブルバッファで表示するFrame""" def __init__(self, parent=None, title=""): """初期化""" wx.Frame.__init__(self, parent=parent, title=title, size=(800, 600)) # PAINTイベント、SIZEイベントで呼ばれるメソッドを割り当てる self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) # マウスボタンを押した時に呼ばれるメソッドを割り当てる self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown) self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp) # マウスカーソルを動かした時に呼ばれるメソッドを割り当てる self.Bind(wx.EVT_MOTION, self.OnMouseMotion) # 画像格納用リストを初期化 self.objs = [] # マウスドラッグ処理用の変数を確保 self.drag_obj = None self.drag_start_pos = wx.Point(0, 0) # 描画用バッファ初期化のために一度 OnSize() を呼ぶ self.OnSize() # ファイルドロップの対象をフレーム全体に self.droptarget = MyFileDropTarget(self) # ファイルドロップ受け入れを設定 self.SetDropTarget(self.droptarget) def LoadImage(self, files): """D&Dされた画像をロードして描画""" x, y = 0, 0 for filepath in files: b = wx.Bitmap(filepath) obj = MyObj(b, x, y) self.objs.append(obj) x += 32 y += 32 self.UpdateDrawing() # 描画更新 def OnSize(self, event=None): """ウインドウサイズが変更された時に呼ばれる処理""" size = self.ClientSize # クライアントのウインドウサイズを取得 # ウインドウサイズで、空の描画用バッファ(bitmap)を作成 self._buffer = wx.Bitmap(*size) self.UpdateDrawing() # 描画更新 def UpdateDrawing(self): """描画更新""" dc = wx.MemoryDC() dc.SelectObject(self._buffer) self.Draw(dc) # 実際の描画処理 del dc # Update()が呼ばれる前に MemoryDC を削除しておく必要がある # Falseを指定して背景を消さなくしたら画面のちらつきが出なくなった self.Refresh(eraseBackground=False) self.Update() def Draw(self, dc): """実際の描画処理""" dc.Clear() # デバイスコンテキストでクリア for obj in self.objs: obj.Draw(dc, True) # オブジェクトを描画 def OnPaint(self, event=None): """画面書き換え要求があった時に呼ばれる処理""" if USE_BUFFERED_DC: # ダブルバッファを使う場合 dc = wx.BufferedPaintDC(self, self._buffer) else: # ダブルバッファを使わない場合 dc = wx.PaintDC(self) dc.DrawBitmap(self._buffer, 0, 0, True) def FindObj(self, pnt): """マウス座標と重なってるオブジェクトを返す""" result = None for obj in self.objs: if obj.HitTest(pnt): result = obj return result def OnMouseLeftDown(self, event): """マウスの左ボタンが押された時の処理""" pos = event.GetPosition() # マウス座標を取得 obj = self.FindObj(pos) # マウス座標と重なってるオブジェクトを取得 if obj is not None: self.drag_obj = obj # ドラッグ移動するオブジェクトを記憶 self.drag_start_pos = pos # ドラッグ開始時のマウス座標を記録 self.drag_obj.SavePosDiff(pos) def OnMouseLeftUp(self, event): """マウスの左ボタンが離された時の処理""" if self.drag_obj is not None: pos = event.GetPosition() self.drag_obj.pos.x = pos.x + self.drag_obj.diff_pos.x self.drag_obj.pos.y = pos.y + self.drag_obj.diff_pos.y self.drag_obj = None self.UpdateDrawing() def OnMouseMotion(self, event): """マウスカーソルが動いた時の処理""" if self.drag_obj is None: # ドラッグしてるオブジェクトが無いなら処理しない return # ドラッグしてるオブジェクトの座標値をマウス座標で更新 pos = event.GetPosition() self.drag_obj.pos.x = pos.x + self.drag_obj.diff_pos.x self.drag_obj.pos.y = pos.y + self.drag_obj.diff_pos.y self.UpdateDrawing() # 描画更新 def main(): app = wx.App(False) frame = MyFrame(None, "DnD Image display use Double Buffer") frame.Show() app.MainLoop() if __name__ == '__main__': main()
py drag_image.py で実行。以下のような感じになった。
[ ツッコむ ]
以上、1 日分です。