파이썬 강의/openCV

파이썬 openCV 8. 히스토그램 명세화(histogram specification)

마리사라 2020. 11. 21. 00:24
반응형

파이썬 openCV 8번째 강의는 히스토그램 명세화입니다. 히스토그램 명세화는 히스토그램 평활화와 같이 명암대비를 개선시키는 기법 중 하나입니다. 하지만 명세화는 평활화와는 완전히 다른 방법으로 명암대비를 개선시키며, 이는 평활화 때보다 더 긴 코드가 필요합니다.

 

 


0. 히스토그램 명세화?

히스토그램 명세화는 사실 명암대비를 개선시키는 방법이라기보다는 영상의 히스토그램을 사용자가 원하는 모양으로 바꿀 때 쓰는 기법입니다. 하지만 명암대비가 이상한 영상을 만들려고 하는 사람은 잘 없겠죠? 그래서 히스토그램이 한쪽으로 치우친 영상을 사용자가 원하는 모양대로 만들어준다면 명암대비가 개선되기에 명암대비를 개선시키는 기법이라 할 수 있습니다.

 

히스토그램 명세화의 기본 원리를 간단하게 설명하면 다음과 같습니다.

1. 변경하고자 하는 영상(원본 영상)을 평활화한다.

2. 원하는 히스토그램 모양(타깃 히스토그램)에 해당하는 히스토그램을 평활화한다.

3. 평활화된 타겟 히스토그램을 다시 역 평활화한다.

4. 역 평활화된 타깃 히스토그램에 해당하는 LUT(룩 업 테이블)을 가지고 평활화된 원본 영상에 매칭 시킨다

 

 

자. 이해가 가시나요?

지금 쓰고 있는 저도 이해가 쉽게 되지 않는 부분입니다. 그래서 처음부터 하나하나 알려드리도록 하겠습니다.

 

1. 변경하고자 하는 영상(원본 영상)을 평활화한다.

이건 그저 원본 영상에 히스토그램 평활화 작업을 해준다는 뜻입니다. 패스.

 

2. 원하는 히스토그램 모양(타깃 히스토그램)에 해당하는 히스토그램을 평활화한다.

만일 원본 영상이 왼쪽으로 치우쳐있다면(어두움) 우리는 중앙이나 오른쪽에 치우친 히스토그램이 필요하겠죠? 그래서 그러한 모양을 가지는 히스토그램을 만들고, 그것을 평활화해줍니다.

여기까지는 이해가 쉽습니다

 

3. 평활화된 타깃 히스토그램을 다시 역 평활화한다.

평활화된 타깃 히스토그램은 말 그대로 2번 작업에서 해주었던 결과입니다. 그런데 여기서 역 평활화라는 말이 나오는데요. 이는 역함수와 비슷한 말이라고 생각하시면 됩니다. 평활화는 명도와 정규화된 누적합을 바탕으로 했었습니다. 그러나 역 평활화는이 됩니다. 이때 역평활화 값이 역함수로서 룩업테이블(Look-up Table)로 사용됩니다.

 

​예를 들어 다음과 같은 히스토그램이 있다고 해보겠습니다.

명도 갯수 누적합 정규화된 누적합
0 0 0 0
1 0 0 0
2 0 0 0
3 2 2 1.42
4 2 4 2.84
5 3 7 5

이때 평활화는 정규화된 누적합을 바탕으로 진행됩니다. 하지만 역 평활화는 정규화된 누적합이 명도 값으로 바뀐다고 했죠? 이때 바뀌는 역 히스토그램 값은 입력 값과 가장 가까운 누적합 값에 해당하는 값으로 바뀝니다.

명도 정규화된 누적합 역히스토그램 값
0 0 0
1 0 3
2 0 3
3 1.42 4
4 2.84 5
5 5 5

역 히스토그램 값을 계산하면 이렇게 나옵니다. 1은 0과 1.42중 1.42에 가까우므로 누적합 1.42의 명도 값 3이 됩니다. 마찬가지로 3은 [0, 1.42, 2.84, 5]중 2.84에 가장 가까우므로 정규화된 누적합 2.84를 가지는 명도 값 4가 되게 됩니다. 이런 식으로 만들어지는 게 바로 역 평활화라고 보시면 되겠습니다.

 

4. 역 평활화된 타깃 히스토그램에 해당하는 LUT(룩 업 테이블)을 가지고 평활화된 원본 영상에 매칭 시킨다

이제 위에서 설명한 대로 원래 영상의 평활화된 명도 값을 위의 LUT에 대입시켜 값을 뽑아내면 완성되게 됩니다.

 

아직은 조금 어려우실 수 있을 겁니다. 직접 코드를 보면서 이해하시면 조금 더 빠르게 이해하실 수 있을 겁니다.


1. openCV에서의 히스토그램 명세화

