이미지 처리 할 일이 생겨서... OpenCVSharp 사용법을 찾던 중 기억해둘 내용을 작성한다.
WPF 앱(.NET Framework)로 프로젝트를 생성한다.
이때 프레임워크는 4.8 이상이어야 한다.
이유는 WpfExtensions를 설치해야 하는데, 아래 콘솔 내용에 따르면 4.8이상부터 지원하기 때문이다.
아래 이미지는 프레임워크 4.7.2에서 WpfExtensions를 설치하려고 했을 때 콘솔에 출력되는 에러 메시지이다.
OpenCV...를 검색해서 OpenCVSharp4, OpenCVSharp4.runtime.win, OpenCVSharp.WpfExtensions를 설치한다.
OpenCVSharp4.runtime.win은 Windows에서 작동하기 위한 내부 구현 패키지이고
OpenCVSharp.WpfExtensions는 WPF에서 사용하는 확장 라이브러리이다.
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>
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에서 다운로드 받은 무료 이미지이다.
이미지를 업로드하면 이런 화면이 된다.
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)는
다음의 범위이다.
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);
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;
}
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);
}
}
실행했을 때 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를 출력한건데 그려진 윤곽선의 개 수를 확인할 수 있다.
예제로 사용한 이미지는 깔끔하게 떨어지는 테두리를 갖고있기 때문에 윤곽선도 예쁘게 그려졌지만 복잡한 형상의 이미지의 윤곽선을 그리려고하면 지저분하게 보인다. 그럴땐 블러나 팽창, 침식 등의 메서드를 추가 적용하여 깔끔하게 그릴 수 있도록 처리하는 듯 하다.
윤곽선을 그리려고 OpenCVSharp 사용법을 알아본건 아니고 인식된 형태에 이벤트를 걸 수 있는 방법을 찾고있다. 되도록 MVVM 패턴을 준수하는 방식으로 찾아보려고 한다.