前回に引き続き、マウス操作です。
目標の、場所を決めて拡大表示を実装します。
目標の動作の定義
- 画像を縮小して全体を表示
- 縮小画像のカーソル位置の一定範囲を別ウィンドウで拡大
- ↑の拡大領域を縮小画面で矩形で表示
- 左ボタン押下+ドラッグで拡大領域を変更
- 左ボタンが押されていないときは拡大領域の変更なし
こんな感じです。
順に実装を乗せていきます。全体のコードは最後に。
ライブラリのインポート
当然、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めちゃ面白いです。
