printf("ho_tari\n");

ep.38 OpenCV4 본문

두산 로보틱스 부트캠프 ROKEY/Computer Vision 교육

ep.38 OpenCV4

호타리 2024. 8. 29. 16:22

2024.8.29

 

7장 영상 분할

7.1 컨투어

 

컨투어(contour)는 등고선을 의미합니다. 등고선은 지형의 높이가 같은 영역을 하나의 선으로 표시한 것입니다. 영상에서 컨투어를 그리면 모양을 쉽게 인식할 수 있습니다. OpenCV에서 제공하는 컨투어 함수는 다음과 같습니다.

4.0 이전

dst, contours, hierarchy = cv2.findContours(src, mode, method, contours, hierarchy, offset)

4.0 이후

contours, hierarchy = cv2.findContours(src, mode, method, contours, hierarchy, offset)

  • src: 입력 영상, 검정과 흰색으로 구성된 바이너리 이미지
  • mode: 컨투어 제공 방식
    • cv2.RETR_EXTERNAL : 가장 바깥쪽 라인만 생성,
    • cv2.RETR_LIST : 모든 라인을 계층 없이 생성,
    • cv2.RET_CCOMP : 모든 라인을 2 계층으로 생성,
    • cv2.RETR_TREE : 모든 라인의 모든 계층 정보를 트리 구조로 생성)
  • method: 근사 값 방식
    • cv2.CHAIN_APPROX_NONE : 근사 없이 모든 좌표 제공,
    • cv2.CHAIN_APPROX_SIMPLE : 컨투어 꼭짓점 좌표만 제공,
    • cv2.CHAIN_APPROX_TC89_L1 : Teh-Chin 알고리즘으로 좌표 개수 축소,
    • cv2.CHAIN_APPROX_TC89_KCOS : Teh-Chin 알고리즘으로 좌표 개수 축소
  • contours(optional) : 검출한 컨투어 좌표 (list type)
  • hierarchy(optional) : 컨투어 계층 정보 (Next, Prev, FirstChild, Parent, -1 [해당 없음])
  • offset(optional) : ROI 등으로 인해 이동한 컨투어 좌표의 오프셋 위 함수로 컨투어를 찾아낸 다음 아래 함수로 컨투어를 그려줄 수 있습니다.
 

mode: 컨투어 제공 방식

  • method : 근사값 방식
    • cv2.CHAIN_APPROX_NONE : 모든 point를 저장
    • cv2.CHAIN_APPROX_SIMPLE : 4개의 point만을 저장하여 메모리를 절약
    • cv2.CHAIN_APPROX_TC89_L1 : Teh_Chin 연결 근사 알고리즘 L1버전을 적용하여 point 개수를 줄임
    • cv2.CHAIN_APPROX_TC89_KCOS : Teh_Chin 연결 근사 알고리즘 KCOS 버전을 적용하여 point 개수를 줄임
  • return 값으로 오는 contours의 shape를 찍어보면 각각 method마다 shape이 다른것을 볼 수 있다.

v2.drawContours(img, contours, contourIdx, color, thickness)

  • img : 입력 영상
  • contours : 그림 그릴 컨투어 배열
    • cv2.findContours() 함수의 반환 결과를 전달해주면 됨
  • contourIdx: 그림 그릴 컨투어 인덱스, -1: 모든 컨투어 표시
  • color: 색상 값
  • thickness: 선 두께, 0: 채우기

cv2.darwContours()는 실제로 컨투어 선을 그리는 함수입니다. img영상에 contours 배열에 있는 컨투어 중 contourIdx에 해당하는 컨투어를 color 색상과 thickness 두께로 선을 그립니다. 위 두 함수를 활용하여 컨투어를 그려보겠습니다.

 

파라미터로 cv2.RETR_EXTERNAL을 전달할 때는 그림의 외곽 부분에만 컨투어를 그립니다.

하지만 cv2.RETR_TREE를 전달할 때는 모든 경계에 컨투어를 그립니다. 이를 트리 계층 컨투어라고 합니다.

위 그림에서는 보기 쉽게 다양한 색깔로 컨투어를 그렸습니다.

위 코드 중

print(len(contour2), hierarchy)

의 출력 결과를 살펴보겠습니다.

cv2.findContours() 함수를 호출하면 컨투어 좌표뿐만 아니라 hierarchy를 다음과 같이 출력합니다.

아래는 contour2, 즉 위 출력 그림에서 오른쪽 결과(트리 계층 컨투어)에 해당하는 hierarchy입니다.

 

 
 

요소 값 중 -1은 의미 없음을 나타냅니다.

우선, 0번째 행을 보겠습니다.

0번째 행은 첫 번째 도형의 컨투어를 의미합니다. 이는 위 두 번째 그림에서 왼쪽의 삼각형 외곽을 뜻합니다.

0번째 행의 Next, Prev, First Child, Parent는 각각 2, -1, 1, -1입니다.

Prev와 Parent는 -1이므로 아무 의미가 없다는 뜻입니다. 즉, 삼각형 외곽 컨투어의 기준에는 이전 도형이 없고, 부모 도형도 없다는 뜻입니다.

그러나 Next와 First Child는 각 2와 1입니다. 이 말은 다음 도형은 2행이고, 자식은 1행이라는 것입니다. 2행인 4, 0 ,3 -1을 이루고 있는 컨투어는 맨 오른쪽 원 도형의 외곽입니다.

