numpyとOpenCVの座標定義

OpenCVを使って画像操作をする際にいつも混乱するので、改めて整理しました。

画像をOpenCVで読み込んで、切り出す時、矩形を描く時、xとyの座標指定の順番がわからなくなることが私は非常によくあります。
x方向に並ぶようにパッチを作ったつもりなのに、なぜか(なぜも何もないのですが)y方向にパッチを切り出していたり。

出力したあとで、「あー、また逆だ」とならないためにここできっちり理解しておくことにします。

画像の読み込み

まずはいつものように画像を読み込んでおきます。
以降のコードはこの状態から動くようになっています。

import numpy as np
import os
import matplotlib.pyplot as plt
import cv2

# 画像の読み込み
img = cv2.imread('IMG004.JPG')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

画像サイズと画像イメージは下の通りです。
横長の画像ですね。

# 画像サイズの確認
print(img.shape) # (3264, 4928, 3)

# 画像表示
plt.imshow(img)
plt.show()
(3264, 4928, 3)

numpyでスライス

では、numpyで矩形領域を切り出してみます。

# 一部を切り出して表示
plt.imshow(img[500:1500,2000:3500,:])
plt.show()

画像の中央上あたりを切り出しています。
y, xの順で座標を定義していることがわかります。

OpenCVで矩形表示

次に、OpenCVで同じ場所を矩形で示してみます。

# 矩形を描画
cv2.rectangle(img, (2000, 500), (3500, 1500), (255, 0, 0), thickness=20)

cv2.rectangle()では、左上座標、右下座標をタプルで指定する必要があります。
ここでは、x, yの順で座標を定義しています。

OpenCVのresize()も試してみましたが座標定義は同じく(x, y)ですね。
リサイズ後のnumpyのサイズは (row, col) で扱われますので、ちょっと混乱します。

# リサイズ
img_resize = cv2.resize(img, (500, 1000))
print(img_resize.shape) # (1000, 500, 3)

結論

多分なのですが、numpyは行列 (row, column) で配列を定義し、OpenCVはx, yで定義しているためにこのような挙動になるのでしょう。
私は画像を扱うことが多くて、(x, y) の順での指定に慣れてしまっていたため、混乱してしまったのですが、思想を理解して使えば問題ないと思います。

(おまけ) パッチ画像を作る

最後に、Deep Learningのタイトルらしく、500ピクセルずつに分割したパッチ画像を作ってみました。

# 500x500で画像を切り取って出力
img_size = 500

# 最終的に出力される画像の枚数を確認
num_x = img.shape[1]//img_size
num_y = img.shape[0]//img_size

# 切り出しパッチ数分のsubplotからなるfigureを作成
fig, axes = plt.subplots(nrows=num_y, ncols=num_x)

# x方向で順に切り出し
for y in range(num_y):
    for x in range(num_x):

        # 画像切り出し
        img_area = img[img_size*y:min(img_size*(y+1), img.shape[0]), 
                       img_size*x:min(img_size*(x+1), img.shape[1]),
                       :]

        # 切り出した画像をfigureに配置
        axes[y,x].imshow(img_area)
        axes[y,x].set_title('{}_{}'.format(y,x), fontsize=8)
        axes[y,x].axis('off')

plt.show()

全部並べてみるとこんな感じです。
matplotlibも行列で扱っていますし、OpenCVの座標定義だけがちょっと特殊、という認識でいたほうがいいのかもしれません。

OpenCVでマウス操作する (3)

highgui面白いなあ、と遊んでいます。
画像処理ソフトによくあるトラックバーはないのかなあ、と思ってちょっと調べてみたら普通にある!!

トラックバーで色々遊んだのでメモします。

関数名

主に使うのはこの関数です。

cv2.createTrackbar() # 画面下部にトラックバーを表示する
cv2.getTrackbarPos() # トラックバーの位置を読み取る
cv2.setTrackbarPos() # トラックバーを指定の位置に設定する

実装

単純に画像のRGBの色味をトラックバーで変更できるようなツールを作ってみました。
下の図のように、RGBそれぞれの最小・最大値をトラックバーで調整して、強さを変えていくという感じです。

どう変わったかがわかるように、左側に元画像、右側に変更後の画像を並べることにします。

ライブラリのインポート

前回と途中までは流れはかぶりますが、個人用のメモも兼ねているので。
おなじみのnumpyとopencvとcopyをインポートします。

processing_imageは最小・最大値を元に画像の色味を調整する関数を入れたファイルです。色味調整以外にも色々やってみたいので、別出しすることにしました。

import numpy as np
import cv2
import copy

import processing_image # 画像の色味調整用

全体の流れの設定

ここは流れそのものは前回とほぼ同じです。
trackbar関連の項目については、次項以降で詳細に説明します。

# ===============
# 初期画面の設定
# ===============
bgr = ['Blue', 'Green', 'Red']

# 画像読み込み
img = cv2.imread('IMG004.JPG')
h1, w1, _ = img.shape

# 縮小画像のサイズを高さが500pixelになるように定義
h2 = 500
resize_rate = h2/h1
w2 = int(w1 * resize_rate)

# 画像縮小
disp_img = cv2.resize(img, (w2, h2))
disp_img_ = copy.deepcopy(disp_img) # 描画を戻す用にオリジナルの画像を保持

