Morpology 는 이미지를 구조적이고 기하학적인 특징을 기반으로 분석하고 처리하는 기법입니다. 주로 이진화된 이미지나 그레이스케일 이미지를 대상으로 특정 구조를 강조하거나, 불필요한 노이즈를 제거하거나, 원하는 영역을 추출하는 데 사용됩니다. 이 글에서는 이진화된 이미지의 morpology 만을 다룹니다.
: 디지털 이미지의 픽셀 값을 나타냅니다. 여기서 와 는 픽셀의 좌표입니다.
: Binary image 에서 픽셀 값이 1인 좌표들의 집합입니다. 즉, binary image 를 좌표 집합으로 표현한 것입니다.
: 픽셀 좌표 로 정의되는 집합의 한 원소입니다.
: 가 집합 의 요소임을 나타냅니다. 즉, 특정 좌표 가 에 속해 있음을 뜻합니다.
: 는 특정 조건을 만족하는 모든 로 구성된 집합을 의미합니다.
주로 대문자는 좌표들의 집합, 즉 binary image 를 의미하고 소문자는 대문자 집합 내에서 특정 좌표를 나타내는 원소를 의미합니다.
집합 안의 모든 원소들을 벡터 만큼 이동시키는 연산입니다.
위 수식은 집합 에 있는 모든 원소 에 벡터 를 더한 새로운 좌표들의 집합인 를 만듭니다.
모든 검은 점들이 (2, 2) 만큼 이동했다.
집합 안의 모든 원소들을 원점에 대해 대칭시키는 연산입니다.
위 수식은 원점을 기준으로 의 모든 점을 대칭 이동시킵니다. 라면 reflection 된 점은 가 됩니다.
흰 점과 검은 점이 원점을 기준으로 대칭이다.
Morpology 연산은 binary image 에서 논리 연산(AND, OR 등) 을 기반으로 수행됩니다. 예를 들어, 두 픽셀 집합을 비교하거나 겁치는 영역을 평가합니다.
입력 binary image() 와 구조 요소(Structuring Element, SE) 가 연산의 주요 구성 요소입니다.
SE 는 origin(원점 혹은 중심점) 을 기준으로 입력 이미지 위에서 이동하며 연산을 수행합니다. 기본적으로 원점은 SE 의 중앙에 위치합니다(특별히 명시되지 않는 한).
3x3 size 의 SE 가 A 위에서 이동 중이다. 특정 영역에 대해 조건이 만족되면 출력 이미지를 갱신할 것이다.
SE 의 origin 이 입력 이미지 의 특정 픽셀에 겹쳐지면, SE 와 입력 이미지가 논리적으로 평가되어 해당 픽셀의 출력 값이 결정됩니다. 출력은 에서 계산되며, SE 가 입력 이미지 위에 놓였을 때의 결과입니다.
특정 연산 결과로 X 에 표시된 출력 값이 생성된다. 출력 이미지는 입력 이미지와 SE 의 연산 결과로 만들어진 binary image 이다.
Dilation 은 morpology 연산 중 하나입니다. Dilation 은 binary image 에서 SE 를 사용하여 입력 이미지를 확장하거나 영역을 넓히는 연산입니다.
Dilation 은 다음과 같이 정의됩니다:
Dilation 에 사용되는 SE 는 대체로 대칭적인 형태를 가집니다()