1행인 -1, -1, -1, 0은 왼쪽 삼각형의 내부 컨투어입니다. 자연스럽게 내부 삼각형의 부모는 0입니다. 2행인 4, 0 ,3 -1을 이루고 있는 컨투어의 다음 도형은 4행인 사각형 외곽이고, 이전은 0행인 삼각형 외곽이며, 자식은 3행인 원 내부입니다.

이런 식으로 컨투어 계층 정보(hierarchy)를 보면 외곽 요소와 자식 요소를 순회할 수 있습니다. 최외곽 컨투어만 골라내려면 부모 항목이 -1인 행만 찾으면 되고, 그것이 이 예제에서는 도형의 개수와 같습니다.

 

7.1.1 이미지 모멘트와 컨투어 속성

 

OpenCV를 사용하여 이미지에서 도형의 컨투어(외곽선)를 찾고, 각 도형의 중심점, 넓이, 외곽선 길이 등을 계산한 후 이미지에 표시하는 예제

이미지에서 도형의 컨투어를 찾아내고, 각 도형의 중심점, 넓이, 외곽선 길이를 계산하여 이미지 위에 표시하는 코드입니다.

도형의 중심점에는 노란색 점이 표시되고, 넓이와 외곽선 길이는 각각 빨간색과 파란색 텍스트로 이미지에 나타납니다.

 

컨투어를 감싸는 도형

 

OpenCV를 활용하면 컨투어를 감싸는 도형을 그릴 수도 있습니다. 컨투어를 감싸는 도형을 그리는 아래 함수들에 대해 먼저 알아보겠습니다.

  • x, y, w, h = cv2.boundingRect(contour): 좌표를 감싸는 사각형 반환
    • x, y: 사각형의 왼쪽 상단 좌표
    • w, h: 사각형의 폭과 높이
  • rotateRect = cv2.minAreaRect(contour) : 좌표를 감싸는 최소한의 사각형 계산
  • vertex = cv2.boxPoints(rotateRect) : rotateRect로부터 꼭짓점 좌표 계산
    • vertex: 4개의 꼭짓점 좌표, 소수점 포함이므로 정수 변환 필요
  • center, radius = cv2.minEnclosingCircle(contour) : 좌표를 감싸는 최소한의 동그라미 계산
    • center: 원점 좌표(x, y)
    • radius: 반지름
  • area, triangle = cv2.minEnclosingTriangle(points) : 좌표를 감싸는 최소한의 삼각형 게산
    • area: 넓이
    • triangle: 3개의 꼭짓점 좌표
  • ellipse = cv2.fitEllipse(points) : 좌표를 감싸는 최소한의 타원 계산
  • line = cv2.fitLine(points, distType, param, reps, aeps, line) : 중심점을 통과하는 직선 계산
    • distType: 거리 계산 방식
      • cv2.DIST_L2
      • cv2.DIST_L1
      • cv2.DIST_L12
      • cv2.DIST_FAIR
      • cv2.DIST_WELSCH
      • cv2.DIST_HUBER
    • param: distType에 전달할 인자, 0 = 최적 값 선택
    • reps: 반지름 정확도, 선과 원본 좌표의 거리, 0.01 권장
    • aeps: 각도 정확도, 0.01 권장
    • line(optional): vx, vy 정규화된 단위 벡터, x0, y0: 중심점 좌표

위 함수를 활용하여 컨투어를 감싸는 다양한 도형을 그려보겠습니다.

 

7.1.2 컨투어 단순화

 

지금까지 살펴본 컨투어 함수는 이미지 외곽을 따라 그림을 그려주는 기능을 제공했습니다.

하지만 실생활에서 얻는 대부분의 이미지는 약간의 노이즈가 포함되어 있습니다.

그래서 컨투어를 너무 정확히 그리는 것도 바람직하지 않습니다.

오히려 약간 단순화해 그리는 게 정확하게 그리는 것보다 더 쓸모 있는 경우가 있습니다.

OpenCV는 아래와 같은 함수를 통해 근사 값으로 컨투어를 계산해줍니다.

approx = cv2.approxPolyDP(contour, epsilon, closed)

  • contour: 대상 컨투어 좌표
  • epsilon: 근사 값 정확도, 오차 범위
  • closed: 컨투어의 닫힘 여부
  • approx: 근사 계산한 컨투어 좌표

cv2.approxPolyDP() 함수를 활용하면 오른쪽과 같이 요철이 있는 부분은 무시하고 컨투어를 계산해줍니다.

컨투어를 단순화하는 또 다른 형태는 볼록 선체(convex hull)를 만드는 것입니다. 볼록 선체란 어느 한 부분도 오목하지 않은 도형을 의미합니다. 따라서 볼록 선체는 대상을 완전히 포함하는 외곽 영역을 찾는데 유용합니다.

hull = cv2.convexHull(points, hull, clockwise, returnPoints): 볼록 선체 계산

  • points: 입력 컨투어
  • hull(optional): 볼록 선체 결과
  • clockwise(optional): 방향 지정 (True: 시계 방향)
  • returnPoints(optional): 결과 좌표 형식 선택 (True: 볼록 선체 좌표 변환, False: 입력 컨투어 중에 볼록 선체에 해당하는 인덱스 반환)

retval = cv2.isContourConvex(contour): 볼록 선체 만족 여부 확인

  • retval: True인 경우 볼록 선체임

