본문 바로가기

OpenCV

이미지 프로세싱 (1)

1. 관심 영역(Region Of Interest, ROI)

■ 관심 영역(ROI)은 이미지에서 분석 대상으로 지정한 특정 부분을 의미한다. 전체 이미지를 처리하는 대신 관심 있는 부분만 잘라내 처리함으로써 연산 효율성을 높일 수 있다.

■ cv2.imread( )로 이미지를 읽으면 이미지 데이터를 넘파이 배열로 반환하기 때문에 슬라이싱을 이용하여 관심 영역만 잘라낼 수 있다.

img[y:y+h, x:x+w]

y, x는 관심 영역 시작점, h, w는 영역의 폭

img[y:y+h, x:x+w], 이렇게 데이터의 양을 줄이면 이미지 형태가 단순화되어 알고리즘 적용과 좌표 계산이 용이해진다.

import cv2
from matplotlib import pyplot as plt

img = cv2.imread('img1.jpg')
print(img.shape)
```#결과#```
(1081, 1440, 3) # 원본 이미지 데이터 크기
````````````

x, y, w, h = 590, 550, 180, 180 # roi 좌표
roi = img[y:y+h, x:x+w] # roi 지정
print(roi.shape)
```#결과#```
(180, 180, 3) # roi 크기
````````````

cv2.rectangle(roi, (0,0), (h-1, w-1), (0,0,255), 10) # roi 전체에 빨간색 사각형 그리기
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) # BGR to RGB
plt.show()

- cv2.rectangle(roi, (0,0), (h-1, w-1), (0,0,255), 10)는 roi 이미지의 (0, 0)부터 (h-1, w-1)까지 빨간색으로 사각형을 표시하는 코드이다.

- 이때 (0, 0)은 왼쪽 위 모서리 좌표, (h-1, w-1)은 오른쪽 아래 모서리 좌표이므로 이 코드는 roi 이미지의 좌측 상단부터 우측 하단까지를 꼭짓점으로 갖는 빨간색 사각형을 표시한다.

 

plt.imshow(cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)) # roi만 따로 출력
plt.show()

■ 위와 같은 방법은 단번에 정확한  x, y, w, h값을 지정해 관심 영역을 잘라내기는 어렵다. 이러한 경우 OpenCV의 cv2.selectROI( ) 함수를 사용하면 마우스를 이용해서 더 수월하게 roi를 지정할 수 있다.

■ cv2.selectROI(win_name, img, showCrossHair=True, fromCenter=False)의 인수를 보면

- win_name은 roi 이미지를 표시할 창의 이름

- img는 원본 이미지,

- showCrossHair는 선택 영역 중심에 십자선 표시 여부(기본값: True),

- fromCenter는 마우스 시작 지점을 영역의 중심으로 지정 여부(기본값: False)

■ 다음과 같이 코드를 작성하면, 이 함수를 사용하면 먼저 원본 이미지가 창에 표시되며, 마우스로 드래그해서 관심 영역을 선택할 수 있다. 선택이 완료되면 roi 이미지만 jpg 파일로 저장한다.

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

x,y,w,h = cv2.selectROI('img', img, False)
if w and h:
    roi = img[y:y+h, x:x+w]
    cv2.imshow('cropped', roi)  # roi를 cropped라는 이름을 가진 창에 표시
    cv2.moveWindow('cropped', 0, 0) # roi 창을 화면 좌측 상단에 이동
    cv2.imwrite('./cropped2.jpg', roi)   # roi만 jpg 파일로 저장

cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

■ 로컬 환경에서 실행할 경우, 다음과 같은 문구와 함께 원본 이미지가 출력된다.

■ 원본 이미지에서 관심 영역만 드래그하면

roi만 표시되는 창이 (0, 0) 위치(좌측 상단)에 표시된다.

 

2. 컬러 스페이스(Color space)

2.1 RGB, BGR, RGBA

■ 색상을 표현하는 방법 중 RGB는 빨강, 초록 ,파랑 세 가지 색의 빛을 조합하여 원하는 색을 만드는 방식이다. 각 색상은 0~255 사이의 값을 가지며, 값이 커질수록 색상의 빛이 밝아진다.

