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)

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

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

前回に引き続き、マウス操作です。
目標の、場所を決めて拡大表示を実装します。

目標の動作の定義

  • 画像を縮小して全体を表示
  • 縮小画像のカーソル位置の一定範囲を別ウィンドウで拡大
  • ↑の拡大領域を縮小画面で矩形で表示
  • 左ボタン押下+ドラッグで拡大領域を変更
  • 左ボタンが押されていないときは拡大領域の変更なし

こんな感じです。

順に実装を乗せていきます。全体のコードは最後に。

ライブラリのインポート

当然、numpyとcv2はインポートします。また、画像に手を加えていくことになるので、オリジナル画像を保持しておくために、copyもインポートしました。

import numpy as np
import cv2
import copy

全体の流れの設定

画像のサイズ・初期画面の設定、画面の表示・終了の定義を行います。ここをメインの流れにして、細かい処理は関数で呼び出すようにします。

縮小画面と拡大画面の更新はrefresh_windowという関数(後述)を作ってまとめて処理をします。

# ===============
# 初期画面の設定
# ===============

img_size = {'orig': {},
            'disp': {},
            'zoom': {}}
            
# 画像読み込み
img = cv2.imread('img00001.jpg')

img_size['orig']['h'], img_size['orig']['w'] = img.shape[0], img.shape[1]

# 縮小画像のサイズを高さが500pixelになるように定義
img_size['disp']['h'] = 500
resize_rate = img_size['disp']['h']/img_size['orig']['h']
img_size['disp']['w'] = int(img_size['orig']['w'] * resize_rate)

# zoom画面の大きさは400x400に
zoom_img_size = 400
img_size['zoom']['w'], img_size['zoom']['h'] = zoom_img_size, zoom_img_size

# 画像縮小
disp_img = cv2.resize(img, (img_size['disp']['w'], img_size['disp']['h']))
disp_img_ = copy.deepcopy(disp_img) # 描画を戻す用にオリジナルの画像を保持

# 初期のzoom画面を設定
# 元画像から切り出す座標位置を指定
fix_coords = {}
fix_coords['x_s'] = 0
fix_coords['x_e'] = img_size['zoom']['w']
fix_coords['y_s'] = 0
fix_coords['y_e'] = img_size['zoom']['h']

# 表示画像の作成
disp_img, zoom_img = refresh_window(img, disp_img, fix_coords, resize_rate)

# 通常、拡大領域は固定
disp_fix = True

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

# マウス操作
cv2.setMouseCallback('image',show_zoom, param={'disp_fix':disp_fix, 'fix_coords': fix_coords})


# escキーが押されると画面を閉じる
while(1):
    cv2.imshow('image', disp_img)
    cv2.imshow('zoom', zoom_img)
    if cv2.waitKey(20) & 0xFF == 27:
        break

cv2.destroyAllWindows()

マウス操作の定義

マウスの左ボタンを押しながらマウスを動かした時のみ、拡大表示の範囲を変えるように設定します。
ですので、定義するマウスの動きとしては下の3つです。

(1) 左ボタンが押された時
(2) マウスが動いている時
(3) 左ボタンが離された時

(2)はそれに加えて、(1)である必要があります。そのため、左ボタンが押されていないときは、disp_fixフラグをたてて表示固定、左ボタンが押されたらフラグオフにすることにしました。
つまり、(2)+disp_fixがオフのときのみ、拡大表示の範囲が変わります。

また、convert_coordinates()では、縮小画面で定義されたカーソル位置を、フルサイズの画像での位置に変換しています。ズーム画面はフルサイズの画像から切り出しているためです。

戻り値が与えられないようなので、更新したい変数(特に更新する画像)はglobal関数としなければならないようです。もしかすると別の方法があるかもしれませんが。