defects = cv2.convexityDefects(contour, convexhull): 볼록 선체 결함 찾기

  • contour: 입력 컨투어
  • convexhull: 볼록 선체에 해당하는 컨투어의 인덱스
  • defects: 볼록 선체 결함이 있는 컨투어의 배열 인덱스, N x 1 x 4 배열, [starts, end, farthest, distance]
    • start: 오목한 각이 시작되는 컨투어의 인덱스
    • end: 오목한 각이 끝나는 컨투어의 인덱스
    • farthest: 볼록 선체에서 가장 먼 오목한 지점의 컨투어 인덱스
    • distance: farthest와 볼록 선체와의 거리

 

7.1.3 컨투어와 도형 매칭

 

서로 다른 물체의 컨투어를 비교하면 두 물체가 얼마나 비슷한지 알 수 있습니다.

이는 아래 함수로 간단히 구현할 수 있습니다.

retval = cv2.matchShapes(contour1, contour2, method, parameter): 두 개의 컨투어로 도형 매칭

  • contour1, contour2: 비교할 두 개의 컨투어
  • method: 휴 모멘트 비교 알고리즘 선택 플래그 (cv2.CONTOURS_MATCH_I1, cv2.CONTOURS_MATCH_I2, cv2.CONTOURS_MATCH_I3)
  • parameter: 알고리즘에 전달을 위한 예비 인수로 0으로 고정
  • retval: 두 도형의 닮은 정도 (0=동일, 숫자가 클수록 다름)

 

7.2 허프 변환

 

허프 변환을 활용해 이미지에서 직선이나 원과 같은 다양한 모양을 인식할 수 있습니다.

여기서는 직선과 원을 검출하는 함수에 대해 배워보겠습니다.

허프 변환에 대한 이론적인 설명은 opencv 한글문서를 참고해주시기 바랍니다. 혹은 한글 위키피디아도 도움이 됩니다.

 

7.2.1 허프 선 변환

 

이미지는 수많은 픽셀로 구성되어 있습니다. 그 픽셀 중 서로 직선 관계를 갖는 픽셀들만 골라내는 것이 허프 선 변환의 핵심입니다. OpenCV에서는 허프 변환을 위해 아래와 같은 함수를 제공합니다.

 

lines = cv2.HoughLines(img, rho, theta, threshold, lines, srn=0, stn=0, min_theta, max_theta)

  • img: 입력 이미지, 1 채널 바이너리 스케일
  • rho: 거리 측정 해상도, 0~1
  • theta: 각도, 라디안 단위 (np.pi/0~180)
  • threshold: 직선으로 판단할 최소한의 동일 개수 (작은 값: 정확도 감소, 검출 개수 증가 / 큰 값: 정확도 증가, 검출 개수 감소)
  • lines: 검출 결과, N x 1 x 2 배열 (r, Θ)
  • srn, stn: 멀티 스케일 허프 변환에 사용, 선 검출에서는 사용 안 함
  • min_theta, max_theta: 검출을 위해 사용할 최대, 최소 각도
 

거리와 각도를 얼마나 세밀하게 계산할 것인지를 rho와 theta 파라미터로 조정할 수 있습니다.

threshold는 같은 직선에 몇 개의 점이 등장해야 직선으로 판단할지를 나타내는 최소한의 개수를 말합니다.

아래는 직선을 검출하고 기준 좌표에 빨간 점을 찍은 예시입니다.

 

7.2.2 확률적 허프 선 변환

 

허프 선 검출은 모든 점에 대해 수많은 선을 그어서 직선을 찾기 때문에 연산량이 무척 많습니다.

이를 개선하기 위한 방법이 확률적 허프 선 변환입니다.

이는 모든 점을 고려하지 않고 무작위로 선정한 픽셀에 대해 허프 변환을 수행하고 점차 그 수를 증가시키는 방법입니다.

다음의 함수로 확률적 허프 선 변환을 수행할 수 있습니다.

 

lines = cv2.HoughLinesP(img, rho, theta, threshold, lines, minLineLength, maxLineGap)

  • minLineLength(optional): 선으로 인정할 최소 길이
  • maxLineGap(optional): 선으로 판단할 최대 간격
  • lines: 검출된 선 좌표, N x 1 x 4 배열 (x1, y1, x2, y2)
  • 이외의 파라미터는 cv2.HoughLines()와 동일
 

cv2.HoughLines()의 검출 결과는 r, Θ이지만 cv2.HoughLinesP()의 검출 결과는 선의 시작과 끝 좌표입니다.

이는 확률적으로 선을 검출하므로 당연히 cv2.HoughLines()보다 선 검출이 적습니다. 따라서 엣지를 강하게 하고 threshold를 낮게 지정해주어야 합니다.

 

7.2.3 허프 원 변환

 

허프 변환을 통해 원을 검출할수도 있습니다.

circle = cv2.HoughCircles(img, method, dp, minDist, circles, param1, param2, minRadius, maxRadius)

  • img: 입력 이미지, 1채널 배열
  • method: 검출 방식 선택 (현재 cv2.HOUGH_GRADIENT만 가능)
  • dp: 입력 영상과 경사 누적의 해상도 반비례율, 1: 입력과 동일, 값이 커질수록 부정확
  • minDist: 원들 중심 간의 최소 거리 (0: 에러, 0이면 동심원이 검출 불가하므로)
  • circles(optional): 검출 원 결과, N x 1 x 3 부동 소수점 배열 (x, y, 반지름)
  • param1(optional): 캐니 엣지에 전달할 스레시홀드 최대 값 (최소 값은 최대 값의 2배 작은 값을 전달)
  • param2(optional): 경사도 누적 경계 값 (값이 작을수록 잘못된 원 검출)
  • minRadius, maxRadius(optional): 원의 최소 반지름, 최대 반지름 (0이면 이미지 전체의 크기)
 

