使用线性插值和双缓冲面板进行图像扭曲






4.29/5 (17投票s)
一个使用简单位移滤镜的 C# 图像扭曲工具。
引言
图像扭曲是一个数字化处理图像的过程,使得图像中描绘的任何形状都发生显著的扭曲。虽然图像数据可以以各种方式进行转换,但纯粹的图像失真意味着点映射到点而不改变颜色。这在数学上可以基于任何从平面(部分)到平面(部分)的函数。如果该函数是单射的,则可以重构原始图像。如果该函数是双射的,则任何图像都可以反向变换。[维基百科]
图像扭曲是从事漫画化或卡通化工作的人们常用的技术。然而,在互联网上很难找到已实现的思路。我使用了一些来自 Christian Graus 的精彩文章集“图像处理入门”中的代码,开发了一个非常基础的图像扭曲器,仅用于说明该操作的思路。
C# 中的图像处理
C/C++ 是图像处理的语言,因为它是原生代码且速度快。然而,因为我们希望将 .NET 的强大功能与图像处理结合起来,所以我们想在 C# 中进行图像或图形处理。一种方法是创建 C++ .NET 包装器,但如果你不想费心于此,你总是可以在 C# 中实现它们,而且大多数时候,算法的性能会比你想象的要快得多。这是因为通过使用不安全代码,我们可以获得更接近原生的(尽管不是完全!.NET 框架仍然存在,但开销较小)并且具有更低层访问权限。C# 的这种特性使其在图形应用程序和算法实现中越来越受欢迎。
双线性插值
在数学中,双线性插值是对二维变量函数在规则网格上进行插值的线性插值的扩展。关键思想是首先在一个方向上进行线性插值,然后在另一个方向上进行线性插值。这样,就可以解决在扭曲图像中找到合适值的问题。在大多数情况下,这足以获得良好的近似。下面的图表解释了整个过程。[维基百科]
以下是维基百科关于双线性插值的说法:
或者,等效地:
其中 f
是图像函数,x
和 y
是当前位置的坐标。
在我使用双线性插值时,我确定了目标图像的新像素值。图像根据鼠标输入进行扭曲。因此,会插值鼠标位置周围的像素值。这减少了计算量。
偏移滤镜
此滤镜也可以称为位移滤镜,完全取自 Christian Graus 的代码。它的基本工作是计算像素从原始位置到目标位置的平移。有关更多详细信息,请参考他的代码。
双缓冲
由于此应用程序的用户交互性非常高,我使用了双缓冲面板。顾名思义,双缓冲在将图像数据发送到屏幕句柄之前会创建一个图像数据缓冲区。因为总有一个缓冲区,所以在用户交互时屏幕不会闪烁。
我使用了 Panel
控件来实现这一点。新的 Panel 类如下所示:
public class DoubleBufferPanel : Panel
{
public DoubleBufferPanel()
{
// Set the value of the double-buffering style bits to true.
this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint, true);
this.UpdateStyles();
}
}
// and we do everything on the paint event
// ....
// This is just the part that calculates and paints the new warped image.
// The grid is also drawn, and the new values are calculated.
private void panel1_Paint(object sender, PaintEventArgs e)
{
if (checkBox2.CheckState == CheckState.Checked)
{
Image temp = (Image)img.Clone();
CalcOffsets();
OffsetFilter((Bitmap)temp, GImg);
if (img != null)
e.Graphics.DrawImage(temp, new Rectangle
(0, 0, panel1.Width, panel1.Height));
}
else
{
if (img != null)
e.Graphics.DrawImage(img, new Rectangle
(0, 0, panel1.Width, panel1.Height));
}
if (checkBox1.CheckState == CheckState.Checked)
{
Pen P = new Pen(Color.Red);
for (int i = 1; i < 9; i++)
for (int j = 1; j < 9; j++)
{
e.Graphics.DrawLine(P, new Point(i * 48 + G[i, j].X, j * 64 +
G[i, j].Y), new Point(i * 48 + G[i, j + 1].X,
(j + 1) * 64 + G[i, j + 1].Y));
e.Graphics.DrawLine(P, new Point(i * 48 + G[i, j].X,
j * 64 + G[i, j].Y), new Point((i + 1) * 48 +
G[i + 1, j].X, j * 64 + G[i + 1, j].Y));
}
}
}
理解代码
好的,这是核心:插值。网格单元大小为 64*48(我知道这相当大),并且在此网格中计算插值(在某些情况下也不希望这样做)。G
是网格本身,GImg
是根据网格上的变形而扭曲的图像。
for (int m = 0; m < 48; m++)
{
for (int n = 0; n < 64; n++)
{
double xfrac = (double)1 / 48.0;
double yfrac = (double)1 / 64.0;
double s = (G[i + 1, j].X - G[i, j].X) * m * xfrac + G[i, j].X;
double t = (G[i + 1, j].Y - G[i, j].Y) * m * xfrac + G[i, j].Y;
double u = (G[i + 1, j + 1].X - G[i, j + 1].X) *
m * xfrac + G[i, j+1].X;
double v = (G[i + 1, j + 1].Y - G[i, j + 1].Y) *
m * xfrac + G[i, j+1].Y;
GImg[i * 48 + m, j * 64 + n].X = -(int)( s + (u - s) * n * yfrac);
GImg[i * 48 + m, j * 64 + n].Y = -(int)( t + (v - t) * n * yfrac);
}
}
使用图像扭曲工具
只需运行代码,然后勾选“显示原始图像”和“显示网格”复选框。现在,双击网格上的一个点并拖动该点。您将看到图像被扭曲。再次双击,您将停止扭曲该点。
进一步改进
- 网格点应该更密集,以便可以从几乎任何位置进行扭曲。
- 由图像中某个单元格的扩展引起的一些失真应该得到妥善处理和纠正。
- 可以实现双三次插值。
- 还有一些,我现在想不起来了。
结论
这只是我开发扭曲应用程序的一个简单尝试,之后我提出了更复杂的方法。我相信这可以帮助那些像我一样疯狂寻找简单扭曲想法的人。