본문 바로가기
코딩/OpenCV

[c++ opencv] Labeling(라벨링, 레이블링)으로 객체 카운팅하는 방법

by DIYver 2020. 12. 25.

 

본문 목표

영상처리를 하다보면 연속된 픽셀들이 이루는 그룹을 판단해야 할 상황이 있다.

이런 경우 대상에 이름을 붙여주는 작업이 라벨링 또는 레이블링이라 한다.

(라벨링은 한국식 콩글리쉬인듯, 영문권에서는 레이블링 이라고 한다.)

Opnecv를 사용하지 않고도 레이블링을 할 수 있지만, 원리를 알아보고 그 사용법을 알아보도록 한다.

 

 

키워드 : 라벨링, 레이블링, labeling, connectedComponents(), connectedComponentsWithStats()

 

 

 

 

레이블링( labeling ) 이란?

인접한 같은 값을 갖는 픽셀끼리 하나의 그룹으로 묶어주는 작업이다.

쉽게 말해서 이진화 이미지에서 경계를 이룬 영역에 이름(숫자)을 부여하는 작업이다.

 

 

 

왜 레이블링이 필요한가?

영상인식 과 영상처리에서 가장 중요한 것은 노이즈를 없애는 것이다.

노이즈는 내가 찾고자 하는 대상을 제외한 모든것이 노이즈인 것이다.

 

예를 들어서 자율주행 시스템에 사용할 차선 검출 알고리즘을 개발했는데,
여러 차선이 동시에 보이는 경우 우회전 구간에서 직진으로 인식할 수 있다.

이 상황에서 레이블링을 적용하면, 내가 검출한 차선의 정보가 같은 객체에서 나온 것인지 판단하므로 오작동을 차단할 수 있다.

 

또 다른 상황으로 보면, 객체를 묶어주므로 RoI(Region of Interest, 관심영역)를 효과적으로 잡아줄 수 있는 것이다.

고정된 영역에서 RoI 를 사용하는 것이 아니라, 움직이는 상황에서 RoI 역시 대상을 따라가며 잡아주어 빠른 연산시간과 정확도를 사용자에게 선사해준다.

 

 

 

 

레이블링의 원리는 이미지에서 픽셀들의 연속성을 따지는 것에서 시작한다.

Image Topology 분야이다.

 

이전 강의들을 봤다면 연속의 방향성을 기억해내어야 한다.

4-neighbors 와 8-neighbors 가 있다.

 

다시 한번 짚고 넘어 가보자.

위의 사진 자료에서 보면 알겠지만, 4방향성은 중심 p에서 상하좌우만 연속성을 따진다.

8방향성은 중심 p에서 대각방향까지 연속성을 따지는 것이다.

 

 

연속성을 살펴봤으므로, 이제 살펴봐야하는 것은 방향성이다.

이미지의 픽셀 하나하나마다 조사를 해야하기 때문에, 언제나 동일한 규칙을 가지고 모든 픽셀에 적용해야 한다.

보통은 좌에서 우로, 위에서 아래 방향으로 조사를 시작한다.

 

 

 

이제 자료를 보면서 이해를 해보자.

4-방향성을 조사해볼 것이다.

위와 같은 이미지가 있다고 하자.

 

 

 

제일 위의 행에서 왼쪽부터 조사가 들어간다.

제일 처음 만난 픽셀 데이터에 숫자 1을 부여한다.

1행에는 데이터가 한 개만 있으므로 2행으로 넘어간다.

 

 

2행의 1열에 데이터가 존재한다.

2행 1열의 데이터를 기준으로 방향성을 조사해본다.

위에 픽셀(1행 1열)은 비어있고, 왼쪽 픽셀(2행 0열)은 존재하지 않는다.

따라서 새로운 값인 2를 부여해준다.

그리고 그 다음 열로 중심 픽셀을 설정한다.

 

 

2행 2열 에도 데이터가 존재한다. 