cv2.HoughCircles는 캐니 엣지를 수행하고 나서 소벨 필터를 적용해 엣지의 경사도(gradient)를 누적하는 방법으로 원 검출을 구현했습니다.

그래서 캐니 엣지 및 경사도 누적에 대한 파라미터(param1, param2)가 있는 것입니다.

 

7.3 연속 영역 분할

7.3.1 거리 변환

 

이미지에서 물체 영역을 정확히 파악하기 위해서는 물체 영역의 뼈대를 찾아야 합니다. 뼈대를 검출하는 방법 중 하나가 외곽 경계로부터 가장 멀리 떨어진 곳을 찾는 방법인 거리 변환입니다. OpenCV에는 거리 변환을 해주는 cv2.distanceTransform() 함수가 있습니다.

cv2.distanceTransform(src, distanceType, maskSize)

  • src: 입력 영상, 바이너리 스케일
  • distanceType: 거리 계산 방식
    • cv2.DIST_L2
    • cv2.DIST_L1
    • cv2.DIST_L12
    • cv2.DIST_FAIR
    • cv2.DIST_WELSCH
    • cv2.DIST_HUBER
  • maskSize: 거리 변환 커널 크기
 
  • cv2.DIST_L1
    • 맨해튼 거리(Manhattan distance)라고도 불립니다.
    • 이 방식은 수직 및 수평 방향의 거리만 계산합니다. 즉, 두 점 사이의 거리는 각각의 좌표 차이의 합으로 계산됩니다.
    • 격자(grid) 기반의 거리 계산에서 유용하며, 가장 빠르게 계산할 수 있습니다.
    • 예시: 체스판에서 말이 움직이는 거리처럼, 직선으로만 이동하는 경우.
  • cv2.DIST_L2
    • 유클리드 거리(Euclidean distance)라고도 불립니다.
    • 이 방식은 두 점 사이의 직선 거리를 계산합니다. 피타고라스 정리를 사용하여 계산됩니다.
    • 가장 직관적이며 실제 물리적 거리를 측정할 때 유용합니다.
    • 예시: 두 점 사이의 직선 거리 측정.
  • cv2.DIST_C
    • 체비쇼프 거리(Chebyshev distance)라고도 불립니다.
    • 이 방식은 두 점 사이의 최대 축 방향 차이(maximum coordinate difference)를 거리로 측정합니다.
    • 바둑판 패턴에서 대각선 및 직선 방향 모두 동일한 거리로 계산할 수 있습니다.
    • 예시: 체스판에서 킹이 이동하는 경우와 유사한 방식.
  • cv2.DIST_L12
    • L_{1-2} 거리는 혼합된 거리 계산 방식으로, 주로 다양한 거리를 결합한 형태로 사용됩니다.
    • L1+L2 형태로 결합된 거리 계산을 수행합니다.
  • cv2.DIST_FAIR
    • 공정 거리(Fair distance)는 두 점 사이의 거리 계산에서 좀 더 균형 잡힌 측정을 제공합니다.
    • 이 방법은 특히 노이즈가 있는 이미지에서 안정적인 거리 계산을 위해 사용될 수 있습니다.
  • cv2.DIST_WELSCH
    • 웰치 거리(Welsch distance)는 두 점 사이의 거리를 계산할 때, 거리가 커질수록 증가율이 감소하는 방식입니다. 따라서 극단적인 값(즉, 먼 거리)에 덜 민감한 방식으로 계산됩니다.
  • cv2.DIST_HUBER
    • 후버 거리(Huber distance)는 일반적인 유클리드 거리와 절대 거리(L1 거리) 사이에서 선택적인 거리 측정을 제공합니다. 작은 거리에서는 유클리드 거리처럼 계산되지만, 큰 거리에서는 절대 거리처럼 계산됩니다. 이는 노이즈에 강한 특성을 가집니다.

 

7.3.2 연결 요소 레이블링

 

연결된 요소끼리 분리하는 방법 중 레이블링이라는 방법이 있습니다.

아래와 같이 이미지에서 픽셀 값이 0으로 끊어지지 않는 부분끼리 같은 값을 부여해서 분리를 할 수 있습니다.

OpenCV에서 제공하는 cv2.connectedComponents() 함수를 활용하면 이를 구현할 수 있습니다.

이 함수는 이미지 전체에서 0으로 끊어지지 않는 부분끼리 같은 값을 부여합니다.

 

retval, labels = cv2.connectedComponents(src, labels, connectivity=8, ltype) : 연결 요소 레이블링과 개수 반환

  • src : 입력 이미지, 바이너리 스케일
  • labels(optional) : 레이블링된 입력 이미지와 같은 크기의 배열
  • connectivity(optional) : 연결성을 검사할 방향 개수(4, 8 중 선택)
  • ltype(optional) : 결과 레이블 배열 dtype
  • retval(optional) : 레이블 개수
 

