OpenCV 4로 배우는 컴퓨터 비전과 머신 러닝/ 레이블링과 외곽선 검출

레이블링

레이블링의 이해

  • 이진화를 수행하면 주요 객체와 배경 영역을 구분할 수 있다. 배경과 객체를 구분하였다면 다시 각각의 객체를 구분하고 분석하는 작업이 필요한데 이때 사용할 수 있는 기법이 레이블링(labeling)이다.
  • 레이블링은 영상 내에 존재하는 객체 픽셀 집합에 고유 번호를 매기는 작업으로 연결된 구성 요소 레이블링(connected components labeling)이라고도 한다.
    • 레이블링 기법을 이용하여 각 객체의 위치와 크기 등 정보를 추출하는 작업은 객체 인식을 위한 전처리 과정으로 자주 사용된다.
  • 영상의 레이블링은 일반적으로 이진화된 영상에서 수행된다. 이때 검은색 픽셀은 배경으로 간주하고 흰색 픽셀은 객체로 간주한다. (정확하게는 입력 영상의 픽셀값이 0이면 배경, 0이 아니면 객체로 인식한다.)
    • 하나의 객체는 한 개 이상의 인접한 픽셀로 이루어지며, 하나의 객체를 구성하는 모든 픽셀에는 같은 레이블 번호가 지정된다.
  • 특정 픽셀과 이웃한 픽셀의 연결 관계는 크게 두 가지 방식으로 정의할 수 있다.
    • 첫 번째는 특정 픽셀의 상하좌우로 붙어 있는 픽셀끼리 연결되어 있다고 정의하는 4-방향 연결성(4-way connectivity)이고
    • 다른 하나는 상하좌우 뿐만 아니라 대각선 방향으로 인접한 픽셀도 연결되어 있다고 간주하는 8-방향 연결성(8-way connectivity)이다.

  • 이진 영상에 레이블링을 수행하면 각각의 객체 영역에 고유의 번호가 매겨진 2차원 정수 행렬이 만들어진다.
    • 레이블링에 의해 만들어지는 이러한 2차원 정수 행렬을 레이블 맵(label map)이라한다. 레이블링을 수행하는 알고리즘은 매우 다양하게 존재하지만 모두 같은 형태의 레이블 맵을 생성한다.
  • 작은 크기의 입력 영상에 대해 레이블링을 수행했을 때 만들어지는 레이블 맵이 아래 그림과 같다.
    • (a)는 이진화된 영상이며, (b)는 레이블링을 수행한 후 정수로 구성된 레이블 맵 행렬이 생성된 것이다.

  • OpenCV는 3.0.0 부터 레이블링 함수를 제공하는데, connectedCompoents()가 그것이다.
    • connectedCompoents() 함수는 입력 영상 image에 대해 레이블링을 수행하여 구한 레이블 맵 labels를 반환한다.
    • connectedCompoents() 함수의 입력 image에는 보통 threshold() 또는 adaptiveThreshold() 등 함수를 통해 얻은 이진 영상을 지정한다. 회색이 포함된 그레이스케일 영상을 입력으로 사용할 경우 픽셀 값이 0이 아니면 객체 픽셀로 갖누한다.
    • labels 인자에는 Mat 자료형의 변수 이름을 전달한다.

