본문 바로가기

OpenCV

분할(segmentation) (1)

1. 컨투어(contour)

■ 컨투어는 일반적으로 등고선을 의미하며, 등고선은 지형의 높이를 하나의 선으로 표시하여 지형의 형태를 쉽게 인식할 수 있도록 해준다.

[출처] https://opencv-python.readthedocs.io/en/latest/doc/15.imageContours/imageContours.html

■ 이와 유사하게, 영상 처리에서는 객체의 외곽선(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 모든 외곽선을 검출, 모든 계층 정보를 트리 구조로 생성

[출처] https://charlezz.com/?p=45375

- 위의 그림과 같이 하나의 이미지에는 여러 개의 컨투어가 존재하고, 그 사이에는 서로 포함하는 관계가 존재한다. 이 관계를 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 인자보다 작으면 근사화를 종료한다.

[출처] https://velog.io/@jnary/CV-12.-%EB%A0%88%EC%9D%B4%EB%B8%94%EB%A7%81%EA%B3%BC-%EC%99%B8%EA%B3%BD%EC%84%A0-%EA%B2%80%EC%B6%9C

 

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는 휴 모멘트 비교 알고리즘

method 종류 [출처] https://docs.opencv.org/4.x/d3/dc0/group__imgproc__shape.html#gaf2b97a230b51856d09a2d934b78c015f

- 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()

[출처] https://datahacker.rs/006-opencv-projects-how-to-detect-contours-and-match-shapes-in-an-image-in-python/

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