14.2.6 实现模糊效果
我们最终的效果不只是一个颜色滤镜。模糊图像的过程依赖于计算新的像素值,基于多个原始像素。我们仍然可以执行逐个像素转换图像。不过,转型需要访问整个图像,以及我们想要转换的像素坐标。
我们把 RunEffect 和 RunEffectParallel 的实现留下作为练习,但它是相当简单的;这是只需改变循环的具体内容,给转换函数更多的信息。从序列到并行形式的转换,与这个至于效果颜色滤镜是相同的。如果你被卡住,去本书的网站上看看完整的源代码。
模糊转型本身是很有趣,如清单 14.19 中所示。它并不特别困难,它提供了一个很好的声明性编程的示范。
Listing 14.19 Implementing the Blur effect (F#)
let blur(arr:SimpleColor[,], x, y) =
let height, width = arr.GetLength(0) - 1, arr.GetLength(1) – 1
let checkW x = max 0 (min width x)
let checkH y = max 0 (min height y)
[ for dy in -2 .. 2 do
for dx in -2 .. 2 do
yield arr.[checkH(y + dy), checkW(x + dx)] ]
|> List.average
blur 函数取三个参数,指定的图像,我们想要计算像素的 X,Y 坐标。如果以命令式风格实现 blur,就需要创建一个变量,将其初始化为 0,把附近所有的像素的颜色加起来,再把结果除以像素数,以获得平的均颜色。
在 F# 中,我们可以使用更加声明式的方法,来写平均颜色的计算。我们首先声明一些工具函数,用来检查该索引在数组的范围中。接下来,要使用序列表达式,来创建一个列表,包含所有邻近像素的颜色, 用 List.average 计算平均值。
探索 List.average
要计算值的平均值,List.average 函数需要知道三件事:
■ 对于特定的类型,"零"值是什么。
■ 如何将值相加。
■ 如何将特定类型的值除以整数。
前两项对于计算这个列表的和是足够的。然后,需要把结果除以列表中的元素的个数,就是这里的第三项。在我们的效果中,我们将处理 SimpleColor 类型的值,这个类型实现了加法运算符。我们还添加了特殊成员,Zero 和 DivideByInt。average 函数使用这些成员,它是一个泛型函数,但它需要这个类型实现适当的成员。像这样的约束并不能用 .NET 的泛型表示。可用于泛型类型的约束,可能要求无参数构造函数或接口,但不是特定的成员。
为此,F# 实现了自己的编译时成员约束的机制。在这种情况下,该约束由 F# 编译器在编译时解决(相对于 .NET 泛型的约束,也是由 .NET 的运行时检查)。下面的示例演示了如何使用此功能,来定义一个函数,计算任何值的一半,这个值支持除以整数:
> let inline half (num: ^a) : ^a =
LanguagePrimitives.DivideByInt< (^a) > num 2
;;
val inline half : ^a -> ^a
when ^a : (static member DivideByInt : ^a * int -> ^a)
> half(42.0);;
val it : float = 21.0
> half(SimpleColor(255, 128, 0));;
val it : SimpleColor = SimpleColor {B = 0; G = 64; R = 127;}
> half("hello");;
error FS0001: The type 'string' does not
support any operators named 'DivideByInt'
这个函数叫 half,我们使用显式类型注释,来指定参数 num 的类型,结果的类型是泛型参数 ^a。注意,我们将使用的类型参数,以(^) 开头,而不是使用惯常的撇号(')。在函数体中,我们调用来自 LanguagePrimitives 模块的 DivideByInt 函数。这是一个有成员约束的基元函数,它把数值类型除以整数,或者,使用 DivideByInt 成员,如果可用的话。
正如你所看到的,成员约束表现为推导出的类型签名。现在,我们有了这个函数,可以将它用于各种数值类型,而且,也可以处理我们的 SimpleColor 类型。如果我们尝试调用它,用不支持 DivideByInt 操作的参数值,就会发生编译时错误。
在这个示例中,我们已经研究了一个大型应用程序的关键部分。可以从本书的网站上获得完整的源代码,看到我们这一章中实现的部分如何相连。虽然这个应用程序的最重要的部分使用了可变数组,但是,设计的整个应用程序还是函数式的,包括第 11 章中所述的以函数式风格使用数组。这种方法让我们轻松和安全地并行化核心算法。
这是一个以行为为中心的应用程序的示例。我们主要关心的是如何并行化单独的行为。并行化以行为为中心的应用程序的另一种方式,是以并行的方式运行不同的行为。我们可能想要批处理一系列的图像,并每个图像应用多个效果。在下一节中,我们会把注意力转向以数据为中心的应用程序。