65.9K
CodeProject 正在变化。 阅读更多。
Home

图像变形的乐趣:本地基于网格的图像变形器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (51投票s)

2011年4月15日

CPOL

11分钟阅读

viewsIcon

145543

downloadIcon

4094

独立于平台的图像变形引擎

背景

一般来说,数字图像变形 是对数字图像进行几何变换的过程,包括从简单的变换(如缩放或旋转)到复杂、不规则的变形。这个主题在 20 世纪 80 年代引起了学术界的广泛关注。如今,它是基础主题,经常在本科生的数字图像处理/计算机图形学课程中讲授,例如 这个这个这个。数字图像变形的应用在娱乐行业广泛可见,尤其是在基于计算机的图像修饰和整容手术中。

cows.jpg

图 1. 原始图像(左)与相应的 VPSS 结果(中)和我们的结果(右)。原始图像应用了平移(蓝色圆圈)、放大(红色)和缩小变形器(黄色)。原始图像取自 此处。本文中的图像不归我所有,它们是从互联网上随机收集的。

距离我上大学时的数字图像处理本科课程已经很久了(虽然我记得,图像变形技术并没有包含在那门课程中,我不知道为什么),也许我早就完全忘记了这些东西,如果我没有偶然发现 这个页面。VPSS 是一个出色的图像变形器,它以一种巧妙的方式实现,使得输出质量非常出色。是的,正如您可能已经注意到的,它是一个商业应用程序,价格为 29.95 美元。

嗯,我只是想知道我对这个领域的理解是否能帮助我开发另一个图像变形器,并产生(希望是)有竞争力的结果。因此,我“奉献”了我的上一个周末来开发我自己的变形引擎,它具有以下功能:

  • 允许用户在特定位置平移、放大和缩小图像,具有不同的画笔大小,即有效区域半径的不同值。这些也是 VPSS 的主要功能。
  • 平台独立性。图像处理可能取决于平台和/或我们使用的库,例如,Win32 API 中的 `BITMAP`、.NET 中的 `Bitmap`、OpenCV 中的 `IplImage`,甚至 iOS 中的 `CGImage`。我期望变形引擎应该是平台独立的,以便可以在不同平台上重用而无需任何修改。为此,该引擎完全用 C++ 构建,并且仅处理原始形式的图像。这意味着我们必须处理 `char[]` 数组和填充字节的混乱……像 `Point`、`Rectangle`……这样的其他基本结构也需要定义在引擎内部。
  • 良好的(且足够灵活的)过滤策略。变形数字图像通常涉及重采样过程,这可能会引入混叠并降低图像质量。为了尽量减少质量损失,需要进行过滤。有许多适用的过滤器,从简单的插值和抽取到复杂的过滤器,如椭圆加权平均滤波器 (EWA)。然而,这又是传统质量-性能权衡再次出现的地方:过滤器越好,处理时间就越长。假设我们正在为 iPhone 开发一个交互式图像变形器,性能标准可能至关重要,因此使用简单的插值滤波器是唯一可接受的解决方案。因此,引擎应该灵活支持不同的过滤策略。

坦率地说,我必须承认我并没有实现所有这些目标。在过去的周末两天里,我只设法构建了一个质量可接受的平台独立引擎。关于最后一个目标,我只实现了一个简单的插值滤波。但是添加其他过滤策略非常直接。

我的实现的と结果以及与 VPSS 的比较如图 1 所示。

Using the Code

该引擎支持(但不限于)三种类型的变形,这些类型由以下常量表示

#define WARPER_TRANSLATE 0
#define WARPER_GROW      1
#define WARPER_SHRINK    2 

要使用该引擎,您有 2 种选择:

  • 将源代码集成到您的 C++ 应用程序中。这样,您可以以面向对象的方式访问引擎,并且更有可能更改引擎的工作方式。但是,假设您正在处理一个 .NET 项目并想使用该引擎。由于 .NET Framework 中处理非托管对象的方式,您肯定不能简单地将这些 C++ 类“导出”并在 .NET 中直接使用它们。在这种情况下,您可以在下一样式中使用该引擎。
  • 将引擎编译成一个库,并使用一些简单的子例程调用它,我称之为非面向对象的方式

