Elsaの技術日記(徒然なるままに)

主に自分で作ったアプリとかの報告・日記を記載

MENU

OpenCVでのマウスによるエリア指定方法

今回はOpenCVでマウスイベントを取得する方法とマウスクリックした位置を取得してエリア指定する方法をまとめていきたいと思います。
マウス操作で領域切り出しを実施してみたかったので、知れて良かった!!



■環境

 ・OS:WIndows10
 ・プラットフォーム:anaconda
 ・言語:Python

■マウスイベント取得

まずはOpenCVにてマウスイベントを取得する方法です。
コードはこちら。

import cv2

def onMouse(event, x, y, flag, params):
    a, b = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click " + str(a))
    
    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click " + str(b))

if __name__ == '__main__':
    img = cv2.imread("ファイル名")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [1, 2])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

やっていることですが、、、
マウス割り込み時の関数 onMouseを定義し、

def onMouse(event, x, y, flag, params):
    a, b = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click " + str(a))
    
    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click " + str(b))

このonMouseをマウスイベントとしてセットしていきます。

cv2.setMouseCallback(wname, onMouse, [1, 2])

第1引数にimshowで表示するwindow名、
第2引数にコールバック関数、
第3引数にセットした関数に渡す引数になります。

ここで、onMouse関数の引数であるx,yですがクリックした座標が格納されます。
このためこちらのように関数を作成すると、左クリックするごとに画像上に赤丸が付与されるようにすることもできます。

def onMouse(event, x, y, flag, params):
    wname, img = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click ")

動作させてみるとこんな感じです。
f:id:Elsammit:20210610225853g:plain

■選択した領域で囲う

では今度は先ほどの点を結んで領域として囲んでみます。
コードはこちらになります。

import cv2

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

def onMouse(event, x, y, flag, params):
    wname, img, ptlist = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        ptlist.add(x, y)
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        print("Right button Click ")
        for i in range(ptlist.pos):
                    cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                         (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)
        cv2.imshow(wname, img)

if __name__ == '__main__':
    img = cv2.imread("IMG_0967.JPG")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    ptlist = PointList(0)
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [wname, img, ptlist])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

先ほどと異なる点として、
まずクリックした位置を覚えておくクラス、
PointListクラスを定義。

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

そして左クリック毎に先ほどのクラス定義したオブジェクトptlistへ
クリックしたxy座標の値を記録しています。

ptlist.add(x, y)

最後に、右クリック時に記録していたplistからx,y座標を読み出し、線を引くことで領域指定が出来るようになります。

for i in range(ptlist.pos):
            cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)

さらに、右クリック時の処理に、

arr_ld = np.array(ptlist.ptlist)
rect = cv2.boundingRect(arr_ld)
x,y,w,h = rect
croped = img[y:y+h, x:x+w].copy()

pts = arr_ld - arr_ld.min(axis=0)
mask = np.zeros(croped.shape[:2], np.uint8)
cv2.drawContours(mask, [pts], -1, (255, 255, 255), -1, cv2.LINE_AA)
dst = cv2.bitwise_and(croped, croped, mask=mask)

img = cv2.fillConvexPoly(img, np.array(arr_ld, 'int32'), color=(255, 255, 255))

img_gray = cv2.cvtColor(dst, cv2.COLOR_BGR2GRAY)
ret, img_thresh = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)
cv2.imshow("wname", img_thresh)
cv2.imshow(wname, img)

を追加すると囲んだ領域に白くマスクを付けつつ、別画像として表示させることが可能になります。
動作としてはこんな感じ。
f:id:Elsammit:20210610231812g:plain

■最終的なコード

囲んだ領域に白くマスクを付けつつ、別画像として表示させる最終的なコードはこちらになります。

import cv2
import numpy as np

class PointList():
    def __init__(self, npoints):
        self.ptlist = []
        self.pos = 0
    
    def add(self, x, y):
        self.ptlist.append([x, y])
        self.pos += 1
        return True

def onMouse(event, x, y, flag, params):
    wname, img, ptlist = params
    if event == cv2.EVENT_LBUTTONDOWN:
        print("Left button Click ")
        ptlist.add(x, y)
        cv2.circle(img, (x, y), 3, (0, 0, 255), 3)
        cv2.imshow(wname, img)

    if event == cv2.EVENT_RBUTTONDOWN:
        if(ptlist.pos >= 3): 
            print("Right button Click ")
            for i in range(ptlist.pos):
                cv2.line(img, (ptlist.ptlist[i][0], ptlist.ptlist[i][1]),
                    (ptlist.ptlist[(i+1)%ptlist.pos][0], ptlist.ptlist[(i+1)%ptlist.pos][1]), (0, 0, 255), 3)
            arr_ld = np.array(ptlist.ptlist)
            rect = cv2.boundingRect(arr_ld)
            x,y,w,h = rect
            croped = img[y:y+h, x:x+w].copy()

            pts = arr_ld - arr_ld.min(axis=0)
            mask = np.zeros(croped.shape[:2], np.uint8)
            cv2.drawContours(mask, [pts], -1, (255, 255, 255), -1, cv2.LINE_AA)
            dst = cv2.bitwise_and(croped, croped, mask=mask)

            img = cv2.fillConvexPoly(img, np.array(arr_ld, 'int32'), color=(255, 255, 255))

            cv2.imshow("wname", dst)
            cv2.imshow(wname, img)
        else:
            print("NG")

if __name__ == '__main__':
    img = cv2.imread("IMG_0967.JPG")
    size = (752, 1008)
    
    img = cv2.resize(img, size)
    wname = "MouseEvent"
    ptlist = PointList(0)
    cv2.namedWindow(wname)
    cv2.setMouseCallback(wname, onMouse, [wname, img, ptlist])

    cv2.imshow(wname, img)
    while 1:
        if cv2.waitKey(10) == ord('q'):
            break
            
    cv2.destroyAllWindows()

■最後に

マウスでエリア指定して画像を切り出せるのは使い方によっては便利だな!!と思いました。
特に、Yoloなどの物体識別やOCRはもろに画像サイズが効いてくるので、エリアを指定し必要部分のみ抽出できるのはいいですね。