def show_zoom(event, x, y, flags, param):
    global img, disp_img, zoom_img, coords, fix_coords

    # zoom画面の中心位置を取得する
    x_orig = int(x/resize_rate)
    y_orig = int(y/resize_rate)

    # =================
    # 左ボタン+ドラッグで拡大領域を移動
    # =================
    if event == cv2.EVENT_LBUTTONDOWN:
        # 左ボタン押下でzoom画面の固定解除
        param['disp_fix'] = False

        coords = convert_coordinates(img_size, x_orig, y_orig, zoom_img_size)
        disp_img, zoom_img = refresh_window(img, disp_img, coords, resize_rate)

    if event == cv2.EVENT_MOUSEMOVE and not param['disp_fix']:
        # zoom画面の固定解除のときのみ、拡大領域を現在位置に変更
        coords = convert_coordinates(img_size, x_orig, y_orig, zoom_img_size)
        disp_img, zoom_img = refresh_window(img, disp_img, coords, resize_rate)

    if event == cv2.EVENT_LBUTTONUP:
        # 左ボタンが元に戻ったらzoom画面固定に戻す
        param['disp_fix'] = True
        fix_coords = copy.deepcopy(coords)
        disp_img, zoom_img = refresh_window(img, disp_img, fix_coords, resize_rate)

周辺関数

あとは、周辺関数の整備ということで、

縮小画像から元画像の座標への変換

# 画像の切り出し位置を定義
def convert_coordinates(img_size, x_orig, y_orig, window_size):
    window_size = int(window_size/2)
    coords = {}
    coords['x_s'] = max(0, x_orig - window_size)
    coords['x_e'] = min(img_size['orig']['w'], x_orig + window_size)
    coords['y_s'] = max(0, y_orig - window_size)
    coords['y_e'] = min(img_size['orig']['h'], y_orig + window_size)

    return coords

表示画像の更新

def refresh_window(img, disp_img, coords, resize_rate):

    zoom_img = img[coords['y_s']:coords['y_e'], coords['x_s']:coords['x_e'], :]

    # imageウィンドウに矩形表示
    disp_img = copy.deepcopy(disp_img_)

    disp_img = cv2.rectangle(disp_img, (int(coords['x_s']*resize_rate), int(coords['y_s']*resize_rate)),
                            (int(coords['x_e']*resize_rate), int(coords['y_e']*resize_rate)),
                                    color=(255, 255, 255))

    return disp_img, zoom_img

です。
これで、左クリック+ドラッグで拡大領域を移動させることができました。

画像だと分かりにくいですが、左ボタン+ドラッグでimageウィンドウの白い矩形が動いて、矩形内の画像をzoomウィンドウに表示するように動いています。

というわけで、とりあえずできたコードを以下に乗せています。リファクタリングのやり方は勉強中なので、コードの汚さはご愛嬌で。

import numpy as np
import cv2
import copy

# 画像の切り出し位置を定義
def convert_coordinates(img_size, x_orig, y_orig, window_size):
    window_size = int(window_size/2)
    coords = {}
    coords['x_s'] = max(0, x_orig - window_size)
    coords['x_e'] = min(img_size['orig']['w'], x_orig + window_size)
    coords['y_s'] = max(0, y_orig - window_size)
    coords['y_e'] = min(img_size['orig']['h'], y_orig + window_size)
    return coords


def refresh_window(img, disp_img, coords, resize_rate):
    zoom_img = img[coords['y_s']:coords['y_e'], coords['x_s']:coords['x_e'], :]
    # imageウィンドウに矩形表示
    disp_img = copy.deepcopy(disp_img_)
    disp_img = cv2.rectangle(disp_img, (int(coords['x_s']*resize_rate), int(coords['y_s']*resize_rate)),
                            (int(coords['x_e']*resize_rate), int(coords['y_e']*resize_rate)),
                                    color=(255, 255, 255))
    return disp_img, zoom_img


