관리 메뉴

공부 기록장 💻

[OpenCV/C++] 영상의 구성 요소 레이블링과 레이블 맵 통계 구하기 (Connected Components Labeling of Image) 본문

# Tech Studies/Computer Vision • OpenCV

[OpenCV/C++] 영상의 구성 요소 레이블링과 레이블 맵 통계 구하기 (Connected Components Labeling of Image)

dream_for 2022. 12. 22. 02:02

OpenCV 4로 배우는 영상 처리와 컴퓨터 비전 CH 12. 레이블링과 외곽선 검출  정리

 

레이블링

 

이진화를 수행하면 주요 객체와 배경 영역을 구분할 수 있다. 객체와 배경을 구분하였다면, 이제 다시 각각의 객체를 구분하고 분석하는 작업이 필요하다.

이 때 사용하는 기법이 레이블링(labeling) 기법으로, 이진 영상에서 흰색으로 구분된 각 객체 픽셀 집합에 고유의 번호를 매기는 작업으로 연결된 구성 요소 레이블링(connected componetns labeling) 이라고도 한다. 레이블링 기법을 이용해 각 객체의 위치와 크기, 모양 등 정보를 추출하고 특징을 분석하는 작업은 객체 인식을 위한 전처리 과정으로 자주 사용된다.

 

영상의 레이블링은 일반적으로 이진화된 영상에서 수행되며, 이때 검은색 픽셀은 배경으로, 흰색 픽셀은 객체로 간주한다. 정확하게는 입력 영상의 픽셀 값이 0이면 배경, 0이 아니면 객체 픽셀로 인식한다. 하나의 객체는 한 개 이상의 인접한 픽셀로 이루어지며, 하나의 객체를 구성하는 모든 픽셀에는 같은 레이블 번호가 지정된다.

특정 픽셀과 이웃한 픽셀의 연결 관계는 크게 2가지 방식으로 정의할 수 있는데, 첫번째는 특정 픽셀의 상하좌우로 붙어 있는 픽셀끼리 연결되어 있다고 정의하는 4-방향 연결성(4-way connectivity)이다. 다른 하나는 상하좌우로 연결된 픽셀뿐만 아니라 대각선 방향으로 인접한 픽셀도 연결되어 있다고 간주하는 8-방향 연결성(8-way connectivity)이다. 

 

다음 그림이 두 픽셀 연결 관계를 나타낸 그림이다.

 

 

이진 영상에 레이블링을 수행하면, 각각의 객체 영역에 고유한 번호가 매겨진 2차원 정수 행렬이 만들어진다. 이러한 정수 행렬을 레이블 맵(label map)이라 부른다. 레이블링을 수행하는 알고리즘은 다양하지만 모두 같은 형태의 레이블 맵을 생성한다.

 

다음 그림을 통해 레이블 맵에 대해 이해해보자. 서로 분리된 객체 3개의 흰색 객체 영역을 갖고 있는, 8x8 크기의 이진 영상 (a)에 대하여 레이블링을 수행하면 정수로 구성된 레이블 맵 행렬이 (b)와 같이 생성된다. 입력 영상에서 배경 픽셀(검은색, 픽셀 값 0)은 레이블맵 행렬에서 0으로 설정되고, 각 객체 픽셀 영역에는 고유의 번호가 1부터 매겨진 것을 확인할 수 있다.

 

connectedComponents()

OpenCV 라이브러리는 3.0.0 버전부터 레이블링 함수 connectedComponents() 를 제공하고 있으며, 원형은 다음과 같다. 

threshold() 또는 adaptiveThreshold() 등의 함수로 얻은 이진 영상을 입력 영상 image로 지정하고, 이에 대해 레이블링을 수행하여 구한 레이블 맵 labels를 반환한다. 회색이 포함된 그레이스케일 영상이 입력으로 사용된 경우, 픽셀 값이 0이 아니면 모두 객체 픽셀로 간주한다.  labels 인자에는 Mat자료형의 변수 이름을 전달한다. 

 

레이블링을 수행하는 connectedComponents() 함수

 

위의 예제 그림과 같이, 8x8 크기의 작은 영상을 입력으로 하여 레이블링을 수행하는 예제를 실행해보자.

 

// 8x8 입력 영상에 대한 레이블링 수행
void labeling_basic() {
	uchar data[] = {
		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,0,1,0,0,1,0,
		0,0,1,1,1,1,1,0,
		0,0,0,0,0,0,0,0
	};

	Mat src = Mat(8, 8, CV_8UC1, data) * 255;

	Mat labels;
	int cnt = connectedComponents(src, labels);

	cout << "src:\n" << src << endl;
	cout << "labels:\n" << labels << endl;
	cout << "number of labels: " << cnt << endl;
}

 

0과 1로 이루어진 2차원 행렬 data를 입력 영상 Mat 객체로 변환하여 이를 connectedComponents()의 첫번째 인자로 지정하여 함수를 실행한 후, labels를 출력해보면 아래와 같이 배경 픽셀에 대해서는 0, 그리고 3개의 객체 픽셀에 고유 번호 1,2,3이 각각 매겨진 것을 확인할 수 있다.

