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

ウィジェットを配置するには、pack,grid,placeの3つのメソッドがありますが、ここではplaceについて説明します。

placeではウィジェットの位置を座標を指定して配置します。

初期状態では下記のように親(配置先)の左上を原点として、ウィジェットの左上の座標(初期状態の場合、anchorにより変更可)を指定します。

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

import tkinter as tk

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

        self.master.title("ウィジェットの配置(place)")     # ウィンドウタイトル
        self.master.geometry("300x200")       # ウィンドウサイズ(幅x高さ)

        #--------------------------------------------------------
        # ボタンの作成
        button = tk.Button(self.master, text = "ボタン")
        # 座標を指定して配置
        button.place(x = 100, y = 50)
        #--------------------------------------------------------

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

(実行画面)

構文

ウィジェット.place(オプション1 = 設定値, オプション2 = 設定値,・・・)

オプション

オプション名 説明
x 配置するX座標を指定します。
y 配置するY座標を指定します。
relx 配置先の座標を配置先の幅に対して相対的な位置を0.0~1.0の値で指定します。
0.0:左端、1.0:右端
rely 配置先の座標を配置先の高さに対して相対的な位置を0.0~1.0の値で指定します。
0.0:上端、1.0:下端
anchor ウィジェットを配置する座標の基準位置を指定します。
【設定値】tk.N, tk.NE, tk.E, tk.SE, tk.S, tk.SW, tk.W, tk.NW, tk.CENTER
【初期値】tk.NW
width ウィジェットの幅を画素数で指定します。
height ウィジェットの高さを画素数で指定します。
relwidth ウィジェットの幅を配置先の幅に対して相対的な値(0.0~1.0)で指定します。
relheight ウィジェットの高さを配置先の高さに対して相対的な値(0.0~1.0)で指定します。

anchor

anchorの基準位置はウィジェットに対して以下のようになります。

サンプル

各種オプション設定を使ったサンプルを示します。

import tkinter as tk

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

        self.master.title("ウィジェットの配置(place)")     # ウィンドウタイトル
        self.master.geometry("300x400")       # ウィンドウサイズ(幅x高さ)

        #--------------------------------------------------------
        # ボタンの作成
        button1 = tk.Button(self.master, text = "ボタン1")
        button1.place(x = 30, y = 20)

        button2 = tk.Button(self.master, text = "ボタン2")
        button2.place(x = 80, y = 70, anchor = tk.CENTER) # 指定座標の基準位置変更

        button3 = tk.Button(self.master, text = "ボタン3")
        button3.place(x = 30, y = 90, width = 100, height = 40) # ウィジェットのサイズ指定

        button4 = tk.Button(self.master, text = "ボタン4")
        button4.place(relx = 0.5, rely = 0.4, anchor = tk.CENTER) # 相対座標指定

        button5 = tk.Button(self.master, text = "ボタン5")
        button5.place(relx = 0.5, relwidth = 0.8, y = 200, anchor = tk.CENTER) # ウィジェットのサイズを相対的に指定
        #--------------------------------------------------------
if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()

(実行画面)

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

ウィジェットを配置するには、pack,grid,placeの3つのメソッドがありますが、ここではgridについて説明します。

gridでウィジェットを配置するのは、どことなくエクセルのセルにウィジェットを配置するようなイメージに似ています。

こんなイメージ↓

ただし、gridでは行番号(row)、列番号(column)は0(ゼロ)から始まります。

また、エクセルにはセルを結合して中央揃えという機能がありますが、gridにも同様の機能があり、rowspancolumnspanを使ってセルを結合します。

最初のエクセルのイメージで配置したプログラムがこちら↓(ウィジェットの範囲が分かり易いように背景色をつけました)

import tkinter as tk

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

        self.master.title("ウィジェットの配置(grid)")     # ウィンドウタイトル
        self.master.geometry("300x180")       # ウィンドウサイズ(幅x高さ)

        #--------------------------------------------------------
        # ラベルの作成
        label1 = tk.Label(self.master, text = "ラベル1", bg = 'cyan1')
        label2 = tk.Label(self.master, text = "ラベル2", bg = 'green1')
        label3 = tk.Label(self.master, text = "ラベル3", bg = 'yellow1')
        label4 = tk.Label(self.master, text = "ラベル4", bg = 'pink1')
        label5 = tk.Label(self.master, text = "ラベル5", bg = 'MediumPurple1')
        label6 = tk.Label(self.master, text = "***ラベル6***", bg = 'LightSteelBlue1')

        #--------------------------------------------------------
        # gridでウィジェットの配置
        label1.grid(row = 0, column = 1, columnspan = 3, sticky = tk.W+tk.E)
        label2.grid(row = 0, column = 0, rowspan = 5, sticky = tk.N+tk.S)
        label3.grid(row = 1, column = 1)
        label4.grid(row = 1, column = 3)
        label5.grid(row = 2, column = 2)
        label6.grid(row = 3, column = 1, columnspan = 3)
        #--------------------------------------------------------

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

構文

ウィジェット.grid(オプション1 = 設定値, オプション2 = 設定値,・・・)

オプション

オプション名 説明
column ウィジェットを配置する列番号(0始まり)を指定します。
columnspan グリッドを横方向に結合する数を指定します。
ipadx ウィジェットの内側の横方向の隙間を設定します。
ipady ウィジェットの内側の縦方向の隙間を設定します。
padx ウィジェットの外側の横方向の隙間を設定します。
pady ウィジェットの外側の縦方向の隙間を設定します。
row ウィジェットを配置する行番号(0始まり)を指定します。
rowspan グリッドを縦方向に結合する数を指定します。
sticky グリッド内のウィジェットを配置する位置
アンカーの機能にも似ていますが、例えば上下(tk.N+ tk.S)を指定すると、ウィジェットが上下方向にグリッド内いっぱいに広がります。【設定値】tk.N, tk.S, tk.W, tk.E, tk.NW, tk.NE, tk.SW, tk.SE, tk.NSEW
および上記組み合わせ(tk.N+ tk.Sなど)

ウィンドウのリサイズに合わせて行、列の幅、高さを調整する

gridでウィジェットを配置すると、行や列方向の最大の高さ、幅に合わせてグリッド状に配置されますが、ウィンドウをリサイズしても幅や高さが変わる事がありません。

↓ウィンドウのリサイズ

そこで、ウィンドウに合わせて行の高さを調整するにはgrid_rowconfigure()メソッドを、列の幅を調整するにはgrid_columnconfigure()メソッドを用います。

 

構文

親ウィジェット.grid_columnconfigure(列番号, オプション)
親ウィジェット.grid_rowconfigure(行番号, オプション)

オプション

オプション名 説明
weight 1以上の整数を指定すると、幅/高さが調整されます。
複数の行もしくは列に対してweightを指定すると、指定したweightの値の比率で幅/高さが調整されます。
minsize 最小の幅/高さを画素単位で指定します。
pad 列/高さの隙間を画素単位で指定します。

 

サンプル

