Skip to content

Mouse as a Paint-Brush

이 문서의 코드는 cv2.imshow가 가능한 local 서버에서 동작하는 jupyter note 등을 대상으로 구현됨.
Colab 이나 원격지 서버에서 동작중인 jupyter note등에서는 동작하지 않음.

Goal

  • OpenCV 에서 mouse event를 다루는 방법을 익힌다.
    • mouse event를 처리할 callback함수를 만드는 법을 익힌다.
    • 실제로는 cv2.setMouseCallback() 함수를 통해 callback함수 등록하는 법을 익힌다.

Simple Demo

이를 위해 더블클릭을 한 지점에 원을 그리는 간단한 application을 만들어 본다.

  • 우선, 마우스에서 발생하는 이벤트 Mouse event 가 발생시 호출되도록 지정된 mouse callback function 를 구현한다.
  • Mouse eventleft-button down, left-button up, left-button double-click 등과 같이 모든 마우스 관련 이벤트를 가르킴.
  • Mouse event 가 발생시, 모든 경우 이벤트와 관련 마우스의 위치 좌표 (x,y)가 제공된다.
  • 이벤트 종류와 위치 정보를 가지고 원하는 처리를 수행하도록 cv2.setMouseCallback() 를 구현하면 된다.

cv2에서 사용가능한 event가 무엇이 있는지는 다음의 코드를 통해 확인할 수 있다.

import cv2

events = [i for i in dir(cv2) if 'EVENT' in i]
print(events)

결과로 다양한 event들을 확인 가능하다.

  • 'EVENT_FLAG_ALTKEY',
  • 'EVENT_FLAG_CTRLKEY',
  • 'EVENT_FLAG_LBUTTON',
  • 'EVENT_FLAG_MBUTTON',
  • 'EVENT_FLAG_RBUTTON',
  • 'EVENT_FLAG_SHIFTKEY',
  • 'EVENT_LBUTTONDBLCLK',
  • 'EVENT_LBUTTONDOWN',
  • 'EVENT_LBUTTONUP',
  • 'EVENT_MBUTTONDBLCLK',
  • 'EVENT_MBUTTONDOWN',
  • 'EVENT_MBUTTONUP',
  • 'EVENT_MOUSEHWHEEL',
  • 'EVENT_MOUSEMOVE',
  • 'EVENT_MOUSEWHEEL',
  • 'EVENT_RBUTTONDBLCLK',
  • 'EVENT_RBUTTONDOWN',
  • 'EVENT_RBUTTONUP'

mouse callback function 은 파라메터 수 등의 정의된 형식을 변경해선 안된다. 형식은 어디서나 동일하게 구현해야 한다. callback으로 등록되기 위해서는 각 파라메터가 openCV가 정한대로 정의되어야 한다.

즉, 내부에서 어떻게 이벤트를 처리할지는 개발자가 본인이 원하는대로 구현하나, 함수의 형식은 꼭 지켜야 해당 함수가 이벤트 발생시 호출이 가능 하다

다음은 더블클릭 이벤트 발생시, 해당 위치에 원을 그리는 구현이 되어 있다.

import cv2
import numpy as np

# mouse callback function
def draw_circle(event,x,y,flags,param):
    if event == cv2.EVENT_LBUTTONDBLCLK: # double click event!
        cv2.circle(img,(x,y),100,(255,0,0),-1)

