Contours 1 - Drawing, Hierarchy and Modes

김성빈·2024년 5월 1일
0

Modern Computer Vision

목록 보기
14/117

What are Contours?
윤곽?

정의:
"윤곽선은 개체의 전체 경계를 묶거나 덮는 연속선 또는 곡선입니다."

# 간단한 이미지 번호판 이미지를 로드해 보겠습니다
image = cv2.imread('images/LP.jpg')
imshow('Input Image', image)

pcx508 이라 적혀있으며 우리가 물체의 경계선을 그리거나 곡선으로 그을때 우리가 보여주고 싶은 경계선이 P 정도 라고 가정했을때,

윤곽은 가장자리 주변에 그린 라인을 그리는것 이다.

라인을 그렸을때의 이점은 컴퓨터 시각 프로그램이 제어하는 데 아주 유용해진다.

하지만 요즘엔 딥 러닝으로 많은것을 해결하므로 많이 사용하지는 않다.

지금은 그 이전에는 어떻게 원식적인 물체를 감지했을까? 에 대한 공부이다.

Applying cv2.findContours()

cv2.findContours(image, Retrieval Mode, Approximation Method)

Retrieval Mode
RETR_LIST: 모든 컨투어를 검출하며 부모-자식 관계를 생성하지 않음. 부모와 자식은 동일한 수준으로 간주됨.
RETR_EXTERNAL: 가장 외곽의 컨투어만 반환. 모든 하위 컨투어는 무시됨.
RETR_CCOMP: 모든 컨투어를 검출하고, 2단계 계층 구조로 정렬함. 객체의 외곽 컨투어(경계)는 계층-1에 배치되고, 객체 내부의 구멍 컨투어(있는 경우)는 계층-2에 배치됨.
RETR_TREE: 모든 컨투어를 검출하고 완전한 계층 구조 목록을 생성함.
Approximation Method
CHAIN_APPROX_NONE: 선 위의 모든 점을 저장함(비효율적).
CHAIN_APPROX_SIMPLE: 각 선의 끝점만 저장함.

인자에 대해서 알아봤으니 직접 사용을 해보자.

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
imshow('After thresholding', th2)

# Finding Contours
# Use a copy of your image e.g. edged.copy(), since findContours alters the image
contours, hierarchy = cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

# Draw all contours, note this overwrites the input image (inplace operation)
# Use '-1' as the 3rd parameter to draw all
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
imshow('Contours overlaid on original image', image)

print("Number of Contours found = " + str(len(contours)))

우선 위의 코드는 아래의 항목을 수행한다.

  1. 이미지를 불러와서 회색조(grayscale)로 변환합니다.
  2. 회색조 이미지를 바탕으로 Otsu의 이진화(thresholding)를 적용하여 이미지를 이진화
  3. 이진화된 이미지에서 윤곽선(contours)을 찾는다.
  4. 찾은 윤곽선을 원본 이미지 위에 그린다.
  5. 찾은 윤곽선의 개수를 출력

실제로 그러한지 결과부터 확인

다시 코드 확인

  1. Otsu의 이진화 적용:
cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

Otsu의 이진화를 적용하여 이진화된 이미지를 얻으며,임계값은 자동으로 계산

  1. 윤곽선 찾기:
cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

이진화된 이미지에서 윤곽선을 찾고,
cv2.RETR_LIST는 모든 윤곽선을 검출하고, cv2.CHAIN_APPROX_NONE은 윤곽선을 구성하는 모든 점을 반환

  1. 윤곽선 그리기:
cv2.drawContours(image, contours, -1, (0,255,0), thickness=2)

-1은 모든 윤곽선을 그리라는 의미이며, (0,255,0)은 윤곽선의 색상을 지정
thickness는 윤곽선의 두께를 지정

  1. 윤곽선 개수 출력:
print("Number of Contours found = " + str(len(contours)))

이렇게 윤곽처리를 하니 물체 인식에 유용하며
글자를 하나씩 뺄수가 있다.

윤곽의 구성

그럼 이제 윤곽이 어떻게 구성돼 있는지도 확인하자.

윤곽을 찾을때에는
contours[0]을 사용해서 찾는다.

