by museonghwang

OpenCV Image Processing 관심영역

|

해당 게시물은 파이썬으로 만드는 OpenCV 프로젝트(이세우 저) 를 바탕으로 작성되었습니다.


관심영역

이미지에 어떤 연산을 적용해서 새로운 이미지나 정보를 얻어내려고 할 때 전체 이미지를 대상으로 연산을 하는 것보다는 관심이 있는 부분만 잘라내서 하는 것이 훨씬 효과적입니다. 예를 들어 어떤 사진에 찍힌 사람이 누군지 알고 싶다면 사진 전체를 분석하는 것보다 인물이 있는 부분이나 얼굴이 있는 부분만 잘라서 분석하면 훨씬 쉽고 빠를 것입니다.

이렇게 관심 있는 영역만 잘라내서 연산을 하면 단순히 연산할 데이터의 양을 줄이고 수행 시간을 단축시키는 이점도 있지만, 데이터의 양이 줄어 들면 그 형태도 단순해지므로 적용해야 하는 알고리즘도 단순해지는 이점도 있습니다. 또한, 이미지 연산은 항상 좌표를 기반으로 해야 하는데, 그 영역이 클 때보다 작을 때 좌표 구하기가 쉽다는 이점도 있습니다.

  • NumPy를 이용해서 관심영역을 지정할 때 주의해야 할 사항 두 가지
    1. NumPy 배열은 행(row), 열(column) 순으로 접근하므로 반드시 높이(height), 폭(width) 순으로 지정해야 합니다.
    2. NumPy 배열의 슬라이싱(slicing)과 Python의 리스트(list)의 슬라이싱 방식이 다릅니다.
      • 파이썬 리스트의 슬라이싱은 새로운 리스트 객체를 반환하는 데 반해, NumPy 배열의 슬라이싱은 원본의 참조를 반환합니다.
      • NumPy 배열 객체는 슬라이싱 연산해서 얻은 결과의 값을 수정하면 슬라이싱하기 전의 원본 배열 객체에도 똑같이 값이 달라집니다.
      • 만약 원본과는 무관한 새로운 작업을 하려면 반드시 슬라이싱 결과에 복제본을 생성해서 작업해야 합니다. 복제본은 copy() 함수로 만들 수 있습니다.

1. 관심영역 지정

전체 이미지에서 연산과 분석의 대상이 되는 영역만을 지정하고 떼어내는 것관심영역(Region Of Interest, ROI)을 지정한다고 합니다.

전체 이미지가 img라는 변수에 있을 때, 관심 있는 영역의 좌표가 x, y이고 영역의폭이 w, 높이가 h라고 하면 이것을 이용하여 관심영역을 지정하는 코드는 다음과 같습니다. img의 y행에서부터 y+h행까지, x열에서 x+w열까지를 슬라이싱한 것입니다.

roi = img[y:y+h, x:x+w]

image

위 사진은 부두의 일몰 사진인데, 일몰 중인 태양을 관심영역으로 지정하고 사각형으로 표시했습니다. 이미지에서 태양 영역의 시작 좌표는 x:320, y:150이고, 태양 영역의 크기는 50 × 50입니다. 앞의 결과를 나타내는 코드는 다음과 같습니다.

'''관심영역 지정'''
import cv2
import numpy as np

img = cv2.imread('./img/sunset.jpg')

x = 320; y = 150; w = 50; h = 50    # roi 좌표
roi = img[y:y+h, x:x+w]             # roi 지정 ---①

print(roi.shape)    # roi shape, (50,50,3)

cv2.rectangle(roi, (0,0), (h-1, w-1), (0,255,0)) # roi 전체에 사각형 그리기 ---②
cv2.imshow("img", img)

key = cv2.waitKey(0)
print(key)
cv2.destroyAllWindows()

위 코드 ①에서 관심영역을 지정하고 있습니다. 좌표만 알고 있다면 관심영역을 지정하는 것은 별로 어렵지 않습니다. 이렇게 관심영역을 지정하고 나서 해당 영역에 사각형을 표시하기 위한 코드는 ②에 나타납니다. 만약 관심영역을 따로 지정하지 않았다면 이 코드는 다음과 같게 됩니다.

cv2.rectangle(img, (x,y), (x+w,y+h), (0,255,0))

코드의 양은 별로 차이가 없지만, 사각형을 그리기 위한 좌표를 지정하는 것이 위 코드 ② 에서보다 불편해 보입니다. 그저 0에서부터 끝까지 지정했기 때문입니다.

여기에 다음 코드처럼 간단한 코드를 추가하면 지정한 관심영역을 원본 이미지에 추가해서 태양이 두 개로 보이게 하거나 지정한 관심영역만 새 창에 표시할 수 있습니다.

