在 C#/WPF 中使用简化版 MLS 进行图像变形
演示程序 WarpImage,创建和拖动链接点(缩略图、MVVM),从/向 WPF 图像访问所有像素,刚性 MLS 算法
- 下载 ImageWarp 演示项目 - 934.9 KB (需要 .NET 4.5)
- 下载 ImageWarp 源代码 - 527.3 KB(Visual Studio 2012 for Desktop Express)
- 原始样本来自“使用移动最小二乘法的图像变形”(图像中的蓝色标记部分)
- 拖放的变形点
- 结果
目录
- 引言
- 使用 WarpImage 程序
- 一些背景和链接
- 项目、ViewModel 和命令的结构
- 图像和点的 XAML/视图
- 拖动点的样式:模板化缩略图和 MVVM
- 将图像坐标转换为位图坐标
- 从 WPF 图像到 2D 像素数组
- 刚性点对点均方差最小算法
- 使用矩形网格的简单反向变换
- 关注点
- 许可证
引言
我对将静态图片转换为“动态”图片很感兴趣。我认为点对点图像变形是一个有用的开始。我找到了由 Scott Schaefer、Travis McPhail 和 Joe Warren 撰写的文章:“使用移动最小二乘法的图像变形”。但是我没有找到任何用于 MLS 算法的 C# 代码,所以我编写了这个 C#/WPF 爱好程序(专业演示,肤浅的测试,没有优化计算时间)。读者可能感兴趣的点
- 点对点图像变形的初步印象。
- 使用模板化缩略图和 MVVM 创建和拖动(此处:链接点)的简单示例。
- 从/到 WPF 图像访问 2D 像素数组中*所有*像素的示例。
- 刚性点对点 MLS 算法的 C# 实现(无需“预计算”)。
- 使用简单反向变换的图像变换。
我假设读者熟悉 C#、WPF 和 MVVM 模式。所应用的 MVVM 模式(不那么严格或示范性)用于组织代码。本文为希望利用 WPF/XAML 的优势制作精美 UI 并对图像所有像素执行基本计算的 C#/WPF 程序员提供了一个工作解决方案/最小框架的示例。
在本文中,我将使用接口来提供代码的紧凑全局描述。接口不是代码的一部分,我是使用 SharpDevelop 单独提取接口的。为了解释的目的,我有时将一些局部函数全局化并为变量引入了属性。
使用 WarpImage 程序
请参阅图 1 中的程序两个屏幕截图。左图:指定变形点,右图:变形后。点 1..12:给男人一个更大的咖啡杯,13-14 挤压头部,15 更多下巴,16-20 腹肌锻炼,21-24 移动棕色奇异鸟,其余点锚定图像的其余部分。注意连接点的注释功能。
程序最初显示应用程序目录或项目 Images 目录中的文件“Test.jpg”的图像。可以从文件资源管理器中拖入新图像。通过左键单击图像创建点,通过右键单击编号点,可以使用上下文菜单删除该点。可以使用“清除点”按钮清除所有点。一个拖放的变形点实际上由一个小锚点、一个编号较大的拖动点(用于指定变形后的所需位置)以及它们之间的连接/线组成。锚点和拖动点,在本文中也称为变形点,指定了变形。
该程序有两种模式:如果选择“使用变形按钮”,则使用原始文件中的图像,并在按下“变形图像”后,图像将变形。在执行新的变形之前,必须按下“重置图像”按钮。可以修改变形点。使用这些按钮可在原始图像和变形图像之间切换。
在交互模式“拖动点”中,所有锚点都设置为拖动的大编号点的位置。现在可以拖动一个点,释放后,将在现有图像上执行图像变形。它可以用于微调变换,但目前图像质量在每次变形中都会下降。移出 WPF 图像的像素将丢失。
应该注意的是,MLS 算法会尽其所能地匹配指定的变形点。它的自由度可以通过添加一些未拖动的变形点来限制/必须限制,以固定图像的其他部分。如果我们只指定一个点并拖动它,整个图像就会被平移。如果我们指定两个点并拖动一个,整个图像就会全局“旋转”。因此,如果我们想使对象边界变薄,我们必须添加一些不拖动的变形点以防止整体平移。另一方面,太多的或极端的约束无法解决。
我的第一个观察是,在图 0 的样本中,点对点变换按预期工作。当应用于图 1 中时,例如放大咖啡杯,点对点变换不适用。必须使用多个点来指定变换,结果中,边界被局部点约束扭曲。如果我们在更远的距离指定点,可以看到头部,可以获得稍微更好的结果。我们遇到了一个基本的上下文/分辨率问题。从实际角度来看,最好支持(连接的)线对线 MLS 变换。
一些背景和链接
作为参考,我给出了一些我在编写这个程序时遇到的最相关的书签,我没有对图像处理进行系统扫描。还要注意,图像处理不是我的(主要)专长。
为了创建和拖动东西,我找到了 Josh Smits 的在画布中拖动元素和在 WPF 中注释图像。我没有使用装饰器,例如参见WPF 示意图设计器:第一部分和MVVM 示意图设计器,其中 Sacha Barber 介绍了 MVVM。我的方法受到 Denis Vuyka 博客WPF。可拖动对象和简单形状连接器。的启发。我从一个ListBox
和带Grid
的ItemTemplate
开始,最终得到了一个ItemsControl
和Thumb
。
对于更复杂的 XAML 绑定,我总是使用 MSMD 模式:猴子看猴子做,或者用猴子和泰山来说:我 Google,你 Google。大多数标准问题都得益于 WPF 社区的帮助。这个社区一直秉持着一种“开源”态度。我注意到 VS2013 express for desktop 的早期版本没有生成一个老旧的 .exe 文件。现在随着 Windows 8 的变化,所有应用程序都通过商店(VS 转向云),我正在想 WPF 是否仍然是一个好的“开源”爱好平台。
在当前的应用程序中,我们使用批处理:我们获取 2D 数组中的所有像素,处理它们并显示结果。使用可写入位图也可以处理图像的较小部分。我只非常肤浅地看了 WPF Windows.Media.Imaging。我发现,由于专业位图类的数量(大多数是不可变的(一次生成后不可更改))以及某些情况下延迟加载导致位不可用,以最小努力方法处理 WPF 图像并不那么简单。
请注意,在处理像素时,始终存在一个问题:是尽可能简单地工作并接近系统/软件对图像的访问(32 字节?),还是引入更准确的浮点像素数组,并且不必担心字节格式。另一个问题是自己编写一些滤镜,还是使用开发更完善的图像库。
有关图像变形,请参阅普林斯顿大学的讲座此处。图像变形与旋转和调整大小存在类似的问题,有关流行介绍,请参阅数字图像插值。
对于现有的图像变形 C# 代码,请参见在 C# 中任意变形图像,以及 CodeProject 文章,如使用 C# 和 GDI+ 的图像处理傻瓜教程 第 5 部分 - 置换滤镜,包括漩涡和趣味图像变换:基于局部网格的图像变形器以及使用线性插值和双缓冲面板的图像变形。一种常见的方法是使用 x,y 坐标的多项式变换来实现某些效果。
我从“使用移动最小二乘法的图像变形”中实现了简化版 MLS 算法,只使用了文章的前 4 页。我在标题中使用了图像变形而不是图像形变,因为大多数 C# 文章都使用了这个术语。对于 MLS 代码,我找到了曲线移动最小二乘法的形状操作 [带代码]和一个Matlab 实现。后来,我发现了一个现有的程序Fiji,它是 ImageJ、Java 和许多插件(包括 MLS)的一个发行版。然而,直接实现文章中的方程对我来说是最简单的方法。我曾期望 MLS 算法会更复杂,并找到一个很好的理由来深入研究ILNumerics或其他库。
前向变换非常适合线条点的变换。像素坐标的前向变换会产生浮点坐标。结果像素坐标默认不在矩形网格上,因此目标图像像素坐标的 RGB 插值似乎有点复杂。对于图像像素,我决定实现从目标图像像素坐标到原始图像像素的反向变换,以保持简单。反向变换也会产生浮点坐标,但可以很容易地找到原始图像规则矩形像素的最近邻点,以在这些浮点坐标处插值 RGB 像素值。反向变换的优点通常包括:
- 只计算目标图像中的像素值。
- 它们只计算一次。
- 目标图像中没有孔洞。
关于图像插值(最近邻、双线性和三次),可以找到很多链接。我偶然发现了插值方法比较、C# 和 GDI+ 图像处理傻瓜教程 第 4 部分 - 双线性滤镜和调整大小、快速图像缩放算法、线性插值:过去、现在和未来以及最快的 RGB — 双线性插值。
项目、ViewModel 和命令的结构
该项目只是一个普通的 WPF 项目,如下图所示。我们有一些文件支持读取图像,一些 (MVVM) 工具,一个带命令的主 ViewModel 和 MLSWarpImages.cs。

