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)))
우선 위의 코드는 아래의 항목을 수행한다.
실제로 그러한지 결과부터 확인
다시 코드 확인
cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
Otsu의 이진화를 적용하여 이진화된 이미지를 얻으며,임계값은 자동으로 계산
cv2.findContours(th2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
이진화된 이미지에서 윤곽선을 찾고,
cv2.RETR_LIST는 모든 윤곽선을 검출하고, cv2.CHAIN_APPROX_NONE은 윤곽선을 구성하는 모든 점을 반환
cv2.drawContours(image, contours, -1, (0,255,0), thickness=2)
-1은 모든 윤곽선을 그리라는 의미이며, (0,255,0)은 윤곽선의 색상을 지정
thickness는 윤곽선의 두께를 지정
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)))
우선 위 두개 코드와 다른점이다.
Canny 엣지 검출 사용:
이 코드에서는 먼저 이미지를 그레이스케일로 변환한 후 Canny 엣지 검출을 사용하여 엣지를 찾는다.
이전 코드들에서는 그레이스케일 변환 후에 이진화를 적용하거나 원본 이미지를 사용하여 윤곽선을 검출했다.
엣지 검출 이후 윤곽선 검출:
엣지 검출을 통해 찾은 엣지 이미지에서 윤곽선을 검출
이전 코드들에서는 이미지를 그레이스케일로 변환하거나 이진화한 후에 바로 윤곽선을 검출했다.
결과를 보면 윤곽의 갯수가 77개로 기존의 방식보다 노이즈가 많다.
밑에 초록색 칠을 한 이미지를 봐도 밑에 기존에 없던 선들이나 점들이 많다.
만약 소음이 심한 윤곽을 제거하려면 블러링을 하는것이 좋다.
위에서 계속 사용한 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]]]
이런식으로 데이터가 구성돼있다는 것만 알아두자.
ㄴ RETR_LIST
극한 외부 플래그만 반환합니다. 모든 자식 윤곽선은 남겨집니다
ㄴ 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]]]
ㄴ RETR_LIST
ㄴ RETR_EXTERNAL
ㄴ 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]]]
ㄴ RETR_LIST
ㄴ RETR_EXTERNAL
ㄴ RETR_CCOMP
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]]]
정리하자면
모든 윤곽선을 검색하지만 모두 동일한 계층 수준에 속합니다.
따라서 hierarchy 배열에서 모든 윤곽선의 부모 및 자식 관계는 -1로 표시됩니다.
극한 외부 플래그만 반환합니다. 즉, 가장 바깥쪽 윤곽선만 반환됩니다.
따라서 이미지의 외부 윤곽선만 검출되며, 내부 윤곽선은 무시됩니다.
hierarchy 배열에서 외부 윤곽선은 자식을 가지지 않으므로, 세 번째 항과 네 번째 항은 -1로 표시됩니다.
모든 윤곽선을 검색하고 계층 구조를 2단계로 구성합니다.
외부 윤곽선은 hierarchy 배열에서 첫 번째 단계로 분류되고, 내부 윤곽선은 두 번째 단계로 분류됩니다.
따라서 hierarchy 배열에서 외부 윤곽선의 자식은 해당 내부 윤곽선을 나타내며, 이에 따라 세 번째 항이 해당 내부 윤곽선의 인덱스로 표시됩니다.
모든 윤곽선을 검색하고 계층 구조를 트리 구조로 구성합니다.
각 윤곽선의 부모와 자식 관계를 나타내며, 계층 구조를 자세히 파악할 수 있습니다.
hierarchy 배열에서 각 윤곽선의 부모, 자식, 이전, 다음 등고선에 대한 정보를 포함합니다.
윤곽에 대해서 알아봤는데 다음 글에는 이렇게 확인한 윤곽선을 정렬하고 근사화하고 일치시키는 공부를 하면서 이해 하겠다.