Remap (픽셀 재배치)을 이용한 Wave Effect를 이번 WPF OpenCV 프로젝트에 정리하고 추가/구현 하도록 하겠습니다. 지난 포스팅(#24)에서는 원근 변환(Perspective Transform)을 통해 찌그러진 문서를 펴는 마법을 부렸습니다. 기억나시죠?
그동안 다뤘던 이동, 회전, 어핀, 원근 변환의 공통점이 있습니다. 바로 행렬(Matrix) 하나로 모든 것이 해결되는 ‘선형 변환‘이라는 점입니다. 즉, 직선은 변환 후에도 여전히 직선으로 남아있었습니다.
하지만 현실 세계는 그렇게 반듯하지 않습니다. 투명한 유리잔 뒤로 비친 세상이나, 일렁이는 물결에 비친 모습, 혹은 광각 렌즈(GoPro 등)로 찍었을 때 둥글게 휘어지는 현상(Lens Distortion) 등은 matrix(행렬)만으로는 표현할 수 없습니다.
오늘은 이렇게 제멋대로 휘어지는 이미지를 만들기 위해 픽셀 재배치(Remapping) 기술인 Cv2.Remap에 대해 알아보고, 이를 이용해 Wave Effect (물결 효과)를 구현해 보겠습니다.
Remap (픽셀 재배치)
기존의 변환이 “종이를 통째로 돌리거나 당기는 것”이었다면, Remap은 “종이를 픽셀 단위로 잘게 찢어서 내 맘대로 퍼즐을 다시 맞추는 것”과 같습니다.
- 규칙: “(x, y)에 있는 픽셀에게, 너는 (x’, y’)로 이동해라!” 라는 명령서를 모든 픽셀에게 각각 내려주는 방식입니다.
- 장점: 모양을 마음대로(원형, 물결, 소용돌이 등) 만들 수 있습니다.
- 단점: 픽셀 하나하나의 위치를 다 계산해야 하므로 연산량이 많습니다.
이 방법은 로봇 비전에서 렌즈 왜곡(Barrel Distortion)을 펴거나, 360도 파노라마 영상을 만들 때 아주 많이 사용됩니다. 이번 포스팅에서는 기초 단계로 사인(Sin), 코사인(Cos) 함수를 이용해 이미지를 울렁거리는 효과를 만들어 보겠습니다.
Wave Effect : 수학적 원리
“갑자기 웬 삼각함수?” 하실 수 있지만, 주기적으로 반복되는 물결(Wave) 모양을 만드는 데는 sin/cos만 한 게 없습니다. 어려운 삼각함수 공식은 생각 나지 않더라도, sin 그래프와 cos 그래프 모양은 다들 알잖아요?
아무튼 이미지의 픽셀을 다음과 같은 규칙으로 옮길 겁니다. 그냥 그렇구나 하고 알아만 두세요.
- A (Amplitude, 진폭): 물결이 얼마나 세게 칠지 결정 (픽셀 이동 거리)
- (Wavelength, 파장): 물결의 간격이 얼마나 넓을지 결정
- (Phase, 위상): 물결의 시작 위치 (이 값을 계속 바꾸면 물결이 움직이는 애니메이션이 됩니다!)
쉽게 말해, 가로(x) 좌표는 세로(y) 위치에 따라 흔들고, 세로(y) 좌표는 가로(x) 위치에 따라 흔들어서 전체적으로 꿀렁꿀렁 거리게 만드는 것입니다.
OpenCV Remap()
사용할 OpenCV 라이브러리 관점 즉 사용하는 함수를 좀 들여다 보죠. OpenCV에서는 undistort() 라는 함수가 있긴 한데, remap() 함수를 주로 사용합니다. 아주 깊숙히 알 필요는 없을 거 같구요. 간단히 요약 하자면, undistort() 보다 remap() 함수가 최적화에 더 높은 성능을 내기 때문이라고 알고 있으면 될듯 해요. 우리는 영상 처리를 성능 좋은 PC 기반에서 처리하고 있지만, 임베디드 시스템 같은 시스템 리소스가 PC 보다 상대적으로 그리 좋지 않은 경우에 undistort() 함수에서는 CPU 부하를 감당하기 벅찹니다. 이놈은 매 프레임 마다 모든 픽셀에 대해 왜곡 계산을 수행하게 하거든요. 하지만, remap() 함수는 실행 시 딱 한 번만 복잡한 수식을 풀어서 map_x, map_y 라는 Look-Up Table(LUT)을 만들어두고, 매 프레임마다 들어 오는 영상에 대해 미리 만들어 둔 map-LUT를 사용해 단순히 픽셀 복사만 수행하게 하죠. 즉 CPU 부하가 획기적으로 줄어 들게 되어 있다는 것이죠. OpenCV에서 제공하는 Remap() 함수의 사용법은 코드를 구현하면서 살펴보도록 하겠습니다.
요약 하면, remap() 을 쓰는 이유는 복잡한 좌표 계산은 처음에 한번만 하고 (Pre-Calculation), 실시간 처리에서는 픽셀 복사만 하여 고속 처리를 하기 위해 주로 사용합니다. remap() 함수는 렌즈의 왜곡를 만들 수 있을 뿐만 아니라, 이미지를 물결치게 하거나, 뒤집거나, 파노라마 처럼 펼쳐 보이게 하는 등 픽셀의 위치를 바꾸는 모든 변환에 사용할 수 있는 만능 도구로 사용됩니다.
구현 요약
이제 프로젝트에 구현할 내용을 요약하고 WPF OpenCV 에 추가해 보도록 하겠습니다.
AlgorithmParameters.cs: RemapParams 클래스 생성 (파장, 진폭, 위상)
MainWindow.xaml: 파라미터 조절용 슬라이더 UI 추가
MainViewModel.cs: Lens Distortion (Remap) 메뉴를 추가
OpenCVService.cs: mapX, mapY 배열 생성 및 Cv2.Remap 적용
Step 1: AlgorithmParameters (Model)
AlgorithmParameters.cs 파일에서 RemapParams 클래스를 새로 만듭니다. 이 클래스에는 삼각 함수를 활용하여 Wave effect에 필요한 비-선형 매핑 파라미터인 Wavelength (파장), Amplitude (진폭), Phase (위상) 속성을 추가하여 사용자가 조정할 수 있게 합니다.
public class RemapParams : AlgorithmParameters
{
// 파장 (Wavelength): 물결의 너비 (기본 100)
private double _wavelength = 100.0;
public double Wavelength
{
get => _wavelength;
set { if (_wavelength != value) { _wavelength = value; OnPropertyChanged(); } }
}
// 진폭 (Amplitude): 물결의 높이/세기 (기본 10)
private double _amplitude = 10.0;
public double Amplitude
{
get => _amplitude;
set { if (_amplitude != value) { _amplitude = value; OnPropertyChanged(); } }
}
// 위상 (Phase): 물결의 흐름 (기본 0)
private double _phase = 0.0;
public double Phase
{
get => _phase;
set { if (_phase != value) { _phase = value; OnPropertyChanged(); } }
}
// 보간법 설정
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: UI(View)
MainWindow.xaml 파일에 RemapParams를 위한 UI 템플릿을 추가합니다. 파장, 진폭, 위상을 조절할 수 있는 슬라이더 등을 배치합니다.
<DataTemplate DataType="{x:Type local:RemapParams}">
<StackPanel>
<TextBlock Text="Remap (Ripple Effect)" 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="Wavelength (파장)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="10" Maximum="500" Value="{Binding Wavelength}" />
<TextBox Grid.Column="1" Text="{Binding Wavelength}" TextAlignment="Center" />
</Grid>
<TextBlock Text="Amplitude (진폭/세기)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="0" Maximum="100" Value="{Binding Amplitude}" />
<TextBox Grid.Column="1" Text="{Binding Amplitude}" TextAlignment="Center" />
</Grid>
<TextBlock Text="Phase (위상/움직임)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="50"/></Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="0" Maximum="6.28" Value="{Binding Phase}" TickFrequency="0.1" />
<TextBox Grid.Column="1" Text="{Binding Phase, StringFormat=N2}" TextAlignment="Center" />
</Grid>
<TextBlock Text="* Phase를 움직이면 물결이 흐릅니다." Foreground="Gray" FontSize="11" Margin="0,5,0,0"/>
</StackPanel>
</DataTemplate>
Step 3: ViewModel
MainViewModel.cs 파일에서 MainViewModel() 생성자 함수에 Lens Distortion (Remap) 메뉴를 추가합니다. 그리고, 사용자가 해당 알고리즘 메뉴을 선택하였을 때 RemapParams 클래스 객체를 생성하도록 CreateParametersForAlgorithm() 함수에도 아래와 같이 코드를 추가 합니다.
public MainViewModel()
{
_cvServices = new OpenCVService();
AlgorithmList = new ObservableCollection<string>
{
"Threshold",
"Otsu Threshold",
"Adaptive Threshold",
"Histogram",
"Normalize",
"Equalize",
"CLAHE",
"Geometric Transformation",
"Affine Transform",
"Perspective Transform",
"Lens Distortion (Remap)"
};
}
private void CreateParametersForAlgorithm(string algoName)
{
// 선택된 이름에 따라 적절한 설정 클래스 생성
switch (algoName)
{
// ............. 기존 코드 유지 ...................
case "Lens Distortion (Remap)":
CurrentParameters = new RemapParams();
break;
default:
CurrentParameters = null; // 설정이 필요 없는 경우
break;
}
}
Step 4: OpenCVService
OpenCVService.cs 파일에 Cv2.Remap 함수를 사용하여 비선형 변환 로직을 구현합니다. 사용자가 설정한 파라미터와 삼각함수(Sin, Cos)를 이용하여 매핑 테이블(mapX, mapY)을 생성하고 이를 이미지에 적용합니다. OpenCVService.cs 파일을 열어 ProcessImageAsync() 함수에서 RemapParams 의 속성을 이용하여 영상 처리 프로세싱 코드를 추가 해야겠죠? 아래의 코드를 switch 구문 아래에 Lens Distortion (Remap) 처리 관련 코드들을 추가해 주세요. Cv2.Remap() 함수를 사용하고, mapX와 mapY를 삼각함수(sin, cos)로 생성하였습니다. 코드 내에 Parallel.For(); 를 사용했는데요. undistort() 에 비해 부하 부담이 덜 하긴 하지만, Remap() 함수 역시 다른 영상 처리 함수에 비해 시스템의 부하를 주기 때문에 병령 처리하도록 하였습니다.(혹시 병렬 처리 함수를 모른다면, 흠…. 난감 하네요… ^^; 나중에 따로 포스팅할 기회가 있다면 Task 및 동기화 관련 내용을 한번 정리해 보도록 할게요. 여기서는 주제가 좀 많이 벗어나니까.. 패스 ~)
case "Remap":
if (parameters is RemapParams remapParams)
{
// 1. 매핑 테이블 생성 (MapX, MapY)
// MapX: 각 픽셀의 새로운 X 좌표를 저장할 행렬 (float형)
// MapY: 각 픽셀의 새로운 Y 좌표를 저장할 행렬
Mat mapX = new Mat(_srcImage.Size(), MatType.CV_32FC1);
Mat mapY = new Mat(_srcImage.Size(), MatType.CV_32FC1);
// 빠른 접근을 위해 Indexer 사용
var indexerX = mapX.GetGenericIndexer<float>();
var indexerY = mapY.GetGenericIndexer<float>();
int w = _srcImage.Width;
int h = _srcImage.Height;
// 파라미터 캐싱 (속도 최적화)
double wavelength = remapParams.Wavelength;
double amplitude = remapParams.Amplitude;
double phase = remapParams.Phase;
// 2. 모든 픽셀을 순회하며 새로운 좌표 계산
// 병렬 처리(Parallel)로 속도 향상
Parallel.For(0, h, y =>
{
for (int x = 0; x < w; x++)
{
// [공식 적용]
// X좌표는 Y값에 따라 Sin파형으로 흔들림
// Y좌표는 X값에 따라 Cos파형으로 흔들림
float newX = (float)(x + amplitude * Math.Sin(y / wavelength + phase));
float newY = (float)(y + amplitude * Math.Cos(x / wavelength + phase));
// 매핑 테이블에 저장
indexerX[y, x] = newX;
indexerY[y, x] = newY;
}
});
// 3. Remap 적용
// 입력 이미지(_srcImage)의 픽셀을 mapX, mapY 규칙에 따라 결과 이미지(_destImage)로 옮김
// 빈 공간(Border)은 검은색(Constant, 0)으로 채움
Cv2.Remap(_srcImage, _destImage, mapX, mapY,
remapParams.Interpolation, BorderTypes.Constant, Scalar.All(0));
// 리소스 정리
mapX.Dispose();
mapY.Dispose();
resultMessage += $": Remap (Wave:{remapParams.Wavelength}, Amp:{remapParams.Amplitude})";
}
break;
실행 및 이미지 확인
이제 빌드 후 실행 해서 Lens Distortion (Remap) 알고리즘을 선택해 보세요. 잘 따라 왔다면 전혀 문제 없이 잘 될거라 믿습니다. 영상 처리 실행 결과와 관련하여 Wave Effect 를 이미지에 적용하려면 Lens Distortion (렌즈 왜곡) 을 적용하는 것이며, 좌표의 재 배치(Remapping)을 진행할 때, X 좌표는 Sin 함수를 Y좌표는 Cos 함수를 사용한다는 점! 기억하고 이미지 결과를 참고해 주세요. 만약 sin과 cos 을 다르게 하고 싶다면 코드 상에서 변경 후 빌드 하여 적용해도 되니까 각자 변경 해서 진행 해 보세요. RemapParams 클래스의 파장, 진폭, 위상을 조절해 가면서 이미지의 변환을 확인 해 보면 나름 재미있는 이미지도 만들어 낼 수 있을 겁니다.
다음에는 이번 포스팅에 다루지 않았던, 렌즈 변환 중 오목렌즈, 볼록렌즈 효과를 다루고, 내용이 길어 지지 않는다면 방사 왜곡에 대해 다루도록 하겠습니다.




참고 자료
[Post #24] 원근 변환: [WPF OpenCV Project #24] – Perspective Transform (행렬 기반 변환)
Remap: OpenCV Docs – remap – 픽셀 재배치 함수 상세 설명
Geometric Transformations: OpenCV Tutorial – 기하학적 변환의 전반적인 내용