retval, labels, stats, centroids = cv2.connectedComponentsWithStats(src, labels, stats, centroids, connectivity, ltype) : 레이블링된 각종 상태 정보 반환

  • stats: N x 5 행렬 (N: 레이블 개수) [x좌표, y좌표, 폭, 높이, 너비]
  • centroids: 각 레이블의 중심점 좌표, N x 2 행렬 (N: 레이블 개수)
 

cv2.connectedComponents() 함수를 활용해서 연결된 요소끼리 같은 색상을 칠해보겠습니다. 주석 처리된 cv2.connectedComponentsWithStats()로 코드를 돌려도 동일한 결과가 나올 겁니다.

 

7.3.3 색 채우기

 

그림판 같은 그리기 도구에서 채우기 기능을 활용하여 색상을 칠해본 경험이 있을 겁니다. OpenCV의 cv2.floodFill()은 이런 기능을 제공합니다. 연속되는 영역에 같은 색상을 채워 넣는 기능을 합니다.


retval, img, mask, rect = cv2.floodFill(img, mask, seed, newVal, loDiff, upDiff, flags)

  • img: 입력 이미지, 1 또는 3채널
  • mask: 입력 이미지보다 2 x 2 픽셀이 더 큰 배열, 0이 아닌 영역을 만나면 채우기 중지
  • seed: 채우기 시작할 좌표
  • newVal: 채우기에 사용할 색상 값
  • loDiff, upDiff(optional): 채우기 진행을 결정할 최소/최대 차이 값
  • flags(optional): 채우기 방식 선택
    • cv2.FLOODFILL_MASK_ONLY : img가 아닌 mask에만 채우기 적용, cv2.FLOODFILL_FIXED_RANGE: 이웃 픽셀이 아닌 seed 픽셀과 비교
  • retval: 채우기 한 픽셀의 개수
  • rect: 채우기가 이루어진 영역을 감싸는 사각형
 
 

이 함수는 img 이미지의 seed 좌표에서부터 시작해서 newVal의 값으로 채우기를 시작합니다.

이때 이웃하는 픽셀에 채우기를 계속하려면 현재 픽셀이 이웃 픽셀의 loDiff를 뺀 값보다 크거나 같고 upDiff를 더한 값보다 작거나 같아야 합니다.

이것을 식으로 정리하면 아래와 같습니다. (만약 loDiff와 upDiff를 생략하면 seed의 픽셀 값과 같은 값을 갖는 이웃 픽셀만 채우기를 진행합니다.)

이웃 픽셀 - loDiff <= 현재 픽셀 <= 이웃 픽셀 + upDiff

하지만, 마지막 인자인 flags에 cv2.FLOODFILL_FIXED_RANGE가 전달되면 이웃 픽셀이 아닌 seed 픽셀과 비교하며 색을 채웁니다.

또한, flags에 cv2.FLOODFILL_MASK_ONLY가 전달되면 img에 채우기를 하지 않고 mask에만 채우기를 합니다.

 

7.3.4 워터셰드

 

워터셰드(watershed)는 강물이 한 줄기로 흐르다가 갈라지는 경계인 분수령을 뜻합니다.

워터셰드는 앞서 살펴본 색 채우기(flood fill)과 비슷한 방식으로 연속된 영역을 찾는 것이라고 볼 수 있습니다. 다만, seed를 하나가 아닌 여러 개를 지정할 수 있고 이를 마커라고 합니다.

 

markers = cv2.watershed(img, markers)

  • img : 입력 이미지
  • markers : 마커, 입력 이미지와 크기가 같은 1차원 배열(int32)
 

markers는 입력 이미지와 행과 열 크기가 같은 1차원 배열로 전달해야 합니다.

markers의 값은 경계를 찾고자 하는 픽셀 영역은 -1을 갖게 하고 나머지 연결된 영역에 대해서는 동일한 정수 값을 갖게 합니다.

예를 들어 1은 배경, 2는 전경인 식입니다. cv2.watershed() 함수를 활용해 경계를 나눠보겠습니다.

 

이미지의 전경과 배경을 분리하는 코드입니다. 마우스를 드래그하여 로봇 태권 V 내부를 표시해주고, 또 배경도 따로 표시해줍니다.

그런 다음 오른쪽 마우스 버튼을 클릭하면 전경과 배경이 구분된 이미지를 얻을 수 있습니다.

우선은 아래 코드를 통해 0으로 채워진 마커를 생성합니다.

marker = np.zeros((rows, cols), np.int32)

그다음 아래 코드를 통해 마우스 드래그한 부분의 좌표에 해당하는 마커 좌표에 현재의 마커 아이디를 채웁니다.

이 예제에서는 전경은 1, 배경은 2로 채웁니다. 이것은 앞서 살펴본 색 채우기(flood fill)의 seed 값이 여러 개인 것 과 같은 의미입니다.

marker[y,x] = markerId

그 다음 마우스 오른쪽 버튼을 클릭하면 아래 코드로 워터셰드를 실행합니다. 워터셰드를 실행하면 경계에 해당하는 영역은 -1로 채워지고 전경은 1, 배경은 2로 채워집니다.

cv2.watershed(img, marker)

마지막으로 -1로 채워진 마커와 같은 좌표의 이미지 픽셀은 초록색으로 바꾸고, 같은 마커 아이디 값을 갖는 영역끼리 같은 색으로 채웁니다. 이때 색은 맨 처음 마우스 왼쪽 버튼을 클릭했을 때 좌표의 픽셀 값으로 지정했습니다. 그래서 위 그림에서는 전경은 빨간색으로 채워졌고, 배경은 회색으로 채워졌습니다. 맨 처음 전경을 선택할 때 귀부분(빨간색)을 클릭했습니다.

