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

您的明信片

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (41投票s)

2011年4月17日

CPOL

13分钟阅读

viewsIcon

57029

downloadIcon

1668

本文介绍如何为您的图像应用晕影效果。

screenshot.png

1. 引言

根据维基百科,摄影中的晕影是指图像的角落和边缘清晰度下降的任何过程。换句话说,这是图像边缘的亮度或饱和度相对于图像中心的降低。就本文而言,晕影是图像周围的柔和边框,边框的形状就是晕影的形状。许多商业软件包都提供晕影效果作为其功能之一。尽管我无法访问 Photoshop 等流行软件包,但我通过查看互联网上的教程,对它的晕影功能有所了解。在本文中,我将介绍一个简单的程序,该程序可以在您喜欢的图像周围“绘制”一个晕影边框。希望这个程序也能创造出 Photoshop 尚未提供的某些晕影效果。

2. 程序特性

此软件应用程序允许您

  • 从五种形状中选择晕影的形状——圆形、椭圆形、菱形、矩形和正方形。
  • 修改晕影的方向(圆除外),范围在 0 到 180 度之间。
  • 修改晕影的覆盖区域,从图像的一小部分到几乎覆盖整个图像。
  • 修改晕影的“混合区”(本文稍后会介绍)的宽度,从窄到宽。
  • 修改混合的平滑度,从粗糙到精细。
  • 修改晕影中心的 x 和 y 坐标,以便可以在图像中移动它。
  • 修改边框区域的颜色。
  • 预览修改以上每个参数的效果。
  • 保存带有晕影效果的图像。

我们将逐一考察这些特性及其实现。由于椭圆形可能是所有形状中最复杂的,因此我将详细解释椭圆晕影,并简要介绍其他形状。

3. 椭圆晕影基础

下图显示了椭圆晕影的基本几何形状。为了清晰起见,我将此图画得有些夸张。给定一张图像,称为“源图像”,以及一种边框颜色,任务是创建一个称为“目标图像”的带有晕影的图像。矩形 OABC 代表图像,其中 OA 是宽度,OC 是高度;Ox 和 Oy 轴代表全局坐标系。期望创建一个具有以下特性的椭圆晕影:

  • 椭圆的方向,图中由角度 theta 表示。
  • 覆盖范围,平均而言,由图中的椭圆 E3 表示。
  • “混合区”的宽度,由图中椭圆 E5 和 E1 的长轴之差表示。这也等于这些椭圆的短轴之差。
  • 混合的平滑度(或粗糙度),由内外椭圆之间的步数表示。例如,椭圆 E5 和 E4 之间的长轴差表示步长。由于从外到内有五个椭圆 - E5、E4、E3、E2 和 E1 - 因此步数为四。暂时忽略虚线椭圆 E34。
  • 椭圆中心的 x 和 y 坐标(点 O1),由x-offsety-offset 表示。图像本身的中心点由图像中间的 + 号表示。
  • 边框的颜色,由椭圆 E5 外围的颜色表示。

Figure3.png

3.1 更详细的问题陈述

在图像中创建三个区域,使得

  • 在区域 A 中,代表图中内层椭圆 E1 内部的区域,目标图像中的像素直接取自源图像中的相应像素。包围区域 A 的椭圆 E1 的长轴和短轴计算如下:
  • 椭圆 E1 的长轴 = 椭圆 E3 的长轴 - 混合区宽度的一半

    椭圆 E1 的短轴 = 椭圆 E3 的短轴 - 混合区宽度的一半

  • 在区域 C 中,代表图中外层椭圆 E5 外围的区域,目标图像中的像素直接对应于边框颜色。被区域 C 包围的椭圆 E5 的长轴和短轴由以下公式确定:
  • 椭圆 E5 的长轴 = 椭圆 E3 的长轴 + 混合区宽度的一半

    椭圆 E5 的短轴 = 椭圆 E3 的短轴 + 混合区宽度的一半

  • 区域 B 可以称为混合区。在区域 B 中,代表图中椭圆 E5 和 E1 之间的区域,目标图像中的像素是根据源图像和边框颜色计算的加权和。混合函数的选择决定了混合的平滑度。沿着从内层椭圆到外层椭圆的一条有向线 PQRST,源图像的影响逐渐减小,边框颜色的影响逐渐增加。在这条线的内端 P,源图像完全决定目标像素;而在外端 T,边框颜色完全决定目标像素。在中间的点,目标像素值是两者(如前所述)的加权和。

