【问题标题】:Image Neighbourhood Processing in HaskellHaskell 中的图像邻域处理
【发布时间】:2014-12-30 01:16:48
【问题描述】:

我是 Haskell 的新手,并试图通过图像处理方面的思考来学习它。

到目前为止,我一直在思考如何在 Haskell(或任何函数式编程语言,实际上)中实现邻域过滤算法。

如何在功能上编写空间平均滤波器(比如 3x3 内核、5x5 图像)?来自一个完全命令式的背景,我似乎无法想出一种方法来构造数据以使解决方案优雅,或者不通过迭代图像矩阵来做到这一点,这似乎不是很具有声明性。

【问题讨论】:

  • 我认为这是一个特定领域的问题。我认为,您可以通过包含一些其他语言的示例代码来吸引更多注意力。 repa 包用于并行数据处理,并有一个很好的教程,其中包括一些图像处理示例。另请查看repa-examples 包,特别是“模糊”示例。
  • 你可以看看 reparepa-devil 库是如何做到的。
  • 有一篇很棒的博文,介绍了comonads 如何为图像邻域运算提供一种直接的方法 (jaspervdj.be/posts/2014-11-27-comonads-image-processing.html)。

标签: image haskell image-processing functional-programming


【解决方案1】:

使用函数式语言可以轻松优雅地与社区合作。像内核卷积这样的操作是高阶函数,可以用函数式编程语言的常用工具之一 - 列表来编写。

要编写一些真正有用的代码,我们将首先假装解释一个库。

假装

您可以将每个图像视为一个函数,从图像中的坐标到保存在该坐标处的数据的值。这将在所有可能的坐标上定义,因此将它与一些 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。大多数基本类型都有实例,例如IntIntegerBoolChar 等。例如,(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 来定义它,它从与rangerange 相同顺序的项目列表中构造一个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,但没有IntInteger 或其他数字的实例,因为组合它们的合理方法不止一种:+ 和@ 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 创建了一个在任何地方都定义的 ! 版本,在 Arraybounds 之外它返回 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 中的值的乘积(*)内核aboundsrange 中的每个偏移量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,而不是自己实现图像处理工具包。

【讨论】:

    猜你喜欢
    • 2012-11-10
    • 2012-08-05
    • 2012-04-24
    • 2021-05-07
    • 1970-01-01
    • 2016-04-30
    • 2012-01-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多