워터셰드는 경계 검출이 어려운 경우 사용할 수 있습니다.

전경이나 배경으로 확신할 수 있는 몇몇 픽셀을 지정해줌으로써 경계를 찾을 수 있습니다.

 

7.3.5 그랩컷

 

그랩컷은 사용자가 전경(배경이 아닌 부분)으로 분리할 부분에 사각형 표시를 해주면 전경과 배경의 색상 분포를 추정해서 동일한 레이블을 가진 연결된 영역에서 전경과 배경을 분리합니다.

아래의 함수로 그랩컷을 구현할 수 있습니다.

 

mask, bgdModel, fgdModel = cv2.grabCut(img, mask, rect, bgdModel, fgdModel, iterCount, mode)

  • img: 입력 이미지
  • mask: 입력 이미지와 크기가 같은 1 채널 배열, 배경과 전경을 구분하는 값을 저장
    • cv2.GC_BGD : 확실한 배경(0),
    • cv2.GC_FGD : 확실한 전경(1)
    • cv2.GC_PR_BGD : 아마도 배경(2),
    • cv2.GC_PR_FGD : 아마도 전경(3)
  • rect: 전경이 있을 것으로 추측되는 영역의 사각형 좌표, 튜플 (x1, y1, x2, y2)
  • bgdModel, fgdModel: 함수 내에서 사용할 임시 배열 버퍼 (재사용할 경우 수정하지 말 것)
  • iterCount: 반복 횟수
  • mode(optional): 동작 방법
    • cv2.GC_INIT_WITH_RECT : rect에 지정한 좌표를 기준으로 그랩컷 수행
    • cv2.GC_INIT_WITH_MASK : mask에 지정한 값을 기준으로 그랩컷 수행
    • cv2.GC_EVAL: 재시도
 

mode에 cv2.GC_INIT_WITH_RECT를 전달하면 세 번째 파라미터인 rect에 전달한 사각형 좌표를 가지고 전경과 배경을 분리합니다.

그 결과를 두 번째 파라미터인 mask에 할당해 반환합니다.

mask에 할당받은 값이 0과 1이면 확실한 배경, 전경을 의미하고, 2와 3이면 아마도 배경, 전경일 가능성이 있다는 뜻입니다.

이렇게 1차적으로 배경과 전경을 구분한 뒤 mode에 cv2.GC_INIT_WITH_MASK를 지정해서 다시 호출하면 좀 더 정확한 mask를 얻을 수 있습니다.

이때 bgdModel과 fgdModel은 함수가 내부적으로 연산에 사용하는 임시 배열로 다음 호출 시 이전 연산을 반영하기 위해 재사용하므로 그 내용을 수정하면 안 됩니다.

 
 

아래는 그랩컷을 활용하여 배경을 분리하는 예제 코드입니다.

우선 마우스로 드래그하여 전경 외곽 영역을 표시해줍니다. 1차적으로 배경과 전경이 분리됩니다.

배경을 추가로 제거하고 싶으면 원본 이미지에 쉬프트 키를 누른 상태로 마우스로 검은색 선을 그어주면 됩니다.

잘못 제거된 전경을 추가하고 싶으면 원본 이미지에 컨트롤키를 누른 상태로 마우스로 흰색 선을 그어주면 됩니다.

 

마우스 이벤트 처리 때문에 코드가 다소 길어졌습니다. 맨 처음 마우스 드래그로 사각형을 그려주었습니다.

처음 마우스 버튼을 누른 좌표와 마지막으로 마우스 버튼을 뗀 좌표를 구해서 cv2.grabCut()을 호출할 때 mode를 cv2.GC_INIT_WITH_RECT로 설정해서 호출하면 됩니다.

그다음 쉬프트와 컨트롤키를 누른 상태로 마우스 드래그를 해주었을 때의 좌표를 mask에 반영했다가 마우스 뗀 시점에 cv2.grabCut()을 호출하여 mode에 cv2.GC_INIT_WITH_MASK를 전달하면 됩니다.

이때 쉬프트와 컨트롤키에 따라 mask에 반영할 값이 cv2.GC_BGD 또는 cv2.GC_FGD가 됩니다.

마우스 이벤트를 처리하는 onMouse() 함수를 뜯어보겠습니다. 우선 아래 코드는 키보드의 아무 키도 누르지 않은 상태로 마우스 왼쪽 버튼을 클릭했을 때를 처리해줍니다.

mode = cv2.GC_INIT_WITH_RECT으로 설정하고 시작 좌표를 구합니다.

    if event == cv2.EVENT_LBUTTONDOWN : # 왼쪽 마우스 누름
        if flags <= 1: # 아무 키도 안 눌렀으면
            mode = cv2.GC_INIT_WITH_RECT # 드래그 시작, 사각형 모드 ---①
            rect[:2] = x, y # 시작 좌표 저장

아래 코드는 마우스 왼쪽 버튼이 눌러진 상태로 드래그되었을 때를 처리해줍니다.

마우스 왼쪽 버튼이 눌러진 상태로 드래그가 되었는데 그때의 mode가 cv2.GC_INIT_WITH_RECT이라면, 단순히 마우스가 움직이는 동안 화면에 사각형을 표시합니다.

