OpenCV - 영상의 밝기/명암 조절

4 분 소요

이미지 밝기 조절

픽셀값은 255에 가까울수록 밝고, 0에 가까울수록 어둡다. 이 점을 활용하여 픽셀 값에 값을 더하거나 빼줌으로서 이미지의 밝기 조절이 가능하다.

img = cv2.imread('a.jpg')
img2 = img+20  #이미지 밝게 처리
img3 = img-20  #이미지 어둡게 처리

포화연산

그런데, 색상 값은 0-255 사이의 값을 가지는데 +, - 연산을 수행하면 0보다 작거나 255보다 큰 값을 만들어낼 수도 있다.

하지만 이미지 값의 타입은 uint8, 8비트 숫자 255까지만 표현이 가능하기 때문에 256은 이진수로 100000000(2) 9비트가 필요하여 uint8 형식에서는 다시 0이 된다. 이렇게 처리되면 아주 밝은 부분이 아주 어둡게, 또는 아주 어두운 부분이 아주 밝게 반대로 표현되어 이미지가 변질된다.

그래서 픽셀값이 255를 넘으면 255로, 0 미만이면 0으로 처리하는 것이 포화연산이다.

np.clip을 활용한 포화연산

# np.clip(src, 최소값, 최대값)

import numpy as np

arr = np.array([-12, 213, 307])
arr2 = np.clip(arr, 0, 255)
arr2
>>> array([0, 213, 255])

주의할 점

#1
img2 = img+100
img3 = np.clip(img2, 0, 255)

#2
img2 = np.clip(img+100, 0, 255)

#3
img2 = img.astype('int32')
img3 = np.clip(img2, 0, 255)
img3 = img3.astype('uint8')
  • 위 #1 처럼 코드를 작성할 경우 이미 img2는 uint8로 처리가 되어있기 때문에 np.clip을 해도 소용이 없다.

    #2 처럼 np.clip 안에서 연산을 진행시키거나 #3 처럼 이미지 타입을 uint8이 아닌 다른 것으로 변환시켜준 후 np.clip을 사용해준 뒤 다시 uint8로 돌려주면 된다.

이미지 명암비(대비) 조절

명암비 = 이미지의 밝은 부분과 어두운 부분의 밝기 차를 의미.

명암비 조절은 이미지의 밝은 부분은 더 밝게, 어두운 부분은 더 어둡게 함으로써 이미지 윤곽을 뚜렷하게 해주어 활용도가 높다.

색상 값을 곱해주기

def saturate_contrast1(p, num):
    pic = p.copy()
    pic = pic.astype('int64')
    pic = np.clip(pic*num, 0, 255)
    pic = pic.astype('uint8')
    return pic

#명암비를 1보다 작게 주면 밝기 차가 줄어들고 전반적으로 어두어짐
img4 = saturate_contrast1(img,0.5)

#명암비를 1보다 크게 주면 밝기 차가 커지고 흰색 영역이 넓어짐
img5 = saturate_contrast1(img, 2)  
  • 값이 클 수록 배수의 값도 더 커지기 때문에 밝은 부분은 더 밝게, 어두운 부분은 덜 밝게 하는 것이 가능하다.
  • 하지만 밝은 부분은 더 밝게 하고 어두운 부분을 더 어둡게 하는 것은 이 방식으로 되지 않는다.

명암비의 효율적 조절

명암 차이를 크게 만들기 위해서는 밝은 부분은 더 밝게 해주고, 어두운 부분은 더 어둡게 해줘야 한다. 밝고 어두움의 기준으로 0~255의 중간 값인 128을 사용하고, 128보다 더 큰 값은 더 밝게 만들고 128보다 작은 값은 더 어둡게 만들어 대비를 더 크게 만들어주면 된다.

**dst(x,y) = src(x,y) + (src(x,y)-128)*alpha**

# src(x,y)-128 을 통해 128보다 작은 값은 음수로 만들고 128보다 큰 값은 양수로 만든다
# alpha는 대비를 얼마나 크게 만들 것인지 결정하는 -1 보다 같거나 큰 실수
def saturate_contrast2(p, num):
    pic = p.copy()
    pic = pic.astype('int32')
    pic = np.clip(pic+(pic-128)*num, 0, 255)
    pic = pic.astype('uint8')
    return pic

img4 = saturate_contrast2(img,-0.5)
img5 = saturate_contrast2(img, 2)

히스토그램 분석

주어진 영상의 픽셀 밝기 분포를 조사하여 밝기 및 명암비를 조절할 수 있다.

히스토그램 구하기

히스토그램: 영상의 픽셀 값 분포를 그래프 형태로 표현한 것. 모든 픽셀의 밝기 값들을 구한 후 밝기 값 마다의 픽셀 개수를 세어서 그래프를 구성한다.

히스토그램에서 가로축을 히스토그램의 빈(bin) 이라고 하며 그레이스케일 영상의 경우 256개의 빈을 가진 히스토그램을 구하는 것이 일반적이지만 경우에 따라서 빈 개수를 다르게 할 수도 있다.

히스토그램은 다음 함수를 사용하여 계산한다.

cv2.calcHist([img], channel, mask, histSize, range)

  • img: 이미지 배열
  • channel: 분설할 색상 채널
  • mask: 분석할 영역. None이면 이미지 전체를 의미
  • histSize: 히스토그램의 크기 = 빈의 개수
  • range: x축 값 범위