'''관심영역 복제 및 새 창 띄우기'''
import cv2
import numpy as np

img = cv2.imread('./img/sunset.jpg')

x = 320; y = 150; w = 50; h = 50
roi = img[y:y+h, x:x+w]     # roi 지정
img2 = roi.copy()           # roi 배열 복제 ---①

img[y:y+h, x+w:x+w+w] = roi # 새로운 좌표에 roi 추가, 태양 2개 만들기
cv2.rectangle(img, (x,y), (x+w+w, y+h), (0,255,0)) # 2개의 태양 영역에 사각형 표시

cv2.imshow("img", img)      # 원본 이미지 출력
cv2.imshow("roi", img2)     # roi 만 따로 출력

cv2.waitKey(0)
cv2.destroyAllWindows()

image

image

위의 코드 ① img2 = roi.copy() 은 관심영역으로 지정한 배열을 복제해서 새로운 배열을 생성합니다. 만약 copy() 함수로 복제본을 만들지 않았다면 새 창에 띄운 태양 그림에도 초록색 사각형이 그려지게 됩니다. 이렇게 관심영역을 지정할 때는 원본 배열의 슬라이싱만을 이용하는 것이 편리할 때도 있고 원본하고는 완전히 분리된 복제본을 사용해야 할 때도 있습니다.

image

2. 마우스로 관심영역 지정

관심영역을 지정하려면 시작 좌표와 크기(폭, 높이) 값이 꼭 필요한데, 매번 눈 대중으로 값을 지정하면서 찾아내는 것은 무척 피곤합니다. 이럴 때는 마우스를 이용해서 원하는 영역을 직접 지정하고 좌표를 알아내면 편리합니다. 마우스 이벤트를 적용하면 쉽게 구현할 수 있습니다.

다음 예제는 마우스 이벤트 처리를 적용해서 마우스로 관심영역을 지정하고 잘라낸 부분만 새 창에 표시하고 파일로 저장하는 예제입니다.

'''마우스로 관심영역 지정'''
import cv2
import numpy as np

isDragging = False                      # 마우스 드래그 상태 저장 
x0, y0, w, h = -1, -1, -1, -1           # 영역 선택 좌표 저장
blue, red = (255,0,0), (0,0,255)        # 색상 값 

def onMouse(event, x, y, flags, param): # 마우스 이벤트 핸들 함수  ---①
    global isDragging, x0, y0, img      # 전역변수 참조
    if event == cv2.EVENT_LBUTTONDOWN:  # 왼쪽 마우스 버튼 다운, 드래그 시작 ---②
        isDragging = True
        x0 = x
        y0 = y
    elif event == cv2.EVENT_MOUSEMOVE:  # 마우스 움직임 ---③
        if isDragging:                  # 드래그 진행 중
            img_draw = img.copy()       # 사각형 그림 표현을 위한 이미지 복제
            cv2.rectangle(img_draw, (x0, y0), (x, y), blue, 2) # 드래그 진행 영역 표시
            cv2.imshow('img', img_draw) # 사각형 표시된 그림 화면 출력
    elif event == cv2.EVENT_LBUTTONUP:  # 왼쪽 마우스 버튼 업 ---④
        if isDragging:                  # 드래그 중지
            isDragging = False          
            w = x - x0                  # 드래그 영역 폭 계산
            h = y - y0                  # 드래그 영역 높이 계산
            print("x:%d, y:%d, w:%d, h:%d" % (x0, y0, w, h))
            if w > 0 and h > 0:         # 폭과 높이가 양수이면 드래그 방향이 옳음 ---⑤
                img_draw = img.copy()   # 선택 영역에 사각형 그림을 표시할 이미지 복제
                # 선택 영역에 빨간 사각형 표시
                cv2.rectangle(img_draw, (x0, y0), (x, y), red, 2) 
                cv2.imshow('img', img_draw) # 빨간 사각형 그려진 이미지 화면 출력
                roi = img[y0:y0+h, x0:x0+w] # 원본 이미지에서 선택 영역만 ROI로 지정 ---⑥
                cv2.imshow('cropped', roi)  # ROI 지정 영역을 새창으로 표시
                cv2.moveWindow('cropped', 0, 0) # 새창을 화면 좌측 상단에 이동
                cv2.imwrite('./cropped.jpg', roi) # ROI 영역만 파일로 저장 ---⑦
                print("croped.")
            else:
                cv2.imshow('img', img)  # 드래그 방향이 잘못된 경우 사각형 그림이 없는 원본 이미지 출력
                print("좌측 상단에서 우측 하단으로 영역을 드래그 하세요.")

