ROI 이미지 저장 및 ROI 영역 사각형의 크기 조절 핸들(Resize Handle)을 WPF OpenCV 프로젝트에 구현하도록 하겠습니다.
지난 포스팅(#10)에서 ROI(관심 영역)를 자르는 기능까지 구현했는데, 글을 올리고 나서 보니 “저장하기“를 빼 먹었더군요. (가끔은 중요한 걸 깜빡하곤 하죠? 저만 그런 거 아니죠?)
그래서 오늘은 1. ROI 이미지 저장 기능을 마무리하고, 사용하다 보니 너무 불편했던 2. ROI 영역 수정(크기 조절) UI를 만들어보겠습니다.
ROI 이미지 저장 하기
사실 MainViewModel.cs에는 이미 SaveRoiImage 함수를 만들어 뒀으니, View(UI)에서 해당 함수를 호출해 주기만 하면 됩니다. MainWindow.xaml.cs의 MenuItem_Save_Click 이벤트 핸들러에 아래 코드를 넣어주세요. 코드를 보면 알겠지만, 이미지 저장을 위해 C#의 기본 SaveFileDialog 를 사용하여 경로를 받습니다.
private void MenuItem_Save_Click(object sender, RoutedEventArgs e)
{
// ROI 영역이 유효한지 체크
if (_currentRoiRect.Width <= 0 || _currentRoiRect.Height <= 0) return;
var vm = this.DataContext as MainViewModel;
if (vm != null)
{
Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
dlg.Filter = "PNG Image|*.png|JPEG Image|*.jpg;*.jpeg|Bitmap Image|*.bmp|All Files|*.*";
dlg.FileName = "ROI_Image"; // 기본 파일명
if (dlg.ShowDialog() == true)
{
// ViewModel의 저장 함수 호출
vm.SaveRoiImage(dlg.FileName, (int)_currentRoiRect.X, (int)_currentRoiRect.Y,
(int)_currentRoiRect.Width, (int)_currentRoiRect.Height);
// 저장 후 깔끔하게 ROI 사각형 숨기기
HideRoiAndHandles();
}
}
}
ROI 사각형 지우기
ROI를 그리고 나서 ‘자르기’나 ‘저장’을 하거나, 혹은 취소하고 싶어서 Clear 메뉴를 눌렀는데 빨간 사각형이 그대로 남아있으면 곤란하겠죠? 그래서 ROI 사각형과 핸들을 숨기는 공통 함수 HideRoiAndHandles()를 만들어서 여기저기서 호출하도록 하겠습니다.
private void HideRoiAndHandles()
{
RoiRect.Visibility = Visibility.Collapsed;
RoiRect.Width = 0;
RoiRect.Height = 0;
_currentDrawMode = DrawingMode.None;
Cursor = Cursors.Arrow;
}
// 자르기 버튼 클릭 시에도 호출
private void MenuItem_Crop_Click(object sender, RoutedEventArgs e)
{
if (_currentRoiRect.Width <= 0 || _currentRoiRect.Height <= 0) return;
var vm = this.DataContext as MainViewModel;
if (vm != null)
{
vm.CropImage((int)_currentRoiRect.X, (int)_currentRoiRect.Y, (int)_currentRoiRect.Width, (int)_currentRoiRect.Height);
HideRoiAndHandles(); // 화면 정리
FitImageToScreen();
}
}
Picker : Resize Handle
지금까지 구현한 ROI 기능의 치명적인 단점은 “한 번 그리면 끝“이라는 점입니다. 크기를 조금만 줄이고 싶은데, 다시 처음부터 그려야 한다면 사용자 입장에서 화가 나겠죠.
그래서 Resize Handle(크기 조절 손잡이), 흔히 ‘피커(Picker)‘라고 부르는 작은 사각형 8개를 만들어 보겠습니다. 상하좌우 모서리(4개) + 각 변의 중앙(4개) = 총 8개입니다.
UI (XAML): 스타일 및 핸들 추가
8개의 핸들을 일일이 설정하면 코드가 지저분해지니, <Style>을 사용해 공통 속성을 정의하겠습니다. MainWindow.xaml의 <Window.Resources> 섹션에 추가해 주세요.
<Window.Resources>
<Style x:Key="ResizeHandleStyle" TargetType="Rectangle">
<Setter Property="Width" Value="10" />
<Setter Property="Height" Value="10" />
<Setter Property="Fill" Value="White" />
<Setter Property="Stroke" Value="Black" />
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Window.Resources>
그리고 ZoomBorder 안의 RoiRect 아래에 8개의 핸들을 배치합니다. 여기서 중요한 건 Cursor 속성입니다. 마우스를 올렸을 때 화살표 모양이 바뀌어야 사용자가 “아, 이걸로 크기 조절하는구나!” 하고 알 수 있으니까요.
<Rectangle x:Name="Handle_TL" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNWSE" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_TR" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNESW" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_BL" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNESW" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_BR" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNWSE" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_Top" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNS" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_Bottom" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeNS" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_Left" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeWE" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
<Rectangle x:Name="Handle_Right" Style="{StaticResource ResizeHandleStyle}" Cursor="SizeWE" MouseLeftButtonDown="ResizeHandle_MouseDown"/>
Code Behind: 핸들 동작 구현
Enum 및 변수 선언
어떤 핸들을 잡았는지 알아야 하므로 MainWindow.xaml.cs 파일의 상단 부분에 ResizeDirection 열거형(Enum)을 만들고, 상태 변수를 선언합니다.
public enum ResizeDirection
{
None,
TopLeft, TopRight, BottomLeft, BottomRight, // 모서리
Top, Bottom, Left, Right // 변
}
// MainWindow 클래스 내부
private bool _isResizing = false;
private ResizeDirection _resizeDirection = ResizeDirection.None;
MouseDown
아래의 코드는 Picker 핸들을 클릭했을 때, “지금 내가 잡은 게 왼쪽 위(TopLeft) 핸들이다!” 라고 저장해두는 로직 입니다. 아래와 같이 MainWindow.xaml.cs 코드를 추가해 주세요.
private void ResizeHandle_MouseDown(object sender, MouseButtonEventArgs e)
{
var rect = sender as Rectangle;
if (rect == null) return;
if (rect == Handle_TL) _resizeDirection = ResizeDirection.TopLeft;
else if (rect == Handle_TR) _resizeDirection = ResizeDirection.TopRight;
// ... (나머지 방향들도 동일하게 매핑) ...
else _resizeDirection = ResizeDirection.None;
ImgCanvas.CaptureMouse(); // 중요: 마우스가 캔버스 밖으로 나가도 이벤트를 받기 위해
e.Handled = true; // 이벤트가 부모(ZoomBorder)로 전파되어 패닝(Pan)이 되지 않도록 막음
}
Picker 핸들 위치 업데이트 (UpdateRoiVisual)
이제 ROI 사각형이 그려질 때, 8개의 Picker 핸들도 사각형의 각 위치에 딱 맞춰서 따라다녀야 합니다. 기존 UpdateRoiVisual 함수에 핸들 위치를 잡는 코드를 추가합니다.
private void UpdateRoiVisual(Point start, Point end)
{
// ... (기존 ROI 좌표 계산 및 그리기 코드) ...
Canvas.SetLeft(RoiRect, screenX);
Canvas.SetTop(RoiRect, screenY);
// [추가] Resize Handle 위치 업데이트
// 모서리 핸들
UpdateResizeHandle(Handle_TL, screenX, screenY);
UpdateResizeHandle(Handle_TR, screenX + screenW, screenY);
UpdateResizeHandle(Handle_BL, screenX, screenY + screenH);
UpdateResizeHandle(Handle_BR, screenX + screenW, screenY + screenH);
// 변의 중앙 핸들
UpdateResizeHandle(Handle_Top, screenX + screenW / 2, screenY);
UpdateResizeHandle(Handle_Bottom, screenX + screenW / 2, screenY + screenH);
UpdateResizeHandle(Handle_Left, screenX, screenY + screenH / 2);
UpdateResizeHandle(Handle_Right, screenX + screenW, screenY + screenH / 2);
}
// 핸들 위치를 지정하는 헬퍼 함수
private void UpdateResizeHandle(Rectangle handle, double x, double y)
{
handle.Visibility = Visibility.Visible;
// 중요: 핸들 크기가 10x10이므로, 중심을 맞추기 위해 5씩 뺍니다.
Canvas.SetLeft(handle, x - 5);
Canvas.SetTop(handle, y - 5);
}
Tip
위 코드를 보면서 “왜 x - 5를 하나요?” 라고 궁금했을 수 있어 정리하고 가겠습니다. 핸들의 크기(Width, Height)를 10으로 설정했습니다. 만약 x, y 좌표에 그대로 그리면 핸들의 왼쪽 상단 모서리가 해당 위치에 오게 됩니다. 핸들의 중심(Center)이 정확히 ROI 사각형의 꼭지점에 오게 하려면, 핸들 크기의 절반(5)만큼 왼쪽 위로 이동 시켜야 합니다.
실행 결과
이제 빌드 후 실행해서 ROI를 그려보세요. 빨간 사각형 주변에 8개의 하얀 점(핸들)이 생기고, 마우스를 올리면 커서 모양이 바뀌는 것을 볼 수 있습니다.

하지만, 아직 핸들을 잡고 드래그(Drag)해도 크기가 변하지는 않습니다. (이 기능은 다음 포스팅을 위해 남겨뒀습니다. 너무 한 번에 다 하면 머리 아프니까요!) 하지만 UI적으로는 완벽한 준비가 끝났습니다. 다음 시간에는 이 핸들을 잡고 실제로 ROI 영역을 수정하는 로직을 구현해 보겠습니다.
참조 자료
SaveFileDialog 클래스:
https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.win32.savefiledialog
WPF Cursor 속성:
https://learn.microsoft.com/ko-kr/dotnet/api/system.windows.input.cursor
Canvas.SetLeft/Top:
https://learn.microsoft.com/ko-kr/dotnet/api/system.windows.controls.canvas.setleft