본문 바로가기

OpenCV

이미지 프로세싱 (3)

1. 1D 히스토그램

■ 1차원 히스토그램은 도수 분포표를 그래프로 나타낸 것이다.
전체 영상/이미지의 픽셀 값 중 픽셀 값 1이 몇 개인지, 2가 몇 개인지, ... , 255가 몇 개인지 히스토그램으로 나타내면 전체 영상/이미지에서 픽셀들의 색상이나 명암의 분포를 파악할 수 있다.
OpenCV에서는 cv2.calHist(img, channel, mask, histSize, ranges) 함수로 영상/이미지에 대한 히스토그램을 생성할 수 있다.
- img는 영상/이미지로  리스트([img])로 값을 전달해야 한다.
- channel은 분석할 채널이며 리스트로 값을 전달해야 한다. 예를 들어 BGRA에서 Blue 채널을 분석하고 싶으면 Blue 채널의 인덱스는 0이므로 [0]을 전달하면 된다.
- mask 파라미터를 사용하면 마스크에 지정한 픽셀만 히스토그램 계산을 할 수 있다. None으로 지정하면 전체 영역에 대해 히스토그램을 계산한다.
- histSize는 Bin의 개수로 채널 개수에 맞게 리스트로 전달해야 한다. 예를 들어 1 채널이면 [256], 2 채널이면 [256, 256], 3 채널이면 [256, 256, 256]
- ranges에는 각 픽셀이 가질 수 있는 값의 범위를 지정한다. 예를 들어 RGB인 경우 [0, 256]

■ 회색조 이미지는 채널이 한 개이므로 하나의 그래프, 컬러 이미지는 채널이 R, G, B 세 가지이므로 3개의 그래프를 그려 픽셀 값의 분포를 확인해야 한다.

img = cv2.imread('555.jpg', cv2.IMREAD_GRAYSCALE) # 그레이 스케일
hist = cv2.calcHist([img], [0], None, [256], [0, 256])

- 이 예에서 img는 그레이 스케일이므로 채널은 하나 밖에 없다. (채널의 인덱스는 0밖에 없다.)

plt.subplot(2, 1, 1)
plt.title('img')
plt.imshow(img, cmap = 'gray')
plt.xticks([]); plt.yticks([])

plt.subplot(2, 1, 2)
plt.title('hist')
plt.plot(hist)

print('hist.shape:',hist.shape)
```#결과#```
hist.shape: (256, 1)
````````````

print(f'hist.sum(): {hist.sum()}, img.shape: {img.shape}') # 히스토그램 총 합계와 이미지 크기
```#결과#```
hist.sum(): 1555200.0, img.shape: (1440, 1080)
````````````

- 이미지 크기가 1080 x 1440이므로 총 픽셀의 개수는 1,555,200개이다. 그 중에서 픽셀 값 90의 개수가 가장 많은 것을 확인할 수 있다.

img = cv2.imread('555.jpg') # BGR
channels = cv2.split(img) # R, G, B 채널 분리

plt.subplot(2, 1, 1)
plt.title('img')
plt.imshow(img[:,:,::-1])
plt.xticks([]); plt.yticks([])

plt.subplot(2, 1, 2)
plt.title('hist')
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()

-  히스토그램을 보면 전반적으로 파란색 분포가 큰 것을 확인할 수 있다. 이는 이미지에서 파란색 영역이 많기 때문이다.

 

2. 정규화(Normalize)

이미지 작업에서 정규화가 필요한 대표적인 경우는 픽셀의 분포가 특정 영역에 몰려 있어 명암비(contrast)가 낮은 경우이다. 
이러한 경우, 정규화를 통해 픽셀 분포를 늘려(stretching) 명암비를 높이면 밝은 영역은 더 밝아지고 어두운 영역은 더 어두워져, 뿌옇게 찍힌 이미지를 보다 선명하게 보이도록 할 수 있다.
OpenCV에서는 cv2.normalize(src, dst, alpha, beta, type_flag) 함수로 영상/이미지에 대한 정규화를 적용할 수 있다.
- src는 정규화할 영상/이미지
- dst는 정규화 이후(=결과) 영상/이미지
- alpha는 정규화 구간1 (최솟값)
- beta는 정규화 구간2 (최댓값)
- type_flag에는 사용할 정규화 알고리즘을 지정한다.