위에 len(contours)를 했을때 38이 나왔는데

이말은 contours[0] ~ contours[37] 까지 존재한다는 의미이며
숫자가 작을 수록 바깥쪽의 윤곽이고 가장 바깥쪽의 윤곽인 0을 출력해야 한다.

그렇다면 contours[0]의 결과는 어떻게 나올까?


위와 같이 가장 바깥 윤곽의 좌표를 알려준다.

위의 코드는 이진화된 이미지에서 윤곽선을 검출했고 밑의 코드는 그레이스케일 이미지에서 윤곽선을 검출 할 것이다.

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
imshow('After Grayscaling', gray)

# Finding Contours
contours, hierarchy = cv2.findContours(gray, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

# 모든 윤곽선을 그립니다. 입력 이미지를 덮어씁니다(제자리 작동)
# 세 번째 매개변수로 '-1'을 사용하여 모두 그리기
#cv2.그림 등고선(이미지, 등고선, -1, (0,255,0), 두께 = 2)
imshow('Contours overlaid on original image', image)

print("Number of Contours found = " + str(len(contours)))

결과를 보면 아래와 같다.

해당 코드는 위의 이진화 한 코드처럼 초록색 윤곽을 칠해주는것을 생략했다.

그런데 윤곽의 선을 보면 이진화한 것과 달리 한개만 있는것을 확인할 수 있는데,

그레이스케일 이미지의 경우, 이미지의 밝기 값에 따라 연속적인 윤곽선이 생길 수 있으며, 따라서 하나의 윤곽선으로 인식될 수 있다.

반면에 이진화된 이미지의 경우, 픽셀 값이 두 개의 값(예: 검정과 흰색)만 가지므로 더 분명한 윤곽선이 나타날 가능성이 높으므로

이진화된 이미지의 윤곽의 개수는 38개이며 그레이스케일한 이미지의 윤곽의 개수는 1개로 많은 차이가 있다.

Canny Edges instead of Thresholding

이진화와 그레이스케일을 한 이미지는 결과는 다르지만 threshold(임계값)을 주고 이미지 데이터를 처리하고 한것은 동일하다.

그렇다면 임계값 대신 Canny Edge를 적용해보자.

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Canny Edges
edged = cv2.Canny(gray, 30, 200)
imshow('Canny Edges', edged)

# Finding Contours
contours, hierarchy = cv2.findContours(edged, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

# Draw all contours, note this overwrites the input image (inplace operation)
# Use '-1' as the 3rd parameter to draw all
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
imshow('Contours overlaid on original image', image)

print("Number of Contours found = " + str(len(contours)))

우선 위 두개 코드와 다른점이다.

  1. Canny 엣지 검출 사용:
    이 코드에서는 먼저 이미지를 그레이스케일로 변환한 후 Canny 엣지 검출을 사용하여 엣지를 찾는다.
    이전 코드들에서는 그레이스케일 변환 후에 이진화를 적용하거나 원본 이미지를 사용하여 윤곽선을 검출했다.

  2. 엣지 검출 이후 윤곽선 검출:
    엣지 검출을 통해 찾은 엣지 이미지에서 윤곽선을 검출
    이전 코드들에서는 이미지를 그레이스케일로 변환하거나 이진화한 후에 바로 윤곽선을 검출했다.

결과를 보면 윤곽의 갯수가 77개로 기존의 방식보다 노이즈가 많다.

밑에 초록색 칠을 한 이미지를 봐도 밑에 기존에 없던 선들이나 점들이 많다.

만약 소음이 심한 윤곽을 제거하려면 블러링을 하는것이 좋다.

Retreival Modes

ㄴ RETR_LIST

위에서 계속 사용한 RETR_LIST이며
이 인자는 모든 윤곽선을 검색하지만 모두 동일한 계층 수준에 속한다.

ㄴ RETR_EXTERNAL
ㄴ RETR_CCOMP
ㄴ RETR_TREE

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

hierarchy

hierarchy는 윤곽선에 대한 정보를 나타내며 앞뒤 윤곽선들의 관계를 나타내고 인자에 대한 설명이다.

첫 번째 항은 다음 등고선의 인덱스입니다
두 번째 항은 이전 윤곽선의 인덱스입니다
세 번째 항은 부모 등고선의 인덱스입니다
네 번째 항은 자식 윤곽선의 인덱스입니다

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

contours, hierarchy = cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

print(hierarchy)	 
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [ 3  1 -1 -1]
  [ 4  2 -1 -1]
  [ 5  3 -1 -1]
  [ 6  4 -1 -1]
  [ 7  5 -1 -1]
  [ 8  6 -1 -1]
  [ 9  7 -1 -1]
  [10  8 -1 -1]
  [11  9 -1 -1]
  [12 10 -1 -1]
  [13 11 -1 -1]
  [14 12 -1 -1]
  [15 13 -1 -1]
  [16 14 -1 -1]
  [17 15 -1 -1]
  [18 16 -1 -1]
  [19 17 -1 -1]
  [20 18 -1 -1]
  [21 19 -1 -1]
  [22 20 -1 -1]
  [23 21 -1 -1]
  [24 22 -1 -1]
  [25 23 -1 -1]
  [26 24 -1 -1]
  [27 25 -1 -1]
  [28 26 -1 -1]
  [29 27 -1 -1]
  [30 28 -1 -1]
  [31 29 -1 -1]
  [32 30 -1 -1]
  [33 31 -1 -1]
  [34 32 -1 -1]
  [35 33 -1 -1]
  [36 34 -1 -1]
  [37 35 -1 -1]
  [-1 36 -1 -1]]]