사용 예

import cv2
from matplotlib import pyplot as plt

img = cv2.imread('img/lenna.png', 0)
**hist = cv2.calcHist([img], [0], None, [256], [0,255])**
plt.subplot(2,1,1),plt.imshow(img,'gray')
plt.subplot(2,1,2),plt.plot(hist)
plt.xlim([0,255])
plt.show()

1

위 이미지의 밝기를 조절할 경우, 더 밝게 한 경우는 히스토그램이 전체적으로 오른쪽으로 이동하고 더 어둡게 한 경우는 히스토그램이 전체적으로 왼쪽으로 이동한다.

2

3

명암비를 크게 주면 분포가 아주 밝고 아주 어두운 쪽으로 이동하게 되며,

img = saturate_contrast2(img, 2)  # 위에서 사용했던 명암비 조절 함수, 명암비 2
hist = cv2.calcHist([img],[0],None,[256],[0,256])
plt.subplot(2,1,1),plt.imshow(img4,'gray')
plt.subplot(2, 1, 2),plt.plot(hist,color='r')
plt.xlim([0,256])
plt.show()

4

명암비를 줄이면 분포 영역이 더 작아진다. 이는 이미지 색상이 비슷한 값에 몰려있다는 뜻이며 이미지가 선명하지 않음을 나타낸다.

img = saturate_contrast2(img, -0.8)  # 위에서 사용했던 명암비 조절 함수, 명암비 -0.8
hist = cv2.calcHist([img],[0],None,[256],[0,256])
plt.subplot(2,1,1),plt.imshow(img4,'gray')
plt.subplot(2, 1, 2),plt.plot(hist,color='r')
plt.xlim([0,256])
plt.show()

5

히스토그램 스트레칭 (histogram stretching)

위 이미지 처럼 명암비가 낮아 히스토그램이 특정 구간에 집중되어 있을 때 히스토그램을 펼쳐서 그래프가 그레이스케일 전 구간에서 나타나도록 변환하는 기법을 히스토그램 스트레칭이라 한다.

히스토그램 스트레칭을 수행하면 명암비가 높아지기 때문에 흐릿한 이미지가 더 선명해진다.

히스토그램 스트레칭을 수식으로 표현하면 다음과 같다.

**dst(x, y) = ((src(x, y) - 최소픽셀값)/(최대픽셀값 - 최소픽셀값)) *255**

원본(src)에서 최소픽셀값을 빼 결과물(dst)의 최소픽셀값이 0이 되도록 하고, 히스토그램의 범위(최대픽셀값 - 최소픽셀값)로 나눈 값에 255를 곱해 결과물(dst)의 최대픽셀값은 255가 되도록 한다.

이 과정을 코드로 구현하면 다음과 같다

f_max = src.max()  # 최대픽셀값
f_min = src.min()  # 최소픽셀값
nframe = img.astype('int64')
dst = np.clip(((nframe - f_min)/(f_max - f_min))*255, 0, 255).astype('uint8')

hist = cv2.calcHist([dst],[0],None,[256],[0,256])
plt.subplot(2,1,1),plt.imshow(dst)
plt.subplot(2, 1, 2),plt.plot(hist,color='r')
plt.xlim([0,256])
plt.show()

6

히스토그램 평활화 (histogram equalization)

히스토그램 스트레칭은 분포도를 넓혀주지만 특정 그레이스케일 값에 픽셀 분포가 너무 뭉쳐있는 경우 이를 평준화 해주지는 않는다. 히스토그램 평활화는 그래프에서 너무 돌출되는 부분을 깎아주어 평준화해준다.

이는 다음 수식을 사용한다.

**dst(x, y) = round((누적합(src(x, y))*픽셀최대값)/픽셀누적최대값-최소값)**

그리고 수식을 코드로 구현할 경우 다음과 같다.

hist, bins = np.histogram(src.flatten(), 256,[0,255])

cdf = hist.cumsum()  # 누적합. 각 빈의 누적합 계산
cdf_m = np.ma.masked_equal(cdf,0)  # 속도개선을 위해 0인 부분 제외

#히스토그램 평활화
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())

# Mask처리를 했던 부분을 다시 0으로 변환
cdf = np.ma.filled(cdf_m,0).astype('uint8')

dst = cdf[src]  # src의 값이 cdf배열의 인덱스로 사용됨
                # cdf는 히스토그램 평활화된 값이 저장되어 있으므로
                # src[12][10]칸의 픽셀값이 125이면 cdf[125]의 값을 추출
                # 이 값은 픽셀값 125가 평활화된 값이다
            
hist = cv2.calcHist([dst],[0],None,[256],[0,256])  # 결과

하지만 히스토그램 스트레칭과는 다르게 평활화는 별도 함수가 존재하므로 매번 위 코드를 작성하지 않아도 된다! 히스토그램 평활화 함수는 cv2.equalizeHist()

dst = cv2.equalizeHist(src)
hist = cv2.calcHist([dst],[0],None,[256],[0,256])
plt.subplot(3,1,1),plt.imshow(src,'gray')
plt.subplot(3,1,2),plt.imshow(dst,'gray')
plt.subplot(3,1,3),plt.plot(hist,color='r')
plt.xlim([0,256])
plt.show()

7

댓글남기기