import tkinter as tk

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

        self.master.title("ウィジェットの配置(grid)")     # ウィンドウタイトル
        self.master.geometry("300x180")       # ウィンドウサイズ(幅x高さ)

        #--------------------------------------------------------
        # ラベルの作成
        lbl_00 = tk.Label(self.master, text = "row,col")

        lbl_col1 = tk.Label(self.master, text = "col1")
        lbl_col2 = tk.Label(self.master, text = "col2")
        lbl_col3 = tk.Label(self.master, text = "col3")

        lbl_row1 = tk.Label(self.master, text = "row1")
        lbl_row2 = tk.Label(self.master, text = "row2")
        lbl_row3 = tk.Label(self.master, text = "row3")

        #--------------------------------------------------------
        # Entry(テキストボックス)の作成
        entry1 = tk.Entry(self.master, width = 20)
        entry2 = tk.Entry(self.master, width = 20)
        entry3 = tk.Entry(self.master, width = 20)

        #--------------------------------------------------------
        # ボタンの作成
        button1 = tk.Button(self.master, text = "...")
        button2 = tk.Button(self.master, text = "...")
        button3 = tk.Button(self.master, text = "...")

        #--------------------------------------------------------
        # gridでウィジェットの配置
        lbl_00.grid(row = 0, column = 0)

        lbl_col1.grid(row = 0, column = 1)
        lbl_col2.grid(row = 0, column = 2)
        lbl_col2.grid(row = 0, column = 3)

        lbl_row1.grid(row = 1, column = 0)
        lbl_row2.grid(row = 2, column = 0)
        lbl_row3.grid(row = 3, column = 0)

        entry1.grid(row = 1, column = 1, sticky=tk.EW) # 幅に合わせて大きくする
        entry2.grid(row = 2, column = 1, sticky=tk.EW) # 幅に合わせて大きくする
        entry3.grid(row = 3, column = 1, sticky=tk.EW) # 幅に合わせて大きくする

        button1.grid(row = 1, column = 3)
        button2.grid(row = 2, column = 3)
        button3.grid(row = 3, column = 3)

        #--------------------------------------------------------
        # ウィンドウのリサイズに合わせてEntryの幅(column=1)を広げる
        self.master.grid_columnconfigure(1, weight=1) # 列の調整
        #self.master.grid_rowconfigure(1, weight=1) # 行の調整

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

実行結果

↓ウィンドウのリサイズ

gridを使うサンプル

個人的にはウィンドウ内にFrameを配置し、その中にpackでウィジェットを配置する場合が多いのですが、gridはラベルテキストボックス(Entry)を並べて配置する場合に使っています。

以下は、ラベルとテキストボックスを並べたサンプルです。

import tkinter as tk

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

        self.master.title("ウィジェットの配置(grid)")     # ウィンドウタイトル
        self.master.geometry("300x180")       # ウィンドウサイズ(幅x高さ)

        #--------------------------------------------------------
        # ラベルの作成
        label0 = tk.Label(self.master, text = "設定値")

        label1 = tk.Label(self.master, text = "項目1")
        label1_1 = tk.Label(self.master, text = "項目1_1")
        label1_2 = tk.Label(self.master, text = "項目1_2")
        label1_3 = tk.Label(self.master, text = "項目1_3")

        label2 = tk.Label(self.master, text = "項目2")
        label2_1 = tk.Label(self.master, text = "項目2_1")
        label2_2 = tk.Label(self.master, text = "項目2_2")
        label2_3 = tk.Label(self.master, text = "項目2_3")

        # テキストボックス(Entry)の作成
        self.entry1_1 = tk.Entry(self.master, justify = tk.RIGHT)
        self.entry1_2 = tk.Entry(self.master, justify = tk.RIGHT)
        self.entry1_3 = tk.Entry(self.master, justify = tk.RIGHT)

        self.entry2_1 = tk.Entry(self.master, justify = tk.RIGHT)
        self.entry2_2 = tk.Entry(self.master, justify = tk.RIGHT)
        self.entry2_3 = tk.Entry(self.master, justify = tk.RIGHT)

        #--------------------------------------------------------
        # gridでウィジェットの配置
        label0.grid(row = 0, column = 0, rowspan = 8)

        label1.grid(row = 0, column = 1, columnspan = 2)
        label1_1.grid(row = 1, column = 1); self.entry1_1.grid(row = 1, column = 2)
        label1_2.grid(row = 2, column = 1); self.entry1_2.grid(row = 2, column = 2)
        label1_3.grid(row = 3, column = 1); self.entry1_3.grid(row = 3, column = 2)

        label2.grid(row = 4, column = 1, columnspan = 2)
        label2_1.grid(row = 5, column = 1); self.entry2_1.grid(row = 5, column = 2)
        label2_2.grid(row = 6, column = 1); self.entry2_2.grid(row = 6, column = 2)
        label2_3.grid(row = 7, column = 1); self.entry2_3.grid(row = 7, column = 2)
        #--------------------------------------------------------

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

(実行画面)

【Python/tkinter】Label(ラベル)

tkinterでラベルの作成のサンプルを以下に示します。

import tkinter as tk

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

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

        #--------------------------------------------------------
        # ラベルの作成
        self.label = tk.Label(self.master, text = "ラベルの文字")
        # ラベルの配置
        self.label.pack()
        #--------------------------------------------------------

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

(実行画面)

構文

ラベルオブジェクト = tk.Label(親ウィジェット, オプション1 = 設定値, オプション2 = 設定値,・・・)

オプション

オプション名 説明
activebackground クリックされたときの背景色を指定します。
activeforeground クリックされたときの文字色を指定します。
anchor 文字の配置位置を指定します。
【設定値】tk.N, tk.S, tk.W, tk.E, tk.NW, tk.NE, tk.SW, tk.SE, tk.CENTER
background 背景色を指定します。
bd 枠線の太さを指定します。
ただし、初期状態では枠線が表示されていないため、reliefで枠線のスタイルを指定する必要があります。(borderwidthと同じ)
bg 通常時の背景色を指定します。
bitmap モノクロのBitmapを指定します。
(参考)https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/bitmaps.html
borderwidth bdと同じ
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 表示する画像を指定します。
justify 複数行の文字のときの、文字寄せ方向を指定します。
【設定値】左寄せ(tk.LEFT), 中央寄せ(tk.CENTER), 右寄せ(tk.RIGHT)
padx 文字の両側の隙間を指定します。
pady 文字の上下の隙間を指定します。
relief ラベルの枠線のスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
state
takefocus
text 表示する文字を指定します。
textvariable StringVarクラスオブジェクトを指定し、ラベルの文字列を指定します。
underline 指定した順番(先頭から0始まり)の文字にアンダーラインを付加します。
width ラベルの幅を文字数で指定します。
wraplength 文字の折り返し幅を指定します。

 

ラベルはプログラムの各種情報を表示するのに、プログラムの実行中に文字列を変更する場合が多いかと思います。

ラベルの文字を変更する方法は主に下記の2通りあります。

  • textvariableオプションのStringVarを指定し変更する方法
  • ラベルのtextオプションを直接変更する方法

 

textvariableオプションのStringVarを指定し変更する方法

以下のサンプルでは、LabeltextvariableオプションにStringVarを指定し、ボタンのクリック時にラベルの文字列を変更しています。

import tkinter as tk

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

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

        #--------------------------------------------------------
        # ラベルの作成
        self.label_text = tk.StringVar(value = "ラベルの文字") # valueは初期値
        self.label = tk.Label(self.master, textvariable = self.label_text)
        # ラベルの配置
        self.label.pack()
        #--------------------------------------------------------
        # ボタンの作成
        self.button = tk.Button(self.master, text = "ボタン", command = self.button_click)
        # ボタンの配置
        self.button.pack()
        #--------------------------------------------------------

    def button_click(self):
        '''ボタンがクリックされたとき'''
        # ラベル文字の変更方法1
        # StringVarの値を設定する
        self.label_text.set("ボタンがクリックされた")

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

(プログラム起動時)

(ボタンクリック後)

 

ラベルのtextオプションを直接変更する方法

以下のサンプルでは、ボタンのクリック時にラベルのtextオプションを直接変更しています。

ただし、textオプションとtextvariableオプションを両方指定することは出来ないので注意してください。

import tkinter as tk

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

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

        #--------------------------------------------------------
        # ラベルの作成
        self.label = tk.Label(self.master, text = "ラベルの文字")
        # ラベルの配置
        self.label.pack()
        #--------------------------------------------------------
        # ボタンの作成
        self.button = tk.Button(self.master, text = "ボタン", command = self.button_click)
        # ラベルの配置
        self.button.pack()
        #--------------------------------------------------------

    def button_click(self):
        '''ボタンがクリックされたとき'''
        # ラベル文字の変更方法2
        # ラベルのプロパティを直接設定する。ただし、textvariableプロパティは設定しないこと
        self.label["text"] = "ボタンがクリックされた"

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

