[python] 이미지 전처리 Auto White Balance 알고리즘

우연·2024년 11월 17일

사람의 눈은 빛의 색, 조명 조건 등과 상관없이 색을 인식할 수 있는 능력이 있지만 컴퓨터는 그렇지 않다.
흰색이 흰색으로 보이도록 하는 과정이 이미지 전처리에 있어서 필요한 이유다.

White Balancing은 다양한 조명 조건의 영향을 보정하여 올바른 색으로 보정해주는 기술이다.
이미지 속 다른 색들보다는 흰색이 흰색이도록 조정하는 방식으로 적용된다.

대표적인 3가지 알고리즘을 python으로 구현한 코드에 대해 설명하겠다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from skimage import io, img_as_ubyte
from skimage.io import imread, imshow
from matplotlib.patches import Rectangle

사용하는 라이브러리들이다.
기본적인 라이브러리들로 구현이 가능하다.

from skimage import io
import matplotlib.pyplot as plt

image = io.imread('./test_img.jpg')  #이미지 경로에서 np.array 형식으로 이미지 불러옴
plt.figure(figsize=(10,10))          #크기 설정
plt.title('Original Image')          #제목 설정
plt.imshow(image)
plt.show()							 #출력


먼저 보정할 이미지 원본이다.

AWB를 사용하려 하는 나의 프로젝트는 기저귀 위의 아기똥을 분류하는 서비스다.
즉 주로 흰 기저귀 위에 놓인 아기똥이 분류 대상이다.
이러한 점을 반영하기 위해 흰 배경에 놓인 버건디색의 다이어리 사진을 사용하여 auto white balance algorithm들을 적용해보려 한다.

먼저 보정하기 전에 이미지의 색상값이 얼마나 치우쳐져 있는 지 확인해보겠다.

def calc_color_overcast(image):
    #각 color channel 별 색상 값 출력
    red_channel = image[:, :, 0]
    green_channel = image[:, :, 1]
    blue_channel = image[:, :, 2]

    # 색상별 평균, 표준편차, 최소값, 중앙값, 80%, 90%, 99%, 최대값 계산하여 데이터프레임 형식으로 출력
    channel_stats = pd.DataFrame(columns=['Mean', 'Std', 'Min', 'Median', 'P_80', 'P_90', 'P_99', 'Max'])

    for channel, name in zip([red_channel, green_channel, blue_channel], ['Red', 'Green', 'Blue']):
        mean = np.mean(channel)					#평균 계산
        std = np.std(channel)					#표준편차 계산
        minimum = np.min(channel)			 	#최소값 계산
        median = np.median(channel)				#중앙값 계산
        p_80 = np.percentile(channel, 80)		#80% 백분위수 계산
        p_90 = np.percentile(channel, 90)		#90% 백분위수 계산
        p_99 = np.percentile(channel, 99)       #99% 백분위수 계산
        maximum = np.max(channel)				#최대값 계산

        channel_stats.loc[name] = [mean, std, minimum, median, p_80, p_90, p_99, maximum]

    return channel_stats

calc_color_overcast(image)


작성한 함수를 이용해서 이미지를 살펴보니 빨간색의 비중이 크고 파란색의 비중이 적다.
그리고 가장 밝은 곳도 흰색이 아님을 확인할 수 있다.

1. white patch algorithm

첫번째 알고리즘은 각각의 color channel의 제일 밝은 픽셀이 흰색이 되도록 스케일링 하는 것을 목표로 한다. 이미지의 가장 밝은 픽셀이 흰색일 것이라고 가정하기 때문에 가능한 알고리즘이다.

장점
  • 쉽고 간단
  • 흰색 부분이 두드러지거나 회색 영역이 존재하는 이미지에서 효과적
  • 이미지에 밝은 부분이 구별될 때 잘 작동
단점
  • 가정이 성립하지 않을 수 있음
  • 가정이 성립하지 않을 경우에 over-correction 으로 자연스럽지 않은 결과를 얻을 수 있음
  • 이미지의 일부 영역에서 색상 이동 등이 발생할 수 있음

알려진 가장 밝은 부분이 흰색이고 이미지 내에 조명 조건이 일정할 때 효과적으로 작동하지만 여러개의 조명이 존재하거나 조명 조건이 일정하지 않을 때 제대로 작동하지 않는다.

def white_patch(image, percentile=100):

    """
    Parameters
    ----------
    image : 입력으로 받는 이미지는 RGB 채널을 각각 가지는 (height, width, channels) 3차원 numpy 배열
    percentile : 채널 값의 보정값으로 사용하고자 하는 값을 지정하기 위한 비율, 기본값은 100으로 최대값을 사용
    """
    
    white_patch_image = img_as_ubyte((image * 1.0     #이미지 배열을 부동 소수점 형식으로 변환하여 나눗셈 연산이 제대로 수행되도록 함
        							  / np.percentile #지정된 백분위 수 계산
                                      	(image, 	  #입력 이미지
                                     	 percentile,  #입력 퍼센티지
                                     	 axis=(0, 1)  #높이, 너비 축을 따라 계산
                                        )             #이미지가 반환된 보정값을 사용해 정규화 됨
                                      ).clip(0, 1)    #0,1 사이의 범위로 맞춰줌  
                                     )
    return white_patch_image

