귀퉁이 서재

OpenCV - 11. 2차원 히스토그램과 역투영(back project) 본문

OpenCV

OpenCV - 11. 2차원 히스토그램과 역투영(back project)

Baek Kyun Shin 2020. 9. 23. 21:30

이번 포스팅에서는 2차원 히스토그램과 역투영에 대해 알아보겠습니다. 이번 포스팅 역시 '파이썬으로 만드는 OpenCV 프로젝트(이세우 저)'를 정리한 것임을 밝힙니다.

코드: github.com/BaekKyunShin/OpenCV_Project_Python/tree/master/04.img_processing

2차원 히스토그램 (2D Histogram)

1차원 히스토그램은 이미지 안에 픽셀이 각각 몇 개인지를 표현합니다. 반면, 2차원 히스토그램은 축이 2개이고, 각 축이 만나는 지점의 개수를 표현합니다. 바로 예를 들어보겠습니다.

# 2D 히스토그램 (histo_2d.py)

import cv2
import matplotlib.pylab as plt

plt.style.use('classic')            # --①컬러 스타일을 1.x 스타일로 사용
img = cv2.imread('../img/mountain.jpg')

plt.subplot(131)
hist = cv2.calcHist([img], [0,1], None, [32,32], [0,256,0,256]) #--②
p = plt.imshow(hist)                                            #--③
plt.title('Blue and Green')                                     #--④
plt.colorbar(p)                                                 #--⑤


plt.subplot(132)
hist = cv2.calcHist([img], [1,2], None, [32,32], [0,256,0,256]) #--⑥
p = plt.imshow(hist)
plt.title('Green and Red')
plt.colorbar(p)

plt.subplot(133)
hist = cv2.calcHist([img], [0,2], None, [32,32], [0,256,0,256]) #--⑦
p = plt.imshow(hist)
plt.title('Blue and Red')
plt.colorbar(p)

plt.show()

역투영(Back Projection)

역투영이란 관심 영역의 히스토그램과 유사한 히스토그램을 갖는 영역을 찾아내는 기법입니다. 역투영을 활용하면 이미지 내에서 특정 물체나 배경을 분리할 수 있습니다. 아래 이미지를 보겠습니다. 이미지 내에서 잔디만 분리하고 싶은 경우 잔디에 해당하는 관심 영역(ROI, region of interest)을 지정하고 역투영을 적용합니다. 그러면 잔디에 해당하는 부분은 흰색으로, 잔디가 아닌 부분은 검은색으로 서로 분리가 됩니다. 다만 이 방법은 색상을 기준으로 분리하기 때문에 잔디와 비슷한 색상을 가진 다른 물체가 있는 경우 성능이 떨어집니다.

출처: docs.opencv.org

아래는 역투영을 활용하여 이미지를 분리하는 예제 코드입니다.

# 마우스로 선택한 영역의 물체 분리하기 (histo_backproject.py)

import cv2
import numpy as np
import matplotlib.pyplot as plt

win_name = 'back_projection'
img = cv2.imread('../img/pump_horse.jpg')
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
draw = img.copy()

#--⑤ 역투영된 결과를 마스킹해서 결과를 출력하는 공통함수
def masking(bp, win_name):
    disc = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
    cv2.filter2D(bp,-1,disc,bp)
    _, mask = cv2.threshold(bp, 1, 255, cv2.THRESH_BINARY)
    result = cv2.bitwise_and(img, img, mask=mask)
    cv2.imshow(win_name, result)

#--⑥ 직접 구현한 역투영 함수
def backProject_manual(hist_roi):
    #--⑦ 전체 영상에 대한 H,S 히스토그램 계산
    hist_img = cv2.calcHist([hsv_img], [0,1], None,[180,256], [0,180,0,256])
    #--⑧ 선택영역과 전체 영상에 대한 히스토그램 그램 비율계산
    hist_rate = hist_roi/ (hist_img + 1)
    #--⑨ 비율에 맞는 픽셀 값 매핑
    h,s,v = cv2.split(hsv_img)
    bp = hist_rate[h.ravel(), s.ravel()]
    # 비율은 1을 넘어서는 안되기 때문에 1을 넘는 수는 1을 갖게 함
    bp = np.minimum(bp, 1)
    # 1차원 배열을 원래의 shape로 변환
    bp = bp.reshape(hsv_img.shape[:2])
    cv2.normalize(bp,bp, 0, 255, cv2.NORM_MINMAX)
    bp = bp.astype(np.uint8)
    #--⑩ 역 투영 결과로 마스킹해서 결과 출력
    masking(bp,'result_manual')
 