레이블링 응용

  • 레이블링 수행 후에 각각의 객체 영역이 어느 위치에 어느 크기로 존재하는지 확인할 필요가 있는데, 이를 반복문을 이용하여 직접 구현하는 것은 꽤 번거로운 일이다. 다행히 OpenCV에서는 레이블 맵과 각 객체 영역의 통계 정보를 한번에 반환하는 connectedComponentsWithStats() 함수를 제공해준다.
    • connectedComponentsWithStats() 함수는 connectedComponents() 함수 인자에 stats와 centroids가 추가된 형태이다. 보통 stats와 centroids 인자에는 Mat 자료형 변수를 지정한다.
  • 앞선 8×8 영상에 대해 생성되는 labels, stats, cetroids 행렬은 아래 그림과 같다.
    • (a)는 레이블 맵을 담고 있는 labels 행렬이고, (b)는 CV_32SC1 타입의 stats 행렬이고 (c)는 CV_64FC1 타입의 centroids 행렬이다.
    • stats 행렬의 행 개수는 레이블 개수와 같고, 열 개수는 항상 5이다.
      • stats 행렬의 각 행은 labels 행렬에 나타난 번호에 해당하는 영역을 나타낸다.
      • 첫 번째 행은 배경 영역 정보를 담고 있고, 두 번째 행부터는 1번부터 시작하는 객체 영역에 대한 정보를 담고 있다.
      • stats 행렬의 각 열은 차례대로 특정 영역을 감싸는 바운딩 박스의 x좌표, y좌표, 가로크기, 세로크기, 해당 영역의 픽셀 개수를 담고 있다.
      • centroids 행렬의 행 개수는 레이블과 같고 열 개수는 항상 2이다.
        • centroids 행렬의 각 열은 차례대로 각 영역의 무게중심 x좌표와 y좌표이다.
        • 무게중심 좌표는 해당 객체에 속하는 픽셀의 모든 x, y좌표를 더한 후 픽셀 개수로 나눈 값이 된다.

외곽선 검출

외곽선 검출

  • 객체의 외곽선(contour)은 객체 영역 픽셀 중에서 배경 여역과 인접한 일련의 픽셀을 의미한다.
    • 보통 검은색 배경 안에 있는 흰색 객체 영역에서 가장 최외곽에 있는 픽셀을 찾아 외곽선으로 정의한다.
    • 만약 흰색 객체 영역 안에 검은색 배경 여역인 홀(hole)이 존재한다면 홀을 둘러싸고 있는 객체 픽셀들도 외곽선으로 검출할 수 있다.
    • 즉 객체의 외곽선은 객체 바깥쪽 외곽선과 안쪽 홀 외곽선으로 구분할 수 있다.
  • 객체 하나의 외곽선은 여러 개의 점으로 구성된다. 그러므로 객체 하나의 외곽선 정보는 vector<Point> 타입으로 저장할 수 있다.
    • 또한 하나의 영상에는 여러 개의 객체가 존재할 수 있으므로 영상 하나에서 추출된 전체 객체의 외곽선 정보는 vector<vector<Point>> 타입으로 표현할 수 있다.
  • 외곽선 검출이 어떻게 동작하는지에 대해 아래 그림으로 표현하였다.
    • 아래 그림의 (a)는 원본영상이며, (b)는 외곽선 검출을 수행하여 외곽선 점들을 찾아낸 것이다.
    • 검출된 외곽선 점들의 좌표는 contours 변수에 모두 저장된다. contours 변수로부터 전체 객체 개수를 알고 싶다면 contours.size() 를 확인하면 된다.

  • OpenCV에서 영상 내부 객체들의 외곽선을 검출하는 함수 이름은 findContours()이다. 이 함수는 hierarchy 인자가 있는 형태와 없는 형태 두 가지로 정의되어 있다.
    • findContours() 함수의 입력 영상으로는 보통 threshold() 등 함수에 의해 만들어진 이진 영상을 사용한다.
    • 실제 동작할 때는 입력 영상에서 픽셀 값이 0이 아니면 객체로 간주하여 외곽선을 검출한다.
    • contours 인자에는 검출된 외곽선 좌표 정보가 저장되고 보통 vector<vector<Point>> 타입의 변수를 지정한다.
    • hierarchy 인자에는 검출된 외곽선의 계층 정보가 저장되고, 보통 vector<Vec4i> 타입의 변수를 지정한다. Vec4i는 int 자료형 네 개를 저장할 수 있는 OpenCV 벡터 클래스로 i 번째 외곽선에 대해 hierarchy[i][0]에는 다음 외곽선 번호, hierarchy[i][1]에는 이전 외곽선 번호, hierarchy[i][2]에는 자식 외곽선 번호, hierarchy[i][3]에는 부모 외곽선 번호가 저장된다. 만약 계층 구조에서 해당 외곽선이 존재하지 않으면 -1이 저장된다.
    • findContours() 함수의 mode 인자에는 외곽선을 어떤 방식으로 검출할 것인지를 나타내는 검출 모드를 지정합니다. mode 인자에는 RetrievalModes 열거형 상수 중 하나를 지정할 수 있다.