def show_zoom(event, x, y, flags, param):
    global img, disp_img, zoom_img, coords, fix_coords
    # zoom画面の中心位置を取得する
    x_orig = int(x/resize_rate)
    y_orig = int(y/resize_rate)

    # 左ボタン押下でzoom画面の固定解除
    if event == cv2.EVENT_LBUTTONDOWN:    
        param['disp_fix'] = False
        coords = convert_coordinates(img_size, x_orig, y_orig, zoom_img_size)
        disp_img, zoom_img = refresh_window(img, disp_img, coords, resize_rate)

    # zoom画面の固定解除のとき(=左ボタンが押されているとき)のみ
    # 拡大領域を移動
    if event == cv2.EVENT_MOUSEMOVE and not param['disp_fix']:
        coords = convert_coordinates(img_size, x_orig, y_orig, zoom_img_size)
        disp_img, zoom_img = refresh_window(img, disp_img, coords, resize_rate)

    # 左ボタンが元に戻ったらzoom画面固定に戻す
    if event == cv2.EVENT_LBUTTONUP:
        param['disp_fix'] = True
        fix_coords = copy.deepcopy(coords)
        disp_img, zoom_img = refresh_window(img, disp_img, fix_coords, resize_rate)


# ===============
# 初期画面の設定
# ===============

img_size = {'orig': {},
            'disp': {},
            'zoom': {}}
            
# 画像読み込み
img = cv2.imread('img00001.jpg')

img_size['orig']['h'], img_size['orig']['w'] = img.shape[0], img.shape[1]

# 縮小画像のサイズを高さが500pixelになるように定義
img_size['disp']['h'] = 500
resize_rate = img_size['disp']['h']/img_size['orig']['h']
img_size['disp']['w'] = int(img_size['orig']['w'] * resize_rate)

# zoom画面の大きさは400x400に
zoom_img_size = 400
img_size['zoom']['w'], img_size['zoom']['h'] = zoom_img_size, zoom_img_size

# 画像縮小
disp_img = cv2.resize(img, (img_size['disp']['w'], img_size['disp']['h']))
disp_img_ = copy.deepcopy(disp_img) # 描画を戻す用にオリジナルの画像を保持

# 初期のzoom画面を設定
# 元画像から切り出す座標位置を指定
fix_coords = {}
fix_coords['x_s'] = 0
fix_coords['x_e'] = img_size['zoom']['w']
fix_coords['y_s'] = 0
fix_coords['y_e'] = img_size['zoom']['h']

# 表示画像の作成
disp_img, zoom_img = refresh_window(img, disp_img, fix_coords, resize_rate)

# 通常、拡大領域は固定
disp_fix = True

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

# マウス操作
cv2.setMouseCallback('image',show_zoom, param={'disp_fix':disp_fix, 'fix_coords': fix_coords})

# escキーが押されると画面を閉じる
while(1):
    cv2.imshow('image', disp_img)
    cv2.imshow('zoom', zoom_img)
    if cv2.waitKey(20) & 0xFF == 27:
        break
    
cv2.destroyAllWindows()

OpenCVのhighguiめちゃ面白いです。

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

タイトルでDeep Learningと銘打っておきながら、全然ディープラーニングネタがないなあと思いつつ、またもや違う内容です。

OpenCVが便利すぎていつもお世話になっているのですが、今までほとんど使ってこなかったマウス操作(+描画)について、かなり遊べるということがわかったので、メモします。

おおまかなところはここに書いてあるのですが、いかんせん、利用のイメージがつかみにくいところがあります。

色々と調べながら、画像を拡大するツール(画像参照)を作ってみましたので、備忘としてコードを残しておくことにします。

まずは基本の理解

まずは、上のサイトで書かれている画像上に円を描画することから始めます。
私の場合、使った画像が大きすぎて扱いづらかったので、サイズ変更の処理を入れています。

import numpy as np
import cv2

# 画像読み込み
img = cv2.imread('img00001.jpg')

# 高さが500ピクセルになるようにサイズ変更
h, w, _ = img.shape
resize_rate = 500/h
scaled_img = cv2.resize(img, (int(w*resize_rate), 500))

# マウス操作の定義
def draw_circle(event, x, y, flags, param):
    print(x, y, event, flags, param)
    if event == cv2.EVENT_LBUTTONDBLCLK:
        cv2.circle(scaled_img,(x,y),100,(255,0,0),-1)
    return scaled_img

param = {'a':1, 'b':2}

# 画像表示
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle, param={'a':1, 'b':2})

# escキーが押されると画面を閉じる
while(1):
    cv2.imshow('image',scaled_img)
    if cv2.waitKey(20) & 0xFF == 27:
        break
cv2.destroyAllWindows()