现在让我们来看看方程及其逐项实现。

4. 每个椭圆的位置、方向和大小(覆盖范围)

从上图可以看出,五个参数决定了每个椭圆的位置、方向和大小:

  • 两个参数 - 椭圆“中心”的 X 坐标和 Y 坐标,在上面的图中由x-offsety-offset 表示。
  • 椭圆长轴的方向“theta”,在上面的图中显示。
  • 两个参数 - 椭圆的长轴和短轴的大小,例如上图中椭圆 E3 的长轴和短轴。

绘制一个“直线”椭圆——即与全局 x 和 y 轴对齐的椭圆——是很直接的。对于倾斜的椭圆则不然。但是,如果您听说过“旋转矩阵”这个词,您会注意到这并不难。以下公式给出了具有上述参数的椭圆方程——包括旋转和平移。在代码中实现此方程非常直接。

eqnRotation.png

关于椭圆的另一件值得注意的事情是,一个点是否在椭圆内或外。这是这样确定的:

eqnEllipseInOut.png

椭圆的覆盖范围非常直接。如果一个椭圆的长轴和短轴值大于另一个椭圆的相应值,那么前者椭圆的面积就大于后者。

在应用程序中,提供了一个滑块来修改椭圆的方向。提供了一个滑块(称为“覆盖范围”)来(按比例)同时增加椭圆的长轴和短轴。两个滑块修改x-offsety-offset 值。

5. 混合区的宽度

区域 B 代表混合的宽度。这是椭圆 E5 和 E1 之间的区域。椭圆 E5 和 E1 的特征是它们的长轴和短轴。混合区的宽度由用户通过其相应的滑块进行修改。

6. 混合的平滑度

要正确处理这一点并不容易。为了得出正确的方程,我付出了很多努力。

考虑两个图像 I1 和 I2 要在区域 I12 上混合。在上图二中,区域 B 代表混合区。在此区域中,目标像素值由源图像和边框颜色共同确定;这里边框颜色也可以看作是另一个图像,称为边框图像。沿线 PQRST 图像贡献的变化需要确定。目标像素值可以取为加权函数的总和

目标像素值 = I1 * w1 + I2 * w2

其中权重w1 = w1(s)w2 = w2(s) 分别是图像 I1 和 I2 的贡献。参数s 是从点 P 沿线 PQRST 测量的线性距离。选择这些加权函数,使其在任何点的总和为一。选择正确的函数w1(s)w2(s) 对于获得视觉上令人愉悦的效果至关重要。让我们定义d 为距离 PT(等于混合区的宽度)。(注意,权重仅在混合区内定义,在混合区外不定义。)

我首先尝试了线性权重(混合函数),其方程为:

eqnLinear.png

对于某些图像-边框颜色组合,效果很好;但对于其他组合,则很糟糕。这让我开始思考新的混合方程。我开始在互联网上搜索合适的混合函数,并偶然发现了 Burt 和 Adelson 的论文 [Peter J Burt 和 Edward H Adelson,具有应用于图像拼接的多分辨率样条,ACM Transactions on Graphics,第 2 卷。第 4 期,1983 年 10 月,第 217-236 页]。我刚读完前三页,就在那篇论文中看到了一个图。这让我想到了使用余弦函数进行混合。推导出方程后,出现了以下混合函数(我想在这里包含推导的详细过程,但发现用 Microsoft Word 2003 输入非常麻烦!)。

eqnCosine.png

数学爱好者可能会注意到,线性混合函数在边界处是 C0 连续的,但在端点s = 0s = d 处不是 C1 连续的。然而,基于余弦的混合函数在这些端点处既是 C0 又是 C1 连续的,这产生了令人愉悦的混合效果。另一个这样的函数是 sigmoid 函数,我将在稍后的版本中尝试它。