- 예를 들어 RGB = (0, 0, 0)은 검은색, RGB = (255, 255, 255)는 흰색

■ OpenCV에서 사용하는 색상은 RGB의 반대 순서인 BGR을 사용한다. 

- 예를 들어 RGB에서 빨강은 RGB = (255, 0, 0)이지만, BGR에서 빨강은 (0, 0, 255), 

- RGB에서 파랑은 RGB = (0, 0, 255), BGR에서는 (255, 0, 0)

■ RGBA는 RGB에 투명도를 의미하는 \( \alpha \)가 추가된 색상 표현 방법이다. \( \alpha \)도 R, G, B처럼 0~255 사이의 값을 가질 수 있지만, 배경의 투명도를 표현하기 위해 0과 255만 사용하는 경우가 많다.

-  \( \alpha \)값이 0이면 검은색, 255면 흰색이다.

■ OpenCV에서는 알파 채널을 가지고 있는 경우 BGRA 방식으로 이미지를 읽는다. 

# 알파 채널이 있는 이미지 사용
img = cv2.imread('114293632.png') # BGR
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # RGB
bgr = cv2.imread('114293632.png', cv2.IMREAD_COLOR) # BGR
bgra = cv2.imread('114293632.png', cv2.IMREAD_UNCHANGED) # 투명도까지 포함해 이미지 읽음 # BGRA

print('rgb', rgb.shape, 'bgr', bgr.shape, 'bgra', bgra.shape)
```#결과#```
rgb (101, 120, 3) bgr (101, 120, 3) bgra (101, 120, 4)
````````````

- RGB, BGR은 빨간색, 초록색, 파란색 세 가지 채널을 가지므로 채널 수가 3이지만, BGRA에서는 알파 채널을 추가로 가지고 있으므로 채널 수가 4인 것을 확인할 수 있다.

cv2.imshow('rgb', rgb)
cv2.imshow('bgr', bgr)
cv2.imshow('bgra', bgra)
cv2.imshow('alpha', bgra[:,:,3])  # 알파 채널만
cv2.waitKey(0)
cv2.destroyAllWindows()

- BGRA의 경우 채널 수가 4개이므로 B는 배열의 0번 축, G는 1번 축, R은 2번 축,  A는 3번 축에 담겨 있다.

- 다음 그림과 같이 알파 채널만 사용할 경우 전경의 \( \alpha \)값은 255(흰색), 배경의 \( \alpha \)값은 0(검은색)을 가지기 때문에 전경과 배경을 쉽게 구분할 수 있다. 이런 이유로 알파 채널은 마스크 채널(mask channel)이라고도 부른다.

순서대로 RGB, BGR, BGRA, 알파 채널만

 

2.2 컬러 스페이스 변환

2.2.1 HSV 방식

■ HSV는 RGB와 마찬가지로 3개의 채널을 갖는 색상 표현법이다. 여기서 3개의 채널은 H(Hue, 색조), S(Saturation, 채도), V(Value, 명도)이며, 같은 색상이어도 RGB로 표현한 것을 HSV 표현으로 바꿀 수 있다. 

- 같은 색이어도 보는 관점에 따라 달라진다. 색상으로 보면 RGB이지만, 동일한 색을 색조 & 채도 & 명도로 볼 수 있다.

- 색조(H)는 이미지가 어떤 색상인지를 나타내며,

- 채도(S)는 이미지의 색상이 얼마나 순수하게 포함되어 있는지 나타내며 바깥으로 갈수록 채도가 높아지고 안쪽으로 갈수록 채도가 낮아진다. 그러므로 채도가 없다면 흰색이 된다.

- 명도(V)는 색상이 얼마나 밝은지 어두운지 밝기 정도를 나타낸다.

HSV 색 공간 [출처] https://ko.wikipedia.org/wiki/HSV_%EC%83%89_%EA%B3%B5%EA%B0%84#:~:text=HSV%20%EC%83%89%20%EA%B3%B5%EA%B0%84%20%EB%98%90%EB%8A%94%20HSV,%ED%8A%B9%EC%A0%95%ED%95%9C%20%EC%83%89%EC%9D%84%20%EC%A7%80%EC%A0%95%ED%95%9C%EB%8B%A4.

