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めちゃ面白いです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です