【Python/Pillow(PIL)】画像のヒストグラム取得、表示

Pillowで画像のヒストグラムを取得し、取得した画像データをmatplotlibで表示するには、とても簡単で以下のようにします。

import matplotlib.pyplot as plt # ヒストグラム表示用
from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("./Mandrill.bmp")

# 画像の表示
img.show()

# ヒストグラムの取得
hist = img.histogram()

# ヒストグラムをmatplotlibで表示
plt.plot(range(len(hist)), hist)
plt.show()

(実行結果)

ただし、カラー画像になると、こんな感じ↓になってしまいます。

これは、取得したヒストグラムデータがR,G,Bのデータがつながって768個の一次元のリストに格納されてしまっているためで、具体的にはリストのインデックスで

    0~255:Rのヒストグラム
  256~511:Gのヒストグラム
  512~767:Bのヒストグラム
が格納されています。

以上のことを考慮して、最初のプログラムを変更して、

import matplotlib.pyplot as plt # ヒストグラム表示用
from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("./Mandrill.bmp")

# 画像の表示
img.show()

# ヒストグラムの取得
hist = img.histogram()

# 各色の名前を取得
# カラーのとき:('R', 'G', 'B')
# モノクロのとき:("L")
bands = img.getbands()

# チャンネル数
ch = len(bands)

# グラフの表示色
if (ch == 1):
    colors = ["black"]
else:
    colors = ["red", "green", "blue", "black"]

# ヒストグラムをmatplotlibで表示
x = range(256)
for col in range(ch):
    y = hist[256 * col : 256 * (col + 1)]
    plt.plot(x, y, color = colors[col], label = bands[col])

# 凡例の表示
plt.legend(loc=2)

plt.show()

のようにすると、モノクロ、カラーの両方に対応できます。

【Python/PyLint】Visual Studioでエラー、警告の確認

Pythonでは、基本的にビルドしないので、実行するまでエラーが分からないのですが、Visual StudioのPythonプロジェクトからPyLintというエラーチェックツールが使えるようになっており、エラー、警告を確認することができます。

例えば、以下のようなダメダメコード

import numpy as np

flag = False

flag == True

a = a + 1;

if flag == True:
    print("flag:True")
else:
    print("flag:False")

このエラーを確認するには、プロジェクトの右ボタン→Python→PyLintの実行 をクリックします。

初めてPyLintを実行する場合は、以下のようにPyLintをインストールするか?を聞かれるので、足りないパッケージをインストールしてからコマンドを実行しますをクリックして、PyLintをインストールします。

すると、以下のようなメッセージが表示されるので、今すぐ昇格をクリックします。

これで、PyLintがインストールされ、PyLintが実行されます。

最初に示したコードでのエラーの解析結果は以下のようになります。

エラーが1つ、警告が3つ、メッセージが9つ。

内容を見てみると、

  • エラー Using variable ‘a’ before assignment [E:used-before-assignment]
    変数 a が定義される前(値が代入される前)に変数を参照している
  • 警告 Unused numpy imported as np [W:unused-import] 
    numpyのインポートは未使用
  • 警告 Statement seems to have no effect [W:pointless-statement] 
    このコードは意味がなさそう
  • 警告 Unnecessary semicolon [W:unnecessary-semicolon]
    セミコロンは必要ありません

となります。

このエラーと警告の分類ですが、エラーに関しては修正すべき内容がほとんど、警告に関しては実行する分には無視できるかもしれない内容ですが、

flag == True

の部分が

flag = True

を意図していた場合は危険な警告なので、警告に関してもできるだけ修正しておいた方が良さそうです。

エラーコードに関しては、こちら↓が参考になると思います。

http://pylint-messages.wikidot.com/all-codes

【Python/Pillow(PIL)】対応画像ファイルフォーマット

Pillowで画像ファイルを開くときはopen()関数、保存はsave()関数を使って

from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("./Mandrill.bmp")

# OS標準の画像ビューアで表示
img.show()

# 画像のファイル保存
img.save("image.pdf")

と書くだけで、このよう↓に画像ファイルが開き、別のファイルフォーマットで画像を保存できます。

このopen()関数、save()関数で扱う事のできるファイルフォーマットは以下の通りです。

フォーマット open save
BMP
DIB
EPS
GIF
ICNS
ICO
IM
JPEG
JPEG2000
MSP
PCX
PNG
PPM
SGI
SPIDER
TGA
TIFF
WebP
XMB
BLP
CUR
DCX
FLI,FLC
FPX
FREX
GBR
GD
IMT
IPTC/NAA
MCIDAS
MIC
MPO
PCD
PIXAR
PSD
WAL
WMF
WMF
XPM
PALM
PDF
XV Thumbnails

Identify-only formats (認識のみ??)

  BUFR, FITS, GRIB, HDF5, MPEG

(参考)

https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html

 

個人的に使うのは、BMP, PNG, TIFF, GIFぐらいですね。
また、用途は少ないですが、画像をPDFファイルに出力できるのは、ちょっと面白い。

【Python/Pillow(PIL)】transformメソッドで射影変換(ホモグラフィ変換)

transform()メソッドでアフィン変換を行う方法はここ↓で紹介しました。

【Python/Pillow(PIL)】transformメソッドでアフィン変換

このページでも説明しているように method に PIL.Image.PERSPECTIVE を指定し、data の部分にホモグラフィ変換行列を指定すれば、射影変換(ホモグラフィ変換)を行う事ができるのですが、ホモグラフィ変換行列を求めるのが少々難しいので、射影変換後の形状が長方形でいいのであれば、method に PIL.Image.QUAD を指定することで、簡単に射影変換を行う事が可能になります。

 

(サンプルプログラム)

from PIL import Image  # 画像データ用

# PIL.Imageで画像を開く
src = Image.open("./buisiness_card.jpg")

quad_data = (
    57, 174,   # 左上
    41, 418,   # 左下
    580, 252,  # 右下
    488, 13    # 右上
    )

# 4点からなる四角形を幅、高さからなる長方形へ変換
dst = src.transform(
            (320, 240),     # 出力サイズ(幅, 高さ)
            Image.QUAD,     # 変換方法
            quad_data,      # 四角を構成する4点の座標
            Image.BICUBIC   # 補間方法
            )

# OS標準の画像ビューアで表示
src.show() # 元画像
dst.show() # 変換画像

(実行結果)

PIL.Image.QUAD では4点の座標からなる四角形を (幅, 高さ)で指定した長方形へ変換します。

4点の座標は下図のように、四角形の角の点を 左上、左下、右下、右上 の順で指定します。

射影変換では、変換後の形状を長方形にする場合が多いかと思いますので、長方形でいいのなら、とても簡単!!

参考記事

【Python/Pillow(PIL)】transformメソッドでアフィン変換

【Python/NumPy】座標からホモグラフィ変換行列を求める方法

【Python/Pillow(PIL)】transformメソッドでアフィン変換

「Python アフィン変換」と検索すると、OpenCVを使った説明が多いような気がしますが、画像を表示するだけなら、Pillowにtransform()メソッドというのがあり、これもなかなか高機能な処理が可能になります。

Pillowならtkinterとの相性もいいので、GUIで画像をウィンドウに表示したいのなら、おススメです。

最終的には、以下のようにウィンドウに画像を表示するのが目標です。

transformメソッドの構文

Image.transform(size, method, data=None, resample=0, fill=1, fillcolor=None)
パラメータ 説明
size (幅, 高さ)のタプル
method 変換方法
  • PIL.Image.EXTENT:元画像の矩形領域を指定して出力先に合わせて変換
  • PIL.Image.AFFINE:アフィン変換
  • PIL.Image.PERSPECTIVE:射影変換(ホモグラフィ変換)
  • PIL.Image.QUAD:元画像の4点で示した領域を出力先に合わせて変換(ホモグラフィ変換の簡易版)
  • PIL.Image.MESH:任意グリッドの変換??
data methodにより内容が異なります。
  • PIL.Image.EXTENT
  • PIL.Image.AFFINE
    アフィン変換行列の2行3列の要素をタプルで指定します。
  • PIL.Image.PERSPECTIVE
    ホモグラフィ変換行列の3行3列の要素をタプルで指定します。
  • PIL.Image.QUAD
  • PIL.Image.MESH