# デフォルト値を設定
default_values = get_default_value(disp_img)

# 画像表示
cv2.namedWindow('image') # 縮小画面

create_trackbar(default_values)
values = {'color' : {'Red':[0,255], 'Green':[0,255], 'Blue':[0,255]}}

while(1):
    # 中央ボタンを押してデフォルト値に戻す
    cv2.setMouseCallback("image", set_default_value, default_values)

    # トラックバーの値を取得して画像変換
    values = get_trackbar_value(values)
    disp_img = processing_image.color_adjustment(disp_img_, values['color'])

    disp_img = cv2.hconcat((disp_img_, disp_img))

    cv2.imshow('image', disp_img)

    if cv2.waitKey(20) & 0xFF == 27:
        break
    
cv2.destroyAllWindows()

デフォルト値の保存

該当は上の63行目です。

default_values = get_default_value(disp_img)

下の関数を呼び出します。
単純に元の画像の最小・最大値をdefault_valuesとして保存しているだけですね。

def get_default_value(img):
    default_values = {'color':{}}

    for n, color in enumerate(bgr):
        default_values['color'][color] = []
        default_values['color'][color] = [img[:,:,n].min(), img[:,:,n].max()]

    return default_values

トラックバーの作成

create_trackbar(default_values)

設定を行っているのはこの関数です。
cv2.createTrackbar()は順に以下の項目を引数として渡します。
– トラックバーのタイトル
– 作成するウィンドウ名
– 初期値
– 最大値
– 呼び出す関数

bgrは”Blue”, “Green”, “Red”を順に格納したリストですので、たとえば、下のようなトラックバーが定義されます。


- トラックバーのタイトル : "Blue (min)"
- 作成するウィンドウ名 : "image"
- 初期値 : "Blue"バンドの最小値
- 最大値 : 255
- 呼び出す関数 : print_position


なお、呼び出す関数には、引数としてトラックバーの値が渡されます。
特に毎回、値をもらっても何をすることもありませんので、passとだけ書かれたnothingという関数を呼び出しています。

def create_trackbar(default_values):
    for n, color in enumerate(bgr):
        cv2.createTrackbar('{} (min)'.format(color), 'image', default_values['color'][color][0], 255, nothing)
        cv2.createTrackbar('{} (max)'.format(color), 'image', default_values['color'][color][1], 255, nothing)

トラックバーの値の取得

ここまでで下準備ができました。今の状態でも、トラックバーは画面に表示されて、動かせますが、ただ動くだけです。
そのため、次にトラックバーの値を取得します。

values = get_trackbar_value(values)

呼び出された関数は下です。
cv2.getTrackbarPos()は、タイトルと、存在するウィンドウ名を指定することで、該当のトラックバーの値を取ってきます。

def get_trackbar_value(values):
    for n, color in enumerate(bgr):
        values['color'][color][0]= cv2.getTrackbarPos("{} (min)".format(color), "image")
        values['color'][color][1]= cv2.getTrackbarPos("{} (max)".format(color), "image")

    return values

色味の調整

取ってきた最小・最大値を使って画像処理を行うのが以下の行です。

disp_img = process_image.color_adjustment(disp_img_, values['color'])

呼び出し先は別ファイルで、該当箇所は下のようになっています。
よくある、norm = (img-img.min())/(img.max()-img.min())と言うやつで、指定された最小・最大値で0~1の範囲に正規化をしています。

最後に255をかけて8bitに戻せば元通りですが、0~1の範囲を超えてしまったときのため、明示的に0~1で値を切っています。

def color_adjustment(img, color):

    img = img.astype(np.float)

    # 最大値と最小値で正規化
    for n, bgr in enumerate(['Blue', 'Green', 'Red']):
        # ゼロ割の防止
        # min==maxのときは全てゼロにする
        if color[bgr][0] == color[bgr][1]:
            img[:,:,n] = 0
        else:
            img[:,:,n] = (img[:,:,n] - color[bgr][0])/(color[bgr][1]-color[bgr][0])

    img = np.clip(img, 0, 1) * 255

    return img.astype(np.uint8)

変えた結果をなかったことにしたい

ひとまずはこれで色味を自在に変えることができます。
ただ、色々と遊びすぎて元に戻せなくなってしまった、ということが起きたときのために、初期値に戻せる仕組みを追加しました。
マウスの中央ボタンを押せば、予め保存しておいたデフォルト値に設定し直します。

まずは、マウスのCallbackを設定します。

cv2.setMouseCallback("image", set_default_value, default_values)

eventはマウスの中央ボタンが押された時のみ定義します。
cv2.setTrackbarPos()は、getTrackbarPos()とほぼ同じ引数ですが、最後に設定する値を与えるところが違います。
ここにデフォルト値を与えることで、元通りになります。

def set_default_value(event, x, y, flags, param): 
    default_color = param['color']
    if event == cv2.EVENT_MBUTTONDOWN:
        for n, color in enumerate(bgr):
            cv2.setTrackbarPos("{} (min)".format(color), "image", default_color[color][0])
            cv2.setTrackbarPos("{} (max)".format(color), "image", default_color[color][1])

動作イメージ