为了给出应用程序结构的整体概念,我们提供了 ViewModel 的接口和命令,请参阅下面的代码。在本文中,我将使用接口来提供代码的紧凑全局描述。接口不是代码的一部分,我是使用 SharpDevelop 单独提取接口的。我们看到 DragPoints、AnchorPoints 和 Connections 用于指定变形点,以及 FileName、MainBitmap、ImageW 和 ImageH 用于处理图像。属性和命令将绑定到 View/XAML。
// Interface IMyMainVm
// Note: All properties are notified MVVM properties, for example
// property double X in interface IPointItem is in MyMainVm class:
// private double x;
// public double X
// {
// get { return x; }
// // Common implementation of SetProp in Utils requires .NET 4.5
// set { SetProp(ref x, value); }
// }
public interface IPointItem
{
double X { get; set; }
double Y { get; set; }
string Name { get; set; }
string Color { get; set; }
}
public interface IConnection
{
MyMainVm.PointItem Point1 { get; set; }
MyMainVm.PointItem Point2 { get; set; }
}
public interface IMyMainVm
{
ObservableCollection<MyMainVm.PointItem> DragPoints { get; set; }
ObservableCollection<MyMainVm.PointItem> AnchorPoints { get; set; }
ObservableCollection<MyMainVm.Connection> Connections { get; set; }
bool WarpOriginalOnCommand { get; set; }
bool NotWarpedYet { get; set; }
bool MyDebug { get; set; }
ObservableCollection<string> LogLines { get; set; }
// FileName some extra actions in setter.
string FileName { get; set; }
MyBitmap MainBitmap { get; set; }
double ImageW { get; set; }
double ImageH { get; set; }
Commands Commands1 { get; set; }
}
public interface ICommands
{
...
void SetAnchorsToDragPoints();
// AddNewPoint generates for new points a new colour
void AddNewPoint(double x, double y);
void SetToBitmapPixelCoord
(ref Point[] P, ObservableCollection<ViewModel.MyMainVm.PointItem> Points)
void OnDoWarpImage
(bool useImageFromFile, bool anchors2DragPoints);
ICommand ResetImage { get; }
ICommand SaveImage { get; }
ICommand ClearAll { get; }
ICommand AddTestPoints { get; }
ICommand DeletePoint { get; }
ICommand DoWarpImage { get; }
ICommand StartDragging { get; }
// CanExecute EndDragging is coupled to !WarpOriginalOnCommand
ICommand EndDragging {get;}
}
图像和变形点的 XAML/视图
现在我们将讨论视图(MainWindow.xaml)。我们将重点关注负责显示图像和指定变形点的部分,请参阅下面的 XAML 代码。一个网格包含一个 Image
和 3 个 ItemsControl
。由于未指定 GridRow
或 GridColumn
,所有这些组件都彼此叠放渲染,Image
首先渲染,最后一个 ItemsControl
最后渲染。
如果我们查看 Image
的 XAML,我们会看到图像源绑定到 ViewModel
中的 MainBitmap.MyBitmapSource
。我们将需要 ViewModel
中的 ImageW
和 ImageH
来将 Image
坐标转换为 Bitmap
坐标。但是,它们不能直接绑定到图像的 ActualHeight
和 ActualWidth
,因为只读依赖属性不能绑定到 ViewModel
变量(哎呀??)。这里我们使用 DataPiping
进行此数据绑定,请参阅用户 Dmity Tashkinov 对 StackOverflow 问题 将只读 GUI 属性推回 ViewModel 的回答。
OnDragDelta
事件就属于那里。MouseLeftButtonDown
(我在 View
中做了一些事实调查)和文件拖放事件的代码可以移动到 ViewModel
,但我有点懒。 <Grid>
<Image x:Name="myImage" Stretch="Uniform" Height="600" Width="600"
HorizontalAlignment="Left" VerticalAlignment="Top"
MouseLeftButtonDown="Grid_MouseLbdnNewPoint"
Source="{Binding MainBitmap.MyBitmapSource}" >
<!--Just bind ImageH, ImageW to a read only DP..-->
<u:DataPiping.DataPipes>
<u:DataPipeCollection>
<u:DataPipe
Source="{Binding RelativeSource=
{RelativeSource AncestorType={x:Type Image}}, Path=ActualHeight}"
Target="{Binding Path=ImageH, Mode=OneWayToSource}"/>
<u:DataPipe
Source="{Binding RelativeSource=
{RelativeSource AncestorType={x:Type Image}}, Path=ActualWidth}"
Target="{Binding Path=ImageW, Mode=OneWayToSource}"/>
</u:DataPipeCollection>
</u:DataPiping.DataPipes>
</Image>
<!--Note subtle order: zindex determined by drawing in ItemControl,
so zindex AncherPoints < DragPoints-->
<ItemsControl ItemsSource="{Binding Connections}"
Style="{StaticResource connectionStyle}"/>
<ItemsControl ItemsSource="{Binding AnchorPoints}"
Style="{StaticResource ancherPointsStyle}"/>
<ItemsControl ItemsSource="{Binding DragPoints}"
Style="{StaticResource dragPointsStyle}"/>
</Grid>
变形点由 3 个 ItemsControl
表示。它们绑定到 ViewModel
中的 Connections
、AnchorPoints
和 DragPoints
,并具有 Style
。我们将在下一节讨论这些 ItemsControl
,重点是 dragPointsStyle
。
拖动点的样式:模板化缩略图和 MVVM
在本节中,我们将讨论具有最复杂样式(dragPointsStyle
)的 ItemsControl
的样式。我们将更详细地讨论样式,原因有二,这可能会启发读者:
- 使用此方法显示例如连接图非常容易
- 我们在
ViewModel
中使用了PointItem
。但是,使用此方法,也很容易定义一个ImageItem
(包含图像)并创建/拖放它们。
请参阅下面样式对应的 XAML 代码,我只会简要描述 XAML 代码。为了强调 XAML 样板代码的简洁性,我们跳过了一些代码,例如上下文菜单
- 样式
dragPointsStyle
定义了一个ItemTemplate
。(中间的 XAML 代码块) ItemTemplate
定义了一个DataTemplate
。DataTemplate
是一个带Thumb
的Canvas
。Thumb
有一个模板:dragPointThumb
(第一个代码块)。- 此
ControlTemplate
定义了DragPoints
项的渲染方式:一个用于删除点的上下文菜单、一个彩色Ellipse
和一个Name
(此处为数字)。
<!-- Some Snippets from XAML -->
<Window.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="dragPointThumb">
<Canvas>
<!-- Quick hack1. Position around centre by manually setting Margin -0.5* H,W.-->
<Grid
Margin="-10"
<Grid.ContextMenu>
......
</Grid.ContextMenu>
<!-- Other application: Just add an image here and to VM -->
<Ellipse Fill="{Binding Color}" Width="20" Height="20"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<ContentPresenter Content="{Binding Name}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Canvas>
</ControlTemplate>
<Style x:Key="dragPointsStyle" TargetType="{x:Type ItemsControl}">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Canvas>
<Thumb
Template="{StaticResource dragPointThumb}"
Canvas.Left="{Binding X, Mode=TwoWay}"
Canvas.Top="{Binding Y, Mode=TwoWay}"
DragDelta="OnDragDelta"
<!--Binding to DataContext see-->
<!--http://stackoverflow.com/questions/3404707/access-parent-datacontext-from-datatemplate-->
u:Event2Command1.Command=
"{Binding Path=DataContext.Commands1.StartDragging,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type ItemsControl}}}"
u:Event2Command1.OnEvent="DragStarted"
...
u:Event2Command2.OnEvent="DragCompleted"
>
</Thumb>
</Canvas>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="connectionStyle" TargetType="{x:Type ItemsControl}">
...
<DataTemplate>
<Canvas>
<Line Stroke="{Binding Point1.Color}"
X1="{Binding Point1.X, Mode=OneWay}"
Y1="{Binding Point1.Y, Mode=OneWay}"
X2="{Binding Point2.X, Mode=OneWay}"
...
</ResourceDictionary>
</Window.Resources>
- 请注意,
connectionStyle
的DataTemplate
(最后一个代码块)定义了Connection
点之间的一条线。
我现在将讨论 Thumb
的一些细节。因为我们选择了 Canvas
作为 ItemTemplate
,所以我们可以访问附加依赖属性 Canvas.Left
和 Canvas.Top
。这些在 XAML 中绑定到 DragPoints
项的 X,Y
属性,因此它们通过绑定机制进行更新,反之亦然。当拖动时,Canvas.Left
和 Canvas.Top
在视图的后台代码中通过 OnDragDelta
事件处理程序更新,请参阅下面的代码。通过使用 Thumb
(“专为拖动而生”)和 DragDelta
事件,我们可以稳定地拖动。
private void OnDragDelta(object sender, DragDeltaEventArgs e)
{
var thumb = e.Source as Thumb;
var left = Canvas.GetLeft(thumb) + e.HorizontalChange;
var top = Canvas.GetTop(thumb) + e.VerticalChange;
Canvas.SetLeft(thumb, left);
Canvas.SetTop(thumb, top);
}
StartDragging
和 EndDragging
命令用于交互式“拖动点”模式。DragStarted
和 DragCompleted
事件在 XAML 中使用 Event2Command
(哎呀!)绑定到这些命令,这是一个来自这里的旧解决方案。如果您喜欢,可以使用 NuGet(Visual Studio:工具...库包管理器)中的 MVVM 框架、Blend 行为或 .NET 4.5 标记扩展。
将图像坐标转换为位图坐标
可以通过选择“测试坐标”复选框并执行图像变形来测试一些坐标。在 Image 目录中,我们从另一个 CodeProject 复制了一些具有不同 DPI 的图像进行测试。我们没有提供带有网格的图像或叠加网格的图像。应该注意的是,Image
总是缩放的。目前,Image
在给定固定最大 Width
和 Height
的情况下进行 Uniform
缩放。当使用 Scaling="None"
并且拖入具有不同 DPI 的类似 Image
文件时,渲染的 Image
的大小是不同的。
为了将相对于 Image
的坐标转换为 Bitmap
像素中的坐标,我使用了 ViewModel
的 ImageW
(绑定到 ActualHeight
)和 ImageH
,以及 ViewModel
的 MyBitmap.MyBitmapSource
(在下一节中讨论)中的 PixelWidth
和 PixelHeight
。
托管代码中从 WPF 图像到 2D 像素数组
我们处理 WPF 图像的方式如下。在 ViewModel
中,我们有一个 MainBitmap
。它是 MyBitmap
类型,请参阅下面的代码了解其接口。它有一个属性 MyBitmapSource
,其 setter 始终发送通知。MyBitMapSource
可以通过设置 FullName
从文件设置/读取,或者我们可以向其分配新的 BitmapSource
(或 WritableBitmap
)。因为它在 View/XAML 中绑定到视图,所以更改将显示出来。
public interface IMyBitmap
{
BitmapSource GetBitmapSource(string fullName, int thumbnailWitdh);
void SaveCurrentImage2File(string fullFileName);
// Load MyBitmapSource from file if set:
string FullName { get; set; }
// User friendly name for display:
string ShortName { get; }
// Maximize ImageWidth if set:
int ThumbnailWidth { get; set; }
// Lazy loading using fullname. If null Getter loads from file fullName
// Setter always gives a notification (only property with notify)
BitmapSource MyBitmapSource { get; set; }
}
我们对处理图像的所有像素感兴趣。接下来是使用托管代码获取一个 4 字节像素的 2D 数组。我重新使用了我编写的 SideViewer 中的代码,这是一个简单的图像查看器,带有一个实验性选项来查找重复图像。首先,我们定义一个像素类,请参阅下面的代码。我们可以访问整个无符号 32 位整数,或者只访问一个字节,例如 pixel.Blue
。该类来自用户 Dänu 在 StackOverflow 问题 查找 BitmapImage 的特定像素颜色 的回答。
[StructLayout(LayoutKind.Explicit)]
public struct PixelColor
{
// 32 bit BGRA
[FieldOffset(0)]
public UInt32 ColorBGRA;
// 8 bit components
[FieldOffset(0)]
public byte Blue;
[FieldOffset(1)]
public byte Green;
[FieldOffset(2)]
public byte Red;
[FieldOffset(3)]
public byte Alpha;
}
接下来,我们必须将字节从 BitmapSource
复制到 PixelColor
数组。我们使用了 Ray Burns 用户在已知 StackOverflow 问题 查找 BitmapImage 的特定像素颜色 中给出的令人印象深刻的回答中的代码。其思想是从文件读取或使用现有 BitmapSource
并将其格式设置为 PixelFormats.Bgra32
。接下来可以获得一个 1D 数组,该数组可以转换为 2D 4 字节数组或 2D R、G、B 浮点数组。2D 数组的索引是 [iy,ix
],这样该数组就可以用作 Writeable
位图的输入。请参阅下面的代码,了解 Image2PixelsArray.cs
中的函数片段。
public static class Image2PixelArray
{
public static PixelColor[,] GetPixelsTopLeftFromFilename
(string _fileName, int DecodeHW = 0)
public static PixelColor[,] GetPixelsTopLeft(BitmapSource source)
// Given PixelFormats.Bgra32 BitmapSource copies pixels in 1D array
// and next to 2D PixelColor array. Copy to other representation here.
private static void CopyPixelsTopLeft2
(this BitmapSource source, PixelColor[,] pixels)
public static int GetH(PixelColor[,] PixelsTopLeft)
public static int GetW(PixelColor[,] PixelsTopLeft)
// Return a writeable bitmap to assign to a notified BitmapSource
public static WriteableBitmap BitmapSourceFromPixelsTopLeft
(PixelColor[,] PixelsTopLeft, double DpiX = 96.0,double DpiY = 96.0)
}
要从 2D PixelColor
数组转换为新的位图,我们使用新创建的 WriteableBitmap
。WriteableBitmap.WritePixels
直接接受 PixelColor
数组。因为我们创建了一个新的 BitmapSource
,所以我们可能不需要 WriteableBitmap
。请注意,WriteableBitmap
通常通过 Lock
、在 BackBuffer
中写入 Rect
,然后 Unlock
来使用。这在处理图像的一部分时肯定更合适,但即使在处理整个图像时,它也可能更快。
我没有在各种图像格式上系统地测试这个,但在一些彩色 .Jpeg、.Gif 和 .Bmp 图像样本中没有发现问题。如果您想知道其他图像格式是否有效,可以自行通过图像变形来尝试。我假设如果图像可以显示,那么 Bgra32 格式化是可能的。我模糊地记得,在过去,图像格式(.Tiff?)可以支持 4 个原点(左上角等)和 2 个 xy 扫描线方向,所以我不知道所有这些格式是否按预期工作。
刚性点对点均方差最小算法
现在我将讨论文章中刚性 MLS 点对点图像变形的实现:“使用移动最小二乘法的图像变形”,无需预计算。我只是实现了方程,没有理解、质量测试和实验。本文这一部分的复杂性在实现方面有点令人失望,这是本文的优点。我将讨论我在当前实现中使用的主要方程。
方程见下图。第一个方程最重要:它是被优化的准则。我们的 AncherPoints
将是 p,DragPoints
(所需的变形位置)将是 q。原始图像的 MeshPoint
将是 v。(请注意,在下一节中我们将改变所有这些。)对于每个 MeshPoint
v,我们尝试找到一个最优变换 Lv。因此,每个 MeshPoint
我们有一个最优的共同变换,但每个网格点(位置 v)都有不同的变换。最优变换是在所有指定的变形点 i 上,变换后的 AncherPoint
和所需位置之间的加权距离最小的变换。
第二个方程表明,权重对于每个位置 v 都不同。如果我们将 alpha=1,则变形点 i 的距离(变换后的 AncherPoint
,拖动的 PointItem
)的权重与 MeshPoint
v 到 AncherPoint
i 的距离成反比。请注意,其他加权方案也是可能的,例如在 p-q 线区域周围更对称的“高斯”条,或根据 N 个最近邻居调整准则。
所有方程都非常基本。请参阅下面代码中构建的 MeshPoint
类接口
// Interface IMeshPoint - not in source;
// introduced some properties and made some functions public
public interface IMeshPointV
{
// _x,_y coordinates MeshPoint v.
void ComputeTransformationParameters
(double _x, double _y, int _nPoint, Point[] _p, Point[] _q);
Point TransformL(Point p);
// Variables named after article
double X { get; set; }
double Y { get; set; }
int NPoint { get; set; }
Point[] P { get; set; }
Point[] Q { get; set; }
double[] W { get; set; }
double WSum { get; set; }
double PStarX { get; set; }
double PStarY { get; set; }
double QStarX { get; set; }
double QStarY { get; set; }
Point[] PHat { get; set; }
Point[] QHat { get; set; }
double M11 { get; set; }
double M12 { get; set; }
// supporting procedures
void ComputeW();
void Compute_pStar_qStar();
void Compute_pHat_qHat();
void ComputeM();
}
刚性变换由底部方程(文章中的(6))给出。在简化的矩阵计算中,我没有使用预计算,而是直接对矩阵进行了归一化,请参见下面的代码。我们只需要计算矩阵 M 的 2 个元素。乍一看,一切似乎都很简单,但如果发现任何错误,请报告。
// We do no pre-computation but compute and normalise M directly
private void ComputeM()
{
m11 = 0;
m12 = 0;
for (int i = 0; (i < nPoint); i++)
{
double a = pHat[i].X;
double b = pHat[i].Y;
double c = qHat[i].X;
double d = qHat[i].Y;
// a b c d
// M = MuNorm* Sum w[i] ( ) ( ) (eq. 6) from article
// b -a d -c
m11 = m11 + w[i] * (a * c + b * d);
m12 = m12 + w[i] * (a * d + b * -c);
// m21 = m21 + b*c - a*d; = -m12
// m22 = m22 + b*d + a*c; = m11
}
// Norm, Mt M = I so muNorm is
double muNorm = Math.Sqrt(m11 * m11 + m12 * m12);
// If we do not have a valid transformation, use Identity transformation
// (nPoint==1) ==> M = (a,b,c,d) == (0,0,0,0), muNorm test fails
// Only blob errors observed at nPoint==1, so for now extra test
if ((muNorm < ToSmallHeuristic) || (nPoint == 1))
{
m12 = 0.0;
m11 = 1.0;
}
else
{
m11 = m11 / muNorm;
m12 = m12 / muNorm;
}
}
给定 AncherPoints
的原始坐标 (p) 和所需的 DragPoints
(q),我们可以通过调用 MeshPoint
类中的函数 ComputeTransformationParameters(..)
和 TransformL(..)
来计算 MeshPoint
v 的局部变换。使用矩形网格的简单反向变换
现在,我们想构建变形后的图像。为了保持简单,我们使用反向变换。对于目标图像的每个像素,我们计算原始图像中的(浮点)坐标。接下来,我们计算其在原始图像中的最近邻像素。利用(浮点)坐标及其邻居像素的 RGB 值,我们可以使用线性或三次插值来获取给定(浮点)坐标处目标像素的 RGB 值。对于目标图像像素坐标的反向变换,我们调用函数
ComputeTransformationParameters(..)
和 TransformL(..)
。对于第一个函数调用,我们现在使用 (..,DragPoints,AncherPoints)
而不是 (..,AncherPoints,DragPoints)
,例如我们将 DragPoints
作为参数 _p,将 AncherPoints
作为参数 _q。(请注意,由于准则中 p 和 q 之间距离的不对称性,这并不完全正确。)我们只在 MeshGrid
点计算反向变换,通过线性插值在行和行之间插值其他像素的变换后 (x,y
) 坐标。请参阅下面的代码,了解 MovingLeastSquaresRectGrid
类的接口。函数 InitBeforeComputation(..)
设置内部变量,函数 WarpImage(..)
计算变形图像。
public interface IMovingLeastSquaresRectGrid
{
void InitBeforeComputation(Point[] _p, Point[] _q, int _ImgH, int _ImgW, int stepSize);
WriteableBitmap WarpImage(PixelColor[,] Pixels, double DpiX, double DpiY);
int NPoint { get; set; }
Point[] P { get; set; }
Point[] Q { get; set; }
int ImgH { get; set; }
int ImgW { get; set; }
// MeshPoint computed if (VXCompute[ix]&&VYCompute[iy])
bool[] VXCompute { get; set; }
bool[] VYCompute { get; set; }
void SetXYCompute(int ImgH, int ImgW, int stepSize);
Point[] ComputeVAndInterpolateXYRow(int iy);
Point[] InterpolateXYRow(int iy, int iy1, int iy2, Point[] xyRow1, Point[] xyRow2);
PixelColor BilinearRgbInterpolation(PixelColor[,] PixelsOrg, Point coordOrg);
}
如果 VXCompute[ix]
和 VYCompute[iy]
都为 true
,则点 (ix,iy
) 是 MeshPoint
。网格点的步长指定了变形/非线性的细节。在函数 WarpImage(..)
的主循环中,处理所有行。使用 ComputeVAndInterpolateXYRow(iy)
计算 2 个连续行(其中 (VCYCompute[iy]==true)
)的变换坐标。后一个函数计算像素 ix 的反向变换(其中 (VXCompute[ix]==true)
)并在它们之间进行插值。InterpolateXYRow()
用于在计算网格点的两个连续行之间进行插值。
以这种方式,计算目标图像所有像素的变换坐标,最后使用函数 BilinearRgbInterpolation(..)
在 PixelColor
数组中设置像素。该数组最终写入 Writeable
位图。
关注点
- 该程序给出了点对点图像变形的初步印象。
- 给出了一个链接点的简单示例,并展示了它们的注释能力。
- 给出了一种访问 2D 像素数组的方法。可以自由实现自己的算法。
- 可以下载刚性点对点 MLS 的 C# 代码和反向变换。
- 点对点变形在某些情况下按预期执行。
- 附近的点可能会导致图像局部变形(咖啡杯示例)。我们遇到了一个上下文/分辨率问题。
- 可以存储所有网格点的 (
x,y
) 平移,并对其应用高斯滤波器以控制分辨率。 - 点并不总是指定变形的最佳方式。
- 一种解决方案是实现线对线 MLS。另一种处理此类局部变形的方法是众所周知的引入像素坐标多项式变换(放大、缩小、倾斜等)的方法。
- 在交互模式下,图像质量每次迭代都会下降。在交互模式下不再可见的像素会丢失。可以引入内部浮点表示,并结合更好的(三次)插值。
- 一些大的转换可能会产生一些奇怪的效果。假设我们有一个变形点(锚点、期望的拖动位置)。通常在变形之后,锚点处的对象会显示在期望的位置。然而,其他变形点的放置可能具有这样的性质,即在该对象原始位置处它也可见。这与移动像素流的思想是反直觉的。我们能否将大转换分解为更小的步骤以防止此类效果?
历史
- 2014-01-06. 本项目结束。2014 年编码愉快!