importやらサイズ変更やらは、見慣れたいつもの処理ですが、18-19行目の画像表示のところで、cv2.namedWindow()とcv2.setMouseCallback()という関数が出てきます。
このcv2.setMouseCallback()が、マウス操作を定義するための関数です。

もう少し詳しく見てみます。

マウス操作の定義

# 画像表示
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle)

cv2.namedWindow(‘image’)は、表示するウィンドウに名前をつけています。マウス操作は、ウィンドウ上でマウスがどう動いたかを判断する必要があるため、複数のウィンドウに対応できるように各ウィンドウを識別するための名前をつけます。

cv2.setMouseCallback(‘image’,draw_circle)で指定のウィンドウ(ここでは’image’ウィンドウです)上でマウスがなんらかの動作をしたときに、draw_circleに飛ぶようにされています。

次にdraw_circleを見てみましょう。

# 円の描画
def draw_circle(event, x, y, flags, param):
    if event == cv2.EVENT_LBUTTONDBLCLK:
        cv2.circle(scaled_img,(x,y),100,(255,0,0),-1)

cv2.setMouseCallback()から渡された変数は5つあります。

  • event: 発生したマウスイベント
  • x, y: そのイベントが発生したときのカーソル位置(対象ウィンドウ上)
  • flags: イベント発生時に押されていたキー
  • param: その他、設定された変数など

マウスイベント

マウスイベントとは、たとえば以下のようなものがあります。

  • cv2.EVENT_LBUTTONDBLCLK:マウスの左ボタンがダブルクリック
  • cv2.EVENT_MOUSEMOVE:マウスがドラッグ

フラグ

flagは明示的に使ったことはありませんが、Alt, ctrl, shiftのキーが押されたor何も押されていないのフラグだそうです。

渡された情報の確認

マウスイベントはマウスが動くだけで発生しますので、imageウィンドウ上でマウスを動かすたびに、draw_circle()に飛んでいることになります。
こんなふうに、print()を入れてマウスを動かしたり、クリックしたりしてみると、

# 円の描画
def draw_circle(event, x, y, flags, param):
    print(x, y, event, flags, param)
    if event == cv2.EVENT_LBUTTONDBLCLK:
        cv2.circle(scaled_img,(x,y),100,(255,0,0),-1)

こんな感じで、座標とevent, flag, paramが表示されました。

386 179 0 0 None
385 180 0 0 None
385 180 0 0 None
385 180 1 1 None
385 181 0 1 None
385 181 4 1 None
384 181 0 0 None
384 181 0 0 None
384 181 1 1 None
384 181 4 1 None
384 181 7 1 None
384 181 1 1 None
384 181 4 1 None

せっかくなので、paramも入れてみます。

# 画像表示
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle, param={'a':1, 'b':2})

設定した値がちゃんと渡されていることが分かりました。

407 209 0 0 {'a': 1, 'b': 2}
409 211 0 0 {'a': 1, 'b': 2}
414 215 0 0 {'a': 1, 'b': 2}
415 216 0 0 {'a': 1, 'b': 2}
416 216 0 0 {'a': 1, 'b': 2}
416 216 1 1 {'a': 1, 'b': 2}
416 216 4 1 {'a': 1, 'b': 2}

まだまだ勉強不足なのですが、どうもcv2.setMouseCallback()は普通の関数とは違って、返り値を設定することができないようです。
変数のやり取りには若干クセがある印象です。

ウィンドウを閉じる

では、最後のブロックを見てみます。
これは画面を閉じるときの条件を設定したものです。
while(1)はコードによってはwhile(True)と書かれていることもあります。Trueが変わることないので、breakが入らない限り、永遠に繰り返しになります。

では、breakのタイミングは何か?というと、cv2.waitKey(20)かつ0xFF==27のときです。
cv2.waitKey()というのはキー入力されるまで、指定時間分待機することを表します。括弧内が指定時間(ミリ秒)で、0.02秒だけ入力を待つということになります。
その入力が、0xFF==27で、これは入力がescキーであった場合を表します。

つまり、escキーが入力されるまでプログラムは0.02秒間隔でscaled_imgを表示し直し続けるということです。