resample 画素補間方法
  • PIL.Image.NEAREST:ニアレストネイバー
  • PIL.Image.BILINEAR:バイリニア
  • PIL.Image.BICUBIC:バイキュービック

(参考)https://imagingsolution.net/imaging/interpolation/

fill ???
fillcolor 出力先の画像の外側の背景色

(参考)
https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.transform

 

アフィン変換のサンプルプログラム

画像ファイルを読み込み、30°回転 → 横2倍、縦0.5倍 → 横+50、縦+70の平行移動 を行うサンプルを示します。

import numpy as np              # アフィン変換行列演算用
import tkinter as tk

from PIL import Image, ImageTk  # 画像データ用

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack()

        self.master.title("画像の表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)
        
        # Canvasの作成
        back_color = "#008B8B" # 背景色
        canvas = tk.Canvas(self.master, bg = back_color)
        # Canvasを配置
        canvas.pack(expand = True, fill = tk.BOTH)

        # アフィン変換行列(numpy ndarray)
        # 30°回転→横2倍、縦0.5倍→横+50、縦+70の平行移動の例
        # 回転行列(30°回転)
        rad = np.radians(30) # 30°をラジアン単位へ変換
        r = np.array([
            [np.cos(rad), -np.sin(rad), 0],
            [np.sin(rad),  np.cos(rad), 0],
            [0,            0,           1]
            ])
        # 拡大縮小(横2倍、縦0.5倍)
        s = np.array([
            [2,   0, 0],
            [0,   0.5, 0],
            [0,   0, 1]
            ])
        # 平行移動(横+50、縦+70)
        t = np.array([
            [1, 0,  50],
            [0, 1,  70],
            [0, 0,   1]
            ])
        # アフィン変換行列の計算(回転→拡大縮小→平行移動の順)
        affine = np.matmul(t, np.matmul(s, r))
        # 等倍のとき、単位行列
        #affine = np.identity(3)

        # アフィン変換を使った画像の表示
        self.disp_image(canvas, "./Mandrill.bmp", affine, fillcolor = back_color)

    def disp_image(self, canvas, filename, affine, method = Image.AFFINE, resample = Image.NEAREST, fillcolor = None):
        '''画像をCanvasに表示する
        canvas      :表示先のCanvasオブジェクト
        filename    :表示する画像ファイルパス
        affine      :画像データ→表示先へのアフィン変換行列
        method      :アフィン変換(Image.AFFINE)、射影変換(Image.PERSPECTIVE)
        resample    :補間方法:ニアレストネイバー(Image.NEAREST)、バイリニア(Image.BILINEAR)、バイキュービック(Image.BICUBIC)
        fillcolor   :画像の外側の色
        '''

        # キャンバスのサイズを取得(Canvasのサイズに合わせています)
        self.master.update()
        size = (canvas.winfo_width(), canvas.winfo_height())

        # PIL.Imageで開く
        pil_image = Image.open(filename)

        # アフィン変換行列の逆行列の計算
        affine_inv = np.linalg.inv(affine)
        # アフィン変換行列を一次元のタプルへ変換
        affine_tuple = tuple(affine_inv.flatten())

        # アフィン変換で画像を変換
        pil_image = pil_image.transform(
                    size,           # 出力サイズ(幅, 高さ)
                    method,         # 変換方法
                    affine_tuple,   # アフィン変換行列(出力→入力への変換行列)
                    resample,       # 補間方法
                    1,
                    fillcolor       # 画像の外側の背景色
                    )

        #PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=pil_image)

        # 画像の描画
        canvas.create_image(
                0, 0,                   # 画像表示位置
                anchor = tk.NW,         # 画像表示位置の基準(左上)
                image=self.photo_image  # 表示画像データ
                )

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

(実行結果)

アフィン変換(method = PIL.Image.AFFINE)で使う時のポイント

transform()メソッドの第3引数(data)に渡す引数にアフィン変換行列のデータを渡すのですが、transform()メソッドのアフィン変換行列は出力先から元画像へ変換するアフィン変換行列となります。

 

ただし、一般的には元画像から出力先へのアフィン変換で考える場合が多いので、元画像から出力先へのアフィン変換行列(affine)を計算して、最後にアフィン変換行列の逆行列(affine-1)をtransform()メソッドの data へ渡します。

つまり、変換前の座標を(x, y)、変換後の座標を(x’, y’)とすると、アフィン変換の行列は

$$\left(\begin{array}{c}x^{‘}\\ y^{‘}\\ 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)
\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

となりますが、この逆の変換を計算し、

$$\left(\begin{array}{c}x\\ y\\ 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)^{-1}
\left(\begin{array}{c}x^{‘}\\ y^{‘} \\ 1\end{array}\right)$$

から、逆行列の部分を

$$\left(\begin{array}{c}a^{‘} & b^{‘} & c^{‘}\\ d^{‘} & e^{‘} & f^{‘} \\ 0 & 0 & 1\end{array}\right)=
\left(\begin{array}{c}a & b & c\\ d & e & f \\ 0 & 0 & 1\end{array}\right)^{-1}$$

と置くと、transform()メソッドの第3引数(data)へは

$$data = (a^{‘}, b^{‘}, c^{‘}, d^{‘}, e^{‘}, f^{‘})$$

となるタプルを渡せばOKです。(サンプルプログラムではタプルの要素数を9個指定してますが、使わない部分を渡しても大丈夫なようです。)

アフィン変換の行列の計算部分については、行列の積、逆行列が計算できるnumpyを使うと便利だと思います。

(参考)
https://imagingsolution.net/program/python/numpy/matrix_operation/

また、アフィン変換の座標系ですが、画像の左上原点(左上の画素のさらに左上が原点)で、右方向が+X方向、下方向が+Y方向、時計回りが+θ方向となります。

ちなみに、C#では左上の画素の中心が原点(0, 0)でした。

(参考)
https://imagingsolution.net/program/csharp/image_coordinate/

参考ページ

【Python/NumPy】座標からホモグラフィ変換行列を求める方法

アフィン変換では長方形を平行四辺形には変換できるものの、台形には変換できないと説明しましたが、任意四角形から任意四角形へ変換できるのがホモグラフィ変換となります。

実際には書類や名刺のような長方形の被写体を斜めから撮影した時に、上から撮影したような感じに長方形に見えるように変換するときにホモグラフィ変換が用いられることが多いです。

 

 

 

 

 

 

 

このホモグラフィ変換は、変換前の座標を(x, y)、変換後の座標を (x’, y’) とすると、

$$s\left(\begin{array}{c}x^{‘}\\ y^{‘}\\ 1\end{array}\right)
=
\left(\begin{array}{c}a & b & c\\ d & e & f\\ g & h & 1\end{array}\right)
\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

となります。

アフィン変換の行列と比較すると、変換後の座標の頭に s が付いているのと、アフィン変換行列の3行目の要素が 0 だった部分に gh が登場しています。

このホモグラフィ変換の行列の計算は以下のようにします。

$$s\left(\begin{array}{c}x^{‘}\\ y^{‘}\\ 1\end{array}\right)
=
\left(\begin{array}{c}a & b & c\\ d & e & f\\ g & h & 1\end{array}\right)
\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

を展開し、

$$\begin{cases}sx^{‘}=ax+by+c\\sy^{‘}=dx+ey+f\\s=gx+hy+1\end{cases}$$

より

$$\begin{cases}x^{‘}=\frac{ax+by+c}{gx+hy+1}\\y^{‘}=\frac{dx+ey+f}{gx+hy+1}\end{cases}$$

となり、ホモグラフィ変換後の座標を求める事ができます。

式を見ると分かりますが、 g = h = 0 のときは s = 1 となり、アフィン変換そのものとなります。
つまり、アフィン変換で出来る変換はホモグラフィ変換でも可能です。

このホモグラフィ変換行列の未知数(a ~ h)を求めるには、座標からアフィン変換行列を求める方法でも似たような説明をしていますが、ホモグラフィ変換行列では未知数が8個なので8本の連立方程式を立てれば未知数を解く事ができます。

$$\begin{cases}x^{‘}=\frac{ax+by+c}{gx+hy+1}\\y^{‘}=\frac{dx+ey+f}{gx+hy+1}\end{cases}$$

の式を変形して、

$$\begin{cases}x^{‘}(gx+hy+1)=ax+by+c\\y^{‘}(gx+hy+1)=dx+ey+f\end{cases}$$

$$\begin{cases}ax+by+c-gxx^{‘}-hyx^{‘}=x^{‘}\\
dx+ey+f-gxy^{‘}-hyy^{‘}= y^{‘}\end{cases}$$

となり、この x, y, x’, y’ に変換前の座標 (x0, y0), (x1, y1), (x2, y2), (x3, y3)と変換後の座標
(x’0, y’0), (x’1, y’1), (x’2, y’2), (x’3, y’3)を代入すると、式が8本立つので、8個の未知数を求める事ができます。

実際に変換前、変換後の座標を代入して8本の式を書くと

$$\begin{cases}
ax_{0}+by_{0}+c-gx_{0}x_0^{‘}-hy_{0}x_0^{‘}=x_0^{‘}
\\
dx_{0}+ey_{0}+f-gx_{0}y_0^{‘}-hy_{0}y_0^{‘}= y_0^{‘}
\\
ax_{1}+by_{1}+c-gx_{1}x_1^{‘}-hy_{1}x_1^{‘}=x_1^{‘}
\\
dx_{1}+ey_{1}+f-gx_{1}y_1^{‘}-hy_{1}y_1^{‘}= y_1^{‘}
\\
ax_{2}+by_{2}+c-gx_{2}x_2^{‘}-hy_{2}x_2^{‘}=x_2^{‘}
\\
dx_{2}+ey_{2}+f-gx_{2}y_2^{‘}-hy_{2}y_2^{‘}= y_2^{‘}
\\
ax_{3}+by_{3}+c-gx_{3}x_3^{‘}-hy_{3}x_3^{‘}=x_3^{‘}
\\
dx_{3}+ey_{3}+f-gx_{3}y_3^{‘}-hy_{3}y_3^{‘}= y_3^{‘}
\end{cases}$$

となります。

この8本の式を行列を使って解くのですが、行列を使って連立方程式を解く時のポイントですが、未知数の部分を1列の行列になるようにして、行列で表現します。

今回の場合は、未知数が a~h までの8個あるので、

$$\left(\begin{array}{c}{}\\ {}\\ {}\\ {}&{}&{?}&{}&{}\\ {}\\ {}\\ {}\\ {}\end{array}\right)
\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}{}\\ {}\\ {}\\ {?}\\ {}\\ {}\\ {}\\ {}\end{array}\right)$$