R, G, B는 각각 0~255 사이의 값. 즉, \( 2^8 = 256 \)개의 값을 가지므로 R은 8bit, G도 8bit, B도 8bit로 색상 표현이 처리되며 H, S, V도 마찬가지로 각각 8bit씩 색상을 담을 수 있다. 

하지만, 색조(H)의 단위는 각도 \( 0^\circ \sim 360^\circ \)이다. 256개의 값을 \( 1^\circ \)씩 보면 \( 256^\circ \leq 360^\circ \)이므로 색조(H)에 \( 0^\circ \sim 360^\circ \)에 있는 값을 모두 담을 수 없다.

■ 이 문제를 해결하기 위해 \( 0^\circ \sim 360^\circ \)에 2를 나눈 \( 0^\circ \sim 180^\circ \)를 색조(H) 단위로 사용한다.

- 예를 들어 색조(H)가 50으로 표현되었다면, 색조 100이 저장된 것으로 봐야 한다.

OpenCV에서 cv2.cvtColor() 함수에 두 번째 파라미터로 cv2.COLOR_BGR2HSV를 지정하면 BGR 방식을 HSV 방식으로 변환한다.

img = cv2.imread('114293632.png')
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # BGR to HSV

cv2.imshow('BGR',img)
cv2.imshow('HSB',hsv)
cv2.waitKey(0)
cv2.destroyAllWindows()

BGR, HSV

## BGR 컬러 스페이스로 원색 픽셀 생성
blue_bgr = np.array([[[255,0,0]]], dtype=np.uint8)  
green_bgr = np.array([[[0,255,0]]], dtype=np.uint8) 
red_bgr = np.array([[[0,0,255]]], dtype=np.uint8)  
yellow_bgr = np.array([[[0,255,255]]], dtype=np.uint8) # 노랑 값만 갖는 픽셀

## BGR 컬러 스페이스를 HSV 컬러 스페이스로 변환
red_hsv = cv2.cvtColor(red_bgr, cv2.COLOR_BGR2HSV);
green_hsv = cv2.cvtColor(green_bgr, cv2.COLOR_BGR2HSV);
blue_hsv = cv2.cvtColor(blue_bgr, cv2.COLOR_BGR2HSV);
yellow_hsv = cv2.cvtColor(yellow_bgr, cv2.COLOR_BGR2HSV);

print(f'Blue: BGR {blue_bgr} \t--->\t HSV {blue_hsv}')
print(f'Green: BGR {green_bgr} \t--->\t HSV {green_hsv}')
print(f'Red: BGR {red_bgr} \t--->\t HSV {red_hsv}')
print(f'Yellow: BGR {yellow_bgr} \t--->\t HSV {yellow_hsv}')
```#결과#```
Blue: BGR [[[255   0   0]]] 	--->	 HSV [[[120 255 255]]]
Green: BGR [[[  0 255   0]]] 	--->	 HSV [[[ 60 255 255]]]
Red: BGR [[[  0   0 255]]] 	--->	 HSV [[[  0 255 255]]]
Yellow: BGR [[[  0 255 255]]] 	--->	 HSV [[[ 30 255 255]]]
````````````

■ HSV의 장점은 RGB의 경우 색상을 알고 싶으면 R, G, B 세 가지 채널의 값을 모두 알아야 하지만 HSV는 색조(H) 값 하나만 알면 색상을 알 수 있다.

- 예를 들어, 결과를 보면 파란색 BGR = (255, 0 ,0)은 HSV = (120, 255, 255)로 변환된 것을 확인할 수 있는데, 여기서 HSV의 색조(H)에는 120˚가 저장된 것을 확인할 수 있다. 여기에 2를 곱하면 색조는 240˚이며 이는 파란색을 의미한다. 또한, 채도와 명도가 255이기 때문에 다른 색이 섞이지 않은 순수한 파란색이다.

