반응형


이미지에서 2차원 히스토그램을 구하는 방법과 응용으로 Histogram Backprojection을 설명합니다.




다음 OpenCV Python 튜토리얼을 참고하여 강좌를 비정기적로 포스팅하고 있습니다.


https://docs.opencv.org/4.0.0/d6/d00/tutorial_py_root.html




최초작성 2018. 12. 15


1. 2차원 히스토그램(2D Histograms)

그레이스케일 이미지로 부터 구한 히스토그램은 1차원 히스토그램이었습니다. 그레이스케일 이미지가 하나의 채널을 가지고 있었기 때문에 하나의 특징만을 고려했습니다.




2차원 히스토그램에서는 두가지 특징을 고려합니다.  예를 들어 HSV 색공간 이미지를 입력으로 사용한다면  모든 픽셀에서 두가지 특징 Hue 값와 Saturation 값에 대한 컬러 히스토그램을 구합니다.


컬러 이미지에서 보였던 빨간색, 파란색, 노란색이



오른쪽에 보이는 컬러 히스토그램에 색으로 표현되고 있습니다.


 




테스트에 사용한 코드입니다. 영상을 입력으로 사용하도록 되어있습니다.  hist 창에서 scale 바를 오른쪽으로 옮기면 히스토그램이 좀 더 잘보이게 됩니다.  


# 원본 코드 - https://github.com/opencv/opencv/blob/master/samples/python/color_histogram.py
# 수정 - webnautes
import numpy as np
import cv2 as cv


if __name__ == '__main__':

   hsv_map = np.zeros((180, 256, 3), dtype=np.uint8)

   h, s = np.indices(hsv_map.shape[:2])
   hsv_map[:,:,0] = h
   hsv_map[:,:,1] = s
   hsv_map[:,:,2] = 255
   hsv_map = cv.cvtColor(hsv_map, cv.COLOR_HSV2BGR)
   cv.imshow('hsv_map', hsv_map)

   cv.namedWindow('hist', 0)
   hist_scale = 10

   def set_scale(val):
       global hist_scale
       hist_scale = val
   cv.createTrackbar('scale', 'hist', hist_scale, 32, set_scale)


   cam = cv.VideoCapture(0)

   while True:
       ret, frame = cam.read()

       if ret == False:
           continue;

       cv.imshow('camera', frame)

       small = cv.pyrDown(frame)

       hsv = cv.cvtColor(small, cv.COLOR_BGR2HSV)
       dark = hsv[...,2] < 32
       hsv[dark] = 0
       h = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])

       h = np.clip(h*0.005*hist_scale, 0, 1)
       #vis = hsv_map * h[:,:,np.newaxis] / 255.0
       vis = hsv_map * h[:, :, np.newaxis]
       scaled = np.uint8(vis)

       cv.imshow('hist', scaled)

       ch = cv.waitKey(1)
       if ch == 27:
           break
   cv.destroyAllWindows()




컬러 히스토그램을 어떻게 구현하는지 알아봅시다. 이후 Histogram Backprojection을 이해하는데 도움이 됩니다.



HSV 색공간의 색을 2차원 평면에 표현합니다. HSV 색공간으로 변환한 영상에서 구한 Hue값과 Saturation 값에 대한 히스토그램 결과를 화면에 보여주기 위해 사용됩니다.  



   hsv_map = np.zeros((180, 256, 3), dtype=np.uint8)

   h, s = np.indices(hsv_map.shape[:2])
   hsv_map[:,:,0] = h
   hsv_map[:,:,1] = s
   hsv_map[:,:,2] = 255
   hsv_map = cv.cvtColor(hsv_map, cv.COLOR_HSV2BGR)
   cv.imshow('hsv_map', hsv_map)



BGR 색공간의 이미지를 HSV 색공간 이미지로 변환 합니다.


hsv = cv.cvtColor(small, cv.COLOR_BGR2HSV)



hsv 이미지에서 Value값이 32이하인 픽셀을 0으로 만듭니다. 같은 Hue값을 가지는 색 중에 너무 밝기가 어두워 검은색에 가까운 색들을 제거하기 위해서입니다.  


dark = hsv[...,2] < 32
hsv[dark] = 0



hsv 이미지에서 Hue와 Saturation에 대한 히스토그램을 구합니다.


h = cv.calcHist([hsv], [0, 1], None, [180, 256], [0, 180, 0, 256])



컬러 히스토그램에 보이는 색은 앞에서 생성한 hsv_map 이미지에 아래처럼 구한 값을 곱하여 얻어집니다.

히스토그램에서  색을 잘 구분되게 하기 위해서 히스토그램에  0.005를 곱하여 값을 낮추고 hist_scale 변수로 강도를 조정 가능하도록 합니다.