# Create a black image, a window and bind the function to window
img = np.zeros((512,512,3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle)

while True:
    cv2.imshow('image',img)
    if cv2.waitKey(20) & 0xFF == 27: # enter ESC
        break
    if cv2.getWindowProperty('image', cv2.WND_PROP_VISIBLE ) <1:
        break
cv2.destroyAllWindows()

More Advanced Demo

다음은 좀 더 기능이 추가된 application을 구현한 코드임.

  • 원과 네모를 선택하여 그릴 수 있고, 네모의 경우, 마우스를 드래그하여 크기를 결정할 수 있음.
  • 이를 위해 mouse callback function 에서 mode에 따라 다른 도형을 그리도록 만들어짐.
import numpy as np
import cv2 as cv
drawing = False # true if mouse is pressed
mode = True # if True, draw rectangle. Press 'm' to toggle to curve
ix,iy = -1,-1
# mouse callback function
def draw_circle(event,x,y,flags,param):
    global ix,iy,drawing,mode
    if event == cv.EVENT_LBUTTONDOWN:
        drawing = True
        ix,iy = x,y
    elif event == cv.EVENT_MOUSEMOVE:
        if drawing == True:
            if mode == True:
                cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
            else:
                cv.circle(img,(x,y),5,(0,0,255),-1)
    elif event == cv.EVENT_LBUTTONUP:
        drawing = False
        if mode == True:
            cv.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
        else:
            cv.circle(img,(x,y),5,(0,0,255),-1)
  • 위에서 구현한 callback 함수를 OpenCV의 window 와 binding시킴 : cv2.setMouseCallback 이용.
  • mode를 변경하기 위해서 키보드의 키 m을 toggle 키로 동작하도록 main loop에서 해당 구현을 추가함.

다음 조각코드가 실행코드임.

img = np.zeros((512,512,3), np.uint8)
cv.namedWindow('image')
cv.setMouseCallback('image',draw_circle)
while(1):
    cv.imshow('image',img)
    k = cv.waitKey(1) & 0xFF
    if k == ord('m'):
        mode = not mode
    elif k == 27:
        break
    if cv2.getWindowProperty('image', cv2.WND_PROP_VISIBLE ) <1:
        break
cv.destroyAllWindows()

Note0 : 기능 개선

  • 원을 그리는 부분을 내접타원을 그리는 것으로 변경해보라.
  • 드래그 중에 그려지는 부분이 겹쳐지는 버그를 수정해보라.
import cv2
import numpy as np

drawing = False # true if mouse is pressed
mode    = True  # if True, draw rectangle. Press 'm' to toggle to curve
ix,iy = -1,-1

# mouse callback function
def draw(event,x,y,flags,param):
    global ix, iy, drawing, mode

    tmp = None

    if event == cv2.EVENT_LBUTTONDOWN: # left button down event
        drawing = True
        ix,iy = x,y

    elif event == cv2.EVENT_MOUSEMOVE: # mouse move event
        if drawing == True:
            tmp = img.copy()
            if mode == True:                
                cv2.rectangle(tmp,(ix,iy),(x,y),(0,255,0),-1)

            else:
                w = x-ix
                h = y-iy                                                
                cv2.ellipse(tmp, (ix+w//2,iy+h//2), 
                            (np.abs(w)//2,np.abs(h)//2),0,0,360, (0,0,255),-1)
                cv2.rectangle(tmp,(ix,iy),(x,y),(0,255,0),1)

            cv2.imshow("image",tmp)
            cv2.waitKey(1)

                # cv2.circle(img,(x,y),5,(0,0,255),-1)

    elif event == cv2.EVENT_LBUTTONUP: # left button up event
        drawing = False
        if mode == True:
            cv2.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)            
        else:
            # if not (tmp is None):
            #     img = tmp.copy()
            w = x-ix
            h = y-iy
            # cv2.ellipse(img, ( (ix+w//2,iy+h//2), (w,h),0), (0,0,255),-1)
            cv2.ellipse(img, (ix+w//2,iy+h//2), (np.abs(w)//2,np.abs(h)//2),0,0,360, (0,0,255),-1)
            # cv2.rectangle(img,(ix,iy),(x,y),(0,255,0),1)
            # cv2.circle(img,(x,y),5,(0,0,255),-1)
            cv2.imshow("image",img)

다음은 수행코드임.

img = np.zeros((512,512,3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw)

while(1):
    cv2.imshow('image',img)
    k = cv2.waitKey(1) & 0xFF
    if k == ord('m'):   # enter m
        mode = not mode
    elif k == 27:       # enter ESC
        break

    if cv2.getWindowProperty('image', cv2.WND_PROP_VISIBLE ) <1:
        break

cv2.destroyAllWindows()

관련 자료