이런식으로 데이터가 구성돼있다는 것만 알아두자.

Retreival Modes

ㄴ RETR_LIST

ㄴ RETR_EXTERNAL

극한 외부 플래그만 반환합니다. 모든 자식 윤곽선은 남겨집니다
ㄴ RETR_CCOMP
ㄴ RETR_TREE

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
imshow('After thresholding', th2)

# Use a copy of your image e.g. edged.copy(), since findContours alters the image
contours, hierarchy = cv2.findContours(th2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

# Draw all contours, note this overwrites the input image (inplace operation)
# Use '-1' as the 3rd parameter to draw all
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
imshow('Contours overlaid on original image', image, size = 10)

print("Number of Contours found = " + str(len(contours)))
print(hierarchy)
Number of Contours found = 16
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [ 3  1 -1 -1]
  [ 4  2 -1 -1]
  [ 5  3 -1 -1]
  [ 6  4 -1 -1]
  [ 7  5 -1 -1]
  [ 8  6 -1 -1]
  [ 9  7 -1 -1]
  [10  8 -1 -1]
  [11  9 -1 -1]
  [12 10 -1 -1]
  [13 11 -1 -1]
  [14 12 -1 -1]
  [15 13 -1 -1]
  [-1 14 -1 -1]]]

Retreival Modes

ㄴ RETR_LIST
ㄴ RETR_EXTERNAL

ㄴ RETR_CCOMP

ㄴ RETR_TREE

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
imshow('After thresholding', th2)

# Use a copy of your image e.g. edged.copy(), since findContours alters the image
contours, hierarchy = cv2.findContours(th2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)

# Draw all contours, note this overwrites the input image (inplace operation)
# Use '-1' as the 3rd parameter to draw all
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
imshow('Contours overlaid on original image', image)

print("Number of Contours found = " + str(len(contours)))
print(hierarchy)
Number of Contours found = 38
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [ 3  1 -1 -1]
  [ 4  2 -1 -1]
  [ 5  3 -1 -1]
  [ 6  4 -1 -1]
  [ 7  5 -1 -1]
  [ 8  6 -1 -1]
  [ 9  7 -1 -1]
  [10  8 -1 -1]
  [17  9 11 -1]
  [12 -1 -1 10]
  [13 11 -1 10]
  [14 12 -1 10]
  [15 13 -1 10]
  [16 14 -1 10]
  [-1 15 -1 10]
  [25 10 18 -1]
  [19 -1 -1 17]
  [20 18 -1 17]
  [21 19 -1 17]
  [22 20 -1 17]
  [23 21 -1 17]
  [24 22 -1 17]
  [-1 23 -1 17]
  [32 17 26 -1]
  [27 -1 -1 25]
  [28 26 -1 25]
  [29 27 -1 25]
  [30 28 -1 25]
  [31 29 -1 25]
  [-1 30 -1 25]
  [35 25 33 -1]
  [34 -1 -1 32]
  [-1 33 -1 32]
  [36 32 -1 -1]
  [-1 35 37 -1]
  [-1 -1 -1 36]]]