(プログラム起動時)

(ボタンクリック後)

【OpenCV/Python】日本語の画像ファイル読込・保存

OpenCVで画像ファイルを開くとき、ファイル名やパスに日本語が含まれていると、画像ファイルを開いてくれません。

試しに以下のようなコードを実行すると、エラーが起き実行できません。

import cv2

# OpenCVで画像ファイルを開く(ファイル名が日本語)
img = cv2.imread("画像ファイル.bmp", cv2.IMREAD_UNCHANGED)

cv2.imshow("Image", img)
cv2.waitKey(0)

エラー情報

Message=OpenCV(4.5.3) C:\Users\runneradmin\AppData\Local\Temp\pip-req-build-sn_xpupm\opencv\modules\imgproc\src\color.cpp:182: 
error: (-215:Assertion failed) !_src.empty() in function 'cv::cvtColor'

エラーそのものはimread関数で画像ファイルを開くのに失敗し、戻り値の画像データ(img)が空(None)になっているため、エラーが発生しています。

日本語の画像ファイルを開くためには、PillowもしくはNumPyで画像ファイルを開いてOpenCVの画像データであるNumPyのndarray形式に変換すれば、OpenCVで日本語の画像ファイルを扱えるようになります。

Pillowで画像ファイルを開き、OpenCV(NumPyのndarray)に変換する

import cv2
import numpy as np
from PIL import Image

# Pillowで画像ファイルを開く
pil_img = Image.open("画像ファイル.bmp")
# PillowからNumPyへ変換
img = np.array(pil_img)
# カラー画像のときは、RGBからBGRへ変換する
if img.ndim == 3:
    img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)

cv2.imshow("Image", img)
cv2.waitKey(0)

Pillowで画像ファイルを開くと、カラー画像の場合、RGBの順でデータが格納されるので、cvtColorを用いて、OpenCVの形式(BGR)へ変換します。

PillowからNumPyの画像データへ変換する方法は下記ページを参照ください。

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

NumPyで画像ファイルを開き、OpenCV(NumPyのndarray)に変換する

import cv2
import numpy as np
from PIL import Image

import time

start = time.perf_counter()

# NumPyで画像ファイルを開く
buf = np.fromfile("画像ファイル.bmp", np.uint8)
img = cv2.imdecode(buf, cv2.IMREAD_UNCHANGED)

print((time.perf_counter() - start) * 1000)#, "msec")

cv2.imshow("Image", img)
cv2.waitKey(0)

NumPyのfromfileで画像ファイルをバイナリで開き、ファイルの中身をメモリ(buf)に格納します。

OpenCVのimdecodeでメモリ上の画像データをOpenCVの画像データ(NumPyのndarray)に変換します。

OpenCVからPillowへ変換し画像ファイルに保存する

import cv2
from PIL import Image

# OpenCVで画像ファイルを開く(ファイル名に日本語が無い場合)
img = cv2.imread("image_file.bmp", cv2.IMREAD_UNCHANGED)

# カラー画像のときは、BGRからRGBへ変換する
if img.ndim == 3:
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# NumPyからPillowへ変換
pil_image = Image.fromarray(img)
# Pillowで画像ファイルへ保存
pil_image.save("画像ファイル_pillow.bmp")

Pillowで画像ファイルを開いたときの逆の事をすると、日本語のファイル名でも画像ファイルに保存することができます。

OpenCVの画像データ(ndarray)を画像形式に変換しファイルに保存する

import cv2
import numpy as np

# OpenCVで画像ファイルを開く(ファイル名に日本語が無い場合)
img = cv2.imread("image_file.bmp", cv2.IMREAD_UNCHANGED)

# 画像データを画像ファイル形式のメモリ変換する
ret, buf = cv2.imencode(".bmp", img)
# NumPyで画像ファイルへ保存
with open("画像ファイル_numpy.bmp", mode='w+b') as f:
    buf.tofile(f)

OpenCVの画像データ(NumPyのndarray)をimencodeで画像ファイル形式にメモリ上で変換し、NumPyのtofileで画像ファイルに書き込みます。

まとめ

日本語の画像ファイルの読込・保存をするのにPillowおよびNumPyを介して処理を行いましたが、それぞれの処理時間を比較しました。

処理時間は8192×8192画素のカラー画像を5回処理したときの平均時間です。

方法 平均処理時間(msec)
Pillowで画像ファイルを開く 513
NumPyで画像ファイルを開く 210
Pillowで画像ファイルへ保存 337
NumPyで画像ファイルへ保存 1106

これを見ると、日本語ファイル名の画像を開くときは、NumPy、保存するときはPillowを使った方が速い結果になりました。

NumPyで保存するときが極端に遅かったので、関数ごとの処理時間を見てみたところ、imencodeに約100msec、tofileに約1000msecという時間でした。

画像の読込と保存でPillowとNumPyを使い分けるのは、少々面倒ですが、処理時間もそれなりに違うので、画像の読込はNumPy、保存はPillowを使った方がいいかもしれません。

【Python】指定フォルダ内のファイル、フォルダ一覧を取得

フォルダに格納された画像ファイルの一覧を取得する場合など、フォルダのパスを指定してファイルの一覧を取得したい場合があります。

その場合に、Pythonでは主に3通りの方法があります。

  • glob.glob
    検索条件を指定してファイル、フォルダ一覧を取得
  • os.listdir
    フォルダ内のファイル、フォルダ一覧を取得
  • os.scandir
    フォルダ内のファイル、フォルダ一覧をファイルか?フォルダか?の属性付きで取得

 

試しに以下のようなファイル、フォルダ構成の時に、どのようにファイル、フォルダを取得できるのか?をみていきたいと思います。

└─images
    ├─annotation.csv
    ├─0
    │ ├─img0001.bmp
    │ ├─img0001.png
    │ ├─img0002.png
    │ └─img0003.png
    ├─1
    │ ├─img0101.png
    │ ├─img0102.png
    │ └─img0103.png
    └─2
      ├─img0201.png
      ├─img0202.png
      └─img0203.png

glob.glob

指定したフォルダ内において、検索条件を指定してファイル、フォルダ一覧を取得。

最も使いやすいと思います。

(サンプル)

import glob

files = glob.glob("./images/*")
print(files)
# ['./images\\0', './images\\1', './images\\2', './images\\annotation.csv']

files = glob.glob("./images/*.*")
print(files)
# ['./images\\annotation.csv']

files = glob.glob("./images/0/*.png")
print(files)
# ['./images/0\\img0001.png', './images/0\\img0002.png', './images/0\\img0003.png']

files = glob.glob("./images/**", recursive=True)
print("recursive", files)
# ['./images\\', './images\\0', './images\\0\\img0001.bmp', './images\\0\\img0001.png', './images\\0\\img0002.png', './images\\0\\img0003.png', './images\\1', './images\\1\\img0101.png', './images\\1\\img0102.png', './images\\1\\img0103.png', './images\\2', './images\\2\\img0201.png', './images\\2\\img0202.png', './images\\2\\img0203.png', './images\\annotation.csv']# 

検索条件に * を指定すると、フォルダ内のファイル、フォルダ全てを取得します。

検索条件に *.* を指定すると、フォルダ内のファイルを取得します。ただし、フォルダ名に . が含まれる場合は、そのフォルダも取得します。

検索条件に *.png のように指定すると、指定した拡張子のファイルを取得します。

検索条件に ** を指定し、さらに、recursive=True と指定すると、指定したフォルダ以下(子のフォルダ内を含む)のファイル、フォルダ全てを取得します。

詳細はこちら↓のページを参照ください。

https://docs.python.org/ja/3/library/glob.html?highlight=glob#glob.glob

os.listdir

指定したフォルダ内のファイル、フォルダを取得します。

(サンプル)

import os
files = os.listdir("./images")
print(files)
# ['0', '1', '2', 'annotation.csv']

