이번 포스팅에서는 ROI(관심 영역) 크기 조절 및 이동 기능 구현(Interaction)을 WPF OpenCV 프로젝트에 구현하겠습니다. 지난 포스팅(#11)에서 ROI 사각형 주변에 8개의 크기 조절 핸들(Picker)을 예쁘게 배치했습니다. 하지만 아직은 핸들을 잡고 흔들어도 아무 반응이 없었죠? (그림의 떡이었죠.)
오늘은 드디어 이 Picker 핸들에 생명을 불어넣어 ROI 크기를 늘리고 줄이는 기능, 그리고 잘못 그린 ROI를 통째로 이동 시키는 기능을 구현해 보겠습니다.
Picker Handle 구현 목표 및 동작 흐름
코드를 짜기 전에, 우리가 무엇을 하려는지 머릿속으로 시뮬레이션을 한번 돌려보죠.
- Resizing (크기 조절):
- 핸들 8개 중 하나를 클릭(
MouseDown) →_isResizing시작. - 마우스를 움직임(
MouseMove) → 핸들 방향에 따라 사각형 좌표 계산. - 마우스를 놓음(
MouseUp) → 조절 끝.
- 핸들 8개 중 하나를 클릭(
- Moving (이동):
- ROI 사각형 내부를 클릭(
MouseDown) →_isMovingRoi시작. - 마우스를 움직임(
MouseMove) → 마우스 이동 거리만큼 사각형 이동. - 마우스를 놓음(
MouseUp) → 이동 끝.
- ROI 사각형 내부를 클릭(
자, 이 흐름대로 코드를 하나씩 채워 넣어 봅시다.
ROI Resizing
아무래도 이번 포스팅에 가장 복잡한 부분이 될 것 같습니다. ZoomBorder_MouseMove 함수에서 마우스가 움직일 때마다 좌표를 다시 계산되어야 하거든요.
지난 포스팅에서 핸들을 클릭했을 때 _resizeDirection에 방향(TopLeft, Bottom 등)을 저장해 뒀었죠? (기억하나요? 기억에 문제가 있다면 다시 한번 쓰윽~ 보고 와주세요!) 아무튼 그 방향에 따라 계산 식이 달라지게 됩니다. 그래서 기존 ZoomBorder_MouseMove 함수에 아래 내용을 추가해 주세요.
// MainWindow.xaml.cs > ZoomBorder_MouseMove 내부
else if (_isResizing || ImgView.Source != null)
{
var bitmap = ImgView.Source as BitmapSource;
Point currentPos = e.GetPosition(ImgView);
// 1. 이미지 범위 밖으로 나가지 못하게 제한 (Clamp)
if (currentPos.X < 0) currentPos.X = 0;
if (currentPos.Y < 0) currentPos.Y = 0;
if (currentPos.X > bitmap.PixelWidth) currentPos.X = bitmap.PixelWidth;
if (currentPos.Y > bitmap.PixelHeight) currentPos.Y = bitmap.PixelHeight;
double newX = _currentRoiRect.X;
double newY = _currentRoiRect.Y;
double newW = _currentRoiRect.Width;
double newH = _currentRoiRect.Height;
// 2. 방향(Direction)에 따른 좌표 재계산
switch (_resizeDirection)
{
// 모서리 핸들 (대각선 조절)
case ResizeDirection.TopLeft:
double right = _currentRoiRect.Right;
double bottom = _currentRoiRect.Bottom;
newX = Math.Min(currentPos.X, right - 1); // 너비가 음수가 되지 않게 방지
newY = Math.Min(currentPos.Y, bottom - 1);
newW = right - newX;
newH = bottom - newY;
break;
case ResizeDirection.TopRight:
double left = _currentRoiRect.Left;
bottom = _currentRoiRect.Bottom;
newY = Math.Min(currentPos.Y, bottom - 1);
newW = Math.Max(currentPos.X - left, 1);
newH = bottom - newY;
break;
case ResizeDirection.BottomLeft:
right = _currentRoiRect.Right;
double top = _currentRoiRect.Top;
newX = Math.Min(currentPos.X, right - 1);
newW = right - newX;
newH = Math.Max(currentPos.Y - top, 1);
break;
case ResizeDirection.BottomRight:
left = _currentRoiRect.Left;
top = _currentRoiRect.Top;
newW = Math.Max(currentPos.X - left, 1);
newH = Math.Max(currentPos.Y - top, 1);
break;
// 변의 중앙 핸들 (직선 조절)
case ResizeDirection.Top:
bottom = _currentRoiRect.Bottom;
newY = Math.Min(currentPos.Y, bottom - 1);
newH = bottom - newY;
break;
case ResizeDirection.Bottom:
top = _currentRoiRect.Top;
newH = Math.Max(currentPos.Y - top, 1);
break;
case ResizeDirection.Left:
right = _currentRoiRect.Right;
newX = Math.Min(currentPos.X, right - 1);
newW = right - newX;
break;
case ResizeDirection.Right:
left = _currentRoiRect.Left;
newW = Math.Max(currentPos.X - left, 1);
break;
}
// 3. 시각적 업데이트
UpdateRoiVisual(new Point(newX, newY), new Point(newX + newW, newY + newH));
}
위 코드가 길어 보이지만 사실 패턴은 단순합니다. 예를 들어 TopLeft 핸들을 잡고 움직이면, 오른쪽(Right)과 바닥(Bottom) 좌표는 고정 시키고, 마우스 위치에 따라 왼쪽(X)과 위쪽(Y) 좌표만 갱신하는 방식입니다.
ROI Moving: ROI 사각형 이동
ROI 사각형 즉 관심 영역을 그리다가 “아, 크기는 딱 맞는데 위치가 조금 빗나갔네?” 이럴 때 다시 그리지 않고 슥~ 옮길 수 있어야 진짜 편리한 UI 가 되겠네요. 이제 이동 상태와 위치 오차를 저장할 변수를 선언하고 ROI 영역의 이동에 따른 이벤트 처리를 아래와 같이 진행 하겠습니다.
변수 선언
아래 코드와 같이 이동 상태와 클릭 위치의 오차(Offset)를 저장할 변수를 선언합니다.
// MainWindow.xaml.cs 내부
// ROI 사각형 이동 상태 관련 변수
private bool _isMovingRoi = false;
private Vector _moveOffset; // 클릭한 지점과 사각형 좌상단 사이의 거리(오차)
MouseEvent: MouseDown/MouseMove
먼저 ZoomBorder_MouseDown 함수에 “ROI 내부를 클릭했는지” 확인하는 로직을 추가합니다.
else if (e.ChangedButton == MouseButton.Left && ImgView.Source != null)
{
Point mousePos = e.GetPosition(ImgView);
var bitmap = ImgView.Source as BitmapSource;
// ROI 사각형 내부를 클릭했는지 확인 (이동 시작)
if (RoiRect.Visibility == Visibility.Visible && _currentRoiRect.Contains(mousePos))
{
_isMovingRoi = true;
// 중요: 사각형의 (0,0)이 마우스에 딱 붙는 것을 방지하기 위해 오차(Offset)를 저장함
_moveOffset = mousePos - new Point(_currentRoiRect.X, _currentRoiRect.Y);
ImgCanvas.CaptureMouse();
return; // 그리기 모드로 넘어가지 않도록 리턴
}
// ... (기존 그리기 시작 로직) ...
}
이제 마우스를 움직이면 저장해둔 오차(_moveOffset)를 반영해서 ZoomBorder_MouseMove() 함수에서 위치를 옮기는 코드를 추가합니다.
// ZoomBorder_MouseMove 내부에 추가
else if (_isMovingRoi && ImgView.Source != null)
{
var bitmap = ImgView.Source as BitmapSource;
Point currentPos = e.GetPosition(ImgView);
// Offset 을 적용하여 새 위치 계산
double newX = currentPos.X - _moveOffset.X;
double newY = currentPos.Y - _moveOffset.Y;
double w = _currentRoiRect.Width;
double h = _currentRoiRect.Height;
// 이미지 영역 밖으로 나가지 않도록 제한 (Clamping)
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + w > bitmap.PixelWidth) newX = bitmap.PixelWidth - w;
if (newY + h > bitmap.PixelHeight) newY = bitmap.PixelHeight - h;
// 이동 된 위치로 업데이트
UpdateRoiVisual(new Point(newX, newY), new Point(newX + w, newY + h));
}
MouseEvent: MouseUp
이제 클릭해서 드래그 하던 왼쪽 마우스 버튼을 떼면, Resizing이든 Moving이든 모든 동작을 멈추고 상태를 초기화해야 합니다. ZoomBorder_MouseUp() 함수에 아래의 코드로 업데이트 해주세요.
private void ZoomBorder_MouseUp(object sender, MouseButtonEventArgs e)
{
if (_isDragging && e.ChangedButton == MouseButton.Middle)
{
// ... (기존 Pan 종료) ...
}
else if(_isRoiDrawing && e.ChangedButton == MouseButton.Left)
{
_isRoiDrawing = false;
ImgCanvas.ReleaseMouseCapture();
}
else if(_isResizing)
{
_isResizing = false;
_resizeDirection = ResizeDirection.None;
ImgCanvas.ReleaseMouseCapture();
}
else if(_isMovingRoi)
{
_isMovingRoi = false;
ImgCanvas.ReleaseMouseCapture();
}
}
실행 결과
드디어 ROI 기능이 완성되었습니다! 이제 마우스로 ROI 관심 영역 사각형을 그리고, Picker 핸들을 잡아 늘리고, ROI 관심 영역이 마음에 안 들면 통째로 옮길 수도 있습니다. 잘 따라 왔다면 아래에 실행 영상처럼 자알~ 동작할 겁니다.
아래의 첫 번째 실행 영상은 프로젝트 빌드 후 Picker 핸들 조절 실행 영상입니다.
두 번째 실행 영상은 ROI 관심 영역 사각형의 이동과 이전 포스팅(#10)에서 작성했던 이미지 잘라내기 까지 동작하는 영상이니 참고하여 주세요.
단계 별로 코드를 추가하느라 꽤 긴 여정이었지만, WPF 좌표계 변환, 마우스 이벤트 처리, MVVM과의 연동까지 깊이 있게 다룰 수 있었습니다. 잘 따라오셨다면 에러 없이 아주 부드럽게 동작할 겁니다. (확신합니다!)
다음 포스팅에서는 또 다른 재미있는 WPF 기반의 OpenCV 기능을 WPF에 얹어보도록 하겠습니다. 기대해 주세요!
참조 자료
Vector 구조체: 점(Point)과 점 사이의 ‘거리와 방향’을 나타낼 때 사용되며,이동 로직에서
_moveOffset을 계산할 때 필요합니다.
https://learn.microsoft.com/ko-kr/dotnet/api/system.windows.vector
BitmapSource: WPF에서 이미지를 다루는 기본 class.
픽셀 너비/높이(PixelWidth,PixelHeight) 정보를 가져올 때 사용 합니다.
https://learn.microsoft.com/ko-kr/dotnet/api/system.windows.media.imaging.bitmapsource
Math.Min / Math.Max: 값의 범위를 제한(Clamping)할 때 필수적인 함수.
ROI가 이미지 밖으로 나가지 않게 막아줍니다.
https://learn.microsoft.com/ko-kr/dotnet/api/system.math.min