IDL/Image Processing

Region Growing 기법에 관하여

이상우_idl 2018. 6. 6. 22:32
728x90
반응형

오늘은 이미지 처리 기법의 하나인 Region Growing에 관하여 살펴보고자 합니다. Region Growing을 우리말로 어떻게 표현하는 것이 좋을지 모르겠는데, 저는 주로 "영역 확장"이라고 부릅니다. 물론 표준화된 약속은 아니므로 다르게 표현할 수도 있겠지만, 기법 자체의 특성상 이렇게 부르는 것도 나쁘지 않은 것 같습니다. IDL에는 이러한 기법의 처리를 담당하는 REGION_GROW라는 전용 함수가 있습니다. IDL 도움말에서 이 함수에 관한 내용을 찾아보면, REGION_GROW 함수의 역할인 Region Growing 기법(이하 RG 기법이라고 부르겠습니다)에 관하여 다음과 같이 소개하고 있습니다.


The REGION_GROW function performs region growing for a given region within an N-dimensional array by finding all pixels within the array that are connected neighbors to the region pixels and that fall within provided constraints.


사실 이 기법 자체는 배열이 반드시 2차원이어야 하는 것은 아니고 임의의 차원의 배열에 대하여 모두 적용 가능합니다. 하지만 2차원 배열 형태의 이미지 데이터에 대하여 한정지어 얘기한다면, 이미지 배열 내의 특정한 부분영역으로부터 시작하여 특정한 조건을 만족하는 인접 화소들을 그 주변으로 계속 추적하여 찾아나가는 과정이라고 볼 수 있습니다. 여기서 "특정한 부분영역"은 이미지 내에서 NxM의 형태를 갖는 부분 배열이 되어야 합니다. 그리고 주변으로 계속 추적하는데 있어서는 반드시 "인접한" 화소를 대상으로 합니다. 멀리 떨어진 화소를 찾는 것이 절대 아니라는 점을 유념해야 합니다. 그리고 "특정한 조건"의 구체적 기준은 두 종류가 있습니다. 이와 관련해서는 아래에서 예제를 보면서 살펴보기로 하겠습니다. 그러면 먼저 예제로 사용할 2차원 이미지 데이터를 다음과 같이 불러옵시다.


fname = FILEPATH('muscle.jpg', SUBDIR=['examples', 'data'])

READ_JPEG, fname, data

HELP, data

PRINT, MIN(data), MAX(data)


여기서는 IDL의 설치 디렉토리 내에 기본적으로 제공되는 muscle.jpg라는 파일로부터 652x444 형태의 2차원 배열을 불러왔습니다. 배열 내 화소값은 0~255 범위의 바이트스케일로 되어 있습니다. 이러한 정보는 위에서 HELP 및 PRINT에 의하여 출력된 결과로부터 확인할 수 있습니다. 일단 이 이미지를 다음과 같은 과정에 의하여 원본 그대로 표출해봅시다.


sz = SIZE(data, /DIM)

win = WINDOW(DIMENSIONS=sz, /NO_TOOLBAR)

im = IMAGE(data, MARGIN=0, /CURRENT)


그 모습은 다음 그림과 같습니다.



그러면 지금부터 RG 기법을 적용해봅시다. 이를 위해서는 먼저 기준이 될 부분 영역을 정해야 합니다. 일종의 초기 씨앗(Initial Seed)의 역할을 하는 부분이라고 볼 수 있습니다. 이 부분 영역의 위치와 크기는 다음 그림에서 우측에 보이는 사각형과 같습니다.



그런데 RG 기법을 적용하기 위해서는 REGION_GROW 함수를 사용해야 하는데, IDL 도움말에서 그 문법을 확인해보면 다음과 같습니다.


Result REGION_GROW(ArrayROIPixels [, /ALL_NEIGHBORS] [, /NAN] [, STDDEV_MULTIPLIER=value | THRESHOLD=[min,max]] )


