WPF OpenCV 프로젝트 #32: Morphology (Erosion, dilation, Opening, Closing)

Morphology (모폴로지: 형태학)를 주제로 Erosion (침식), Dilation (팽창), Opening (열림), Closing (닫힘), Gradiant (그레디언트), Top-Hat, Black-Hat 에 대해 OpenCvSharp에서 제공하는 함수들을 이용해서 한꺼번에 순차적으로 간단히 개념을 정리하고, WPF OpenCV 프로젝트에 구현해 보겠습니다.
이전 포스팅(#31)들에서 Average Blur, Box Filter, Gaussian Blur, Median Blur, Bilateral Filter 들을 OpenCvSharp에서 제공하는 함수들을 이용해서 처리하는 방법과 Edge Detection Filter 들에 대해 나름 깊이 있게 다뤘습니다. 이번 포스팅에서는 Morphology (모폴로지)Erosion, Dilation Opening, Closing, Gradiant, Top-Hat, Black-Hat 에 대해 조근 조근, 자근 자근, 씹으면서 알아 보도록 하겠습니다.

Theory

Morphology (모폴로지)

Morphology (모폴로지)는 형태학이란 뜻으로 영상 분야에서는 노이즈 제거, 구멍 메꾸기, 연결되지 않은 경계를 이어 붙이기와 같은 형태학적 관점에서 영상의 연산을 의미합니다. Morphology (모폴로지)는 주로 형태를 다루다 보니까 Binary Image (바이너리이미지)를 대상으로 하게 되죠. 서두에도 언급했듯이 Morphology 의 연산에는 erosion (침식), dilatation (팽창) 을 주 연산으로 사용하는데, 이것들을 결합해서 Opening (열림), Closing (닫힘) 연산 이라는 응용 연산을 하게 되는데요. 아래 쪽에 다루겠지만, 두 가지 응용만 하는 것이 아니라, 원본 영상을 이용한 응용 연산으로 Gradiant, Top-hat, Black-hat 연산도 한답니다. 이제 하나씩 조져 나가 보도록 하죠.

Kernel : Structuring Element

Morphology 연산의 핵심은 Structureing Element (구조 요소) 입니다. 특별한것은 아니고, 작은 Kernel Matrix (커널 행렬) 이며 이것을 이용해서 적용하려는 image (이미지) 위를 미끄러지듯이 슬라이딩 하며 객체(전경 또는 대상 물체)와 커널이 어떻게 겹치는지를 판단하게 합니다.

이런 구조화 요소는 아래의 3가지를 사용합니다.

Rect (사각형) : 모든 방향으로 균등하게 확장/축소.
Cross (십자형) : 상/하/좌/우 에 대한 연결성을 강조.
Ellipse (타원형) : 둥근 형태를 유지하며 처리.

OpenCV 에서는 Structureing Element 와 관련하여 Cv2.GetStructuringElement() 라는 함수를 제공합니다. 함수의 파라미터라고 해봐야 별거 없긴 한데 한번 살펴 보고 가도록 하죠.

Cv2.GetStructuringElement(
MorphShapes shape,
Size ksize
);
MorphShapes shape: 커널의 모양 (Rect, Cross, Ellipse)
Size ksize: 커널의 크기. 값이 클수록 연산의 강도가 세집니다.

Erosion (침식)

대상 물체를 이번 포스팅 내용에서 객체라고 하겠습니다. 이미지에서 원래 있던 객체의 영역을 깍아 내는 연산을 Erosion (침식) 이라고 합니다. Erosion 연산을 위해서 앞서 언급했던, Structuring Element (줄여서 이번 포스팅 글에서는 kernel 이라고 할께요.) 이라는 구조화 요소는 1과 0으로 채워진 kernel 이 필요한데, 1이 채워진 모양에 따라 위에서 언급한 사각형, 십자형, 타원형을 사용하게 됩니다. 아무튼 Erosion객체의 크기를 줄여서 미세한 돌기나 잡음을 제거할 수 있고, kernel 내의 모든 픽셀이 객체 (흰색)와 일치할 때만 결과 값이 1이 되게 하는 거죠. 수학적으로 표현하면 아래와 같다고 하네요. ^^;

AB=Z(B)zAA ⊖ B = {Z ∣(B)z​ ⊆A }​

Erosion (침식)은 작은 노이즈 (Salt Noise) 제거에 탁월합니다. 객체들에 붙어 있는 작은 노이즈를 분리하는 것을 상상하면 되는데, 가느다란 선을 없애거나 서로 살짝 닿아 있는 두 물체를 떼어 놓고 싶을 때 자주 사용합니다.

Dilation (팽창)

Dilation (팽창)은 erosion (침식)과 반대로 영상 속 사물의 주변을 덧붙여서 영역을 더 확장하는 연산입니다. erosion (침식) 처럼 구조화 요소 kernel 을 입력 영상에 적용해 1로 채워진 영역이 온전히 덮이지 않으면 1로 메꿔버리는 거죠. 그래서 객체의 크기를 늘려 구멍을 메우거나 끊어진 부분을 연결할 수 있는 연산인 거죠. kernel 내에 pixel 이 하나라도 객체와 겹치면 결과 값이 1이 되게 됩니다. 이것도 굳이 수학적으로 표현해 보면 아래와 같습니다. 참고 해 주세요.

A⊕︎B=Z|(B)zA|A ⊕B = {Z |(B)z​∩A ≠∅ |}

Dilation (팽창)은 객체 내부의 작은 구멍을 채우고, 끊어진 선을 연결하는 용도로 사용합니다. 특히 저해상도의 문자 이미지에서 문자 인식 전에 문자의 끊어진 획을 붙이거나, 객체의 밀도를 높이 때 사용하면 최고 입니다.

Opening (열림)

Erosion (침식)과 Dilation (팽창) 연산은 이미지에서 밝은 부분이나 어두운 부분의 점 노이즈를 없애는 데 효과적이지만, 원래의 모양이 홀쭉해지거나 뚱뚱해지는 변형이 일어하게 됩니다. 그런데 침식과 팽창 연산을 조합하여 연산하면 원래의 모양을 유지하면서 노이즈만 제거할 수 있게 할 수 있다는 놀라운 사실을 전해 드립니다. 아무튼 침식 연산을 먼저 적용하고, 팽창 연산을 적용하는 것을 Opening (열림) 연산이라고 하며, 객체의 전체적인 크기는 유지하면서, 표면의 돌기나 작은 파편을 제거하도록 해주죠. 그래서, Opening (열림) 연산은 주변 보다 밝은 노이즈 제거에 효과적으로, 맞닿아 있는 것으로 보이는 독립된 개체를 분리하거나 돌출된 pixel을 제거하는데 짱! 입니다. 이 녀석도 굳이 수학식으로 표현해 보죠. ㅠ.ㅠ

AB=(AB)⊕︎BA ∘ B =(A ⊖ B ) ⊕ B

밝은 영역의 노이즈 제거에 탁월한 Openning (열림) 연산은 배경에 모래처럼 흩뿌려진 미세한 점들을 제거하고 주요 객체만 남길 때 아주 필수적으로 사용됩니다.

Closing (닫힘)

Opening (열림) 연산이 erosion 연산 후 dilation 연산을 수행한 것이라고 했죠.
(언제 했냐구요? 바로 위에서 했잖아요!!!! ㅠ.ㅠ)
Closing (닫힘) 연산은 Opening 연산의 반대로 연산하는 것입니다. 즉 이미지에서 dilation (팽창) 연산을 먼저 진행하고, erosion (침식) 연산을 한것을 뜻합니다. 이렇게 적용하면, 이미지에서 주변 보다 어두운 노이즈 제거에 효과적이면서 끊어져 보이는 개체를 연결하거나, 구멍을 메우는 데 짱! 좋습니다.
이 녀석도 굳이 수학식으로 표현해 보죠. 아래와 같이요.

AbuletB=(A⊕︎B)BA bulet B = (A ⊕ B) ⊖ B​

Closing (닫힘) 연산은 OCR 처리와 같이 문자 인식 처리 시 글자 내부의 미세한 구멍을 메워 글자를 온전하게 만들 때 킹! 짱! 효과적입니다.

Gradiant (그레디언트)

먼저 언급한 Opening, Closing 처럼 erosion, dilation 연산을 이용한 방법인데요. 별건 아니고 dilation (팽창) 결과에서 Erosion (침식) 결과를 빼서 결과 이미지를 얻는 방법입니다. 이렇게 처리하면 객체의 경계 즉 외곽선만 남게됩니다. 수학적으로 아래와 같이 표현합니다. 참고해 주세요.

G = (A ⊕︎ B)  (A  B)G\ =\ \left(A\ \oplus \ B\right)\ -\ \left(A\ \ominus \ B\right)

Morphology 에서 Gradiant 연산은 객체의 윤곽 추출 방법 중 하나로, 단순한 edge (엣지) 검출 보다 두꺼운 경계선을 얻고 싶을 때나 객체의 형태적 외곽이 중요할 때 많이 사용하는 방법입니다.

Top-Hat (탑햇)

원본 이미지에서 Opening (열림) 연산이 적용된 이미지를 빼는 방법으로 밝은 미세한 부분을 추출하게 됩니다. 수학적으로 아래와 같이 표현 합니다. 참고해 주세요.

T=A(AB)T = A − (A ∘ B)​

Top-Hat 연산은 앞서 언급했지만, 이미지의 어두운 배경에서 밝은 국소 부위(결함, 먼지)를 찾을 때 매우 유용하게 사용하게 될겁니다.

Black-Hat (블랙햇)

Closing (닫힘) 연산이 적용된 이미지에서 원본 이미지를 뺍니다. 이렇게 처리하면, 주변 보다 어두운 미세한 부분을 추출하게 되죠. 즉 이미지에서 밝은 배경에 존재하는 어두운 결함(스크래치, 검은 점)을 찾을 때 자주 애용하게 될 겁니다. 수학적으로는 아래와 같이 표현 되니 참고해 주세요.

T = (A  B) AT\ =\ \left(A\ \bullet \ B\right)\ -A

OpenCvSharp Function

OpenCvSharp 에서는 위에서 언급한 erotion, dilation, opening, closing, gradiant, top-hat, black-hat 을 이용한 이미지 연산 함수를 하나의 함수로 제공합니다. 앞서 Structuring Element 설명에서 관련 함수는 설명 했으니 Morphology 에서 사용되는 함수만 간략히 설명하고 WPF OpenCV 프로젝트에 구현 하도록 하죠. OpenCvSharp에서 제공하는 함수는 Cv2.MorphologyEx() 입니다. 이 함수의 주요 파라미터들 중에서 몇 개만 살펴 보겠습니다.

Cv2.MorphologyEx(
Mat src,
Mat dst,
MorphTypes op,
Mat Kernel,
int iterations,
);

MorphTypes op: 연산 종류 (Erode, Dilate, Open, Close, Gradient, TopHat, BlackHat).
Mat kernel: 위에서 생성한 구조 요소.
int iterations: 연산 반복 횟수. 횟수가 늘어날수록 침식은 더 작게, 팽창은 더 크게 작용합니다.

구현 요약

위 에서 사용할 Morphology 들에 대해 간략히 (?) 정리했으니까 이제 구현을 해야겠죠. 늘 그래왔던 것 처럼 WPF OpenCV 프로젝트에 구현할 내용을 아래와 같이 요약하고 단계 별로 구현 해 보도록 하죠.

AlgorithmParameters.cs: MorphologyParams 클래스와 MorphologyType 열거형을 추가하여 각 알고리즘 필터별 파라미터(kernel, iterations, MorphoTypes 등)를 정의합니다.
MainViewModel.cs: 알고리즘 목록에 “Morphology“를 추가하고, 선택 시 파라미터 객체를 생성하도록 연결하도록 하죠.
MainWindow.xaml: “Morphology” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 DataTemplate에 추가하고, 선택할 수 있도록 구현 합니다.
OpenCVService.cs: erosion, dilation, opening, closing, gradiant, top-hat, black-hat 에 대한 영상 처리를 위해 OpenCvSharp 함수 이용해서 실제로 수행하는 로직을 구현합니다.

Step 1: MorphologyParams (Model)

AlgorithmParameter.cs 파일에 먼저 MorphologyType 열거형 데이터를 추가합니다. MorphologyParams 클래스에는 적용할 Filter 함수들의 파라미터를 아래의 코드와 같이 정의해서 UI와 연결 되도록 하죠.

Step 2: MainWindow.xaml (View)

MainWindow.xaml 파일에는 MorphologyParams 의 Property 속성을 UI에 연결하고, 사용자가 “Morphology” 선택 시 나타날 UI(콤보박스, 슬라이더 등)를 아래의 코드와 같이 DataTemplate에 추가해서, 선택된 필터를 설정합니다.

Step 3: MainViewModel (ViewModel)

MainViewModel.cs 파일의 MainViewModel() 생성자 함수의 알고리즘 목록에 Morphology 를 추가하고, 사용자가 Morphology 를 선택했을 때, CreateParametersForAlgorithm() 함수에서 MorphologyParams 객체의 파라미터 객체를 생성해 연결 하도록 아래 코드로 업데이트 합니다.

Step 4: OpenCVService (model)

OpenCVService.cs 파일에서는 ProcessImageAsync() 함수OpenCvSharp에서 제공하는 함수 중Cv2.GetStructuringElement() 함수Cv2.MorphologyEx() 함수를 이용해서 영상 처리 하는 코드를 아래와 같이 추가하면 됩니다.

실행 및 확인

한꺼번에 Morphology (모폴로지: 형태학) 관련 연산(erotion, dilation, opening, closing, gradiant, top-hat, black-hat) 들을 모두 추가하였습니다. 이전 포스팅도 그랬지만, 추가된 내용에 비해 코드량은 그다지 많지 않았던것 같은데, 어떻게 느꼈는지 모르겠네요. 아마도 OpenCV 에서 제공하는 함수를 이용해서 관련 기능들을 구현했기 때문에 아주 단순하게 구현되었네요. 이미지 로딩 후 영상 처리에 대해 Morphology 적용 시 각각의 알고리즘에 대해 간단히 요약하고 연산들의 결과를 살펴 보도록 하죠.

Erosion (침식): 글자가 얇아지거나, 작은 흰 점들이 사라집니다.
Dilation (팽창): 글자가 뚱뚱해지거나, 검은 점들이 메워집니다.
Opening (열림): 배경의 노이즈(흰 점)만 쏙 사라지고 물체는 원래 크기를 유지합니다.
Closing (닫힘): 물체 안의 구멍(검은 점)만 쏙 메워지고 물체는 원래 크기를 유지합니다.
Gradient: 물체의 테두리만 하얗게 남습니다. (Edge 검출과 비슷합니다.)
Top-Hat: 밝은 미세한 부분을 (결함, 먼지) 검출에 유용합니다.
Black-Hat: 어두운 미세한 부분 추출(스크래치, 검은 점) 에 유용 합니다.

​아래의 이미지는 erosion (침식) 연산을 위해 사용한 원본 이미지 입니다. 문자를 전경(객체)이라고 할때 배경은 검은색이며, 문자를 제외한 배경에 흰색 노이즈가 있는 이미지 입니다.

Erosion 적용 전 원본 이미지
원본 이미지 (배경 흰색 노이즈 점)

위 이미지에 대해 알고리즘 속성 창에서 사용자가 선택할 수 있는 파라미터를 아래와 같이 변경해서 적용해 보도록 하겠습니다. (배경의 흰색 노이지가 제거되는 것을 확인해 주세요. 침식 연산의 영향 때문에 문자가 홀쭉해졌는데, 느껴 지시나요? Iterations 값을 더 늘리면 확연히 보일 겁니다.)
Operation Type: Erode
Kernel Shape: Rect
Kernel Size: 3
Iterations : 2

Erode 적용 이미지
Erode 적용 이미지

다음은 dilation (팽창) 연산을 이미지에 적용하기 위한 원본 이미지입니다. 위와 동일하게 문자를 전경(객체)이라고 할 때 배경은 검은색이며, 문자 내에 배경에 검은색 노이즈가 있는 이미지 입니다.

Dilation 적용 전 원본 이미지 (문자 내 검은 점 노이즈)
Dilation 적용 전 원본 이미지 (문자 내 검은 점 노이즈)

위 이미지에 대해 알고리즘 속성 창에서 사용자가 선택할 수 있는 파라미터를 아래와 같이 변경해서 적용해 보도록 하겠습니다. (문자 내의 검은색 노이즈가 제거되는 것을 확인해 주세요. 팽창 연산의 영향 때문에 문자가 뚱뚱해 졌는데, 느껴 지시나요? Iterations 값을 더 늘리면 확연히 보일 겁니다.)
Operation Type: Dilate
Kernel Shape: Rect
Kernel Size: 3
Iterations : 6

Dilation 적용 이미지
Dilation 적용 이미지

다음은 Opening (열림) 연산을 수행한 이미지를 테스트해 보겠습니다. 먼저 사용한 원본 이미지 입니다.

Opening 적용 전 원본 이미지
Opening 적용 전 원본 이미지

위 이미지를 Opening (열림) 연산을 수행하면 다음과 같은 이미지처럼 결과 이미지가 나옵니다. erosion (침식) 연산 처리 이미지와 달라진것이 보이나요? erosion 연산 적용 시에도 배경의 흰색 노이즈 점들이 제거되었지만, 문자가 홀쭉해졌던 것 기억하나요? 하지만, Opening (열림) 처리를 한 이미지는 문자의 크기는 거의 동일하게 유지되고있음을 확인 할수 있을 겁니다. 변경 파라미터는 Operation Type: Opening 을 제외하고는 erosion 적용 파라미터와 동일하게 하였으니 참고해 주세요.

Opening 적용 이미지
Opening 적용 이미지

다음은 Closing (닫힘) 연산을 수행한 이미지를 테스트해 보겠습니다. 먼저 사용한 원본 이미지 입니다.

Closing 적용 전 원본 이미지
Closing 적용 전 원본 이미지

위 이미지를 Closing (닫힘) 연산을 수행하면 다음과 같은 이미지처럼 결과 이미지가 나옵니다. dilation (닫힘) 연산 처리 이미지와 달라진 것이 보이나요? dilation 연산 적용 시에도 객체(문자)내의 검은색 노이즈 점들이 제거되었지만, 문자가 뚱뚱해졌던 것 기억하나요? 하지만, Closing (닫힘) 처리를 한 이미지는 문자(객체)의 크기는 거의 동일하게 유지 되고 있음을 확인 할 수 있을 겁니다. 변경 파라미터는 Operation Type: Closing 을 제외하고는 dilation 적용 파라미터와 거의 동일하게 하였으니 참고해 주세요.

Closing 적용 이미지
Closing 적용 이미지

다음에는 Gradient 연산에 대한 검증을 해보죠. 검증을 위해 사용한 이미지는 아래의 이미지입니다.
전경과 배경이 확실히 구분된 아주 깨끗한 이미지 입니다.

Gradient 적용 전 원본 이미지
Gradient 적용 전 원본 이미지

위 이미지를 Gradient 연산을 수행하면 다음과 같은 이미지처럼 결과 이미지가 나옵니다. 위에서 개념을 정리할 때 Gradient 는 경계선 검출과 비슷하게 이미지가 만들어 진다고 했었습니다. 기억하시죠? (기억이 안 난다면 위로 기어 올라가 다시 한번 읽어봐 주세요. ㅠ.ㅠ)
사용자가 조절하는 파라미터들은 다음과 같이 설정하였습니다.
Operation Type: Gradient
Kernel Shape: Rect
Kernel Size : 3
Iterations: 1

Gradient 적용 이미지
Gradient 적용 이미지

다음에는 Top-Hat 연산에 대한 검증을 해보죠. 검증을 위해 사용한 이미지는 아래의 이미지입니다.
기존에 사용하던 이미지를 이용하면 확연히 보이지 않아 사용 이미지를 변경하였습니다.

Tap-Hat 적용 전 원본 이미지
Tap-Hat 적용 전 원본 이미지

위 이미지를 Top-Hat 연산을 수행하면 다음과 같은 이미지처럼 결과 이미지가 나옵니다. 위에서 개념을 정리할 때 Top-Hat 연산은 밝은 부분을 더 강조하는 이미지가 만들어 진다고 했었습니다. 기억하시죠? (기억이 안 난다면 위로 기어 올라가 다시 한번 읽어봐 주세요. ㅠ.ㅠ)
사용자가 조절하는 파라미터들은 다음과 같이 설정하였습니다.
Operation Type: TopHat
Kernel Shape: Rect
Kernel Size : 3
Iterations: 100

Top-Hat 적용 이미지
Top-Hat 적용 이미지

다음에는 Black-Hat 연산에 대한 검증을 해보죠. 검증을 위해 사용한 이미지는 아래의 이미지입니다.
Top-Hat 검증에 사용하던 이미지를 그대로 사용하였습니다.

Black-Hat 적용 전 원본 이미지
Black-Hat 적용 전 원본 이미지

위 이미지를 Black-Hat 연산을 수행하면 다음과 같은 이미지처럼 결과 이미지가 나옵니다. 위에서 개념을 정리할 때 Black-Hat 연산은 어두운 부분을 더 강조하는 이미지가 만들어 진다고 했었습니다. 기억하시죠? (기억이 안 난다면 위로 기어 올라가 다시 한번 읽어봐 주세요. ㅠ.ㅠ)
사용자가 조절하는 파라미터들은 다음과 같이 설정하였습니다.
Operation Type: BlackHat
Kernel Shape: Rect
Kernel Size : 3
Iterations: 10

Black-Hat 적용 이미지
Black-Hat 적용 이미지

자~ 어떤가요? 가지고 있는 이미지들을 가지고 알고리즘의 속성들을 변경해 가면서 검증을 해보면 충분히 위에서 언급한 개념 정리 부분들이 이해가 충분히 갈거라 생각합니다. 마지막으로 몇 가지만 더 언급할게요. Morpholgy (형태학) 를 이용한 방법은 산업 현장의 영상처리 시스템에서 단독으로 사용되지는 않습니다. 주로 획득한 이미지의 Pre-Processing (전처리) 에 아주 아주 많이 사용됩니다. 예를 들어 볼까요?

PCB 회로의 패턴(pattern) 검사에서 패턴의 미세한 돌기는 전기적으로 Short 를 발생 시킵니다. 이것을 위해 주로 Opening (열림) 연산으로 노이즈를 제거하여 정상 패턴을 추출합니다.

반도체 공정의 Wafer 결함을 검사하는 장비에서는 Top-Hat 또는 Black-Hat 연산을 사용해서 배경의 불균형한 조명을 제거하고, 미세한 이물(particle)이나 스크래치만 선명하게 띄워 검사 알고리즘의 정확도를 높이기도 하죠.

OCR (문자 인식) 을 위한 이미지 전처리 과정에서도 Morphology 는 판독률을 높이기 위해 아주 많이 사용되구요, Metrology (치수 측정) 와 같이 객체의 정확한 끝단을 찾기 위해 Gradient 를 사용해 외곽선을 획득하고 이후 Sub-pixel (서브 픽셀) 정밀도로 위치를 정확히 계산하는데에도 매우 자주 사용됩니다.

예를 들자면 너무나 많습니다. 중요한 것은 그만큼 Morphology 알고리즘이 중요하다는 얘기겠죠?

다음 포스팅에서는 Filter 내용 중 마지막으로 Image Pyramides 에 대해 정리하고 구현하도록 하겠습니다.

참고 자료

[Post #31] 엣지 검출: [WPF OpenCV Project #31] – 엣지 검출과 모폴로지의 차이
[Post #16] 이진화: [WPF OpenCV Project #16] – Binary Image (모폴로지의 주 대상)
MorphologyEx: OpenCV Docs – 모폴로지 확장 함수
GetStructuringElement: OpenCV Docs – 커널 생성 함수
Mathematical Morphology: Wikipedia – 침식과 팽창의 수학적 정의

댓글 남기기