지난 포스팅에서 MVVM 구조를 잡았으니, 이제 WPF OpenCV 프로젝트의 심장이라고 할 수 있는 OpenCVService.cs를 작성할 차례입니다. 이 클래스는 OpenCV 라이브러리를 사용해 이미지를 불러오고, 영상처리를 수행하며, 결과를 UI에 보여줄 수 있는 형태로 변환하는 역할을 담당합니다 .
MVVM 패턴에서는 이 클래스를 Model 데이터를 다루는 서비스로 이해하시면 됩니다 .
영상처리를 위한 기능 설계
코드를 작성하기 전에 어떤 변수와 함수가 필요할지 먼저 정의해 보겠습니다 .
- 원본 이미지 (
_srcImage): 파일에서 불러온Mat형식의 원본 데이터입니다 . - 결과 이미지 (
_destImage): 영상처리가 완료된Mat데이터입니다 . - UI 표시용 버퍼 (
ImageSource): OpenCV의Mat데이터는 WPF UI(Image 컨트롤)에 바로 띄울 수 없습니다. 따라서BitmapSource형태로 변환된 별도의 버퍼가 필요합니다 .
필요한 핵심 기능(함수)은 다음과 같습니다.
- LoadImageAsync: 이미지를 비동기로 불러오는 함수 .
- ProcessImageAsync: 알고리즘을 적용하여 영상처리를 하는 함수 .
- Cleanup: 사용된 메모리(Mat)를 정리하는 함수 .
OpenCVService.cs 전체 코드 작성
설계한 내용을 바탕으로 코드를 구현해 보겠습니다. OpenCVService.cs 파일에 아래 코드를 작성합니다.
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions; // WPF 확장 기능 필수
namespace Vision_OpenCV_App
{
public class OpenCVService
{
// 1. 데이터 변수 선언
public Mat? _srcImage; // Original Image (OpenCV용)
public Mat? _destImage; // Processing Image (OpenCV용)
// 2. UI 바인딩용 이미지 버퍼
public ImageSource? _cachedOriginal;
public ImageSource? _cachedProcessed;
public OpenCVService()
{
}
// 3. 이미지 비동기 로드
public async Task LoadImageAsync(string filePath)
{
await Task.Run(() =>
{
// 이미지를 컬러 모드로 읽어옴 (그레이스케일 변환 등 후처리를 위해 원본은 컬러 유지)
var img = Cv2.ImRead(filePath, ImreadModes.Color);
_srcImage = img;
_destImage = _srcImage.Clone();
});
// UI 스레드에서 이미지 갱신
await Application.Current.Dispatcher.InvokeAsync(() =>
{
_cachedOriginal = _srcImage.ToWriteableBitmap();
_cachedProcessed = _destImage.ToWriteableBitmap();
});
}
// 4. 영상처리 로직 실행
public async Task<string> ProcessImageAsync(string algorithm, AlgorithmParameters parameters)
{
if (_srcImage == null || _srcImage.IsDisposed) return "Non Image";
string resultMessage = "Processing Complete";
await Task.Run(() =>
{
// 기존 결과 메모리 해제 후 원본 복사
if (_destImage != null) _destImage.Dispose();
_destImage = _srcImage.Clone();
switch (algorithm)
{
case "Threshold":
if (parameters is ThresholdParams thParams)
{
using (Mat gray = new Mat())
{
// 컬러 -> 그레이 변환 후 이진화 처리
Cv2.CvtColor(_srcImage, gray, ColorConversionCodes.BGR2GRAY);
Cv2.InRange(gray, new Scalar(thParams.ThresholdValue), new Scalar(thParams.ThresholdMax), _destImage);
resultMessage += $": {algorithm}";
}
}
break;
}
});
// 결과 이미지를 UI 포맷으로 변환
await Application.Current.Dispatcher.InvokeAsync(() =>
{
_cachedProcessed = _destImage.ToBitmapSource();
});
return resultMessage;
}
// 5. 메모리 해제 (중요!)
private void CleanupImages()
{
if (_srcImage != null) { _srcImage.Dispose(); _srcImage = null; }
if (_destImage != null) { _destImage.Dispose(); _destImage = null; }
}
public void Cleanup()
{
CleanupImages();
}
// 6. 람다식을 이용한 속성 반환
public ImageSource? GetOriginalImage() => _cachedOriginal;
public ImageSource? GetProcessedImage() => _cachedProcessed;
}
}
코드 핵심 포인트 분석
왜 Task와 async/await를 사용했나요?
실무 프로그램을 개발할 때 가장 중요한 것 중 하나가 UI 응답성입니다. 용량이 큰 고화질 이미지를 불러오거나 복잡한 영상처리를 할 때, 메인 스레드(UI 스레드)에서 처리하면 작업이 끝날 때까지 화면이 멈춰버립니다(일명 ‘렉’ 걸림) .
이를 방지하기 위해 “오래 걸리는 작업은 뒤에서 처리하고 결과만 알려줘!”라고 맡기기 위해 Task를 사용했습니다 .
왜 흑백 영상 처리에도 컬러로 저장하나요?
_srcImage를 무조건 3채널 컬러로 불러오는 이유는 확장성 때문입니다. 나중에 영상처리 결과 위에 빨간색이나 파란색으로 검출 영역(ROI)을 표시해야 할 때, 원본이 그레이스케일(1채널)이면 색상을 입힐 수 없기 때문입니다 .
수동 메모리 관리 (Dispose)
C#은 가비지 컬렉터(GC)가 메모리를 관리해 주지만, **OpenCV의 Mat 데이터는 Unmanaged Memory(관리되지 않는 메모리)**를 사용합니다 . 따라서 사용이 끝난 이미지는 Dispose()를 호출하여 직접 메모리를 반환해주어야 메모리 누수(Memory Leak)를 막을 수 있습니다 .
🚀 다음 단계
이제 영상처리를 위한 로직인 Model과 Service가 완성되었습니다. 다음 포스팅에서는 이 기능들을 화면과 연결해 줄 ViewModel을 구현하여, 실제 버튼을 눌렀을 때 이미지가 보이도록 만들어 보겠습니다 .