이 문법에 의하면 첫번째 인수는 대상 배열이므로 지금의 예제에서는 원본 이미지 배열인 data가 됩니다. 그리고 두번째 인수로 명시되어 있는 ROIPixels는 초기 씨앗에 해당되는 부분 영역 화소들의 위치 인덱스들이 되어야 합니다. 위의 이미지에서 우측에 표시된 사각형이 바로 씨앗 부분영역의 역할을 하게 되는데, 이 영역은 10x10의 화소 크기를 가지며 그 위치는 X축 방향으로는 550~559이고 Y축 방향으로는 300~309입니다. 크기가 10x10이므로 총 100개의 화소들로 구성됩니다. ROIPixel라는 항목은 결국 이 100개의 화소들의 위치 인덱스들의 집합이 되어야 합니다. 이 정보는 다음과 같은 과정을 통하여 seed라는 배열로 생성하였습니다. 여기서 xi와 yi는 부분영역의 좌측 하단 꼭지점의 좌표이며, lenx와 leny는 부분영역의 가로 및 세로 방향 크기입니다.


xi = 550

yi = 300

lenx = 10

leny = 10

xx = INDGEN(lenx*leny) MOD lenx + xi

yy = INDGEN(lenx*leny) / leny + yi

seed = xx + yy*sz[0]

HELP, seed


그런데 이 내용이 왜 이런 요상한 문법들로 구성이 되어야 할까요? 실제로 HELP에 의한 출력 결과를 보면 seed는 100개의 값들로 구성된 배열임이 확인됩니다. 다만 100개의 화소들의 위치 하나하나는 (x, y)와 같은 형태의 좌표가 아닌 단일값 형태의 좌표가 되도록 한 것입니다. 예를 들어 어떤 배열이 5x5의 2차원 형태라고 할 때, 맨 마지막 원소의 위치 인덱스 좌표는 (4, 4)라고 생각하는 것이 자연스럽습니다. 각 방향별로 인덱스의 범위가 0~4이기 때문입니다. 하지만 배열 내 원소 갯수가 총 25개이므로, 맨 마지막 원소의 위치 인덱스는 0~24 범위의 맨 끝 값인 24라는 단일값이 되기도 합니다. 즉 위의 코드는 씨앗 부분영역을 구성하는 100개의 화소들의 좌표를 이러한 방식으로 단일값 형태로 산출하여 모아서 seed라는 배열로 생성하는 내용이 됩니다. 좀 독특한 문법이 사용된 것은 사실이지만, IDL에서는 이런 식으로도 작업을 한다고 생각하시면 됩니다.


그리고 위의 이미지에서는 원본 이미지를 먼저 띄운 상태에서 씨앗 부분영역에 해당되는 사각형을 함께 표시하고 있습니다. 이 사각형은원본 이미지가 있는 상태에서 다음과 같이 POLYGON 함수를 사용하여 사각형을 원본 이미지상에 중첩 표출한 것입니다.


xverts = [xi, xi+lenx-1, xi+lenx-1, xi, xi]

yverts = [yi, yi, yi+leny-1, yi+leny-1, yi]

plg_seed = POLYGON(xverts, yverts, FILL_COLOR='yellow', /DATA)


그러면 이제 REGION_GROW 함수를 사용하여 RG 기법을 적용하겠습니다. 일단 다음과 같이 해봅시다.


rg = REGION_GROW(data, seed, THRESHOLD=[70, 90])

HELP, rg


여기서 REGION_GROW 함수에 사용된 첫번째 및 두번째 인수는 각각 원본 이미지 배열 및 씨앗 부분영역 배열입니다. 그런데 여기서는 THRESHOLD라는 키워드가 함께 사용되고 있습니다. 그리고 이 키워드에는 70, 90 두 값이 설정되어 있습니다. 이것은 씨앗 부분영역의 주변으로 화소값이 70~90의 범위에 해당되는 인접 화소들을 추적하라는 의미입니다. 하필 제가 70~90이란 범위를 정한 이유는, 씨앗 부분영역의 100개의 화소값들의 범위를 다음과 같이 확인해봤더니 75~85라고 나왔기 때문입니다.


PRINT, MIN(data[seed]), MAX(data[seed])