A 와 B 가 단 한 점이라도 겹치면 origin 을 1로 처리한다.
skimage.morphology.binary_dilation
from skimage.morphology import binary_dilation as bwdilate
A = plt.imread('morph_txt.png') # 입력 이미지
B = np.ones((3, 3)) # 3x3 크기의 SE
D = bwdilate(A, footprint=B, mode='min') # Dilation 수행
Erosion 은 객체의 외각을 줄이거나 제거하는 데 사용되는 morphology 연산입니다. 입력 이미지 에서 SE 인 가 겹치는 부분을 평가하며, 의 모든 픽셀이 와 겹쳐야 출력값이 1이 됩니다.
Erosion 은 다음과 같이 정의됩니다:
를 입력 이미지 위에서 움직이며, 의 모든 요소가 에 포함되는 경우에만 해당 픽셀 값을 1로 설정합니다.
from skimage.morphology import binary_erosion as bwerode
from skimage.morphology import binary_erosion as bwerode
A = plt.imread('rings.png') # 입력 이미지
B = np.ones((3, 3)) # 3x3 크기의 SE
E = bwerode(A, footprint=B, mode='min') # Erosion 수행
Erosion 후 객체의 선이 얇아졌다.
Dilation 과 erosion 을 적절히 수행하여 객체의 경계를 추출할 수 있습니다.
입력 이미지 에서 erosion 연산() 결과를 빼면 내부 경계만 남게 됩니다.
Erosion 은 객체의 외곽을 줄이므로, 와의 차이는 경계에 해당합니다.
n = io.imread('pinenuts.png') # 이미지를 불러옴
p = ~((n > 130) & (n < 165)) # Binary segmentation
pe = bwerode(p, footprint=sq, mode='min') # Erosion 연산 수행
p_int = p - pe # 내부 경계 추출
입력 이미지 를 dilation () 한 결과에서 원본 이미지를 빼면 외부 경계만 남습니다.
Dilation 은 객체의 외곽을 확장하므로, 확장된 부분과 의 차이가 외부 경계입니다.
# 내부 경계 추출 구현 코드와 이어짐
pd = bwdilate(p, footprint=sq, mode='min') # Dilation 연산 수행
p_exp = pd - p
Dilation 과 erosion 결과의 차이를 계산하여 객체의 전체 경계(내부 + 외부)를 추출합니다.
객체의 전체 외곽선이 강조됩니다.
외부 경계 추출, morphological gradient.
# 외부 경계 추출 구현 코드와 이어짐
# pd 와 pe 는 반드시 boolean 에서 int 나 float 으로 변경되어있어야 함
p_grad = pd - pe # Morphological gradient 추출
Opening 은 erosion 후 dilation 을 순차적으로 적용하는 morphology 연산입니다. Opening 은 다음과 같이 정의됩니다:
Opening 연산 예시.
Opening 연산은 작은 객체(noise)를 제거하거나, 객체 사이의 좁은 연결부를 분리하는 데 사용됩니다. Erosion 을 통해 작은 영역을 제거한 뒤, dilation 으로 객체의 크기를 다시 복구합니다.
이미지에서 객체의 외곽선을 부드럽게 만듭니다(smoothing).
좁은 연결부를 제거하여 객체를 분리합니다.
객체의 불필요한 돌출부를 제거합니다.
Opening 예시 1.
Opening 예시 2.
Closing 은 dilation 후 erosion 을 순차적으로 수행하는 morphology 연산입니다. Closing 연산은 다음과 같이 정의됩니다:
Closing 연산 예시.
Closing 연산은 객체의 내부 빈 공간을 채우고, 끊어진 객체를 연결하는 데 유용합니다. Dilation 으로 빈 공간을 메우고, erosion 으로 외곽선을 복구합니다.
Clsoing 예시 1.
Closing 예시 2.
from skimage.morphology import binary_closing as bwclose
from skimage.morphology import binary_opening as bwopen
B_cr = np.array([[0,1,0], [1,1,1], [0,1,0]]) # 십자 모양 SE
B_sq = np.ones((3,3)) # 3x3 크기의 SE
test = np.zeros((10,10)) # 10x10 크기의 binary test image
test[1:6,1:4] = 1 # 왼쪽의 큰 사각형
test[2:5,5:9] = 1 # 오른쪽의 큰 사각형
test[7:9,3:8] = 1 # 아래쪽의 긴 사각형
test[3,5] = 1 # 노이즈 점
test_open = bwopen(test, footprint=B_cr, mode='min') * 1 # 십자 모양 SE(B_cr)을 사용하여 opening 연산 수행
test_close = bwclose(test, footprint=B_cr, mode='min') * 1 # 십자 모양 SE(B_cr)을 사용하여 closing 연산 수행
Opening 후 noise 점(단일 픽셀)이 제거되었다. 좁은 연결부가 분리되어 객체 간격이 늘어났다. 반면 closing 후 객체 내부의 빈 공간이 메워졌고, 끊어진 객체 간 좁은 틈이 연결되었다.
# 테스트를 위해 입력 이미지에 노이즈 추가
img = plt.imread('circles.png') # 입력 이미지 읽기
A = img.astype('bool') * 1 # Binary image 로 변환
x = np.random.random_sample(A.shpae) # 임펄스 노이즈 생성
A[np.nonzero(x > 0.95)] = 0 # 배경에 랜덤하게 흑점 noise 추가
A[np.nonzero(x <= 0.05)] = 1 # 객체에 랜덤하게 백점 noise 추가
# Opening 후 Closing
A_f1 = bwclose(bwopen(A, footprint=B_sq, mode='min'), footprint=B_sq, mode='min') # 정사각형 SE 사용
A_cf2 = bwclose(bwopen(A, footprint=B_cr, mode='min'), footprint=B_cr, mode='min') # 십자형 SE 사용
흑점과 백점 noise 를 원본 이미지 전반에 무작위로 분포시켰습니다. 정사각형 SE 를 사용한 결과 객체 내부의 작은 검은 점 noise 가 제거되었습니다. 십자형 SE 는 내부의 검은 점을 완벽하게 제거하지는 못했지만, 객체의 외곽선이 더 부드럽게 정리되었습니다.
Hit-or-Miss transform 은 한 이미지에서 같은 모양을 탐지(shape detection)하는데 유용한 기법입니다. Hit-or-Miss transform 의 수학적 정의는 다음과 같습니다:
입력 이미지에서 과 같은 모양을 가지는 부분을 찾습니다. 는 의 배경에서 주변 조건을 설정합니다. 두 개의 SE 를 사용해 와 각각에 erosion 연산을 수행한 뒤 교집합을 취합니다.
A 에서 첫번째 SE 인 B 를 erosion.
A 의 차집합에 두번째 SE 인 C 를 erosion.
: 찾고자 하는 모양
: 모양 주변 조건
: 결합 연산
b1 = np.array([[0,1,1,0],
[1,1,1,1],
[0,1,1,0]])
b2 = np.ones((6,6))
b2[1:5,1:5] = 1-b1
tb1 = bwerode(t, footprint=b1, mode='min')
tb2 = bwerode(1-t, footprint=b2, mode='min')
np.where((tb1 & tb2) == 1)
b1:탐지할 모양 정의 (위 코드에서는 원형)
b2: 배경 조건 설정 (예: b1 의 보수와 일치)
bwerode: skimage 의 erosion 연산으로, 와 각각에 대해 SE 를 적용
np.where: 두 결과의 교집합을 구하여 최종 탐지된 위치를 반환
결과적으로 탐지된 픽셀은 b1 과 동일한 모양을 가지며, b2 조건을 만족한다.
Region filling 은 binary image 에서 경계로 둘러싸인 내부 영역을 채우는 방법입니다. 주어진 픽셀 좌표 가 포함된 영역 내부를 채우는 방법에 대해 알아봅시다.
from skimage.morphology import binary_erosion as bwerode
import scipy.ndimage as nd
n = io.imread('nicework.png') / 255 # 이미지 불러오기 및 이진화
B_sq = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]]) # 십자 모양의 SE 생성
nb = n - bwerode(t, footprint=B_sq, mode='min') # 내부 경계 추출
nb1 = nb[0:120, 0:80] # 'N' 문자 영역 분리
nf = ndi.binary_fill_holes(nb1, B_sq) # 영역 채우기
nb[0:120, 0:80] = nf # 결과 병합
binary_erosion: 내부 경계 추출
binary_fill_hoels: Region filling 을 통해 경계로 둘러싸인 영역을 채움
결과 병합: 채워진 결과를 원래 이미지로 병합하여 최종 출력 생성
Connected components 는 연결된 픽셀 그룹으로, 각 그룹 내 모든 픽셀은 이웃 관계를 통해 연결되어 있습니다.
4-connected components: 십자 모양 SE 을 사용하여 상하좌우로 연결된 픽셀을 포함
8-connected components: 정사각형 SE 을 사용하여 대각선 방향까지 포함해 연결된 픽셀을 분석
이는 다음과 같은 알고리즘 흐름으로 진행됩니다:
를 시작으로 dilation 을 반복 수행
팽창된 결과와 의 교집합을 계산
결과가 수렴()할 때까지 반복
from skimage.morphology import label
# 입력 이미지 생성
A = np.zeros((6,6)).astype('uint8')
A[0,(0,1,3,4)] = 1
A[1,(0,1,2,4)] = 1
A[2,3:6] = 1
A[3:6,0:3] = 1
# 연결 성분 분석
L4 = label(A, connectivity=1) # 4-connected components
L8 = label(A, connectivity=2) # 8-connected components
Skeleton 은 객체의 중심 축을 따라 남아있는 가장 얇은 형태의 구조를 나타냅니다. 이는 객체의 모양을 유지하면서도 최소한의 데이터로 표현하기 위해 사용됩니다.
원의 중심이 특정 픽셀에 위치
원의 가장자리(boundary)가 객체 경계(A)를 두 번 이상 접촉해야 함
Skeleton 은 반복적인 erosion 과 opening 을 조합하여 계산합니다. 계산 과정은 다음과 같습니다:
: 원본 이미지
: SE
: Erosion
: Opening
: 각 반복에서 생성된 skeleton 요소의 합집합
: SE 를 사용한 번의 erosion.
: Erosion 된 결과를 다시 opening
차이 : Erosion 결과와 opening 결과의 차집합
: 최종 skeleton 으로 모든 를 합친 결과
from skimage.morphology import binary_erosion as bwerode
from skimage.morphology import binary_opening as bwopen
def bwskeleton(image, se):
nr, nc = image.shape
skeleton = np.zeros((nr, nc)) # 초기 skeleton
eroded = image.copy() # 이미지 복사
while eroded.any():
opened = bwopen(eroded, footprint=se) # Opening 연산
skeleton += (eroded - opened) # Erosion 결과와 opening 결과 차집합
eroded = bwerode(eroded, footprint=se) # Erosion 연산
return skeleton.astype('bool')
Binary image 의 skeletonization.