【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/Pillow(PIL)】画像の輝度値の取得/設定

画像を開き輝度値を取得/設定するのは、画像処理を行う、はじめの一歩的な処理ですよね。

まずは、最も基本的なgetpixel/putpixelを使った方法を紹介します。

getpixel()を使った輝度値の取得

getpixel()の構文は以下の通りです。

value = Image.getpixel(xy)

xyは画像の左上を原点とするxy座標で、(x, y)のようにタプルで指定します。

戻り値が指定した画像の輝度値となり、モノクロ画像の場合は、指定した座標の輝度値が戻り、カラーの場合は(r, g, b)のように3つの要素のタプルが戻ってきます。カラー画像でも ‘RGBA’ のように透過付きの画像データの場合は(r, g, b, a)のように4つの要素のタプルが戻ります。

実行例

putpixel()を使った輝度値の設定

putpixel()の構文は以下の通りです。

Image.putpixel(xy, value)

引数はgetpixel()と同じ用に、xyには(x, y)のように座標をタプルを指定します。valueの部分には、モノクロの場合は、指定座標の輝度値を、カラーの場合は(r, g, b)もしくは(r, g, b, a)のように輝度値をタプルで指定します。

さらに輝度値の値に0~255の範囲を超えて指定した場合、値が負の場合は0、値が256以上の場合は255に修正されます。

実行例

処理の高速化の検討

C#でも似た関数は処理時間が遅いで有名でしたが、getpixel()、putpixel()も処理時間が遅いらしい。

そこでいくつかの輝度値の取得/設定方法を試して処理時間の比較を行ってみたいと思います。

 

まずは処理時間の基準となるgetpixel(), putpixel() を使った処理時間を計測します。

輝度値を取得し、コントラストを調整し、輝度値を画像に設定するサンプルです。

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

上記プログラムを実行すると、以下のようになります。

 

処理前画像

処理後画像

 

この他に以下の方法を試してみます。

  • getdata(), putdata() を使った方法
    getdata()は画像全体の輝度値を画像の左上から順に各画素の輝度値(R, G, B)の値がタプルの一次元のリストで取得します。
    putdata()は輝度値(R, G, B)のタプルの一次元のリストを指定し、画像全体の輝度値を設定します。
  • numpy を使った方法
    Pillowの画像データからNumPyの画像データへ変換し、NumPyデータを処理し、Pillowの画像データに戻しています。
  • Pillow <-> numpy の相互変換だけの時間
    PillowとNumPyの画像データの変換時間を参考に計測します。
  • point() を使った方法
    輝度値の取得/設定の処理時間の評価の趣旨から外れますが、画像処理に周辺画素の輝度値を用いない場合、LUT(Look Up Table)を用いると高速に処理が行えるため、Pillowのpoint()メソッドでLUT変換を行った処理時間を参考に計測しています。

 

使用した全プログラム

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# getdata(), putdata() を使った方法
img = img_original.copy()
start = time.perf_counter()

data = img.getdata()
# 処理後のデータをlistで確保
data_dst = [None] * len(data)

for y in range(height):
    for x in range(width):
        r, g, b = data[x + y * width]
        data_dst[x + y * width] = (r * 5 - 500, g * 5 - 500, b * 5 - 500)

img.putdata(data_dst)