现在 comes 一个重要的问题。如何绘制线 PQRST。理想情况下,它应该垂直于它相交的所有椭圆。这需要计算椭圆的导数,并且计算量可能很大。进一步思考后,我决定将混合区分成多个椭圆区域——例如,椭圆 E4 和 E3 之间的区域就是一个这样的椭圆区域。在每个区域内,我考虑对应于中点 U(中椭圆,图中显示为 E34(用虚线绘制的椭圆))的s 值。所有椭圆区域都如此。当椭圆区域的数量较少时,在结果图像中可以看到离散的步长。然而,当椭圆区域的数量增加时,会感知到平滑的混合效果。因此,屏幕上的“平滑度”滑块用于改变混合中的步数。

上述论文谈到了多分辨率,但鉴于我没有使用它就获得了不错的结果,我决定只使用“单分辨率”。

7. 关于其他形状——圆形、菱形、矩形和正方形

一旦完全理解了椭圆晕影,就很容易理解其他晕影形状了:

  • 圆形:圆形是椭圆的一种特殊情况,其长轴和短轴值相同。
  • 菱形:菱形是直线围成的图形,长轴和短轴的概念不相关。考虑到直线的“两截距”形式,菱形的方程为(其中ab 等同于长轴和短轴):
  • eqnDiamond.png

    可以使用与椭圆类似的方法来确定一个点是否在菱形内。

  • 矩形:处理矩形形状非常直接。.NET 提供了一种方法来确定一个点是否在矩形内或外,这使得它变得容易得多。
  • 正方形:正方形是矩形的一种特殊情况,其长度和宽度尺寸相同。

8. 关于代码