flag 설명
cv2.NORM_MINMAX alpha와 beta 구간으로 정규화
cv2.NORM_L1 L1 정규화
cv2.NORM_L2 L2 정규화.
cv2.NORM_INF 최댓값으로 나누기
img = cv2.imread('abnormal.jpg', cv2.IMREAD_GRAYSCALE)
img_norm = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)

hist = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_norm = cv2.calcHist([img_norm], [0], None, [256], [0, 256]) 

cv2.imshow('img',img)
cv2.imshow('img_normi',img_norm)

cv2.waitKey()
cv2.destroyAllWindows()

뿌옇게 찍힌 이미지 정규화 이전 vs 이후

plt.subplot(1, 2, 1)
plt.title('before')
plt.plot(hist)

plt.subplot(1, 2, 2)
plt.title('normalize')
plt.plot(hist_norm)

픽셀 분포 정규화 이전 vs 이후

■ 이미지 변화와 분포 변화를 보면, 특정 영역(100 ~ 200)에 몰려 있는 픽셀 분포를 최소-최대 정규화를 통해 분포를 늘려줌으로써 이미지가 더 선명해진 것을 확인할 수 있다.

- 정확히는 정규화 이전의 이미지의 픽셀 분포를 보면, 0(검은색)이나 255(흰색)에 픽셀이 부족한 것을 볼 수 있다. 그러므로 어두움과 밝음을 잘 표현할 수 없다. 

- 반면, 정규화된 이미지의 픽셀 분포가 0에서 255까지의 전체 범위에 비교적 고르게 퍼져 있음을 확인할 수 있다. 이는 정규화 이전의 이미지보다 명암비가 높아져 화질이 개선된 결과라고 볼 수 있다.

 

3. 평탄화(Equalization)

■ 정규화는 위의 예시처럼 분포가 한 곳에 집중되어 있는 경우 분포를 늘려주므로 명암비 개선의 효과를 볼 수 있다. 
그러나 분포가 집중된 영역에서 멀리 떨어진 영역에 값이 존재할 경우 효과가 떨어진다. 이런 경우에 사용하는 것이 평탄화이다. 

평탄화를 사용하면 다음 그림처럼 특정 영역에 집중되어 있는 분포를 재분배해서 명암비를 개선할 수 있다.

[출처] https://opencv-python.readthedocs.io/en/latest/doc/20.imageHistogramEqualization/imageHistogramEqualization.html

OpenCV에서는 cv2.equalizeHist(src, dst) 함수로 영상/이미지에 평탄화를 적용할 수 있다.
- src는 평탄화할 이미지, 그레이 스케일 영상/이미지(8비트 1채널)
- dst는 결과 이미지로 선택할 수 있는 옵션

img = cv2.imread('hist_unequ.jpg', cv2.IMREAD_GRAYSCALE) # 원본 이미지
img_norm = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX) # 정규화
img_eq = cv2.equalizeHist(img) # 평탄화

cv2.imshow('img',img)
cv2.imshow('img_norm',img_norm)
cv2.imshow('img_eq',img_eq)

cv2.waitKey()
cv2.destroyAllWindows()

원본 vs 정규화 vs 평탄화

hist = cv2.calcHist([img], [0], None, [256], [0, 256])
hist_norm = cv2.calcHist([img_norm], [0], None, [256], [0, 256]) 
hist_eq = cv2.calcHist([img_eq], [0], None, [256], [0, 256])

hists = {'Before':hist, 'normalize':hist_norm, 'equalize':hist_eq}

plt.figure(figsize=(12, 4))
for i, (k, v) in enumerate(hists.items()):
    plt.subplot(1,3,i+1)
    plt.title(k)
    plt.plot(v)
plt.subplots_adjust(wspace=0.5) 
plt.show()

원본 vs 정규화 vs 평탄화