という形になるように連立方程式を行列で表現します。

すると、

$$\left(\begin{array}{c}
x_{0}&y_{0}&1&0&0&0&-x_{0}x_0^{‘}&-y_{0}x_0^{‘}
\\
0&0&0&x_{0}&y_{0}&1&-x_{0}y_0^{‘}&-y_{0}y_0^{‘}
\\
x_{1}&y_{1}&1&0&0&0&-x_{1}x_1^{‘}&-y_{1}x_1^{‘}
\\
0&0&0&x_{1}&y_{1}&1&-x_{1}y_1^{‘}&-y_{1}y_1^{‘}
\\
x_{2}&y_{2}&1&0&0&0&-x_{2}x_2^{‘}&-y_{2}x_2^{‘}
\\
0&0&0&x_{2}&y_{2}&1&-x_{2}y_2^{‘}&-y_{2}y_2^{‘}
\\
x_{3}&y_{3}&1&0&0&0&-x_{3}x_3^{‘}&-y_{3}x_3^{‘}
\\
0&0&0&x_{3}&y_{3}&1&-x_{3}y_3^{‘}&-y_{3}y_3^{‘}
\end{array}\right)
\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\ x_1^{‘}\\ y_1^{‘}\\ x_2^{‘}\\ y_2^{‘}\\ x_3^{‘}\\ y_3^{‘}\end{array}\right)$$

となるので、あとは逆行列を使って

$$\left(\begin{array}{c}a\\ b\\ c\\ d\\ e\\ f\\ g\\ h\end{array}\right)
=
\left(\begin{array}{c}
x_{0}&y_{0}&1&0&0&0&-x_{0}x_0^{‘}&-y_{0}x_0^{‘}
\\
0&0&0&x_{0}&y_{0}&1&-x_{0}y_0^{‘}&-y_{0}y_0^{‘}
\\
x_{1}&y_{1}&1&0&0&0&-x_{1}x_1^{‘}&-y_{1}x_1^{‘}
\\
0&0&0&x_{1}&y_{1}&1&-x_{1}y_1^{‘}&-y_{1}y_1^{‘}
\\
x_{2}&y_{2}&1&0&0&0&-x_{2}x_2^{‘}&-y_{2}x_2^{‘}
\\
0&0&0&x_{2}&y_{2}&1&-x_{2}y_2^{‘}&-y_{2}y_2^{‘}
\\
x_{3}&y_{3}&1&0&0&0&-x_{3}x_3^{‘}&-y_{3}x_3^{‘}
\\
0&0&0&x_{3}&y_{3}&1&-x_{3}y_3^{‘}&-y_{3}y_3^{‘}
\end{array}\right)^{-1}
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\ x_1^{‘}\\ y_1^{‘}\\ x_2^{‘}\\ y_2^{‘}\\ x_3^{‘}\\ y_3^{‘}\end{array}\right)$$

を求めると未知数が求まるので、ホモグラフィ変換行列も求まります。

それでは、具体的に変換前、変換後の座標を使ってPythonのNumPyでホモグラフィ変換行列を解いてみたいと思います。

変換前 → 変換後

(100, 50) → (50, 50)
(120, 350) → (50, 400)
(500, 500) → (500, 400)
(600, 200) → (500, 50)

import numpy as np

mat = np.array([[100, 50, 1, 0, 0, 0, -100*50, -50*50],
                [0, 0, 0, 100, 50, 1, -100*50, -50*50],
                [120, 350, 1, 0, 0, 0, -120*50, -350*50],
                [0, 0, 0, 120, 350, 1, -120*400, -350*400],
                [500, 500, 1, 0, 0, 0, -500*500, -500*500],
                [0, 0, 0, 500, 500, 1, -500*400, -500*400],
                [600, 200, 1, 0, 0, 0, -600*500, -200*500],
                [0, 0, 0, 600, 200, 1, -600*50, -200*50]])

dst = np.array([50, 50, 50, 400, 500, 400, 500, 50]).T

ans = np.matmul(np.linalg.inv(mat), dst)

homography = np.array([[ans[0], ans[1], ans[2]],
                   [ans[3], ans[4], ans[5]],
                   [ans[6], ans[7], 1]])

print(homography)