これで完成です。
色味を変えて遊んでみます。

赤の最大値を低くすることで、赤の強い画像になります。

逆に、赤の最小値を大きくすると赤の弱い画像になります。

画像だと表現できないのでのせませんが、マウスの中央をクリックすると、自動でトラックバーの値が初期値に戻ります。

全体コード

全部のコードを乗っけます。
今回は2つのファイルで構成しています。

<main>

import numpy as np
import cv2
import copy

import processing_image


def nothing(x):
    pass

def create_trackbar(default_values):
    # トラックバーの設定
    # BGRの値の初期値を順に入れていく
    for n, color in enumerate(bgr):
        cv2.createTrackbar('{} (min)'.format(color), 'image', default_values['color'][color][0], 255, nothing)
        cv2.createTrackbar('{} (max)'.format(color), 'image', default_values['color'][color][1], 255, nothing)

def get_trackbar_value(values):
    # 現在のトラックバーの値を取得
    for n, color in enumerate(bgr):
        values['color'][color][0]= cv2.getTrackbarPos("{} (min)".format(color), "image")
        values['color'][color][1]= cv2.getTrackbarPos("{} (max)".format(color), "image")

    return values

def get_default_value(img):
    # 元画像の最大・最小値を取得
    default_values = {'color':{}}

    for n, color in enumerate(bgr):
        default_values['color'][color] = []
        default_values['color'][color] = [img[:,:,n].min(), img[:,:,n].max()]

    return default_values

def set_default_value(event, x, y, flags, param):
    # デフォルトの値に戻す 
    default_color = param['color']
    if event == cv2.EVENT_MBUTTONDOWN:
        for n, color in enumerate(bgr):
            cv2.setTrackbarPos("{} (min)".format(color), "image", default_color[color][0])
            cv2.setTrackbarPos("{} (max)".format(color), "image", default_color[color][1])

import numpy as np
import cv2
import copy

import processing_image


def nothing(x):
    pass

def create_trackbar(default_values):
    # トラックバーの設定
    # BGRの値の初期値を順に入れていく
    for n, color in enumerate(bgr):
        cv2.createTrackbar('{} (min)'.format(color), 'image', default_values['color'][color][0], 255, nothing)
        cv2.createTrackbar('{} (max)'.format(color), 'image', default_values['color'][color][1], 255, nothing)

def get_trackbar_value(values):
    # 現在のトラックバーの値を取得
    for n, color in enumerate(bgr):
        values['color'][color][0]= cv2.getTrackbarPos("{} (min)".format(color), "image")
        values['color'][color][1]= cv2.getTrackbarPos("{} (max)".format(color), "image")

    return values

def get_default_value(img):
    # 元画像の最大・最小値を取得
    default_values = {'color':{}}

    for n, color in enumerate(bgr):
        default_values['color'][color] = []
        default_values['color'][color] = [img[:,:,n].min(), img[:,:,n].max()]

    return default_values

def set_default_value(event, x, y, flags, param):
    # デフォルトの値に戻す 
    default_color = param['color']
    if event == cv2.EVENT_MBUTTONDOWN:
        for n, color in enumerate(bgr):
            cv2.setTrackbarPos("{} (min)".format(color), "image", default_color[color][0])
            cv2.setTrackbarPos("{} (max)".format(color), "image", default_color[color][1])

# ===============
# 初期画面の設定
# ===============
bgr = ['Blue', 'Green', 'Red']

# 画像読み込み
img = cv2.imread('IMG004.JPG')
h1, w1, _ = img.shape

# 縮小画像のサイズを高さが500pixelになるように定義
h2 = 500
resize_rate = h2/h1
w2 = int(w1 * resize_rate)

# 画像縮小
disp_img = cv2.resize(img, (w2, h2))
disp_img_ = copy.deepcopy(disp_img) # 描画を戻す用にオリジナルの画像を保持

# デフォルト値を設定
default_values = get_default_value(disp_img)

# 画像表示
cv2.namedWindow('image') # 縮小画面

create_trackbar(default_values)
values = {'color' : {'Red':[0,255], 'Green':[0,255], 'Blue':[0,255]}}

while(1):
    # 中央ボタンを押してデフォルト値に戻す
    cv2.setMouseCallback("image", set_default_value, default_values)

    # トラックバーの値を取得して画像変換
    values = get_trackbar_value(values)
    disp_img = processing_image.color_adjustment(disp_img_, values['color'])

    disp_img = cv2.hconcat((disp_img_, disp_img))

    cv2.imshow('image', disp_img)

    if cv2.waitKey(20) & 0xFF == 27:
        break
    
cv2.destroyAllWindows()

<processing_image>

import numpy as np
import cv2

def color_adjustment(img, color):

    img = img.astype(np.float)

    # 最大値と最小値で正規化
    for n, bgr in enumerate(['Blue', 'Green', 'Red']):
        # ゼロ割の防止
        # min==maxのときは全てゼロにする
        if color[bgr][0] == color[bgr][1]:
            img[:,:,n] = 0
        else:
            img[:,:,n] = (img[:,:,n] - color[bgr][0])/(color[bgr][1]-color[bgr][0])

    img = np.clip(img, 0, 1) * 255

    return img.astype(np.uint8)