files = os.listdir("./images/0")
print(files)
# ['img0001.bmp', 'img0001.png', 'img0002.png', 'img0003.png']

os.listdirではファイル、フォルダの区別なく、指定したフォルダ内のファイル、フォルダの一覧を取得します。

取得した一覧からファイルか?フォルダか?を判断するにはos.path.isfile()メソッドos.path.isdir()メソッドを使って以下のように行うことも可能です。

import os

files = os.listdir("./images")

for f in files:
    path = os.path.join("./images", f)
    if os.path.isfile(path):
        # ファイルの場合
        print("[file  ]", f)
    if os.path.isdir(path):
        # ファイルの場合
        print("[folder]", f)

# [Dir ] 0
# [Dir ] 1
# [Dir ] 2
# [File] annotation.csv

詳細はこちら↓のページを参照ください。

https://docs.python.org/ja/3/library/os.html?highlight=os%20listdir#os.listdir

os.scandir

指定したフォルダ内のファイル、フォルダをファイルか?フォルダか?の属性付きで取得します。

os.listdirではファイル、フォルダの一覧を取得後にファイルか?フォルダか?を判断しましたが、os.scandirでは、取得時に属性付きで取得します。

(サンプル)

import os

with os.scandir("./images") as it:
    for entry in it:
        if entry.is_file():
            print("[file   ]", entry.name)
        elif entry.is_dir():
            print("[folder ]", entry.name)

# [Dir ] 0
# [Dir ] 1
# [Dir ] 2
# [File] annotation.csv

あまり使用する機会が少ないのですが、フォルダ構成のまま、ファイルを取得した場合など、再帰的に呼び出すとフォルダ構成を取得できると思います。

詳細はこちら↓のページを参照ください。

https://docs.python.org/ja/3/library/os.html?highlight=os%20listdir#os.scandir

【Python】画像データがNumPyかPillowか調べる方法

Pythonで画像処理をしていると、画像データの型(クラス)は、OpenCVを使っているとNumPyだし、Tkinterで画像を表示しようとすると、Pillowを使ったりもするので、どうしても画像データがNumPyとPillowが混在しがちです。

そこで、画像データがNumPyなのか?Pillowなのか?を調べる方法の紹介です。

画像データに限らず、インスタンスしたクラスのオブジェクトが、どのクラスなのかを調べるにはisinstance関数を用います。

ininstance関数の書式は以下の通りです。

ininstance(クラスオブジェクト, クラス)

クラスオブジェクトが指定したクラスと一致している場合はTrueが、異なる場合はFalseが返ります。

このininstance関数を使って、画像データがNumPyなのか?Pillowなのか?を調べる関数の例を以下に示します。

def check_image_data(image):
    '''画像データがNumPyか、Pillowかを調べる'''
    if isinstance(image, np.ndarray):
        print("NumPy Image")
    elif isinstance(image, Image.Image):
        print("Pillow Image")

この関数を使って、実際に画像データがNumPyかPillowかを調べるサンプルは以下の通りです。

from PIL import Image
import numpy as np

def check_image_data(image):
    '''画像データがNumPyか、Pillowかを調べる'''
    if isinstance(image, np.ndarray):
        print("NumPy Image")
    elif isinstance(image, Image.Image):
        print("Pillow Image")

# Pillowの画像データ
pillow_image = Image.open("Mandrill.bmp")
check_image_data(pillow_image)

# NumPyの画像データ
numpy_image = np.asarray(pillow_image)
check_image_data(numpy_image)

実行結果

 

型を調べるだけならtype関数を使うこともできます。

(例)

print(type(numpy_image))
# <class 'numpy.ndarray'>
print(type(pillow_image))
# <class 'PIL.BmpImagePlugin.BmpImageFile'>

上記のコメント部分がtype関数を使って型を表示した結果になりますが、NumPyの型は‘numpy.ndarray’と表示されているので、まだ分かり易いのですが、Pillowの型は、’PIL.Image.Image’と表示されるのを期待しているのですが、‘PIL.BmpImagePlugin.BmpImageFile’と表示されてしまいます。

これは、bmpファイルからPillowの画像データを開いたためで、別のjpegファイルから開くと別の型が表示されます。

そのため、型を調べる、表示するだけなら type関数、型を判断するならisinstance関数という使い分けが良さそうです。

関連記事

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【Python/tkinter】OpenCVのカメラ動画をCanvasに表示する

USBカメラなどで取得した画像(動画)をOpenCVの cv2.imshow() で表示するには比較的簡単に表示する事ができますが、tkinterを使ってWindow付でCanvasに表示したい場合には、少しコツが必要になります。

そこで、tkinterのCanvasに動画を表示する方法を紹介します。

OpenCVでUSBカメラの動画を表示

USBカメラの動画をOpenCVのの cv2.imshow() で表示するプログラムは、以下のようにすれば表示されます。

import cv2

# カメラをオープンする
capture = cv2.VideoCapture(0)

# カメラがオープン出来たか?
camera_opened = capture.isOpened()

while camera_opened:

    # フレーム画像の取得
    ret, frame = capture.read()
    
    # 画像の表示
    cv2.imshow("Image", frame)

    if cv2.waitKey(1) != -1:
        # キー入力で終了
        break

capture.release()
cv2.destroyAllWindows()

tkinterのCanvasに動画を表示

tkinterのCanvasに動画を表示する場合は、OpenCVの動画表示プログラムの cv2.imshow() の部分を書き換えて表示しようとしても動画が表示されません。

これは、while文中でCanvasへ画像を表示しようとすると、画像の更新スレッドがブロックされた状態となってしまい、画像を更新するには、少しの時間、スレッドを空ける必要があります。

このスレッドを空ける処理には after() 関数を使います。

after() 関数では、指定した時間を待ってから、指定した関数を実行することができます。

処理の意味合い的には sleep() 関数とも似ていますが、sleep() 関数では、指定時間分、スレッドをブロックしてしまいますが、 after() 関数ではスレッドがブロックされません。

それらを考慮し、tkinterのCanvas上にUSBカメラの動画を表示したプログラムが以下になります。

 

以下のサンプルプログラムでは、Camvas上をマウスの左ボタンをクリックすると、画像の取得が開始/停止するようになっています。

import tkinter as tk
from tkinter import filedialog
from PIL import Image, ImageTk, ImageOps  # 画像データ用

import cv2

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

        self.master.title("OpenCVの動画表示")       # ウィンドウタイトル
        self.master.geometry("400x300")     # ウィンドウサイズ(幅x高さ)
        
        # Canvasの作成
        self.canvas = tk.Canvas(self.master)
        # Canvasにマウスイベント(左ボタンクリック)の追加
        self.canvas.bind('<Button-1>', self.canvas_click)
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)

        # カメラをオープンする
        self.capture = cv2.VideoCapture(0)

        self.disp_id = None

    def canvas_click(self, event):
        '''Canvasのマウスクリックイベント'''

        if self.disp_id is None:
            # 動画を表示
            self.disp_image()
        else:
            # 動画を停止
            self.after_cancel(self.disp_id)
            self.disp_id = None

    def disp_image(self):
        '''画像をCanvasに表示する'''

        # フレーム画像の取得
        ret, frame = self.capture.read()
    
        # BGR→RGB変換
        cv_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # NumPyのndarrayからPillowのImageへ変換
        pil_image = Image.fromarray(cv_image)

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

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

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

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

        # disp_image()を10msec後に実行する
        self.disp_id = self.after(10, self.disp_image)

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

実行結果

ポイント

  • 動画を表示するときは、while文ではなく、after()関数で繰り返し処理を行う。
  • OpenCVの画像は、カラー画像の場合、BGRのデータの並びをRGBへ並び替えてからOpenCVの画像データ(NumPyのndarray)をPillowのImageへImage.fromarray()関数で変換する。

関連記事

【OpenCV-Python】Tkinter GUI Sample

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

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【Python/NumPy】カラー画像データをRGBからBGRへ変換