이름을 붙이기 전에 인접 픽셀을 방향 원칙에 맞게 조사해야한다.

일단 위에서 아래 원칙이기에, 바로 위의 픽셀인 1행 2열 데이터를 조사한다.

이미 이름을 붙였던 1을 확인했다.

여기서 끝이 아니라, 왼쪽 픽셀인 2행 1열 데이터도 조사한다.

이번에도 이름을 붙였던 2를 확인했다.

 

이런 경우 낮은 넘버를 사용하는데, 1로 이름을 붙이던 2를 붙이던 딱히 상관은 없다. 

왜냐하면 이 과정에서 우리는 1과 2를 동일하게 취급해야 하기 때문이다.

나중에 데이터 값이 2라면 모두 1로 바꿔줄 것이다.

 

여기까지 하고 계속 또 오른쪽으로 중심 픽셀 위치를 이동시켜본다.

 

 

2행 4열에 또 데이터가 존재하므로, 주변 픽셀 조사를 시작한다.

다행히도 위와 왼쪽 픽셀에 데이터가 존재하지 않으므로, 새로운 넘버 3을 부여한다.

 

이과정으로 3행도 조사를 시작한다.

 

 

 

3행을 조사해본 결과 3행 3열에 데이터가 존재하고, 

위와 왼쪽에 데이터가 없으므로 새로운 넘버 4를 부여해준다.

 

3행 4열에도 데이터가 존재하면서 인접 픽셀에 데이터가 존재하므로 낮은 값인 3을 부여해준다.

이 과정에서 이미 3과 4는 같은 객체라고 판단을 해줘야한다.

 

이 과정으로 4행도 조사를 시작한다.

 

 

 

 

조사를 마치고보니 위와같은 데이터 맵이 생성되었다.

 

이 데이터를 한번 더 가공해야하는데, 1번과 2번이 같고, 3번과 4번, 5번이 같으므로 다시 이름을 부여해준다.

 

그러면 위와같은 최종 레이블링 맵이 생성된다.

4 방향성을 따져서 조사했으므로 위의 데이터에서는 분리된 객체가 2개 존재한다는 것을 알 수 있다.

 

 

 

 

 

 

8방향성 역시 마찬가지이다.

다만 방향성에서 추가해야하는 것이 있다.

바로 좌상단과 우상단 픽셀을 추가적으로 조사해야 한다는 것이다.

 

 

 

사실 이 과정을 코드로 직접 구현해도 금방 구현할 수 있을 것이다.

이중 반복문을 이용해서 이미지의 모든 픽셀에 대해서 조사를 하면 되는 것이라

처음 규칙만 잘 잡아준다면 조건문을 간단 명료하게 쓸 수 있을 것이고,

그것이 이 알고리즘의 전부인 것이다.

 

 

 

 

코드를 못 짜는데 쉽게 사용하고 싶은 분들에게는 다행히도 OpenCV에서 기본함수로 제공해주고 있으므로,

굳이 힘들게 코드를 짜지 않고도 이 원리를 이용할 수 있다.

이제부터 그 함수를 알아보도록 하자.

 

 

 

 

알아볼 함수 원형

- 레이블링 ( connectedComponentsWithStats )

	Mat img = imread("bacteria.tif");

	Mat img_gray;
	cvtColor(img, img_gray, COLOR_BGR2GRAY);

	Mat img_threshold;
	threshold(img_gray, img_threshold, 100, 255, THRESH_BINARY_INV);

	Mat img_labels, stats, centroids;
	int numOfLables = connectedComponentsWithStats(img_threshold, img_labels, stats, centroids, 8, CV_32S);

 