トラックバー関連を全部関数で外出しにしているのは、ぼかしとかコントラスト調整とか付け加えたいと考えているからです。
カメラ特性を勉強しながらなので、いつになるのか分かりませんが、画像処理って面白いですね。

Pythonでグラフを書く (3)

次に、データに近似直線を引く方法を整理します。

Numpyで直線近似

import numpy as np
import matplotlib.pyplot as plt

データを準備します。
結果が適切かどうかを見たいので、直線状に乗るデータにランダムなノイズを加えたものを作ります。

# シードを固定
# ランダム値が実行ごとに変わらないようにするため (必須ではない)
np.random.seed(0)

# xは-10〜+10までの整数をインクリメント
# yはxの2倍

x = np.linspace(-10, 10, 21)
y_ = x*2

# ±10の範囲でノイズを加える
random_noise = np.random.rand(21)*10
y = y_ + random_noise

# 各データの値を表示
for n in range(len(x)):
    print('x: {}, y_: {}, y: {}'.format(x[n], y_[n], np.round(y[n], 2)))

出力結果は以下のようになります。
2列目(y_)がノイズを加える前の値で、3列目(y)がノイズを加えたあとの値です。

x: -10.0, y_: -20.0, y: -14.51
x: -9.0, y_: -18.0, y: -10.85
x: -8.0, y_: -16.0, y: -9.97
x: -7.0, y_: -14.0, y: -8.55
x: -6.0, y_: -12.0, y: -7.76
x: -5.0, y_: -10.0, y: -3.54
x: -4.0, y_: -8.0, y: -3.62
x: -3.0, y_: -6.0, y: 2.92
x: -2.0, y_: -4.0, y: 5.64
x: -1.0, y_: -2.0, y: 1.83
x: 0.0, y_: 0.0, y: 7.92
x: 1.0, y_: 2.0, y: 7.29
x: 2.0, y_: 4.0, y: 9.68
x: 3.0, y_: 6.0, y: 15.26
x: 4.0, y_: 8.0, y: 8.71
x: 5.0, y_: 10.0, y: 10.87
x: 6.0, y_: 12.0, y: 12.2
x: 7.0, y_: 14.0, y: 22.33
x: 8.0, y_: 16.0, y: 23.78
x: 9.0, y_: 18.0, y: 26.7
x: 10.0, y_: 20.0, y: 29.79

では、xとy(ノイズ付加)を図に表します。

# ノイズを加えた結果を散布図で表示
fig = plt.figure()
ax = fig.add_subplot(111)

ax.scatter(x,y)
fig.show()

ばらつきはあるものの、元になった直線がうっすらと見えてはいますね。
この値を使って直線近似を行います。

直線近似

# 1次 (直線) 近似
z1 = np.polyfit(x, y, 1) # 1次式の係数がzに格納される
print(z1)

# # 2次近似
# z2 = np.polyfit(x, y, 2) # 2次式の係数がzに格納される
# print(z2)

結果は以下のとおりです。
1つ目が傾き、2つ目が切片を示しています。
元々は、\(y=2x+5\)でしたので、それなりの値が出せています。

[1.98233716 4.64964655]

今回はコメントアウトしていますが、polyfit()の3番目の引数を2に設定すると、2次の近似ができます。

近似結果を重ねる

では、データの近似結果が本当にそれらしくなっているかを見てみます。
求めた近似直線の係数を使って、xに該当するyの値を求めます。
ここでは、y1_predという配列に格納しました。

fig2 = plt.figure()
ax = fig2.add_subplot(111)

# 求めた係数を用いて、xを代入してyの値を算出する
y1_pred = np.poly1d(z1)(x)
# y2_pred = np.poly1d(z2)(x) # 2次近似の場合

# オレンジ色で近似直線を引く
ax.scatter(x,y)
ax.plot(x, y1_pred, '-', color='orange')  
fig.show()

いい感じです。

決定係数(\(R^2\))の計算

もうひとつ、この近似結果がどれくらい使えるのかを示す指標を出してみます。
実際の値と、近似結果によって推定した値とがどれくらい一致するかを表す決定係数 (\R^2\)です。
エクセルでも近似を行ったときに出してくれますね。

\(R^2\)を求めるためには、sklearnをインポートする必要があります。

from sklearn.metrics import r2_score

使用する関数はその名のとおり、r2_score()です。
真(実測)値と推定値を関数に入れることで、\(R^2\)を計算してくれます。

r2 = r2_score(y, y1_pred) 
print(r2)

0.949794904242459

ここで\(R^2\)は、0~1の範囲の値を取り、1に近くなるほど真値と推定値との相関が高いことを示します。つまり、上の数値0.95は真値を表すのに非常によい近似ができていると言えます。

元のデータ自体に外れ値もありませんし、見てわかるように近似した直線に沿ってデータが並んでいますので、納得のできる結果です。

とりあえず、図の描画についてはここまでにします。

pythonでグラフを書く (2)

前回の続きです。
これから使用頻度が高そうなグラフについて、書き方を調べてみました。

散布図

import numpy as np
import matplotlib.pyplot as plt
# 乱数の生成
x = np.random.rand(100)
y = np.random.rand(100)
fig_1 = plt.figure()