print("getdata(), putdata() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpy を使った方法
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

for y in range(height):
    for x in range(width):
        r = numpy_iamge[y, x, 0]
        g = numpy_iamge[y, x, 1]
        b = numpy_iamge[y, x, 2]
        r = r * 5 - 500
        g = g * 5 - 500
        b = b * 5 - 500
        if r < 0:
            r = 0
        if g < 0:
            g = 0
        if b < 0: b = 0 if r > 255:
            r = 255
        if g > 255:
            g = 255
        if b > 255:
            b = 255
        numpy_iamge[y, x, 0] = r
        numpy_iamge[y, x, 1] = g
        numpy_iamge[y, x, 2] = b

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpy を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その1
# numpyの配列(ndarray)をそのまま演算する
# 0~255に制限するのにclipを用いる
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換(計算後、負になるのでint32型へ変換)
numpy_iamge = np.array(img, dtype = np.int32)

dst_img = numpy_iamge * 5 - 500

# 0~255のuint8型へ変換
dst_img = dst_img.clip(0, 255).astype(np.uint8)

# numpy → pillowへ変換
img = Image.fromarray(dst_img)

print("numpyらしい処理 その1\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その2
# LUTを使った変換
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

# LUT(Look Up Table)の作成
lut = np.empty(256, dtype = np.uint8)
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0
    if val > 255:
        val = 255
    lut[i] = val

# LUTを介して変換
numpy_iamge = lut[numpy_iamge]

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpyらしい処理 その2\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# Pillow ⇔ numpy の相互変換だけの時間
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)
# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("Pillow ⇔ numpy の相互変換だけの時間\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# point() を使った方法
img = img_original.copy()
start = time.perf_counter()

# LUT(Look Up Table)の作成
lut = []
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0 
    if val > 255:
        val = 255
    lut.append(val)
# R, G, Bに同じLUTを使用
lut = lut * 3

# pointメソッドでLUT変換を行う
img = img.point(lut)

print("point() を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

処理時間の比較(使用した画像は256×256の24bitカラー画像)

方法 処理時間
getpixel(), putpixel() を使った方法 99.223 msec
getdata(), putdata() を使った方法 37.833 msec
numpy を使った方法 440.229 msec
(参考)numpyらしい処理 その1 2.081 msec
(参考)numpyらしい処理 その2 1.041 msec
(参考)Pillow <-> numpy の相互変換だけの時間 0.254 msec
(参考)point() を使った方法 0.156 msec

まとめ

輝度値の取得/設定を行う処理については、getdata(), putdata() を使った方法が一番速い結果となりました。

numpyを使うと、もう少し速いかと思っていたのですが、あまりに遅かったので、Pillow <-> numpy の相互変換の処理時間を計測してみましたが、やはり画像処理している部分が遅い事が分かりました。
numpyの処理だけ、0~255に輝度値が入るようにif文で処理をしていますが、これは、numpyのデータがuint8(8bitの符号なし整数)になるため、この処理を入れないと、下図のように変な画像になってしまいます。

逆に、getpixel(), putpixel() も getdata(), putdata() も、輝度値に0~255の範囲外の値を指定しても0~255の範囲に調整してくれるので、これは便利です。

ただし、numpyに画像データを変換すると、OpenCVも使えるので、numpyで画像処理するなら、使える処理があれば極力OpenCVを使うようにするとよいでしょうね。
numpyのデータはfor文で値を参照すると、どうしても遅いようです。

また、参考にpoint()メソッドによりLUTを使った処理時間を計測してみましたが、こちらは爆速でした!

今回はpoint()メソッドを使いましたが、他にもPillowでできる画像処理のメソッドが用意されているので、おいおい紹介したいと思います。

結局、Pythonでベタな画像処理をしてはいけないということですね。
OpenCVなどに無いオリジナルの画像処理をしたい場合は、やっぱりC言語のライブラリで処理を行う必要があるんでしょうね。

【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 は不向きなのでご注意下さい。

 

ちなみに買ったチョコビ

【Python】rangeの構文(開始,終了,ステップ数)

Pythonを勉強して、for文を覚えると何となくrangeを使っていましたが、rangeの構文をまとめておきたいと思います。

 

終了の値を指定する方法

range(終了の値未満)

実行結果

開始と終了の値を指定する方法

range(開始の値以上, 終了の値未満)

実行結果

開始と終了の値、ステップ数を指定する方法

range(開始の値以上, 終了の値未満, ステップ数)

実行結果

 

※rangeの値には整数のみ設定可能です。

【Python/Pillow(PIL)】画像のビット数、チャンネル数を調べる

画像のビット数(8や24など)やチャンネル数(色の数、Lの場合は1、RGBの場合は3など)は画像処理をするときに、画像データを直接参照する場合などに必要になってきます。

jpegファイル(*.jpg)を開いたときには bits という値が拾え、1画素、1色あたり8bitであることがわかります。

しかしながら、他の形式のファイル(少なくとも bmp, pmg, gif)では、この bits の値がありません。

 

そこで、どのファイルでも画像のビット数やチャンネル数を調べられるようにするには、mode の値を調べるようにします。
modeの取得は、以下のように行います。

from PIL import Image

# Pillow で画像を読み込む
pil_image = Image.open("image_color.bmp")
# modeの表示
print("モード:", pil_image.mode)

モードの種類は以下の通りです。

mode 説明
1 1-bit pixels, black and white, stored with one pixel per byte
L 8-bit pixels, black and white
P 8-bit pixels, mapped to any other mode using a color palette
RGB 3×8-bit pixels, true color
RGBA 4×8-bit pixels, true color with transparency mask
CMYK 4×8-bit pixels, color separation
YCbCr 3×8-bit pixels, color video format
LAB 3×8-bit pixels, the L*a*b color space
HSV 3×8-bit pixels, Hue, Saturation, Value color space
I 32-bit signed integer pixels
F 32-bit floating point pixels

(参考)

https://pillow.readthedocs.io/en/stable/handbook/concepts.html

 

画像のビット数やチャンネル数を調べるのに、この mode を取得して、条件分岐でビット数やチャンネル数を上記の表から取得すると正確に求まります。

ただ、実際に使われるのは L, RGB, RGBA の3つぐらいなので、1画素8bit限定として考えると、チャンネル数が求まればビット数も求まります。

チャンネル数を直接取得できる方法は無さそうなので、 getbands() というメソッドを使って行います。

この getbands() は、例えば mode = ‘RGB’ のとき、各チャンネルの色の名前の(‘R’, ‘G’, ‘B’)というタプルを返すメソッドになります。そのため、このタプルの長さを取得すればチャンネル数も求まります。

 

サンプルプログラム

from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_mono = Image.open("image_mono.bmp")
print("■■ モノクロ画像 ■■")
print("モード:\t\t", pil_image_mono.mode)
print("バンド:\t\t", pil_image_mono.getbands())
print("チャンネル数:\t", len(pil_image_mono.getbands()))

# Pillow でカラー画像を読み込む
pil_image_color = Image.open("image_color.bmp")
print("■■ カラー画像 ■■")
print("モード:\t\t", pil_image_color.mode)
print("バンド:\t\t", pil_image_color.getbands())
print("チャンネル数:\t", len(pil_image_color.getbands()))

実行結果

 

参考

https://pillow.readthedocs.io/en/stable/reference/Image.html?highlight=getbands#PIL.Image.Image.getbands

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

matplotlibを使って画像を表示すると、下図のように画像の画像の座標軸が表示され、右下にはマウスポインタの座標および、その位置の画像の輝度値が表示されるので便利です。

さらに矢印アイコンで、画像の移動、虫眼鏡アイコンで画像の領域を選択すると、その領域が拡大表示されます。

 

Pillow画像の表示

上図ではPillowで画像を開いて、matplotlibで表示しているのですが、そのプログラムは以下の通りです。

import matplotlib.pyplot as plt
from PIL import Image

# Pillow でカラー画像を読み込む
pil_image_color = Image.open("Parrots.bmp")
# matplotlibで表示
plt.imshow(pil_image_color)    
plt.show()

ただし、モノクロ画像を以下のようなプログラムで画像を表示すると、変な色合いになります。

import matplotlib.pyplot as plt
from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_color = Image.open("Text.bmp")
# matplotlibで表示
plt.imshow(pil_image_color)    
plt.show()

 

モノクロのグレースケールで表示するには、imshowの引数にグレースケールの cmap(カラーマップ、カラーパレット) “gray” を指定して、以下のようにします。

import matplotlib.pyplot as plt
from PIL import Image

# Pillow でモノクロ画像を読み込む
pil_image_color = Image.open("Text.bmp", cmap = "gray")
# matplotlibで表示
plt.imshow(pil_image_color)    
plt.show()

 

このカラーマップ(cmap)にはデフォルトで “viridis” というものが設定されていて、下図のような色味になっています。

 

カラーマップは他にも hsv や rainbow, jet など様々用意されており、例えば、rainbowを使うと、下図のようになります。

 

カラーマップは色々用意されているので、モノクロ画像を疑似カラーで表示したい場合は、gray以外のものを使用するのも便利だと思います。

カラーマップの詳細については、以下のページで確認できます。

https://matplotlib.org/stable/gallery/color/colormap_reference.html

 

OpenCV画像の表示

matplotlibのimshow関数に指定する画像データはPillowの画像以外にも、OpenCVの画像データ(実態はnumpyのndarray)も指定可能です。

以下のようなプログラムを実行すると

import matplotlib.pyplot as plt

import cv2

# OpenCVでモノクロ画像を読み込む
cv_image_mono = cv2.imread("Text.bmp", cv2.IMREAD_UNCHANGED)
print(type(cv_image_mono))
print(cv_image_mono.shape)
# matplotlibで表示
plt.imshow(cv_image_mono, cmap = "gray")    
plt.show()

下図のようにモノクロ画像が表示されます。

さらにコマンドライン上には、OpenCVで読み込んだ画像の型が <class ‘numpy.ndarray>と表示されていることが確認でき、OpenCVの画像データがnumpyのndarrayそのものだという事がわかります。

画像データのサイズを shapeで表示していますが、モノクロ画像データの時は、二次元配列となっています。

 

同様にして、OpenCVのカラー画像を表示すると、

import matplotlib.pyplot as plt

import cv2

# OpenCVでカラー画像を読み込む
cv_image_color = cv2.imread("Parrots.bmp", cv2.IMREAD_UNCHANGED)
print(type(cv_image_color))
print(cv_image_color.shape)
# matplotlibで表示
plt.imshow(cv_image_color)    
plt.show()

プログラムの実行結果は下図のようになります。

OpenCVのカラー画像をmatplotlibで表示すると、モノクロの時のように、また、変な色になってしまいます。

これは、OpenCVのカラー画像のデータの並びが B, G, R, B, G, R ・・・となっているのですが、matplotlibへは R, G, B, R, G, B ・・・の並びのデータで渡す必要があり、OpenCVのカラー画像をmatplotlibで表示するには、RGBのデータの並びを逆にする必要があります。

また、 shapeの結果が (256, 256, 3) と表示されているように、カラー画像の場合、三次元配列である事が確認できます。

 

BGRのデータをRGBのデータへ変換するには、OpenCVのcvtColor関数を使い

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

のように、2番目の引数にcv2.COLOR_BGR2RGBを指定し、RGBの並びを逆にします。

import matplotlib.pyplot as plt

import cv2

# OpenCVでカラー画像を読み込む
cv_image_color = cv2.imread("Parrots.bmp", cv2.IMREAD_UNCHANGED)

# BGR から RGB へ変換
cv_image_color = cv2.cvtColor(cv_image_color, cv2.COLOR_BGR2RGB)
#cv_image_color = cv_image_color[:, :, ::-1] # numpyのndarrayに対してスライスを使って反転
# matplotlibで表示
plt.imshow(cv_image_color, cmap = "gray")    
plt.show()

 

OpenCVの画像データはnumpyのndarrayなので、スライスを使って、BGRからRGBへ変換することも可能です。

cv_image_color = cv_image_color[:, :, ::-1]

ただし、この場合、カラー画像データが32bitの場合、OpenCVのカラーデータの並びはB, G, R, A となっているため、このまま並びを反転すると A, R, G, B となり、matplotlibで表示すると、変な色になってしまいます。

32bitのカラー画像をmatplotlibへ渡すには、R, G, B, A の順になっている必要があるため、numpyで並びを逆にしたい場合は

cv_image_color = cv_image_color[:, :, [2, 1, 0, 3]]

とする必要があり面倒なので、OpenCVを使っている場合、cvtColorを使った方がよさそうです。

ちなみに、OpenCVで32bitのカラー画像の並びを反転させるには、本来であれば、

cv_image_color = cv2.cvtColor(cv_image_color, cv2.COLOR_BGRA2RGBA)

もしくは

cv_image_color = cv2.cvtColor(cv_image_color, cv2.COLOR_BGRA2RGB)

とすべきなのですが、

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

としても、 cv2.COLOR_BGRA2RGBと同じ動き(32bitBGRAから24bitRGBへの変換)となっていました。

 

listデータの表示

matplotlibのimshowで表示する画像データは、ここ↓のページ

https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imshow.html

を見ても分かるように、配列かPIL Imageを渡す事ができます。

この配列の値は 0~1のfloatの値か、もしくは 0~255のintの値であれば、表示することが可能です。

という事は、Deep Learningでは画像データの0~255を0~1へ変換することが多いので、便利そうです。

また、配列はnumpyのndarrayだけでなく、Pythonのlistも渡す事ができ、以下にサンプルを示します。

import matplotlib.pyplot as plt

list_data = [list(range(256)) for i in range(256)]
plt.imshow(list_data, cmap = "gray")    
plt.show()

 

参考

https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imshow.html

https://matplotlib.org/stable/gallery/color/colormap_reference.html

【PyTorch】Visual Studioでインストールする方法

私はC#をメインで使っているので、Pythonのプログラムを組む時も慣れたVisual Studioで行っています。

さらに、Visual Studio 2019であればPythonの環境(バージョン)ごとにインストールするのも簡単なのですが、PyTorchをインストールするときは、少々、ハマったので、そのメモです。。

 

PythonモジュールのインストールはPython環境で、インストールしたい環境(バージョン)を選択し、 PyPIとインストールされたパッケージの検索 の部分に torch と入力して、Enterを押せば、インストールできるかも?知れないのですが、インストールされるPyTorchのバージョンやGPUの対応などに不安があったので、別の方法でインストールを行いました。

 

まず、PyTorchのページへ移動し、少し下に表示されているバージョンやOSの選択表示部分で、自分のインストールしたい環境を選択します。

私の場合は以下のように選択しました。

 

すると、Run this Commandの部分に

 

pip3 install torch==1.8.1+cu111 torchvision==0.9.1+cu111 torchaudio===0.8.1 -f https://download.pytorch.org/whl/torch_stable.html

 

と表示されているので、この torch以降の部分を選択し、コピー(Ctrl+C)します。

コピーしたテキストを、 PyPIとインストールされたパッケージの検索 の部分に貼り付け(Ctrl+V)ると、 次のコマンドを実行する のリンクが表示されるので、この部分をクリックすると、PyTorchのインストールが始まります。

 

 

これで、特に問題が無かった人は以下のように torch, torchaudio, torchvision の3つがインストールされると思います。

 

しかし、私はそう簡単にはいきませんでした。。

 

そもそも自分のPCにインストールされているCUDAのバージョンがわからなかったのですが、CUDAのバージョンを調べるにはコマンドプロンプトから、

nvcc -V

と入力すると、CUDAのバージョンを調べることができます。

 

私の場合、CUDAのバージョンが古かったので、CUDAもインストールしました。

(参考)

CUDAの入手、ダウンロード、インストール方法

 

さらに、pip install 中に、このような↓メッセージが表示されたのですが、

 

ここで今すぐ昇格をクリックして、インストールを続けても、下記のようなエラーが表示されて、インストールできませんでした。

----- 'torch==1.8.1+cu111 torchvision==0.9.1+cu111 torchaudio===0.8.1 -f https://download.pytorch.org/whl/torch_stable.html' をインストールしています -----
Looking in links: https://download.pytorch.org/whl/torch_stable.html
Collecting torch==1.8.1+cu111
  Using cached https://download.pytorch.org/whl/cu111/torch-1.8.1%2Bcu111-cp39-cp39-win_amd64.whl (3055.6 MB)
Collecting torchvision==0.9.1+cu111
  Using cached https://download.pytorch.org/whl/cu111/torchvision-0.9.1%2Bcu111-cp39-cp39-win_amd64.whl (1.9 MB)
Collecting torchaudio===0.8.1
  Using cached torchaudio-0.8.1-cp39-none-win_amd64.whl (109 kB)
Requirement already satisfied: typing-extensions in c:\users\akira\appdata\local\packages\pythonsoftwarefoundation.python.3.9_qbz5n2kfra8p0\localcache\local-packages\python39\site-packages (from torch==1.8.1+cu111) (3.7.4.3)
Requirement already satisfied: numpy in c:\users\akira\appdata\local\packages\pythonsoftwarefoundation.python.3.9_qbz5n2kfra8p0\localcache\local-packages\python39\site-packages (from torch==1.8.1+cu111) (1.20.2)
Requirement already satisfied: pillow>=4.1.1 in c:\users\akira\appdata\local\packages\pythonsoftwarefoundation.python.3.9_qbz5n2kfra8p0\localcache\local-packages\python39\site-packages (from torchvision==0.9.1+cu111) (8.2.0)
Installing collected packages: torch, torchvision, torchaudio
ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory: 'C:\\Users\\akira\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python39\\site-packages\\caffe2\\python\\serialized_test\\data\\operator_test\\collect_and_distribute_fpn_rpn_proposals_op_test.test_collect_and_dist.zip'
----- 'torch==1.8.1+cu111 torchvision==0.9.1+cu111 torchaudio===0.8.1 -f https://download.pytorch.org/whl/torch_stable.html' をインストールできませんでした -----

このエラーの

ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory:

の部分がいまいち意味が分からず、インストール中に管理者権限のメッセージが表示されていたので、インストール先のフォルダの権限が悪いのか?と思ったのですが、フォルダの権限の変更方法も分からず。。

結局、Pythonをアンインストールして、再インストールすることで、PyTorchもインストールできました。

 

ちなみに、Pythonのインストールは、下記のページ

https://www.python.org/downloads/

より、Windowsの場合であれば Download Python X.X.X の部分をクリックし、ファイルをダウンロードし、ファイルをダブルクリックしてインストールします。

インストール時に Add Python 3.9 to PATH にチェックを入れ  Customize installation をクリックしてインストールします。

次に表示されたウィンドウでインストール先のディレクトリが指定できるので、アクセス制限のなさそうな場所を指定して、 install をクリックします。

最終的に以下のようなウィンドウが表示されれば、Pythonのインストールは完了です。

ここまでやって、無事、PyTorchをインストールすることが出来ました!!

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

Canvasに画像を表示する のページではtkinterでGUIを作り画像ビューアを作りましたが、これに アフィン変換 を追加し、画像の拡大/縮小、移動の出来る画像ビューアを作成しました。

機能は、Fileメニューから画像ファイルを開き、マウスホイールの上下で画像の拡大/縮小を行い、マウスの左ボタンのドラッグで画像を移動します。
左ボタンのダブルクリックで画像全体を表示します。
また、ウィンドウ下にはCanvas上のマウスポインタの座標と、マウスポインタ位置の画像の座標および、その輝度値を表示します。
ウィンドウの右下には画像ファイルの種類、画像サイズ、画像の種類を表示します。

全ソースコード

参考

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

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

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

【Python/Pillow(PIL)】カラーパレットの設定(インデックスカラー)

PythonのPillowでモノクロ画像ファイルを開くと、Imageクラスの mode は “L” となりますが、これはカラーパレットを持たない画像データとなります。

C言語やC#ではモノクロ画像データを表示するときは、カラーパレットを参照して表示してモニタ上に画像を表示するインデックスカラーという仕組みがありました。

このインデックスカラーですが、モノクロ画像データの場合は 0~255 の輝度値を持ちますが、実際にモニタ上に表示する場合は、 0~255 のインデックスを持つカラーパレットのR,G,Bの値を参照して、モニタに表示されています。

index R G B
0 0 0 0
1 1 1 1
2 2 2 2
3 3 3 3
: : : :
254 254 254 254
255 255 255 255

モノクロ画像の場合、通常、indexの値とR,G,Bの値を同じ値にして表示するのですが、このR,G,Bの値を変更する事で、モノクロ画像に擬似的に色を付けて表示する事が可能になります。

Pillowでモノクロ画像にカラーパレットを使うには putpalette関数を用います。

構文

Image.putpalette(data, rawmode='RGB')

(参考)

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.putpalette

dataの部分にカラーパレットを指定しますが、これはリストで、

[R0, G0, B0, R1, G1, B1, R2, G2, B2, R3, G3, B3, … R255, G255, B255]

の順で、256×3個(768個)の一次元の配列で指定します。

サンプルプログラム

from PIL import Image

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

palette = []

for i in range(0, 64):
    palette.append(i) # R
    palette.append(i) # G
    palette.append(i) # B

for i in range(64, 128):
    palette.append(0) # R
    palette.append(0) # G
    palette.append(255) # B

for i in range(128, 192):
    palette.append(0) # R
    palette.append(255) # G
    palette.append(0) # B

for i in range(192, 256):
    palette.append(255) # R
    palette.append(0) # G
    palette.append(0) # B

img.putpalette(palette)

# 画像の表示
img.show()

(実行結果)

実際にはカラーパレットは、モノクロカメラに擬似的に色を付けて輝度分布を見やすくする疑似カラー表示を行う場合だったり、二値化を行う際のプレビュー用として私は使っています。

二値化のプレビュー用のプログラムを以下に示します。

from PIL import Image, ImageTk, ImageOps
import tkinter as tk

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

        self.master.title("カラーパレット")     # ウィンドウタイトル

        self.src_img = Image.open("./Mandrill.bmp")

        #---------------------------------------------------------------
        # Canvasの作成
        self.canvas = tk.Canvas(self.master, bg = "#008B8B")
        # Canvasを配置
        self.canvas.pack(expand = True, fill = tk.BOTH)   
        
        #---------------------------------------------------------------
        # Scaleの作成
        self.scale_var = tk.IntVar()
        scaleH = tk.Scale( self.master, 
                    variable = self.scale_var, 
                    command = self.slider_scroll,
                    orient=tk.HORIZONTAL,   # 配置の向き、水平(HORIZONTAL)、垂直(VERTICAL)
                    from_ = 0,            # 最小値(開始の値)
                    to = 256,               # 最大値(終了の値)
                    tickinterval=64         # 目盛りの分解能(初期値0で表示なし)
                    )
        scaleH.pack(fill = tk.X)
        #---------------------------------------------------------------

    def slider_scroll(self, event=None):
        '''スライダーを移動したとき'''
        self.disp_image(self.src_img, self.scale_var.get())

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

        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()

        #---------------------------------------------------------------
        # カラーパレットの設定
        palette = []

        for i in range(0, threshold):
            palette.append(i) # R
            palette.append(i) # G
            palette.append(i) # B

        for i in range(threshold, 256):
            palette.append(255) # R
            palette.append(0)   # G
            palette.append(0)   # B
        
        # カラーパレットの設定
        image.putpalette(palette)

        #---------------------------------------------------------------
        #PIL.ImageからPhotoImageへ変換する
        self.photo_image = ImageTk.PhotoImage(image=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()

(実行結果)

まとめ

PythonのPillowを使っていると、モノクロ画像のカラーパレットを意識する事は少ない気がしますが、カラーパレットを使わずにモノクロ画像に擬似的に色を付けようとすると、モノクロ8bitのデータをカラーの24bitに変換して、輝度値に合わせてR,G,Bの値を画像の全画素に対して変換を行う必要がありますが、カラーパレットを使うと、たかだか768個のデータを指定するだけなので、簡単に色を付ける事が可能になります。

ただし、このカラーパレットを指定できるのは、8bitのモノクロ画像(インデックスカラーも含む)のみとなります。

参考記事

https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.putpalette

疑似カラー(Pseudo-color)

【C#】Bitmapのカラーパレットの設定

【Python/tkinter】Scale(トラックバー、スライダー)

つまみを動かして値を調整できる、C#でいうところのトラックバーは、tkiterでは Scale といいます。

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

import tkinter as tk

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

        self.master.title("Scaleの作成")     # ウィンドウタイトル

        #---------------------------------------------------------------
        # Scaleの作成

        # Scale(デフォルトで作成)
        scaleV = tk.Scale( self.master)
        scaleV.pack(side = tk.RIGHT)

        # Scale(オプションをいくつか設定)
        self.scale_var = tk.DoubleVar()
        scaleH = tk.Scale( self.master, 
                    variable = self.scale_var, 
                    command = self.slider_scroll,
                    orient=tk.HORIZONTAL,   # 配置の向き、水平(HORIZONTAL)、垂直(VERTICAL)
                    length = 300,           # 全体の長さ
                    width = 20,             # 全体の太さ
                    sliderlength = 20,      # スライダー(つまみ)の幅
                    from_ = 100,            # 最小値(開始の値)
                    to = 300,               # 最大値(終了の値)
                    resolution=0.5,         # 変化の分解能(初期値:1)
                    tickinterval=50         # 目盛りの分解能(初期値0で表示なし)
                    )
        scaleH.pack()
        #---------------------------------------------------------------

    def slider_scroll(self, event=None):
        '''スライダーを移動したとき'''
        print(str(self.scale_var.get()))

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

オプション

オプション名 説明
activebackground マウスカーソルがスケール(つまみ)上にあるときのスケールの色を指定します。
background 背景色(値、メモリの文字、スケール(つまみ)の背景色)を指定します。
bd スライダー部の枠線の太さを指定します。
【初期値】2
bg 背景色(値、メモリの文字、スケール(つまみ)の背景色)を指定します。
borderwidth スライダー部の枠線の太さを指定します。
【初期値】2
command 値が変更された時に呼び出されるメソッド名を指定します。
cursor ウィジェット上にマウスポインタがある際のカーソルの種類を指定します。
digits 値、メモリに表示される値の桁数を指定します。
fg フォントの文字色を設定します。
font フォントを指定します。
foreground フォントの文字色を設定します。
from_ 最小値(開始の値)を指定します。
highlightbackground
highlightcolor
highlightthickness
label スライダーの左上(水平方向に配置のとき)もしくは右上(垂直方向に配置のとき)に表示する文字(ラベル)を指定します。
length 全体の長さを指定します。
orient Scaleの配置の向き(縦(tk.VERTICAL)か横(tk.HORIZONTAL))を指定します。
【初期値】tk.HORIZONTAL
relief Scale全体の枠線のスタイルを設定します。
【設定値】tk.RAISED, tk.GROOVE, tk.SUNKEN, tk.RIDGE, tk.FLAT
repeatdelay
repeatinterval
resolution つまみを移動した時の変化の分解能を指定します。
【初期値】1
showvalue スライダーの値を表示する(True) /表示しない(False)を指定します。
sliderlength スライダー(つまみ)の幅
sliderrelief
state state = tk.DISABLEDを指定するとScaleが無効(変更できない)になります。
takefocus
tickinterval 目盛りの文字の分解能を指定します。
【初期値】1
to 最大値(終了の値)を指定します。
troughcolor スライダが動く領域(つまみの背景)の色を指定します。
variable Scaleの値をIntVar,DoubleVar,StringVarクラスオブジェクトで指定します。
width 全体の太さを指定します。

【Python/Pillow(PIL)】画像データフォーマット(mode)

画像データにはカラー画像の24bit,32bit、モノクロ画像の8bit,16bitなどがありますが、Pillowで対応している画像データフォーマット(mode)の一覧は以下の通りです。

mode 説明
1 1-bit pixels, black and white, stored with one pixel per byte
L 8-bit pixels, black and white
P 8-bit pixels, mapped to any other mode using a color palette
RGB 3×8-bit pixels, true color
RGBA 4×8-bit pixels, true color with transparency mask
CMYK 4×8-bit pixels, color separation
YCbCr 3×8-bit pixels, color video format
LAB 3×8-bit pixels, the L*a*b color space
HSV 3×8-bit pixels, Hue, Saturation, Value color space
I 32-bit signed integer pixels
F 32-bit floating point pixels

また、機能が限定的となりますが、以下のものも用意されています。

LA L with alpha
PA P with alpha
RGBX true color with padding
RGBa true color with premultiplied alpha
La L with premultiplied alpha
I;16 16-bit unsigned integer pixels
I;16L 16-bit little endian unsigned integer pixels
I;16B 16-bit big endian unsigned integer pixels
I;16N 16-bit native endian unsigned integer pixels
BGR;15 15-bit reversed true colour
BGR;16 16-bit reversed true colour
BGR;24 24-bit reversed true colour
BGR;32 32-bit reversed true colour

(参考)

https://pillow.readthedocs.io/en/stable/handbook/concepts.html

一般的に用いられるのは、カラー24bitの RGB とモノクロ(グレースケール)8bitの L が多いと思います。

C言語やC#などでは、モノクロ画像の場合はカラーパレットを参照して表示するインデックスドカラーというのが標準的だったのですが、このインデックスドカラーに相当するのは P となります。

使用例

from PIL import Image

# PIL.Imageで画像を開く
img = Image.open("./Parrots.bmp")
print(img.mode)
img.show()  # 画像の表示

# カラー→モノクロ変換
gray = img.convert("L")
gray.show()  # 画像の表示

(実行結果)