코드를 하나씩 살펴보면 이렇다.

axis=(0, 1) 옵션은 (height, width, channels) 중 0, 1 번째인 이미지의 높이와 너비 축을 따라 백분위 값을 계산하도록 지정한다. 즉, 각 채널 (Red, Green, Blue)에 대해 독립적으로 계산하도록 한다.

percentile 옵션은 사용할 백분위 값으로, 100이 기본값이다. 예를 들어, percentile=100이면 각 채널의 최대값을 구한다. 인자로 입력받은 percentile 값을 사용할 수 있다.

이 옵션들을 통해 np.percentile() 은 지정된 percentile의 백분위 수를 각 채널별로 1개씩 반환하게 된다.

구한 percentile 값으로 image * 1.0을 통해 부동소숫점으로 변환된 이미지를 나누어 정규화가 진행된다.

.clip(0, 1) 은 이렇게 정규화된 이미지 값을 0과 1 사이로 클리핑한다. 즉, 0보다 작은 값은 0으로, 1보다 큰 값은 1로 제한한다.이 과정은 각 채널의 값이 유효한 범위 내에 있도록 보장한다.

img_as_ubyte() 는 클리핑된 이미지를 0에서 255 사이의 값으로 스케일링하여 이미지 형식인 unit8의 8비트 정수형 형식으로 변환하여 최종적으로 보정된 이미지를 반환하게 된다.

def compare_imgs(left_img, right_img):    #비교를 위한 두 개의 이미지를 입력받아 나란히 출력

   fig, ax = plt.subplots(1, 2, figsize=(10, 10))   #화면을 2개로 분할한다
   ax[0].imshow(left_img)							#왼쪽 이미지 출력
   ax[0].set_title('Original Image')
   ax[0].axis('off')								#깔끔한 이미지를 위한 축 제거

   ax[1].imshow(right_img)							#오른쪽 이미지 출력
   ax[1].set_title('Corrected Image')
   ax[1].axis('off')

   plt.show()

원본 이미지와 보정된 이미지를 나란히 출력해 비교해서 보기 위한 함수다.

white_patch_image = white_patch(image, 100)
compare_imgs(image, white_patch_image)

이 경우에 두드러지지는 않지만 이미지의 가장 밝은 값이 흰색인 [255,255,255] 값을 가질 때 제대로 보정되지 않을 수 있다. 2가지 정도의 이유가 있다.

첫번째는 이미지에서 극히 일부만 최대값인 255를 가지고 있을 때 이런 극단적인 값이 전체 이미지 보정에 과도한 영향을 미치게 되면서 나머지 부분이 과하게 밝아지거나 색상이 왜곡될 수 있다.
두번째로는 최대값 255는 전체 이미지 색상 분포를 대표하지 못할 수 있기 때문이다.

white_patch_image = white_patch(image, 85)
compare_imgs(image, white_patch_image)


그럴 때는 이렇게 percentile 값을 낮추어 상위 값들을 제외한 보정값을 사용하여 극단적인 값이 보정에 미치는 영향을 줄이고, 전체 이미지의 색상 균형을 보다 잘 맞출 수 있게 할 수 있다.


2. Gray world algorithm

Gray world algorithm은 이미지의 색상값 평균은 회색이어야 한다고 가정하여 이에 따라 색상값들을 보정한다.

장점
  • 간단하고 효과적인 연산
  • 이미지 색상값의 평균은 회색이라는 가정은 대부분의 이미지에 있어서 상당히 합리적인 가정
단점
  • 하나의 특정 색이 지배적인 이미지에 취약하다
  • 가정이 항상 성립하는 것이 아니다
def gray_world(image):
    
    image_grayworld = ((image * 	
                        	(image.mean() / 				#전체 이미지의 평균
                            	image.mean(axis=(0, 1)))  	#각 채널별 평균
                        )  									#각 채널의 평균 값이 동일해짐
                        .clip(0, 255).astype(int))			#이미지 형식으로 변환
    return gray_world_image

image.mean(axis=(0, 1)) 으로 각 채널(R, G, B)의 평균 값을 계산한다.

image * (image.mean() / image.mean(axis=(0, 1))) 는 전체 이미지의 평균 값을 각 채널의 평균 값으로 나누어 전체 이미지 값에 곱해주어 채널 별로 보정한다. 각 채널의 평균 값이 동일해지도록 조정하게 된다.

.clip(0, 255) : 값이 0과 255 사이에 있도록 제한한다.

.astype(int): 대부분 이미지 파일 형식은 이미지를 uint8, 8비트 정수형으로 저장하므로 보정된 이미지를 정수형으로 확실히 변환한다.

