공돌이 공룡의 서재

[python openCV] 이미지 처리 - 임계 처리 (1): inRange, threshold 본문

코딩/opencv

[python openCV] 이미지 처리 - 임계 처리 (1): inRange, threshold

구름위의공룡 2020. 9. 2. 13:42

<들어가기 전>

 

출처: http://www.incheontoday.com/news/articleView.html?idxno=115623

오늘 갖고놀 이미지.

 

임계 처리란 임계값(threshold value. 경계가 되는 기준 값)을 기준으로 이미지를 이진화화는 것을 말한다. 이진화를 이진화를 했을 때 0과 255로 이루어진 흑백 이미지로 만들 수 있고 값이 2개만 있기 때문에 True False형태로 바꾸어서 다른 작업을 수행하는 것이 가능하다. 간단하게 구현하면 다음과 같다.

 

import cv2
import time

road = cv2.imread('./road.jpg')
gray = cv2.cvtColor(road, cv2.COLOR_BGR2GRAY)
height, width = gray.shape
th = 150

start = time.perf_counter()
for i in range(height):
    for j in range(width):
        if gray[i][j] > th:
            gray[i][j] = 255
        else:
            gray[i][j] = 0

finish = time.perf_counter()
print(finish - start)	# 0.343 s

cv2.imshow('after', gray)
cv2.waitKey()

 

이중 for문으로 직접 모든 픽셀값들에 대해 처리하는 것인데 0.343초 정도가 걸린다. 동영상 촬영을 할 때 1초당 30프레임을 찍는다 가정할 때 30프레임에는 9.2초가 걸린다는 의미로 실시간 이미지 처리에 쓸 수 없다. 

 

opencv에서는 이보다 훨씬 여러가지 threshold 함수를 제공한다.

 


<inRange>

inRange(src, lowerb, upperb, dst=None)

 

src: 처리할 이미지

lowerb: 하한값

upperb: 상한값

dst: output. 선언하지 않을 시 결과가 return 되므로 따로 변수를 정해주면 됨.

 

작동원리는 간단하게 도식화하면 다음과 같다.

lowerb 와 upperb 사이의 픽셀값들은 흰색, 그 외 영역은 검정색으로 처리된다.

 

import cv2
import time

road = cv2.imread('./road.jpg')
gray = cv2.cvtColor(road, cv2.COLOR_BGR2GRAY)
height, width = gray.shape
th = 150

start = time.perf_counter()

gray = cv2.inRange(gray, th, 255)

finish = time.perf_counter()
print(finish - start)

cv2.imshow('after', gray)
cv2.waitKey()

 

 

inRange(150, 255) 결과

 

inRange(50, 150) 결과


<threshold>

cv2.threshold(src, thresh, maxval, type, dst=None)

 

src: 처리할 이미지

thresh: 기준 임계값 (inRange의 lowerb와 같은 기능)

maxval: 최대 임계값 (inRange의 upperb와 같은 기능)

type: 임계 처리 방법을 설정

dst: output. 지정 안하면 결과 return

 

실행하면 ret, mat 2개의 값을 return한다. ret은 threshold에서 사용한 임계값을 의미하고, mat은 처리한 이미지 배열이다. 임계값을 thresh로 설정했는데 왜 굳이 return을 또 하지? 라는 질문이 생길 수 있다. ret에 대해서 type을 알아보고 다시 설명하겠다.

 

type의 종류가 많다. 하나하나 알아보자.

 

  • cv2.THRESH_BINARY:  임계값 이상이면 maxval, 이하면 0

  • cv2.THRESH_MASK:  흑색 이미지로 변경

  • cv2.THRESH_TOZERO:  임계값 이하면 0, 이상은 변화 X

  • cv2.THRESH_TRUNC:  임계값 이상이면 임계값, 이하는 원본값.

  • cv2.THRESH_BINARY_INV:  BINARY의 반대. 임계값 이하면 maxval, 이상이면 0

  • cv2.THRESH_TOZERO_INV: TOZERO의 반대. 임계값 이하면 변화 X 이상이면 0

  • cv2.THRESH_TRIANGLE: triangle 알고리즘으로 적절한 threshold를 찾으면서 임계 처리

  • cv2.THRESH_OTSU: otsu 알고리즘으로 적절한 threshold를 찾으면서 임계 처리

출처: docs.opencv.org

타입 종류에 따라서 결과 이미지가 완전히 달라질 수 있으므로 타입에 대해서는 제대로 알고 있는게 좋겠다. maxval이 필요한 타입은 BINARY와 BINARY_INV다. 나머진 아무런 값이나 지정해도 무관. 또한 _INV는 흑백이 반전된 이미지를 얻을 수 있다. 

 