【Python/tkinter】新しいウィンドウを開く(モーダル、モードレスダイアログ)

新しくウィンドウを開く場合、モーダルダイアログモードレスダイアログというものがあります。

モーダルダイアログとは、新しいウィンドウを開いたとき、新しいウィンドウのクリックやテキスト入力などの操作ができるが、元のウィンドウの操作ができない表示方法で、モードレスダイアログとは、新しいウィンドウと元のウィンドウの両方とも操作できる表示方法となります。

PythonのtkinterではToplevel()メソッドを実行すると新しいウィンドウを作成することができますが、デフォルトではモードレスダイアログとして開きます。

モーダルダイアログにするには、Toplevelで作成したウィンドウに対して、grab_set()メソッドを実行することで、モーダルダイアログになります。

 

サンプル実行結果

サンプルプログラム

import tkinter as tk

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

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

        # ボタンの作成
        btn_modeless = tk.Button(
            self.master, 
            text = "Modeless dialog",   # ボタンの表示名
            command = self.create_modeless_dialog    # クリックされたときに呼ばれるメソッド
            )
        btn_modeless.pack()

        btn_modal = tk.Button(
            self.master, 
            text = "Modal dialog",      # ボタンの表示名
            command = self.create_modal_dialog    # クリックされたときに呼ばれるメソッド
            )
        btn_modal.pack()

    def create_modeless_dialog(self):
        '''モードレスダイアログボックスの作成'''
        dlg_modeless = tk.Toplevel(self)
        dlg_modeless.title("Modeless Dialog")   # ウィンドウタイトル
        dlg_modeless.geometry("300x200")        # ウィンドウサイズ(幅x高さ)

    def create_modal_dialog(self):
        '''モーダルダイアログボックスの作成'''
        dlg_modal = tk.Toplevel(self)
        dlg_modal.title("Modal Dialog") # ウィンドウタイトル
        dlg_modal.geometry("300x200")   # ウィンドウサイズ(幅x高さ)

        # モーダルにする設定
        dlg_modal.grab_set()        # モーダルにする
        dlg_modal.focus_set()       # フォーカスを新しいウィンドウをへ移す
        dlg_modal.transient(self.master)   # タスクバーに表示しない

        # ダイアログが閉じられるまで待つ
        app.wait_window(dlg_modal)  
        print("ダイアログが閉じられた")

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

ポイント

  • Toplevelメソッドで新しいウィンドウを作成することができる。
  • Toplevelではデフォルトでモードレスダイアログとなる。
  • モーダルダイアログボックスにするには、grab_set()メソッドを実行する。
  • 新しく作成したウィンドウの表示直後はフォーカスが無いため、フォーカスするにはfocus_set()メソッドを実行する。
  • 新しいウィンドウをタスクバーに表示したくない場合はtransient()メソッドを実行する。
  • モーダルダイアログボックスが閉じられるまで待つには、wait_window(ダイアログ)メソッドで待つ。

【Python/os】パスからファイル名、拡張子、フォルダ名などを取得する

ファイルパスからファイル名、拡張子、フォルダ名などを取得するには、os.pathモジュールを用います。

まずは、サンプルを参照ください。

import os

filepath = r"C:\temp\Image.bmp"

# ファイル名 os.path.basename()
print(os.path.basename(filepath))       # 'Image.bmp'

# フォルダ名 os.path.dirname()
print(os.path.dirname(filepath))        # 'C:\temp'

# 拡張子の取得 os.path.splitext()
print(os.path.splitext(filepath)[1])    # '.bmp'

# 拡張子なしのパス os.path.splitext()
print(os.path.splitext(filepath)[0])    # 'C:\temp\Image'

# 拡張子なしのファイル名 os.path.basename(), os.path.splitext()
print(os.path.splitext(os.path.basename(filepath))[0])    # 'Image'

# ルートディレクトリ os.path.splitdrive()
print(os.path.splitdrive(filepath)[0])  # 'C:'

実行結果

 

使用した関数の説明は以下の通りです。

os.path.basename(path)

パス名(path)から最後の’\’以降のファイル名を返します。

(例)

入力path 戻り値
C:\temp\Images\Image01.bmp Image01.bmp
C:\temp\Images Images

os.path.dirname(path)

パス名(path)のディレクトリ名を返します。

(例)

入力path 戻り値
C:\temp\Images\Image01.bmp C:\temp\Images
C:\temp\Images C:\temp

os.path.splitext(path)

パス名(path)を (拡張子以外, 拡張子)のタプルを返します。

(例)

入力path 戻り値
C:\temp\Images\Image01.bmp (‘C:\\temp\\Images\\Image01’, ‘.bmp’)
C:\temp\Images (‘C:\\temp\\Images’, ”)

os.path.splitdrive(path)

パス名(path)を (ルートディレクトリ, それ以外)のタプルを返します。

(例)

入力path 戻り値
C:\temp\Images\Image01.bmp (‘C:’, ‘\\temp\\Images\\Image01.bmp’)
C:\temp\Images (‘C:’, ‘\\temp\\Images’)

参考

https://docs.python.org/ja/3/library/os.path.html

【Python/NumPy】カラー画像データをRGBからBGRへ変換

カラー画像データは各データが8bit(0~255の256諧調)のR, G, Bの要素からなる24bitカラー画像や、さらに透過率(A)を追加しR, G, B, Aの要素からなる32bitカラー画像があります。

Pythonでは、このカラー画像のデータの並びが使用するモジュールによって異なり、PillowではRGBRGBA の並びとなり、NumPyのndarrayで画像データを管理しているOpenCVでは BGRBGRA の並びとなっています。

つまり、カラー画像をPillowとOpenCV間で変換する場合、画像データの並びも RGB⇔BGRやRGBA⇔BGRA と変換する必要があります。

これを間違うと、RとBが入れ替わった状態となるため、下図のように変な画像になってしまいます。

(正しい画像データの並び)

(間違った画像データの並び)

カラー画像データの事前準備

Pillowでカラー画像を開き、24bitのカラー画像と32bitのカラー画像を用意し、これをNumPyのndarrayへ変換します。

import numpy as np
from PIL import Image
import cv2

# Pillowでカラー画像(RGB)を開く
pillow_rgb24 = Image.open("Mandrill.bmp")
# 24bitカラー(RGB)から32bitカラー(RGBA)へ変換
pillow_rgb32 = pillow_rgb24.convert("RGBA")

###############################
# PillowからNumPyのndarrayへ変換
numpy_rgb24 = np.array(pillow_rgb24) # 24bitカラー(RGB)
numpy_rgb32 = np.array(pillow_rgb32) # 32bitカラー(RGBA)

上記のようにPillowで開いたカラー画像をNumPyへ変換しただけの状態の画像をOpenCVのimshowで表示すると、RとBが入れ替わった画像が表示されます。

cv2.imshow("Image", numpy_rgb24)
cv2.waitKey()

(表示結果)

NumPyでRGB→BGR, RGBA→BGRAへ変換

Pillowのカラー画像をNumPyへ変換した直後のデータは
24bitカラーのとき
[[[R, G, B], [R, G, B], [R, G, B]],
[[R, G, B], [R, G, B], [R, G, B]],
[[R, G, B], [R, G, B], [R, G, B]]]

32bitカラーのとき
[[[R, G, B, A], [R, G, B, A], [R, G, B, A]],
[[R, G, B, A], [R, G, B, A], [R, G, B, A]],
[[R, G, B, A], [R, G, B, A], [R, G, B, A]]]

のように、RGBやRGBAの順で並んでいます。

これをOpenCVで使うときは、データの並びをBGRやBGRAの順へ変換する必要があります。

具体的には

24bitカラーのとき
[[[B, G, R], [B, G, R], [B, G, R]],
[[B, G, R], [B, G, R], [B, G, R]],
[[B, G, R], [B, G, R], [B, G, R]]]

