레이블링과 외곽선 검출

  • 객체 단위 분석

    • (흰색) 객체를 분할하여 특징을 분석

    • 객체 위치 및 크기 정보, ROI 추출, 모양 분석

    • 레이블링 & 외곽선 검출

  • 레이블링(connected component labeling)

    • 서로 연결되어 있는 객체 픽셀에 고유한 번호를 지정(레이블 맵)

    • 영역 기반 모양 분석

    • 레이블맵, 바운딩 박스, 픽셀 개수, 무게 중심 좌표를 반환

  • 외곽선 검출(contour tracing)

    • 각 객체의 외곽선 좌표를 모두 검출

    • 외곽선 기반 모양 분석

    • 다양한 외곽선 처리 함수에서 활용이 가능함 (근사화, 컨벡스헐)

레이블링

  • 동일 객체에 속한 모든 픽셀에 고유한 번호를 매기는 작업
  • 일반적으로 이진 영상에서 수행
  • OpenCV 3.x 버전부터 최신 논문 기반의 레이블링 함수를 제공

  • 픽셀의 연결 관계
    • 4 - 이웃 연결 관계(4 - neighbor connectivity)
      • 상하좌우, 4개 픽셀만 연결된 것으로 취급
    • 8 - 이웃 연결 관계 (8 - neighbor connectivity)
      • 상하좌우, 대각선 방향 8개 픽셀이 모두 연결된 것으로 취급

image

  • 레이블링 알고리즘의 입력과 출력
    • 이진 영상은 CV_8UC1 형태의 행렬이지만 출력 형상은 객체의 개수가 256개 이상이 나올 수 있어 일반적으로 CV_32S의 행렬로 나타내는 것이 보편적임

image

레이블링 함수

int connectedComponents(InputArray image, OutputArray labels, int connectivity =8, int ltype = CV_32S)
  • image : (입력) 8비트 1채널 영상
  • labels : (출력) 레이블링 결과 행렬. 레이블맵. Mat 객체
  • connectivity : 픽셀의 연결 관계 (8 or 4)
  • ltype : 출력 영상 타입 (CV_32S or CV_16S)
  • 반환값 : 객체 개수로 N을 반환하면 0~(N-1) 사이의 레이블이 존재하며, 0은 배경을 의미하므로 실제 객체 개수는 N-1개임

객체 정보를 함께 반환하는 레이블링 함수

int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity =8, int ltype = CV_32S)
  • image : (입력) 8비트 1채널 영상
  • labels : (출력) 레이블링 결과 행렬. 레이블맵. Mat 객체
  • stats : (출력) 각 객체의 바운딩 박스, 픽셀 개수 정보를 담은 행렬 (CV_32SC1, N x 5)
  • centroids : (출력) 각 객체의 무게 중심 위치 정보를 담은 행렬 (CV_64FC1, N x 2)
  • connectivity : 픽셀의 연결 관계 (8 or 4)
  • ltype : 출력 영상 타입 (CV_32S or CV_16S)
  • 반환값 : 객체 개수로 N을 반환하면 0~(N-1) 사이의 레이블이 존재하며, 0은 배경을 의미하므로 실제 객체 개수는 N-1개임

connectedComponentsWithStats 예제 코드

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

using namespace std;
using namespace cv;

int main()
{
	uchar data[] = { // 정수 값 64개의 배열을 선언
		0, 0, 1, 1, 0, 0, 0, 0,
		1, 1, 1, 1, 0, 0, 1, 0,
		1, 1, 1, 1, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 1, 1, 0,
		0, 0, 0, 1, 1, 1, 1, 0,
		0, 0, 1, 1, 0, 0, 1, 0,
		0, 0, 1, 1, 1, 1, 1, 0,
		0, 0, 0, 0, 0, 0, 0, 0,
	};

	Mat src(8, 8, CV_8UC1, data); // 8 x 8 행렬을 선언, unsigned character channel 1개

#if 0
	Mat labels;
	int num_labels = connectedComponents(src, labels);

	cout << "src:\n" << src << endl;
	cout << "number of labels: " << num_labels << endl;
	cout << "labels:\n" << labels << endl;
#else
	Mat labels, stats, centroids;
	int num_labels = connectedComponentsWithStats(src, labels, stats, centroids);

	cout << "src:\n" << src << endl;
	cout << "number of labels: " << num_labels << endl;
	cout << "labels:\n" << labels << endl;
	cout << "stats:\n" << stats << endl;
	cout << "centroids:\n" << centroids << endl;
#endif
}
  • src : 8 x 8 행렬 원본
  • numbers of label : 배경, 객체 3개 총 4개
  • labels : 8 x 8 행렬에 label을 붙임
  • stats : (x, y, width, height, label 면적) * numbers of label(4)
  • centroids : (무게 중심의 x 좌표, 무게 중심의 y 좌표) * numbers of label(4)

