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()
plt.subplot(1, 2, 1)
plt.title('before')
plt.plot(hist)
plt.subplot(1, 2, 2)
plt.title('normalize')
plt.plot(hist_norm)
■ 이미지 변화와 분포 변화를 보면, 특정 영역(100 ~ 200)에 몰려 있는 픽셀 분포를 최소-최대 정규화를 통해 분포를 늘려줌으로써 이미지가 더 선명해진 것을 확인할 수 있다.
- 정확히는 정규화 이전의 이미지의 픽셀 분포를 보면, 0(검은색)이나 255(흰색)에 픽셀이 부족한 것을 볼 수 있다. 그러므로 어두움과 밝음을 잘 표현할 수 없다.
- 반면, 정규화된 이미지의 픽셀 분포가 0에서 255까지의 전체 범위에 비교적 고르게 퍼져 있음을 확인할 수 있다. 이는 정규화 이전의 이미지보다 명암비가 높아져 화질이 개선된 결과라고 볼 수 있다.
3. 평탄화(Equalization)
■ 정규화는 위의 예시처럼 분포가 한 곳에 집중되어 있는 경우 분포를 늘려주므로 명암비 개선의 효과를 볼 수 있다.
■ 그러나 분포가 집중된 영역에서 멀리 떨어진 영역에 값이 존재할 경우 효과가 떨어진다. 이런 경우에 사용하는 것이 평탄화이다.
■ 평탄화를 사용하면 다음 그림처럼 특정 영역에 집중되어 있는 분포를 재분배해서 명암비를 개선할 수 있다.
■ 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()
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()
■ 이미지 변화와 분포 변화를 보면, 원본 이미지의 픽셀 분포는 특정 영역(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()
- 요트 부분의 밝기가 더 개선된 것을 볼 수 있다.
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()
- 단순히 전체 이미지에 평탄화를 적용한 경우 가운데 영역이 너무 밝아져 윤곽선을 알아볼 수 없다.
- 반면, 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 |