귀퉁이 서재

OpenCV - 10. 히스토그램과 정규화(Normalize), 평탄화(Equalization), CLAHE 본문

OpenCV

OpenCV - 10. 히스토그램과 정규화(Normalize), 평탄화(Equalization), CLAHE

Baek Kyun Shin 2020. 9. 22. 21:42

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

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

히스토그램

히스토그램은 도수 분포표를 그래프로 나타낸 것입니다. 쉽게 말해 무엇이 몇 개 있는지 개수를 세어 놓은 것을 그래프로 나타낸 것을 말합니다. 이미지의 픽셀값을 히스토그램으로 표시하는 것은 이미지를 분석하는 데 도움을 줍니다. 예를 들어 전체 이미지에서 픽셀 값이 1인 픽셀이 몇 개이고, 2인 픽셀이 몇 개이고, 255인 픽셀이 몇 개인지까지 세는 것입니다. 이렇게 함으로써 픽셀들의 색상이나 명암의 분포를 파악할 수 있습니다.

출처: https://www.cambridgeincolour.com/tutorials/histograms1.htm

OpenCV에서는 cv2.calcHist()라는 함수를 통해 히스토그램을 구현할 수 있습니다.

  • cv2.calHist(img, channel, mask, histSize, ranges)
    img: 이미지 영상, [img]처럼 리스트로 감싸서 전달
    channel: 분석 처리할 채널, 리스트로 감싸서 전달 - 1 채널: [0], 2 채널: [0, 1], 3 채널: [0, 1, 2]
    mask: 마스크에 지정한 픽셀만 히스토그램 계산, None이면 전체 영역
    histSize: 계급(Bin)의 개수, 채널 개수에 맞게 리스트로 표현 - 1 채널: [256], 2 채널: [256, 256], 3 채널: [256, 256, 256]
    ranges: 각 픽셀이 가질 수 있는 값의 범위, RGB인 경우 [0, 256]

간단한 회색조 이미지의 히스토그램을 그려보겠습니다.

# 회색조 1채널 히스토그램 (histo_gray.py)

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

#--① 이미지 그레이 스케일로 읽기 및 출력
img = cv2.imread('../img/mountain.jpg', cv2.IMREAD_GRAYSCALE)
cv2.imshow('img', img)

#--② 히스토그램 계산 및 그리기
hist = cv2.calcHist([img], [0], None, [256], [0,256])
plt.plot(hist)

print("hist.shape:", hist.shape)  #--③ 히스토그램의 shape (256,1)
print("hist.sum():", hist.sum(), "img.shape:",img.shape) #--④ 히스토그램 총 합계와 이미지의 크기
plt.show()

회색조로 이미지를 읽어 1차원 히스토그램으로 표현해봤습니다. range가 [0, 256]인데 마지막 값은 범위에 포함되지 않으므로 실제 범위는 0부터 255입니다. cv2.calcHist()로 반환한 hist 객체는 plt.plot(hist)을 통해 그래프로 그려줄 수 있습니다.

이제, 회색조가 아닌 색상이 있는 이미지를 3 채널(즉, RGB)로 계산해 히스토그램을 그려보겠습니다.

# 색상 이미지 히스토그램 (histo_rgb.py)

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

#--① 이미지 읽기 및 출력
img = cv2.imread('../img/mountain.jpg')
cv2.imshow('img', img)

#--② 히스토그램 계산 및 그리기
channels = cv2.split(img)
colors = ('b', 'g', 'r')
for (ch, color) in zip (channels, colors):
    hist = cv2.calcHist([ch], [0], None, [256], [0, 256])
    plt.plot(hist, color = color)
plt.show()

cv2.split(img) 함수를 호출하면 R, G, B 채널이 각각 나뉩니다. 즉, img는 색상 이미지이므로 3 채널 이미지인데, cv2.split(img)을 해주면 빨강, 파랑, 초록의 각 1 채널 이미지로 나뉩니다. 히스토그램을 보면 파란색 분포가 큰 것을 알 수 있습니다. 이미지에서 파란 하늘의 영역이 많기 때문입니다. 

정규화(Normalization)

이미지 작업에서도 정규화가 필요한 경우가 있습니다. 특정 영역에 몰려 있는 경우 화질을 개선하기도 하고, 이미지 간의 연산 시 서로 조건이 다른 경우 같은 조건으로 만들기도 합니다. OpenCV는 cv2.normalize()라는 함수로 정규화를 제공합니다.

  • dst = cv2.normalize(src, dst, alpha, beta, type_flag)
    src: 정규화 이전의 데이터
    dst: 정규화 이후의 데이터
    alpha: 정규화 구간 1
    beta: 정규화 구간 2, 구간 정규화가 아닌 경우 사용 안 함
    type_flag: 정규화 알고리즘 선택 플래그 상수

type_flag는 alpha와 beta 구간으로 정규화하는 cv2.NORM_MINMAX, 전체 합으로 나누는 cv2.NORM_L1, 단위 벡터로 정규화하는 cv2.NORM_L2, 최댓값으로 나누는 cv2.NORM_INF가 있습니다.

