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()

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

奥が深い。

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)

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

OpenCVで画像マッチングをする

OpenCVというのは、コンピュータビジョン用のライブラリです。
カメラキャリブレーションもできたり、(多分)すごい機能がたくさん入っているんだと思います。

その道の人には怒られるかもしれないですが、普通に画像処理をするにもなかなか便利なライブラリがたくさん入っていたり、画像の入出力も分かりやすかったりで、Pythonで画像を扱うとき、私はだいたいOpenCV経由でやっています。

さて、OpenCVを使って画像マッチングというものを行うことができます。
どんなものかというと、同じ対象が写っている2枚の画像から、対応点を見つけ出すという処理です。

何のために行うのかというと、例えば以下のような用途でしょうか。
・パノラマ画像の作成
 重複するエリアの同じ地点を見つけ出せるので、ずれることなく重ねられます。
・3次元形状の復元
 視点の異なる2点間の対応点が取れれば、カメラの相対姿勢が分かり、
 対象の形状を推定できます

実際にやってみた例が下の画像です。

これはORBによるもので、若干、マッチングポイントに偏りがあるような気がしますが、写真の視点と範囲が異なっていても、対応が取れていることがわかります。
ロープウェーに乗っているときに撮った写真ですので、撮影したときの高さに差異があって、写真の範囲も重なる部分はありますが、異なっています。

OpenCVのインストール

OpenCVは完全フリーなものと権利的な制約のあるものとでパッケージが明示的に分かれています。制約付きのものはcontribというパッケージに入っています。
contribに含まれているライブラリには、例えば、SIFTやSURFなどの特徴抽出・記述アルゴリズムがあります。

インストールは 以下のコマンドで 簡単にできます。1行目のopencv-pythonだけでも動くのですが、私はSIFTも使いたいのでcontribも入れました。
なお、contribも入れる場合、バージョン指定は必須です。

★2019/9/8時点でバージョン3.4.2.16がなくなっていたようでしたので、指定バージョンを修正しました。

pip install opencv-python==3.4.2.17
pip install opencv-contrib-python==3.4.2.17

そうでないと、エラーが出ることがあります(下はSIFTを動かしたときに発生)。多分、デフォルトだとバージョンの不一致が生じるのでしょう。

# -----出力-----
cv2.error: OpenCV(4.1.0) /io/opencv_contrib/modules/xfeatures2d/src/sift.cpp:1207: error: (-213:The function/feature is not implemented) This algorithm is patented and is excluded in this configuration; Set OPENCV_ENABLE_NONFREE CMake option and rebuild the library in function 'create'

特徴点抽出

早速動かしてみます。

マッチングは二段階の処理で実施しています。
①画像中から特徴点を見つけて、その特徴量を記述する
②2画像間の特徴量を比較して、似ている特徴点同士をマッチングする。

この特徴量抽出/記述に各アルゴリズムの工夫があります。
英語論文なので、そのうち読んでまとめたいなと思います。

基本的なコードは、ここを参考にしています。ただし、仕様の変更(?)により、そのままでは動かない箇所がありますので、手を加えてはいます。

img1 = cv2.imread('IMG001.JPG')
img2 = cv2.imread('IMG002.JPG')

h,w,_ = img1.shape
rate = int(max(h,w)/512)

# 画像サイズを最大512に変更
img1 = cv2.resize(img1, (int(w/rate), int(h/rate)))
img2 = cv2.resize(img2, (int(w/rate), int(h/rate)))

detector = cv2.ORB_create()

# 特徴点抽出
kp1, des1 = detector.detectAndCompute(img1, None)
kp2, des2 = detector.detectAndCompute(img2, None)
    
bf = cv2.BFMatcher()

# マッチング
matches = bf.match(des1, des2)
matches = sorted(matches, key = lambda x:x.distance)

# 画像書き出し、出力
out = cv2.drawMatches(img1, kp1, img2, kp2, matches[:10], None, flags=2)
cv2.imwrite('outimg.png', out)

