[C# WPF] OpenCVSharp FindContours 이미지 윤곽선 검출

우롱밀크티당도70·2024년 6월 19일
1

WPF

목록 보기
21/22
post-thumbnail
post-custom-banner

1. 배경

이미지 처리 할 일이 생겨서... OpenCVSharp 사용법을 찾던 중 기억해둘 내용을 작성한다.


2. 환경

  • VisualStudio 2022 / WPF 애플리케이션(.NET Framework 4.8)

3. 내용

1. 프로젝트 생성 및 환경 설정

1-1. 프로젝트 생성


WPF 앱(.NET Framework)로 프로젝트를 생성한다.
이때 프레임워크는 4.8 이상이어야 한다.

이유는 WpfExtensions를 설치해야 하는데, 아래 콘솔 내용에 따르면 4.8이상부터 지원하기 때문이다.
아래 이미지는 프레임워크 4.7.2에서 WpfExtensions를 설치하려고 했을 때 콘솔에 출력되는 에러 메시지이다.

1-2. 도구>NuGet 패키지 관리자>솔루션용 NuGet 패키지 관리...


OpenCV...를 검색해서 OpenCVSharp4, OpenCVSharp4.runtime.win, OpenCVSharp.WpfExtensions를 설치한다.

OpenCVSharp4.runtime.win은 Windows에서 작동하기 위한 내부 구현 패키지이고
OpenCVSharp.WpfExtensions는 WPF에서 사용하는 확장 라이브러리이다.

1-3. MVVM 패턴으로 프로젝트 구조 구성

이 글 참고

2. Control 배치

2-1. WPF Control 배치

Grid를 나눠서 위에는 이미지를 업로드하는 버튼과 없애는 버튼을 배치하고 아래에는 이미지소스를 바인딩할 이미지 컨트롤을 배치한다.
이미지 없애는 버튼은 그렇게 중요하지 않다...

	<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <StackPanel Orientation="Horizontal">
                <Button Command="{Binding UploadImageCommand}" Cursor="Hand" Content="UploadImage" Width="100" Margin="10,10,10,10"/>
                <Button Command="{Binding ClearImageCommand}" Cursor="Hand" Content="Clean" Width="70" Margin="10,10,10,10" />
            </StackPanel>
        </Grid>

        <Grid Grid.Row="1" Margin="10,10,10,10">
            <Image Source="{Binding ImageSource}" />
        </Grid>
    </Grid>

2-2. ViewModel에서 Command 작성

xaml의 버튼과 이미지에 바인딩 될 Command를 만들어준다.
UploadImage 버튼을 누르면 파일을 선택할 다이얼로그를 띄우게 한다.

		private BitmapImage _ImageSource;
        public BitmapImage ImageSource
        {
            get { return _ImageSource; }
            set
            {
                _ImageSource = value;
                OnPropertyChanged(nameof(ImageSource));
            }
        }

        public ICommand UploadImageCommand { get; private set; }
        public ICommand ClearImageCommand { get; private set; }

        public MainWindowViewModel() 
        {
            UploadImageCommand = new RelayCommand(UploadImage);
            ClearImageCommand = new RelayCommand(ClearImage);
        }

        private void UploadImage()
        {
            Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
            openFileDialog.Filter = "Image files (*.jpg, *.jpeg, *.png) | *.jpg; *.jpeg; *.png";

            if (openFileDialog.ShowDialog() == true)
            {
                string imagePath = openFileDialog.FileName;

                ImageSource = new BitmapImage(new Uri(imagePath));

            }
        }

        private void ClearImage()
        {
            ImageSource = null;
        }

예제에서 사용할 이미지는 FreePik에서 다운로드 받은 무료 이미지이다.

이미지를 업로드하면 이런 화면이 된다.

3. OpenCVSharp

3-1. InRange

InRange는 사용자 지정 Scalar범위 픽셀을 추출한다.
Scalar(B, G, R) 어두운 색에서-밝은 색까지의 범위 안에서 찾는다.

Cv2.InRange(src, new Scalar(0, 125, 125), new Scalar(100, 255, 255), edge);

new Scalar(0, 125, 125), new Scalar(100, 255, 255)는
다음의 범위이다.

3-2. FindContours, DrawContours

Cv2.FindContours(edge, out contours, out hierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxTC89KCOS);

List<OpenCvSharp.Point[]> new_contours = new 
List<OpenCvSharp.Point[]>();
foreach (OpenCvSharp.Point[] p in contours)
{
	double length = Cv2.ArcLength(p, true);
	if (length > 100)
	{
		new_contours.Add(p);
	}
}

Cv2.DrawContours(dst, new_contours, -1, new Scalar(0, 0, 255), 2, LineTypes.AntiAlias, null, 1);

3-3. ImShow와 WpfExtensions.BitmapSourceConverter

Cv2.ImShow("dst", dst);

WPF의 ImageSource에 바인딩하려면 Mat -> BitmapImage로 바꿀 필요가 있다. 이 때 사용하는 것이 WpfExtensions이다.

WpfExtensions에서 사용할 수 있는 BitmapSourceConverter는 ToBitmapSource라는 Mat -> BitmapSource 메서드가 있다.
BitmapSource로 바뀐 dst 데이터를 다시 BitmapImage로 바꿔주는 작업이 필요하다.

BitmapSource bitmapSource = OpenCvSharp.WpfExtensions.BitmapSourceConverter.ToBitmapSource(dst); // Mat to BitmapSource
ImageSource = ConvertBitmapSourceToBitmapImage(bitmapSource); //BitmapSource to BitmapImage

ConvertBitmapSourceToBitmapImage를 작성한다.

		private BitmapImage ConvertBitmapSourceToBitmapImage(BitmapSource bitmapSource)
        {
            if (!(bitmapSource is BitmapImage bitmapImage))
            {
                bitmapImage = new BitmapImage();

                BmpBitmapEncoder encoder = new BmpBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(bitmapSource));

                using (MemoryStream memoryStream = new MemoryStream())
                {
                    encoder.Save(memoryStream);
                    memoryStream.Position = 0;

                    bitmapImage.BeginInit();
                    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
                    bitmapImage.StreamSource = memoryStream;
                    bitmapImage.EndInit();
                }
            }

            return bitmapImage;
        }