그래서 이 범위에서 약간 확대해서 70~90 범위의 값들을 주변으로 추적해나가도록 제가 한번 설정해본 것입니다. 이와 같이 씨앗 부분영역의 주변으로 화소들을 추적하는 기준을 화소값 범위로 지정하고자 할 경우에는 THRESHOLD 키워드를 사용하면 됩니다. 그래서 그 결과를 rg라는 배열로 생성하였는데, HELP로 확인해보면 40454개의 값들로 구성된 배열임을 확인할 수 있습니다. 즉, 조건에 맞는 인접 화소들을 주변으로 계속 추적한 결과 총 40454개의 화소들을 찾았다는 얘기입니다. 그 결과가 어떻게 나왔는지 시각적으로 확인하기 위하여, 추적된 화소들을 다음과 같은 방법을 사용하여 중첩 표출하였습니다.


mask = MAKE_ARRAY(sz, VALUE=!values.f_nan)

mask[rg] = 1

ct = COLORTABLE(['red'])

imm = IMAGE(mask, RGB_TABLE=ct, MARGIN=0, /OVERPLOT)


여기서는 추적된 40454개의 화소들만 붉은색으로 표시하고 나머지 화소들은 원본 이미지의 모습을 그대로 볼 수 있도록 하기 위하여 약간의 테크닉을 사용하였습니다. 먼저 원본 이미지와 동일한 구조를 갖는 mask라는 배열을 만들면서, 그 안의 화소값들을 모두 NaN값으로 채워넣었습니다. 그 다음에는 앞서 REGION_GROW 함수의 결과물은 rg를 사용하여, mask 배열 내에서 이 위치의 값들만 1로 대체하였습니다. 그 다음에는 이렇게 처리된 mask를 중첩하여 표출하기 위하여 IMAGE 함수를 사용할 때, 붉은색으로만 구성된 컬러테이블을 사용하는 방식입니다. 어쨌든 그 결과는 다음 그림과 같습니다.



그리고 씨앗 부분영역의 위치를 함께 볼 수 있도록 하기 위하여 다음과 같이 POLYGON 함수를 한번 더 사용하였습니다. 그 결과는 다음 그림과 같습니다.


plg_seed = POLYGON(xverts, yverts, FILL_COLOR='yellow', /DATA)



이 결과 그림을 보면, RG 기법에 의하여 씨앗 영역으로부터 어떻게 인접 화소들을 탐문수색(?)했는지를 확인할 수 있습니다. 당연한 얘기지만 THRESHOLD 키워드에 주어지는 범위를 어떻게 설정하느냐에 따라 결과는 다릅니다. 예를 들어 70~90이라는 범위 대신 다음과 같이 75~85라는 더 좁은 범위를 사용하면 아무래도 추적되는 화소들의 갯수도 그만큼 줄어들게 됩니다.


rg = REGION_GROW(data, seed, THRESHOLD=[7585])


그 결과는 다음 그림과 같습니다.



어쨌든 이와 같이 REGION_GROW 함수를 사용하여 인접 화소들을 추적하는데 있어서 THRESHOLD 키워드를 사용하여 범위를 설정하는 방식을 사용할 수 있습니다. 그런데 범위 설정이 아닌 또 다른 방식도 있습니다. 다음과 같이 STDDEV_MULTIPLIER라는 키워드를 사용하는 방식입니다. 이 방식에서는 씨앗 부분영역 내 화소값들의 평균값 및 표준편차를 구한 다음, 평균값으로부터 표준편차의 몇 배만큼 벗어난 하한 및 상한값을 산정하여 범위로 사용하게 됩니다. 그래서 앞서 제시되었는 예제 코드의 내용에서 REGION_GROW 함수가 사용된 부분만 다음과 같은 내용으로 바꿔봅시다.


rg = REGION_GROW(data, seed, STDDEV_MULTIPLIER=3)


여기서는 씨앗 부분영역 내 화소값들의 평균으로부터 표준편차의 3배만큼 벗어난 하한 및 상한값으로 범위를 결정하여 주변 화소들을 추적하게 됩니다. 실제로 씨앗 부분영역 내 화소값들의 평균 및 표준편차를 다음과 같은 방법을 사용하여 확인해볼 수 있습니다.


