14.2.2 实现和运行颜色滤镜

来源:互联网 发布:极限挑战网络更新时间 编辑:程序博客网 时间:2024/06/07 05:42

14.2.2 实现和运行颜色滤镜

 

    首先,我们将讨论一种特殊效果的类型:颜色滤镜。稍后,我们将扩展这个应用程序,处理任何效果,实现模糊的示例。彩色滤镜只更改图像的色调,所以很简单。这个滤镜为每个像素计算新的颜色,无需访问图像的其他部分。正如我们在第 8 章中看到的,这是一种行为,自然表示为一个函数。

    调整颜色的滤镜可以表示为函数,取原来的颜色,返回一个新的颜色。F# 的类型签名是 SimpleColor-> SimpleColor。在 C# 中,我们可以使用 Func 委托,表示相同的事。运行这个滤镜的代码,将把这个函数应用到图像的每个像素。当我们处理位图时,把它表示为一个二维数组。

 

将位图转换为数组

 

    图像的 .NET 表示形式是用来自 System.Drawing 命名空间下的 Bitmap 类,这个类可以使用 GetPixel 和 SetPixel 访问像素,但是,这两个方法效率低下,当需要访问大量的像素时。当你每次想读一个字节的数据,相当于重新打开图形文件。这就是为什么我们要用一个二维数组来表示位图了。

    我们不仅需要将位图转换为数组,还要再转换回来。这可以使用 LockBits 方法高效地进行,它为我们提供了一个非托管内存中的位置,可以直接访问。读写内存可以使用 .NET 的 Marshal 类完成。在我们的应用程序中,需要两个函数实现转换。这些函数在 BitmapUtils 模块中实现,分别是 ToArray2D 和 ToBitmap。而其有趣在于它们自身,与并行化的主题不直接有关。可以在本书的网站上的在线源代码找到完整实现。

 

    滤镜的实现在 C# 和 F# 中是类似的,但是,顺序执行的滤镜的代码将会不同。F# 库包括处理二维数组的高阶函数,但是,.NET 中没有,因此,我们就需要首先实现这些。我们不会使代码像 F# 的 Array2D 模块中的函数一样完全通用的。让我们首先实现 C# 中的几个滤镜。

 

在 C# 中创建并应用颜色滤镜

 

    虽然我们将使用 Func 委托来表示颜色滤镜,仍把它们实现为普通的方法,当我们需要时,可以将它转换为委托,比如,能将它们保存在滤镜的集合中。清单 14.10 显示了两个简单的颜色滤镜。第一个将颜色转换为灰度,第二个使图像变亮。

 

Listing 14.10 Grayscale and lighten filters (C#)

 

class Filters {
  public static SimpleColor Grayscale(SimpleColor clr) {
    var c = (clr.R*11 + clr.G*59 + clr.B*30) / 100;
    return new SimpleColor(c, c, c);
  }

  public static SimpleColor Lighten(SimpleColor clr) {
    return (clr * 2).ClipColor();
  }
}

 

    若要计算灰度颜色,我们使用加权平均,因为人眼对绿色光比红色或蓝色更敏感。第二个滤镜的实现是更简单,但中,这一次它使用 SimpleColor 类型的运算符的重载。它使用组件按位(component-wise)乘法,把颜色乘以 2。这样,组件创建的颜色可能会超出正常范围 0 至 255,所以,我们使用 ClipColor 方法,适当地限制每个组件。

    现在,我们的滤镜有了方法,把它们应用到表示图像的二维数组。清单 14.11 通过在数组类型上,实现扩展方法做到的。目前,我们仍将在单个线程中,执行所有计算。

 

Listing 14.11 Sequential method for applying filters (C#)

 

public static SimpleColor[,] RunFilter
    (this SimpleColor[,] arr, Func<SimpleColor, SimpleColor> f) {
  int height = arr.GetLength(0), width = arr.GetLength(1);
  var result = new SimpleColor[width, height];
  for(int y = 0; y < height; y++)
    for(int x = 0; x < width; x++)
      result[y, x] = f(arr[y, x]);
  return result;
}

 

    RunFilter 方法首先创建一个新的数组,将返回作为结果。我们将以函数的方式写应用程序,所以,不会修改作为输入提供的数组。在方法体中,我们以命令方式,循环访问数组中的所有像素,并把这个颜色滤镜函数,应用到每个像素。注意,我们指定 Y 作为数组的第一个坐标。这可以使图像上的一些操作效率更高,因为,在这种设置中,一个水平扫描线就是一个内存块。

    鉴于我们以前对 Parallel.For 的体验,你可能已经可以看出,如何对这段代码并行化。在我们进行之前,先完成单线程的版本,看一下 F# 代码。

 

在 F# 中创建并应用颜色滤镜

 

    第十章,当我们想要把一个函数应用到数组的所有元素,并把结果收集一个新的数组中时,使用了 Array.map 函数。这正是我们清单 14.11 中的 RunFilter 方法所做的,除了它是处理二维数组之外。你可能不会惊奇,F# 库包含了一个用于处理二维数组的模块 Array2D,类似于一维数组的 Array 模块。这个模块也包含一个 map 函数,使得 F# 的  runFilter 实现很简单。你可以在清单 14.12 中看到它,连同两个颜色滤镜。

 

Listing 14.12 Applying filters and two simple filters (F# Interactive)

> let runFilter f arr = Array2D.map f arr

module ColorFilters =
  let Grayscale(clr:SimpleColor) =
    let c = (clr.R*11 + clr.G*59 + clr.B*30) / 100
    SimpleColor(c, c, c)
  let Lighten(clr:SimpleColor) =
    (clr * 2).ClipColor()
;;

 

val runFilter : ('a -> 'b) -> 'a [,] -> 'b [,]

module ColorFilters =
val Grayscale : SimpleColor -> SimpleColor
val Lighten : SimpleColor –> SimpleColor

 

    RunFilter 函数调用 Array2D.map 来完成这项工作。事实上,在后面的代码中,我们可能只使用 Array2D.map。把Array2D.map 打包到另一个函数,使代码更具可读性和自解释(self-explanatory)。此外,如果我们最终决定改变位图的表示,只需要更新 runFilter 函数,而不必接触使用它的代码。

    我们还使用 F# 模块来组织代码,使其更有结构化的风格。所有图形滤镜封装在 ColorFilters 模块中。这个清单显示了可以在 F# Interactive 中输入整个模块,以看到推断的类型签名。我们的两个示例滤镜的实现几乎与 C# 一样,后但在面,我们可以看到,F# 允许我们做更多一些的自定义类型,提供标准的重载运算符。

    在我们讨论如何并行化应用程序之前,需要把到目前为止所写的代码,打包到可以运行的应用程序中。这样,我们可以测试滤镜,测量其性能。我们将在下一节中这样做。我们只显示 C# 版本,而且没有更多的细节;完整的源代码在本书的网站上就有。我们将关注有趣的部分。

原创粉丝点击