【Python/Pillow(PIL)】画像の一部を切り抜く

Pillowで画像の一部を切り抜くには、Imageクラスのcropメソッドを用います。

書式は

Image.crop(box=None)
引数 説明
box 切り抜く領域を(左, 上, 右, 下)の座標のタプルで指定します。

 

(サンプルプログラム)

from PIL import Image

# 画像を開く
img = Image.open("Parrots.bmp")

# 画像を切り抜く
img_roi = img.crop((146, 81, 253, 183)) # (left, upper, right, lower)

# 切り抜いた画像の保存
img_roi.save("Parrots_roi.bmp")

(実行結果)

元画像 切り抜き画像

(補足)指定座標について

画像を切り抜く座標の定義ですが、画像の幅がWidth、画像の高さがHeightとすると、
画像の左上が(0, 0)、画像の右下が(Width-1, Height-1)となります。

実際に切り抜かれる画像の領域は、左上の座標(left, upper) を含み、右下の座標(right, lower) の1画素内側の領域が切り抜かれます。

参考

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

【Python/Pillow(PIL)】画像データの新規作成

画像データ(PIL.Image)を画像ファイルなどからではなく、新規に作成するには、Imageモジュールのnew関数を使います。

new関数の書式は以下の通り

PIL.Image.new(mode, size, color=0)
mode 画像のモードを設定します。
主なものとして、
“L”      8bitグレースケール
“RGB”  3x8bit カラー画像
詳細はこちらを参照ください。
size 画像のサイズを(幅, 高さ)のタプルで指定します。
color 画像全体のデータの色の値を指定します。
初期値は黒となります。
カラーの場合は、(R, G, B)のように各チャンネルごとの値のタプルで指定します。

 

グレースケール画像を作成するには、以下のようにします。

from PIL import Image

# グレースケールの画像データを作成
img = Image.new("L", (320, 240))
# 画像の表示
img.show()

(実行結果)

初期値を指定すると

# 輝度値を指定して画像データを作成
img = Image.new("L", (320, 240), 128)
# 画像の表示
img.show()

(実行結果)

カラー画像の場合は以下のようにします。

# カラー画像データを作成
img = Image.new("RGB", (320, 240), (0, 128, 255))
# 画像の表示
img.show()

(実行結果)

【Python/Pillow(PIL)】画像ファイルを開く,保存する

jpegやbmpなどの画像ファイルをPillowで開くには、Imageモジュールのopen関数を使います。

同様に画像をファイルに保存するにはsave関数を用います。

以下に、bmp形式の画像ファイルを開き、画像をカラーからモノクロのグレースケールに変換し、pngファイルに保存する例を示します。

from PIL import Image

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

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

# グレースケールへ変換
img_gray = img.convert("L")
img_gray.show()

# 画像のファイル保存
img_gray.save("image_gray.png")

(実行結果)

 

ファイル名に日本語も指定できる(OpenCVのimread関数は日本語が使えない)ので、使い勝手がいいと思います。

読込、保存のできる画像ファイルのフォーマット(bmp,jegなど)は別途こちら↓にまとめました。

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

 

画像ファイルの保存では、jpegファイルでは品質(quality)など、ファイルフォーマットごとに指定できるオプションがあるので、詳細はこちらのページ↓を参照ください。

【Python/Pillow(PIL)】JPEG画像の品質を指定して保存する

【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/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】画像データ(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か調べる方法

【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

【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/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/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/

参考ページ