ax_1 = fig_1.add_subplot(1,1,1)
ax_1.scatter(x, y, label='test1') # plotをscatterに変えるだけ
fig_1.legend(loc='upper center')
fig_1.show()

これでできました。
マーカーの表現を変えて遊んでみます。

色を変える

マーカーの色は、c (color) オプションで変えられます。

今回は、色を名前(red)としましたが、もちろんRGB指定でもOKのようです。(ただ、書き方がよくわかりませんでした……)

fig_1 = plt.figure()

ax_1 = fig_1.add_subplot(1,1,1)
ax_1.scatter(x, y, label='test1', c='red') # cオプションで色指定
fig_1.legend(loc='upper center')
fig_1.show()

透明度を変える

散布図ではある場所に値が集中して見づらいことがありますが、マーカーを半透明にすれば解決です。
透明度はalphaで指定できます。

fig_1 = plt.figure()

ax_1 = fig_1.add_subplot(1,1,1)
ax_1.scatter(x, y, label='test1', c='red', alpha=0.2) # alphaオプションで透明度指定
fig_1.legend(loc='upper center')
fig_1.show()

大きさを変える

マーカーの大きさは、s (=size) オプションで変更できます。
なお、色指定はcolor=’red’でもc=’red’でも対応してくれましたが、sオプションはsize=だとエラーが返ってくるので注意です。

fig_1 = plt.figure()

ax_1 = fig_1.add_subplot(1,1,1)
ax_1.scatter(x, y, label='test1', c='red', s=200, alpha=0.2) # sオプションでサイズ指定
fig_1.legend(loc='upper center')
fig_1.show()

ちなみに、このサイズはリストで指定することもできます。
下では、yの値に対応して大きさが変わるようにマーカーを設定しました。

fig_1 = plt.figure()

ax_1 = fig_1.add_subplot(1,1,1)
ax_1.scatter(x, y, label='test1', c='red', s=y*100, alpha=0.2) # サイズをリストで定義
fig_1.legend(loc='upper center')
fig_1.show()

棒グラフを描く

つづいて、棒グラフです。

# 値はplotのときと同じく、線形増加
x=np.linspace(0, 20, 21)
y=x*2

fig_2 = plt.figure() # インスタンス作成
ax_2 = fig_2.add_subplot(1,1,1)
ax_2.bar(x, y)

fig_2.show()

これで棒グラフができました。
さらに積み上げグラフを作ってみます。

積み上げグラフ

fig_3 = plt.figure()
ax_31 = fig_3.add_subplot(2,1,1)
ax_31.bar(x, y, color='blue')

# もうひとつyの値を定義する
# yに比べて増加量の小さいy2として定義
y2 = x

ax_32 = fig_3.add_subplot(2,1,2)
ax_32.bar(x, y2, color='gray')
ax_32.bar(x, y, bottom=y2, color='blue') # y2の上にyの値が加算されるような積み上げグラフを作成

fig_3.show()

今回はここまで。
次回は、近似直線の書き方についてまとめます。

pythonでグラフを書く

pythonにはグラフ描画用のライブラリmatplotlibがあります。
たまに使ったりはするのですが、毎回ググって既存コードをそのままコピペみたいなことが多く、内容をきちんと理解できていませんでしたので、基本的な使い方を改めて理解することにしました。

インポートは以下で行います。
グラフを描画したいときはたいていnumpyも必要になるだろうと思いますので、合わせてインポートしておくことにします。

import numpy as np
import matplotlib.pyplot as plt

単純な線形のグラフを書いてみます。

グラフのクイックな描画

# xは-10〜+10までの整数をインクリメント
# yはxの2倍
x=np.linspace(-10, 10, 21)
y=x*2

plt.plot(x, y, label='test')

plt.legend() # 凡例の定義
plt.show()

これですごく単純なグラフを描画できました。
細かい設定を行うには、この次に書くグラフをインスタンス化するほうがいいのだと思いますが、とりあえず結果を見たいという場合にはこれで十分です。

グラフをインスタンス化する

この方法を使うと、複数のグラフを同時に扱うことができます。スクリプト内に埋め込むときは、こっちのほうが扱いやすい(はず)です。

fig_1 = plt.figure()
ax_1 = fig_1.add_subplot(1,1,1)
ax_1.plot(x, y, label='test')

fig_1.legend() # 凡例を表示する
fig_1.show()

さらに、test2という名前でもうひとつグラフを作ってみます。

fig_2 = plt.figure()
ax_2 = fig_2.add_subplot(1,1,1)
ax_2.plot(x, y, label='test2')

fig_2.legend() # 凡例を表示する
fig_2.show()

インスタンス情報は”fig_1″と”fig_2″のそれぞれに保持されていますので、fig_1.show()、fig_2.show()とやることで、それぞれ表示したいグラフがきちんと表示されることがわかります。

複数のグラフを表示する

また、add_subplot()はグラフをどこに描画するかを指定するメソッドで、カッコ内の数字は順に、(グラフの分割行数、グラフの分割列数、該当のグラフの位置)を示しています。
ひとつのウィンドウにグラフをひとつだけ描画するときは、(1, 1, 1)です。