晕影代码组织在一个名为 `VignetteEffect` 的类中,该类具有以下重要方法:

  1. `SetupParameters`:为了提高性能,我首先计算重要参数,然后将这些参数应用于晕影。此方法首先计算不同椭圆的长轴和短轴(或菱形和矩形的等效尺寸);此外,它还计算混合区使用的权重。权重计算计算量很大(因为它涉及三角函数的计算),最好在设置时完成。
  2. `ApplyEffectCircleEllipseDiamond`:可以将圆形、椭圆形或菱形的计算合并到一个函数中,因为公式只有微小差异。此方法遍历目标图像的所有像素。首先,确定一个像素所属的区域(区域 A、区域 B 或区域 C)。其次,计算目标像素值。对于区域 A 中的像素,源像素只是复制到目标图像。对于区域 C 中的像素,边框颜色决定目标图像像素值。对于区域 B 的像素,使用混合公式确定目标像素值。以下是相关代码片段:
  3. void ApplyEffectCircleEllipseDiamond()
    {
      int k, el, w1, w2;
      byte r, g, b;
      double wb2 = width * 0.5 + Xcentre * width * geometryFactor;
      double hb2 = height * 0.5 + Ycentre * height * geometryFactor;
      double thetaRadians = Angle * Math.PI / 180.0;
      double cos = Math.Cos(thetaRadians);
      double sin = Math.Sin(thetaRadians);
      double xprime, yprime, potential1, potential2, potential;
      double factor1, factor2, factor3, factor4;
      byte redBorder = BorderColour.R;
      byte greenBorder = BorderColour.G;
      byte blueBorder = BorderColour.B;
    
      // Loop over the number of pixels
      for (el = 0; el < height; ++el)
      {
          w2 = width * el;
          for (k = 0; k < width; ++k)
          {
              // This is the usual rotation formula, along with translation.
              // xprime and yprime are the same as x2 and y2
              // in the above figure, respectively.
    
              xprime = (k - wb2) * cos + (el - hb2) * sin;
              yprime = -(k - wb2) * sin + (el - hb2) * cos;
    
              factor1 = 1.0 * Math.Abs(xprime) / aVals[0];
              factor2 = 1.0 * Math.Abs(yprime) / bVals[0];
              factor3 = 1.0 * Math.Abs(xprime) / aVals[NumberSteps];
              factor4 = 1.0 * Math.Abs(yprime) / bVals[NumberSteps];
    
              if (Shape == VignetteShape.Circle || Shape == VignetteShape.Ellipse)
              {
                  // Equations for the circle / ellipse. 
                  potential1 = factor1 * factor1 + factor2 * factor2 - 1.0;
                  potential2 = factor3 * factor3 + factor4 * factor4 - 1.0;
              }
              else //if (Shape == VignetteShape.Diamond)
              {
                  // Equations for the diamond. 
                  potential1 = factor1 + factor2 - 1.0;
                  potential2 = factor3 + factor4 - 1.0;
              }
              w1 = w2 + k;
    
              if (potential1 <= 0.0)
              {
                  // Point is within the inner circle / ellipse / diamond
                  r = pixRedOrig[w1];
                  g = pixGreenOrig[w1];
                  b = pixBlueOrig[w1];
              }
              else if (potential2 >= 0.0)
              {
                  // Point is outside the outer circle / ellipse / diamond
                  r = redBorder;
                  g = greenBorder;
                  b = blueBorder;
              }
              else
              {
                  // Point is in between the outermost
                  // and innermost circles / ellipses / diamonds
                  int j, j1;
    
                  for (j = 1; j < NumberSteps; ++j)
                  {
                      factor1 = Math.Abs(xprime) / aVals[j];
                      factor2 = Math.Abs(yprime) / bVals[j];
    
                      if (Shape == VignetteShape.Circle ||
                          Shape == VignetteShape.Ellipse)
                      {
                          potential = factor1 * factor1 + factor2 * factor2 - 1.0;
                      }
                      else // if (Shape == VignetteShape.Diamond)
                      {
                          potential = factor1 + factor2 - 1.0;
                      }
                      if (potential < 0.0) break;
                  }
                  j1 = j - 1;
                  // The formulas where the weights are applied to the image, and border.
                  r = (byte)(pixRedOrig[w1] * weight1[j1] + redBorder * weight2[j1]);
                  g = (byte)(pixGreenOrig[w1] * weight1[j1] + greenBorder * weight2[j1]);
                  b = (byte)(pixBlueOrig[w1] * weight1[j1] + blueBorder * weight2[j1]);
              }
              pixRedModified[w1] = r;
              pixGreenModified[w1] = g;
              pixBlueModified[w1] = b;
          }
      }
  4. `ApplyEffectRectangleSquare`:这与上述方法类似,但专门针对矩形和正方形。.NET 的 `Rect` 结构有一个名为 `Contains(Point pt)` 的方法,如果一个点在矩形内,则返回 `true`,否则返回 `false`。这用于识别区域,从而计算目标像素值。

主窗口包含所有 UI 元素。代码隐藏类执行图像读取、填充像素列表、将这些像素传输到 `VignetteEffect` 对象、应用效果以及根据它返回的修改后的像素形成图像的功能。所有这些都包含在一个 Visual Studio 2008 解决方案中。

9. 晕影图库

由于用户有多种修改晕影参数的方式,因此此应用程序可能存在无限多的晕影模式。在此,我展示了一组使用此程序创建的一些晕影。源图像是 Windows 7 自带的郁金香图像。

gallery.png

10. 关注点

性能:我在这里关注的是功能而非性能。但是,为了提供响应式的 UI,我已将原始图像缩放到 600 x 600 的区域,并考虑了纵横比。所有用户操作都在此缩放图像上进行。这是提高性能的一种方法。但是,这似乎还不够。您可能会注意到一些滑块移动不便,这会引起烦恼。在我思考提高性能的方法时,请发送您的建议。(多线程是一个可以尝试的选项,但这要到稍后的版本。)

WPF 还是 Windows Forms:当我向朋友展示这个应用程序时,他说它看起来更像一个 Windows Forms 应用程序。也许是吧。同样,我没有使用 MVVM 等任何模式。另外值得注意的是,我**没有**使用路径和几何图形等 WPF 功能来创建图像上的蒙版;我也没有使用图像笔刷。但是,我怀疑是否可以使用这些来达到通过暴力像素操作可实现的混合区效果。本质上,我可能没有充分利用 WPF 的潜力。在这里也请发送您的建议。

抗锯齿:我没有尝试在算法中实现任何抗锯齿(双线性插值或亚像素计算)。我认为在没有抗锯齿的情况下结果是可以接受的。

11. 结束语

在本文中,展示了一种创建晕影的方法。也许我将晕影的概念发挥到了极致,为用户提供了修改其属性的所有可能途径。我希望您喜欢这个应用程序,尽管它的速度有点慢,并使用它来创建您想要的带有晕影的图像。如果您觉得需要添加更多功能,请回复。即使没有,您的反馈也深表感谢。

历史

  • 版本 1.0:2011 年 4 月 17 日。
© . All rights reserved.