openCV에서 히스토그램 명세화를 위해 직접 원하는 히스토그램을 만드셔도 됩니다. 하지만 256가지의 크기를 가지는 그레이 스케일(흑백)이라 해도 직접 히스토그램을 만드시기에는 어려움이 따르실 겁니다. 그래서 굳이 히스토그램을 만들지 말고, 다른 영상에 있는 히스토그램을 가져오는 방법이 있습니다.

 

peppers.png
0.51MB

이번 명세화에 사용될 영상입니다. 이제 이걸 pepper라고 하겠습니다.

 

 

img = cv2.imread('lenna.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
target = cv2.imread('peppers.png')
target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)

늘 그렇듯 이미지를 불러와줍니다. 이번에는 타깃 영상이 존재하니, 타깃 영상도 불러와 줍니다.

 

shape = gray.shape
original = gray.ravel()
specified = target_gray.ravel()

이제 작업을 위한 기본 준비로, 영상의 크기와 원본 영상과 타깃 영상의 1차 원화 된 배열을 준비합니다.

 

s_values, bin_idx, s_counts = np.unique(original, return_inverse=True, return_counts=True)

이번에 새로운 함수로 unique가 나왔습니다. unique는 배열이나 리스트의 값들 중에서 중복이 되는 값들을 제거하고, 크기순으로 정렬해주는 함수입니다. 예를 들어 [5, 1, 2, 4, 5, 2, 2]라는 배열(리스트)이 있다면, 이를 unique함수에 넣으면 [1, 2, 4, 5]가 반환되는 형식이죠.

이 unique함수에는 여러 인자가 같이 붙을 수 있는데요. return_inverse가 True라면 반환 값에 원래의 위치에 해당하는 값이 반환됩니다. 예를 들어 위에서 [1, 2, 4, 5]가 반환되었을 때, return_inverse=True가 있었다면, [3, 0, 1, 2, 3, 1, 1]이 같이 반환되는 형식입니다. 이 값이 있다면 1:1 매칭을 통해 다시 원래의 배열을 만들 수 있는 겁니다.

return_counts함수가 True라면 반환 값에 각 원소가 몇 개가 있었는지를 반환해줍니다. 위의 값에서 [1, 2, 4, 5]가 반환되었을 때, [1, 3, 1, 2]가 같이 반환되는 형식으로 입니다.

t_values, t_counts = np.unique(specified, return_counts=True)

같은 형식으로 specified들도 unique값들과 counts값들을 계산해줍니다.

 

s_quantiles = np.cumsum(s_counts).astype(np.float64)
s_quantiles /= s_quantiles[-1]
sour = np.around(s_quantiles * 255)

t_quantiles = np.cumsum(t_counts).astype(np.float64)
t_quantiles /= t_quantiles[-1]
temp = np.around(t_quantiles * 255)

이제 각 값들을 정규화된 누적합으로 바꾸어 줄 차례입니다. 평활화에서도 소개해 드렸던 cumsum함수를 통해 각 원소의 개수의 누적합을 계산하고, 그중 가장 큰 값(배열의 맨 마지막 값)으로 나누어줍니다.

그 후 255를 곱해 정규화해주고, arounds함수를 통해 0.5를 기준으로 소수를 반올림해 줍니다. 어차피 역 평활화는 가장 가까운 수로 바뀌는 거니까요!

 

b = []

이제 빈 리스트 b를 선언해줍니다.

 

for data in sour:
    diff = temp - data
    mask = np.ma.less_equal(diff, -1)
    if np.all(mask):
        c = np.abs(diff).argmin()
        b.append(c)
    masked_diff = np.ma.masked_array(diff, mask)
    b.append(masked_diff.argmin())

이 부분이 이번 강의의 핵심입니다.

우선 타깃 영상의 누적합인 temp에서 원본 영상의 값인 data를 뺀 diff를 계산합니다.

그 후 np.ma.less_equal(diff, -1)을 이용합니다. 이 ma라는 함수는 마스크와 관련된 함수입니다. 마스크는 계산에서 제외되는 부분을 말하게 되는데, ma가 그 대부분을 처리합니다. 그중 중요한 부분이 그다음 붙은 less_equal입니다. 해당 코드를 기준으로 설명드리면, "diff에서 -1 이하인 값들을 마스크 처리하라."입니다. 즉, 음수는 전부 마스크 처리가 됩니다.(소수는 전부 around함수로 반올림되었기에)

 

if문은 np.all(mask)라는 부분이 있습니다. np.all(배열(리스트))는 배열(리스트)이 전부 True인가를 판별하여, 전부 True라면 True를, 아니라면 False를 반환합니다. 아까 np.ma.less_equal(diff, -1)을 통해 음수는 전부 마스크 처리하라고 했습니다. 이때, print(mask)를 통해 살펴보면 마스크 처리될 부분들은 True, 마스크 처리되지 않을 부분들은 False가 되어 있습니다. 즉 np.all(mask)는 diff의 모든 값이 음수인가? 와 같은 뜻이죠.

 