mode가 cv2.GC_INIT_WITH_RECT라는 것은 키보드를 아무것도 누르지 않았다는 뜻입니다.

# 마우스가 움직이고 왼쪽 버튼이 눌러진 상태
    elif event == cv2.EVENT_MOUSEMOVE and flags & cv2.EVENT_FLAG_LBUTTON :
        if mode == cv2.GC_INIT_WITH_RECT: # 드래그 진행 중 ---②
            img_temp = img.copy()
            # 드래그 사각형 화면에 표시
            cv2.rectangle(img_temp, (rect[0], rect[1]), (x, y), (0,255,0), 2)
            cv2.imshow('img', img_temp)

반면, 마우스 왼쪽 버튼이 눌러진 상태로 드래그가 되었는데 그때의 mode가 cv2.GC_INIT_WITH_RECT이 아니고, flags가 1보다 크다면 아래의 코드가 실행됩니다.

flags가 1보다 크다는 것은 키보드의 어떤 버튼이 눌렸다는 뜻입니다. 쉬프트든 컨트롤이든 눌렸다는 거죠.

이때는 mode를 cv2.GC_INIT_WITH_MASK로 설정합니다. 그리고 컨트롤/쉬프트 키가 눌렸을 때에 대해 각각 화면에 흰색 점, 검은색 점을 표시합니다.

또한 마우스가 움직인 좌표에 해당하는 mask 인덱스에 각각 cv2.GC_FGD/cv2.GC_BGD을 반영했습니다.

cv2.GC_FGD는 확실한 전경, cv2.GC_BGD는 확실한 배경을 뜻합니다.

        elif flags > 1: # 키가 눌러진 상태
            mode = cv2.GC_INIT_WITH_MASK    # 마스크 모드 ---③
            if flags & cv2.EVENT_FLAG_CTRLKEY :# 컨트롤 키, 분명한 전경
                # 흰색 점 화면에 표시
                cv2.circle(img_draw,(x,y),3, (255,255,255),-1)
                # 마스크에 GC_FGD로 채우기      ---④
                cv2.circle(mask,(x,y),3, cv2.GC_FGD,-1)
            if flags & cv2.EVENT_FLAG_SHIFTKEY : # 쉬프트키, 분명한 배경
                # 검정색 점 화면에 표시
                cv2.circle(img_draw,(x,y),3, (0,0,0),-1)
                # 마스크에 GC_BGD로 채우기      ---⑤
                cv2.circle(mask,(x,y),3, cv2.GC_BGD,-1)
            cv2.imshow('img', img_draw) # 그려진 모습 화면에 출력

아래 코드는 마우스를 뗀 지점의 좌표를 구해서 사각형을 표시합니다.

    elif event == cv2.EVENT_LBUTTONUP: # 마우스 왼쪽 버튼 뗀 상태 ---⑥
        if mode == cv2.GC_INIT_WITH_RECT : # 사각형 그리기 종료
            rect[2:] =x, y # 사각형 마지막 좌표 수집
            # 사각형 그려서 화면에 출력 ---⑦
            cv2.rectangle(img_draw, (rect[0], rect[1]), (x, y), (255,0,0), 2)
            cv2.imshow('img', img_draw)

최종적으로 아래의 코드로 그랩컷을 적용합니다.

mask에서 배경으로 표시된 cv2.GC_BGD(확실한 배경), cv2.GC_PR_BGD(아마도 배경)에 해당하는 좌표를 0으로 채워서 배경을 제거합니다.

그리고 최종 결과를 출력합니다.

        cv2.grabCut(img, mask, tuple(rect), bgdmodel, fgdmodel, 1, mode)
        img2 = img.copy()
        # 마스크에 확실한 배경, 아마도 배경으로 표시된 영역을 0으로 채우기
        img2[(mask==cv2.GC_BGD) | (mask==cv2.GC_PR_BGD)] = 0
        cv2.imshow('grabcut', img2) # 최종 결과 출력
        mode = cv2.GC_EVAL # 그랩컷 모드 리셋

사실 그랩컷을 적용하는 코드는 한 줄이지만, 마우스 이벤트 처리 때문에 코드가 다소 복잡해졌습니다.

 

7.3.6 평균 이동 필터


평균 이동 필터를 활용하면 물감으로 그림을 그린 것과 같이 이미지를 바꿀 수 있습니다. 평균 이동 필터를 제공하는 OpenCV 함수는 아래와 같습니다.

 

dst = cv2.pyrMeanShiftFiltering(src, sp, sr, dst, maxLevel, termcrit)

  • src: 입력 이미지
  • sp: 공간 윈도 반지름 크기
  • sr: 색상 윈도 반지름 크기
  • maxLevel(optional): 이미지 피라미드 최대 레벨
  • termcrit(optional): 반복 중지 요건
    • type = cv2.TERM_CRITERIA_MAX_ITER + cv2.TERM_CRITERIA_EPS : 중지 형식
      • cv2.TERM_CRITERIA_EPS : 정확도가 최소 정확도(epsilon) 보다 작아지면 중지
      • cv2.TERM_CRITERIA_MAX_ITER : 최대 반복 횟수(max_iter)에 도달하면 중지
    • default max_iter=5 : 최대 반복 횟수
    • default epsilon=1 : 최소 정확도
 

이 함수는 내부적으로 이미지 피라미드를 만들어 작은 이미지의 평균 이동 결과를 큰 이미지에 적용합니다.

