오늘은 히스토그램 평활화(Histogram Equalization)에 대해서 포스팅하도록 하겠습니다. 히스토그램 평활화는 히스토그램이 평평하게 되도록 영상을 조작해 영상의 명암 대비를 높이는 기법입니다. 영상의 명암 대비가 높아지면 영상에 있는 물체를 더 잘 식별할 수 있게 됩니다.
이름만 봐도 알 수 있듯이 히스토그램 평활화를 이해하기 위해서는 먼저 히스토그램에 대한 지식이 필요합니다.
히스토그램(Histogram)
히스토그램은 0, 1, 2, ... , L-1의 명암단계 각각에 대해 화소의 발생 빈도를 나타내는 1차원 배열입니다. 아래 그림은 크기가 4 x 4이고 명암단계가 L = 8인 영상에 대한 히스토그램을 막대 그래프로 표현한 예시입니다.
OpenCV에서도 영상의 히스토그램을 구해주는 함수가 존재하는데요, 바로 calcHist 함수입니다. 귀여운 푸바오 사진으로 calcHist함수를 사용하여 히스토그램을 그래프로 표현해 확인해봅시다.
import cv2 as cv
import matplotlib.pyplot as plt
# 푸바오 영상 읽기
img = cv.imread("/home/hsm/Study/OpenCV/image/panda.jpg")
# 명암 영상으로 변환
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.imshow('panda',gray)
# 히스토그램 구하기
hist = cv.calcHist([gray],[0],None,[256],[0,256])
# 히스토그램을 빨간색 선의 그래프로 표현
plt.plot(hist,color='r',linewidth=1)
plt.title("Histogram")
plt.show()
푸바오 사진을 명암 영상으로 변환한 뒤 calcHist 함수를 이용해 히스토그램을 구해줬습니다. 흰색이 많아 200이상의 높은 명암값을 가지는 화소의 개수가 많을 줄 알았는데 의외로 낮은 명암값에서의 개수가 높게 나온 것을 확인할 수 있습니다. 아마 확실한 검은색에 해당하여 명암값이 낮은 부분이 많기 때문인 것 같습니다.
OpenCV에서 제공하는 calcHist() 함수는 다음과 같습니다.
cv2.calcHist(img, channel, mask, histSize, ranges)
- img: 히스토그램을 구하고 싶은 이미지 영상, [img]와 같이 리스트로 입력해주어야 함
- channel: 히스토그램을 구할 영상의 채널, grayscale 이미지의 경우 [0]을 인자로 입력하고 color 이미지일 경우 B, G, R에 대한 히스토그램을 구하기 위해서 [0], [1], [2]를 인자로 입력
- mask: 히스토그램을 구할 영역을 지정하는 마스크, None을 입력하면 전체 영상에서 히스토그램을 구함
- histSize: 히스토그램의 칸의 수를 지정, 보통 한 pixel당 0~255까지의 값을 가져 명암단계가 L = 256이기 때문에 [256]과 같이 리스트로 전달해주면 됨. 128로 지정하면 0과 1을 0, 2와 3을 1, ...로 간주해 128개의 칸을 가진 히스토그램을 구함.
- ranges: 각 픽셀값이 가질 수 있는 범위, 보통 [0,256]으로 지정, 만약 [0,128]로 지정했다면 128 이상인 값은 세지 않음.
calHist 함수의 channel 인수 값을 알맞게 넣어주면 아래와 같이 color이미지의 B, G, R 각 채널마다 히스토그램을 구해 나타낼 수도 있습니다.
import cv2 as cv
import matplotlib.pyplot as plt
img = cv.imread("/home/hsm/Study/OpenCV/image/panda.jpg")
cv.imshow('panda',img)
colors = ('b', 'g', 'r')
for i in range(3):
# channel 인수 값을 바꿔가며 B, G, R 별 히스토그램 시각화
hist = cv.calcHist([img],[i],None, [256], [0,256])
plt.plot(hist, color = colors[i])
plt.title("Histogram(Color)")
plt.legend(['B_channel', 'G_channel', 'R_channel'])
plt.show()
히스토그램 평활화(Histogram Equalization)
자 그럼 본격적으로 히스토그램 평활화에 대해서 알아보도록 합시다. 히스토그램 평활화는 특정 영역에 집중되어 있는 영상의 히스토그램 분포를 평평하게 만들어 명암 대비를 높이는 기법입니다. 명암 값이 몰려 있어서 어둡기만 한 영상 또는 밝기만 한 영상을 평활화하여 좀 더 선명한 영상을 얻는 것이죠.
히스토그램 평활화는 모든 칸의 값을 더하면 1.0이 되는 정규화 히스토그램 $\dot{h}$와 i번 칸은 0~i번 칸을 더한 값을 가진 누적 정규화 히스토그램 $\ddot{h}$를 가지고 아래 식을 수행하면서 히스토그램 분포를 평평하게 만듭니다.
$$l'=round(\ddot{h}(l) \times (L-1))$$
위 식에서 $l$은 원래 명암값이고 $l'$은 평활화로 얻은 새로운 명암값입니다. 아래 예시를 통해서 평활화 과정을 자세히 살펴보도록 합시다.
아래와 같이 명암단계가 L = 8인 4 x 4 크기의 영상에 히스토그램 평활화를 적용한다고 가정해봅시다.
먼저, 계산된 히스토그램 분포 h(g)에 전체 크기인 16씩을 나눠주어 정규화 히스토그램 p(g)를 계산합니다. (총합이 1)
각 명암값 별로 이전까지의 정규화 히스토그램 p(g)값을 합쳐 누적 정규화 히스토그램 cdf(g)를 계산합니다. 마지막 명암값의 누적 정규화 히스토그램 값은 무조건 1이 되겠죠.
각 누적 정규화 히스토그램 값 cdf(g)에 최대 명암값($L_{max}$)인 7씩을 곱하고 반올림하여(round) 최종적으로 새로운 명암값을 얻습니다.
OpenCV의 equalizeHist 함수를 사용하면 손쉽게 원하는 영상에 히스토그램 평활화를 적용할 수 있습니다. 아래 코드를 통해 확인해봅시다.
import cv2 as cv
import matplotlib.pyplot as plt
# 히스토그램 평활화를 적용할 영상 읽기
img = cv.imread("/home/hsm/Study/OpenCV/image/fog4.jpeg")
# grayscale로 변환
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
plt.figure(1)
plt.suptitle('GrayScale Image')
plt.subplot(1,2,1)
plt.imshow(gray, cmap='gray'), plt.xticks([]), plt.yticks([])
# histogram 시각화
h = cv.calcHist([gray],[0],None, [256], [0,256])
plt.subplot(1,2,2)
plt.plot(h,color='r',linewidth=1)
# 히스토그램 평활화 적용
equal = cv.equalizeHist(gray)
plt.figure(2)
plt.suptitle('Histogram Equalization')
plt.subplot(1,2,1)
plt.imshow(equal, cmap='gray'), plt.xticks([]), plt.yticks([])
# 적용 결과의 histogram 시각화
h = cv.calcHist([equal], [0], None, [256], [0,256])
plt.subplot(1,2,2)
plt.plot(h,color='r', linewidth=1), plt.show()
원본 영상과 히스토그램 평활화 적용한 영상의 히스토그램을 각각 시각화하여 적용 결과를 비교할 수 있도록 코드를 짰습니다. 아래가 위 코드의 출력 결과입니다.
입력 영상의 히스토그램을 살펴보면 전체 명암값 범위에서 0~ 50 범위는 거의 사용하지 않고 대부분의 화솟값이 200 ~ 230 사이에 몰려있음을 확인할 수 있습니다. 일부러 안개 낀 사진을 준비했지만 영상이 흐릿한 이유는 명암값이 특정 영역에 몰려있기 때문이라고 볼 수 있습니다.
히스토그램 평활화된 영상의 히스토그램은 이전보다는 평평해졌으며 영상이 처음 영상에 비해서 어느정도 선명해진 것을 확인할 수 있습니다.
equalizeHist 함수는 다음과 같이 단순히 히스토그램 평활화를 적용시키길 원하는 입력 영상만 인자로 주면 사용할 수 있습니다.
cv2.equalizeHist(img, dst)
- img: 히스토그램 평활화를 적용시키고자 하는 영상
- dst: 결과 이미지(입력 안해도됨)
컬러 영상에서의 히스토그램 평활화
컬러영상에 히스토그램 평활화를 적용할 때 RGB 채널 각각에 히스토그램 평활화를 적용한 후 합치면 색이 변하는 문제가 일어날 수 있습니다. 따라서 먼저 RGB로 받은 이미지를 HSV 또는 YCrCb 형태의 이미지로 변경한 다음에 밝기값 채널에 해당하는 V 또는 Y 채널에 대해서만 히스토그램 평활화를 적용해야 색을 변경하지 않고 선명하게 만들 수 있습니다. 코드는 다음과 같습니다.
import cv2 as cv
import matplotlib.pyplot as plt
img = cv.imread("/home/hsm/Study/OpenCV/image/night.jpg")
# HSV 공간으로 변환
hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV)
h, s, v = cv.split(hsv)
equalv = cv.equalizeHist(v)
hsv_dst = cv.merge([h, s, equalv])
dst = cv.cvtColor(hsv_dst, cv.COLOR_HSV2BGR)
cv.imshow('org image', img)
cv.imshow('color equalize',dst)
cv.waitKey(0)
cv.destroyAllWindows()
#YCrCb 공간으로 변환
# ycrcb = cv.cvtColor(img, cv.COLOR_BGR2YCrCb)
# y, cr, cb = cv.split(ycrcb)
# equaly = cv.equalizeHist(y)
# ycrcb_dst = cv.merge([equaly, cr, cb])
# dst = cv.cvtColor(ycrcb_dst, cv.COLOR_YCrCb2BGR)
# cv.imshow('org image', img)
# cv.imshow('color equalize',dst)
# cv.waitKey(0)
# cv.destroyAllWindows()
HSV 공간의 H 채널에 대해서 히스토그램 평활화를 적용하고 합친 결과 아래 그림과 같이 원본 영상에서는 잘 보이지 않던 왼편의 글씨가 잘 보일 정도로 영상이 선명해졌다는 것을 확인할 수 있습니다.
이렇듯 히스토그램 평활화는 영상을 어느정도 선명하게 만들어주지만 우리가 원하는 만큼의 선명한 영상은 만들어주지 못할 가능성이 큽니다. 당장 이전 안개 낀 영상의 결과만 봐도 영상이 입력 영상에 비해 아주 선명해졌다고는 볼 수 없습니다. 또한 히스토그램 평활화는 전체 픽셀들에 대해서 평활화를 일괄적으로 적용하기 때문에 이미 밝은 영역이 더 밝아져 오히려 형체를 알아보지 못하는 문제가 생기면서 원본 영상보다 품질이 더 안좋아질 수도 있습니다. 아래 그림과 같이 처음 푸바오 사진에 히스토그램 평활화를 적용해보면 오히려 영상이 더 흐릿해보이는 것을 확인할 수 있죠.
이러한 히스토그램 평활화의 한계점을 해결하기 위해 입력 영상을 일정한 영역으로 나누어 평탄화를 적용하는 CLAHE(Contrast Limited Adaptive Histogram)가 나오게 되었습니다. CLAHE에 대해서는 다음 포스팅에서 설명하도록 하겠습니다.
Reference
'Perception > OpenCV' 카테고리의 다른 글
[OpenCV] 허프 변환 (Hough Transform) - 원 검출 (1) | 2023.02.18 |
---|---|
[OpenCV] 허프 변환 (Hough Transform) - 직선 검출 (1) | 2023.02.13 |
[OpenCV] 캐니 에지 검출(Canny edge detection) (1) | 2023.02.12 |