# 座標変換の確認
dst = np.matmul(homography, np.array([100, 50, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([120, 350, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([500, 500, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])
dst = np.matmul(homography, np.array([600, 200, 1]).T)
print("x'=", dst[0]/dst[2])
print("y'=", dst[1]/dst[2])

(実行結果)

[[ 1.11407581e+00 -1.11269833e-01 -5.49716851e+01]
 [-2.55930820e-01  9.08099927e-01  3.10604901e+01]
 [ 5.63236570e-04 -7.77511351e-04  1.00000000e+00]]
x'= 50.000000000000064
y'= 50.000000000000014
x'= 50.000000000000135
y'= 400.00000000000006
x'= 500.0000000000006
y'= 400.0000000000002
x'= 500.00000000000057
y'= 50.000000000000064

出来た!!

参考

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

【Python/NumPy】行列の演算(積、逆行列、転置行列、擬似逆行列など)

【Python/NumPy】座標からアフィン変換行列を求める方法

【Python/NumPy】座標からアフィン変換行列を求める方法

アフィン変換行列は、これまで移動量、スケール、回転角度からアフィン変換行列を求める方法を紹介してきました。

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

ただ、実際にはアフィン変換前の点とアフィン変換後の点の組み合わせからアフィン変換行列を求めたい場合もあるので、今回はその方法を紹介します。

アフィン変換行列は、こんな感じ↓です。

$$\left(\begin{array}{c}x^{’}\\ y^{‘}\\1\end{array}\right)=\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

アフィン変換行列を求めるという事は、この未知数の a, b, c, d, e, f を求める事になります。

この行列を求めるには、アフィン変換前の3点、アフィン変換後の3点の3ペアの座標があれば求める事ができます。

求め方は比較的簡単で、式で書くと以下のようになります。

$$\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 1\end{array}\right)
=
\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)$$

より、逆行列を用いて、

$$\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}
=
\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

$$\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
=
\left(\begin{array}{c}x_0^{‘} & x_1^{‘} & x_2^{‘}\\ y_0^{‘} & y_1^{‘} & y_2^{‘}\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}x_{0} & x_{1} & x_{2}\\ y_{0} & y_{1} & y_{2} \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

として求める事ができます。

この計算は行列の積と逆行列を求める事ができれば、解くことができますが、最近勉強しているPythonのNumPyを使って例題を解いてみたいと思います。

変換前 → 変換後
(0, 0) → (200, 100)
(600, 0) → (719.6152, 400)
(0, 400) → (0, 446.4102)

このアフィン変換行列は

$$\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)
=
\left(\begin{array}{c}200 & 719.6152 & 0\\ 100 & 400 & 446.4102\\1 & 1 & 1\end{array}\right)
\left(\begin{array}{c}0 & 600 & 0\\ 0 & 0 & 400 \\ 1 & 1 & 1 \end{array}\right)^{-1}$$

で求まります。

この計算をNumPyを使って計算すると、

import numpy as np

src = np.array([[0, 600, 0],
               [0, 0, 400],
               [1, 1, 1]])

dst = np.array([[200, 719.6152, 0],
               [100, 400, 446.4102],
               [1, 1, 1]])

affine = np.matmul(dst, np.linalg.inv(src))

print(affine)

(実行結果)

[[  0.86602533  -0.5        200.        ]
 [  0.5          0.8660255  100.        ]
 [  0.           0.           1.        ]]

として、アフィン変換行列を求める事ができます。

ただし、この計算方法だと、3行1列目、3行2列目の要素が計算誤差できっちりと 0 にならない場合もあり、気持ち悪いので、もう一つの計算方法を紹介します。

アフィン変換の行列の式は

$$\left(\begin{array}{c}x^{’}\\ y^{‘}\\1\end{array}\right)=\left(\begin{array}{c}a & b & c\\ d & e & f\\0 & 0 & 1\end{array}\right)\left(\begin{array}{c}x\\ y \\ 1\end{array}\right)$$

でしたが、この式を展開すると

$$x^{‘}=ax + by + c$$

$$y^{‘}=dx + ey + f$$

この式にアフィン変換前の3点、アフィン変換後の3点の3ペアの座標を代入すると、

$$x_0^{‘}=ax_{0} + by_{0} + c$$

$$y_0^{‘}=dx_{0} + ey_{0} + f$$

$$x_1^{‘}=ax_{1} + by_{1} + c$$

$$y_1^{‘}=dx_{1} + ey_{1} + f$$

$$x_2^{‘}=ax_{2} + by_{2} + c$$

$$y_2^{‘}=dx_{2} + ey_{2} + f$$

となります。

式が6本で未知数が a ~f の6個なので、この連立方程式を求めれば、アフィン変換行列も求まります。

この連立方程式も行列を用いて解きたいと思います。

未知数の部分を6行1列の行列になるように、行列で表すと、

$$\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\x_1^{‘}\\ y_1^{‘}\\x_2^{‘}\\ y_2^{‘} \end{array}\right)
=
\left(\begin{array}{c}x_{0} & y_{0} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{0} & y_{0} & 1\\x_{1} & y_{1} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{1} & y_{1} & 1\\x_{2} & y_{2} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{2} & y_{2} & 1 \end{array}\right)
\left(\begin{array}{c}a\\ b\\c\\d\\e\\f\end{array}\right)$$

となり、6行6列の行列の部分に左側から逆行列を掛ければ、未知数が求まるので、

$$\left(\begin{array}{c}a\\ b\\c\\d\\e\\f\end{array}\right)=\left(\begin{array}{c}x_{0} & y_{0} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{0} & y_{0} & 1\\x_{1} & y_{1} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{1} & y_{1} & 1\\x_{2} & y_{2} & 1 & 0 & 0 & 0\\ 0 & 0 & 0 & x_{2} & y_{2} & 1 \end{array}\right)^{-1}
\left(\begin{array}{c}x_0^{‘}\\ y_0^{‘}\\x_1^{‘}\\ y_1^{‘}\\x_2^{‘}\\ y_2^{‘} \end{array}\right)$$

を計算すれば、アフィン変換行列が求まります。

この計算もNumPyを使って計算してみたいと思います。

import numpy as np

mat = np.array([[0, 0, 1, 0, 0, 0],
                [0, 0, 0, 0, 0, 1],
                [600, 0, 1, 0, 0, 0],
                [0, 0, 0, 600, 0, 1],
                [0, 400, 1, 0, 0, 0],
                [0, 0, 0, 0, 400, 1]])

dst = np.array([200, 100, 719.6152, 400, 0, 446.4102]).T

ans = np.matmul(np.linalg.inv(mat), dst)

affine = np.array([[ans[0], ans[1], ans[2]],
                   [ans[3], ans[4], ans[5]],
                   [0, 0, 1]])

print(affine)

(実行結果)

[[  0.86602533  -0.5        200.        ]
 [  0.5          0.8660255  100.        ]
 [  0.           0.           1.        ]]

となり、アフィン変換行列が求まります。

参考

アフィン変換(平行移動、拡大縮小、回転、スキュー行列)

【Python/NumPy】行列の演算(積、逆行列、転置行列、擬似逆行列など)

【Python/NumPy】座標からホモグラフィ変換行列を求める方法

【Python/tkinter】Canvasに画像を表示する

まず、Canvasを作成し、画像ファイルを開き、Canvasに画像を表示するサンプルは以下のようになります。

import tkinter as tk
from PIL import ImageTk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("画像の表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)

        # Canvasの作成
        self.canvas = tk.Canvas(self.master)
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)

        # 画像ファイルを開く(対応しているファイルフォーマットはPGM、PPM、GIF、PNG)
        self.photo_image = ImageTk.PhotoImage(file = "Mandrill.png")

        # キャンバスのサイズを取得
        self.update() # Canvasのサイズを取得するため更新しておく
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        # 画像の描画
        self.canvas.create_image(
                canvas_width / 2,       # 画像表示位置(Canvasの中心)
                canvas_height / 2,                   
                image=self.photo_image  # 表示画像データ
                )

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

(実行結果)

プログラムの説明

画像ファイルは PhotoImage 関数の file引数に ファイル名を指定します。ただし、PhotoImage 関数が対応している画像ファイルフォーマットは PGM、PPM、GIF、PNG の4つのみとなります。

PhotoImage関数で開いた画像データは戻り値で取得できる(上記のプログラムではself.photo_image)ので、このデータを create_image関数の image に渡すことで、Canvas上に画像を表示することができます。

PhotoImage関数が対応している画像ファイルフォーマットが4つのみというのは、画像処理のプログラムを作成するのには致命的(最低限 bmpは必要)なので、画像ファイルをPillowで開いてPhotoImageへ変換し、create_imageで画像を表示する例を次に示します。

Pillowで画像ファイルを開く例

import tkinter as tk
from PIL import Image, ImageTk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("画像の表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)

        # Canvasの作成
        self.canvas = tk.Canvas(self.master)
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)

        # PillowのPIL.Imageで画像ファイルを開く
        pil_image = Image.open("Mandrill.bmp")

        # PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=pil_image)

        # キャンバスのサイズを取得
        self.update() # Canvasのサイズを取得するため更新しておく
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        # 画像の描画
        self.canvas.create_image(
                canvas_width / 2,       # 画像表示位置(Canvasの中心)
                canvas_height / 2,                   
                image=self.photo_image  # 表示画像データ
                )

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

上記の例はPillowの Image.open関数で画像ファイルを開いています。このopen関数で開くことのできる画像ファイルフォーマットは、bmpやjepg, tiff, png など、ほとんどの画像ファイルを開くことができます。詳しくはこちらのページを参照ください。