connectedComponentsWithStats( src, labelimg_img, stats, centroid, connected_type, CV_32S ) 

 

  ○ src : 라벨링 할 원본 이미지(이진화 이미지 입력해야함, Object가 흰색이어야 함

  

  ○ labeling_img : 라벨링이 결과 데이터가 저장될 이미지

 

  ○ stats : 객체의 정보가 저장됨, 5열의 행렬

1열에는 객체를 사각형으로 봤을 때, 좌상단 끝의 x좌표가 저장됨

2열에는 객체를 사각형으로 봤을 때, 좌상단 끝의 y좌표가 저장됨

3열에는 객체를 사각형으로 봤을 때, 너비를 저장함(가로크기)

4열에는 객체를 사각형으로 봤을 때, 높이를 저장함(세로크기)

5열에는 객체를 사각형으로 봤을 때, 면적을 저장함(총 픽셀 수)

 

  ○ centroid : 객체의 중심 정보가 저장됨, 2열의 행렬

1열에는 객체의 중심좌표의 x좌표가 저장됨

2열에는 객체의 중심좌표의 y좌표가 저장됨

  

  ○ connected_type : 픽셀의 인접 방향성을 설정

- 4 connected : 상하좌우만 인접할 경우 연결처리 함, 4

- 8 connected : 대각방향으로 인접할 경우 연결처리 함, 8

 

 

 

 

 

 

 

코드 테스트 결과

- CODE

#include <opencv2/opencv.hpp>

#include <windows.h>

using namespace cv;
using namespace std;

int main(int ac, char** av)
{
	Mat img = imread("bacteria.tif");

	Mat img_resize;
	resize(img, img_resize, Size(img.cols * 3, img.rows * 3));

	Mat img_gray;
	cvtColor(img_resize, img_gray, COLOR_BGR2GRAY);

	Mat img_threshold;
	threshold(img_gray, img_threshold, 100, 255, THRESH_BINARY_INV);

	Mat img_labels, stats, centroids;
	int numOfLables = connectedComponentsWithStats(img_threshold, img_labels, stats, centroids, 8, CV_32S);

	// 레이블링 결과에 사각형 그리고, 넘버 표시하기
	for (int j = 1; j < numOfLables; j++) {
		int area = stats.at<int>(j, CC_STAT_AREA);
		int left = stats.at<int>(j, CC_STAT_LEFT);
		int top = stats.at<int>(j, CC_STAT_TOP);
		int width = stats.at<int>(j, CC_STAT_WIDTH);
		int height = stats.at<int>(j, CC_STAT_HEIGHT);


		rectangle(img_resize, Point(left, top), Point(left + width, top + height),
			Scalar(0, 0, 255), 1);

		putText(img_resize, to_string(j), Point(left + 20, top + 20),FONT_HERSHEY_SIMPLEX, 1, Scalar(255, 0, 0),1);
	}


	imshow("img_resize", img_resize);

	cout << "numOfLables : " << numOfLables - 1 << endl;	// 최종 넘버링에서 1을 빼줘야 함
	waitKey(0);



	return 0;
}

 

- RESULT

 

 

이미지 테두리에 제로패딩이 되어 있어서 완벽한 결과는 아니지만,

어찌되었든 라벨링이 제대로 된 것을 확인할 수 있다.

 

직접 개수를 세어봐도 21개가 나오는데, 사람이 세는 것보다 몇배는 더 빠르게 세어주니 이렇게 좋을 수가 없다.

 

똑같은 사진을 이용하고 싶다면 아래 파일을 이용하면 된다.

위에서 조금 수정을 한다면 erode 기법을 사용해주면 맨위의 박테리아에 제대로된 라벨이 붙지않을까 싶다.

 

bacteria.tif
0.03MB

 

 

 

그리고 코드에서 보면 connectedComponentsWithStats( ) 함수의 값이 레이블링 넘버를 나타내는데,

실제 객체수보다 1이 크므로 

카운팅이 중요하다면 결과값에서 1을 빼고 사용해야 한다.

 

 

 

 

 

 

 

 

 

 

도움이 되었거나, 문제가 있는 경우 댓글로 알려주세요~!

감사의 댓글은 작성자에게 큰 힘이 됩니다 ^^

댓글