- 초록색에 대한 HSV도 저장된 60˚에 2를 곱하면 색조는 120 ˚이며 이는 초록색을, 빨간색도 0˚에 2를 곱하면 색조는 0 ˚이며 이는 빨간색을, 노란색도 30˚에 2를 곱하면 색조는 60 ˚이며 이는 노란색을 나타낸다.

 

2.2.2 YUV(=YCbCr) 방식

■ YUV 방식은 YCbCr 방식이라고도 하며, 여기서 Y는 밝기(Luma), U는 밝기와 파란색과의 색상 차(Chroma Blue, Cb), V는 밝기와 빨간색과의 색상 차(Chroma Red, Cr)이다.

cf) YUV는 보통 4:2:2 형식으로 Y에는 많은 비트, U와 V에는 적은 비트를 할당하여 RGB 4:4:4 형식보다 데이터를 약 30% 압축하면서 RGB보다 영상의 화질 저하를 최소로 유지할 수 있다고 알려져 있다.

img = cv2.imread('114293632.png')
yuv = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) # BGR to YUV

cv2.imshow('BGR',img)
cv2.imshow('YUV',yuv)

cv2.waitKey(0)
cv2.destroyAllWindows()

BGR, YUV

## BGR
dark = np.array([[[0,0,0]]], dtype=np.uint8) # 3 채널 모두 0 == 가장 어두운 픽셀
middle = np.array([[[127,127,127]]], dtype=np.uint8) # 3 채널 모두 127 == 중간 밝기 픽셀
bright = np.array([[[255,255,255]]], dtype=np.uint8) # 3 채널 모두 255 == 가장 밝은 픽셀

## BGR to YUV
dark_yuv = cv2.cvtColor(dark, cv2.COLOR_BGR2YUV)
middle_yuv = cv2.cvtColor(middle, cv2.COLOR_BGR2YUV)
bright_yuv = cv2.cvtColor(bright, cv2.COLOR_BGR2YUV)

print(f'Dark: BGR {dark} \t--->\t YUV {dark_yuv}')
print(f'Middle: BGR {middle} \t--->\t YUV {middle_yuv}')
print(f'Bright: BGR {bright} \t--->\t YUV {bright_yuv}')
```#결과#```
Dark: BGR [[[0 0 0]]] 	--->	 YUV [[[  0 128 128]]]
Middle: BGR [[[127 127 127]]] 	--->	 YUV [[[127 128 128]]]
Bright: BGR [[[255 255 255]]] 	--->	 YUV [[[255 128 128]]]
````````````

- BGR에서 YUV로 변환하면, '가장 어두운 \( \rightarrow \) 중간 밝기 \( \rightarrow \) 가장 밝은'으로 갈수록 U와 V 값은 동일한 값으로 고정이고 YUV의 첫 번째 값 Y(밝기)만 0 \( \rightarrow \) 127 \( \rightarrow \) 255로 '가장 어두운 \( \rightarrow \) 중간 밝기 \( \rightarrow \) 가장 밝은'을 나타내는 것을 확인할 수 있다.

■ 정리하자면, OpenCV에서 색상을 표현하는 방식으로 ① BGR, ② BGRA ③ HSV, ④ YUV 방식이 있으며 

- ① OpenCV에서 기본값으로 사용하는 BGR은 RGB와 색상 순서가 반대인 표현법이고,

- ② BGR에 투명도를 나타내는 \( \alpha \) 채널이 추가된 색상 표현법이 BGRA

- ③ 이미지를 R, G, B 세 가지 색상이 아닌  색조, 채도, 명도 관점에서 고려할 때(더 정확히는 색조에 중점을 둘 때) 사용하는 색상 표현법은 HSV 

- ④ 색보다는 밝기에 중점을 둔 표현법은 YUV이다.

■ OpenCV에서 cv2.cvtColor(img, flag)함수를 통해 각 표현법으로 변환할 수 있다.

- img는 넘파이 배열로, 변환할 이미지