4. GetContour 메서드 작성

		private void GetContour(string imagePath)
        {
            Mat src = new Mat(imagePath); 
            Mat edge = new Mat(); 
            Mat dst = src.Clone();

            OpenCvSharp.Point[][] contours;
            HierarchyIndex[] hierarchy;

            Cv2.InRange(src, new Scalar(0, 125, 125), new Scalar(100, 255, 255), edge); //사용자 지정 Scalar범위 픽셀 추출 Scalar(B, G, R) 어두운 색에서-밝은 색까지의 범위 안에서 찾는다.
            Cv2.FindContours(edge, out contours, out hierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxTC89KCOS); //Cv2.FindContours(원본 배열, 검출된 윤곽선, 계층 구조, 검색 방법, 근사 방법, 오프셋)

            List<OpenCvSharp.Point[]> new_contours = new List<OpenCvSharp.Point[]>();
            foreach (OpenCvSharp.Point[] p in contours)
            {
                double length = Cv2.ArcLength(p, true);
                if (length > 100)
                {
                    new_contours.Add(p);
                }
            }

            Cv2.DrawContours(dst, new_contours, -1, new Scalar(0, 0, 255), 2, LineTypes.AntiAlias, null, 1);

			BitmapSource bitmapSource = OpenCvSharp.WpfExtensions.BitmapSourceConverter.ToBitmapSource(dst); // Mat to BitmapSource
            ImageSource = ConvertBitmapSourceToBitmapImage(bitmapSource); //BitmapSource to BitmapImage
            
            Cv2.ImShow("dst", dst);
            Cv2.WaitKey(0); //시간 대기 함수의 값을 0으로 두어 키 입력이 있을때 까지 유지
        }

UploadeImage()에 GetContour()를 추가한다.

  		private void UploadImage()
        {
            Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
            openFileDialog.Filter = "Image files (*.jpg, *.jpeg, *.png) | *.jpg; *.jpeg; *.png";

            if (openFileDialog.ShowDialog() == true)
            {
                string imagePath = openFileDialog.FileName;

                ImageSource = new BitmapImage(new Uri(imagePath));

                GetContour(imagePath);
            }
        }

4. 결과

실행했을 때 MainWindow의 상태이다.
Mat 형식 데이터를 BitmapImage로 변환하여 Image에 바인딩했기 때문에 윤곽선이 검출된 상태의 이미지가 보인다.
new Scalar(0, 125, 125)에서 new Scalar(100, 255, 255)까지의 색 범위인 윤곽선을 찾아 new Scalar(0, 0, 255) 색으로 선을 그리도록 했기 때문에 첫 번째 줄의 노란색 사각형과 맨 아래 줄의 노란색 삼각형에 빨간 윤곽선이 그려진 것을 확인할 수 있다.

.ImShow로 띄운 window이다.
이미지의 원본사이즈 크기로 window가 생성되기 때문에 작게 보려면 resize가 필요할 것이다.

아래는 Console.WriteLine("new_contours count : " + new_contours.Count); 를 추가 작성하여 콘솔로 new_contours의 count를 출력한건데 그려진 윤곽선의 개 수를 확인할 수 있다.


5. 참조


6. 보완할 점

예제로 사용한 이미지는 깔끔하게 떨어지는 테두리를 갖고있기 때문에 윤곽선도 예쁘게 그려졌지만 복잡한 형상의 이미지의 윤곽선을 그리려고하면 지저분하게 보인다. 그럴땐 블러나 팽창, 침식 등의 메서드를 추가 적용하여 깔끔하게 그릴 수 있도록 처리하는 듯 하다.
윤곽선을 그리려고 OpenCVSharp 사용법을 알아본건 아니고 인식된 형태에 이벤트를 걸 수 있는 방법을 찾고있다. 되도록 MVVM 패턴을 준수하는 방식으로 찾아보려고 한다.

profile
안뇽하세용
post-custom-banner

0개의 댓글