本节将展示如何在两种选项中都使用该引擎。

以面向对象的方式使用

在 C++ 项目中,您可以以面向对象的方式访问引擎。引擎的所有功能都可以通过 `Warper` 类访问,因此您需要为每个图像分配一个新的 `Warper`。

// The general idea is to create a new ImageData object,
// and then create a new Warper.
// ImageData is a structure defined inside the engine.

ImageData imgData;
imgData.Resize(bmpData.Width, bmpData.Height, 3, bmpData.Stride);
memcpy(imgData.Data, (char*)(void*)bmpData.Scan0, bmpData.Stride * bmpData.Height);

// Create new Warper.
m_warper = new Warper(imgData); 

在基于交互式的变形系统中,用户期望应用程序在他们的每次交互步骤中都能响应。因此,要开始、更新和完成一个“变形”,您只需要使用 `Warper` 的三个函数,分别称为 `BeginWarp()`、`UpdateWarp()` 和 `EndWarp()`。

void form1_MouseDown(...)
{
  if (e.Button == MouseButtons.Left)
  {
    Point pt;
    //...
    m_warper.BeginWarp(pt, m_iRadius, m_iWarperType);
    //...
}

void form1_MouseMove(...)
{
  // ....
  // Get the result in warpedImg by calling UpdateWarp()
  WarpedImage* warpedImg = m_warper->UpdateWarp(pt); 
  
  // show the result on UI
  if(warpedImg)
    DrawImage(m_bmpImage, warpedImg);
}

void form1_MouseUp(...)
{
  //......
  // first, calling UpdateWarp() function
  WarpedImage* warpedImg = m_warper->UpdateWarp(pt);  

  // then call EndWarp()
  if(warpedImg)
  {
    warpedImg = m_warper->EndWarp(warpedImg);
    DrawImage(m_bmpImage, warpedImg, false);
  }
}

通常,平移变形器可以在 `MouseUp()` 和 `MouseDown()` 事件中直接启动和结束。然而,缩小和放大变形器需要一个定时器来连续更新,为用户提供良好的体验。您可以查看演示应用程序以获取更多详细信息。就是这样。您的应用程序现已准备就绪!

以非面向对象的方式使用

如果您正在处理 .NET 应用程序,则无法直接使用 C++ DLL 导出的类。原因是 .NET Framework 中的 CLR 无法处理 C/C++ 非托管对象。但是,我们可以考虑这些变通方法:

  1. 将 C++ DLL 封装在另一个 C++/CLI 管理项目中。C++/CLI 项目将充当中间协调者,负责管理 C++ DLL 的非托管对象。这可以提供从 .NET 应用程序使用面向对象特性的能力。
  2. 这种方式,尽管我宁愿不这样做。
  3. 在 C++ DLL 中创建 C 风格的包装器函数,以便它仅通过子例程暴露其 API。这些子例程负责分配和释放 C++ 对象。 .NET 中的应用程序只需要调用这些子例程。这将把面向对象的实现分解成一些 .NET 应用程序可以通过其 P/Invoke 机制调用的函数。

我在这里不会详细讨论这些方法的缺点 vs. 优点。粗略地说,您可以看到第一种方法更具可扩展性,但可能会影响引擎的性能,因为 C++/CLI 通常比非托管 C++ 慢。另一方面,最后一种方法速度很快但不可扩展:如果 DLL 中有更多对象,您将需要为 API 函数做更多工作。我选择了最后一种方法,仅仅因为我认为在这种情况下更容易实现。

这是引擎的 6 个函数的 C# 声明:

[DllImport("ImageWarper.dll", EntryPoint="CreateWarper")]
private static extern int CreateWarper(int width, int height, 
    int scanWidth, int bpp, IntPtr rawData); 

[DllImport("ImageWarper.dll", EntryPoint="ReleaseWarper")]
private static extern int ReleaseWarper(int warperId);

[DllImport("ImageWarper.dll", EntryPoint="BeginWarp")]
private static extern void BeginWarp(int warperId, 
    int centerX, int centerY, int brushSize, int warperType);

[DllImport("ImageWarper.dll", EntryPoint="UpdateWarp")]
private static extern IntPtr UpdateWarp(int warperId, int x, int y, 
    ref int xRet, ref int yRet, ref int width, ref int height, ref int scanWidth);

[DllImport("ImageWarper.dll", EntryPoint = "EndWarp")]
private static extern IntPtr EndWarp(int warperId,
    ref int xRet, ref int yRet, ref int width, ref int height, ref int scanWidth);

它们完全不言自明,所以我不做更多细节。在附加的源代码中,该引擎被实现为一个名为 `ImageWarper` 的库。我包含了一个名为 `ImageWarperTest` 的 C++/CLI 项目,以及另一个名为 `WarperTestManaged` 的 C# 项目,它们都引用 `ImageWarper`。`ImageWarperTest` 以面向对象的方式调用引擎,而 `WarperTestManaged` 以非面向对象的方式使用引擎。您可以深入研究源代码以获取更多详细信息。

实现

本节大致描述了引擎的设计以及它是如何/为什么实际工作的。
作为本文的泛化,变形引擎分两步进行:

  1. 创建并维护一个围绕初始光标位置的网格。网格只覆盖一小块区域,并且当用户将光标移出初始网格时,其大小可以动态自动更改。当光标在网格内移动时,它会根据当前的变形样式(平移、放大或缩小)进行变形。网格上的交点作为下一步的引导。
  2. 网格内的像素值是根据网格的新配置计算的。此步骤可能涉及一些滤波技术,这些技术可以提高最终图像的整体质量。

这两个步骤构成了本文的名称:局部网格图像变形器。局部意味着变形器仅更改光标周围的区域,而基于网格表示变形过程使用网格作为引导。这种区别也有助于使变形器更灵活,因为我们可以通过应用新的合适策略来采用任何变形样式(第一步),也可以为第二步实现任何滤波技术。这些步骤如图 2 所示。

grid_initial.jpg
(a)
grid_translate.jpg
(b)
grid_grow.jpg
(c)
grid_shrink.jpg
(d)
图 2. 基于网格的变形器。(a):创建初始局部网格。(b)、(c) 和 (d):在平移、放大和缩小的情况下变形网格。注意网格是如何变形的,网格上的每个交点将指导其下方像素的变形过程。

引擎的所有功能都在 `Warper` 类中实现。

class DLLEXPORTED Warper
{
public:

  // ....

  // Begin the warping interaction. Called when the interaction begins.
  // ptCenterPos: Location (in pixels) of the mouse click, in regard of the image.
  // iBrushSize: The radius of the effective window, in pixels. 
  // This should not be too large (<= 50).
  // iWarpType: WARPER_TRANSLATE, WARPER_GROW or WARPER_SHRINK
  void BeginWarp(Point &ptCenterPos, int iBrushSize, int iWarperType);

  // Called at every moving step of the interaction.
  // ptMouse: Location (in pixels) of the current interaction, in regard of the image
  // Returns: a WarpedImage, contains the location and the image data
  // of the warped image patch.
  // Please DO NOT modify/delete the returned pointer!
  WarpedImage *UpdateWarp(Point &ptMouse);

  // Called when the interaction finished.
  // Should be call right after a call to UpdateWarp().
  WarpedImage *EndWarp(WarpedImage *warpedImg);

  // ....
};

第一步由 `WarperCanvas` 的派生类执行。在引擎中,我们有 `TranslateCanvas` 和 `GrowCanvas`,它们将根据各自的变形策略(`TranslateCanvas` 用于平移,`GrowCanvas` 用于放大和缩小)变形网格。这可以在 `Warper::UpdateWarp()` 中看到。

WarpedImage* Warper::UpdateWarp(Point &ptMouse)
{
  // ...
  // deforms the grid
  m_canvas->Force(m_ptCenterPos, ptMouse);  

  // interpolating the pixel values based on the grid
  // (which is stored in m_canvas->GetOffsetPoints())   
  Warper::OffsetFilter(m_imgOriginal, m_canvas->GetOffsetPoints(),
        *(m_canvas->GetBoundary()), &(m_warpedImage->Image));

  return m_warpedImage;
}

在平移变形器中,网格上的一个交点 \mathbf{p}=\left(x,y\right)^T 会通过应用以下公式移动到新位置:

\mathbf{p}\leftarrow \mathbf{p}+ \left(\frac{\parallel \mathbf{p}-\mathbf{s}\parallel}{d}-1\right)\left(\mathbf{c}-\mathbf{s}\right)

其中 `c` 和 `s` 分别是光标的当前位置和起始位置,`d` 是反映有效区域半径的距离。

对于放大和缩小变形器,公式甚至看起来更简单:

\mathbf{p}\leftarrow\mathbf{c}+\left(\frac{\parallel\mathbf{p}-\mathbf{c}\parallel}{d}\right)^r\left(\mathbf{p}-\mathbf{c}\right)

其中 `r` 是放大因子。对于放大情况,`r` 大于 0;对于缩小情况,`r` 小于 0。

第二步是推断网格内像素的新值。这是通过一个简单的插值滤波完成的,该滤波在Tolga Birdal 的文章中有描述。您可以在 `Warper::EndWarp()` 函数的源代码中看到。

未来工作

这个项目有很多可以改进的地方。

  • 我想到的第一个想法是改进通用策略,从而提高最终图像的质量。看看这两个图像序列。第二行是我的变形器的结果,底部行是 VPSS 的结果。我现在不知道他们是如何实现这个功能的。根据我的最佳猜测,我认为他们以某种方式“记住”了每个像素的移动轨迹,然后在需要时进行重建。这很有意义,因为当您“放大”一个图像块时,您将把该块中的信息分布到相邻位置,而由于中心位置信息不足而导致的模糊。我仍在考虑实现这些技术的可能性,并热烈欢迎任何建议!
dog.jpg
(a)
dog_mine_Shrink.jpg
(b)
dog_mine.jpg
(c)

dog_vpss_shrink.jpg
(d)
dog_vpss.jpg
(e)
图 3. 原始图像 (a) 被缩小 (b 和 d),然后在同一位置应用了放大变形 (c 和 e)。第二行是我们实现的,它在 (c) 中显示额头上有许多模糊,而 VPSS 产生了非常详细和清晰的结果,没有模糊 (e)。这可能归因于缩小和放大的策略。缩小图像时,我们实际上会丢失一些视觉信息,如果我们能以某种方式“存储”这些信息,我们就可以在放大步骤中重新使用它们,从而避免模糊的结果。
  • 看看下面的图片。我们想要的是增强身体,但是由于那个位置附近的背景不均匀,背景也被“变形”了。这个问题的解决方案很简单。引擎应该提供一些方法供用户定义哪些应该被变形,哪些不应该。这是图像处理中一个著名的问题,称为基于交互式的图像分割。分割完成后,引擎将仅变形前景像素。(对于不熟悉交互式图像分割的读者,您可以尝试 Microsoft Office 2010 中的内置功能,称为背景移除。如果您没有 Office 2010,则有大量关于此主题的研究论文,这个这个 是一些著名的方法)。
horse.jpg
(a)
horse_deformed.jpg
(b)
horse_bgs.jpg
(c)
图 4. 带有杂乱背景的图像变形。原始图像 (a) 在某些地方被变形,如 (b) 所示,但是附近的背景也被变形了,因为我们无法区分哪个像素属于该对象。如果应用某种背景减除方法并仅变形前景像素 (c),则可以解决此问题。(原始图像取自 此处)。
  • 其他滤波策略也需要用于获得更好的最终图像质量。

结论

在本文中,我介绍了一个平台独立的通用图像变形引擎。结果与当前最先进的现成商业应用程序相比并不具有竞争力,但是有很多改进的空间,这让我相信该引擎可以以某种方式升级,并提供更好的最终图像。事实上,当我有空的时候,我将实现这些想法,同时,非常欢迎提出建议。

历史

  • 2011年4月15日:首次提交
  • 2011年4月18日:更新了源代码包,以修复翻译接近边界时的错误
  • 2011年5月27日:更新了源代码包,以修复测试项目中绘图错误(如此处所述)
© . All rights reserved.