1. 컨투어(contour)
■ 컨투어는 일반적으로 등고선을 의미하며, 등고선은 지형의 높이를 하나의 선으로 표시하여 지형의 형태를 쉽게 인식할 수 있도록 해준다.
■ 이와 유사하게, 영상 처리에서는 객체의 외곽선(contour)을 정의하는데, 이는 객체 영역의 픽셀 중 배경과 인접한 일련의 최외곽 픽셀을 따라 그려진 선을 의미한다.
■ 보통 배경은 검은색, 전경(객체 영역)은 흰색으로 구성된 바이너리 이미지에서 가장 외곽에 있는 픽셀들을 찾아 컨투어를 그리면 객체의 모양을 정확하게 인식할 수 있다.
■ OpenCV에서는 컨투어 함수 cv2.findContours(src, mode, method, contours, hierarchy, offset)를 제공한다.
- src는 입력, 바이너리 이미지
- mode는 외곽선(contour) 검출 방식
mode | 의미 |
cv2.RETR_EXTERNAL | 객체 영역의 가장 바깥쪽 외곽선만 검출, 계층 정보 x |
cv2.RETR_LIST | 모든 외곽선을 검출, 계층 정보 x |
cv2.RETR_CCOMP | 모든 외곽선을 검출, 2계층으로 계층 정보 생성 |
cv2.RETR_TREE | 모든 외곽선을 검출, 모든 계층 정보를 트리 구조로 생성 |
- 위의 그림과 같이 하나의 이미지에는 여러 개의 컨투어가 존재하고, 그 사이에는 서로 포함하는 관계가 존재한다. 이 관계를 Contours Hierarchy라고 한다.
- 이 외곽선의 계층 구조는 외곽선의 포함 관계에 의해 결정된다. 예를 들어 계층 정보를 트리 구조로 생성하는 cv2.RETR_TREE의 결과는 0번과 4번 외곽선 안에는 자식 외곽선이 있고, 0번과 4번 외곽선은 서로 포함 관계가 없이 대등하기 때문에 최상위 레벨의 노드는 0번과 4번이 된다.
- method는 외곽선 근사화 방법
method | 의미 |
cv2.CHAIN_APPROX_NONE | 근사없이 모든 컨투어 좌표 제공 |
cv2.CHAIN_APPROX_SIMPLE | 외곽선을 그릴 수 있는 꼭짓점 좌표만 제공 (예를 들어 사각형이면 4개 좌표) |
cv2.CHAIN_APPROX_TC89_L1 | Teh-Chin 알고리즘으로 L1버전을 적용하여 좌표 개수 축소 |
cv2.CHAIN_APPROX_TC89_KCOS | Teh-Chin 알고리즘으로 KCOS 버전을 적용하여 좌표 개수 축소 |
- cv2.findContours( ) 함수의 결과로 반환되는 것은 검출한 컨투어 좌표인 contours와 컨투어 계층 정보인 hierarchy
- cv2.findContours( ) 함수는 원본 이미지를 직접 수정하기 때문에, 원본 이미지를 재사용 혹은 보존하려면 원본 이미지를 복사해서 사용해야 한다.
■ cv2.findContours( ) 함수로 컨투어(외곽선)를 찾은 다음, OpenCV에서 제공하는 cv2.drawContours(img, contours, contourIdx , color, thickness) 함수로 외곽선을 그릴 수 있다.
- img는 입력 영상/이미지
- contours는 cv2.findContours( ) 함수로 찾은 전체 외곽선 정보
- contourIdx는 외곽선 인덱스 번호, -1을 지정하면 전체 외곽선 표시
- color는 외곽선 색상
- thickness는 외곽선 두께. 음수로 지정하면 외곽선 내부를 채움
img_0 = cv2.imread('cat1.jpg')
img = cv2.cvtColor(img_0, cv2.COLOR_BGR2GRAY) # 그레이스케일
ret, img_binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # 바이너리화
## 가장 바깥쪽 외곽선의 모든 좌표 반환
contour, hierarchy = cv2.findContours(img_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2:]
## 가장 바깥쪽 외곽선의 꼭지점 좌표만 반환
contour2, hierarchy2 = cv2.findContours(img_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
## 가장 바깥쪽 외곽선의 꼭지점 좌표만 반환
contour3, hierarchy3 = cv2.findContours(img_binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[-2:]
- [-2:]는 cv2.findContours( ) 함수 버전에 상관없이 contours, hierarchy 값을 반환하기 위함이다.
## 모든 외곽선 shape 출력
for i, c in enumerate(contour):
print(f"contour[{i}] shape: {c.shape}")
```#결과#```
contour[0] shape: (1, 1, 2)
contour[1] shape: (2, 1, 2)
contour[2] shape: (4, 1, 2)
contour[3] shape: (2, 1, 2)
contour[4] shape: (1, 1, 2)
contour[5] shape: (1, 1, 2)
contour[6] shape: (1, 1, 2)
contour[7] shape: (1083, 1, 2)
````````````
for i, c in enumerate(contour2):
print(f"contour2[{i}] shape: {c.shape}")
```#결과#```
contour2[0] shape: (1, 1, 2)
contour2[1] shape: (2, 1, 2)
contour2[2] shape: (2, 1, 2)
contour2[3] shape: (2, 1, 2)
contour2[4] shape: (1, 1, 2)
contour2[5] shape: (1, 1, 2)
contour2[6] shape: (1, 1, 2)
contour2[7] shape: (320, 1, 2)
````````````
for i, c in enumerate(contour3):
print(f"contour3[{i}] shape: {c.shape}")
```#결과#```
contour3[0] shape: (1, 1, 2)
contour3[1] shape: (2, 1, 2)
contour3[2] shape: (2, 1, 2)
contour3[3] shape: (2, 1, 2)
contour3[4] shape: (1, 1, 2)
contour3[5] shape: (1, 1, 2)
contour3[6] shape: (1, 1, 2)
contour3[7] shape: (320, 1, 2)
...
...
contour3[44] shape: (11, 1, 2)
contour3[45] shape: (8, 1, 2)
contour3[46] shape: (6, 1, 2)
contour3[47] shape: (8, 1, 2)
contour3[48] shape: (4, 1, 2)
````````````
print(f'총 도형 갯수 {len(contour)}, {len(contour2)}, {len(contour3)}')
```#결과#```
총 도형 갯수 8, 8, 49
````````````
img2 = img_binary.copy()
img3 = img_0.copy()
## 모든 좌표를 갖는 외곽선 그리기, 밝은 청록색, 두께는 4
contour_img1 = cv2.drawContours(img_0, contour, -1, (255,255,0), 4) # img_0은 컬러 이미지
## 꼭지점 좌표만을 갖는 외곽선 그리기, 초록색, 두께 4
contour_img2 = cv2.drawContours(img2, contour2, -1, (0,255,0), 4) # img2는 흑백 이미지
## 꼭지점 좌표만을 갖는 외곽선 그리기, 빨간색, 두께 4
contour_img3 = cv2.drawContours(img3, contour2, -1, (0,0,255), 4) # img0는 컬러 이미지
for i in contour3:
for j in i:
cv2.circle(contour_img3, tuple(j[0]), 1, (255,0,0), -1)
cv2.imshow('RETR_EXTERNAL_1', contour_img1)
cv2.imshow('RETR_EXTERNAL_2', contour_img2)
cv2.imshow('RETR_TREE', contour_img3)
cv2.waitKey(0)
cv2.destroyAllWindows()
- 첫 번째 이미지는 원본 이미지를 바이너리 이미지로 만든 다음, cv2.CHAIN_APPROX_NONE을 사용해서 모든 외곽선 좌표로 밝은 청록색, 두께 4인 외곽선을 그린 이미지이다.
- 두 번째와 세 번째 이미지도 원본 이미지를 바이너리 이미지로 만든 다음, cv2.CHAIN_APPROX_SIMPLE을 사용해서 외곽선의 꼭짓점 좌표 정보로 초록색, 두께 4인 외곽선을 그린 후, 꼭짓점 좌표를 파란색 점(원)으로 표시한 이미지이다. 단, 베이스가 흑백 이미지이므로 모든 정보는 흑백으로 표시된다.
- 세 번째 이미지는 빨간색, 두께 4인 외곽선을 기른 후, 꼭짓점 좌표를 파란색 점(원)으로 표시한 이미지이다.
## 계층 출력
print(len(contour), hierarchy.shape);print();print(hierarchy);print()
print(len(contour2), hierarchy2.shape);print();print(hierarchy2);print()
```#결과#```
8 (1, 8, 4)
[[[ 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]
[-1 6 -1 -1]]]
8 (1, 8, 4)
[[[ 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]
[-1 6 -1 -1]]]
````````````
- contour와 contou2의 계층 정보인 hierarchy와 hierarchy2는 cv2.RETR_EXTERNAL를 지정했을 때의 관계로
서로 포함 관계가 아닌 컨투어(외곽선)의 개수가 8개임을 확인할 수 있다.
- hierarchy의 요소 중 -1은 아무 관계가 없음을 의미한다.
- cv2.RETR_LIST나 cv2.RETR_EXTERNAL로 지정하면 선/후 관계인 next/prev 관계만 표현하고 부모/자식 관계는 표현하지 않는다.
- 첫 번째 두 번째 열이 선/후 관계, 세 번째와 네 번째 열이 부모/자식 관계를 나타내는데 부모/자식 관계는 모두 -1로 나타난 것을 확인할 수 있다.
print(len(contour3), hierarchy3.shape);print(hierarchy3)
```#결과#```
49 (1, 49, 4)
[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[ 5 3 -1 -1]
[ 6 4 -1 -1]
...
...
[44 42 -1 7]
[45 43 -1 7]
[46 44 -1 7]
[47 45 -1 7]
[48 46 -1 7]
[-1 47 -1 7]]]
````````````
- contour3과 hierarchy3은 cv2.RETR_TREE를 지정했을 때의 결과로 모든 경계에 외곽선을 그리며 모든 계층 정보를 트리 구조로 생성한다.
- cv2.RETR_TREE를 지정했을 때 hierarchy의 첫 번째 열은 다음(next) 노드, 두 번째 열은 이전(prev) 노드에 대한 정보이고, 세 번째 열은 자식 노드, 네 번째 열은 부모 노드에 대한 정보이다.
- 예를 들어 hierarchy3 결과의 2행 [2 0 -1 -1]은 인덱스 1을 갖는 외곽선에 대한 정보이며, 의미는 next가 2, prev가 0이므로 다음 도형은 2행[3 1 -1 -1], 이전 도형은 0행[1 -1 -1 -1]임을 나타낸다.
- 그리고 자식과 부모가 -1이므로 자식 도형과 부모 도형이 없음을 의미한다.
- 이렇게 컨투어(외곽선) 계층 정보를 통해 외곽 요소와 자식 요소를 확인할 수 있으며, 최외곽 컨투어만 골라내려면 부모가 -1인 행만 선택하면 된다.
1.1 외곽선 처리 함수
■ OpenCV에서는 외곽선(컨투어) 검출 후 외곽선의 좌표 정보를 이용해서 외곽선을 감싸는 도형을 그리는 함수들을 제공한다.
- (1) cv2.boundingRect(contour) 함수는 외곽선 좌표를 감싸는 최소한의 사각형(=바운딩 박스)을 계산한다.
- cv2.boundingRect( ) 함수에 외곽선 좌표(점)들의 집합을 입력으로 넣으면, 결과로 사각형의 왼쪽 상단 좌표와 사각형의 폭과 높이를 반환한다.
- (2) cv2.minAreaRect(contour) 함수는 외곽선 좌표를 감싸는 최소 크기의 회전된 사각형을 계산한다.
- 계산된 사각형을 cv2.boxPoints( ) 함수의 입력으로 넣으면 사각형으로부터 꼭짓점 좌표를 계산한다.
- cv2.boxPoints( ) 함수의 결과로 4개의 꼭짓점 좌표가 소수점으로 반환되기 때문에 정수로 변환할 필요가 있다.
- (3) cv2.minEnclosingCircle(contour) 함수는 외곽선 좌표를 감싸는 최소 크기의 원을 계산한다. 함수의 결과로 원점 좌표(x, y)와 반지름을 반환한다.
- (4) cv2.fitEllipse(points) 함수는 외곽선 좌표를 감싸는 최소 크기의 타원을 계산한다.
- (5) cv2.minEnclosingTriangle(points) 함수는 외곽선 좌표를 감싸는 최소 크기의 삼각형을 계산한다. 함수의 결과로 넓이와 삼각형 3개의 꼭짓점 좌표를 반환한다.
- (6) cv2.fitLine(points, distType, param, reps, aeps, line) 함수는 중심점을 통과하는 직선을 계산한다.
- distType은 거리 계산 방식이며 다음과 같은 방식들을 지정할 수 있다.
https://docs.opencv.org/4.x/d3/dc0/group__imgproc__shape.html#gaf849da1fdafa67ee84b1e9a23b93f91f
OpenCV: Structural Analysis and Shape Descriptors
lineOutput line parameters. In case of 2D fitting, it should be a vector of 4 elements (like Vec4f) - (vx, vy, x0, y0), where (vx, vy) is a normalized vector collinear to the line and (x0, y0) is a point on the line. In case of 3D fitting, it should be a v
docs.opencv.org
- param을 0으로 지정하면 최적 (거리)값을 계산한다.
- reps는 최적 직선을 찾기 위해 사용하는 반지름 정확도(원본 좌표와 선 사이의 거리), aeps는 직선의 방향을 결정할 때 사용하는 각도 정확도로 둘 다 0.01을 사용하는 것이 권장된다.
img = cv2.imread('lightning.png', 0)
ret, img_binary = cv2.threshold(img, 127,255,cv2.THRESH_BINARY_INV) # 바이너리 이미지
## 컨투어 검출
contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
contr = contours[0] # 컨투어 정보
print(type(contr), contr.shape, contr.dtype)
```#결과#```
<class 'numpy.ndarray'> (332, 1, 2) int32
````````````
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
## 사각형
x, y, w, h = cv2.boundingRect(contr)
cv2.rectangle(img, (x,y), (x+w, y+h), (0,0,0), 3) # 검정색, 두께 3
## 회전 사각형
rect = cv2.minAreaRect(contr)
box = cv2.boxPoints(rect) # 중심점과 각도를 4개의 꼭짓점 좌표로 변환
box = np.int0(box) # 정수로 변환
cv2.drawContours(img, [box], -1, (255,0,0), 3) # 컨투어 그리기, 파란색, 두께 3
## 원
(x,y), radius = cv2.minEnclosingCircle(contr)
cv2.circle(img, (int(x), int(y)), int(radius), (0,255,255), 2) # 노란색 원
## 타원
ellipse = cv2.fitEllipse(contr)
cv2.ellipse(img, ellipse, (0,255,0), 3) # 초록색
## 삼각형
ret, tri = cv2.minEnclosingTriangle(np.float32(contr))
cv2.polylines(img, [np.int32(tri)], True, (0,0,255), 2) # 빨간색
## 직선
vx,vy,x,y = cv2.fitLine(contr, cv2.DIST_L2,0,0.01,0.01)
cols,rows = img.shape[:2]
cv2.line(img, (0, int(0-x*(vy/vx) + y)), (cols-1, int((cols-x)*(vy/vx) + y)), (255,255,0),2) # 밝은 청록색
1.2 모멘트(Moments)
■ Image Moment는 대상을 구분할 수 있는 특징을 의미한다. OpenCV에서는 cv2.moments( ) 함수를 이용해 외곽선의 moment 특징을 추출할 수 있다.
contr = contours[0] # 외곽선을 형성하는 점들의 집합
moment = cv2.moments(contr) # 컨투어(외곽선) 특징 추출
print(moment)
```#결과#```
{'m00': 7052.5, 'm10': 1138236.8333333333, 'm01': 858127.0, 'm20': 190555664.75, 'm11': 145550316.875, 'm02': 115085133.91666666, 'm30': 32993224893.45,
'm21': 25551868318.6, 'm12': 20427610548.4, 'm03': 16739947881.800001, 'mu20': 6850157.65936628, 'mu11': 7053080.564779043, 'mu02': 10670820.108938903, 'mu30': 27371658.025600433,
'mu21': 88966780.20635104, 'mu12': 137072990.84260416, 'mu03': 139948107.57040405, 'nu20': 0.13772550625192867, 'nu11': 0.14180536269725763, 'nu02': 0.21454164629589423,
'nu30': 0.0065530482900619785, 'nu21': 0.021299535686083438, 'nu12': 0.03281664294558583, 'nu03': 0.03350497460380018}
````````````
참고) https://076923.github.io/posts/Python-opencv-25/
Python OpenCV 강좌 : 제 25강 - 모멘트
모멘트(Moments)
076923.github.io
## 외곽선의 중심점 (cx, cy)
cx = int(moment['m10']/moment['m00'])
cy = int(moment['m01']/moment['m00'])
- m10/m00과 m01/m00은 각각 \( x \) 축과 \( y \) 축에 대한 무게 중심을 의미한다.
- m00은 외곽선 영역의 넓이를 의미한다.
■ 외곽선을 형성하는 좌표(점)들의 집합을 이용해 cv2.arcLength(curve, closed) 함수로 외곽선(곡선)의 길이를, cv2.contourArea(contour, oriented = false) 함수로 외곽선이 감싸는 영역의 넓이(면적)을 계산할 수 있다.
- cv2.arcLength(InputArray curve, bool closed)에서 curve는 외곽선을 형성하는 점들의 집합, closed는 폐곡선 여부이다.
- closed를 True로 지정하면 곡선의 시작점과 끝점이 연결되어 있는 폐곡선의 길이를 계산하고, False로 지정하면 시작점과 끝점을 연결하지 않고 외곽선의 길이를 계산한다.
- cv2.contourArea(contour, oriented = false)에서 contour는 외곽선을 형성하는 점들의 집합, oriented는 False로 지정해야 면적의 절댓값을 반환한다.
## 외곽선이 감싸는 영역의 넓이(면적)
area = cv2.contourArea(contr)
area
```#결과#```
7052.5
````````````
## 외곽선(곡선)의 길이
perimeter1 = cv2.arcLength(contr,True)
perimeter2 = cv2.arcLength(contr,False)
perimeter1, perimeter2
```#결과#```
(597.6782723665237, 596.6782723665237)
````````````
1.3 외곽선 단순화
■ OpenCV에서는 외곽선을 근사화하는 cv2.approxPolyDP(contour, epsilon, closed) 함수를 제공한다. 이 함수를 이용해 외곽선의 형태를 단순화하여 작은 수의 좌표들로 구성된 외곽선을 생성한다.
- contour는 외곽선을 형성하는 점들의 집합
- epsilon은 근사화 정밀도 파라미터로 입력 곡선과 근사화된 곡선까지의 최대 거리를 지정한다. 최대거리가 클 수록 더 먼 곳의 좌표까지 고려하기 때문에 좌표 수가 줄어든다.
https://opencv-python.readthedocs.io/en/latest/doc/16.imageContourFeature/imageContourFeature.html
- closed는 폐곡선 여부. True로 지정하면 폐곡선, False로 지정하면 폐곡선이 아니다.
■ 이 함수를 사용하는 이유는 이미지에 노이즈가 포함되어 있는 경우 외곽선이 불필요하게 복잡해질 수 있기 때문이다. 노이즈를 포함해 외곽선을 정확히 그리는 것보다 간결하게 그리는 것이 더 적합하다.
img = cv2.imread('bad_rect.png')
img1 = img.copy()
img2 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 그레이스케일
ret, th = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY) # 바이너리
contours, hierachy = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:] # 컨투어 검출
contour = contours[0] # 외곽선을 형성하는 점들의 집합
contour.shape # 215개의 좌표
```#결과#```
(215, 1, 2)
````````````
## 엡실론 설정 - 적용하는 숫자가 커질수록 좌표 개수 감소
epsilon1 = 0.01 * cv2.arcLength(contour, True) # 1%
epsilon2 = 0.1 * cv2.arcLength(contour, True) # 10%
epsilon1, epsilon2
```#결과#```
(9.459310187101364, 94.59310187101364)
````````````
## 외곽선 근사화
approx1 = cv2.approxPolyDP(contour, epsilon1, True)
approx2 = cv2.approxPolyDP(contour, epsilon2, True)
## 각각 컨투어 선 그리기
cv2.drawContours(img, [contour], -1, (0,255,0), 3)
cv2.drawContours(img1, [approx1], -1, (0,255,0), 3)
cv2.drawContours(img2, [approx2], -1, (0,255,0), 3)
cv2.imshow('contour', img)
cv2.imshow('1%', img1)
cv2.imshow('10%', img2)
cv2.waitKey()
cv2.destroyAllWindows()
■ 위의 그림을 보면 epsilon 값이 커짐에 따라 외곽선이 단순한 사각형 형태로 바뀐 것을 볼 수 있다. 이는 cv2.approxPolyDP( ) 함수가 더글라스-포이커(Douglas-Peucker) 알고리즘 사용하여 곡선 또는 다각형을 단순화시키기 때문이다.
■ 더글라스-포이커 알고리즘은 다음 그림과 같이 먼저, 외곽선에서 가장 멀리 떨어진 두 점을 찾아 직선으로 연결하고, 이 직선에서 가장 멀리 떨어진 외곽선 위의 점을 찾아 연결한다. 이 작업을 반복하다가 새로 추가할 외곽선 위의 점과 근사화에 의한 직선과의 수직 거리가 epsilon 인자보다 작으면 근사화를 종료한다.
1.4 볼록 선체(Convex Hull)
■ 볼록 선체는 외곽선의 좌표를 모두 포함하는 볼록한 외곽선을 의미하며, 외곽선 근사화(또는 단순화)의 또 다른 형태이다. 객체 영역을 완전히 포함하는 외곽선을 찾는데 유용하다.
■ OpenCV에서는 볼록 선체를 계산하는 cv2.convexHull(points, hull, clockwise, returnPoints) 함수를 제공한다.
- points는 외곽선을 형성하는 점들의 집합
- clockwise에는 방향을 지정한다. True로 지정하면 시계 방향
- returnPoints에는 결과 좌표 형식을 지정한다. True로 지정하면 볼록 선체 좌표 변환, False로 지정하면 (입력) 실제 외곽선 중에 볼록 선체에 해당하는 인덱스를 반환한다.
img = cv2.imread('hand.jpg')
img1 = img.copy()
img2 = img.copy()
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, th = cv2.threshold(imgray, 127, 255, cv2.THRESH_BINARY_INV)
contours, heiarchy = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
contour = contours[0]
cv2.drawContours(img, [contour], -1, (0, 255,0), 1)
## 볼록 선체 적용
hull = cv2.convexHull(contour) # 볼록 선체를 좌표 기준으로 찾음
cv2.drawContours(img1, [hull], -1, (0,255,0), 1)
print(cv2.isContourConvex(contour), cv2.isContourConvex(hull))
```#결과#```
False True
````````````
- cv2.isContourConvex(contour) 함수는 볼록 선체 여부를 확인하는 함수로 반환되는 값이 True라면 볼록 선체임을 나타낸 것이다.
cv2.imshow('contour', img)
cv2.imshow('convex hull', img1)
cv2.waitKey(0)
cv2.destroyAllWindows()
■ 볼록 선체를 적용한 이미지를 보면 손의 형태에서 손가락 사이 움푹 들어간 부분이 있다. 이 부분은 볼록 선체와 외곽선 사이의 차이이며, 이를 결함이라한다.
■ 구체적으로 객체의 실제 외곽선이 볼록 선체에서 움푹 들어간 부분을 말한다. 다음 그림과 같이 화살표의 차이를 convexity defect라고 한다. convexity defect는 실제 외곽선과 볼록 선체의 최대 차이를 나타낸 것으로 볼 수 있다.
■ 결함은 cv2.convexHull( ) 함수의 returnPoints를 False로 지정해서 실제 외곽선 중에 볼록 선체에 해당하는 인덱스를 반환한 다음, OpenCV에서 제공하는 볼록 선체 결함 찾기 함수 cv2.convexityDefects(contour, convexhull)에 넣어 찾을 수 있다.
- contour는 외곽선을 형성하는 점들의 집합, convexhull는 볼록 선체에 해당하는 외곽선의 인덱스를 지정한다.
■ 결함을 찾는 이유는 객체의 외곽선과 볼록 선체 사이의 차이를 분석하여 움푹 들어간 부분을 식별하기 위함이다. ex) 손가락 제스처 인식
hull2 = cv2.convexHull(contour, returnPoints=False) # 볼록 선체를 인덱스 기준으로 찾음
## 볼록 선체 결함 찾기
defects = cv2.convexityDefects(contour, hull2)
defects.shape
```#결과#```
(19, 1, 4)
````````````
defects[0] # [starts, end, farthest, distance]
```#결과#```
array([[ 0, 2, 1, 162]], dtype=int32)
````````````
for i in range(defects.shape[0]): # 볼록 선체 결함 순회
startP, endP, farthestP, distance = defects[i, 0]
farthest = tuple(contour[farthestP][0])
dist = distance/256.0
if dist > 1 :
cv2.circle(img1, farthest, 3, (0,0,255), -1) # 빨강색 점 표시
cv2.circle(img2, farthest, 3, (0,0,255), -1) # 빨강색 점 표시
- cv2.convexityDefects( ) 함수의 결과인 defects 변수에는 볼록 선체 결함이 있는 외곽선의 인덱스가 배열로 저장된다.
- defects 변수에 담겨 있는 starts는 오목한 각이 시작되는 외곽선의 인덱스 ends는 오목한 각이 끝나는 외곽선의 인덱스, farthest는 볼록 선체에서 가장 먼 오목한 지점의 외곽선 인덱스, distance는 farthest와 볼록 선체의 거리를 의미한다.
- contour[farthestP]로 볼록 선체에서 가장 먼 오목한 지점의 외곽선 인덱스를 가져온다. 이는 손가락 이미지에 결함을 표시하기 위해서이다.
- distance / 256.0을 하는 이유는 cv2.convexityDefects( ) 함수가 결함의 거리를 8비트 정수값으로 반환하는데, 이 값은 실제 거리(픽셀 단위)가 아니라 스케일링된 값이다. 그러므로 distance 값을 원래 실제 거리(픽셀 단위)로 복원하기 위해 256으로 나눈다.
- dist > 1을 통해 결함만 추출한다.
cv2.imshow('convexity defect', img2)
cv2.imshow('convex hull & convexity defect', img1)
cv2.waitKey(0)
cv2.destroyAllWindows()
1.5 컨투어(외곽선)를 이용한 도형 매칭
■ 서로 다른 물체의 외곽선을 비교하면 두 물체의 형태가 얼마나 비슷한지 알 수 있다. OpenCV에서는 두 개의 외곽선으로 도형을 매칭하는 cv2.matchShapes(contour1, contour2, method, parameter) 함수를 제공한다.
- contour1, contour2는 비교할 두 개의 외곽선
- method는 휴 모멘트 비교 알고리즘
- parameter는 Method-specific 파라미터로 지금은 지원하지 않아서 0으로 고정한다.
- cv2.matchShapes( ) 함수의 결과로 두 도형의 닮음 정도가 출력되며, 두 도형이 다르다고 판단되면 큰 값을 반환하고 비슷하다고 판단되면 작은 값을 반환한다. 만약, 완전히 동일하다고 판단되면 0을 반환한다.
target = cv2.imread('stop_sign.jpg') # 타겟 이미지
shapes = cv2.imread('Signs.jpg') # 매칭할 이미지
plt.subplot(121),plt.imshow(target[:,:,::-1]),plt.title('stop_sign'),plt.xticks([]); plt.yticks([])
plt.subplot(122),plt.imshow(shapes[:,:,::-1]),plt.title('Signs'),plt.xticks([]); plt.yticks([])
plt.show()
targetGray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)
shapesGray = cv2.cvtColor(shapes, cv2.COLOR_BGR2GRAY)
ret, targetTh = cv2.threshold(targetGray, 127, 255, cv2.THRESH_BINARY_INV)
ret, shapesTh = cv2.threshold(shapesGray, 127, 255, cv2.THRESH_BINARY_INV)
## 외곽선만 필요하므로 계층 정보는 필요 없음
cntrs_target, _ = cv2.findContours(targetTh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
cntrs_shapes, _ = cv2.findContours(shapesTh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2:]
## 각 도형들과 매칭
matchs = [] # 매칭 점수를 보관할 리스트
for contr in cntrs_shapes:
match = cv2.matchShapes(cntrs_target[0], contr, cv2.CONTOURS_MATCH_I2, 0.0)
matchs.append((match, contr)) # 매칭 점수와 외곽선 쌍으로 저장
- 타겟을 여러 도형 중 하나와 매칭을 실행한다. 반복문을 통해 타겟과 매칭 이미지에 있는 모든 도형들의 매칭 점수가 계산된다.
matchs.sort(key=lambda x : x[0])
print(f'best score {match}')
```#결과#```
best score 0.0009485006830136644
````````````
- 가장 작은 매칭 점수가 0에 가까운 값을 받았다. 이는 도형 중에 사실상, 타겟과 완전히 닮은 도형이 존재하는 것으로 볼 수 있다.
## 가장 적은 매칭 점수를 얻은 도형의 외곽선에 노란색 표시
cv2.drawContours(shapes, [matchs[0][1]], -1, (0,255,255), 5)
cv2.imshow('target', target)
cv2.imshow('Match Shape', shapes)
cv2.waitKey()
cv2.destroyAllWindows()
'OpenCV' 카테고리의 다른 글
매칭(Matching) (1) (0) | 2024.12.23 |
---|---|
분할(segmentation) (2) (0) | 2024.12.22 |
모폴로지(Morphology) 연산, 이미지 피라미드(Image Pyramid) (0) | 2024.12.21 |
필터와 블러링 (0) | 2024.12.18 |
기하학적 변환 (2) (0) | 2024.12.16 |