- flag에는 변환할 컬러 스페이스를 지정한다. 

   
flag 설명
cv2.COLOR_BGR2GRAY BGR 컬러 이미지를 그레이 스케일로 변환
cv2.COLOR_GRAY2BGR 그레이 스케일 이미지를 BGR 컬러 이미지로 변환
cv2.COLOR_BGR2RGB BGR 컬러 이미지를 RGB 컬러 이미지로 변환
cv2.COLOR_BGR2HSV BGR 컬러 이미지를 HSV 컬러 이미지로 변환
cv2.COLOR_HSV2BGR HSV 컬러 이미지를 BGR 컬러 이미지로 변환
cv2.COLOR_BGR2YUV BGR 컬러 이미지를 YUV 컬러 이미지로 변환
cv2.COLOR_YUV2BGR YUV 컬러 이미지를 BGR 컬러 이미지로 변환

 

3. 스레시홀딩(Thresholding)

■ 스레시홀딩은 바이너리 이미지(검은색과 흰색만으로 표현된 이미지)를 만드는 방법으로, 여러 값을 어떤 임계점을 기준으로 두 가지(0과 1 또는 검은색과 흰색)로 분류한다.

■ 스레시홀딩 방법으로 전역 스레시홀딩과 적응형 스레시홀딩이 있다.

3.1 전역 스레시홀딩

■ 전역 스레시홀딩은 하나의 임곗값으로 픽셀 값이 임곗값을 넘으면 255, 임곗값을 넘지 않으면 0으로 분류하는 방식이다. 

OpenCV에서는 cv2.threshold(img, threshold, value, type_flag) 함수를 이용해 전역 스레시홀딩을 수행할 수 있다.

- img는 변환할 이미지

- threshold는 스레시홀딩 임곗값

- value는 임곗값 기준에 만족하는 픽셀에 적용할 값 (예를 들어 threshold보다 큰 값은 value값으로 분류)

- type_flag는 스레시홀딩 적용 방법으로 다음과 같은 방법들이 있다.

type_flag 설명
cv2.THRESH_BINARY 픽셀 값이 threshold를 넘으면 value로 지정. threshold를 넘지 못하면 0으로 지정
cv2.THRESH_BINARY_INV cv2.THRESH_BINARY와 반대로 작동
cv2.THRESH_TRUNC 픽셀 값이 threshold를 넘으면 value로 지정. threshold를 넘지 못하면 원래 값 유지
cv2.THRESH_TOZERO 픽셀 값이 threshold를 넘으면 원래 값 유지. threshold를 넘지 못하면 0으로 지정
cv2.THRESH_TOZERO_INV cv2.THRESH_TOZERO와 반대로 작동

- cv2.threshold( )는 두 개의 결과를 반환한다. 하나는 스레시홀딩에 사용한 임곗값이고 나머지 하나는 스리세홀딩이 적용된 바이너리 이미지이다.

■ 예를 들어 검은색에서 흰색으로 변하는 이미지를 회색조로 읽은 다음, 픽셀 값이 임곗값 127보다 크면 255, 127보다 작거나 같으면 0으로 바꾸는 전역 스레시홀딩을 수행하면, 다음 그림과 같이 임곗값을 넘지 못하면 0(검은색), 넘으면 255(흰색)으로 픽셀 값이 분류되는 바이너리 이미지가 생성된다.

img = cv2.imread('67609.png', cv2.IMREAD_GRAYSCALE) # 그레이 스케일로 읽기

## 픽셀 값이 127(중간값)을 넘으면 255, 그렇지 않으면 0으로 분류
ret, threshold = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) 

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

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

3.1.1 오츠의 알고리즘

■ 위의 예시와 같이 픽셀 값이 0(검은색)과 255(흰색)으로 구성된 바이너리 이미지를 만들 때 가장 큰 영향을 미치는 것은 임곗값임을 알 수 있다. 그러므로 최적의 임곗값을 찾는 것이 중요하다.

■ 최적 임곗값을 자동으로 찾는 알고리즘으로 오츠 알고리즘이 있다.

