안녕하세요.

 

 

오늘은 scipy.ndimage.label과 skimage.measure.regionprops를 이용해서 이미지 내에서 threshold 이상의 값을 가지는 위치를 찾는 작업을 해보려고 합니다.

 

 

구글링 해보니 한국어로 된 자료가 검색이 잘 안되어서, 작성해두면 많은 분들께 도움이 되지 않을까 싶어 작성하게 되었습니다.

 

예시 코드들을 첨부해보면서, 하나하나 같이 익혀보겠습니다.

 

그럼 시작해보겠습니다.

 

 

 

이미지 내에서 특정 값 이상을 가지는 위치를 어떻게 하면 찾을 수 있을까?

 

 

 

먼저, 필요한 library를 import 해줍니다.

 

import numpy as np
import scipy
from scipy.ndimage import label
from skimage.measure import regionprops

 

저는 이해를 돕기 위해, 임의로 numpy array를 생성하여 설명을 드리겠습니다.

 

원리만 이해한다면, 일반적으로는 제가 설명드리는 예시보다 훨씬 더 큰 이미지 내에서도 동일한 작업을 수행하실 수 있을 겁니다.

 

(혹시나 pytorch나 tensor flow 등 deep learning framework를 사용하신다면, 대부분 .numpy()를 통해 tensor 형태의 데이터를 numpy array로 전환하실 수 있습니다.)

 

이해를 돕기 위해, 간단하게 (6, 6) 짜리 numpy array를 생성해줍니다.

 

image = np.array([[1, 2, 3, 4, 4, 4],
               [1, 2, 6, 10, 9, 7],
                [2, 3, 11, 15, 16, 7],
               [1, 2, 7, 8, 9, 7],
               [6, 7, 8, 9, 10, 7],
               [4, 4, 4, 4, 4, 4]])

 

아까 import 했던 scipy.ndimage.label을 사용할 시간입니다.

 

제가 하고 싶은 작업은, 해당 image에서 6보다 큰 부위를 찾아내고 싶습니다.

 

따라서, 다음과 같은 코드를 작성해줍니다.

 

labeled, nr_objects = label(image > 6)
print(labeled)

 

여기서 labeled는 입력으로 준 image 중에서 6보다 큰 지점들에 label을 주게 됩니다.

 

print를 해보면 다음과 같이 나오게 됩니다.

 

 

이렇게 하게 되면, labeled 된 지점들만 추출해 위치를 잡을 수 있게 됩니다.

 

그리고 nr_objects의 경우는 1이 됩니다.

 

 

 

이걸 보시고 이런 생각을 하실 수 있을 것 같습니다.

 

만약 특정값보다 큰 지점들이 저렇게 다 연결되어 있는 것이 아니라, 조금씩 조금씩 모여있는 경우는 어떻게 될까?

 

그런 경우도 보여드리겠습니다.

 

먼저, 기존에 사용했던 image의 값을 다음과 같이 변경해줍니다.

 

image = np.array([[1, 2, 3, 7, 7, 7],
               [1, 2, 6, 10, 9, 7],
                [2, 3, 4, 4, 4, 4],
               [1, 2, 7, 8, 9, 7],
               [6, 7, 8, 9, 10, 7],
               [4, 4, 4, 4, 4, 4]])

 

이렇게 설정하고 나서, 다시 label을 찍어보면 다음과 같이 되는 것을 보실 수 있습니다.

 

 

이번에는 오른쪽 상단에 한 구역과 오른쪽 하단에 한 구역으로, 총 두 구역이 잡히게 됩니다.

 

이처럼, 특정 값보다 높은 지역이 모두 연결되는 것이 아니라 분리되어 있을 경우, label이 1, 2,... 등으로 매겨지면서 지점을 찾아주게 됩니다.

 

그리고 이렇게 두 구역이 잡히는 경우, nr_objects는 2가 됩니다. 

 

즉, nr_objects는 구역의 개수를 뜻한다는 것이죠.

 

 

 

자 그럼, 여기까지 진행하면 내가 가지고 있는 이미지에 대해서 특정 값보다 높은 값을 가지는 구역에 대해 labeling 작업이 된 것이죠?

 

다음으로는 그 구간의 좌표와 면적을 구해보겠습니다.

 

아까 구해둔 labeled라는 변수를 이용해서 skimage.measure.regionprops를 이용합니다.

 

# regionprops : Measure properties of labeled image regions.
props = regionprops(labeled)

 

props라는 변수로 regionprops 객체를 저장하였는데요. 

 

이는 다음과 같은 것을 포함합니다.

 

 

RegionProperties의 list이죠. 따라서 이는 type을 찍어보면 list가 됩니다.

 

다음으로는 이 RegionProperties의 property를 사용해서 여러 가지 정보를 얻어보겠습니다.

 

 

 

먼저, 구역이 1개인 기존 케이스에 대해서 적용해보겠습니다.

 

 