あとは、Image.open関数で開いた画像データをPhotoImageで変換し、create_imageで画像を表示します。

応用編

これまで、ウィンドウの作成ウィジェットの配置MenuCanvasファイルを開くダイアログボックスと説明してきましたが、これらを使ってCanvasに画像を表示するプログラムを作成しました。

 

(作成したプログラムのイメージ)

(プログラム)

import tkinter as tk
from tkinter import filedialog

from PIL import Image, ImageTk, ImageOps  # 画像データ用

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack()

        self.master.title("画像の表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)
        
        # メニューの作成
        self.create_menu()

        # Canvasの作成
        self.back_color = "#008B8B" # 背景色
        self.canvas = tk.Canvas(self.master, bg = self.back_color)
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)

    def create_menu(self):
        # メニューバーの作成
        menubar = tk.Menu(self)

        # ファイル
        menu_file = tk.Menu(menubar, tearoff = False)
        menu_file.add_command(label = "画像ファイルを開く", command = self.menu_file_open_click, accelerator="Ctrl+O")
        menu_file.add_separator() # 仕切り線
        menu_file.add_command(label = "終了", command = self.master.destroy)
        # ショートカットキーの関連付け
        menu_file.bind_all("<Control-o>", self.menu_file_open_click)

        # メニューバーに各メニューを追加
        menubar.add_cascade(label="ファイル", menu = menu_file)

        # 親ウィンドウのメニューに、作成したメニューバーを設定
        self.master.config(menu = menubar)

    def menu_file_open_click(self, event=None):
        filename = filedialog.askopenfilename(
            title = "ファイルを開く",
            filetypes = [("Image file", ".bmp .png .jpg .tif"), ("Bitmap", ".bmp"), ("PNG", ".png"), ("JPEG", ".jpg"), ("Tiff", ".tif") ], # ファイルフィルタ
            initialdir = "./" # 自分自身のディレクトリ
            )
        # 画像の表示
        self.disp_image(filename)

    def disp_image(self, filename):
        '''画像をCanvasに表示する'''
        if not filename:
            return
        # PIL.Imageで開く
        pil_image = Image.open(filename)

        # キャンバスのサイズを取得
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        # 画像のアスペクト比(縦横比)を崩さずに指定したサイズ(キャンバスのサイズ)全体に画像をリサイズする
        pil_image = ImageOps.pad(pil_image, (canvas_width, canvas_height), color = self.back_color)

        #PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=pil_image)

        # 画像の描画
        self.canvas.create_image(
                canvas_width / 2,       # 画像表示位置(Canvasの中心)
                canvas_height / 2,                   
                image=self.photo_image  # 表示画像データ
                )

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

ポイント

  • Canvasへ画像を表示する際はcreate_imageメソッドを用いますが、引数のimageへ渡す事ができる画像データはPhotoImageか、BitmapImageのどちらかとなります。
    BitmapImageはXBMファイルと呼ばれる二値化された画像なので、実質的に使うのはPhotoImageのみとなります。
  • PhotoImageで開く事のできるファイルフォーマットはPGM、PPM、GIF、PNGの4つのみ。
    これでも画像処理をするには物足りない(BMPは必須)ので、Pillowで画像ファイルを開き、PhotoImageへ変換します。
    Pillowなら、たくさんのファイルフォーマットに対応しています。
    (参考)

    https://docs.python.org/ja/3/library/tkinter.html?highlight=photoimage

  • create_imageメソッドで画像を表示する位置は初期状態では画像の中心座標を指定します。
    画像の左上の座標を指定する場合には、anchor = tk.NW を指定します。
  • Canvasの大きさを取得するにはwinfo_width()、winfo_height()を用います。
  • キャンバスの大きさに合わせて縦横比を崩さずに全体に表示するにはImageOps.pad()メソッドを用いていますが、ImageOpsモジュールはexperimental(試験的)という事ですが、便利なので使っちゃってます。。
    (参考)
    https://pillow.readthedocs.io/en/stable/reference/ImageOps.html

 

さらに画像の拡大・縮小・平行移動の機能を追加したプログラムについて、以下のページにまとめましたので、参照頂けると幸いです。

【Python】画像ビューア(ズーム(拡大/縮小)、移動表示)

関連ページ

【Python/tkinter】ウィンドウの作成

【Python/tkinter】ウィジェットの配置(pack)

【Python/tkinter】Menu(メニュー)

【Python/tkinter】Canvas(キャンバス)の作成

【Python/tkinter】ファイルを開くダイアログボックスの表示

Visual StudioのPython環境の切り替え変更

Pythonはインストールした環境ごとに使用できるバージョンやモジュール(パッケージ)を切り替えることができます。

Visual Studioでは、AnacondaやPythonのインストーラなどでインストールした環境の一覧が表示されています。このPython環境を切り替える方法を紹介します。

新規プロジェクトで使用するPython環境の変更方法

新規プロジェクトで使用するPython環境を変更しるには、変更したいPython環境を選択し、概要と表示されている状態で、その下のこれを新しいプロジェクトに対する規定の環境にするをクリックします。

すると、選択したPython環境の文字が太字に切り替わり、次にプロジェクトを作成する時のPython環境が切り替わります。

既存プロジェクトのPython環境の変更方法

Python環境に グローバルデフォルト の環境を使用している場合、前項のようにこれを新しいプロジェクトに対する規定の環境にするをクリックすることで、Python環境を切り替える事ができます。

(変更後)Python3.6からPython3.7へ変更後

Python環境がグローバルデフォルトでない場合、プロジェクトを右クリックし、プロパティを選択します。

表示されたウィンドウの全般インタープリターの中から、変更したいPython環境を選択する事で、Python環境を切り替える事ができます。

ただし、インタープリターの部分で選択を変更した直後では、Python環境の表示が切り替わらないため、どこか関係のないウィンドウを選択し、ウィンドウ表示を切り替える事で、Python環境の表示が切り替わるようになります。