이제 모든 값이 음수라면 if문이 작동합니다. diff의 값들을 전부 절댓값(abs함수) 처리하고, 그중 가장 작은 값(argmin함수)을 c에 저장한 후, b에 append(추가)합니다.

이때 생각해볼 점은 diff가 모두 음수라면, 절댓값을 취했을 때 가장 작은 값이 무엇인가입니다. 원래는 작은 값이 맨 앞, 큰 값이 맨 뒤에 있었는데 모두 음수가 나왔다면 제일 큰 값보다 더 큰 값으로 뺄셈이 이루어진 것이겠죠? 그렇기에 절댓값(diff)에서 가장 작은 값은 원래의 diff의 맨 마지막 값이라는 뜻입니다.

 

if문에 해당하지 않거나, if문이 끝나면 다음 코드가 작동합니다. 아까 설명드렸다시피 ma는 마스크와 관련된 함수입니다. 그래서 masked_array는 이름에서도 유추할 수 있듯이 마스크를 통해 새로운 배열(array)을 만드는 함수입니다. 즉 음수들은 마스크 된 새로운 배열 masked_diff가 만들어지는 거죠.

 

이제 masked_diff에서 가장 작은 값이 b에 append 됩니다.

 

LUT = np.array(b, dtype='uint8')

이제 LUT화 시켜줍니다. 위에서 계산했던 b의 값을 정수화(dtype='uint8')하여 배열로 만들어서 LUT를 만들어줍니다.

 

 

그런데 아까 원본 영상을 평활화하고, 타깃 영상도 평활화하고 하라고 했는데, 코드에는 그런 부분이 보이지 않죠? 명세화의 기본 원리는 평활화와 역 평활화를 통해 하는 게 맞습니다. 하지만 그렇게 히스토그램을 만들어서 하나하나 하기에는 파이썬에서는 별로 추천하지 않습니다. 그렇기에 굳이 평활화하지 않고, 바로 원본 영상과 타깃 영상의 누적 합의 차이로 LUT를 만드는 방법을 사용하는 것입니다.

 

out = np.array(LUT[bin_idx].reshape(shape))

자, 이제 대망의 결괏값입니다. LUT에 bin_idx(원래 위치에 해당하는 값들이 저장된 변수)를 넣고, 이를 reshape함수를 통해 원래의 모양대로 만들어줍니다. 이를 위해 위에서 shape = gray.shape가 필요했었던 것입니다.

 

 

이제 잘 되었나 확인해보겠습니다.

 

원본 영상
타겟 영상
명세화된 영상
왼쪽 : 원본 영상 / 중앙 : 타겟 영상 / 오른쪽 : 명세화된 영상

 

원본 영상과 명세화된 영상의 차이가 보이시나요? 겉으로 봤을 땐 크게 느껴지지 않으실 수 있습니다. 하지만 히스토그램을 보시면 원본 영상의 히스토그램보다는 타깃 영상의 히스토그램의 모양과 더 닮았음을 알 수 있습니다. 명세화가 잘 된 것이죠!


2. 마치며

 

히스토그램 명세화는 이상하게 국내에서는 인터넷에 코드가 잘 없었습니다. 그래서 공부할 때, 해외 쪽 커뮤니티를 찾아보며 공부를 했어서 그때의 네이밍이 그대로 반영되어있습니다. 그래서 제가 하던 네이밍에 익숙하신 분들은 갑자기 바뀐 네이밍에 어색하실 수도 있습니다.

 

 

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


img = cv2.imread('lenna.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
target = cv2.imread('peppers.png')
target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
shape = gray.shape
original = gray.ravel()
specified = target_gray.ravel()
s_values, bin_idx, s_counts = np.unique(original, return_inverse=True, return_counts=True)
t_values, t_counts = np.unique(specified, return_counts=True)
s_quantiles = np.cumsum(s_counts).astype(np.float64)
s_quantiles /= s_quantiles[-1]
sour = np.around(s_quantiles * 255)
t_quantiles = np.cumsum(t_counts).astype(np.float64)
t_quantiles /= t_quantiles[-1]
temp = np.around(t_quantiles * 255)
b = []
for data in sour:
    diff = temp - data
    mask = np.ma.less_equal(diff, -1)
    if np.all(mask):
        c = np.abs(diff).argmin()
        b.append(c)
    masked_diff = np.ma.masked_array(diff, mask)
    b.append(masked_diff.argmin())
LUT = np.array(b, dtype='uint8')
out = np.array(LUT[bin_idx].reshape(shape))
cv2.imshow('original', gray)
cv2.imshow('target', target_gray)
cv2.imshow('out', out)

plt.figure()
plt.subplot(1, 3, 1)
plt.hist(img.ravel(), 256, [0, 256])
plt.subplot(1, 3, 2)
plt.hist(target.ravel(), 256, [0, 256])
plt.subplot(1, 3, 3)
plt.hist(out.ravel(), 256, [0, 256])
plt.show()

cv2.waitKey(0)

 

반응형