例えば、test1を上に、test2を下に描画したい場合は、test1は(2, 1, 1)、test2は(2, 1, 2)と書きます。
test1とtest2の見分けがつきやすいように、test2のほうはx*(-2)の値を取ることにします。

fig = plt.figure()
ax_1 = fig.add_subplot(2,1,1)
ax_1.plot(x, y, label='test1')
ax_1.legend()

y2 = x*(-2)
ax_2 = fig.add_subplot(2,1,2)
ax_2.plot(x, y2, label='test2')
ax_2.legend()

fig.show()

分割列数の方を2にしたときは、下図のように縦長に並んでグラフが出力されます。

ちなみに、グラフをひとつだけ描画するときも、add_subplot()は必須みたいです。コメントアウトして動かしてみたら、画像は作成されませんでした。

matplotlibは単純な描画ツールとだけの認識で、あまりじっくりとは見てきませんでした。ただ、今回調べてみて、なかなか奥が深そうだぞ、と分かりましたので、これからもう少し調べていこうと思います。

出力ディレクトリの準備 (os.mkdir)

処理済みデータを出力するときに、保存先のディレクトリを準備しておく必要があります。簡単に言うと、以下の2ステップの処理を行うことです。

  • 保存先のディレクトリが存在するかを確認
  • もしもなかったら作成する

pythonの標準ライブラリの「os」で簡単に実装できるのですが、ちょいちょい混乱することがあるので、メモとして残しておくことにします。

ディレクトリの存在確認

対象のディレクトリがあるかどうかは、os.path.exists()で確認できます。

import os

print(os.path.exists('target_dir'))

戻り値は、True(ディレクトリがある場合)もしくはFalse(ディレクトリがない場合)のどちらかです。

ディレクトリの作成

ディレクトリの作成コマンドは2種類あります。

# parent_dirが存在しない場合はエラー
os.mkdir('parent_dir/target_dir')

# parent_dirが存在しない場合は、parent_dirも含めて作成
os.makedirs('parent_dir/target_dir')

makedirsを使ったほうが楽ちんですが、parent_dirの指定が間違っていた場合に意図しない場所にガンガン保存してしまう可能性がありますので、どちらを使うかは時と場合によりけりという感じでしょうか。

ということで、

ディレクトリがない場合に新規作成する

のは、以下のコードで実現できます。

if not os.path.exists('target_dir'):
    os.mkdir('target_dir')   #あるいは、os.path.makedirs('target_dir')

ググってしまえばすぐに出てくる情報ですが、よく使う処理なので覚えてしまったほうがコーディングに手間取らずにすみます。

乱数を固定する

CNNの既存コードを見ていると、torch.manual_seed()なんていう一文があります。
おまじないみたいなものだろうと全然気にしないでいたのですが、調べてみたら深い意味をもつものでしたので、備忘のために書いておきます。

RNG (Random Number Generator:乱数ジェネレータ)

PyTorchに限らず、ランダムな処理を行う場合には、RNGと呼ばれるジェネレータが乱数を生成し、その数字に基づきランダム処理を行っています。

例えば、データセットからバッチを生成するときに、データを取ってくるためのランダム性だったり、重みの初期化に使う乱数だったり。

この乱数は、seedと呼ばれる数字から生成しています。
(ちゃんと理解できていませんが、)seedが同じであれば、同じ乱数が生成されるそうなのです。

同じ乱数が生成されて何が嬉しいかというと、結果に再現性をもたせることができるのです。同じバッチを使って、同じ初期化重みを使えるので、学習は同じ経過をたどりますね。

最初、seedの意味が分からなかったので、何度学習しても同じ画像を同じタイミングで読み込んでいるのがずっと不思議でした。shuffleが機能していないのだと勘違いしたり……
ランダムに取り出すデータが固定されていたからなのですね。

RNGの動作

numpyの関数で、RNGの動作を確認してみます。

import numpy as np

for n in range(5):
    np.random.seed(0) # seedの設定
    x = np.random.randn(1) # 乱数の生成
    print(x)

[1.76405235]
[1.76405235]
[1.76405235]
[1.76405235]
[1.76405235]

出力結果を見ると、seedに同じ値が入っているため、生成される乱数が同一であることが分かります。

ちなみに、以下のようにseedの設定ループを前に出すと、繰り返すたびに乱数は変わります。毎回、同じseedの値を使うわけではなく、1度使ったseedの続きの値を使っているからだそうです。

import numpy as np

np.random.seed(0) # seedの設定
for n in range(5):
    x = np.random.randn(1) # 乱数の生成
    print(x)

[1.76405235]
[0.40015721]
[0.97873798]
[2.2408932]
[1.86755799]

RNGの固定

再現性のある学習を行うために、以下をコードのはじめに設定します。引数としてのseedは任意の数字を入れます。
様々な段階で乱数を使っていますので、完全再現のためには4種類、忘れずに設定しましょう。

np.random.seed(seed)
random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)

meshgrid

格子状の2次元配列を作るときに使います。前回の等差数列を作る関数と合わせて使うことも多いと思います。

【書式】X, Y = np.meshgrid(x1, x2)
 引数には2つの1次元配列を指定します。

文章だけだと説明しづらいため、実際に動かしてみます。
まず、元となる1次元配列を作ります。

x = np.arange(0,5)
y = np.arange(0,10)
print(x)
print(y)