오츠 알고리즘은 먼저 임곗값을 임의로 지정하여 픽셀 값을 이진 분류한 다음, 두 클래스의 명암 분포를 반복적으로 계산해서 모든 경우의 수 중에서 두 클래스의 명암 분포가 가장 균일할 때의 임곗값을 선택한다. 단, 모든 경우의 수를 확인하기 때문에 속도가 빠르지 않다는 단점이 있다.

 OpenCV에서 오츠의 알고리즘을 사용하려면 cv2.threshold( ) 함수에 cv2.THRESH_OTSU를 추가로 지정하면 된다.

오츠 알고리즘을 사용할 경우 알고리즘에서 최적 임곗값을 계산하기 때문에 cv2.threshold( ) 함수의 threshold 파라미터에임의의 임곗값을 설정해도 된다.

■ 예를 들어 다음 그림과 같이 선명하지 않은 글씨 이미지를 사용하여 임곗값을 임의로 지정했을 때와 오츠 알고리즘을 사용해 최적 임곗값을 지정했을 때 생성되는 바이너리 이미지들을 비교해 보자.

img = cv2.imread('213419.jpg', cv2.IMREAD_GRAYSCALE)
ret, threshold_127 = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # THRESH_BINARY
t, otsu = cv2.threshold(img, -1, 255,  cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 오츠 알고리즘 적용 # threshold에 임의의 값 -1을 지정

print('otsu threshold:', t)
```#결과#```
otsu threshold: 100.0
````````````

- 오츠 알고리즘을 적용할 때  threshold 파라미터에 임의의 값으로 -1을 지정했다. 이때의 threshold 값 -1은 무시되기 때문에 아무 값이나 넣어도 상관없다.

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

plt.subplot(1, 3, 2)
plt.title('threshold = 127')
plt.imshow(threshold_127, cmap = 'gray')
plt.xticks([]); plt.yticks([])

plt.subplot(1, 3, 3)
plt.title('otsu threshold = 100')
plt.imshow(otsu, cmap = 'gray')
plt.xticks([]); plt.yticks([])

- 선명하지 않은 원본 이미지에 임의로 임곗값 127을 설정해 바이너리 이미지로 변환한 경우, 선명해지지만 임곗값을 넘지 못하는 픽셀 값들이 0(검은색)으로 분류되면서 원본 이미지보다 글자를 명확히 구분하기 어렵다.

- 반면, 오츠 알고리즘을 사용해 명암 분포가 가장 균일할 때의 임곗값 100을 사용해 바이너리 이미지로 변환한 경우 임의로 임곗값을 설정했을 때 보다는 글자를 구분할 수 있을 정도로 선명해진 것을 확인할 수 있다.

 

3.2 적응형 스레시홀딩

■ 전역 스레시홀딩은 위의 예시처럼 원본 이미지가 받는 조명이 일정하거나 배경색이 여러 개가 아닌 경우에는 사용하기 적합하지만, 원본 이미지가 받는 조명이 일정하지 않거나 배경색이 여러 개인 경우, 하나의 임곗값으로 원본 이미지를 선명한 바이너리 이미지로 변환하기 어렵다.

■ 적응형(Adaptive) 스레시홀딩은 이런 문제를 해결하기 위해 이미지를 여러 영역으로 나눈 다음, 주변 픽셀 값만 활용하여 임곗값을 계산한다. 즉, 전역 스레시홀딩은 하나의 임곗값을 사용했다면 적응형 스레시홀딩은 여러 개의 임곗값을 사용한다.

 OpenCV에서는 cv2.adaptiveThreshold(img, value, method, type_flag, block_size, C) 함수를 이용해 적응형 스레시홀딩을 수행할 수 있다.

- img는 변환할 이미지

- value는 임곗값 기준에 만족하는 픽셀에 적용할 값

- method는 임곗값을 결정할 방법으로 다음과 같은 방법들이 있다.

method 설명
cv2.ADAPTIVE_THRESH_MEAN_C 이웃 픽셀의 평균으로 결정
cv2.ADAPTIVE_THRESH_GAUSSIAN_C 가우시안 분포에 따른 가중치의 합으로 결정

- type_flag는 스레시홀딩 적용 방법으로 cv2.threshod( )와 동일하다.

- block_size는 전체 이미지를 여러 영역으로 나눌 때, 각 영역의 크기(n x n)

- C는 계산된 임곗값 결과에서 차감할 값이다. 

■ 예를 들어 다음 그림과 같이 조명이 일정하지 않은(그림자로 인해 우측 상단으로 갈수록 더 어두워지는) 이미지를 사용하여 오츠 알고리즘을 사용했을 때와 적응형 스레시홀딩을 사용했을 때 생성되는 바이너리 이미지들을 비교해 보자.

img = cv2.imread('srcthreshold.jpg', cv2.IMREAD_GRAYSCALE)

block_size = 11 # 블럭 사이즈
C = 2 # 차감 상수

## 오츠 알고리즘 적용
th1, otsu = cv2.threshold(img, -1, 255,  cv2.THRESH_BINARY | cv2.THRESH_OTSU) 
print('otsu threshold:', th1)
```#결과#```
otsu threshold: 147.0
````````````

## 적응형 스레시홀딩 - method: 평균
th2 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, block_size, C)