# escキーが押されると画面を閉じる
while(1):
    cv2.imshow('image',scaled_img)
    if cv2.waitKey(20) & 0xFF == 27:
        break
cv2.destroyAllWindows()

読み解きだけで長くなってしまったため、ズーム表示のコードは次回に回すことにします。

奥が深い。

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

ネットワークの重みを表示する

学習の過程で重みが更新されているかを確認したいときに、毎回とまどいながら設定していましたので、備忘的に書いておくことにします。

例として、torchvisionで読み込んだVGG16の重みを確認してみます。

学習済みモデルの読み込み

import numpy as np
import torch
import torchvision.models as models

# VGG16のpretrainモデルを読み込み
model = models.vgg16(pretrained=True)

まずは、読み込んだモデルの形状を確認します。

print(model)

VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)

VGG16は、「features」に13の畳み込み層、「classifier」に3つの全結合層が存在しているのがわかります。13+3=16なので、VGG16と呼ばれているわけですね。

重みの取り出し

では、ここに含まれている重みを見るにはどうすればいいでしょうか。重みは、モデルの「state_dict」メソッドに辞書形式で格納されています。
state_dictを取り出して、表示してみます。

# state_dictの呼び出し
state_dict = model.state_dict()

# 項目表示
print(state_dict.keys())

odict_keys([‘features.0.weight’, ‘features.0.bias’, ‘features.2.weight’, ‘features.2.bias’, ‘features.5.weight’, ‘features.5.bias’, ‘features.7.weight’, ‘features.7.bias’, ‘features.10.weight’, ‘features.10.bias’, ‘features.12.weight’, ‘features.12.bias’, ‘features.14.weight’, ‘features.14.bias’, ‘features.17.weight’, ‘features.17.bias’, ‘features.19.weight’, ‘features.19.bias’, ‘features.21.weight’, ‘features.21.bias’, ‘features.24.weight’, ‘features.24.bias’, ‘features.26.weight’, ‘features.26.bias’, ‘features.28.weight’, ‘features.28.bias’, ‘classifier.0.weight’, ‘classifier.0.bias’, ‘classifier.3.weight’, ‘classifier.3.bias’, ‘classifier.6.weight’, ‘classifier.6.bias’])

これがVGG16が保持・学習する重み群です。名前についている番号は、上で表示したmodelのレイヤ番号(カッコ内の項番)に対応します。

state_dictは辞書形式のため、例えば7番目のfeaturesの重みを確認したいときは、以下のように呼び出せます。

print(state_dict['features.7.weight'])