PRINT, MEAN(data[seed]), STDDEV(data[seed])


그랬더니 평균은 80.91이고 표준편차는 2.21로 확인됩니다. 따라서 위와 같이 STDDEV_MULTIPLIER 키워드에 3이란 값을 주면 추적 대상이 되는 화소값 범위는 74.28~87.54 정도가 됩니다. 그 결과는 다음 그림과 같습니다.



역시 당연한 얘기겠지만, STDDEV_MULTIPLIER 키워드에 주어지는 값이 클수록 추적되는 화소들도 더 많아지게 됩니다. 그래서 이 키워드의 값을 3 대신 5로 바꿔보면 그 결과는 다음 그림과 같습니다.



이와 같이 씨앗 부분영역의 주변으로 화소들을 추적하는 기준을 표준편차의 배수로 지정하고자 할 경우에는 STDDEV_MULTIPLIER 키워드를 사용하면 됩니다. 정리해보면, REGION_GROW 함수에서 인접 화소들을 추적하는데 있어서 그 기준은 직접 범위값을 설정하는 방식 그리고 씨앗 부분영역 내 화소값 평균 및 표준편차를 이용하는 방식 두가지가 존재합니다. 어떤 방식을 사용할 것인지 그리고 해당 키워드의 값을 얼마로 설정해야 할 것인지에 대해서는 실제 데이터의 특성 및 분석의 목적에 따라 얼마든지 달라질 수 있습니다.


그러면 RG 기법은 어떤 경우에 유용할까요? 사실 오늘 예제로 시도했던 내용은 어떤 면에서는 이미지에 대하여 특정한 범위의 화소값들을 찾아내는 과정이라고 볼 수도 있습니다. 따라서 얼핏 보면 마스킹(Masking) 기법과 유사하게 느껴질 수도 있습니다. 위에서 우리가 시도했던 작업 중에서 화소값 범위가 70~90인 화소들을 찾아내는 과정도 있었는데, 그냥 마스킹 기법을 사용하여 다음과 같은 시도를 해볼 수도 있습니다.


rg = WHERE(data GE 70 AND data LE 90)

mask[rg] = 1


이 내용은 앞서 우리가 mask라는 결과 배열을 생성하는데 있어서 REGION_GROW 함수의 결과인 rg를 사용했던 것과 달리, 그냥 마스킹 기법에 의하여 얻어진 rg를 사용했다는 점만 다릅니다. 따라서 이후의 표출 과정은 동일합니다. 그러나 그 결과 그림을 보면 다음과 같습니다.



아마 이 그림을 앞서 우리가 RG 기법으로 얻은 결과 그림과 비교해보면, RG 기법 필요한 이유를 충분히 짐작하실 수 있을 겁니다. 그냥 마스킹의 경우에는 제시된 조건을 만족하는 화소들을 이미지 내 전 영역에 걸쳐 찾아내기 때문에, 특정한 부분영역으로부터 인접한 화소들 추적하면서 그러한 조건을 만족하는가를 확인하는 RG 기법과는 분명한 차이가 있습니다. 따라서 매우 엄격한 조건에 의하여 씨앗이 될만한 부분들을 일단 먼저 찾아낸 다음, 그로부터 유사한 특성의 화소들을 추적해가면서 영역을 확장시켜 탐색하는 방식이 더 효과적일경우에는 RG 기법이 더 적합할 수 있습니다. 실제로 예전에 IDL의 Image Processing 교재에서 이 RG 기법을 소개할 때, 사람의 머리 부분을 CT 촬영한 이미지가 예제로 사용되기도 하였습니다. 병원에서 이러한 촬영을 하기 전에 약품(아마 조영제인 것으로 압니다)을 투여하는데, 이 약품이 인체 내에서 어느 정도 퍼져있는지를 가늠하는데 적용할만한 이미지 처리 기법으로서 Regin Growing이 설명되었던 것으로 기억합니다. 어쨌든 RG 기법의 이러한 특성을 충분히 감안하여, 적절한 경우에 이 기법을 사용해보시는 것도 좋을 것 같습니다.

반응형