Barrel Distortion 과 Pincushion Distortion 을 이번 WPF OpenCV 프로젝트에서 구현하겠습니다.
지난 포스팅(#25)에서는 Cv2.Remap을 이용해 물결처럼 일렁이는 효과(Wave Effect)를 구현했습니다. Sin, Cos 함수로 픽셀을 흔들어주니 재미있는 결과가 나왔었죠?
그리고, Lens Distortion 의 내용을 정리 하면서 Barrel Distortion(볼록 왜곡)과 Pincushion Distortion(오목 왜곡)을 짧게 정리 하긴 했었는데, 이것을 프로젝트에 구현하진 않았습니다. Remap() 함수를 통해 이미지를 X방향과 Y 방향으로 삼각함수를 이용해서 픽셀 좌표를 왜곡 시키고 실행 했었는데요. 이번에는 볼록 왜곡과 오목 왜곡을 이미지에 대한 시각적 크기로 한번 정리하고 구현 후 실행 해서 이미지를 한번 살펴보도록 하죠. CCTV나 블랙박스, 혹은 GoPro 같은 광각 렌즈로 찍은 사진을 보면 가장자리가 둥글게 휘어지는 현상을 보신 적 있죠? 이런 효과를 영상 처리 방법으로 Simulation 보겠습니다.
(원래는 펴는 게 목적이지만, 찌그러뜨리는 법을 알아야 펴는 법도 알 수 있으니까요!)
Barrel Distortion (볼록 왜곡) – “풍선 효과”
- 시각적 느낌: 이미지가 풍선처럼 부풀어 오른 느낌.
- 중심부 크기: 커 보입니다 (확대).
- 가장자리(Edge) 크기: 작아 보입니다 (압축)
Pincushion Distortion (오목 왜곡) – “빨려 들어가는 효과”
- 시각적 느낌: 이미지의 중심을 손가락으로 꾹 눌러서 안으로 빨려 들어가는 느낌입니다.
- 중심부 크기: 작아 보입니다 (축소).
- 가장자리(Edge) 크기: 커 보입니다 (확대/늘어짐).
직교 좌표 vs 극 좌표
지난번 물결 효과는 x, y 좌표 (직교 좌표계)에서 바로 계산했습니다. 하지만 렌즈 왜곡은 “중심점으로부터의 거리”에 따라 휘어짐의 정도가 달라집니다.
그래서 바둑판 같은 직교 좌표(x, y)를 사용하는 것보다, 레이더망 같은 극 좌표 (r, )를 사용하는 것이 훨씬 계산하기 편합니다.
알고리즘 순서:
- 변환 ( ): 픽셀 위치를 ‘중심에서 얼마나 떨어졌는지()’와 ‘어느 각도인지()’로 바꿉니다.
- 왜곡 (): 거리 을 지수승(Power)하여 늘리거나 줄입니다.
- 복원 (): 왜곡된 거리를 다시 좌표로 돌려놓습니다.
수식이 복잡해 보이지만, 핵심은 “거리를 조작한다”는 것이죠. 렌즈는 둥근 유리알입니다. 빛이 굴절되는 정도는 중심에서의 거리()에 따라 결정됩니다. 만약 굴절 정도를 직교 좌표계에서 적용하려면, 오른쪽 3칸, 위로 5칸 이렇게 가기 때문에 왜곡 공식을 적용하기가 힘들죠. 하지만 극좌표계에서는 중심에서 10만큼 떨어진 거리()에 있고, 45도 방향에 있다 와 같이 중심에서 거리 () 만 늘리거나 줄이면 왜곡에 대한 처리는 끝나버리는 거죠. OpenCV에서는 직교 좌표 변환 함수와 극좌표 변환함수를 제공하는데요. 이들의 흐름을 아래와 같이 정리하고 구현하도록 하겠습니다.
[입력: 픽셀 좌표] -> cartToPolar():(직교 좌표 ->극좌표 변환) -> [왜곡 연산] -> polarToCart():극 좌표 ->직교 좌표 변환 -> [출력: 변환된 좌표]
구현 요약
- AlgorithmParameters.cs: RemapParams 클래스를 확장하여 렌즈 왜곡에 필요한 파라미터(Center Point, Mode, Exponent, Scale 등)를 추가 정의합니다.
- MainWindow.xaml: RemapParams를 위한 UI 템플릿을 추가하고, 이미지 위에서 마우스 클릭 시 십자가 표시를 위한 Canvas 등의 요소를 확인합니다. (기존 OverlayCanvas 활용하도록 하죠.)
- MainWindow.xaml.cs: 마우스 클릭 이벤트를 처리하여 “Lens Distortion” 모드일 때 클릭한 좌표를 RemapParams의 중심점(CenterX, CenterY)으로 설정하고, 화면에 십자가를 그리는 로직을 추가합니다.
- MainViewModel.cs: “Lens Distortion” 메뉴를 추가하고 파라미터를 연결합니다.
- OpenCVService.cs: Remap과 cartToPolar, polarToCart를 활용한 렌즈 왜곡 알고리즘을 구현합니다.
Step 1: AlgorithmParameters (Model)
AlgorithmParameters.cs 파일에서 이전 포스팅에서 추가된 RemapParams 클래스를 아래와 같이 Property 변수를 추가하여 확장합니다. 기존 파라미터와 새로운 렌즈 왜곡(볼록/오목) 파라미터가 모두 포함되도록 말이죠.
// 왜곡 모드 선택 열거형
public enum DistortionMode
{
SinCosWave, // 기존 물결 효과
LensSimulate // 신규 볼록/오목 렌즈 효과
}
// Remap (Lens Distortion) 파라미터 확장
public class RemapParams : AlgorithmParameters
{
private DistortionMode _mode = DistortionMode.SinCosWave;
public DistortionMode Mode
{
get => _mode;
set
{
if (_mode == value) return;
_mode = value;
OnPropertyChanged();
}
}
// 파장 (Wavelength)
private double _wavelength = 20.0;
public double Wavelength
{
get => _wavelength;
set
{
if (_wavelength == value) return;
_wavelength = value;
OnPropertyChanged();
}
}
// 진폭 (Amplitude)
private double _amplitude = 10.0;
public double Amplitude
{
get => _amplitude;
set
{
if (_amplitude == value) return;
_amplitude = value;
OnPropertyChanged();
}
}
// 위상 (Phase)
private double _phase = 0.0;
public double Phase
{
get => _phase;
set
{
if (_phase == value) return;
_phase = value;
OnPropertyChanged();
}
}
public string PointInfo => $"Center: ({Center.X:F0}, {Center.Y:F0})";
private Point2f _center = new Point2f(0, 0);
public Point2f Center
{
get => _center;
set
{
if (_center == value) return;
_center = value;
OnPropertyChanged();
OnPropertyChanged(nameof(PointInfo));
}
}
private double _exponent = 1.0; // 1.0=원본, <1.0 오목, >1.0 볼록
public double Exponent
{
get => _exponent;
set
{
if (_exponent == value) return;
_exponent = value;
OnPropertyChanged();
}
}
private double _scale = 0.5; // 반경 크기 비율
public double Scale
{
get => _scale;
set
{
if (_scale == value) return;
_scale = value;
OnPropertyChanged();
}
}
// 보간법
private InterpolationFlags _interpolation = InterpolationFlags.Linear;
public InterpolationFlags Interpolation
{
get => _interpolation;
set
{
if (_interpolation == value) return;
_interpolation = value;
OnPropertyChanged();
}
}
public List<InterpolationFlags> InterpolationSource { get; } = new List<InterpolationFlags>
{
InterpolationFlags.Nearest,
InterpolationFlags.Linear,
InterpolationFlags.Cubic,
InterpolationFlags.Lanczos4
};
// 소스 목록
public List<DistortionMode> ModeSource { get; } = Enum.GetValues(typeof(DistortionMode)).Cast<DistortionMode>().ToList();
}
Step 2: UI (View)
MainWindow.xaml 파일에서는 RemapParams 클래스의 DataTemplate를 수정하도록 하죠. 이전 포스팅의 코드에서 가장 큰 변화는 Triggers를 사용해서 사용자가 선택한 Mode에 따라 필요한 설정 창만 보이도록 구현하였습니다. 아래의 코드를 참고해 주세요.
<DataTemplate DataType="{x:Type local:RemapParams}">
<StackPanel>
<TextBlock Text="Distortion Effects" Margin="0,0,0,5" FontWeight="Bold"/>
<TextBlock Text="Distortion Mode" Margin="0,5,0,0"/>
<ComboBox ItemsSource="{Binding ModeSource}" SelectedItem="{Binding Mode}" Margin="0,2,0,0" Height="25"/>
<TextBlock Text="Interpolation" Margin="0,10,0,0"/>
<ComboBox ItemsSource="{Binding InterpolationSource}" SelectedItem="{Binding Interpolation}" Margin="0,2,0,0" Height="25"/>
<Separator Margin="0,10,0,10"/>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Mode}" Value="SinCosWave">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="[ Sin / Cos Wave Settings ]" FontWeight="Bold" Foreground="Gray"/>
<TextBlock Text="Wavelength (파장)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="1" Maximum="100" Value="{Binding Wavelength}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding Wavelength}" Margin="5,0,0,0" 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="50" Value="{Binding Amplitude}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding Amplitude}" Margin="5,0,0,0" 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="360" Value="{Binding Phase}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding Phase}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
</StackPanel>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Mode}" Value="LensSimulate">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Text="[ Lens Simulation Settings ]" FontWeight="Bold" Foreground="Gray"/>
<TextBlock Text="Center Point" Margin="0,5,0,0"/>
<TextBox Text="{Binding PointInfo, Mode=OneWay}" IsReadOnly="True" Background="#EEEEEE" Padding="3" Margin="0,2,0,0"/>
<TextBlock Text="* 이미지 클릭하여 중심 지정" Foreground="Gray" FontSize="10"/>
<TextBlock Text="Exponent (왜곡 지수)" Margin="0,10,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="0.1" Maximum="3.0" Value="{Binding Exponent}" VerticalAlignment="Center" TickFrequency="0.1" IsSnapToTickEnabled="True"/>
<TextBox Grid.Column="1" Text="{Binding Exponent, StringFormat=F2}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
<TextBlock Text="< 1.0 : 오목 | > 1.0 : 볼록" Foreground="Gray" FontSize="10" HorizontalAlignment="Right"/>
<TextBlock Text="Scale (영역 크기)" Margin="0,5,0,0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Slider Grid.Column="0" Minimum="0.1" Maximum="1.5" Value="{Binding Scale}" VerticalAlignment="Center" TickFrequency="0.1" IsSnapToTickEnabled="True"/>
<TextBox Grid.Column="1" Text="{Binding Scale, StringFormat=P0}" Margin="5,0,0,0" TextAlignment="Center" />
</Grid>
</StackPanel>
</StackPanel>
</DataTemplate>
Step 3: UI (Behind Code)
MainWindow.xaml.cs 파일에서는 이전 포스팅의 코드를 그대로 사용하면 됩니다. Lens Distortion(렌즈 왜곡)에 대해 볼록 효과와 오목 효과를 분리하였다면 추가했어야 하는데, 앞서 설명하였던 것 처럼 RemapParams 클래스를 확장하였기 때문에 여기서는 추가할 것이 없습니다.
Step 4: ViewMode
MainViewModel.cs 파일에서는 Lens Distortion(렌즈 왜곡) 효과를 사용자가 마우스 클릭한 포인트를 기준으로 처리를 하기 때문에 ZoomBorder_MouseDown() 이벤트 함수에 추가 코드가 필요합니다. 그리고, 클릭한 마우스 포인트에 십자가 표시를 위해 DrawCrosshair() 함수도 추가하여 분리하였습니다.
아래 코드를 참고해 주세요.
private void DrawCrosshair(Point center)
{
double size = 15;
double thickness = 2;
Brush brush = Brushes.Magenta;
Line l1 = new Line { X1 = center.X - size, Y1 = center.Y, X2 = center.X + size, Y2 = center.Y, Stroke = brush, StrokeThickness = thickness };
Line l2 = new Line { X1 = center.X, Y1 = center.Y - size, X2 = center.X, Y2 = center.Y + size, Stroke = brush, StrokeThickness = thickness };
OverlayCanvas.Children.Add(l1);
OverlayCanvas.Children.Add(l2);
}
private void ZoomBorder_MouseDown(object sender, MouseButtonEventArgs e)
{
// 휠버튼이 클릭되었고, 이미지가 로드된 상태라면,
if (e.ChangedButton == MouseButton.Middle && ImgView.Source != null)
{
// ............. 기존 코드 유지 ............
}
else if (e.ChangedButton == MouseButton.Left && ImgView.Source != null) // ROI 또는 기타 도형 그리기 시작
{
Point mousePos = e.GetPosition(ImgView);
var bitmap = ImgView.Source as BitmapSource;
var vm = this.DataContext as MainViewModel;
// ............. 기존 코드 유지 ............
#region Remap LensDistortion
if (vm != null && vm.CurrentParameters is RemapParams remapParams)
{
// LensSimulate 모드일 때만 클릭 좌표 업데이트
if (remapParams.Mode == DistortionMode.LensSimulate)
{
if (mousePos.X >= 0 && mousePos.X < bitmap.PixelWidth &&
mousePos.Y >= 0 && mousePos.Y < bitmap.PixelHeight)
{
remapParams.Center = new OpenCvSharp.Point2f((float)mousePos.X, (float)mousePos.Y);
// 십자가 그리기
OverlayCanvas.Children.Clear();
DrawCrosshair(mousePos);
}
return; // 렌즈 모드면 여기서 종료
}
// SinCosWave 모드면 클릭 이벤트를 무시하거나, 아래의 다른 로직(ROI 등)으로 넘어감
}
#endregion
// ............. 기존 코드 유지 ............
}
Step 5: OpenCVService (Model)
OpenCVService.cs 코드에서는 사용자가 지정하고, 조정한 RemapParams 클래스의 파라미터를 이용해 블록 왜곡과 오목 왜곡을 처리하는 코드를 추가합니다. 사실 이 부분에 할 이야기가 좀 더 있긴 한데, 우리는 알고리즘 정리를 하고 구현을 마우스 클릭으로 지정된 위치를 기준으로 직교 좌표계 데이터를 극 좌표계로 변환하여 왜곡 연산을 하겠다고 했었습니다. 기억 하죠? 약속한 방식대로 코드를 아래와 같이 추가하도록 하겠습니다. ProcessImageAsync() 함수에 이전 포스팅에 구현했던 Lens Distortion (Remap) 구문을 아래와 같이 변경해 주세요.
case "Lens Distortion (Remap)":
#region Polar Method
if (parameters is RemapParams remapParams)
{
int w = _srcImage.Width;
int h = _srcImage.Height;
// Remap을 위한 최종 맵 (32비트 float 필수)
Mat mapX = new Mat(_srcImage.Size(), MatType.CV_32FC1);
Mat mapY = new Mat(_srcImage.Size(), MatType.CV_32FC1);
// --- 모드 분기 ---
if (remapParams.Mode == DistortionMode.SinCosWave)
{
// [기존 방식 유지] Sin/Cos 물결 효과
var indexerX = mapX.GetGenericIndexer<float>();
var indexerY = mapY.GetGenericIndexer<float>();
double wavelength = remapParams.Wavelength;
double amplitude = remapParams.Amplitude;
double phase = remapParams.Phase * (Math.PI / 180.0);
Parallel.For(0, h, y =>
{
for (int x = 0; x < w; x++)
{
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;
}
});
resultMessage += $": Wave (W:{wavelength}, A:{amplitude})";
}
else if (remapParams.Mode == DistortionMode.LensSimulate)
{
// [신규 방식 적용] CartToPolar & PolarToCart 사용
float cx = remapParams.Center.X;
float cy = remapParams.Center.Y;
double exp = remapParams.Exponent;
double maxRadius = (Math.Min(w, h) / 2.0) * remapParams.Scale;
if (maxRadius < 1.0) maxRadius = 1.0;
// 1. 상대 좌표(Relative Coordinates) 생성
// mapX, mapY에 (x - cx), (y - cy) 값을 먼저 채웁니다.
var indexerX = mapX.GetGenericIndexer<float>();
var indexerY = mapY.GetGenericIndexer<float>();
Parallel.For(0, h, y =>
{
for (int x = 0; x < w; x++)
{
indexerX[y, x] = x - cx;
indexerY[y, x] = y - cy;
}
});
// 2. 직교좌표(x, y) -> 극좌표(mag, ang) 변환
// mag: 거리(r), ang: 각도(theta)
using (Mat mag = new Mat())
using (Mat ang = new Mat())
{
Cv2.CartToPolar(mapX, mapY, mag, ang);
// 3. 거리(Magnitude)에 왜곡 효과 적용
// 조건부 비선형 변환이므로 병렬 루프로 처리
var indexerMag = mag.GetGenericIndexer<float>();
Parallel.For(0, h, y =>
{
for (int x = 0; x < w; x++)
{
float r = indexerMag[y, x];
// 설정된 반지름 안쪽만 왜곡
if (r < maxRadius)
{
// 정규화: 0.0 ~ 1.0
float rNorm = r / (float)maxRadius;
// 왜곡 적용: r_new = r_norm ^ exp
// (Inverse Mapping 원리에 따라 exp > 1이면 확대(볼록), exp < 1이면 축소(오목))
float rNewNorm = (float)Math.Pow(rNorm, exp);
// 실제 거리로 복원
float rNew = rNewNorm * (float)maxRadius;
indexerMag[y, x] = rNew;
}
}
});
// 4. 극좌표(mag, ang) -> 직교좌표(mapX, mapY) 복원
// 이 시점에서 mapX, mapY에는 왜곡된 상대 좌표가 들어갑니다.
Cv2.PolarToCart(mag, ang, mapX, mapY);
}
// 5. 절대 좌표로 변환 (중심점 더하기)
// Remap 함수는 이미지 상의 절대 좌표(0~Width)를 필요로 합니다.
// Cv2.Add를 사용하여 행렬 전체에 스칼라 값을 더합니다. (루프보다 빠르고 간결함)
Cv2.Add(mapX, new Scalar(cx), mapX);
Cv2.Add(mapY, new Scalar(cy), mapY);
resultMessage += $": Lens (Polar Method, Exp:{exp:F2})";
}
// 6. Remap 적용 (공통)
Cv2.Remap(_srcImage, _destImage, mapX, mapY,
remapParams.Interpolation, BorderTypes.Constant, Scalar.All(0));
mapX.Dispose();
mapY.Dispose();
}
#endregion
break;
실행 결과 확인
Lens Distortion (렌즈 왜곡) 에 대해 이전 포스팅을 포함해 조금 많은 내용이 있었는데요.
이번 포스팅을 정리해보면, 볼록 왜곡과 오목 왜곡 효과를 이미지에 적용하기 위해 파라미터 클래스를 따로 만들지 않았습니다. 이전 포스팅에 추가했던 RemapParams 클래스를 확장했습니다. 확장된 내용을 살펴 보면 왜곡 효과를 줄 좌표를 입력 받을 수 있도록 했구요. 왜곡 지수(exponent) 값을 이용해서 볼록 (1보다 클 경우) 과 오목 효과( 1보다 작고, 0보다 큰 경우)를 적용할 수 있도록 했습니다. 그리고, 왜곡 영역(Scale) 값을 사용하여 렌즈의 왜곡 효과를 줄 수 있는 원 모양의 영역의 크기를 비율로 지정하도록 했습니다. 마지막으로 사용자 좌표는 직교 좌표이므로, 왜곡 연산을 위해 극 좌표 변환이 필요한데, 그렇게 구한 좌표는 Remap() 함수를 사용해서 효과를 준거죠. (시퀀스가 어떻게 되고, 파라미터를 어떻게 설정해야 하는지 헤깔려 할까 싶어 다시 한번 정리한 겁니다.)
아래는 실행한 이미지들입니다. 확인해 보도록 하죠.
먼저 이미지를 불러 들입니다.

알고리즘 선택 부분에서 Lens Distortion (Remap) 항목을 선택하고, 아래에 나타나는 속성 창에서 Distortion Mode 를 선택 후 LensSimulate 를 선택합니다. 그러면 속성 창 아래가 볼록/오목 왜곡 효과를 적용할 파라미터들로 아래 이미지와 같이 변경됩니다.

이상태에서 아래의 그림 처럼 이미지의 코 부분을 클릭하면 십자가 표시가 나타납니다. (잘 따라 오고 있죠?)

Exponent (왜곡 지수) 값을 3.0으로 변경 해서 적용 해보죠. (1 보다 큰 임의의 값으로 변경해 보세요.)

이번에는 Exponent (왜곡 지수) 값을 0.5 로 변경 해서 실행해 봅니다. (1보다 작고, 0보다 큰 임의 값으로 변경해 보세요.)

마지막으로 Exponent 값과 Scale 값을 임의의 값으로 모두 변경한 이미지는 아래와 같습니다. 각자 알아서들 변경 해 보세요. 효과를 줄 위치도 변경해 보면서 말이죠.


이제 마무리 하겠습니다.
Exponent > 1 (볼록): 중심부의 픽셀들이 더 넓은 영역을 차지하려고 밀고 나옵니다. 그래서 코가 왕만두처럼 커집니다.
Exponent < 1 (오목): 가장자리의 픽셀들이 중심으로 몰려듭니다. 얼굴이 홀쭉하게 빨려 들어갑니다.
제가 이번 포스팅 중간에 음.. 정확히는 Step 5에서 볼록 왜곡과 오목 왜곡의 영상 처리를 위해 더 할 얘기가 있긴 한데 라며 이야기 했던 것 기억 하나요? 그러면서 영상 처리 구현 코드에 직교좌표와 극좌표 변환 방식으로 왜곡 효과 처리를 구현했었습니다. 이렇게 구현하는게 코드의 가독성에서는 명확하고 이해하기가 쉽긴 하죠. 하지만, 대용량 이미지에서 해당 방식을 사용하면 느려질 수 있습니다. 내부에 병렬 처리를 하긴 했어도 말이죠. 그래서 말인데, 다음 편에서는 Lens Distortion 효과 처리를 수동 계산하는 방식으로 WPF OpenCV 프로젝트에 구현하는 내용을 진행하도록 할게요.
참고 자료
[Post #25] 물결 효과: [WPF OpenCV Project #25] – Remap 기초 (Wave Effect)
Distortion (Optics): Wikipedia – Distortion – 배럴 및 핀쿠션 왜곡의 광학적 정의
OpenCV Camera Calibration: OpenCV Docs – 실제 렌즈 왜곡 모델과 보정 방법