■ 이미지 변화와 분포 변화를 보면, 원본 이미지의 픽셀 분포는 특정 영역(100 ~ 200)으로부터 떨어진 190 ~ 200 영역에 값이 존재하는 것을 확인할 수 있다. 평탄화를 적용했을 때가 가장 분포가 골고루 분포되어 있고 명암비도 정규화를 적용한 이미지보다 더 뚜렷한 것을 볼 수 있다.

■ 컬러 이미지에 대해서도 평탄화를 적용할 수 있다. 단, 평탄화의 목적이 명암비 개선이라면 BGR 타입을 사용하는 것보다 밝기에 특화된 YUV 타입을 사용해 Y(밝기) 채널 하나만 조절하는 것이 더 간단하다. 

- BGR 타입을 사용할 경우 3개 채널 모두 평탄화를 적용해야 하기 때문이다.

img = cv2.imread('yate.jpg') # BGR
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) # YUV

## Y 채널만 이퀄라이즈 적용
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()

컬러 이미지 평탄화 전 vs 후

- 요트 부분의 밝기가 더 개선된 것을 볼 수 있다.

 

4. CLAHE (Contrast Limited Adaptive Histogram Equalization)

■ 위의 예시처럼 밝은 부분과 어두운 부분이 섞여 있는 일반적인 영상/이미지 전체에 평탄화를 적용하면 어두운 부분은 밝아지지만, 너무 밝아진 부분은 경계선을 알아볼 수 없게 된다.
이 문제를 해결하기 위해 adaptive histogram equalization을 사용한다. 영상/이미지를 일정한 영역으로 나눠서 평탄화를 적용하는 하는 방법이다.
단, 나눠진 영역은 작은 영역이기 때문에 해당 영역에 노이즈(극단적으로 어둡거나 밝은 영역)가 존재하면 노이즈가 증폭되는 현상이 발생한다. 
이 문제를 방지하기 위해 contrast limit라는 제한 값을 지정한다. 제한 값을 넘으면 해당 픽셀을 다른 영역에 배분하고 나서 정규화를 적용한다.
OpenCV에서는 cv2.createCLAHE(clipLimit, tileGridSize)) 함수로 CLAHE 생성할 수 있다.
- clipLimit는 제한 값, contrast limit이다. 디폴트 값은 40
- tileGridSize는 영역 크기, 디폴트 값은 8 x 8
CLAHE를 적용하려면 CLAH에 apply(입력 영상/이미지) 함수를 적용하면 된다.

img = cv2.imread('clahe.png')
img_yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

## Y 채널에 평탄화 적용
img_eq = img_yuv.copy()
img_eq[:,:,0] = cv2.equalizeHist(img_eq[:,:,0]) 
img_eq = cv2.cvtColor(img_eq, cv2.COLOR_YUV2BGR) # 다시 YUV에서 BGR로 변경

img_clahe = img_yuv.copy()
## CLAHE 생성
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
## CLAHE 적용
img_clahe[:,:,0] = clahe.apply(img_clahe[:,:,0]) 
img_clahe = cv2.cvtColor(img_clahe, cv2.COLOR_YUV2BGR) # 다시 YUV에서 BGR로 변경
cv2.imshow('before', img)
cv2.imshow('equalize', img_eq)
cv2.imshow('CLAHE', img_clahe)
cv2.waitKey()
cv2.destroyAllWindows()

원본 vs 정규화 vs CLAHE

- 단순히 전체 이미지에 평탄화를 적용한 경우 가운데 영역이 너무 밝아져 윤곽선을 알아볼 수 없다. 
- 반면, CLAHE를 적용한 경우 윤곽선도 유지되면서 명암비도 개선된 것을 볼 수 있다.

 

5. 2D 히스토그램

■ 1D 히스토그램은 각 픽셀이 몇 개씩인지 픽셀 분포를 그래프로 나타내기 위해 사용했다면, 2D 히스토그램은 2개의 축을 사용해 각각의 축이 만나는 지점의 개수를 나타내기 위해 사용한다.
예를 들어 RGB 채널에서 R과 G, R과 B, G와 B축을 사용하여 각 축이 만나는 지점의 개수를 2D 히스토그램으로 표현할 수 있다.