gray_world_image = gray_world(image)
compare_imgs(image, gray_world_image)

흰색과 버건디색이 대부분을 차지하므로 gray world algorithm의 기본 가정을 거스르게 되므로 제대로 보정되지 않은 것으로 보인다.
특히, 앞에서 살펴봤던 색상 값들의 분포를 고려해보면 비중이 많았던 붉은 빛이 줄어들고 비중이 적었던 푸른 빛이 늘어났음을 확인할 수 있다.


3. Ground Truth algorithm

Ground Truth algorithm은 흰색으로 알려진 이미지의 일부를 레퍼런스로 하여 이미지를 보정한다.

보정값으로 어떤 값을 사용하는지에 따른 2가지 방법이 있다.
1. 평균 사용 : 레퍼런스의 이미지 값 평균으로 이미지를 보정
2. 최대값 사용 : 레퍼런스의 이미지 값들 중 최대값으로 이미지를 보정
사실상 레퍼런스가 있는 상황에서 white patch와 gray world중 무엇을 사용하나의 차이와 다름없다.

장점
  • 레퍼런스를 통해 보다 정확한 보정이 가능
  • 정밀성을 요구하는 분야에 사용하기 적절하다
단점
  • 흰색으로 알려진 레퍼런스가 존재하지 않을 수 있다
  • 레퍼런스로 사용할 부분에 대한 입력이 필요하다는 점에서 다른 두 가지 알고리즘에 비해 덜 자동화 되어있다
def ground_truth(image, img_patch, mode='mean'):
    """
    Parameters
    ----------
    image 
    img_patch : 기준으로 삼을 레퍼런스
    mode : optional, mean 혹은 max로 계산 방법 설정
    """
    
    # 평균을 사용하는 경우 - gray world 에서의 보정 방식과 동일
    if mode == 'mean':
        ground_truth_image = ((image * (img_patch.mean() / image.mean(axis=(0, 1)))).clip(0, 255).astype(int))
       
    # 최대값을 사용하는 경우 - white patch 에서의 보정 방식과 동일
    if mode == 'max':
        ground_truth_image = ((image * 1.0 / img_patch.max(axis=(0, 1))).clip(0, 1))

    return ground_truth_image
from matplotlib.patches import Rectangle   #이미지 위에 사각형을 그릴 수 있게 해줌

fig, ax = plt.subplots(figsize=(10,10))
ax.set_title('Reference patch in red square')
ax.imshow(image)
ax.add_patch(Rectangle((800, 210), 50, 50, edgecolor='r', facecolor='none'))		#사각형의 왼쪽 위 모서리 좌표, 사각형의 너비, 높이, 테두리 색상(빨간색), 사각형 내부 (투명하게) 설정

흰 이불이라는 사실을 알고 있기 때문에 이불의 일부분을 레퍼런스로 지정할 수 있다.

다만 이 사진의 경우에는 그림자 때문에 빛이 이미지에 전반적으로 고른 영향을 주지 못하고 있다. 그에 따라 달라지는 보정 결과도 함께 살펴보겠다.
먼저 그림자 진 부분을 레퍼런스 삼았을 때의 결과를 살펴보겠다.

#레퍼런스 출력
img_patch = image[210:260, 800:850]    #행, 열을 그대로 사용하므로 위의 Rectangle과 달리 y,x 순서로 입력해주어야 함을 조심
imshow(img_patch)

#평균 사용
ground_truth_image = ground_truth(image, img_patch, mode='mean')
compare_imgs(image, ground_truth_image)

#최대값 사용
ground_truth_image = ground_truth(image, img_patch, mode='max')
compare_imgs(image, ground_truth_image)

앞서서 gray world 보다 white patch 알고리즘이 잘 작용했던 것처럼 ground truth 에서 최대값을 사용하는 경우에 더 좋은 성능을 보인다.


이번에는 그림자 지지 않은 곳을 레퍼런스로 사용해보겠다

from matplotlib.patches import Rectangle

fig, ax = plt.subplots(figsize=(10,10))
ax.set_title('Reference patch in red square')
ax.imshow(image)
ax.add_patch(Rectangle((500,100), 50, 50, edgecolor='r', facecolor='none'));

img_patch = image[100:150,500:550]
imshow(img_patch)

#평균 사용
ground_truth_image = ground_truth(image, img_patch, mode='mean')
compare_imgs(image, ground_truth_image)

#최대값 사용
ground_truth_image = ground_truth(image, img_patch, mode='max')
compare_imgs(image, ground_truth_image)

최대값을 사용했을 때 이 이미지의 경우에는 레퍼런스의 차이에도 큰 영향을 받지 않았다.

결론적으로 서비스에 적용하기에 레퍼런스가 필요한 ground truth 알고리즘은 적절하지 않다고 판단하여 제외되었다. white patch 가 gray world 보다 좋은 성능을 보여 white patch 알고리즘을 사용할 예정이다.

0개의 댓글