我是Haskell的新手,并试图通过思考图像处理来学习它。
到目前为止,我一直在思考如何在Haskell(或任何函数式编程语言)中实现邻域过滤算法。
如何在功能上写入空间平均滤波器(例如3x3内核,5x5图像)?来自完全强制性的背景,我似乎无法想出一种方法来构建数据,以便解决方案优雅,或者不通过迭代图像矩阵来实现,这看起来并不是很好声明性的。
答案 0 :(得分:3)
使用功能语言可以轻松地与社区合作。像内核卷积这样的操作是高阶函数,可以用函数式编程语言的常用工具之一 - 列表来编写。
要编写一些真实有用的代码,我们首先会假装解释一个库。
您可以将每个图像视为一个函数,从图像中的坐标到该坐标处保存的数据的值。这将在所有可能的坐标上定义,因此将它与一些bounds
配对会很有用,它会告诉我们函数的定义位置。这会建议像
data Image coordinate value = Image {
lowerBound :: coordinate,
upperBound :: coordinate,
value :: coordinate -> value
}
Haskell的数据类型非常相似,名为Array
in Data.Array
。此数据类型附带了value
中Image
函数不具备的附加功能 - 它会记住每个坐标的值,以便永远不需要重新计算。我们将使用三个函数与Array
一起使用,我将根据如何为上面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
等。例如range
(1, 5)
是[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
来定义Array
,该range
从项目列表中构建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
基础上有一个“结合的东西”的候选类型类,但没有+
或*
或其他数字的实例,因为有多种合理的方法可以将它们组合在一起:Offset
和.+.
。为了解决这个问题,我们将为与名为Offset
的新运算符组合的内容创建自己的类型类Ix
。通常我们不会制作类型,除了有法律的东西。我们只是说class Offset a where
(.+.) :: a -> a -> a
应该与Integer
合理地“合作”。
9
instance Offset Integer where
(.+.) = (+)
s,Haskell在编写像Offset
这样的整数文字时使用的默认类型,可以用作偏移量。
instance (Offset a, Offset b) => Offset (a, b) where
(x1, y1) .+. (x2, y2) = (x1 .+. x2, y1 .+. y2)
此外,0
可以成对组合的事物的对或元组。
pad background
在我们编写卷积之前,我们还有一个皱纹 - 我们将如何处理图像的边缘?为简单起见,我打算用!
填充它们。 bounds
生成的Array
版本定义在任何地方,在background
的{{1}}之外,它会返回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
要b
带有内核a
的图片bounds
,我们会在与b
相同的f
上定义新图片。图像中的每个点都可以通过函数sum
来计算,*
是内核a
中值的乘积(pad
)和b
中的值。对于内核o
range
的{{1}}中的每个偏移bounds
,{1}} ded image a
。
通过上一节中的六个声明,我们可以编写您请求的示例,一个空间平均滤波器,其中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
print
输入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)]
convolve
d输出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,而不是为自己实现图像处理工具包。