# x
array([0, 1, 2, 3, 4])

# y
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

ここで作った配列を引数にして2次元配列を作ります。

xx, yy = np.meshgrid(x,y)
print(xx)
print(yy)

# xx
array([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4],
[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]])

# yy
array([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4],
[5, 5, 5, 5, 5], [6, 6, 6, 6, 6], [7, 7, 7, 7, 7], [8, 8, 8, 8, 8], [9, 9, 9, 9, 9]])

2次元配列のイメージにすると、下図のようになります。
meshgridは2次元の配列の全組み合わせを作ってくれるということですね。

また、\(X=xx(i,j)\)、\(Y=yy(i,j)\)として、\((X, Y)\)をみると、\((i, j)\)と値が一致します。つまりどういうことかというと、座標情報を計算可能な形にできるということです。

PyTorch

pytorchでも同じようなことができます。

x = torch.arange(0,5)
y = torch.arange(0,10)
print(x)
print(y)     

# x
tensor([0, 1, 2, 3, 4])

# y
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

同じように、torchのmeshgridで配列を作ります。

xx, yy = torch.meshgrid(x, y)
print(xx)
print(yy)

# xx
tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
[3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 4, 4, 4, 4, 4, 4, 4, 4, 4]])

# yy
tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])

numpyの時と形状が違いますね。
図にすると、下のような感じです。

メモリの使い方が関係あるようでよくわからないのですが、PyTorchの場合、y方向(画像だと高さ方向)が順序として先に扱うみたいです。

numpyと同じ形状にしたい場合は、xとyの並びを逆にします。この差は意外にハマりどころですので要注意です。

yy, xx = torch.meshgrid(y, x)
print(yy)
print(xx)    

# yy
tensor([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [3, 3, 3, 3, 3], [4, 4, 4, 4, 4], [5, 5, 5, 5, 5], [6, 6, 6, 6, 6], [7, 7, 7, 7, 7], [8, 8, 8, 8, 8], [9, 9, 9, 9, 9]])

# xx
tensor([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]])

利用例

meshgridの使い道がいまいちわからないでいたのですが、画像の変形を行うときに使えるのだと気づきました。

PyTorchで画像変形を行うときのやり方のひとつに、grid_sampleという関数を使う方法があります。この関数は、変形元画像と変形パラメータとして、x, yの座標が求められます。

下図のように、変形パラメータの各gridには、変換後の画像の値が、元画像のどこの座標の値を使うのかを指定します。

なので、例えばaffine変換をかけたいよ、という場合、まずは等差数列+meshgridで座標位置が値として格納された配列を作ります。この配列に対してaffine変換をかけるのです。

その結果をもって、
warp_img = F.grid_sample(img, warp)
で変形できます。

等差数列を作る

同じstep数でインクリメントしていくような配列って、けっこう使用頻度が高いと思います。for文を回しても作れなくはないのですが、関数を使った2通りの作り方を見つけましたので、メモとして残しておきます。

linspace関数

配列の値の区間(start/stop)と何等分するか(N)を指定します。

numpy

【書式】 numpy.linspace(start, stop, N)

例えば、-1~+1までの区間を20等分したい場合は、

import numpy as np

# start:-1, stop:1, N:20の行列
a_np = np.linspace(-1,1,20)
print(a_np)

array([-1. , -0.89473684, -0.78947368, -0.68421053, -0.57894737, -0.47368421, -0.36842105, -0.26315789, -0.15789474, -0.05263158, 0.05263158, 0.15789474, 0.26315789, 0.36842105, 0.47368421, 0.57894737, 0.68421053, 0.78947368, 0.89473684, 1. ])

デフォルトでは”stop”の「+1」は区間に含まれるため、含みたくない場合は、「endpoint=False」を引数に入れます。

b_np = np.linspace(-1,1,20, endpoint=False)
print(b_np)             

array([-1. , -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

pytorch

pytorchの場合も使い方はnumpyとほとんど同じです。

a_torch = torch.linspace(-1,1,20)
print(a_torch)

tensor([-1.0000, -0.8947, -0.7895, -0.6842, -0.5789, -0.4737, -0.3684, -0.2632, -0.1579, -0.0526, 0.0526, 0.1579, 0.2632, 0.3684, 0.4737, 0.5789, 0.6842, 0.7895, 0.8947, 1.0000])

stopも分割に含まれるところも同じですが、numpyと違い、endpoint=Falseは使えません。

b_torch = torch.linspace(-1,1,20,endpoint=False)                    

Traceback (most recent call last): File “”, line 1, in
TypeError: linspace() received an invalid combination of arguments – got (int, int, int, endpoint=bool), but expected one of:
(Number start, Number end, Tensor out, torch.dtype dtype, torch.layout layout, torch.device device, bool requires_grad)
(Number start, Number end, int steps, Tensor out, torch.dtype dtype, torch.layout layout, torch.device device, bool requires_grad)

同じことをやるならどうすればいいかを考えてみました。N+1の配列を作って、Nまでを取り出すのはひとつの手でしょうか。
もしかしたら、普通に関数があるのかもしれませんが……

n_21 = torch.linspace(-1, 1, 21)
n_20 = n_21[:-1]           

# N=21
tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000, 1.0000])