32bitカラーのとき
[[[B, G, R, A], [B, G, R, A], [B, G, R, A]],
[[B, G, R, A], [B, G, R, A], [B, G, R, A]],
[[B, G, R, A], [B, G, R, A], [B, G, R, A]]]

のようにR, G, Bのデータを並び変える必要があります。

このRGB→BGRRGBA→BGRAの変換は以下のように行います。

# NumPyでRGBからBGRへ変換(24bitの場合) その1
numpy_bgr24 = numpy_rgb24[:, :, ::-1]
# NumPyでRGBからBGRへ変換(24bitの場合) その2
numpy_bgr24 = numpy_rgb24[:, :, [2, 1, 0]]

# RGBAからBGRAへ変換(32bitの場合)
numpy_bgr32 = numpy_rgb32[:, :, [2, 1, 0, 3]]

OpenCVでRGB→BGR, RGBA→BGRAへ変換

OpenCVの画像データはNumPyのndarrayなので、PillowからNumPyへ変換した画像データは、そのままOpenCVの関数で処理することができます。

# cvtColorで24bitカラー(RGB)から24bitカラー(BGR)へ変換
numpy_bgr24 = cv2.cvtColor(numpy_rgb24, cv2.COLOR_RGB2BGR)

# cvtColorで32bitカラー(RGBA)から32bitカラー(BGRA)へ変換
numpy_bgr32 = cv2.cvtColor(numpy_rgb32, cv2.COLOR_RGBA2BGRA)

24bitか?、32bitか?を調べる

カラー画像データの並びを入れ替える時は、24bitカラーのときと、32bitカラーのときとで、処理を変える必要があるため、NumPy配列(ndarray)が24bitと32bitのどちらなのか?を調べる必要があります。

それには、NumPyのshapeを取得しshape[2]の値が3であれば24bit、4であれば32bitとなります。

# 24bitか?32bit?かを調べる
print(numpy_rgb24.shape)
print("チャンネル数 = ", numpy_rgb24.shape[2])
print(numpy_rgb32.shape)
print("チャンネル数 = ", numpy_rgb32.shape[2])

(実行結果)

参考

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

【Python/tkinter】Entry(テキストボックス)

tkinterでテキストボックスはEntryと言います。

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

 

(実行結果)

(サンプルプログラム)

import tkinter as tk

class Application(tk.Frame):

    def __init__(self, master = None):
        super().__init__(master)

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

        # 表示する値
        self.entry_text = tk.StringVar() 
        # Entry(テクストボックス)の作成
        entry = tk.Entry(self.master,
            width = 30,         # ウィジェットの幅(文字数で指定)
            justify = tk.RIGHT, # tk.RIGHT:右寄せ、tk.LEFT:左寄せ、tk.CENTER:中央寄せ
            textvariable = self.entry_text # 表示する値
            )
        # ボタンの作成
        btn_input = tk.Button(self.master, text = "入力", command = self.btn_input_click)
        btn_clear = tk.Button(self.master, text = "クリア", command = self.btn_clear_click)

        entry.pack()
        btn_input.pack()
        btn_clear.pack()

    def btn_input_click(self):
        ''' Entryに入力された値を表示 '''
        print("Text = ", self.entry_text.get())

    def btn_clear_click(self):
        ''' Entryのクリア '''
        self.entry_text.set("")

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

ポイント

  • Entryの値はtextvariableにStringVarクラスオブジェクトを指定し、Entryのテキストの取得はStringVarクラスオブジェクト.get()、設定はStringVarクラスオブジェクト.set()で行います。
  • 他のウィジェットには標準的にはるcommandオプションがEntryにはありません。
    その代わりにvalidatecommandでテキストが変更されたときの処理を行います。
    詳細は後半で説明します。

オプション

オプション名 説明
background 通常時(クリックされていないとき)の背景色を指定します。(bgと同じ)
bd 枠線の太さを指定します。
bg backgroundと同じ
borderwidth bdと同じ
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
(参考)https://tkdocs.com/shipman/cursors.html
disabledbackground stateオプションで無効(DISABLED)に設定している際の背景色を設定します。
disabledforeground stateオプションで無効(DISABLED)に設定している際の文字色を設定します。
exportselection
fg 表示する文字色を指定します。(foregroundと同じ)
font 表示する文字のフォントを指定します。
foreground fgと同じ
highlightbackground
highlightcolor
highlightthickness
insertbackground
insertborderwidth
insertofftime
insertontime
insertwidth テキスト挿入時のカーソルの太さを指定します。
invalidcommand
invcmd
justify 文字寄せ方向を指定します。
【設定値】左寄せ(tk.LEFT), 中央寄せ(tk.CENTER), 右寄せ(tk.RIGHT)
relief テキストボックスの枠線のスタイルを指定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
【初期値】tk.FLAT(枠線なし)
selectbackground
selectborderwidth
selectforeground
show テキストボックスに表示する文字列を指定します。
(使用例)パスワード入力のとき
show = “*”
state ウィジェットの有効/無効(操作できない状態)を指定します。
【設定値】tk.NORMAL, tk.DISABLED
【初期値】tk.NORMAL
takefocus
textvariable Entryの値を取得、設定するためのStringVarクラスのインスタンスを指定します。
他にもIntVar, DoubleVar, BooleanVarの指定が可能です。
これらを指定することで、入力する値の型制限が可能になります。
validate 検証の対象を指定します。
‘none’, ‘key’, ‘focus’, ‘focusin’, ‘focusout’, ‘all’ のいづれか
validatecommand 検証の登録を行います。
width テキストボックスの幅を文字数で指定します。
xscrollcommand

メソッド

オプション名 説明
delete(first, last=None) 最初と最後の文字の位置を指定してテキストを削除します。
(例)最初から最後までを削除する方法
entry.delete(0, tk.END)
get() テキストボックスの文字列を取得します。
icursor(index) 挿入するカーソルの位置を指定します。
index(index)
insert(index, s) 指定した位置に文字を挿入します。
select_adjust ( index ) カーソルの位置から指定した位置までを選択します。
select_clear() テキストの選択状態を解除します。
select_from ( index )
select_present() 文字が選択されている場合はTrue, 選択されていない場合はFalseを返します。
select_range ( start, end ) 開始位置、終了位置を指定して文字を選択します。
select_to ( index ) 指定位置からカーソルの位置までを選択します。
xview ( index ) 横方向のスクロール位置を指定します。
xview_scroll ( number, what )

 

入力検証

テキストボックスに文字を入力してから表示するまでの間に、実際に表示するかどうか?を制御することが可能です。例えば、文字数の制限や、数字のみの入力などの制限をかけることができます。

 

(サンプルプログラム)テキストボックスの入力文字数を5文字までに制限する例

import tkinter as tk

class Application(tk.Frame):

    def __init__(self, master = None):
        super().__init__(master)

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

        validate_command = self.master.register(self.enty_validate)
        
        entry = tk.Entry(self.master,
            width = 30,
            validate='all', # 検証をどのタイミングで行うか?を指定します
            validatecommand = (
                validate_command, 
                '%d',   # アクションの種類 1:挿入、0:削除、-1:再検証
                '%i',   # 挿入/削除される文字の位置 挿入/削除されない場合は-1
                '%P',   # テキスト編集後の文字列
                '%s',   # テキスト編集前の文字列
                '%S',   # 挿入/削除される文字列
                '%v',   # validateオプションで指定した種類
                '%V',   # 実際に実施されたvalidateの種類
                '%W'    # Entryウィジェットの名前
                )
        )
        entry.pack()

    def enty_validate(self, action, index, prevalidation, current, test, validata_option, condition, name):
        '''入力検証'''
        print("enty_validate", action, index, prevalidation, current, test, validata_option, condition, name)

        if len(prevalidation) > 5:
            # 入力文字数を5文字までに制限
            # Falseを返すとテキストボックスに入力した文字は反映されない
            return False
        else:
            # Trueを返すとテキストボックスに入力した文字が反映される
            return True

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