그리고 히스토그램의 값을 0에서 1사이의 값으로 클립핑합니다.



       h = np.clip(h*0.005*hist_scale, 0, 1)
       vis = hsv_map * h[:, :, np.newaxis]



hist_scale 변수는 트랙바를 사용하여 조절하도록 되어 있습니다.


   cv.namedWindow('hist', 0)
   hist_scale = 10

   def set_scale(val):
       global hist_scale
       hist_scale = val
   cv.createTrackbar('scale', 'hist', hist_scale, 32, set_scale)




2. Histogram Backprojection

Histogram Backprojection은 입력 이미지와 같은 크기이지만 하나의 채널만 가지는 이미지를 생성합니다.

이 이미지의 픽셀은 특정 오브젝트에 속할 확률을 의미합니다. 관심 오브젝트 영역에 속한 픽셀이 나머지 부분보다 더 흰색으로 표현됩니다.


이미지 세그멘테이션, 관심 객체 찾기, camshift 알고리즘 등에서  사용됩니다.



다음 과정을 통해 Histogram Backprojection이 이루어집니다.


찾고자하는 오브젝트 이미지(빨간색 이미지)와 해당 오브젝트(컬러 서클 이미지)를 찾을 이미지에 대한 히스토그램을 각각 구합니다.


 



컬러 히스토그램을 backproject하여 이미지에서 오브젝트를 찾습니다.

다음처럼 오브젝트 영역을 검출할 수 있습니다. 픽셀이 해당 오브젝트 영역에 속할 확률을 구한 것입니다.  확률이 높을 수록 흰색으로 표현됩니다.




원본 이미지와 and 연산하여 오브젝트 영역만 보여주기 위해서 이진화합니다.




입력 영상과 이진화 영상을 and 연산하여 원하는 영역만 얻습니다.





2.1. Numpy로 알고리즘 구현


roi는 찾을 오브젝트입니다.   HSV 색공간으로 변환합니다.


roi = cv.imread('red.png')
hsv_roi = cv.cvtColor(roi,cv.COLOR_BGR2HSV)



target은  오브젝트 영역을 찾을 이미지입니다. 마찬가지로 HSV 색공간으로 변환합니다.


target = cv.imread('color circle_w.jpg')
hsv_target = cv.cvtColor(target,cv.COLOR_BGR2HSV)  



cv.calcHist 함수를 사용하여 오브젝트 영역 이미지(M)와 오브젝트 영역을 찾을 이미지(I)에 대해 컬러 히스토그램을 계산합니다.

2차원 히스토그램을 그려주는 np.histogram2d 함수를 사용할 수도 있습니다.


HSV 이미지에서 값 범위가 0 ~ 179인 H 채널과 값 범위가 0 ~ 255인 S 채널에 대한 히스토그램을 구하기 때문에 M과 I의  shape가 (180, 256)입니다.


M = cv.calcHist([hsv_roi],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv.calcHist([hsv_target],[0, 1], None, [180, 256], [0, 180, 0, 256] )



히스토그램 M과 R의 비율을 계산합니다.  R=M / I

분모에 더하기 1을 안해주면 배열에 nan 값이 생깁니다. 또한  분모와 분자를 바꾸면 비율이 안구해집니다.


R = M/(I+1)             



HSV 색공간의 타겟 이미지를 h,s,v 채널로 분리합니다.


h,s,v = cv.split(hsv_target)



입력 hue와 saturation값에 R을 팔레트로 사용하여 오브젝트 영역에 있을 확률을 픽셀로 표현하여 새로운 이미지를 생성합니다.  오브젝트 영역에 속할 확률이 높은 픽셀일 수록 흰색으로 표현됩니다.

ravel()함수는  2차원 배열을 1차원 배열로 변환합니다.


B = R[h.ravel(), s.ravel()]



배열 B에서 1이상 값을 1로 만들어줍니다.


B = np.minimum(B, 1)



B의 shape를 1차원에서 이미지 크기의 shape로 다시 변경합니다.  


B = B.reshape(hsv_target.shape[:2])



크기 5 x 5인 타원 모양 디스크를 커널로 사용하여 컨벌루션합니다.

backprojection 결과 영역을 고른 색으로 바꾸어줍니다.


>>disc
array([[0, 0, 1, 0, 0],
      [1, 1, 1, 1, 1],
      [1, 1, 1, 1, 1],
      [1, 1, 1, 1, 1],
      [0, 0, 1, 0, 0]], dtype=uint8)


disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))

cv.filter2D(B, -1, disc, B)



배열 B의 타입을 uint8로 변환하고 0~ 255 사이의 값으로 정규화합니다.