# N=20まで取り出し
tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000])

arange関数

配列の値の区間とstepの指定により数列を生成します。
(分割数(N)は指定しません)

numpy

【書式】numpy.linspace(start, stop, step=1)

stepはデフォルトで1ですので、下のように何も指定しない場合は整数の配列が生成されます。arangeの場合は、stopの値は配列に含まれないのですね。

x_np = np.arange(0,5)
print(x_np)

array([0, 1, 2, 3, 4])

pytorch

PyTorchの場合もnumpyと同じです。

x_torch = torch.arange(0,5)
print(x_torch)

tensor([0, 1, 2, 3, 4])

また、stepの数字を0.1にしてみると、0.1ごとにインクリメントする数列が生成されました。

x_torch = torch.arange(-1,1,0.1)
print(x_torch)

tensor([-1.0000, -0.9000, -0.8000, -0.7000, -0.6000, -0.5000, -0.4000, -0.3000, -0.2000, -0.1000, 0.0000, 0.1000, 0.2000, 0.3000, 0.4000, 0.5000, 0.6000, 0.7000, 0.8000, 0.9000])

まとめ

linspaceとarangeは両方とも等差数列を作る関数ですが、分割数が決まっている場合はlinspace、step数が決まっている場合はarangeを使うという棲み分けになっているようです。

作りたい数式の条件で使い分けが必要ですね。

OpenCVとPILの基本操作

Pythonでの画像操作に使うライブラリは、OpenCVとPILではないかと思います。
私はいつもOpenCVで読み込むことが多いですが、単純な読み書きの場合にPILも使うことがあります。

ファイル操作の方法について、OpenCVとPILで比較を行ってみました。
ちなみに、PILはPython Imaging Libraryの略だそうです。

インポート

OpenCVとPILのインポートは以下のコマンドでできます。
画像表示用にpyplotもインポートしておきます。

import cv2 # OpenCV
from PIL import Image # PIL
import matplotlib.pyplot as plt

ファイルの読み込み

OpenCV

・cv2.imread()
 画像を読み込みます。
 読み込んだ画像をmatplotで表示してみます。

img_cv = cv2.imread('img00001.jpg')
plt.imshow(img_cv)
plt.show()

なんだか色合いが変ですね。
それもそのはず、OpenCVはカラー画像をRGBではなくBRGの順に読み込んでいるのです。
OpenCVの機能だけで読み込み、書き込みをするのであればBGRのままで問題ないのですが、今回のようにOpenCV以外のライブラリを使う場合には、RGBの順に直してあげる必要があります。

・cv2.cvtColor()
 画像タイプを変換します。第2引数にcv2.COLOR_BGR2RGBを指定すればRGBの順序に変換されます。逆方向はcv2.COLOR_RGB2BGRです。

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

RGB変換処理を追加して、画像を表示してみます。

img_cv = cv2.imread('img00001.jpg')
img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)
plt.imshow(img_cv)
plt.show()

きれいな海が表示されました。


PIL

・Image.open()
 こっちはRGBの順番で読んでくれるので、そのまま表示できます。

img_pil = Image.open('img00001.jpg')
plt.imshow(img_pil)
plt.show()

おんなじ画像ですが……

データフォーマット


OpenCVとPILで読み込んだ画像に対して、何かしらの演算処理をしたいという場合、読み込んだときのフォーマットが異なりますので、注意が必要です。

【OpenCVでの読み込み】

img_cv = cv2.imread('img00001.jpg')
print(type(img_cv))

<class ‘numpy.ndarray’>

【PILでの読み込み】

img_pil = Image.open('img00001.jpg')
print(type(img_pil))

<class ‘PIL.JpegImagePlugin.JpegImageFile’>

OpenCVはnumpyのndarray形式ですが、PILのほうはPILの独自フォーマットになっていることがわかります。

どういうことかというと、PILで読み込んだままの状態だと、演算子による計算ができないとうことです。

img_pil += 1
img_cv += 1

この計算をすると、OpenCVで読み込んだほうはエラーなく処理が行われますが、PILのほうは下のようなエラーが返ってきます。

Traceback (most recent call last):
File “”, line 1, in
TypeError: unsupported operand type(s) for +=: ‘JpegImageFile’ and ‘int’

ですので、読み込んだデータをnumpy arrayに変換する処理が必要になります。

img_pil = np.array(img_pil)

ちなみに、numpy arrayをPIL形式に変換するには、Image.fromarray()を使えばOKです。

img_np = np.array(img)
print(type(img_np))
 >> <class 'numpy.ndarray'>
img_pil = Image.fromarray(img_np)
print(type(img_np))
 >> <class 'numpy.ndarray'>

書き込み

OpenCV

・cv2.imwrite()

out_cv_fname = 'output_cv.jpg'
cv2.imwrite(out_cv_fname, img_cv)

ちなみに、OpenCVの場合は出力もBGR並びが前提になっていますので、もしもRGBに並び替えたときは、cv2.cvtColor(img, cv2.COLOR_RGB2BGR)でBGR並びに変更しておく必要があります。

PIL

img.save()

out_fname = 'output_pil.jpg'
img_pil.save(out_fname)

とりあえず、基本の基本はここまで。