上記サンプルは、すべてのタイミング(validateオプション)で、すべての情報を取得していますが、’%P’や’%S’だけを使用しても構いません。ただし、オプションの数に合わせて、呼ばれるメソッド(上記サンプルでは entry_validate)の引数の数を調整する必要があります。

validateオプション

オプション名 説明
none 検証なし(初期値)
key キー入力時
focus フォーカス時?
focusin フォーカスを取得したとき
focusout フォーカスを失ったとき
all 全て

validatecommandオプション

オプション名 説明
‘%d’ アクションの種類 1:挿入、0:削除、-1:再検証
‘%i’ 挿入/削除される文字の位置 挿入/削除されない場合は-1
‘%P’ テキスト編集後の文字列
‘%s’ テキスト編集前の文字列
‘%S’ 挿入/削除される文字列
‘%v’ validateオプションで指定した種類
‘%V’ 実際に実施されたvalidateの種類
‘%W’ Entryウィジェットの名前

 

参考

https://tcl.tk/man/tcl8.6/TkCmd/ttk_entry.htm

【Python/Pillow(PIL)】画像の輝度値をCSVファイルに保存/読込

画像処理をしていると、画像の輝度値をCSVファイル保存して、輝度値そのものや、輝度値の分布などを見たくなります。

Pythonにはcsvモジュールがあり、比較的簡単に画像の輝度値をCSVファイルに保存することができます。

輝度値をCSVファイルに保存するサンプルを示します。

ただし、モノクロとカラーの画像が混在すると難しいので、モノクロ限定とします。

 

(参考)csvモジュール

https://docs.python.org/ja/3/library/csv.html

輝度値の取得はPillowのgetdata()メソッドを使用します。

【Python/Pillow(PIL)】画像の輝度値の取得/設定

 

輝度値のCSVファイル保存

import csv
from PIL import Image

# 画像読込
img = Image.open("Mandrill.bmp")

# モノクロ画像へ変換
img = img.convert("L")
width, height = img.size

########################################################
# 輝度値の取得、CSVファイルに保存

# 画像の輝度値をlistで取得
data = list(img.getdata())

# 輝度値をCSVファイルで保存
with open('image_data.csv', 'w', newline='') as csvfile:
    spamwriter  = csv.writer(csvfile)

    # 画像データを一行ごと書き込み
    x = 0
    for y in range(height):
        # 一行分のデータ
        line_data = data[x:x+width]
        # 一行分のデータを書き込み
        spamwriter.writerow(line_data)
        x += width

CSVファイルをエクセルで開くと以下のようになります。

 

CSVファイルを開き画像へ変換

CSVファイルを開くのも保存と同様にcsvモジュールを用います。

ただし、CSVファイルは前項で保存したCSVファイルのように二次元でモノクロの輝度値が配置されたファイルとします。

csvモジュールでCSVファイルを開いたとき、CSVファイルの各値は文字列のリストに格納されるので、各要素をint型に変換している部分がポイントとなります。

import csv
from PIL import Image

########################################################
# CSVファイルを開く、Pillowの画像データに変換
load_data = []
# CSVファイルを開く
with open('image_data.csv', newline='') as csvfile:
    # ファイルの読込
    spamreader = csv.reader(csvfile)

    height = 0
    # データを一行ごとにリストに追加
    for line_data in spamreader:
        # 各要素の文字列をintに変換
        row = [int(val) for val in line_data]
        # リストに行データを追加
        load_data += row
        # 行数(画像の高さ)カウント
        height += 1

# 画像の幅を計算
width = len(load_data) / height

# 画像を作成
csv_image = Image.new("L", (int(width), height))
# データを読込(輝度値が格納されたリストのデータをPillowの画像データに設定)
csv_image.putdata(load_data)

# 画像の表示
csv_image.show()

処理結果は以下のようにCSVファイルを開くと、画像が表示されます。

CSVファイルをエクセルで見やすくする

CSVファイルをエクセルで開くと、こんな感じ↓で味気ないものとなります。

これを画像らしく、少し見やすくします。

まず、セルのサイズを正方形に近くなるように列の幅を調整します。

輝度値が記載されている列を全て選択し、列の部分を右ボタンでクリックし、列の幅を選択します。

表示された設定画面で、列の幅に2.7を入力します。

するとセルのサイズがだいたい正方形になります。

さらにセルに色を付けて画像らしくします。

輝度値が記載されているセルを全て選択し、ホーム→条件付き書式→カラースケール→その他のルールと選択します。

表示されたウィンドウで、最小値、最大値の部分を以下のように設定します。

最小値 最大値
種類 数値 数値
0 255

すると、セルの背景色が画像らしくなります。

この表示を縮小すると、まさに画像になってます。

エクセルで画像の輝度値を編集

試しに保存されたCSVファイルをエクセルで開き、画像の輝度値をじかに編集してみます。

これをCSVファイルに保存するのですが、エクセルのCSVファイル形式には CSV UTF-8 と CSV があるので、何も付いていない CSV(コンマ区切り)(*.csv)の方を選択して、CSVファイルに保存します。

このCSVファイルを、先ほどのCSVファイルを CSVファイルを開き画像へ変換 のプログラムで開くと以下のようになります。

エクセルで画像を直接編集できるのは、ちょと楽しいのですが、エクセルで画像処理を本気でやろうとするのは大変なので、画像をCSVファイルに保存するときは、画像の輝度値を解析的に見る程度に留めておく事をお勧めします。

【Python】リスト(配列)の繰り返しの注意点

Pythonのlistで同じ要素を繰り返して書く場合は、リストの掛け算のように

data_list = [1, 2, 3] * 5
print(data_list)

とすると、

のようにリストの要素を繰り返したリストを取得することができます。

ただ、ここで注意したいのが、各要素のオブジェクト(メモリ)を繰り返してリストが生成されています。

試しに各要素のIDも繰り返されています。

つまり、同じメモリの値が繰り返されている事になります。

そのため、例えば、リストのリスト(二次元のリスト)を作成し、1つの要素だけを変更してみると、下図のように他の要素まで変更されてしまいます。

しかし、一見同じように一次元のリストの繰り返しでは、他の要素は変更されません。

この差は何なのか?というと、Pythonの語彙力がなくてうまく説明できないのですが、値を代入した時に変数のIDが変わるか?変わらないか?の違いによって、差が出ます。

例えば、変数に値を代入すると、IDの値も変わります。

しかし、リストの要素に値を代入しても、リストの変数のIDは変わりません。

こういう事をなんと言うのか???

 

という事で、リストやクラスオブジェクトを * を使ってリストの繰り返しを作る場合は、メモリも同じメモリが繰り返されている事に注意しておかないと、1つの要素を変更したときに他の要素も変更されてしまうので、気を付けましょう!

 

と、今日、この症状のバグにハマっていたので、戒めでこの記事を書いています。

【Python】処理時間の計測

処理時間の計測には、timeモジュールのtime()関数 もしくは perf_counter()関数を時間計測する2か所で実行し、取得した値の差を計算することで、処理時間(秒単位)が取得できます。

 

サンプルプログラム

import time

print("time")
for j in range(5):
    start = time.time()
    time.sleep(1)
    print(time.time() - start)

print("perf_counter")
for j in range(5):
    start = time.perf_counter()
    time.sleep(1)
    print(time.perf_counter() - start)

実行結果

 

time()関数 と perf_counter()関数 とでは、perf_counter()関数の方が高精度らしいのですが、上記のプログラムでは、差がよく分からず。。

注意点

Visual Studio や Visual Studio Codeを使っている場合、デバッグの開始デバッグなしで開始の2種類の実行方法があります。

(Visual Studioの場合)

(Visual Studio Codeの場合)

 

デバッグのあり/なしで処理時間に大きな差が出るので、処理時間を計測する場合は、デバッグなしで実行を選択して実行してください。

 

例えば、以下のようなプログラムを実行し、処理時間を比較すると、