그리고 connectedComponents()의 반환 값을 cnt에 받아 출력하면, 배경 영역까지 포함한 영역 개수인 4가 label의 개수로 나타나는 것을 확인할 수 있다. 

 

 


 

레이블 맵 통계 정보 확인하기

 

connectedComponentsWithStats() 함수

 

레이블링을 수행한 후, 레이블 맵과 각 객체 영역의 통계 정보를 한꺼번에 반환하는 함수 connectedComponentsStats() 를 알아보자. 해당 함수는 connecetedComponents() 함수에서 stats와 centroids가 추가된 형태이다. 두 인자 모두 Mat 자료형 변수를 지정하면 된다. 

 

레이블링을 수행함과 동시에 통계 정보를 함께 반환하는 connectedComponentsStats() 함수

 

따라서 다음과 같이 이용해 코드를 작성할 수 있다.

 

Mat labels, stats, centroids;
connectedComponentsWithStats(src, labels, stats, centroids);

 

connectedComponentsWithStats() 함수의 출력 행렬인 stats와 centroids를 분석해보면 다음과 같다.

stats 행렬의 경우, 행의 개수는 레이블의 개수와 같고, 열의 개수는 항상 5이다.

각 행에는 배경과 각 객체의 바운딩 박스 정보(x,y,width,height)과 면적의 정보가 담겨 있다.

centroids 행렬의 경우도 마찬가지로 행의 개수가 레이블의 개수와 같으며, 각 열에는 무게 중심의 x,y좌표를 저장한다.

 

 

위의 예에서, 1번 객체의 무게 중심 계산 과정을 살펴보자.

1번 객체의 stats의 바운딩 박스 정보에 의하면 각각 x,y,가로,세로 정보는 (0,0,4,3) 이고, 객체 픽셀의 개수가 10개 임을 확인할 수 있다. centroids에 의하면 무게 중심은 (1.7, 1.2)이다. 

그리고 무게 중심 좌표는 객체 픽셀의 x좌표와 y좌표를 모두 더한 후 픽셀 개수로 나눈 값이라 할 수 있다.

 

 

배경이 검은색인 키보드 입력 이미지에 대해 레이블링을 수행하고, 객체 영역의 정보를 이용해 bounding box를 표시하는 예제를 실행해 보도록 하자.

 

// 레이블 맵 통계 자료까지 반환하는 함수
void labeling_stats() {
	Mat src = imread("keyboard.bmp", IMREAD_GRAYSCALE);

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

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

	Mat labels, stats, centroids;
	int cnt = connectedComponentsWithStats(bin, labels, stats, centroids); // 레이블 영역의 통계, 무게 중심 좌표 정보

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

	// 각 객체 영역에 바운딩 박스 표시하기
	for (int i = 1;i < cnt;i++) {
		int* p = stats.ptr<int>(i);
		
		if (p[4] < 20) continue; // 객체 픽셀 개수가 20보다 작은 경우 잡음으로 간주하고 무시
		rectangle(dst, Rect(p[0], p[1], p[2], p[3]), Scalar(0, 255, 255)), 2; // x,y,가로,세로 크기
	}

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

	waitKey();
	destroyAllWindows();
}

 

다음과 같이 발견한 객체 영역에 대하여 노란색으로 bounding box를 표시한 것을 확인할 수 있다.

 

 

 

여기서, 정확히 몇 개의 객체 영역을 발견했으며, 각 객체 영역의 고유 번호를 출력해보기 위해 

다음과 같이 코드를 일부 수정해 보았다.

cnt 값을 출력하는 코드를 추가하였고, putText() 함수를 이용해 bounding box 옆에 객체의 고유 번호를 출력하는 코드를 추가하였다.

 

	Mat labels, stats, centroids;
	int cnt = connectedComponentsWithStats(bin, labels, stats, centroids); // 레이블 영역의 통계, 무게 중심 좌표 정보
	cout << "number of labels : " << cnt << endl;


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

	// 각 객체 영역에 바운딩 박스 표시하기
	for (int i = 1;i < cnt;i++) {
		int* p = stats.ptr<int>(i);
		
		rectangle(dst, Rect(p[0], p[1], p[2], p[3]), Scalar(0, 255, 255)), 2; // x,y,가로,세로 크기
		putText(dst, to_string(i), Point(p[0] - 5, p[1] + 5), 5, 1, Scalar(255,255,255));
	}

 

결과는 다음과 같았다.

배경을 포함하여 총 38개의 객체 영역을 검출하였고, 각 키보드의 키의 우측 부분에 빛이 반사되어 약간 흰색을 띄는 잡음까지 레이블로 인식한다는 것을 확인할 수 있었다.

결국 잡음을 고려하여 픽셀의 개수가 일정 수 이상이어야먄 원하는 객체를 검출한 것으로 판정하도록 하는 코드가 꼭 필요함을 깨달을 수 있었다.

 

728x90
반응형
Comments