img = cv2.imread('./img/wonyoung.jpg')
cv2.imshow('img', img)
cv2.setMouseCallback('img', onMouse) # 마우스 이벤트 등록 ---⑧
cv2.waitKey()
cv2.destroyAllWindows()

[output]
x:309, y:143, w:73, h:64
croped.

코드 ①에서 선언한 onMouse 함수를 코드 ⑧에서 마우스 콜백으로 등록합니다. onMouse 함수는 마우스를 조작할 때마다 호출되고 그중 세 가지 이벤트에 따라 분기합니다.

코드 ②는 마우스 왼쪽 버튼이 눌렸을 때 반응합니다. 처음 마우스를 드래그하는 지점을 x0, y0 전역변수에 저장하고 드래그가 시작되었다는 것을 기억하기 위해 isDragging 변수를 변경합니다.

마우스가 움직이면 코드 ③이 반응합니다. 왼쪽 마우스를 누른 상태에서 움직이는지를 확인하고 앞서 저장해 둔 드래그 시작 좌표로부터 파란색 사각형을 그립니다. 사각형을 그리기 전에 img_draw = img.copy() 코드로 원본 이미지를 복사하는 이유는 마우스가 움직일 때마다 사각형을 그리게 되는데, 매번 같은 이미지에 사각형을그리면 사각형이 누적된 채 그려져서 보기에 좋지 않기 때문입니다. 아무것도 그리지 않은 깨끗한 원본 이미지를 매번 새롭게 복사해서 거기에 사각형을 표시하고 화면에 표시합니다.

마지막으로 코드 ④는 가장 중요한 이벤트인 왼쪽 마우스 버튼을 손에서 뗄 때입니다. 실제로 원하는 영역을 모두 선택한 상태이므로 여기서 최종적인 좌표를 구해야 합니다. 드래그 상태를 저장해 놓은 isDragging 변수를 원래대로 되돌려 놓고, 최초 드래그가 일어난 지점인 x0, y0에서 현재의 x, y 좌표를 빼서 선택한 영역의 폭과 높이를 구합니다. 이렇게 구한 x, y, w, h 값으로 관심영역을 지정하면 됩니다. 본 예제에서는 관심영역에 빨간 사각형을 그리고, 관심영역을 새로운 창에 표시한 후 ‘cropped.jpg’라는 이름의 파일로 저장하였습니다. 이때에도 원본을 복사해서 거기에 빨간 사각형을 그려서 화면에 표시하고 실제 관심영역을 지정한 것은 원본 이미지입니다. 그렇지 않으면 따로 창을 띄워 표시한 관심영역과 저장한 그림 파일에도 빨간 사각형이 그려지기 때문입니다.

image

image

OpenCV 3이상 버전에서는 관심영역을 지정하기 위한 새로운 함수를 제공합니다. 이 함수를 사용하면 마우스 이벤트 처리를 위한 코드 없이도 마우스로 간단히 ROI를 지정할 수 있습니다.

  • ret = cv2.selectROI([win_name,] img[, showCrossHair=True, fromCenter=False])
    • win_name : ROI 선택을 진행할 창의 이름, str
    • img : ROI 선택을 진행할 이미지, NumPy ndarray
    • showCrossHair : 선택 영역 중심에 십자 모양 표시 여부
    • fromCenter : 마우스 시작 지점을 영역의 중심으로 지정
    • ret : 선택한 영역 좌표와 크기(x, y, w, h), 선택을 취소한 경우 모두 0

cv2.selectROI() 함수의 win_name에 창 이름을 지정하고 ROI 선택에 사용할 이미지를 img에 전달하면 마우스로 영역을 선택할 수 있습니다. 영역을 선택하고 나서 키보드의 스페이스 또는 엔터 키를 누르면 선택한 영역의 x, y 좌표와 영역의 폭과 높이를 튜플에 담아 반환합니다. 만약 선택을 취소하고 싶으면 키보드의 ‘c’ 키를 누르면 되는데, 이때에는 반환하는 모든 값이 0입니다.

'''selectROI로 관심영역 지정'''
import cv2,  numpy as np

img = cv2.imread('./img/wonyoung.jpg')

x, y, w, h = cv2.selectROI('img', img, False)
if w and h:
    roi = img[y:y+h, x:x+w]
    cv2.imshow('cropped', roi)  # ROI 지정 영역을 새창으로 표시
    cv2.moveWindow('cropped', 0, 0) # 새창을 화면 좌측 상단에 이동
    cv2.imwrite('./cropped2.jpg', roi)   # ROI 영역만 파일로 저장

cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

image

image