## 적응형 스레시홀딩 - method: 가우시안 분포
th3 = cv2.adaptiveThreshold(img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, block_size, C)
titles = ['original', 'otsu threshold = 147', 'Adaptive Meang', 'Adaptive Gaussian']
images = [img, otsu, th2, th3]
for i in range(4):
    plt.subplot(2, 2, i+1)
    plt.imshow(images[i], cmap = 'gray')
    plt.title(titles[i])
    plt.xticks([]); plt.yticks([])
plt.show()

- 오츠 알고리즘을 통해 전역 스레시홀딩을 적용한 바이너리 이미지는 오른쪽 상단이 검은색으로 변해 이미지를 식별하기 어렵다.  

이는 원본 이미지의 오른쪽 상단이 다른 영역보다 더 그늘지고 어두워서 즉, 원본 이미지가 받은 조명이 일정하지 않기 때문이다. 이런 문제가 전역 스레시홀딩의 전형적인 문제점이다.

- 반면, 적응형 스레시홀딩을 적용한 경우 전형 스레시홀딩을 적용한 경우보다 상당히 선명한 것을 확인할 수 있다. 

- 또한, 전역 스레시홀딩을 적용한 경우 맨 아래 가운데 사각형의 윤곽선(edge)을 제대로 잡아내지 못하지만, 적응형 스레시홀딩을 적용한 경우 윤곽선을 잡아내는 것을 확인할 수 있다.

■ 이 예에서는 전체 이미지를 총 11개의 블록 영역으로 나눈 다음(이미지 11등분), 각 블록의 이웃 픽셀의 평균으로 임곗값을 결정하는 방법(cv2.ADAPTIVE_THRESH_MEAN_C)과 가우시안 분포에 따른 가중치의 합으로 임곗값을 결정하는 방법(cv2.ADAPTIVE_THRESH_GAUSSIAN_C)을 원본 이미지에 적용했는데 적용 결과, 

- 평균값을 이용한 방법이 가우시안 분포를 이용한 방법보다 이미지가 더 선명하지만, 노이즈가 조금 존재(검은색 점)하며, 

- 가우시안 분표를 이용한 방법은 평균값을 이용한 방법에 비해 선명도는 조금 떨어지지만 가우시안이 필터 역할을 하다 보니 노이즈가 더 적은 것을 볼 수 있다.

■ 이렇게 적응형 스레시홀딩이 전역 스레시홀딩보다 더 선명하고 부드러운 결과를 얻을 수 이유는 한 번의 스레시홀딩을 수행하는 전역 스레시홀딩과는 다르게 적응형 스레시홀딩은 이미지의 각 블록별로 스레시홀딩을 수행하기 때문이다.

■ 그러므로 이미지에 조명 차이가 있거나 윤곽선을 추출하는 것이 중요한 경우 적응형 스레시홀딩을 적용하는 것이 적합이다.

'OpenCV' 카테고리의 다른 글

기하학적 변환 (1)  (0) 2024.12.16
이미지 프로세싱 (3)  (0) 2024.12.15
이미지 프로세싱 (2)  (0) 2024.12.14
NumPy와 Matplotlib  (0) 2024.12.13
기본 입출력  (1) 2024.12.12