지난 시간에는 영상 레이블링에 대해 알아봤습니다. 레이블링을 하면서 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;
}
외곽선을 근사화하면 다양한 응용 분야에 적용할 수 있습니다. 자세한 알고리즘 및 구현은 아래 글을 참조하세요.