# OpenCV API로 구현한 함수 ---⑪ 
def backProject_cv(hist_roi):
    # 역투영 함수 호출 ---⑫
    bp = cv2.calcBackProject([hsv_img], [0, 1], hist_roi,  [0, 180, 0, 256], 1)
    # 역 투영 결과로 마스킹해서 결과 출력 ---⑬ 
    masking(bp,'result_cv')

# ROI 선택 ---①
(x,y,w,h) = cv2.selectROI(win_name, img, False)
if w > 0 and h > 0:
    roi = draw[y:y+h, x:x+w]
    # 빨간 사각형으로 ROI 영역 표시
    cv2.rectangle(draw, (x, y), (x+w, y+h), (0,0,255), 2)
    #--② 선택한 ROI를 HSV 컬러 스페이스로 변경
    hsv_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
    #--③ H,S 채널에 대한 히스토그램 계산
    hist_roi = cv2.calcHist([hsv_roi],[0, 1], None, [180, 256], [0, 180, 0, 256] )
    #--④ ROI의 히스토그램을 매뉴얼 구현함수와 OpenCV 이용하는 함수에 각각 전달
    backProject_manual(hist_roi)
    backProject_cv(hist_roi)
cv2.imshow(win_name, draw)
cv2.waitKey()
cv2.destroyAllWindows()

마우스로 관심영역을 선택한 뒤 엔터를 누르면 관심 영역에 해당하는 물체만 추출됩니다. 관심 영역을 HSV 형식으로 변경한 뒤 H, S 채널에 대한 2차원 히스토그램 만듭니다. 이렇게 만든 히스토그램을 직접 구현한 함수와 OpenCV 함수의 파라미터로 전달합니다.

먼저, backProject_manual(hist_roi) 함수를 살펴보겠습니다. 이는 역투영을 직접 구현한 함수입니다. 파라미터로는 관심 영역 히스토그램이 전달되었습니다. 이 히스토그램을 전체 이미지의 히스토그램으로 나누어 비율을 구합니다. 이때 분모에 1을 더하는 이유는 분모가 0이 되는 것을 막기 위해서입니다. 비율을 구한다는 것은 관심 영역과 비슷한 색상 분포를 갖는 히스토그램은 1에 가까운 값을 갖고, 그렇지 않은 히스토그램은 0에 가까운 값을 갖게 되므로 마스킹에 사용하기 좋다는 뜻입니다. 그다음 이렇게 구한 비율을 원래 이미지의 H, S 픽셀 값에 매핑합니다.

여기서 bp = hist_rate[h.ravel(), s.ravel()] 코드가 헷갈릴 수도 있는데 아래 코드를 보면 이해가 쉬울 겁니다.

>>> v = np.arange(6).reshape(2,3)
>>> v
array([[0, 1, 2,],
	  [3, 4, 5]])
   
>>> row = np.array([1,1,1,0,0,0])
>>> col = np.array([0,1,2,0,1,2])
>>> v[row, col]
arrya([3, 4, 5, 0, 1, 2])

hist_rate는 히스토그램 비율을 값으로 가지고 있고, h와 s는 실제 이미지의 각 픽셀입니다. 따라서 h와 s가 교차되는 지점의 비율을 그 픽셀 값으로 하는 1차원 배열을 얻게 됩니다.  이렇게 얻은 값은 비율이기 때문에 1을 넘어서는 안 됩니다. 따라서 1을 넘는 모든 수는 1로 변환합니다. 마지막으로 정규화를 하고 타입 변경까지 하면 끝입니다. 

하지만 이 모든 작업을 OpenCV에서는 아래와 같은 함수로 제공합니다.

  • cv2.calcBackProject(img, channel, hist, ranges, scale)
    img: 입력 이미지, [img]처럼 리스트로 감싸서 사용
    channel: 처리할 채널, 리스트로 감싸서 사용
    hist: 역투영에 사용할 히스토그램
    ranges: 각 픽셀이 가질 수 있는 값의 범위
    scale: 결과에 적용할 배율 계수

cv2.calcBackProject의 세 번째 파라미터인 hist에는 역투영에 사용할 히스토그램을 전달하면 됩니다. 마지막으로 masking() 함수는 앞서 다룬 스레시홀딩과 마스킹을 활용하여 결과를 출력하는 기능을 합니다. cv2.getStructuringElement()와 cv2.filter2D()는 마스크의 표면을 부드럽게 해주는 역할을 합니다.

역투영의 장단점

역투영은 알파 채널이나 크로마 키 같은 것이 없어도 복잡한 모양의 사물을 분리할 수 있다는 장점이 있습니다. 하지만 역투영은 히스토그램을 기반으로 관심 영역의 색상과 비슷한 물체를 추출하므로, 관심 영역의 색상과 비슷한 다른 물체가 뒤섞여 있을 때는 효과가 떨어질 수 있습니다.

Comments