mieki256's diary



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
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積んでるのだけどな…。

> 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 の日記 にメモ。

#2 [python][linux][ubuntu] termdownはPython 2.7では動かない

一応メモ。termdown という、カウントダウンをしてくれる Python製のツールがあるのだけど。

_mieki256's diary - termdownを試用
_GitHub - trehn/termdown: Countdown timer and stopwatch in your terminal

現行版の termdown 1.18.0 は Python 2.7.x 上で動作しない実装になった模様。

_ImportError: No module named queque - Issue #62 - trehn/termdown - GitHub

「Python 2.7なんてOSから削除しろ」と言われてしまっている…。

とりあえず、Ubuntu Linux 20.04 LTS 上で termdown をアンインストールしておいた、とメモ。

sudo pip uninstall termdown

ちなみに、Python 3.x 側でインストールしたら動いてくれた。

sudo pip3 install termdown

あるいは、Ubuntu Linux 上で使いたい場合は、snap版 termdown を使えばいいのだろうか。

_termdown - 端末にアスキーアートのタイマーを表示するコマンド | Ubuntuアプリのいいところ

もっとも、最近使う場面があるかというと…。自分の場合、スマホでタイマーアプリを起動して用を済ませる場面が多くなってる気もする…。

#3 [ubuntu][python][wxpython] Ubuntu Linux上でwxPythonをpipでインストールしてはいけない

久々に Ubuntu Linux 20.04 LTS を起動してパッケージを更新していたのだけど、Python 関係のモジュールを pip や pip3 を使って更新しようとして少しハマった。wxPython がアップデートできない…。

手元のメモを眺めてみたら、「Ubuntu Linux 上で wxPython をインストールするなら sudo apt でインストールすべし」と書いてあった。完全に忘れてた。pip で作業しちゃダメだったのだな…。また忘れそうなので再度メモ。

sudo apt install python-wxgtk3.0 python3-wxgtk4.0

six も sudo apt でインストールしろとメモしてあった…。

sudo apt install python-six python3-six

さておき。久々に起動したものだから、更新パッケージが200以上あって、なかなか落ちてこない…。今日中にダウンロードが終わらないのでは…。

以上、1 日分です。

過去ログ表示

Prev - 2022/04 - Next
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project