본문 바로가기

프로그래밍 강좌/C++ - OpenCV

[OpenCV][C++] 영상 외곽선(contour) 추출 총정리(1) - cv::findContours 윤곽선 경계 컨투어 inRange 검출

지난 시간에는 영상 레이블링에 대해 알아봤습니다. 레이블링을 하면서 object의 크기를 판별하고 크기를 이용하여 10원짜리인지, 100원짜리인지 500원짜리인지를 확인하고 총 얼마가 있는지 계산까지 해 보았습니다.

이번에는 물체의 경계를 이루고 있는 외곽선(윤곽선)을 검출하는 방법에 대해 알아보겠습니다.

외곽선을 추출하는 findContours()와 외곽선을 그려주는 drawContours() 함수가 있습니다. 입력 영상은 8bit 1 채널 이진 영상(inRange(), threshold(), adaptiveThreshold(), canny() 등)을 사용하면 됩니다.

OpenCV에서는 findContours()가 아래와 같이 두가지 형태로 제공됩니다.

findContours

윤곽선을 검출하는 함수의 원형은 아래와 같습니다.

src
이진 영상(gray-scale도 가능하나 0과 0이 아닌 픽셀(전경)로 구분하여 수행)
contours
윤곽선 정보
hierarchy
윤곽선 계층 정보
mode
윤곽선 검출 모드
method
윤곽선 근사 알고리즘
offset
윤곽선 점 좌표의 offset(이동 변위)

 

1. src

src 8bit 1ch 영상이 가능하나 0과 0이 아닌 픽셀로 구분하여 윤곽선 검출을 수행합니다. 그래서 보통은 이진화 영상을 입력합니다.

 

2. contours

윤곽선 여러 개의 점으로 구성됩니다. 따라서 하나의 윤곽선 정보는 std::vector<cv::Point> 타입으로 저장할 수 있고 이러한 윤곽선이 여러개 존재할 수 있기 때문에 std::vector<std::vector<cv::Point>> 타입으로 contours 정보를 저장하게 됩니다.

 

3. hierarchy

hierarchy 에는 윤곽선의 계층 정보가 저장되고 보통 std::vector<cv::Vec4i> 타입의 변수로 지정합니다. cv::Vec4i는 int타입의 4개 원소로 구성되어 있습니다.

[{동일계층 다음 contour의 index, 없으면 -1}

{동일계층 이전 contour의 index, 없으면 -1}

{자식계층 contour의 index, 없으면 -1}

{부모계층 contour의 index, 없으면 -1}]

로 구성되어 있습니다.

 

4. mode

mode  윤곽선을 어떤 방법으로 검출 할 것인지를 나타내는 검출 방법을 지정합니다.

RETR_EXTERNAL
가장 외곽의 윤곽선만 찾음
RETR_LIST
모든 윤곽선을 찾음
RETR_CCOMP
2레벨 계층구조로 모든 윤곽선을 찾음
1레벨: 가장 외곽,
2레벨: 구멍(만약 구멍 내에 윤곽선이 또 있으면 1레벨로 설정)
RETR_TREE
모든 윤곽선을 계층적 트리 형태로 찾음
RETR_FLOODFILL
에러 발생. 공식 문서 설명도 없고, 동작도 안함(4.7버전 확인 결과)

 

RETR_EXTERNAL 가장 바깥쪽 윤곽선만 추출하고 나머지 모든 윤곽선을 추출합니다. 따라서 윤곽선을 그리게 되면 아래와 같이 같은 결과를 가지고 있습니다.

하지만 hierarchy가 다르기 때문에 hierarchy 를 이용하여 각 윤곽선간 포함 관계를 알아낼 수 있습니다.

 mode 에 따른 hierarchy를 아래에 나타내었습니다.

[EXTERNAL]  [LIST] hierarchy값 나오긴 하지만 의미 없는 데이터 입니다.

[TREE]의 경우 실제 윤곽선간 구조가 눈에 보입니다.

4가지 mode 중에서 가장 유용하게 사용되는 것은 [EXTERNAL]과 [TREE] 입니다.

 

5. method

method는 검출된 윤곽선 정보 좌표들을 어떻게 근사화 할 것인지를 지정합니다.

 
CHAIN_APPROX_NONE
모든 윤곽선 좌표 저장
CHAIN_APPROX_SIMPLE
윤곽선 중 직선 성분은 끝점만 간소화 하여 저장
CHAIN_APPROX_TC89_L1
Teh-Chin L1 근사화 적용하여 저장, 윤곽선 변형 가능
CHAIN_APPROX_TC89_KCOS
Teh-Chin k cos 근사화 적용하여 저장, 윤곽선 변형 가능

 

[NONE] 일 경우에는 모든 point를 저장하기 때문에 붉은 점으로 표시가 되었고,

[SIMPLE]일 경우에는 직선의 경우 양 끝점을 저장하기 때문에 저장 Point 개수가 약간 줄어 듭니다.

[TC89_L1] 또는 [TC89_KCOS] 일 경우 Point 개수가 더 줄어들기는 하지만 윤곽선의 형태가 약간 변형되기 때문에 아래 그림에서 사각형의 픽셀이 휘어지거나 별표 모양의 픽셀에서 별표 안쪽으로 근사화되기도 합니다.

아래 소스 코드는 위의 영상을 그릴 때 사용했던 소스 입니다.

#include <iostream>
#include "opencv2/opencv.hpp"

int main()
{
    cv::Mat src = cv::imread("../contour.bmp", cv::IMREAD_GRAYSCALE);
    if (src.empty())  return 1;

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    
    int mode = cv::RETR_EXTERNAL;    
    int method = cv::CHAIN_APPROX_SIMPLE;
    
    cv::findContours(src, contours, hierarchy, mode, method);

    cv::Mat src_color;
    cv::cvtColor(src, src_color, cv::COLOR_GRAY2BGR);
    
    cv::RNG rng(12345);
    for (int i = 0; i < contours.size(); ++i) {
        cv::drawContours(src_color, contours, i, cv::Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256)), 2);

        for (int j = 0; j < contours[i].size(); ++j) 
            cv::circle(src_color, contours[i][j], 1, cv::Scalar(0, 0, 255), cv::FILLED);
    }
    
    return 0;
}