Pythonを勉強して、for文を覚えると何となくrangeを使っていましたが、rangeの構文をまとめておきたいと思います。
終了の値を指定する方法
range(終了の値未満)
実行結果
開始と終了の値を指定する方法
range(開始の値以上, 終了の値未満)
実行結果
開始と終了の値、ステップ数を指定する方法
range(開始の値以上, 終了の値未満, ステップ数)
実行結果
※rangeの値には整数のみ設定可能です。
Pythonを勉強して、for文を覚えると何となくrangeを使っていましたが、rangeの構文をまとめておきたいと思います。
range(終了の値未満)
実行結果
range(開始の値以上, 終了の値未満)
実行結果
range(開始の値以上, 終了の値未満, ステップ数)
実行結果
※rangeの値には整数のみ設定可能です。
画像のビット数(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を使って画像を表示すると、下図のように画像の画像の座標軸が表示され、右下にはマウスポインタの座標および、その位置の画像の輝度値が表示されるので便利です。
さらに矢印アイコンで、画像の移動、虫眼鏡アイコンで画像の領域を選択すると、その領域が拡大表示されます。
上図では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
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への変換)となっていました。
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のバージョンを確認するには、コマンドプロンプト(ターミナル)からは、
python --version
と入力して、確認します。
Pythonのコード中では sysモジュールを使って
import sys
print(sys.version)
とします。
Canvasに画像を表示する のページではtkinterでGUIを作り画像ビューアを作りましたが、これに アフィン変換 を追加し、画像の拡大/縮小、移動の出来る画像ビューアを作成しました。
機能は、Fileメニューから画像ファイルを開き、マウスホイールの上下で画像の拡大/縮小を行い、マウスの左ボタンのドラッグで画像を移動します。
左ボタンのダブルクリックで画像全体を表示します。
また、ウィンドウ下にはCanvas上のマウスポインタの座標と、マウスポインタ位置の画像の座標および、その輝度値を表示します。
ウィンドウの右下には画像ファイルの種類、画像サイズ、画像の種類を表示します。
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
つまみを動かして値を調整できる、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 | 全体の太さを指定します。 |
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では、基本的にビルドしないので、実行するまでエラーが分からないのですが、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つ。
内容を見てみると、
となります。
このエラーと警告の分類ですが、エラーに関しては修正すべき内容がほとんど、警告に関しては実行する分には無視できるかもしれない内容ですが、
flag == True
の部分が
flag = True
を意図していた場合は危険な警告なので、警告に関してもできるだけ修正しておいた方が良さそうです。
エラーコードに関しては、こちら↓が参考になると思います。
http://pylint-messages.wikidot.com/all-codes
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 | ● | |
● | ||
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ファイルに出力できるのは、ちょっと面白い。
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 アフィン変換」と検索すると、OpenCVを使った説明が多いような気がしますが、画像を表示するだけなら、Pillowにtransform()メソッドというのがあり、これもなかなか高機能な処理が可能になります。
Pillowならtkinterとの相性もいいので、GUIで画像をウィンドウに表示したいのなら、おススメです。
最終的には、以下のようにウィンドウに画像を表示するのが目標です。
Image.transform(size, method, data=None, resample=0, fill=1, fillcolor=None)
パラメータ | 説明 |
size | (幅, 高さ)のタプル |
method | 変換方法
|
data | methodにより内容が異なります。
|
resample | 画素補間方法
|
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()
(実行結果)
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/
アフィン変換では長方形を平行四辺形には変換できるものの、台形には変換できないと説明しましたが、任意四角形から任意四角形へ変換できるのがホモグラフィ変換となります。
実際には書類や名刺のような長方形の被写体を斜めから撮影した時に、上から撮影したような感じに長方形に見えるように変換するときにホモグラフィ変換が用いられることが多いです。
このホモグラフィ変換は、変換前の座標を(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 だった部分に g と h が登場しています。
このホモグラフィ変換の行列の計算は以下のようにします。
$$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
出来た!!
アフィン変換行列は、これまで移動量、スケール、回転角度からアフィン変換行列を求める方法を紹介してきました。
ただ、実際にはアフィン変換前の点とアフィン変換後の点の組み合わせからアフィン変換行列を求めたい場合もあるので、今回はその方法を紹介します。
アフィン変換行列は、こんな感じ↓です。
$$\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. ]]
となり、アフィン変換行列が求まります。
まず、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で画像を表示する例を次に示します。
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で画像を表示します。
これまで、ウィンドウの作成、ウィジェットの配置、Menu、Canvas、ファイルを開くダイアログボックスと説明してきましたが、これらを使って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()
https://docs.python.org/ja/3/library/tkinter.html?highlight=photoimage
winfo_width()、winfo_height()
を用います。
さらに画像の拡大・縮小・平行移動の機能を追加したプログラムについて、以下のページにまとめましたので、参照頂けると幸いです。
Pythonはインストールした環境ごとに使用できるバージョンやモジュール(パッケージ)を切り替えることができます。
Visual Studioでは、AnacondaやPythonのインストーラなどでインストールした環境の一覧が表示されています。このPython環境を切り替える方法を紹介します。
新規プロジェクトで使用するPython環境を変更しるには、変更したいPython環境を選択し、概要と表示されている状態で、その下のこれを新しいプロジェクトに対する規定の環境にするをクリックします。
すると、選択したPython環境の文字が太字に切り替わり、次にプロジェクトを作成する時のPython環境が切り替わります。
Python環境に グローバルデフォルト の環境を使用している場合、前項のようにこれを新しいプロジェクトに対する規定の環境にするをクリックすることで、Python環境を切り替える事ができます。
(変更後)Python3.6からPython3.7へ変更後
Python環境がグローバルデフォルトでない場合、プロジェクトを右クリックし、プロパティを選択します。
表示されたウィンドウの全般のインタープリターの中から、変更したいPython環境を選択する事で、Python環境を切り替える事ができます。
ただし、インタープリターの部分で選択を変更した直後では、Python環境の表示が切り替わらないため、どこか関係のないウィンドウを選択し、ウィンドウ表示を切り替える事で、Python環境の表示が切り替わるようになります。