아래는 희미한 이미지를 정규화해 화질을 개선하는 예제입니다.

# 히스토그램 정규화 (histo_normalize.py)

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

#--① 그레이 스케일로 영상 읽기
img = cv2.imread('../img/abnormal.jpg', cv2.IMREAD_GRAYSCALE)

#--② 직접 연산한 정규화
img_f = img.astype(np.float32)
img_norm = ((img_f - img_f.min()) * (255) / (img_f.max() - img_f.min()))
img_norm = img_norm.astype(np.uint8)

#--③ OpenCV API를 이용한 정규화
img_norm2 = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)

#--④ 히스토그램 계산
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
hist_norm = cv2.calcHist([img_norm], [0], None, [256], [0, 255])
hist_norm2 = cv2.calcHist([img_norm2], [0], None, [256], [0, 255])

cv2.imshow('Before', img)
cv2.imshow('Manual', img_norm)
cv2.imshow('cv2.normalize()', img_norm2)

hists = {'Before' : hist, 'Manual':hist_norm, 'cv2.normalize()':hist_norm2}
for i, (k, v) in enumerate(hists.items()):
    plt.subplot(1,3,i+1)
    plt.title(k)
    plt.plot(v)
plt.show()

맨 왼쪽은 원본 이미지와 그에 대한 히스토그램, 가운데는 공식을 활용하여 수동으로 정규화한 이미지와 그에 대한 히스토그램, 맨 오른쪽은 cv2.normalize() 함수를 활용하여 정규화한 이미지와 그에 대한 히스토그램입니다. 정규화하기 전에는 픽셀 값이 중앙에 몰려있습니다. 하지만 정규화를 적용하니 픽셀 값이 전체적으로 고르게 퍼져서 화질이 개선된 것을 볼 수 있습니다.

평탄화(Equalization)

앞서 설명한 정규화는 분포가 한곳에 집중되어 있는 경우에는 효과적이지만 그 집중된 영역에서 멀리 떨어진 값이 있을 경우에는 효과가 없습니다. 이런 경우 평탄화가 필요합니다. 평탄화는 각각의 값이 전체 분포에 차지하는 비중에 따라 분포를 재분배하므로 명암 대비를 개선하는 데 효과적입니다. 

이미지의 히스토그램이 특정 영역에 너무 집중되어 있으면 명암 대비가 낮아 좋은 이미지라고 할 수 없습니다. 전체 영역에 골고루 분포가 되어 있을 때 좋은 이미지라고 할 수 있습니다. 아래 히스토그램을 보면 좌측처럼 특정 영역에 집중되어 있는 분포를 오른쪽처럼 골고루 분포하도록 하는 작업을 히스토그램 평탄화(Histogram Equalization)라고 합니다. (출처: opencv-python.readthedocs.io)

출처: Wikipedia

OpenCV에서 평탄화를 적용하기 위한 함수는 아래와 같습니다.

  • dst = cv2.equalizeHist(src, dst)
    src: 대상 이미지, 8비트 1 채널
    dst(optional): 결과 이미지

전체적으로 어두운 이미지를 평탄화하여 개선해보겠습니다.

# 회색조 이미지에 평탄화 적용 (histo_equalize.py)

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

#--① 대상 영상으로 그레이 스케일로 읽기
img = cv2.imread('../img/yate.jpg', cv2.IMREAD_GRAYSCALE)
rows, cols = img.shape[:2]

#--② 이퀄라이즈 연산을 직접 적용
hist = cv2.calcHist([img], [0], None, [256], [0, 256]) #히스토그램 계산
cdf = hist.cumsum()                                     # 누적 히스토그램 
cdf_m = np.ma.masked_equal(cdf, 0)                      # 0(zero)인 값을 NaN으로 제거
cdf_m = (cdf_m - cdf_m.min()) /(rows * cols) * 255      # 이퀄라이즈 히스토그램 계산
cdf = np.ma.filled(cdf_m,0).astype('uint8')             # NaN을 다시 0으로 환원
print(cdf.shape)
img2 = cdf[img]                                         # 히스토그램을 픽셀로 맵핑

#--③ OpenCV API로 이퀄라이즈 히스토그램 적용
img3 = cv2.equalizeHist(img)

#--④ 이퀄라이즈 결과 히스토그램 계산
hist2 = cv2.calcHist([img2], [0], None, [256], [0, 256])
hist3 = cv2.calcHist([img3], [0], None, [256], [0, 256])

#--⑤ 결과 출력
cv2.imshow('Before', img)
cv2.imshow('Manual', img2)
cv2.imshow('cv2.equalizeHist()', img3)
hists = {'Before':hist, 'Manual':hist2, 'cv2.equalizeHist()':hist3}
for i, (k, v) in enumerate(hists.items()):
    plt.subplot(1,3,i+1)
    plt.title(k)
    plt.plot(v)
plt.show()

결과 이미지의 크기가 커서 이곳에 삽입하는 것은 생략했습니다. 결과를 보시면 명암 대비가 낮은 부둣가 이미지의 화질이 개선된 걸 볼 수 있습니다. 