image

image

키보드 영역에서 문자 영역 분할 예제

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

using namespace std;
using namespace cv;

int main(void)
{
	Mat src = imread("keyboard.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat src_bin;
	threshold(src, src_bin, 0, 255, THRESH_BINARY | THRESH_OTSU); 

	Mat labels, stats, centroids;
	int cnt = connectedComponentsWithStats(src_bin, labels, stats, centroids);
	cout << cnt << endl;

	Mat dst;
	cvtColor(src, dst, COLOR_GRAY2BGR);

	for (int i = 1; i < cnt; i++) {
		int* p = stats.ptr<int>(i); 
		// stats 행렬에서 i번째 행의 정보 5개 (x, y, width, height, label 면적)를 p로 받음 

		if (p[4] < 20) continue; // 레이블의 픽셀 개수가 너무 작으면 그림을 그리지 않게함

		rectangle(dst, Rect(p[0], p[1], p[2], p[3]), Scalar(0, 255, 255)); 
		// 충분히 큰 객체에 대해서만 사각형을 그림
		
		// Mat crop = src(Rect(p[0], p[1], p[2], p[3])); // 표시한 네모를 crop 해서 crop 객체에 저장
		// imshow("crop", crop);
		// waitKey();
	}

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}

image

  • 레이블의 픽셀 개수를 20개 이하로 설정하지 않았을 경우
    • 지저분한 노이즈들을 잡아내기 때문에, 20개 이하인 레이블은 표시하지 않게 함

image

외곽선 검출

  • 객체의 외곽선 좌표를 모두 추출하는 작업
  • 바깥쪽 & 안쪽(홀) 외곽선

image

외곽선 좌표를 표현하려면?

  • 외곽선 점 하나 : Point p;

image

  • 객체 하나의 외곽선 : vector contour;

image

  • 여러 객체의 외곽선 : vector<vector> contours;

image

image

외곽선 검출 함수

void findContours(InputOutputArray image, OutputArrayOfArrays contours, OutputArray hierachy, int mode, int method, Point offset = Point());
void findContours(InputOutputArray image, OutputArrayOfArrays contours, int mode, int method, Point offset = Point());
  • image : (입력) 보통 흑백으로 구성된 이진 영상. non-zero 픽셀을 객체로 간주
  • contours : (출력) 검출된 외곽선 정보. vector<vector> 자료형
  • hierarchy : 외곽선 계층 정보. vector 자료형 (4 개의 정수 값을 담을 수 있는 자료형)
    • 4개의 정수 값이 차례대로 next, prev, child, parent 외곽선 인덱스를 가리킴
    • 해당 외곽선이 없으면 -1 지정
  • mode : 외곽선 검출 모드
    • RETR_EXTERNAL : 바깥쪽에 있는 외곽선만 검출하고, 안쪽에 있는 외곽선은 검출하지 않음(부모 - 자식 간의 계층 존재가 없음)
    • RETR_LIST : 모든 외곽선을 검출함(부모 - 자식 간의 계층 존재가 없음)
    • RETR_CCOMP : 2단계 계층 정보를 가짐
    • RETR_TREE : 전체 계층 구조를 표현

image

  • method : 외곽선 근사화 방법
    • CHAIN_APPROX_NONE : 근사화 없음
    • CHAIN_APPROX_SIMPLE : 수직선, 수평선, 대각선에 대해 끝 점만 저장
  • offset : 좌표 값 이동 오프셋

OpenCV 외곽선 그리기 함수

void drawContours (InputOutputArray iamge, InputArrayOfArrays contours, int contourIdx, const Scalar& color, int thickness = 1, int lineType = Line_8, InputArray hierarchy = noArray(), int maxLevel = INT_MAX, Point offset = Point());
  • image : 입력 영상
  • contours : 외곽선 정보 (vector<vector> 자료형)
  • contourIdx : 외곽선 인덱스. contourIdx < 0이면 모두 그림
  • color : 외곽선 색상
  • thickness : 외곽선 두께. thickness < 0 이면 내부를 채움
  • lineType : 선 종류 (Line_4, Line_8, Line_AA 중 선택)
  • hierarchy : findContours() 함수에서 구한 외곽선 계층 정보. 계층 정보를 사용하지 않으면 noArray() 또는 Mat() 지정
  • maxLevel : 그리기를 수행할 때 최대 외곽선 레벨. maxLevel = 0이면 contourIdx 외곽선만 그림
  • offset : 좌표 값 이동 오프셋

계층 정보를 사용하는 외곽선 검출 예제

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

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("contours.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	vector<vector<Point>> contours;
	vector<Vec4i> hierarchy;
	findContours(src, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_NONE); // 외곽선 검출을 수행
	// 입력 영상 src가 이진화 형태로 되어있어서 바로 입력 영상으로 줬음
	// contours 는 vector<vector<Point>> 타입의 형태를 지정
	// hierarchy 는 	vector<Vec4i> 로 지정
	// RETR_CCOMP 를 입력하여 부모 자식 관계를 2단계로만..
	// CHAIN_APPROX_NONE 근사화 작업은 따로 하지 않음
	

	Mat dst = Mat::zeros(src.rows, src.cols, CV_8UC3);
	for (int idx = 0; idx >= 0; idx = hierarchy[idx][0]) { // 외곽선을 실제로 그려서 화면에 보여주는 코드
	// 0번 외곽선부터 시작해서 외곽선이 0번보다 클 동안 for 문이 계속 돌음
	// 현재 인덱스에 해당하는 외곽선의 [0]인 next의 정보로 idx 업데이트

		Scalar color(rand() & 255, rand() & 255, rand() & 255);
		drawContours(dst, contours, idx, color, 2, LINE_8, hierarchy);
		// 그리고 특정 색으로 drawContours 실행
	}

	imshow("src", src);
	imshow("dst", dst);
	waitKey();
}
  • 같은 종류의 hierarchy는 같은 색으로 나타냄

image

  • drawContours()에서 hierarchy를 입력하지 않았을 경우

image

  • ​ findContours(src, contours, hierarchy, RETR_TREE, CHAIN_APPROX_NONE);

image

계층 정보를 사용하지 않는 외곽선 검출 예제

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

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("milkdrop.bmp", IMREAD_GRAYSCALE);

	if (src.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat src_bin;
	threshold(src, src_bin, 0, 255, THRESH_BINARY | THRESH_OTSU);

	vector<vector<Point>> contours;
	findContours(src_bin, contours, RETR_LIST, CHAIN_APPROX_NONE); // 모든 외곽선을 다 검출

	Mat dst = Mat::zeros(src.rows, src.cols, CV_8UC3);
	for (unsigned i = 0; i < contours.size(); i++) { 
	// RETR_LIST를 사용했으므로 검출된 외곽선을 0번 부터 contours.size 까지 차례대로 그리면 모든 외곽선을 그릴 수 있음
		Scalar color(rand() & 255, rand() & 255, rand() & 255);
		drawContours(dst, contours, i, color, 1, LINE_8);
	}

	imshow("src", src);
	imshow("src_bin", src_bin);
	imshow("dst", dst);
	waitKey();
}
  • 객체 밖의 잡음이나 객체 안에 hole들도 외곽선이 검출됨

image

OpenCV 외곽선 관련 함수

image

외곽선 길이 구하기

double arcLength(InputArray curve, bool closed);
  • curve : 외곽선 좌표 (vector 타입)
  • closd : true일 경우 폐곡선으로 간주
  • 반환값 : 외곽선 길이

면적 구하기

double contourArea(InputArray contour, bool oriented = false);
  • contour : 외곽선 좌표 (vector 타입, vector )
  • oriented : true일 경우 외곽선 진행 방향에 따라 부호 있는 면적을 반환
  • 반환값 : 외곽선으로 구성된 영역의 면적

  • 객체의 픽셀 개수를 세는 것과 면적 구하는 값은 다름

바운딩 박스(Bounding Box) 구하기

Rect boundingRect(InputArray points);
  • points : 외곽선 좌표
  • 반환값 : 외곽선을 외접하여 둘러싸는 가장 작은 직사각형

바운딩 서클(Bounding Circle) 구하기

void minEnclosingCircle(InputArray points, Point2f& center, float& radius);
  • points : 외곽선 좌표
  • center : (출력) 바운딩 서클 중심 좌표
  • radius : (출력) 바운딩 서클 반지름

컨벡스(볼록 다각형) 검사

bool isCountourConvex(InputArray contour)
  • contour : 입력 곡선 좌표
  • 반환값 : 컨벡스일경우 true 아닐 경우 false

image

외곽선 근사화

void approxPolyDP(InputArray curve, OutputArray approxCurve, double epsilon, bool closed)
  • curve : 입력 곡선(vector or Mat)

  • approxCurve : (출력)더글라스-포이커 알고리즘으로 근사화된 외곽선

  • double epsilon : 근사화 정밀도 조절(입력 곡선과 근사화 곡선 간의 최대 거리)

    e.g. arcLength(curve) * 0.02

  • closed : true

  • 다음과 같은 방법으로 진행됨

image

image

image

image

image

image

image

image

image

image

image

image

image

image

다각형 검출 프로그램

  • 다양한 다각형 객체 영상에서 삼각형, 사각형, 원 찾기

구현 순서

  1. 이진화

  2. 외곽선 찾기

  3. 외곽선 근사화
  4. 너무 작은 객체와 컨벡스가 아닌 객체는 제외
  5. 꼭짓점 개수 확인
    • 삼각형, 사각형 검출
    • 원 판별

원 판별 하기

  • 정해진 외곽선 길이에 대한 넓이 비율이 가장 큰 형태가 원
    • 도형의 넓이(A)와 외곽선 길이(P)의 비율을 검사
    • 어떤 면적/외곽선길이의 제곱의 값이 1/4pi 일 경우 원에 가까운 도형으로 판단할 수 있음
    • 면적/외곽선길이의 제곱의 값에 4pi를 곱했을 때 1보다 점점 멀어지면 원이 아닌 형태로 바뀜
    • 반대로 면적/외곽선길이의 제곱의 값에 4pi를 곱했을 때의 값이 1에 가까워질 수록 원으로 판단

image

코드

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

using namespace std;
using namespace cv;

void setLabel(Mat& img, const vector<Point>& pts, const String& label) 
{
	Rect rc = boundingRect(pts); // 특정 좌표들을 둘러싸고 있는 바운딩 박스를 계산
	rectangle(img, rc, Scalar(0, 0, 255), 1); // 바운딩 박스를 빨간색으로 그림을 그림
	putText(img, label, rc.tl(), FONT_HERSHEY_PLAIN, 1, Scalar(0, 0, 255));
	// rc.tl()은 top left의 약자로 사각형에서 좌측 상단의 좌표를 반환함
	// 사각형 좌측 상단에 label 텍스트를 삽입
}

int main()
{
	Mat img = imread("polygon.bmp", IMREAD_COLOR);

	if (img.empty()) {
		cerr << "Image load failed!" << endl;
		return -1;
	}

	Mat gray;
	cvtColor(img, gray, COLOR_BGR2GRAY); // 그레이스케일로 변환하고 gray에 저장

	Mat bin;
	threshold(gray, bin, 0, 255, THRESH_BINARY_INV | THRESH_OTSU); 
	// Otsu 방법으로 그레이스케일 영상을 INV 이진영상으로 변환하고 bin에 저장
	// 각각의 도형이 배경보다 진해서 단순하게 이진화하면 배경의 흰색, 도형이 검정색 형태로 이진화됨
	// 그렇게 되면 findContours 함수, connectedComponents에서 오동작을 함

	vector<vector<Point>> contours;
	findContours(bin, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE); 
	// 각각의 객체 안에 Hole이 없으므로 외곽선만 검출하기 위해 RETR_EXTERNAL 설정

	for (vector<Point>& pts : contours) {
		if (contourArea(pts) < 400) // 도형의 면적이 400보다 작으면 검출 x
		// 이진화를 하게되면 외곽선 주변에서 자잘한 크기의 잡음이 발생할 수 있음. 이러한 잡음에 대해서 검출 x
			continue;

		vector<Point> approx;
		approxPolyDP(pts, approx, arcLength(pts, true)*0.02, true); 
		// 각각의 외곽선을 근사화 시킴
		// 근사화가 잘 되면, approx는 꼭지점으로 이루어진 vector<Point> 타입으로 채워짐
		
		if (!isContourConvex(approx))
			continue;

		int vtc = (int)approx.size(); // arrpox에 들어있는 점들의 개수를 파악

		if (vtc == 3) {
			setLabel(img, pts, "TRI"); // 파악한 개수가 3이면 삼각형
		} else if (vtc == 4) { 
			setLabel(img, pts, "RECT"); // 파악한 개수가 4면 사각형
		} else { // 나머지 코드에서는 원을 판별하는 코드
			double len = arcLength(pts, true);
			double area = contourArea(pts);
			double ratio = 4. * CV_PI * area / (len * len);

			if (ratio > 0.85) { // 1에 가까운 값이면 원으로 지정
				setLabel(img, pts, "CIR");
			}
		}
	}

	imshow("img", img);
	waitKey();
}
  • ratio > 0.85

image

  • ration > 0.7 로 변경시
    • 파란색 타원형을 검출하는 것을 확인할 수 있음

image