【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

【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)】画像のヒストグラム取得、表示

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

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

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

# 画像の表示
img.show()

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

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

(実行結果)

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

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

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

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

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

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

# 画像の表示
img.show()

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

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

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

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

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

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

plt.show()

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

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

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

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

import numpy as np

flag = False

flag == True

a = a + 1;

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

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

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

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

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

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

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

内容を見てみると、

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

となります。

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

flag == True

の部分が

flag = True

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

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

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

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

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

from PIL import Image

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

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

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

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

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

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

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

  BUFR, FITS, GRIB, HDF5, MPEG

(参考)

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

 

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

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

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

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

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

 

(サンプルプログラム)

from PIL import Image  # 画像データ用

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

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

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

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

(実行結果)

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

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

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

参考記事

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

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

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

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

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

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

transformメソッドの構文

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

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

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

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

 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(実行結果)

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

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

 

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

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

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

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

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

から、逆行列の部分を

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

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

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

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

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

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

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

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

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

参考ページ

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

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

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

 

 

 

 

 

 

 

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

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

となります。

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

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

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

を展開し、

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

より

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

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

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

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

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

の式を変形して、

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

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

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

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

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

となります。

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

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

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

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

すると、

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

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

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

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

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

変換前 → 変換後

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

import numpy as np

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

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

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

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

print(homography)

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

(実行結果)

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

出来た!!

参考

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

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

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

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

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

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

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

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

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

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

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

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

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

より、逆行列を用いて、

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

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

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

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

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

このアフィン変換行列は

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

で求まります。

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

import numpy as np

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

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

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

print(affine)

(実行結果)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

となります。

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

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

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

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

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

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

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

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

import numpy as np

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

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

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

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

print(affine)

(実行結果)

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

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

参考

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

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

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

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

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

import tkinter as tk
from PIL import ImageTk

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

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

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

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

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

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

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

(実行結果)

プログラムの説明

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

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

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

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

import tkinter as tk
from PIL import Image, ImageTk

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

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

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

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

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

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

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

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

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

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

応用編

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

 

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

(プログラム)

import tkinter as tk
from tkinter import filedialog

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ポイント

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

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

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

 

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

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

関連ページ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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