B = np.uint8(B)
cv.normalize(B, B, 0, 255, cv.NORM_MINMAX)



오브젝트를 찾은 영역은 흰색, 배경은 검은색이 되도록 바이너리 이미지로 변환합니다.



ret,thresh = cv.threshold(B, 20, 255, 0)



1채널 바이너리 이미지를 3채널 이미지로 변환한 후 원본 이미지와 and 연산하여 겹치는 부분만 얻습니다.



thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)




포스팅에서 사용한 코드입니다.


# 원본 코드 - https://docs.opencv.org/4.0.0/dc/df6/tutorial_py_histogram_backprojection.html
# 수정 - webnautes

import numpy as np
import cv2 as cv


roi = cv.imread('red.png')
hsv_roi = cv.cvtColor(roi,cv.COLOR_BGR2HSV)

target = cv.imread('color circle_w.jpg')
hsv_target = cv.cvtColor(target,cv.COLOR_BGR2HSV)

M = cv.calcHist([hsv_roi],[0, 1], None, [180, 256], [0, 180, 0, 256] )
I = cv.calcHist([hsv_target],[0, 1], None, [180, 256], [0, 180, 0, 256] )

R = M/(I+1)


h,s,v = cv.split(hsv_target)
B = R[h.ravel(), s.ravel()]
B = np.minimum(B, 1)
B = B.reshape(hsv_target.shape[:2])


disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(B, -1, disc, B)
B = np.uint8(B)
cv.normalize(B, B, 0, 255, cv.NORM_MINMAX)

ret,thresh = cv.threshold(B,20,255,0)

thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)


cv.imshow("result1", B)
cv.imshow("result2", res)
cv.waitKey(0)



테스트에 사용한 이미지입니다.





2.2. OpenCV로 구현


OpenCV에서는 히스토그램의 Backprojection을 계산해주는 cv.calcBackProject()함수를 제공합니다.


dst = cv.calcBackProject( images, channels, hist, ranges, scale[, dst] )



images

오브젝트를 찾을 이미지


channels

오브젝트를 찾을 이미지의 채널 중 backprojection 계산시 사용할 채널 인덱스 지정


hist

오브젝트의 히스토그램


ranges

히스토그램의 범위, 채널별로 지정해줘야 함


scale

결과 생성시 사용되는  scale factor




cv.calcBackProject()함수를 사용하여 다음처럼 구현합니다.


오브젝트 이미지와 오브젝트를 찾을 이미지를 HSV 색공간으로 변환합니다.


roi = cv.imread('red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('color circle_w.jpg')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)



오브젝트의 히스토그램을 구합니다.


roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )



히스토그램을 0 ~ 255 사이의 값으로 변환 후 backprojection을 적용합니다.


cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)



타원 커널을 이미지 dst에 컨볼루션합니다. backprojection 결과 영역이 일정한 색을 가진 영역이 됩니다.



disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst)



이진화 후, 채널 셋인 이미지로 변경하여 오브젝트를 찾을 이미지와 and 연산합니다.

결과적으로 찾는 영역만 화면에 보이게 됩니다.


 


ret,thresh = cv.threshold(dst,50,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)



# 원본 코드 - https://docs.opencv.org/4.0.0/dc/df6/tutorial_py_histogram_backprojection.html
# 수정 - webnautes

import cv2 as cv


roi = cv.imread('red.png')
hsv = cv.cvtColor(roi,cv.COLOR_BGR2HSV)
target = cv.imread('color circle_w.jpg')
hsvt = cv.cvtColor(target,cv.COLOR_BGR2HSV)

roihist = cv.calcHist([hsv],[0, 1], None, [180, 256], [0, 180, 0, 256] )

cv.normalize(roihist,roihist,0,255,cv.NORM_MINMAX)
dst = cv.calcBackProject([hsvt],[0,1],roihist,[0,180,0,256],1)


disc = cv.getStructuringElement(cv.MORPH_ELLIPSE,(5,5))
cv.filter2D(dst,-1,disc,dst)

ret,thresh = cv.threshold(dst,20,255,0)
thresh = cv.merge((thresh,thresh,thresh))
res = cv.bitwise_and(target,thresh)

cv.imshow('result1',dst)
cv.imshow('result2',res)
cv.waitKey(0)



반응형

해본 것을 문서화하여 기록합니다.
부족함이 있지만 도움이 되었으면 합니다.


포스트 작성시에는 문제 없었지만 이후 문제가 생길 수 있습니다.
질문을 남겨주면 가능한 빨리 답변드립니다.


제가 쓴 책도 한번 검토해보세요 ^^

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기

댓글을 달아 주세요

">