Retreival Modes

ㄴ RETR_LIST
ㄴ RETR_EXTERNAL
ㄴ RETR_CCOMP

ㄴ RETR_TREE

image = cv2.imread('images/LP.jpg')

# Convert to Grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

_, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
imshow('After thresholding', th2)

# Use a copy of your image e.g. edged.copy(), since findContours alters the image
contours, hierarchy = cv2.findContours(th2, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

# Draw all contours, note this overwrites the input image (inplace operation)
# Use '-1' as the 3rd parameter to draw all
cv2.drawContours(image, contours, -1, (0,255,0), thickness = 2)
imshow('Contours overlaid on original image', image)

print("Number of Contours found = " + str(len(contours)))
print(hierarchy)
Number of Contours found = 38
[[[ 1 -1 -1 -1]
  [ 2  0 -1 -1]
  [ 3  1 -1 -1]
  [ 4  2 -1 -1]
  [ 5  3 -1 -1]
  [ 6  4 -1 -1]
  [ 7  5 -1 -1]
  [ 8  6 -1 -1]
  [ 9  7 -1 -1]
  [10  8 -1 -1]
  [17  9 11 -1]
  [12 -1 -1 10]
  [13 11 -1 10]
  [14 12 -1 10]
  [15 13 -1 10]
  [16 14 -1 10]
  [-1 15 -1 10]
  [25 10 18 -1]
  [19 -1 -1 17]
  [20 18 -1 17]
  [21 19 -1 17]
  [22 20 -1 17]
  [23 21 -1 17]
  [24 22 -1 17]
  [-1 23 -1 17]
  [32 17 26 -1]
  [27 -1 -1 25]
  [28 26 -1 25]
  [29 27 -1 25]
  [30 28 -1 25]
  [31 29 -1 25]
  [-1 30 -1 25]
  [35 25 33 -1]
  [34 -1 -1 32]
  [-1 33 -1 32]
  [36 32 -1 -1]
  [-1 35 37 -1]
  [-1 -1 -1 36]]]

정리하자면

RETR_LIST:

모든 윤곽선을 검색하지만 모두 동일한 계층 수준에 속합니다.
따라서 hierarchy 배열에서 모든 윤곽선의 부모 및 자식 관계는 -1로 표시됩니다.

RETR_EXTERNAL:

극한 외부 플래그만 반환합니다. 즉, 가장 바깥쪽 윤곽선만 반환됩니다.
따라서 이미지의 외부 윤곽선만 검출되며, 내부 윤곽선은 무시됩니다.
hierarchy 배열에서 외부 윤곽선은 자식을 가지지 않으므로, 세 번째 항과 네 번째 항은 -1로 표시됩니다.

RETR_CCOMP:

모든 윤곽선을 검색하고 계층 구조를 2단계로 구성합니다.
외부 윤곽선은 hierarchy 배열에서 첫 번째 단계로 분류되고, 내부 윤곽선은 두 번째 단계로 분류됩니다.
따라서 hierarchy 배열에서 외부 윤곽선의 자식은 해당 내부 윤곽선을 나타내며, 이에 따라 세 번째 항이 해당 내부 윤곽선의 인덱스로 표시됩니다.

RETR_TREE:

모든 윤곽선을 검색하고 계층 구조를 트리 구조로 구성합니다.
각 윤곽선의 부모와 자식 관계를 나타내며, 계층 구조를 자세히 파악할 수 있습니다.
hierarchy 배열에서 각 윤곽선의 부모, 자식, 이전, 다음 등고선에 대한 정보를 포함합니다.

윤곽에 대해서 알아봤는데 다음 글에는 이렇게 확인한 윤곽선을 정렬하고 근사화하고 일치시키는 공부를 하면서 이해 하겠다.

profile
감사합니다. https://www.youtube.com/channel/UCxlkiu9_aWijoD7BannNM7w

0개의 댓글