使用函数式语言可以轻松优雅地与社区合作。像内核卷积这样的操作是高阶函数,可以用函数式编程语言的常用工具之一 - 列表来编写。
要编写一些真正有用的代码,我们将首先假装解释一个库。
假装
您可以将每个图像视为一个函数,从图像中的坐标到保存在该坐标处的数据的值。这将在所有可能的坐标上定义,因此将它与一些 bounds 配对会很有用,它告诉我们函数的定义位置。这将建议像
这样的数据类型
data Image coordinate value = Image {
lowerBound :: coordinate,
upperBound :: coordinate,
value :: coordinate -> value
}
Haskell 有一个非常相似的数据类型,称为Array in Data.Array。这种数据类型带有Image 中的value 函数所没有的附加功能——它会记住每个坐标的值,因此永远不需要重新计算。我们将使用三个函数来处理Arrays,我将根据上面的Image 定义它们来描述它们。这将帮助我们看到,即使我们使用了非常有用的Array 类型,也可以根据函数和代数数据类型来编写所有内容。
type Array i e = Image i e
bounds 获取Array 的边界
bounds :: Array i e -> (i, i)
bounds img = (lowerBound img, upperBound img)
! 在Array 中查找一个值
(!) :: Array i e -> i -> e
img ! coordinate = value img coordinate
最后,makeArray 构建了一个 Array
makeArray :: Ix i => (i, i) -> (i -> e) -> Array i e
makeArray (lower, upper) f = Image lower upper f
Ix 是一个类型类,用于表现类似于图像坐标的事物,它们有一个range。大多数基本类型都有实例,例如Int、Integer、Bool、Char 等。例如,(1, 5) 的range 是[1, 2, 3, 4, 5]。还有一个产品或事物元组的实例,它们本身具有Ix 实例;元组的实例涵盖每个组件范围的所有组合。例如,range (('a',1),('c',2)) 是
[('a',1),('a',2),
('b',1),('b',2),
('c',1),('c',2)]`
我们只对Ix 类型类中的两个函数range :: Ix a => (a, a) -> [a] 和inRange :: Ix a => a -> (a, a) -> Bool 感兴趣。 inRange 快速检查值是否会出现在 range 的结果中。
现实
实际上,makeArray 不是由Data.Array 提供的,但我们可以根据listArray 来定义它,它从与range 的range 相同顺序的项目列表中构造一个Array它的bounds
import Data.Array
makeArray :: (Ix i) => (i, i) -> (i -> e) -> Array i e
makeArray bounds f = listArray bounds . map f . range $ bounds
当我们convolve 一个带有内核的数组时,我们将通过将内核中的坐标与我们正在计算的坐标相加来计算邻域。 Ix 类型类不要求我们可以将两个索引组合在一起。基础中有一个用于“组合的事物”的候选类型类Monoid,但没有Int 或Integer 或其他数字的实例,因为组合它们的合理方法不止一种:+ 和@ 987654372@。为了解决这个问题,我们将为与名为.+. 的新运算符组合的事物创建自己的类型类Offset。通常我们不会创建类型类,除非是有规律的东西。我们只会说Offset 应该与Ix 一起“合理地工作”。
class Offset a where
(.+.) :: a -> a -> a
Integers,当你编写像9 这样的整数文字时,Haskell 使用的默认类型可以用作偏移量。
instance Offset Integer where
(.+.) = (+)
此外,Offset 可以成对组合成对或元组。
instance (Offset a, Offset b) => Offset (a, b) where
(x1, y1) .+. (x2, y2) = (x1 .+. x2, y1 .+. y2)
在我们写 convolve 之前我们还有一个问题——我们将如何处理图像的边缘?为了简单起见,我打算用0 填充它们。 pad background 创建了一个在任何地方都定义的 ! 版本,在 Array 的 bounds 之外它返回 background。
pad :: Ix i => e -> Array i e -> i -> e
pad background array i =
if inRange (bounds array) i
then array ! i
else background
我们现在准备为convolve 编写一个高阶函数。 convolve a b 将图像b 与内核a 进行卷积。 convolve 更高阶,因为它的每个参数及其结果都是一个Array,它实际上是一个函数! 和它的bounds 的组合。
convolve :: (Num n, Ix i, Offset i) => Array i n -> Array i n -> Array i n
convolve a b = makeArray (bounds b) f
where
f i = sum . map (g i) . range . bounds $ a
g i o = a ! o * pad 0 b (i .+. o)
对于convolve 一个带有内核a 的图像b,我们在与b 相同的bounds 上定义一个新图像。图像中的每个点都可以通过函数f 计算,其中sums 是内核a 中的值与padded 图像b 中的值的乘积(*)内核a 的bounds 的range 中的每个偏移量o。
示例
使用上一节中的六个声明,我们可以编写您要求的示例,将 3x3 内核的空间平均滤波器应用于 5x5 图像。下面定义的内核a 是一个 3x3 图像,它使用来自 9 个采样邻居中每一个的值的九分之一。 5x5 图片b 是一个渐变,从左上角的2 到右下角的10。
main = do
let
a = makeArray ((-1, -1), (1, 1)) (const (1.0/9))
b = makeArray ((1,1),(5,5)) (\(x,y) -> fromInteger (x + y))
c = convolve a b
print b
print c
printed 输入 b 是
array ((1,1),(5,5))
[((1,1),2.0),((1,2),3.0),((1,3),4.0),((1,4),5.0),((1,5),6.0)
,((2,1),3.0),((2,2),4.0),((2,3),5.0),((2,4),6.0),((2,5),7.0)
,((3,1),4.0),((3,2),5.0),((3,3),6.0),((3,4),7.0),((3,5),8.0)
,((4,1),5.0),((4,2),6.0),((4,3),7.0),((4,4),8.0),((4,5),9.0)
,((5,1),6.0),((5,2),7.0),((5,3),8.0),((5,4),9.0),((5,5),10.0)]
convolved 输出 c 是
array ((1,1),(5,5))
[((1,1),1.3333333333333333),((1,2),2.333333333333333),((1,3),2.9999999999999996),((1,4),3.6666666666666665),((1,5),2.6666666666666665)
,((2,1),2.333333333333333),((2,2),3.9999999999999996),((2,3),5.0),((2,4),6.0),((2,5),4.333333333333333)
,((3,1),2.9999999999999996),((3,2),5.0),((3,3),6.0),((3,4),7.0),((3,5),5.0)
,((4,1),3.6666666666666665),((4,2),6.0),((4,3),7.0),((4,4),8.0),((4,5),5.666666666666666)
,((5,1),2.6666666666666665),((5,2),4.333333333333333),((5,3),5.0),((5,4),5.666666666666666),((5,5),4.0)]
根据您想做的事情的复杂程度,您可能会考虑使用更成熟的库,例如经常推荐的 repa,而不是自己实现图像处理工具包。