【Python/tkinter】PanedWindow(C#のSplitContainer相当)

C#では、SplitContainerを使ってPictureBoxのサイズなどを変更していたのですが、tkinterでは、このSplitContainerに相当するものがPanedWindowとなります。

PanedWindowのサンプルプログラムを以下示します。

(実行画面)

仕切り線の移動

 

(プログラム)

import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.geometry("300x200") # ウィンドウサイズ(幅x高さ)

        # PanedWindowの作成
        paned_window = tk.PanedWindow(self.master)
        # フレームの作成
        frame1 = tk.Frame(paned_window, width = 100, bg = "red")
        frame2 = tk.Frame(paned_window, width = 100, bg = "green")
        # フレームをPanedWindowに追加
        paned_window.add(frame1)
        paned_window.add(frame2)

        paned_window.pack(expand = True, fill = tk.BOTH)


if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

PanedWindowはC#のSplitContainerと似てはいるのですが、異なる部分もあります。

  • C#のSplitContainerでは、最初からPanel1,Panel2が配置されていますが、PanedWindowには何も配置されていない。(自分でウィジェットをPanedWindowに追加する必要があります。)
  • PanedWindowにはFrame以外にもLabelやButtonなどのウィジェットが配置できる。
  • ウィジェットを追加すると、ウィジェットとウィジェットの間に仕切り線(sash)が追加される。
  • PanedWindowには3つ以上のウィジェットを追加できる。
  • ウィンドウをリサイズした時に、一番右(もしくは一番下)のウィジェットのみがリサイズされる。

 

オプション

オプション名 説明
background 通常時(クリックされていないとき)の背景色を指定します。(bgと同じ)
bd 外枠の線の太さを指定します。
ただし、初期状態では枠線が表示されていないため、reliefで枠線のスタイルを指定する必要があります。(borderwidthと同じ)
bg backgroundと同じ
borderwidth bdと同じ
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
(参考)https://tkdocs.com/shipman/cursors.html
handlepad ハンドル(四角いマーク)の位置を指定します。
handlesize ハンドル(四角いマーク)の大きさを指定しますs。
height ウィジェットの高さを指定します。
opaqueresize 仕切り線(sash)を移動中に表示を更新する場合はTrue、更新しない場合はFalseを指定します。
【初期値】True
orient ウィジェットを横に並べる場合は tk.HORIZONTAL、縦に並べる場合は tk.VERTICALを指定します。
【初期値】tk.HORIZONTAL
sashpad 仕切り線(sash)の両側に追加する隙間の大きさを指定します。
sashrelief 仕切り線(sash)のスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
【初期値】tk.FLAT(枠線なし)
sashwidth 仕切り線(sash)の幅を指定します。
【初期値】3
showhandle ハンドルと呼ばれる四角い印を仕切り線上に表示する場合はTrue、表示しない場合はFalseを指定します。
【初期値】False
width ウィジェットの幅を指定します。

メソッド

メソッド 説明
add(child[, option=value] …) ウィジェットをPanedWindowに追加します。
forget(child) 追加したウィジェットを削除します。
identify(x, y)
panecget(child, option)
paneconfig(child, option=value, …)
panes() PanedWindowに追加されているウィジェットのリストを取得します。
remove(child)
追加したウィジェットを削除します。
sash_coord(index)
仕切り線(sash)の位置を取得します。
sash_place(index, x, y) 仕切り線(sash)の位置を指定します。
indexは仕切り線の順番0, 1, 2・・・

 

【Python/tkinter】Canvas(キャンバス)の作成

Canvasは、線や円などの図形や画像を表示するためのベースとなるものとなります。

C#でいうところのPictureBoxです。

Canvasには、図形や画像を表示する以外にも描画領域をスクロールできる機能もありますが、今回はCanvasの作成についてです。

ただ、Canvasを配置しただけの簡単なサンプルは以下のようになります。

 

(実行画面)

(プログラム)

import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("タイトル")    # ウィンドウタイトル
        self.master.geometry("300x200") # ウィンドウサイズ(幅x高さ)

        # Canvasの作成
        canvas = tk.Canvas(
            self.master, 
            width = 200,
            height = 100,
            bg = "cyan"
            )
        # Canvasを配置
        canvas.pack()

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

オプション

オプション名 説明
background 背景色を指定します。(bgと同じ)
bd 枠線の太さを指定します。(borderwidthと同じ)
bg 背景色を指定します。(backgroundと同じ)
borderwidth 枠線の太さを指定します。(bdと同じ)
closeenough
confine
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
(参考)https://tkdocs.com/shipman/cursors.html
height キャンバスの高さを指定します。
highlightbackground
highlightcolor
highlightthickness
insertbackground
insertborderwidth
insertofftime
insertontime
insertwidth
offset
relief キャンバスの枠線のスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
scrollregion スクロールする領域のサイズを指定します。
selectbackground
selectborderwidth
selectforeground
state ウィジェットの有効(NORMAL)/無効(DISABLED,操作できない状態)を指定します。
【設定値】tk.NORMAL, tk.DISABLED
【初期値】tk.NORMAL
takefocus
width キャンバスの幅を指定します。
xscrollcommand 横方向にスクロールするスクロールバーの スクロールバー.set を指定します。
xscrollincrement 横方向のスクロールバーの矢印をクリックしたときの移動量を指定します。
yscrollcommand 縦方向にスクロールするスクロールバーの スクロールバー.set を指定します。
yscrollincrement 縦方向のスクロールバーの矢印をクリックしたときの移動量を指定します。

 

関連記事

Canvasに画像を表示する方法については、以下のページを参照ください。

【Python/tkinter】Canvasに画像を表示する

【Python/NumPy】行列の演算(積、逆行列、転置行列、擬似逆行列など)

個人的には、行列は最小二乗法で近似式を求めるときや、アフィン変換を用いて画像の表示やリサイズを行う際に用いるのですが、この行列の演算は、PythonではNumPyを用いて行います。

NumPyのインポート

import numpy as np

行列の生成(array)

# 行列の生成
mat = np.array([[1, 2], [3, 4], [5, 6]])
print(mat)

(実行結果)

[[1 2]
 [3 4]
 [5 6]]

行列の積(matmul)

matA = np.array([[1, 2], [3, 4]])
matB = np.array([[5, 6], [7, 8]])
matAB = np.matmul(matA, matB)
print(matAB)

(実行結果)

[[19 22]
 [43 50]]

単位行列(identity)

matI = np.identity(3)
print(matI)

(実行結果)

[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

零行列(zeros)

mat = np.zeros((3, 4))
print(mat)

(実行結果)

[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

逆行列(linalg.inv)

mat = np.array([[1, 2], [3, 4]])
matInv = np.linalg.inv(mat)
print(matInv)

(実行結果)

[[-2.   1. ]
 [ 1.5 -0.5]]

擬似逆行列(linalg.pinv)

mat = np.array([[1, 2], [3, 4], [5, 6]])
matInv = np.linalg.pinv(mat)
print(matInv)

(実行結果)

[[-1.33333333 -0.33333333  0.66666667]
 [ 1.08333333  0.33333333 -0.41666667]]

転置行列(.T)

mat = np.array([[1, 2], [3, 4]])
matT = mat.T
print(matT)

(実行結果)

[[1 3]
 [2 4]]

行列要素ごとの積(multiply)

matA = np.array([[1, 2], [3, 4]])
matB = np.array([[5, 6], [7, 8]])
matAB = np.multiply(matA, matB)
print(matAB)

(実行結果)

[[ 5 12]
 [21 32]]

【Python/tkinter】名前を付けて保存ダイアログボックスの表示

名前を付けて保存ダイアログボックスを表示するには、tkinter.filedialogモジュールのasksaveasfilename()関数を使います。

以下にシンプルなサンプルプログラムを示します。

from tkinter import filedialog

filename = filedialog.asksaveasfilename()
print(filename)

(実行結果)

 

オプション

オプション名 説明
parent ダイアログを表示する親のウィンドウを指定します。
title ダイアログのタイトルを指定します。
initialdir 最初に表示されるディレクトリを指定します。
initialfile 最初に選択されているファイル名を指定します。
filetypes 選択可能なファイルの種類を設定します。
defaultextension 拡張子が入力されない場合に追加する拡張子を指定します。

(参考)

https://docs.python.org/ja/3/library/dialog.html?highlight=askopenfilename#module-tkinter.filedialog

 

名前を付けて保存ダイアログボックスはファイルを開くダイアログボックスとも似ていますが、defaultextensionオプションが異なります。

これは名前を付けて保存ダイアログの「ファイル名」の部分に拡張子を指定しなかった場合に、defaultextensionオプションで指定した拡張子が付加されます。

 

それでは、defaultextensionオプションを指定せずにfiletypesオプションを指定すると、どうなるか?というと、「ファイル名」の部分に、拡張子を付けずにファイル名を入力しても、「ファイルの種類」で指定された拡張子で、拡張子が自動で付加される事はありません。

filename = filedialog.asksaveasfilename(
    filetypes = [("Bitmap", ".bmp"), ("PNG", ".png"), ("JPEG", ".jpg"), ("Tiff", ".tif") ], # ファイルフィルタ
    )

そのため、filetypesオプションとdefaultextensionオプションは両方指定して、以下のようにします。

from tkinter import filedialog

filename = filedialog.asksaveasfilename(
    title = "名前を付けて保存",
    filetypes = [("Bitmap", ".bmp"), ("PNG", ".png"), ("JPEG", ".jpg"), ("Tiff", ".tif") ], # ファイルフィルタ
    initialdir = "./", # 自分自身のディレクトリ
    defaultextension = "bmp"
    )
print(filename)

このようにすると、「ファイルの種類」で指定された拡張子で、自動で拡張子が付加されます。

上記のプログラムではdefaultextension が “bmp”ですが、「ファイルの種類」がPNGのときは、ちゃんと.pngを付加してくれます。

もっというと、defaultextension = “” としても、「ファイルの種類」で指定された拡張子が付加されます。

(実行結果)

 

関連記事

【Python/tkinter】ファイルを開くダイアログボックスの表示

【Python/tkinter】Frame(フレーム)

Frameはいくつかのウィジェットを配置するための枠、まさにフレームです。
C#でいうところのPanelと同じです。

Frameを使うとウィジェットの配置の自由度が増します。
ウィジェットを配置するには、pack,grid,placeの3つのメソッドがありますが、一つのコンテナ(Frameなど)にはpack,grid,placeの3つの混在使用ができないのですが、複数のFrameを用いて、まずは大枠を作り、そこにウィジェットを配置すると、比較的簡単にイメージ通りの画面が作成できると思います。

Frameを使って、ツールバー、ステータスバー、右カラムを作ってみたいと思います。

(実行画面)

(プログラム)

import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.geometry("300x200") 

        #---------------------------------------
        #  ツールバー
        #---------------------------------------
        # ツールバー用Frame
        frame_toolbar = tk.Frame(self.master)
        # ツールボタン
        button1 = tk.Button(frame_toolbar, text = "1", width = 2)
        button2 = tk.Button(frame_toolbar, text = "2", width = 2)
        button3 = tk.Button(frame_toolbar, text = "3", width = 2)
        # ボタンをフレームに配置
        button1.pack(side = tk.LEFT)
        button2.pack(side = tk.LEFT)
        button3.pack(side = tk.LEFT)
        # ツールバーをウィンドの上に配置
        frame_toolbar.pack(fill = tk.X)

        #---------------------------------------
        #  ステータスバー
        #---------------------------------------
        # ツールバー用Frame
        frame_statusbar = tk.Frame(self.master, relief = tk.SUNKEN, bd = 2)
        # ステータスラベル
        label = tk.Label(frame_statusbar, text = "StatusLabel")
        # ラベルをフレームに配置
        label.pack(side = tk.LEFT)
        # ステータスバーをウィンドの下に配置
        frame_statusbar.pack(side = tk.BOTTOM, fill = tk.X)

        #---------------------------------------
        #  右カラム
        #---------------------------------------
        # 右カラム用Frame
        frame_column = tk.Frame(self.master, relief = tk.SUNKEN, bd = 2, width = 100)
        frame_column.propagate(False) # フーレムサイズの自動調整を無効にする
        # チェックボタン
        check1 = tk.Checkbutton(frame_column, text = "Check1")
        check2 = tk.Checkbutton(frame_column, text = "Check2")
        check3 = tk.Checkbutton(frame_column, text = "Check3")
        # チェックボタンをフレームに配置
        check1.pack()
        check2.pack()
        check3.pack()
        # 右カラムをウィンドの右に配置
        frame_column.pack(side = tk.RIGHT, fill = tk.Y)

        #---------------------------------------
        #  残りの領域
        #---------------------------------------
        frame = tk.Frame(self.master, relief = tk.SUNKEN, bd = 2, bg = 'dark cyan')
        frame.pack(expand = True, fill = tk.BOTH)

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

ポイント

  • 枠線の種類(relief)を指定するときは、枠線の太さ(bd)も指定する事。(枠線の太さは初期値が0のため)
  • Frameの幅(width)もしくは高さ(height)を数値で指定したいときは、
    フレーム.propagate(False)メソッドを呼ぶこと。
    propagate()を呼ばない場合、フレーム内のウィジェットに合わせてサイズが自動調整されます。
  • Frameを残りの領域全体に広げたい場合はpack時にexpand = True, fill = tk.BOTHの両方を指定すること

オプション

オプション名 説明
bd 枠線の太さを指定します。(borderwidthと同じ)
borderwidth bdと同じ
bg 背景色を指定します。(backgroundと同じ)
background bgと同じ
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
(参考)https://tkdocs.com/shipman/cursors.html
height フレームの高さを画素数で指定します。
highlightbackground
highlightcolor
highlightthickness
padx フレームの内側に配置するウィジェットまでの横方向の隙間を指定します。
【初期値】0
pady フレームの内側に配置するウィジェットまでの縦方向の隙間を指定します。
【初期値】0
relief ボタンのスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
takefocus
width フレームの幅を画素数で指定します。
wraplength 文字の折り返し幅を指定します。

 

【Python/tkinter】Menu(メニュー)

ウィンドウにメニューを追加するには、Menuクラスを用います。

メニューで良く使いそうな機能のサンプルプログラムを作成しました。

(実行画面)

ファイル関連のメニュー

表示メニュー(チェックボタンで作成しています)

選択メニュー(ラジオボタンで作成しています)

(サンプルプログラム)

import tkinter as tk
from tkinter import filedialog

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)
        self.pack()

        self.master.title("メニューの作成")       # ウィンドウタイトル
        self.master.geometry("300x150")     # ウィンドウサイズ(幅x高さ)
        
        # ------------------------------------------------
        # メニューの作成

        # メニューバーの作成
        menubar = tk.Menu(self)

        # ファイル
        menu_file = tk.Menu(menubar, tearoff = False)
        menu_file.add_command(label = "ファイルを開く",  command = self.menu_file_open_click,  accelerator="Ctrl+O")
        menu_file.add_command(label = "名前を付けて保存", command = self.menu_file_saveas_click, accelerator="Ctrl+S")
        menu_file.add_separator() # 仕切り線
        menu_file.add_command(label = "終了",            command = self.master.destroy)
        # ショートカットキーの関連付け
        menu_file.bind_all("<Control-o>", self.menu_file_open_click)
        menu_file.bind_all("<Control-s>", self.menu_file_saveas_click)

        # 表示(Checkbutton)
        menu_disp = tk.Menu(menubar, tearoff = False)
        self.disp1_value = tk.BooleanVar()
        self.disp2_value = tk.BooleanVar()
        self.disp3_value = tk.BooleanVar()
        menu_disp.add_checkbutton(label = "表示1", command = self.menu_disp1_click, variable = self.disp1_value)
        menu_disp.add_checkbutton(label = "表示2", command = self.menu_disp2_click, variable = self.disp2_value)
        menu_disp.add_checkbutton(label = "表示3", command = self.menu_disp3_click, variable = self.disp3_value)

        # 選択(Radiobutton)
        self.radio_val = tk.IntVar() # ラジオボタンの値
        menu_select = tk.Menu(menubar, tearoff = False)
        menu_select.add_radiobutton(label = "選択1", command = self.menu_select_click, variable = self.radio_val, value = 1)
        menu_select.add_radiobutton(label = "選択2", command = self.menu_select_click, variable = self.radio_val, value = 2)
        menu_select.add_radiobutton(label = "選択3", command = self.menu_select_click, variable = self.radio_val, value = 3)

        # メニューバーに各メニューを追加
        menubar.add_cascade(label="ファイル", menu = menu_file)
        menubar.add_cascade(label="表示",     menu = menu_disp)
        menubar.add_cascade(label="選択",     menu = menu_select)

        # 親ウィンドウのメニューに、作成したメニューバーを設定
        self.master.config(menu = menubar)

    def menu_file_open_click(self, event=None):
        print("「ファイルを開く」が選択された")
        filename = filedialog.askopenfilename(
            title = "ファイルを開く",
            initialdir = "./" # 自分自身のディレクトリ
            )
        print(filename)

    def menu_file_saveas_click(self, event=None):
        print("「名前を付けて保存」が選択された")

    def menu_disp1_click(self):
        print("「表示1」が選択された")
        print(f"チェック状態は{self.disp1_value.get()}")

    def menu_disp2_click(self):
        print("「表示2」が選択された")
        print(f"チェック状態は{self.disp2_value.get()}")

    def menu_disp3_click(self):
        print("「表示3」が選択された")
        print(f"チェック状態は{self.disp3_value.get()}")

    def menu_select_click(self):
        print(self.radio_val.get(), "番目のラジオボタンが選択されました。")

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

ポイント

  • プルダウンで表示されるメニューが1つのMenuクラスオブジェクトとなり、各メニューの項目はadd_command()メソッドで追加します。
  • Menuクラスオブジェクトを親のMenuに追加するときは、add_cascade()メソッドを用います。

Menuクラスオプション

オプション名 説明
activebackground マウスポインタが各メニューの上にあるときの背景色を指定します。
activeborderwidth
選択されたメニューの枠の太さを指定します???
実質的にメニューの行間が広がります。
activeforeground マウスポインタが各メニューの上にあるときの文字色を指定します。
background 通常時の背景色を指定します。(bgと同じ)
bd 枠線の太さを指定します。(指定できない?)
bg backgroundと同じ
bitmap モノクロのBitmapを指定します。
(参考)https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/bitmaps.html
borderwidth bdと同じ
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。(指定できない?
disabledforeground add_command()メソッドで追加したメニューのstateオプションで無効(DISABLED)に設定している際の文字色を設定します。
fg add_command()メソッドで追加したメニューの文字色を設定します。
foreground fgと同じ
relief ボタンのスタイルを指定します。(指定できない?
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
【初期値】tk.FLAT(枠線なし)
selectcolor チェックボックス(チェックのある四角)の背景色を指定します。(指定できない?
takefocus
tearoff 必ずFalse(0)を指定します。
指定しない場合、破線が表示されます。
tearoffcommand
title
type

メソッド

メソッド 説明
add_command() メニュー項目を追加します。
add_radiobutton() ラジオボタンを追加します。(ただし、見た目はチェックボタンと同じ)
add_checkbutton() チェックボタンを追加します。
add_cascade() 親のMenuにMenuクラスオブジェクトを追加します。
add_separator() 仕切り線を追加します。
add( type, options )
delete( startindex [, endindex ]) メニューの項目をstartindex ~(endindex )まで削除します。
entryconfig( index, options ) メニューのindex番号を指定して、オプション設定値を設定、変更します。
index(item) メニューの文字列を指定してindex番号を取得します。
insert_separator ( index ) 指定した番号の位置に仕切り線を挿入します。
invoke ( index ) チェックボックスの場合、チェックの選択/非選択状態が切り替わります。
ラジオボタンの場合、選択されている項目が切り替わります。
type ( index ) 指定した番号のタイプを取得します。
cascade,  checkbutton,  command,  radiobutton,  separator, tearoff のいづれか

(補足)

メニューのindex番号ですが、私の環境では、上のメニューの番号は1から始まり、

ドロップダウンで表示されるメニューは0始まりとなっていました。

念のためindex()メソッドで、メニューの文字列指定でindex番号を取得した方が安全かも?しれません。

【Python/tkinter】Radiobutton(ラジオボタン)

ラジオボタンはRadiobuttonクラスによって作成します。

チェックボタンでは他のチェックボタンとは関係なくチェック/チェック無しの状態が選べましたが、ラジオボタンは複数のラジオボタンの中から1つだけ選択する場合に用いられます。

 

以下に簡単なサンプルを示します。

import tkinter as tk

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("ラジオボタンの作成")     # ウィンドウタイトル
        self.master.geometry("300x150")             # ウィンドウサイズ(幅x高さ)

        # ラジオボタンの値
        self.radio_value = tk.IntVar(value = 1)     # 初期値を設定する場合
        #self.radio_value = tk.IntVar()             # 初期値を設定しないと0になる
        
        # ラジオボタンの作成
        radio0 = tk.Radiobutton(self.master, 
                           text = "ラジオボタン0",      # ラジオボタンの表示名
                           command = self.radio_click,  # クリックされたときに呼ばれるメソッド
                           variable = self.radio_value, # 選択の状態を設定する
                           value = 0                    # ラジオボタンに割り付ける値の設定
                           )

        radio1 = tk.Radiobutton(self.master, 
                           text = "ラジオボタン1",      # ラジオボタンの表示名
                           command = self.radio_click,  # クリックされたときに呼ばれるメソッド
                           variable = self.radio_value, # 選択の状態を設定する
                           value = 1                    # ラジオボタンに割り付ける値の設定
                           )

        radio2 = tk.Radiobutton(self.master, 
                           text = "ラジオボタン2",      # ラジオボタンの表示名
                           command = self.radio_click,  # クリックされたときに呼ばれるメソッド
                           variable = self.radio_value, # 選択の状態を設定する
                           value = 2                    # ラジオボタンに割り付ける値の設定
                           )

        # ボタンの作成
        button = tk.Button(self.master, 
                           text = "ラジオボタンの選択を次へ",  # ボタンの表示名
                           command = self.button_click  # クリックされたときに呼ばれるメソッド
                           )

        # 配置
        radio0.pack()
        radio1.pack()
        radio2.pack()
        button.pack()

    def radio_click(self):
        # ラジオボタンの値を取得
        value = self.radio_value.get()
        print(f"ラジオボタンの値は {value} です")

    def button_click(self):
        # 選択されているラジオボタンを次に移動させる
        value = self.radio_value.get()
        self.radio_value.set((value + 1) % 3)

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

(実行結果)

ポイント

  • ラジオボタンの値(選択されたときの識別番号)はIntVarクラスオブジェクトをRadiobuttonクラスのvariableオプションに設定する
  • 1つだけ選択される複数のラジオボタンには、それぞれ、同じIntVarクラスオブジェクトをvariableオプションに設定する
  • 各ラジオボタンの値(選択されたときの識別番号)はRadiobuttonクラスのvalueオプションに設定しておく
  • 選択されているラジオボタンの値(valueオプションで指定した値)は IntVarクラスオブジェクト.get() で行う
  • 選択されているラジオボタンを変更するには IntVarクラスオブジェクト.set() の引数にラジオボタンの値(valueオプションで指定した値)を指定します。

オプション

オプション名 説明
activebackground クリックされたときの背景色を指定します。
activeforeground クリックされたときの文字色(チェックマークを含む)を指定します。
anchor 文字の配置位置を指定します。
【設定値】tk.N, tk.S, tk.W, tk.E, tk.NW, tk.NE, tk.SW, tk.SE, tk.CENTER
【初期値】tk.CENTER
※width, heightを指定し、余白が生じている際に有効です。
background 通常時(クリックされていないとき)の背景色を指定します。(bgと同じ)
bd 枠線の太さを指定します。
ただし、初期状態では枠線が表示されていないため、reliefで枠線のスタイルを指定する必要があります。(borderwidthと同じ)
bg backgroundと同じ
bitmap モノクロのBitmapを指定します。
(参考)https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/bitmaps.html
borderwidth bdと同じ
command ボタンがクリックされたときに呼び出すメソッドの名前を指定します。
compound 文字と画像の両方を表示する際に、文字に対して画像の表示位置を指定します。
【設定値】tk.LEFT, tk.RIGHT, tk.BOTTOM, tk.TOP, tk.CENTER
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
(参考)https://tkdocs.com/shipman/cursors.html
disabledforeground stateオプションで無効(DISABLED)に設定している際の文字色を設定します。
fg 表示する文字色を指定します。(foregroundと同じ)
font 表示する文字のフォントを指定します。
foreground fgと同じ
height ボタンの高さを文字数で指定します。
画像を配置した時は、画素数の指定になります。
highlightbackground
highlightcolor
highlightthickness
image 表示する画像を指定します。
indicatoron
justify 複数行の文字のときの、文字寄せ方向を指定します。
【設定値】左寄せ(tk.LEFT), 中央寄せ(tk.CENTER), 右寄せ(tk.RIGHT)
offrelief
overrelief ウィジェット上にマウスポインタがある際のスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
【初期値】tk.FLAT(枠線なし)
padx 文字の両側の隙間を指定します。
pady 文字の上下の隙間を指定します。
relief ボタンのスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
【初期値】tk.FLAT(枠線なし)
selectcolor チェックボックス(チェックのある四角)の背景色を指定します。
selectimage
state ウィジェットの有効/無効(操作できない状態)を指定します。
【設定値】tk.NORMAL, tk.DISABLED
【初期値】tk.NORMAL
takefocus
text 表示する文字を指定します。
textvariable
tristateimage
tristatevalue
underline 指定した順番(先頭から0始まり)の文字にアンダーラインを付加します。
value ラジオボタンの値(選択されたときの識別番号)
variable チェックの状態をBooleanVarクラスオブジェクトで指定します。
width ボタンの幅を文字数で指定します。
画像を配置した時は、画素数の指定になります。
wraplength 文字の折り返し幅を指定します。

メソッド

メソッド 説明
deselect() 選択状態のとき、同じvariableオプションを設定している他のラジオボタンも全て選択状態になります。(詳細不明。。)
flash() backgroundとactivebackgroundで交互に色を点滅させて表示します。
invoke() 選択状態にします。
select() 選択状態にします。