props가 list이므로, list 내부에 있는 값들에 대해서 적용하기 위해 for문을 적용해줍니다.

 

b.area는 labeled 된 구역의 pixel 개수를 구해줍니다. 

 

이전 사진에서 확인하실 수 있겠지만, 1로 labeled 된 pixel은 총 16개입니다.

 

 

 

b.bbox_area는 bounding box의 pixel 개수를 구해줍니다.

 

bounding box는 labeled 된 구역을 감싸는 box를 생성하게 되는 것으로, b.area보다는 크거나 같은 값이 나오게 됩니다.

 

 

 

b.image는 bounding box와 같은 사이즈를 가지고 있는 binary region image를 보여줍니다.

 

따라서, False와 True로 표시된 저 값이 실제 bounding box의 크기와 동일하고, bounding box 내에서 labeled 된 데이터와 unlabeled 데이터가 어떻게 구성되어 있는지 확인할 수 있습니다.

 

 

 

b.bbox의 경우는 Bounding box의 정보를 출력해주게 되며, (min_row, min_col, max_row, max_col)의 형태로 출력을 내줍니다. 

 

주의해야 할 사항은, bounding box에 속하는 pixel은 half-open interval [min_row; max_row)와 [min_col; max_col)에 속하게 됩니다.

 

그렇다면, 이 b.bbox를 어떻게 해석해볼 수 있을지 알아봐야겠죠?

 

기본적으로 col은 가로, row는 세로를 뜻하는 것을 기억해주셔야 하고,

 

index는 0부터 시작합니다.

 

 

이전에 labeled 된 데이터를 보여드렸었는데, 이걸 가지고 설명을 드리겠습니다.

 

빨간색row, 파란색col입니다.

 

방금 bounding box는 half-open interval에 속한다는 말씀을 드렸었는데, 먼저 row부터 보겠습니다.

 

row[min_row; max_row) 범위에 속하게 되는데, 해당 image 데이터 기준으로 보자면 bounding box를 그린다고 했을 때 세로 기준으로 1부터 4까지 그려지는 게 맞겠죠? 

 

근데, max_row는 open입니다. 아마 고등학교 수학에서 open(열린 구간)과 close(닫힌 구간)의 개념을 배우셨겠지만, 쉽게 얘기하면 max_row에는 등호가 없습니다.

 

따라서, bounding box 기준에서 봤을 때는 min_row는 1이고, max_row는 5입니다.

 

쉽게 부등호로 표현하자면 1 <=row <5라는 것입니다.

 

 

col도 보겠습니다.

 

마찬가지로 col도 [min_col; max_col) 범위에 속하게 됩니다.

 

해당 image 데이터 기준으로 bounding box는 1부터 5까지 그려지는 게 맞겠죠?

 

그렇다면 bounding box 기준에서 봤을 때 min_col은 1이고, max_col은 6이게 됩니다. 

 

 

 

그래서, 아까 보여드렸던 이 그림에서 b.bbox의 값들이 이렇게 나온 것입니다.

 

 

(1, 1, 5, 6)인 것을 확인하실 수 있죠.

 

 

다음으로는 이 값들을 이용해서 bounding box의 xy starting point와 width, height를 계산해보겠습니다.

 

bounding box를 그리려면 starting point와 width, height가 필요한데, 먼저 b.bbox [1]이 x 좌표의 시작점이 됩니다.

 

그리고 b.bbox [0]이 y 좌표의 시작점이 되지요.

 

 

width는 가로를 의미하므로, max_col과 min_col을 뺀 b.bbox [3] - b.bbox [1]이 되고, 

 

height는 세로를 의미하므로, max_row와 min_row를 뺀 b.bbox [2] - b.bbox [0]이 됩니다.

 

 

이 정보를 이용해서 원하시는 곳에 bounding box를 그리시면 됩니다...!

 

 

아까 label 쪽을 설명하면서 특정값 이상인 구역이 여러 개인 경우에 대해서도 말씀을 드렸는데, for b in props: 코드를 진행하시게 되면 이렇게 여러 개인 경우 bounding box가 여러 개 나오게 됩니다.

 

이를 이용해서 여러 개의 bounding box를 그리시면 되겠습니다.

 

원리는 당연히 동일하므로, 이 부분에 대해서는 별도로 제가 예시를 들어 설명을 해드리지 않아도 될 것 같습니다.

 

 

 

여기까지 해서 scipy.ndimage.label과 skimage.measure.regionprops를 활용해 특정값보다 큰 값을 가지는 이미지의 위치를 잡아내는 방법을 소개해보았습니다.

 

분명 다양한 이미지 task에 사용할 수 있는 방법이라고 생각이 되고, 유용한 자료가 되었으면 좋겠습니다.

 

감사합니다.

 

 

 

 

 

 

 

 

+ Recent posts