import time

sum = 0
start = time.perf_counter()

for i in range(10000000):
    sum += 1
    
print(time.perf_counter() - start)

処理時間(秒)

デバッグの開始 デバッグなしで実行
Visual Studio 5.0749325 0.8275141
Visual Studio Code 1.6109771 0.7664662999999999

 

Pythonはビルドするわけでは無いので、デバッグあり/なし は処理時間に関係ないと思っていたのですが、特にVisual Studioでは処理時間が大きく異なるので、注意が必要ですね。

【Python】画像データ(NumPy,Pillow(PIL))の相互変換

Pythonで画像処理をしていると、画像データの扱いは各ライブラリによって、NumPyのndarrayかPillowのPIL.Imageのどちらかになる場合が多いかと思います。

そこで NumPyとPillowの画像データの相互変換をまとめておきます。

 

NumPy -> Pillowへの変換

NumPy からPillowへの変換は Pillowの fromarray関数を用います。

from PIL import Image

pil_image = Image.fromarray(numpy_image)

Pillow -> NumPyへの変換

PillowからNumPyへの変換は NumPyの array関数を用います。

import numpy as np

numpy_image = np.array(pil_image)

array関数と似たものにasarray関数がありますが、このasarrayで変換されたNumPyの配列(ndarray)は読み取り専用となり、値の参照はできますが、値を設定することはできません。

import numpy as np

numpy_image = np.asarray(pil_image) # numpy_imageは読み取り専用となる

変換サンプル

NumPyとPillowの画像データを相互変換したサンプルを示します。

import numpy as np
from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_mono = Image.open("image_mono.bmp")
print(type(pil_image_mono))     # <class 'PIL.BmpImagePlugin.BmpImageFile'>
print(pil_image_mono.mode)      # L
print(pil_image_mono.size)      # (400, 300)

# Pillow でカラー画像を読み込む
pil_image_color = Image.open("image_color.bmp")
print(type(pil_image_color))    # <class 'PIL.BmpImagePlugin.BmpImageFile'>
print(pil_image_color.mode)     # RGB
print(pil_image_color.size)     # (400, 300)

# Pillow -> NumPyへ変換(モノクロ画像)
ndarray_mono = np.array(pil_image_mono)
print(type(ndarray_mono))       # <class 'numpy.ndarray'>
print(ndarray_mono.dtype)       # uint8
print(ndarray_mono.shape)       # (300, 400)

# Pillow -> NumPyへ変換(カラー画像)
ndarray_color = np.array(pil_image_color)
print(type(ndarray_color))      # <class 'numpy.ndarray'>
print(ndarray_color.dtype)      # uint8
print(ndarray_color.shape)      # (300, 400, 3)

# NumPy -> Pillowへ変換(モノクロ画像)
pil_image_mono = Image.fromarray(ndarray_mono)
print(type(pil_image_mono))     # <class 'PIL.Image.Image'>
print(pil_image_mono.mode)      # L
print(pil_image_mono.size)      # (400, 300)

# NumPy -> Pillowへ変換(カラー画像)
pil_image_color = Image.fromarray(ndarray_color)
print(type(pil_image_color))    # <class 'PIL.Image.Image'>
print(pil_image_color.mode)     # RGB
print(pil_image_color.size)     # (400, 300)

 

ここで注意しておきたいのが、

Pillowのモノクロ画像をNumPyへ変換したときは
[画像の高さ, 画像の幅]
の順の二次元配列となります。

Pillowのカラー画像をNumPyへ変換したときは
[画像の高さ, 画像の幅, 色(R, B, Gの順)]
の順の三次元配列となります。

NumPyのカラー画像をPillowへ変換する場合は、カラーデータの並びが R,G,B である必要があります。
OpenCVの画像データもNumPyのndarrayで扱われますが、OpenCVの場合、カラーデータの並びが
B,G,Rとなるため、OpenCVからPillowの画像データへ変換する場合は、cvtColor関数を使って、R,G,Bに変換しておく必要があります。

コード例

image_color = cv2.cvtColor(image_color, cv2.COLOR_BGR2RGB)

(参考)

matplotlibで画像データ(OpenCV,pillow,list)を表示する

【Python/NumPy】カラー画像データをRGBからBGRへ変換

【Python】画像データがNumPyかPillowか調べる方法

【OpenCV/Python】adaptiveThresholdの処理アルゴリズム

自動でしきい値を決めて二値化してくれる画像処理と言えば、大津の二値化ですが、OpenCVにはadaptiveThreshold(適応的しきい値処理)という良さげな処理があります。

この adaptiveThreshold は画像全体に影や照明のムラがある場合に、効果を発揮します。

 

以下に大津の二値化とadaptiveThreshold の処理例を示します。

 

使用したプログラム

import cv2

img = cv2.imread("image.jpg")
# カラー→モノクロ変換
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 元画像の表示
cv2.imshow("src image", img)

# 大津の二値化
_, dst1 = cv2.threshold(
    img, 0, 255, cv2.THRESH_OTSU)
cv2.imshow("THRESH_OTSU", dst1)

# 適応的しきい値処理
dst2 = cv2.adaptiveThreshold(
    img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY, 51, 20)
cv2.imshow("adaptiveThreshold", dst2)

cv2.waitKey(0)

元画像

大津の二値化

adaptiveThreshold

 

 

元画像の左側に影のある例を示しています。

今回の画像は、文字の部分を黒く、それ以外の部分を白く二値化することを想定しているのですが、大津の二値化では、自動でしきい値は決めてくれるものの、画像全体に輝度値のムラがある場合は、うまく二値化してくれません。それに比べて adaptiveThreshold ではある程度狙った通りに二値化されています。

 

Pythonですが、この関数定義は以下のようになっています。

adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C)
src 入力画像
maxValue 二値化後の輝度値
adaptiveMethod 適応的しきい値処理で使用するアルゴリズム
cv2.ADAPTIVE_THRESH_MEAN_C
もしくは
cv2.ADAPTIVE_THRESH_GAUSSIAN_C
thresholdType 二値化の種類
cv2.THRESH_BINARY
もしくは
cv2.THRESH_BINARY_INV
blockSize しきい値計算のための近傍サイズ
C 平均あるいは加重平均から引かれる値
戻り値 処理後画像

(参考)

http://opencv.jp/opencv-2svn/c/imgproc_miscellaneous_image_transformations.html

 

だいたい上記のような説明されている場合が多いのですが、よく分からないですよね?!

ただ、やっている事自体は意外と簡単です。

実際にOpenCV内部で行われている処理と異なると思いますが、処理の意味合い的には以下の通りになります。

 

1.adaptiveMethodの設定に従って、平均化(blur)もしくはガウシアンフィルタ(GaussianBlur)で入力画像をぼかします。この時のカーネルのサイズが blockSize x blockSize となります。

 

2.元画像とぼかした画像の差分処理を行います。

 

3.差分画像を指定したしきい値( C ) で二値化し、白黒反転します。

 

すると、adaptiveThreshold で処理した二値化画像が取得できます。
重要なのは、処理の途中に平均化 もしくは ガウシアンフィルタで二値化したい部分をぼかしている部分です。そのため、二値化したい部分の大きさ(今回の例では文字の線幅)に対して十分大きな blockSize を指定する必要があります。

blockSizeを変えながら処理をすると、

blockSize = 5 のとき

blockSize = 21 のとき

blockSize = 51 のとき

 

このようにblockSizeが小さいと、文字の輪郭が二値化され、blockSizeを大きくすると、太い文字も文字全体が二値化されます。

処理の目的的にはトップハットボトムハットに似ています。

(参考)

膨張・収縮・オープニング・クロージング

 

実際の用途的には、画像にムラがあるときに、小さなゴミやキズなどの検出に用いられます。
逆に大きな領域を二値化する場合には adaptiveThreshold は不向きなのでご注意下さい。

 

ちなみに買ったチョコビ