Geometric Transform(기하학적 변환)에서 이미지의 이동과 회전, 확대/축소에 대해 정리하고, WPF OpenCV 프로젝트에 구현하도록 하겠습니다. 지난 포스팅(#21)에서는 히스토그램 버그를 잡느라 잠시 쉬어갔었죠? 오늘은 드디어 잠시 미뤄뒀던 Geometric Transform(기하학적 변환)을 구현해 볼 차례입니다.
그동안 우리는 픽셀의 ‘색상’이나 ‘밝기’를 바꾸는 작업(히스토그램, 정규화 등)을 주로 했습니다. 하지만 오늘 할 작업은 픽셀의 ‘위치’를 바꾸는 작업입니다. 이미지를 옮기고(Translation), 돌리고(Rotation), 크기를 조절하는(Scaling) 것이죠.
잠깐! 오해 하지 말아요 “어? 마우스 휠로 확대/축소하는 기능은 예전에 만들지 않았나요?” -> 맞습니다. 하지만 그건 ‘보여주는 화면(View)’만 확대한 것이고, 실제 ‘이미지 데이터’는 그대로 였던거죠. 이번 포스팅에서 하는 것은 원본 이미지 데이터 자체를 변형 시켜서 새로운 이미지를 만들어내는 작업입니다. (포토샵의 ‘이미지 변형’ 기능과 같습니다.)
Geometric Transform: Matrix
이미지를 변형하려면 수학, 특히 행렬(Matrix)이 등장합니다. (머리 아프시죠? 저도 그렇습니다.)
하지만 OpenCV가 계산은 다 해주니, 우리는 “어떤 행렬을 던져줘야 하는지”만 알면 됩니다.
Translation: 이동
원래 좌표 를 x축으로 , y축으로 만큼 옮기려면 아래와 같은 2×3 행렬이 필요합니다.
Rotation(회전) & Scale (크기)
회전은 조금 복잡해서 , 삼각함수가 들어갑니다. 하지만 걱정 마세요.
OpenCV는 Cv2.GetRotationMatrix2D() 라는 함수를 제공합니다. “중심 점, 각도, 비율”만 넣어주면 알아서 복잡한 행렬을 뱉어줍니다. (캬~악~ 퉤~!)
Interpolation : 보간법
이미지를 회전하거나 늘리면, 픽셀 좌표가 딱 떨어지지 않고 소수점(10.5, 20.7 등)으로 나올 때가 있습니다. 이때 빈 공간을 어떻게 채울지 결정하는 것이 보간법입니다.
- Nearest: 가장 가까운 픽셀 값을 가져옴 (빠르지만 계단 현상 발생)
- Linear: 주변 4개 픽셀을 섞어서 계산 (가장 무난하고 부드러움)
- Cubic / Lanczos: 더 정교하지만 계산이 느림
구현 요약
이번 구현에는 “결과 이미지 저장” 기능도 이번에 추가했습니다. 사실 이전 포스팅 까지 진행하였던 영상 처리된 이미지들은 저장하는 기능이 없었죠. 그래서 언젠가 추가해야 하는 기능이라, 이참에 변환한 이미지를 저장할 수 있도록 추가 마우스 우-클릭 팝업 메뉴에 넣었으니 참고 하세요.
- AlgorithmParameters.cs:
GeometricParams클래스 추가 (이동, 회전, 스케일, 보간법 속성) - MainViewModel.cs:
SaveProcessedImage함수 추가 (저장 기능) - MainWindow.xaml: 슬라이더 UI 및 우클릭 메뉴(ContextMenu) 추가
- OpenCVService.cs:
WarpAffine() 및GetRotationMatrix2D() 적용
Step 1: AlgorithmParameters Class (GeometricParams)
AlgorithmParameters.cs 파일에 아래와 같이 GeometricParams 클래스를 추가합니다. 이동(X, Y), 회전(Angle), 확대(Scale), 그리고 보간법(Interpolation)을 담을 클래스입니다.
public class GeometricParams : AlgorithmParameters
{
// 이동 (Translation)
private double _moveX = 0;
public double MoveX
{
get => _moveX;
set { if (_moveX != value) { _moveX = value; OnPropertyChanged(); } }
}
private double _moveY = 0;
public double MoveY
{
get => _moveY;
set { if (_moveY != value) { _moveY = value; OnPropertyChanged(); } }
}
// 회전 (Rotation) -180 ~ 180도
private double _angle = 0;
public double Angle
{
get => _angle;
set { if (_angle != value) { _angle = value; OnPropertyChanged(); } }
}
// 확대/축소 (Scale) 1.0 = 원본
private double _scale = 1.0;
public double Scale
{
get => _scale;
set { if (_scale != value) { _scale = value; OnPropertyChanged(); } }
}
// 보간법 (Interpolation)
private InterpolationFlags _interpolation = InterpolationFlags.Linear;
public InterpolationFlags Interpolation
{
get => _interpolation;
set { if (_interpolation != value) { _interpolation = value; OnPropertyChanged(); } }
}
// 콤보박스용 리스트
public List<InterpolationFlags> InterpolationSource { get; } = new List<InterpolationFlags>
{
InterpolationFlags.Nearest,
InterpolationFlags.Linear,
InterpolationFlags.Cubic,
InterpolationFlags.Lanczos4
};
}
Step 2: ViewModel (MainViewModel.cs)
MainViewModel.cs 파일에는 아래와 같이 생성자 함수에 Geometric Transformation 항목을 추가하고, CreateParametersForAlgorithm() 함수에 사용자가 Geometric 을 선택했을 때 GeometricParams 클래스 객체를 생성하도록 코드를 추가합니다. 신규 함수 하나를 더 추가했는데요. 알고리즘이 선택되고 영상처리가 완료된 이미지를 저장할 방법이 없어 해당 함수(SaveProcessedImage)를 추가 구현하였습니다. 어려운 코드가 아니니 복사해서 붙여 넣어 주세요. 마지막으로 방금 추가한 함수(SaveProcessedImage)는 영상 처리된 이미지 위에서 마우스 우클릭 발생 시 contextMenu에서 나타나게 하고 처리 하기 위해 Command 바인딩 코드를 추가하였습니다. (Command 바인딩 관련 내용 기억하죠?)
이제 [이미지 저장]을 눌러 변환된 결과물을 파일로 남겨 보세요.
// 1. 알고리즘 목록 추가 (생성자 함수: MainViewModel())
AlgorithmList = new ObservableCollection<string>
{
// ... 기존 목록 ...
"CLAHE",
"Geometric Transformation" // [추가]
};
// 2. 파라미터 생성 로직 : CreateParametersForAlgorithm()
case "Geometric Transformation":
CurrentParameters = new GeometricParams();
break;
// 3. [신규 기능] 이미지 저장 메서드 추가
private void SaveProcessedImage(object obj)
{
// 처리된 이미지가 없으면 중단
if (_cvServices.GetProcessedImage() == null)
{
MessageBox.Show("저장할 처리된 이미지가 없습니다.");
return;
}
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "PNG Image|*.png|JPEG Image|*.jpg;*.jpeg|Bitmap Image|*.bmp|All Files|*.*";
dlg.FileName = "Processed_Image";
if (dlg.ShowDialog() == true)
{
try
{
_cvServices.SaveProcessedImage(dlg.FileName); // Service 호출
AnalysisResult = $"이미지 저장 완료: {dlg.FileName}";
}
catch (Exception ex)
{
MessageBox.Show($"저장 실패: {ex.Message}");
}
}
}
// 4. 커맨드 연결
public ICommand SaveProcessedImageCommand => new RelayCommand(SaveProcessedImage);
Step 3: UI(view – MainWindow.xaml)
MainWindow.xaml 파일에는 UI(View)에서 나타낼 GeometricParams 클래스 객체들을 데이터 바인딩하고 표시 하기 위해 DataTemplate 를 <Window.Resource> </Window.Resource> 사이에 추가합니다. 아~ 한가지 더! 영상 처리된 이미지를 저장하는 메뉴를 추가해야 하니까, <ContextMenu> <ContextMenu> 에도 관련 코드를 추가해 주세요. (아래 코드를 참고해 주세요.)
슬라이더가 많아서 코드가 좀 깁니다. 핵심은 각 파라미터를 조절할 수 있게 만든 것과, 이미지 위에서 우클릭했을 때 저장 메뉴가 나오도록 ContextMenu를 추가한 것입니다.
<ContextMenu x:Key="DrawingContextMenu" >
<Separator />
<MenuItem Header="Save Processed Image (다른 이름으로 저장)" Command="{Binding SaveProcessedImageCommand}" />
</ContextMenu>
<DataTemplate DataType="{x:Type local:GeometricParams}">
<StackPanel>
<TextBlock Text="Geometric Transformation" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Interpolation (보간법)" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding InterpolationSource}" SelectedItem="{Binding Interpolation}" Margin="0,2,0,0" Height="25"/>
<TextBlock Text="Move X / Y (이동)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="-500" Maximum="500" Value="{Binding MoveX}" />
<TextBox Grid.Column="1" Text="{Binding MoveX}" TextAlignment="Center" />
</Grid>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="-500" Maximum="500" Value="{Binding MoveY}" />
<TextBox Grid.Column="1" Text="{Binding MoveY}" TextAlignment="Center" />
</Grid>
<TextBlock Text="Rotation Angle (회전)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="-180" Maximum="180" Value="{Binding Angle}" />
<TextBox Grid.Column="1" Text="{Binding Angle}" TextAlignment="Center" />
</Grid>
<TextBlock Text="Scale Factor (확대/축소)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="0.1" Maximum="3.0" Value="{Binding Scale}" TickFrequency="0.1" IsSnapToTickEnabled="True"/>
<TextBox Grid.Column="1" Text="{Binding Scale}" TextAlignment="Center" />
</Grid>
<TextBlock Text="* 중심축(Center) 기준으로 회전/확대가 적용됩니다." Foreground="Gray" FontSize="11" Margin="0,5,0,0"/>
</StackPanel>
</DataTemplate>
Step 4: OpenCVService
OpenCVService.cs 파일에는 영상 처리를 담당하는 ProcessImageAsync() 함수에 Geometric 처리 부분을 추가합니다. 그리고 영상 처리 후 저장하는 함수를 MainViewModel.cs 파일내에 SaveProcessedImage() 함수를 만들었는데요. 영상 즉 이미지의 저장은 결국 OpenCV 의 영역입니다. 최종적으로 이미지를 저장하는 함수를 여기서 처리하도록 함수를 추가하였습니다. 아래 코드를 참고해 주세요. 여기서 팁! 회전과 스케일은 GetRotationMatrix2D로 행렬을 얻고, 이동(Translation)은 그 행렬의 마지막 열(Column) 값을 수정해서 한 번에 적용합니다.
case "Geometric Transformation":
if (parameters is GeometricParams geoParams)
{
// 1. [회전 & 스케일 행렬 생성]
// 이미지의 정중앙을 기준점(Center)으로 잡습니다.
Point2f center = new Point2f(_srcImage.Width / 2.0f, _srcImage.Height / 2.0f);
// OpenCV가 알아서 회전+스케일 행렬(2x3)을 만들어줍니다. (참 편하죠?)
Mat matrix = Cv2.GetRotationMatrix2D(center, geoParams.Angle, geoParams.Scale);
// 2. [이동(Translation) 추가]
// 생성된 행렬의 [0, 2]는 X축 이동, [1, 2]는 Y축 이동 값을 담당합니다.
// 여기에 사용자가 입력한 Move 값을 더해줍니다.
matrix.Set(0, 2, matrix.At<double>(0, 2) + geoParams.MoveX);
matrix.Set(1, 2, matrix.At<double>(1, 2) + geoParams.MoveY);
// 3. [WarpAffine 적용]
// 최종적으로 행렬을 이용해 이미지를 변환합니다.
// BorderTypes.Constant: 변환 후 빈 공간은 검은색(0)으로 채웁니다.
Cv2.WarpAffine(_srcImage, _destImage, matrix, _srcImage.Size(),
geoParams.Interpolation, BorderTypes.Constant, Scalar.All(0));
matrix.Dispose(); // 행렬 메모리 해제
resultMessage += $": Geometric (Move:{geoParams.MoveX},{geoParams.MoveY} | Rot:{geoParams.Angle} | Scale:{geoParams.Scale})";
}
break;
// ... (아래는 저장 함수 구현) ...
public void SaveProcessedImage(string filePath)
{
if (_destImage == null || _destImage.IsDisposed) return;
_destImage.SaveImage(filePath);
}
실행 결과
후~ 이전 포스팅들 보다 조금 길었네요. 이미지의 기하학적 변환이라는 주제를 가지고 서두에 개념 정리 부분을 넣으면서 조금 길게 진행해 왔습니다. 이제 빌드를 진행해 주세요. 에러가 당연히 없으니까 아래의 이미지들이 나왔겠죠? 이미지를 불러와 Geometric 을 선택하고, 각 파라미터들을 통해 이동, 확대, 축소, 회전 등을 진행해 보세요. 나름 소소한 재미가 느껴질 거라 생각 합니다 만…
자, 이제 이미지를 불러오고 Geometric Transformation을 선택한 뒤 슬라이더를 움직여 보세요.
이동: X, Y 슬라이더를 움직이면 이미지가 캔버스 안에서 이동합니다. 빈 공간은 검은색으로 채워지죠?
회전: 45도로 돌려보세요. 중심 점을 기준으로 예쁘게 돌아갑니다.
확대/축소: 이미지를 키우거나 줄여보세요. Interpolation을 Nearest로 하면 픽셀이 깨져 보이고, Linear나 Cubic으로 하면 부드럽게 보일 것입니다.
아래는 불러 온 원본 이미지입니다.

아래 왼쪽 이미지는 이미지를 이동(X축: 300, Y축: 250)한 이미지이고, 오른쪽은 반 시계 방향으로 45도 회전한 이미지를 보여 줍니다.


아래 왼쪽 이미지는 반 시계 방향 45도 회전하여 축소 변환한 이미지이며, 오른쪽 이미지는 시계 방향으로 45도 회전해서 확대 변환한 이미지를 보여줍니다.


이번 포스팅에는 수학적인 내용이 조금 들어가서 조금 답답할 수 있겠지만, 막상 구현해 보면 “행렬 하나만 잘 만들면 만능” 이라는 것을 알 수 있습니다. OpenCV에서 함수로 잘 만들어 두었죠.
다음 포스팅에서는 Geometric Transform(기하학적 변환) 에 대해 조금 깊게 들어 가보겠습니다.
어핀 변환(Affine Transform) 이라는 녀석인데요. 임의의 점 3개를 찍어서 이미지를 찌그러뜨리고 펴는 기능을 구현 할 겁니다. 아마도 이번 포스팅 보다 재미 있을 거에요.
참고 자료
[Post #21] 버그 수정: [WPF OpenCV Project #21] – 히스토그램 표시 오류 수정
WarpAffine: OpenCV Docs – warpAffine – 기하학적 변환의 핵심 함수
GetRotationMatrix2D: OpenCV Docs – getRotationMatrix2D – 회전 행렬 생성 도우미
OpenCV 공식 튜토리얼 (Geometric Transformations):
https://docs.opencv.org/4.x/da/d6e/tutorial_py_geometric_transformations.html
기하학적 변환 원리: OpenCV 튜토리얼 – Geometric Transformations of Images