detector=cv2.ORB_create()の箇所で、ORBの検出器のインスタンスを作成しています。
ここでは、ORBを使用しています。

detector.detectAndCompute()で、入力画像に対するキーポイント(kp)と特徴量(des)を出力します。なお、detector.detect()だけにすると、キーポイントだけを出力します。
マッチングには特徴量を使用しますので、マッチングしたいときは、detectAndCompute()を選びます。

マッチングポイントを確認する

Query画像とTrain画像で抽出したキーポイントのマッチング結果を確認するとき、drawMatches()を使うと、キーポイント同士を結んだ画像を出力できます。

では、具体的に座標値はいくつになるのでしょうか。
意外と情報が見つからなかったりしますので、以下、メモです。

Matchした点を単純にprintで出力してみます。

print(matches[:10])

DMatch型オブジェクトとして返されて中が見えません。
マッチングした点の情報を知るには、属性にアクセスしなければいけないのです。

# -----出力-----
[<DMatch 0x7fe03e2ac7b0>, <DMatch 0x7fe03e2ad3b0>, <DMatch 0x7fe03e2ac790>, <DMatch 0x7fe03e2ad6f0>, <DMatch 0x7fe03e2ab8d0>, <DMatch 0x7fe03e2ad310>, <DMatch 0x7fe03e2ad530>, <DMatch 0x7fe03e2adf10>, <DMatch 0x7fe03e2ae0f0>, <DMatch 0x7fe03e2adad0>]

DMatch型オブジェクトの持っている属性は、以下の4つです。
・distance:特徴量記述子間の距離
・trainIdx:学習記述子(参照データ)の記述子のインデックス
・queryIdx:クエリ記述子(検索データ)の記述子のインデックス
・imgIdx:学習画像のインデックス

ちなみに、上のコード「bf.match(des1, des2)」で、des1がクエリ、des2が学習画像に該当します。des1と近い特徴をdes2に問い合わせる(クエリ)というイメージですね。

では、1番目のマッチングの情報を見てみます。

print(matches[0].distance)
print(matches[0].trainIdx)
print(matches[0].queryIdx)
print(matches[0].imgIdx)
# -----出力-----
121.76616668701172
214
207
0

ここで、でてきたtrainIdxとqueryIdxはdetectAndComputeで出力されたキーポイントのインデックスです。
したがって、次に 画像の特徴抽出の際に計算したキーポイント情報にアクセス して、具体的に画像座標のどこの点が該当するかを調べなくてはなりません。

このキーポイント情報もKeypoint型のオブジェクトですので、同じように属性にアクセスします。

keypointオブジェクトは以下の属性を持っています。
・pt:キーポイントの座標
・size:キーポイント周辺の重要領域の直径
・angle:計算されたキーポイントの方向(計算できない場合は -1 )
・response:最も強いキーポイントが選択されたときの応答
・octave:キーポイントが抽出されるオクターブ(ピラミッドの階層)
・class_id:オブジェクトクラス

print(kp1[0].pt)
print(kp1[0].size)
print(kp1[0].angle)
print(kp1[0].response)
print(kp1[0].octave)
print(kp1[0].class_id)

クエリ側の最初(0番目)のキーポイントの情報はこんな感じです。

# -----出力-----
(60.0, 129.0)
31.0
276.7517395019531
0.001297724898904562
0
-1

とりあえず、キーポイントの情報で最も距離の近い(似ている)キーポイントを見てみます。
matchesに並べ替えを行っていましたので、一番先頭に来ているインデックスの座標を見てみます。

print('query: ', kp1[matches[0].queryIdx].pt)
print('train: ', kp2[matches[0].trainIdx].pt)

以下の数値が出力されました。
整数にすると、query: (108, 171) , train: (292, 266)です。

# -----出力------
query:  (108.00000762939453, 171.36000061035156)
train:  (292.32000732421875, 266.4000244140625)

画像に重ねるとこのようになりました。
建物の角の部分を最も似ていると判断したようです。