여기서 자세히 설명하고 싶은 것은 otsu와 triangle인데 우선은 코드와 결과부터 살펴보고 가자.

 

import cv2
import time

road = cv2.imread('./road.jpg')
gray = cv2.cvtColor(road, cv2.COLOR_BGR2GRAY)
height, width = gray.shape
th = 150

flag = [cv2.THRESH_BINARY, cv2.THRESH_MASK, cv2.THRESH_TOZERO, cv2.THRESH_TRUNC,
        cv2.THRESH_BINARY_INV, cv2.THRESH_TOZERO_INV, cv2.THRESH_TRIANGLE, cv2.THRESH_OTSU]

for i in range(len(flag)):
    start = time.perf_counter()
    ret, result = cv2.threshold(gray, th, 255, flag[0])
    finish = time.perf_counter()
    print(finish - start)
    print(flag[i], 'threshold:", ret)
    cv2.imshow('after', result)
    cv2.waitKey()
    

 












THRESH_BINARY

THRESH_MASK

THRESH_TOZERO

THRESH_TRUNC

THRESH_BINARY_INV

THRESH_TOZERO_INV

THRESH_TRIANGLE

THRESH_OTSU

 

결과 출력에서 flag[i] 부분은 숫자로 뜨고, ret은 150이 계속 뜨다가 THRESH_OTSU와 THRESH_TRIANGLE에서 다른 값을 출력함을 알 수 있다. 

 

flag는 cv2의 __init__.py에서 확인하면 정수값으로 설정되어 있다. 특정 숫자가 들어오면 어떤 식으로 임계처리할지를 결정하는 것이다. 따라서 Flag에 해당하는 정수를 직접 넣어도 실행이 된다. 가령 cv2.THRESH_BINARY 대신에 0을 넣어도 같은 결과가 나온다. (물론 이 숫자를 외우고 있을 필요는 없다.) 더 쉽게 설명하자면 flag의 정수값이 음식점 메뉴 번호에 해당하고 메뉴 번호에 따라 나오는 메뉴가 달라지는 느낌이다.

 

OTSU와 TRIANGLE이 두 flag는 어떤 방식으로 처리하길래 넣어준 threshold와 다른 값이 나올까?

 

간단하게 설명하자면 입력해준 threshold값을 이용하지 않고 0부터 255까지의 픽셀값들의 수를 나타낸 pixel intensity histogram을 이용하는 통계적인 방식을 사용한다. 

 

이번 포스트에서는 수식을 이용한 자세한 이론보다는 대략적인 느낌만 설명해보겠다.

 


ostu algorithm

다음 코드를 실행해보자.

 

import matplotlib.pyplot as plt

hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
plt.plot(hist)
plt.show()

 

calcHist 결과: pixel intensity histogram

이 그래프는 어떤 픽셀들이 많이 분포되어 있는지를 나타낸 것이다. 80~90정도가 가장 많고 대략 100 ~ 200 사이에 값들이 많이 분포되어 있다. 산으로 생각한다면 큼지막한 봉우리가 3개 정도 있다고 볼 수 있을 것이다.

 

calcHist함수에 대해서는 이미지의 픽셀값들에 대한 빈도수를 히스토그램으로 나타낸다. 정도로 설명하고 공식 문서 링크로 대체하겠다.

https://docs.opencv.org/2.4/modules/imgproc/doc/histograms.html

 

여기서 봉우리를 2개로만 나누고 각각을 class라고 할 때

(class의 픽셀 수 / 전체 픽셀 값) * (다른 클래스의 픽셀 수 / 전체 픽셀 값) * (두 클래스의 평균 값의 차의 제곱)이 최대가 되는 값을 찾는다.

 

이 값이 otsu 알고리즘의 threshold가 된다. 실제 수식에서는 class1, class2로 구분하고, 왜 이런 식이 유도가 되었는지 자세하게 나와있다.

 

 

Triangle Algorithm

이 알고리즘은 그래프상에서의 peak를 이용한다.

 

방식은 다음과 같다.

그래프의 peak와 양끝 중에서 peak에서 더 먼 값 2개를 기준으로 선을 그어보자.

이 선에서 수선을 그래프에 긋는다고 했을 때 가장 거리가 먼 곳이 threshold 값이 된다.

(그림에서의 d가 사실은 d_max인 셈.)

 

otsu에 대한 원리 설명이나 코드 구현은 쉽게 찾을 수 있었으나 triangle은 원리 설명도 잘 나와있지 않았다.

 

 

기회가 되면 다음 포스트에서는 이 두 알고리즘에 대해 좀 더 깊이 알아보고 아니면 다른 thresholding을 알아보겠다.

Comments