img = cv2.imread('img1.jpg') # BGR
plt.imshow(img[:,:,::-1])
plt.xticks([]); plt.yticks([])

img = cv2.imread('img1.jpg') # BGR

plt.style.use('classic')  
plt.figure(figsize=(12, 4)) 
plt.subplot(1, 3, 1)
hist = cv2.calcHist([img], [0,1], None, [256,256], [0,256,0,256]) 
p = plt.imshow(hist)                                            
plt.title('B and G') # blue & green channel                             
plt.colorbar(p)                                                


plt.subplot(1, 3, 2)
hist = cv2.calcHist([img], [1,2], None, [256,256], [0,256,0,256])
p = plt.imshow(hist)
plt.title('G and R') # green & red channel   
plt.colorbar(p)

plt.subplot(1, 3, 3)
hist = cv2.calcHist([img], [0,2], None, [256,256], [0,256,0,256]) 
p = plt.imshow(hist)
plt.title('B and R') # blue & red channel   
plt.colorbar(p)

plt.subplots_adjust(wspace=0.5)
plt.show()

- 오른쪽 막대는 픽셀의 개수를 의미한다.

- 이미지가 하늘, 호수 초목 등을 담고 있으므로 전반적으로 R, G, B 채널이 비교적 균형잡힌 비율로 분포하는 것을 볼 수 있다.

- 하늘, 초목이 많기 때문에 B와 G 채널 값이 모두 중간 이상 분포하는 영역에 픽셀이 집중되는 것을 볼 수 있다.

- 나무나 식물은 G 값이 높고 약간의 R 성분도 포함하는 경우가 있다. 이 이미지에서도 다소 불그스럼한 초목이 담겨 있는 것을 볼 수 있다. 

img = cv2.imread('img1.jpg') # BGR
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # HSV

plt.figure(figsize=(12, 4)) 
plt.subplot(1, 3, 1)
hist = cv2.calcHist([hsv],[0,1],None,[180,256],[0,180,0,256])
p = plt.imshow(hist)                                            
plt.title('H and S') # 색조 & 채도                                       
plt.colorbar(p)                                                


plt.subplot(1, 3, 2)
hist = cv2.calcHist([hsv],[1,2],None,[256,256],[0,256,0,256])
p = plt.imshow(hist)
plt.title('S and V') # 채도 & 명도
plt.colorbar(p)

plt.subplot(1, 3, 3)
hist = cv2.calcHist([hsv],[0,2],None,[180,256],[0,180,0,256])
p = plt.imshow(hist)
plt.title('H and V') # 색조 & 명도
plt.colorbar(p)

plt.subplots_adjust(wspace=0.5)
plt.show()

- H and S의 x축은 채도(S), y축은 색조(H) 값을 나타낸다. y축을 보면 100 근처에 값이 모여 있는 것을 볼 수 있다. 

- 그리고  S and V의 x축은 명도(V), y축은 채도(S) 값을 나타낸다. 하늘과 호수(물), 초목은 대체로 밝고 어느 정도 채도가 있는 편이기 때문에 중간 정도의 S와 높은 V 값을 갖는 픽셀들이 모여 있는 것을 볼 수 있다.

- H값이 100이면 하늘색이다. 그러므로 이 이미지는 하늘색이 많이 분포되어 있고 다소 순수한 색상으로 구성되어 있으며 밝게 표현된 부분이 이미지에서 빈도 높게 등장함을 2D 히스토그램을 알 수 있다.

cf) 특정 구간의 분포를 더 명확하게 확인하고 싶으면 히스토그램의 계급 수(bin)를 줄이면 된다. 예를 들어 cv2.calcHist([img], [0,1], None, [32,32], [0,256,0,256])은 계급 수를 32개로 설정한 코드이다. 총 256개의 값을 가지므로 간격은 256 / 32 = 8이 된다.

'OpenCV' 카테고리의 다른 글

기하학적 변환 (2)  (0) 2024.12.16
기하학적 변환 (1)  (0) 2024.12.16
이미지 프로세싱 (2)  (0) 2024.12.14
이미지 프로세싱 (1)  (0) 2024.12.13
NumPy와 Matplotlib  (0) 2024.12.13