tensor([[[[ 2.5788e-02, -1.9852e-02, -1.0697e-02], [-1.6114e-02, -4.1759e-03, 8.4582e-03], [ 4.0309e-03, 1.8973e-02, 3.8059e-02]], [[-5.4261e-02, -2.9872e-02, 1.1506e-02], [-1.8266e-02, -1.5708e-02, -1.2726e-02], [ 2.0867e-02, -5.6425e-03, -5.1218e-04]], [[ 1.8961e-02, -2.3766e-03, 6.1427e-03], [-5.1761e-02, -1.7272e-02, 8.7075e-03], [-4.4383e-02, 2.3661e-02, 9.0710e-02]], …, (以下略)

辞書形式でテンソルが格納されているだけなので、ここまでできてしまえば、情報を収集するのは慣れたやり方でできます。

# 重みのサイズを確認
print(state_dict['features.7.weight'].size()) 

# 一部の重みだけを表示
print(state_dict['features.7.weight'][0,0,0,0])

torch.Size([128, 128, 3, 3])
tensor(0.0258)

中身の確認はこれでOKです。

出力ディレクトリの準備 (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')

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

Scheduler

学習を行うときの学習率の設定はなかなか悩ましいものがあります。
収束は早くしてほしいけど、きちんとロスの底にたどり着いてほしい。

そういう場合に、Schedulerの機能を使います。Schedulerは学習率を習の途中で変化させることができます。

下の図は、学習率が学習に与える影響を表しています。グラフの底になるように学習をしたいのですが、学習率が高すぎる(左)と底を飛び越えてしまう可能性があり、低すぎる(中央)と収束に時間がかかったり局所解に陥る可能性もあります。
なので、右のように学習の進行に従って学習率を下げていくのが効率的な学習と考えることができます。

PyTorchにもそんなschedulerがいくつか用意されています。
全部見ようと思ったのですが、理解するのが大変そうなので、考え方が分かりやすかったものを2つだけピックアップすることにします。

torch.optim.lr_scheduler.StepLR

一番シンプルなアップデート方法でしょうか。

【書式】StepLR(optimizer, step_size, gamma)

optimizer:ラップ対象のオプティマイザ。SGDとかAdamやらを定義したものが個々に入ります。
step_size:更新タイミングのエポック数
例えば、step_size=30を設定した場合、30, 60, 90, …のタイミングで学習率にgammaが乗算されます。
gamma:更新率。デフォルトは0.1でこの場合、更新タイミングで学習率が1/10になっていきます。

更新タイミングを2エポックごとに設定して、学習率の推移を見ていきます。
学習率の更新はscheduler.step()で、学習率の取得はscheduler.get_lr()で行えます。
※コードは一部を抜き出しています。

num_epoch = 10
scheduler = optim.lr_scheduler.StepLR(opt, step_size=2, gamma=0.1)

for m in range(num_epoch):
    for i_batch, sample_batched in enumerate(train_data):
        x = model(sample_batched[0])
        y = sample_batched[1]

        loss = loss_func(x, y)

        loss.backward()
        opt.step()
        opt.zero_grad()

        print('epoch:{}, lr:{}'.format(epoch, scheduler.get_lr()[0]))       
        scheduler.step()

結果は以下となり、2エポックごとに学習率が1/10になっていることがわかります。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.0001
epoch:3, lr:0.0001
epoch:4, lr:1.0000000000000003e-05
epoch:5, lr:1.0000000000000003e-05
epoch:6, lr:1.0000000000000002e-06
epoch:7, lr:1.0000000000000002e-06
epoch:8, lr:1.0000000000000002e-07
epoch:9, lr:1.0000000000000002e-07

torch.optim.lr_scheduler.MultiStepLR

Multiという単語がついているように、 こちらの関数はより柔軟な設定ができます。

【書式】MultiStepLR(optimizer, milestones, gamma)

違いは、milestonesです。
milestones:学習率の更新エポックのリスト
milestonesのエポックごとに、学習率はgammaを乗算した値になります。

schedulerの設定を、2エポック、6エポックで学習率の更新をするように変更して結果を見てみます。変えるのはこの行だけです。

optim.lr_scheduler.MultiStepLR(opt, milestones=[2, 6], gamma=0.1)

動かした結果です。設定通り、2エポックと6エポックで学習率が1/10になっています。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.0001
epoch:3, lr:0.0001
epoch:4, lr:0.0001
epoch:5, lr:0.0001
epoch:6, lr:1.0000000000000003e-05
epoch:7, lr:1.0000000000000003e-05
epoch:8, lr:1.0000000000000003e-05
epoch:9, lr:1.0000000000000003e-05

他にもいろいろな更新方法があるようなので、かなりフレキシブルな学習が行えるのですね。

追記

スケジューラの動作について、PyTorch1.1にはどうもバグがあるみたいです。
参照:https://github.com/pytorch/pytorch/issues/22107

import torch 
print(torch.__version__) 

1.1.0

PyTorch1.1のバージョンで、StepLR()を動かしてみます。
2エポックごとだと動きが分かりづらいため、step_sizeを4にします。

scheduler = optim.lr_scheduler.StepLR(opt, step_size=4, gamma=0.1)

下に示すように、更新エポックのときだけ学習率がおかしくなっています。gammaが2回かけられているみたいですね。

epoch:0, lr:0.001
epoch:1, lr:0.001
epoch:2, lr:0.001
epoch:3, lr:0.001
epoch:4, lr:1e-05
epoch:5, lr:0.0001
epoch:6, lr:0.0001
epoch:7, lr:0.0001
epoch:8, lr:1.0000000000000002e-06
epoch:9, lr:1e-05

最初に動かした環境がPyTorch1.1だったので、結構はまってしまいました。こういうこともあるんですね。