히스토그램 평탄화는 색상 이미지에도 적용할 수 있습니다. 밝기 값을 개선하기 위해서는 BGR 3개 채널을 모두 평탄화해야 합니다. 하지만 YUV나 HSV를 활용하면 하나의 밝기 채널만 조절하면 됩니다. 아래는 색상 이미지를 BGR에서 YUV 형식으로 변환하여 밝기 채널에 평탄화를 적용한 예제입니다.

# 색상 이미지에 대한 평탄화 적용 (histo_equalize_yuv.py)

import numpy as np, cv2

img = cv2.imread('../img/yate.jpg') #이미지 읽기, BGR 스케일

#--① 컬러 스케일을 BGR에서 YUV로 변경
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) 

#--② YUV 컬러 스케일의 첫번째 채널에 대해서 이퀄라이즈 적용
img_yuv[:,:,0] = cv2.equalizeHist(img_yuv[:,:,0]) 

#--③ 컬러 스케일을 YUV에서 BGR로 변경
img2 = cv2.cvtColor(img_yuv, cv2.COLOR_YUV2BGR) 

cv2.imshow('Before', img)
cv2.imshow('After', img2)
cv2.waitKey()
cv2.destroyAllWindows()

평탄화 적용 전(왼쪽 이미지) 보다 적용 후(오른쪽 이미지)가 더 선명합니다. 요트 부분을 보면 밝기가 더 개선되어 화질이 좋아진 것을 볼 수 있습니다.

CLAHE (Contrast Limited Adaptive Histogram Equalization)

지금까지 평탄화를 통해 명암 대비를 개선하여 이미지의 선명도를 높이는 작업에 대해 알아봤습니다. 하지만 평탄화를 하면 이미지의 밝은 부분이 날아가는 현상이 발생합니다. 아래의 이미지를 보겠습니다.

출처: https://opencv-python.readthedocs.io

평탄화를 적용하니 조각상의 얼굴이 너무 밝게 바뀌어 경계선을 알아볼 수 없게 되었습니다. 평탄화를 이미지 전체에 적용하면 이런 현상이 자주 발생합니다. 이런 현상을 막기 위해 이미지를 일정한 영역(아래 코드에서 tileGridSize 파라미터)으로 나누어 평탄화를 적용합니다. 그러나 이 방식에도 문제가 있습니다. 일정한 영역 내에서 극단적으로 어둡거나 밝은 부분이 있으면 노이즈가 생겨 원하는 결과를 얻을 수 없게 됩니다. 이 문제를 피하기 위해서 어떤 영역이든 지정된 제한 값(아래 코드에서 clipLimit 파라미터)을 넘으면 그 픽셀은 다른 영역에 균일하게 배분하여 적용합니다. 이러한 평탄화 방식을 CLAHE라고 합니다.

출처: Wikipedia

  • clahe = cv2.createCLAHE(clipLimit, tileGridSize)
    clipLimit: 대비(Contrast) 제한 경계 값, default=40.0
    tileGridSize: 영역 크기, default=8 x 8
    clahe: 생성된 CLAHE 객체
  • clahe.apply(src): CLAHE 적용
    src: 입력 이미지

아래는 CLAHE를 적용한 예제입니다.

# CLAHE 적용 (histo_clahe.py)

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

#--①이미지 읽어서 YUV 컬러스페이스로 변경
img = cv2.imread('../img/bright.jpg')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

#--② 밝기 채널에 대해서 이퀄라이즈 적용
img_eq = img_yuv.copy()
img_eq[:,:,0] = cv2.equalizeHist(img_eq[:,:,0])
img_eq = cv2.cvtColor(img_eq, cv2.COLOR_YUV2BGR)

#--③ 밝기 채널에 대해서 CLAHE 적용
img_clahe = img_yuv.copy()
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)) #CLAHE 생성
img_clahe[:,:,0] = clahe.apply(img_clahe[:,:,0])           #CLAHE 적용
img_clahe = cv2.cvtColor(img_clahe, cv2.COLOR_YUV2BGR)

#--④ 결과 출력
cv2.imshow('Before', img)
cv2.imshow('CLAHE', img_clahe)
cv2.imshow('equalizeHist', img_eq)
cv2.waitKey()
cv2.destroyAllWindows()

왼쪽이 원본 사진, 가운데가 CLAHE를 적용한 사진, 오른쪽이 단순한 평탄화를 적용한 사진입니다. 단순한 평탄화를 적용할 땐 밝은 부분은 날아가는 현상이 발생합니다. 하지만 CLAHE를 적용할 땐 이러한 현상이 개선되었습니다.

CLAHE를 적용하기 위해서는 CLAHE 객체를 먼저 생성하고, 생성된 CLAHE 객체에 apply() 함수를 적용해줘야 합니다. 아래의 코드가 CLAHE의 핵심코드입니다.

clahe = cv2.createCLAHE()
img2 = clahe.apply(img)

 

이상으로 이미지 히스토그램, 정규화, 평탄화, CLAHE에 대해 알아봤습니다.

Comments