(3) MVVM 패턴 적용하여 코드 개선_2

00·2025년 1월 19일

3단계: ProcessImage 메서드 구현

ProcessImage 메서드는 템플릿 매칭을 사용하여 게임 화면 영역을 검출하고, 검출된 영역을 GameWindowRect 속성에 저장한다.

// ViewModels/CameraViewModel.cs
// ... (생략) ...

private void ProcessImage(Mat image)
{
    try
    {
        // 템플릿 이미지 파일 경로
        string[] templatePaths = { "Resources/top_left.png", "Resources/bottom_right.png" }; // 템플릿 이미지 파일 경로를 Resources 폴더로 변경

        // 각 템플릿 이미지에 대한 매칭 결과를 저장할 리스트
        List<Point> matchPoints = new List<Point>();

        // 각 템플릿 이미지에 대해 템플릿 매칭 수행
        foreach (string templatePath in templatePaths)
        {
            // 템플릿 이미지 로드
            Mat template = Cv2.ImRead(templatePath, ImreadModes.Grayscale);

            // 템플릿 이미지 로드 확인
            if (template.Empty())
            {
                Console.WriteLine($"템플릿 이미지 로드 실패: {templatePath}");
                continue;
            }

            // 템플릿 이미지 크기 확인
            if (template.Width == 0 || template.Height == 0)
            {
                Console.WriteLine($"템플릿 이미지 크기 오류: {templatePath}");
                continue;
            }

            // 이미지 타입 확인
            if (image.Type() != template.Type())
            {
                Console.WriteLine($"이미지 타입 불일치: 입력 이미지 - {image.Type()}, 템플릿 이미지 - {template.Type()}");
                Cv2.CvtColor(image, image, ColorConversionCodes.BGR2GRAY);
            }

            // 템플릿 이미지가 입력 이미지보다 큰 경우 크기 조정
            if (template.Width > image.Width || template.Height > image.Height)
            {
                Cv2.Resize(template, template, new OpenCvSharp.Size(image.Width / 2, image.Height / 2));
            }

            // 템플릿 매칭 결과를 저장할 Mat 객체 생성
            Mat result = new Mat();

            // MatchTemplate() 함수를 사용하여 템플릿 매칭 수행
            Cv2.MatchTemplate(image, template, result, TemplateMatchModes.CCoeffNormed);

            // 매칭 결과에서 최댓값과 그 위치를 찾음
            double minVal, maxVal;
            Point minLoc, maxLoc;
            Cv2.MinMaxLoc(result, out minVal, out maxVal, out minLoc, out maxLoc);

            // 최댓값이 임계값보다 크면 객체를 찾은 것으로 판단
            if (maxVal > 0.8)
            {
                // 템플릿의 크기를 고려하여 객체의 중심 좌표 계산
                Point center = new Point(maxLoc.X + template.Width / 2, maxLoc.Y + template.Height / 2);
                matchPoints.Add(center);
            }
        }

        // 매칭 결과를 이용하여 게임 화면 영역 검출
        if (matchPoints.Count == 2)
        {
            Point topLeft = matchPoints[0];
            Point bottomRight = matchPoints[1];

            int x = (int)topLeft.X;
            int y = (int)topLeft.Y;
            int width = (int)(bottomRight.X - topLeft.X);
            int height = (int)(bottomRight.Y - topLeft.Y);
            Rect gameWindowRect = new Rect(x, y, width, height);

            // 게임 화면 영역 업데이트
            GameWindowRect = gameWindowRect;
        }
    }
    catch (OpenCvSharp.OpenCVException ex)
    {
        Console.WriteLine($"OpenCV 예외 발생: {ex.Message}");
    }
    catch (FileNotFoundException ex)
    {
        Console.WriteLine($"파일을 찾을 수 없습니다: {ex.FileName}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"예외 발생: {ex.Message}");
    }
}

// ... (생략) ...

4단계: View (MainWindow.xaml) 수정

MainWindow.xamlCameraViewModel의 속성과 Command를 바인딩하여 웹캠 화면, 게임 화면 영역, 캡처 시작/중지 버튼을 표시한다.

<Window ...>
    <Window.DataContext>
        <local:CameraViewModel />
    </Window.DataContext>
    <Grid>
        <Image Source="{Binding CameraImage}" />
        <Rectangle Stroke="Red" StrokeThickness="2" 
                   Visibility="{Binding GameWindowRect, Converter={StaticResource RectToVisibilityConverter}}" 
                   Width="{Binding GameWindowRect.Width}" Height="{Binding GameWindowRect.Height}" 
                   Margin="{Binding GameWindowRect.Left}, {Binding GameWindowRect.Top}, 0, 0" />
        <Button Content="캡처 시작" Command="{Binding StartCaptureCommand}" />
        <Button Content="캡처 중지" Command="{Binding StopCaptureCommand}" />
    </Grid>
</Window>

5단계: RectToVisibilityConverter 추가

RectToVisibilityConverterGameWindowRect 속성 값에 따라 Rectangle 컨트롤의 Visibility를 설정하는 컨버터다.

// Converters/RectToVisibilityConverter.cs
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace OpenCvSharpProjects
{
    public class RectToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Rect rect && !rect.IsEmpty)
                return Visibility.Visible;
            else
                return Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

6단계: ViewModelBase 클래스 및 RelayCommand 클래스 구현

나는 ViewModelBaseRelayCommand 클래스를 직접 구현할 것이다. 굳이 안해도 되는 단계니까, 혹시 내 글을 보면서 만드시는 분이 있다면 참고바란다.

ViewModelBase 클래스와 RelayCommand 클래스는 MVVM 패턴을 구현할 때 자주 사용되는 클래스들이다. ViewModelBase 클래스를 사용하여 ViewModel 클래스에서 반복적인 코드를 줄이고, RelayCommand 클래스를 사용하여 Command를 간결하게 구현할 수 있다.

ViewModelBase 클래스

  • INotifyPropertyChanged 인터페이스 구현: ViewModelBase 클래스는 INotifyPropertyChanged 인터페이스를 구현하여 ViewModel의 속성 값이 변경될 때 View에 알림을 전달하는 역할을 한다. 이를 통해 View는 ViewModel의 변경 사항을 자동으로 반영하여 UI를 업데이트할 수 있다.
  • 속성 변경 알림: ViewModelBase 클래스는 OnPropertyChanged() 메서드를 제공하여 속성 변경 알림을 발생시킨다. ViewModel에서 속성 값을 변경할 때마다 OnPropertyChanged() 메서드를 호출하면 View에 변경 사항이 전달된다.
  • 코드 중복 제거: ViewModelBase 클래스를 사용하면 모든 ViewModel에서 INotifyPropertyChanged 인터페이스를 구현하고 OnPropertyChanged() 메서드를 작성해야 하는 번거로움을 줄일 수 있다.

RelayCommand 클래스

  • ICommand 인터페이스 구현: RelayCommand 클래스는 ICommand 인터페이스를 구현하여 View에서 ViewModel의 메서드를 실행할 수 있도록 하는 Command를 구현하는 역할을 한다.
  • 메서드 실행: RelayCommand 객체를 생성할 때 ViewModel의 메서드를 인자로 전달하면, View에서 해당 Command를 실행할 때 전달된 메서드가 호출된다.
  • CanExecute: RelayCommandCanExecute() 메서드를 통해 Command 실행 가능 여부를 제어할 수 있다. 예를 들어, 특정 조건을 만족해야만 버튼이 활성화되도록 할 수 있다.
  • 코드 간결화: RelayCommand 클래스를 사용하면 View에서 이벤트 핸들러를 작성하지 않고도 ViewModel의 메서드를 직접 실행할 수 있어 코드가 간결해진다.

사용 방법

  1. ViewModelBase 클래스 상속: ViewModel 클래스에서 ViewModelBase 클래스를 상속받습니다.
  2. OnPropertyChanged() 호출: ViewModel에서 속성 값이 변경될 때마다 OnPropertyChanged() 메서드를 호출하여 View에 변경 사항을 알립니다.
  3. RelayCommand 생성: ViewModel에서 Command를 정의할 때 RelayCommand 클래스를 사용합니다. ViewModel에서 RelayCommand 객체를 생성하고, 실행할 메서드를 인자로 전달합니다. (RelayCommand 생성자에 실행할 메서드와 실행 조건(선택 사항임)을 전달합니다.)
  4. Command 바인딩: View에서 RelayCommand 객체를 컨트롤 (예: Button)의 Command 속성에 바인딩합니다.
// ViewModels/ViewModelBase.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace OpenCvSharpProjects
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

// ViewModels/RelayCommand.cs
using System;
using System.Windows.Input;

namespace OpenCvSharpProjects
{
    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public RelayCommand(Action execute, Func<bool> canExecute = null)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute();
        }

        public void Execute(object parameter)
        {
            _execute();
        }
    }
}

7단계: App.xaml에 리소스 추가

RectToVisibilityConverter를 사용하기 위해 App.xaml에 리소스를 추가한다.

<Application ...>
    <Application.Resources>
        <local:RectToVisibilityConverter x:Key="RectToVisibilityConverter" />
    </Application.Resources>
</Application>

MVVM 패턴을 적용하면 코드가 더욱 구조화되고, 각 요소의 역할이 명확해져 유지보수와 테스트가 용이해진다.

0개의 댓글