레이블링(Labeling)
레이블링이란 일반적으로 이진화 된 이미지에서 연속된 픽셀에 대해 고유한 번호를 매기는 작업을 의미 한다. 이진화에 대한 내용은 이전 포스팅에서 확인할 수 있다.
레이블링을 수행하면 객체 단위로 이미지를 분석할 수 있게 된다. 객체의 위치, 크기, ROI 추출, 모양 분석등이 가능해진다. 이 포스팅에서는 기본적인 레이블링의 원리와 OpenCV에서 제공하는 함수에 대해 알아보고 간단한 예제를 포함한다.
픽셀 연결성(Pixel connectivity)
2차원 이미지에서 객체(서로 연결 되어 있는 픽셀)를 결정 하는 것은 크게 두가지 방법으로 나눈다.
- 4방향 연결 (4-connected pixels)
- 8방향 연결 (8-connected pixels)
위의 이미지를 살펴보면, 하나의 픽셀(검정)을 시작으로 인접한 4방향 연결 또는 8방향으로 연결되는 픽셀들(회색)을 하나의 객체로 간주한다. (흰색픽셀은 배경이며, 6-방향 연결도 존재하지만 생략)
레이블링 함수
OpenCV에서는 레이블링을 수행하는 함수를 다음과 같이 제공한다.
- connectedComponents
- connectedComponentsWithStats
int Imgproc.connectedComponents( Mat image, // 8비트 1채널 영상 Mat labels, // 레이블 맵 행렬, 입력영상과 같은 크기 int connectivity, // 픽셀 연결성, 4 또는 8 int ltype // 레이블 타입, CV_32S 또는 CV_16U )
int Imgproc.connectedComponentsWithStats( Mat image, // 8비트 1채널 영상 Mat labels, // 레이블 맵 행렬, 입력영상과 같은 크기 Mat stats, // 각 객체의 바운딩 박스, 객체 면적 Mat centroids, // 각 객체의 무게 중심 위치 정보 int connectivity, // 픽셀 연결성, 4 또는 8 int ltype // 레이블 타입, CV_32S 또는 CV_16U )
connectedComponents 또는 ConnectedComponentsWithtats를 호출 하게 되면, 매개변수를 통해 레이블링 된 정보를 반환하게 된다.
일반적으로 connectedComponentsWithStats가 더 많은 정보를 제공하기 때문에 이 함수를 사용한다.
connectedComponentsWithStats 함수를 수행한 결과를 가시화 한 내용은 다음과 같다.
쌀알 갯수 카운트
쌀알 이미지에 대해 지역이진화와 레이블링을 수행하여, 객체(쌀알)검출을 하고자 한다.
val rows = 4 val columns = 4 // grayscale로 변환 val graySrc = Mat() Imgproc.cvtColor(src, graySrc, Imgproc.COLOR_BGR2GRAY) val width = graySrc.width() val height = graySrc.height() //지역 이진화 for (row in 0 until rows) { for (column in 0 until columns) { val submat = graySrc.submat( height / rows * row, height / rows * (row + 1), width / columns * column, width / columns * (column + 1) ) Imgproc.threshold( submat, submat, 0.0, 255.0, Imgproc.THRESH_BINARY or Imgproc.THRESH_OTSU ) } } // 객체에 대한 정보 저장 할 행렬들 val labels = Mat() val stats = Mat() val centroids = Mat() val count = Imgproc.connectedComponentsWithStats( graySrc, labels, stats, centroids ) // 출력될 이미지 val dst = Mat() // 색상 변환 Imgproc.cvtColor(graySrc, dst, Imgproc.COLOR_GRAY2BGR) // 객체 바운딩 박스 그리기 for (index in 1 until stats.rows()) { val x = stats.row(index).get(0, 0)[0].toInt() val y = stats.row(index).get(0, 1)[0].toInt() val width = stats.row(index).get(0, 2)[0].toInt() val height = stats.row(index).get(0, 3)[0].toInt() Imgproc.rectangle( dst, Rect(x, y, width, height), Scalar(0.0, 0.0, 255.0), 3 ) } // 무게중심 점찍기 for (index in 1 until centroids.rows()) { val centerX = centroids.row(index).get(0,0)[0].toInt() val centerY = centroids.row(index).get(0,1)[0].toInt() Imgproc.circle(dst, Point(centerX.toDouble(), centerY.toDouble()), 5, Scalar(255.0,0.0,0.0),5) } // 쌀알 갯수 및 결과 이미지 출력
지역 이진화를 수행한 뒤 레이블링 한 정보를 토대로 각 쌀알들에 대해 빨간색 바운딩 박스를 그리고 해당 객체에 대한 무게중심점을 파란색으로 그렸다. 검출된 객체(쌀알)는 110개로 나오지만 자세히 살펴보면 레이블링 된 객체가 항상 쌀알은 아니다.
다음의 경우에 검출 된 객체가 쌀알이 아니었다.
- 쌀알 두개가 이어져 있어 하나의 객체로 검출되는 경우
- 쌀알이 아닌 작은 노이즈 픽셀이 검출되는 경우
작은 노이즈 픽셀의 경우 stats의 area를 통해 필터링을 시도 할 수 있었다. 대략 10픽셀 이상의 크기를 가진 객체만 쌀알이라고 임의로 판단해서 카운팅한 결과는 다음과 같다.
쌀알 두개가 붙어 있는 경우는 모폴로지 연산을 통해 다시 객체를 분리 할 수 있다.
0개의 댓글