그래서 함수 이름 앞에 pyr가 붙었습니다. src에는 입력 이미지가 전달되는데 그레이 스케일과 컬러 스케일 모두 가능합니다.

sp 파라미터는 평균 이동(MeanShift)에 사용할 윈도 크기입니다.

몇 픽셀씩 묶어서 평균을 내어 이동할지를 결정합니다. sr 파라미터는 색상 윈도 크기로 색상 값의 차이 범위를 지정합니다.

평균을 계산할 때 값의 차이가 sr 값의 범위 안에 있는 픽셀만을 대상으로 합니다.

따라서 sr이 너무 작으면 원본과 별 차이가 없고, 너무 크면 원본과 많이 달라집니다.

maxLevel은 이미지 피라미드 최대 레벨입니다. 이 값이 0보다 크면 그 값만큼 작은 이미지 피라미드로 평균 이동해서 얻은 결과를 적용합니다.

값이 클수록 속도가 빨라지지만 영역과 색상이 거칠어집니다.

termcrit은 반복을 중지할 기준을 지정하는 파라미터입니다.

 

주요 거리 계산 방식 cv2.DIST_L1

맨해튼 거리(Manhattan distance)라고도 불립니다. 이 방식은 수직 및 수평 방향의 거리만 계산합니다. 즉, 두 점 사이의 거리는 각각의 좌표 차이의 합으로 계산됩니다. 수식: $𝑑(𝑝,𝑞)=∣𝑥1−𝑥2∣+ ∣ 𝑦 1 − 𝑦 2 ∣ d(p,q)=∣x 1​−x 2​∣+∣y 1​−y 2​∣ 이 방법은 격자(grid) 기반의 거리 계산에서 유용하며, 가장 빠르게 계산할 수 있습니다. 예시: 체스판에서 말이 움직이는 거리처럼, 직선으로만 이동하는 경우. cv2.DIST_L2

유클리드 거리(Euclidean distance)라고도 불립니다. 이 방식은 두 점 사이의 직선 거리를 계산합니다. 피타고라스 정리를 사용하여 계산됩니다. 수식: 𝑑 ( 𝑝 , 𝑞 ) = ( 𝑥 1 − 𝑥 2 ) 2 + ( 𝑦 1 − 𝑦 2 ) 2 d(p,q)= (x 1​−x 2​) 2 +(y 1​−y 2​) 2

가장 직관적이며 실제 물리적 거리를 측정할 때 유용합니다. 예시: 두 점 사이의 직선 거리 측정. cv2.DIST_C

체비쇼프 거리(Chebyshev distance)라고도 불립니다. 이 방식은 두 점 사이의 최대 축 방향 차이(maximum coordinate difference)를 거리로 측정합니다. 수식: 𝑑 ( 𝑝 , 𝑞 ) = max ⁡ ( ∣ 𝑥 1 − 𝑥 2 ∣ , ∣ 𝑦 1 − 𝑦 2 ∣ ) d(p,q)=max(∣x 1​−x 2​∣,∣y 1​−y 2​∣) 바둑판 패턴에서 대각선 및 직선 방향 모두 동일한 거리로 계산할 수 있습니다. 예시: 체스판에서 킹이 이동하는 경우와 유사한 방식. cv2.DIST_L12

L_{1-2} 거리는 혼합된 거리 계산 방식으로, 주로 다양한 거리를 결합한 형태로 사용됩니다. 이 방식은 수학적으로 𝐿 1 + 𝐿 2 L1+L2 형태로 결합된 거리 계산을 수행합니다. cv2.DIST_FAIR

공정 거리(Fair distance)는 두 점 사이의 거리 계산에서 좀 더 균형 잡힌 측정을 제공합니다. 이 방법은 특히 노이즈가 있는 이미지에서 안정적인 거리 계산을 위해 사용될 수 있습니다. cv2.DIST_WELSCH

웰치 거리(Welsch distance)는 두 점 사이의 거리를 계산할 때, 거리가 커질수록 증가율이 감소하는 방식입니다. 따라서 극단적인 값(즉, 먼 거리)에 덜 민감한 방식으로 계산됩니다. cv2.DIST_HUBER

후버 거리(Huber distance)는 일반적인 유클리드 거리와 절대 거리(L1 거리) 사이에서 선택적인 거리 측정을 제공합니다. 작은 거리에서는 유클리드 거리처럼 계산되지만, 큰 거리에서는 절대 거리처럼 계산됩니다. 이는 노이즈에 강한 특성을 가집니다.

요약

L1(맨해튼 거리): 수직 및 수평 거리의 합을 계산. L2(유클리드 거리): 두 점 사이의 직선 거리를 계산. C(체비쇼프 거리): 두 점 사이의 축 방향 최대 차이를 계산. 기타: 특정 상황에서 노이즈에 강한 특성을 갖거나 거리를 평탄하게 계산하는 방식들이 존재.

 

opencv강좌_07.ipynb
3.94MB
DR-01408_박성호_컴퓨터비전_12차시.pdf
11.11MB

'두산 로보틱스 부트캠프 ROKEY > Computer Vision 교육' 카테고리의 다른 글

ep.40 퍼셉트론, 신경망  (0) 2024.09.02
ep.39 OpenCV5  (0) 2024.08.30
ep.37 OpenCV3  (0) 2024.08.28
ep.36 OpenCV2  (0) 2024.08.27
ep.35 OpenCV1  (0) 2024.08.26