RetrievalModes 설명
RETR_EXTERNAL 객체 바깥쪽 외곽선만 검색. 계층 구조는 만들지 않는다.
RETR_LIST 객체 바깥쪽과 안쪽 외곽선을 모두 검색. 계층 구조는 만들지 않는다.
RETR_CCOMP 모든 외곽선을 검색하고 2단계 계층 구조를 구성
RETR_TREE 모든 외곽선을 검색하고 전체 계층 구조를 구성
  • findContours() 함수의 method 인자에는 검출된 외곽선 점들의 좌표를 근사화하는 방법을 지정할 수 있다. method 인자에 지정할 수 있는 ContourApproximationModes 열거형 상수는 아래 표와 같다.
    • 저장되는 외곽선 점의 개수를 줄이고 싶다면 CHAIN_APPROX_SIMPLE 상수를 사용하면 유리하다.
    • CHAIN_APPROX_TC89_L1 또는 CHAIN_APPROX_TC89_KCOS 방식은 점의 개수는 많이 줄어들지만 외곽선 모양에 변화가 생기므로 주의해야 한다.
ContoursApporximationModes 설명
CHAIN_APPROX_NONE 모든 외곽선 점들의 좌표를 저장
CHAIN_APPROX_SIMPLE 외곽선 중에서 수평선, 수직선, 대각선 성분은 끝점만 저장
CHAIN_APPROX_TC89_L1 Teh & Chin L1 근사화를 적용
CHAIN_APPROX_TC89_KOCS Teh & Chin k cos 근사화를 적용
  • 외곽선 계층 구조에 대한 예시는 아래 그림과 같다.
    • 아래 그림의 외곽선 계층 구조는 외곿너의 포함 관계에 의해 결정된다.
    • 즉, 0번 외곽선 안에는 1, 2, 3번 홀 외곽선이 있으므로 0번은 1, 2, 3번의 부모고 1, 2, 3은 0의 자식이다. 한편 4번은 0번과 포함 관계가 없이 대등하므로 이전 외곽선 또는 다음 외곽선의 관계를 갖는다.

  • findContours() 함수에서 외곽선 검출 모드를 어떻게 지정하는지에 따라 검출되는 외곽선과 계층 구조가 서로 달라진다. 4가지 외각선 검출 모드에 따른 외곽선 검출 결과와 계층 구조는 아래 그림과 같다.
    • 아래 그림의 숫자는 위 그림의 외곽선 번호를 나타내며, 각 외곽선 번호 사이에 연결된 화살표는 계층 구조를 나타낸다.
    • 화살표가 오른쪽 외곽선 번호를 가리키면 다음 외곽선을 나타내고, 화살표가 왼쪽 외곽선 번호를 가리키면 이전 외곽선을 가리킨다. 화살표가 아래쪽을 가리키면 자식, 위쪽을 가리키면 부모 외곽선을 나타낸다.

  • findContours() 함수에서 RETR_EXTERNAL 외곽선 검출 모드를 사용하면 흰색 객체의 바깥쪽 외곽선만 검출하고 객체 내부의 홀 외곽선은 검출되지 않는다. 때문에 0번과 4번 외곽선만 검출된다.
    • RETR_LIST 검출 모드를 사용하면 바깥쪽과 안쪽 홀 외곽선을 모두 검출하지만 부모/자식 계층 정보는 생성되지 않는다.
    • RETR_CCOMP 검출 모드를 사용하면 모든 흰색 객체의 바깥쪽 외곽선을 먼저 검출하고, 각 객체 안의 홀 외곽선을 자식 외곽선으로 설정한다. 그러므로 RETR_CCOMP 모드에서는 상하 계층이 최대 두 개 층으로만 구성된다.
      • 만일 흰색 객체에 여러 홀이 존재하는 경우 그 중 하나만 자식 외곽선으로 설정된다. 그리고 각 홀 외곽선은 객체 바깥쪽 외곽선을 모두 부모 외곽선으로 설정한다.
    • RETR_TREE 검출 모드를 사용하면 외곽선 전체의 계층 구조를 생성한다.
  • findContours() 함수로 검출한 외곽선 정보를 이용하여 영상 위에 외곽선을 그리고 싶다면 drawContours() 함수를 사용할 수 있다.
    • drawContours() 함수는 findContours() 함수로 얻은 외곽선 정보를 이용하여 영상에 외곽선을 그린다. 전체 외곽선을 한번에 그릴 수도 있고, 특정 번호의 외곽선을 선택하여 그릴 수도 있고, 외곽선 계층 정보를 함께 지정할 경우 자식 외곽선도 함께 그릴 수 있다.

외곽선 처리 함수

  • 주어진 외곽선 점들을 감싸는 가장 작은 크기의 사각형, 즉 바운딩 박스를 구하고 싶다면 boundingRect() 함수를 사용한다.
    • 특정 객체의 바운딩 박스는 connectComponentsWithStats() 함수를 이용해서도 구할 수 있다. 다만 이미 외곽선 정보를 갖고 있는 경우에는 boundingRect() 함수를 이용하는 것이 효율적이다.
  • 외곽선 또는 점들을 감싸는 최소 크기의 회전된 사각형을 구하고 싶을 때는 minAreaRect() 함수를 사용한다.
  • 외곽선 또는 점들을 감싸는 최소 크기의 원을 구하고 싶을 때는 minEnclosingCircle() 함수를 사용한다.
  • 위 함수들을 사용한 예시는 아래 그림과 같다.

  • 임의의 곡선을 형성하는 점들의 집합을 갖고 있을 때, 해당 곡선의 길이를 구하고 싶다면 arcLength() 함수를 사용할 수 있다.
    • arcLength() 함수는 입력 곡선의 길이를 계산하여 반환한다. 입력 곡선 curve에는 보통 vector<Point> 또는 vector<Point2f> 자료형의 변수를 전달한다.
    • 두 번째 인자인 closed 값이 true이면 입력 곡선의 시작점과 끝점이 연결되어 있는 폐곡선이라고 간주하여 길이를 계산한다.
  • 임의의 외곽선 정보를 가지고 있을 때 외곽선이 감싸는 영역의 면적을 알고 싶다면 contourArea() 함수를 사용한다.
    • 예컨대 (0, 0), (10, 0), (0, 10) 세 점에 의해 결정되는 삼각형이 있을 때, 이 삼각형의 외곽선 길이와 면적을 구하려면 다음과 같이 코드를 작성할 수 있다.
vector<Point> pts = { Point(0, 0), Point(10, 0), Point(0, 10) };

cout << "len = " << arcLength(pts, true) << endl;
cout << "area = " << contourArea(pts) << endl;
  • OpenCV는 외곽선 또는 곡선을 근사화하는 approxPolyDP() 함수를 제공한다. approxPolyDP() 함수는 주어진 곡선의 형태를 단순화하여 작은 개수의 점으로 구성된 곡선을 생성한다.
    • approxPolyDP() 함수는 더글라스-포이커(Douglas-Peucker) 알고리즘을 사용하여 곡선 또는 다각형을 단순화시킨다.
    • 더글라스-포이커 알고리즘은 입력 외곽선에서 가장 멀리 떨어져 있는 두 점을 찾아 직선으로 연결하고, 해당 직선에서 가장 멀리 떨어져 있는 외곽선 상의 점을 찾아 근사화 점으로 추가한다.
    • 이러한 작업을 반복하다가 새로 추가할 외곽선 상의 점과 근사화에의한 직선과의 수직 거리가 epsilon 인자보다 작으면 근사화를 멈춘다. (epsilon 인자는 보통 입력 외곽선 또는 곡선 길이의 일정 비율로 지정한다. ex) arcLength(curve, true) * 0.02)
    • 아래 그림은 보트 모양의 객체 외곽선에 대해 더글라스-포이커 알고리즘으로 외곽선 근사화를 수행하는 과정을 나타낸 것이다.

[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.