使用卷积矩阵模糊图像 - 工件(Haskell)

时间:2021-03-30 13:27:18

标签: haskell image-processing pixel convolution blur

我正在尝试使用 Haskell(使用 JuicyPixels)进行一些图像处理。我已经完成了将高斯模糊应用于图像的这个功能,并且在处理后的图像中有一些奇怪的伪影:

blur :: Image PixelRGBA8 -> Image PixelRGBA8 
blur img@Image {..} = generateImage blurrer imageWidth imageHeight
       where blurrer x y | x >= (imageWidth - offset) || x < offset
                          || y >= (imageHeight - offset) || y < offset = whitePx
                         | otherwise = do
                let applyKernel i j p | j >= matrixLength = applyKernel (i + 1) 0 p
                                      | i >= matrixLength = p 
                                      | otherwise = do 
                                         let outPixel = pxMultNum 
                                                         (pixelAt img (x + j - offset) (y + i - offset)) 
                                                         (gblurMatrix !! i !! j)
                                         applyKernel i (j+1) (outPixel `pxPlus` p)
                applyKernel 0 0 zeroPx
             gblurMatrix = [[1  / 255, 4  / 255,  6 / 255,  4 / 255, 1 / 255],
                            [4  / 255, 16 / 255, 24 / 255, 16 / 255, 4 / 255],
                            [6  / 255, 24 / 255, 36 / 255, 24 / 255, 6 / 255],
                            [4  / 255, 16 / 255, 24 / 255, 16 / 255, 4 / 255],
                            [1  / 255, 4  / 255,  6 / 255,  4 / 255, 1 / 255]]
             matrixLength = length gblurMatrix
             offset = matrixLength `div` 2

whitePx = PixelRGBA8 255 255 255 255
zeroPx = PixelRGBA8 0 0 0 255

pxPlus :: PixelRGBA8 -> PixelRGBA8 -> PixelRGBA8 
pxPlus (PixelRGBA8 r1 g1 b1 a1) (PixelRGBA8 r2 g2 b2 a2) = PixelRGBA8 (s r1 r2) (s g1 g2) (s b1 b2) 255
  where s p1 p2 | p1 + p2 > 255 = 255
                | p1 + p2 < 0 = 0
                | otherwise = p1 + p2

pxMultNum :: PixelRGBA8 -> Double -> PixelRGBA8
pxMultNum (PixelRGBA8 r g b a) q = PixelRGBA8 (m r) (m g) (m b) (m a)
  where m px = pxify $ fromIntegral px * q -- pxify is just synonym to function rounding a = (floor (a + 0.5))

这里是输入图像
input image
和输出图像
output image

看起来像高斯模糊矩阵中1/256系数的蓝色区域有点少,但如何完全摆脱它们?

FIX:我猜是因为 Pixel8 类型 0-255 和 Int,但我只是通过将内核中心的 36 更改为 35 来修复整个问题,因此矩阵的总和系数是(我相信)255/255 = 1。

2 个答案:

答案 0 :(得分:3)

你写

| p1 + p2 > 255 = 255
| p1 + p2 < 0 = 0

防止溢出,但这些条件永远不会成立。 Word8始终介于 0255 之间。检查溢出的正确方法如下所示:

| p1 + p2 < p1 = 255

但最好首先避免溢出。我注意到您的模糊矩阵的系数加起来为 256/255。也许你可以先解决这个问题。

答案 1 :(得分:3)

很高兴您在 Haskell 中学习图像处理,但是您的实现中存在太多问题,我不希望其他人从中学习。我将首先解决这些问题,然后为在 Haskell 中进行高斯模糊提供更好的解决方案。

@Daniel Wagner 在另一个答案中已经确定的整数溢出问题绝对是问题所在。但是我会通过更改 pxPlus 的类型来解决它:

pxPlus :: PixelRGBF -> PixelRGBF -> PixelRGBF

但这不是重点。

原始实现中更大的问题是性能,它简直太糟糕了。只是为了给您一个想法,问题中的实现在我的笔记本电脑上需要大约 16 秒,而我在底部提供的解决方案需要 0.007 秒

这里只是一些主要原因:

  • !! 是一个列表的 O(n) 操作,因此使得对内核的索引非常缓慢。并且因为卷积已经是 O(m*n*k^2) 复杂度,其中 mn 是图像维度,k 是内核端,实际实现的复杂度是 O(m*n*k^4)。这放弃了列表的其他性能限制,例如盒装元素和缓存不友好性。
  • blur 函数应用于具有 alpha 通道的图像,它理所当然地被忽略,但由于某种原因它只是被丢弃并设置为 255。最好在提供图像模糊功能之前完全删除它,或者使用原始图像的透明度值。
  • 高斯卷积是可分离的,因此与应用 5x5 核相比,将 1x5 核应用到行然后 5x1 应用到列会更快。这种方法将复杂性降低到 O(m*n*k)
  • 借助各种优化,例如具有不安全内部索引的快速边界分辨率、并行化等,通常可以更快地实现卷积。所有这些都很难做到正确,这就是为什么最好使用具有以下特性的库的原因提供此类功能。
  • 所使用的高斯模糊内核值是为 8 位通道设计的,但您将它们用作浮点值。过去 8bit 的目的是让它在旧硬件上尽可能快地运行,因为浮点数很慢。这就是为什么所有在线教程都使用如此糟糕的内核。像在问题中那样错误地混合使用 Word8Double 会导致过滤器的质量比实际情况差得多。换句话说,由于过滤器中的错误值,您会得到一个错误,然后在求和过程中该错误会进一步放大,因为舍入应该在对浮点值求和之后而不是之前发生。
  • 另一个非常常见的错误是 the filter is applied to the non-linear sRGB color space,这很可能是源图像编码的颜色空间。It requires an inverse gamma correction first
  • 用零填充会产生具有此白框的图像,当然处理稍大的图像会增加一些开销。

JuicyPixels 是一个很棒的编码/解码图像的包,但它绝对不适合更复杂的图像处理任务。您需要的是一个实际的图像处理库或一个已经实现了卷积算法的数组操作库。我有一个库 HIP 是专门为这类东西设计的,但它正在经历一次重大的重写,所以我将提供一个数组库 massivmassiv-io 的答案,实际上使用下面的 JuicyPixels 读取/写入图像。

这是进行高斯模糊的快速而准确的方法:

import Data.Massiv.Array as A
import Data.Massiv.Array.Unsafe (makeUnsafeConvolutionStencil)
import Data.Massiv.Array.IO as A

blurImageF :: (ColorModel cs Float) => Image S cs Float -> Image S cs Float
blurImageF = computeAs S . applyStencil padding blurStencil5x1f
           . computeAs S . applyStencil padding blurStencil1x5f
  where
    padding = noPadding -- decides what happens at the border
{-# INLINE blurImageF #-}

blurStencil1x5f :: (Floating e, ColorModel cs e) => Stencil Ix2 (Pixel cs e) (Pixel cs e)
blurStencil1x5f = makeUnsafeConvolutionStencil (Sz2 1 5) (0 :. 2) stencil
  where
    stencil f = f (0 :. -2) 0.03467403390152031 .
                f (0 :. -1) 0.23896796340399287 .
                f (0 :.  0) 0.45271600538897480 .
                f (0 :.  1) 0.23896796340399287 .
                f (0 :.  2) 0.03467403390152031
    {-# INLINE stencil #-}
{-# INLINE blurStencil1x5f #-}

blurStencil5x1f :: (Floating e, ColorModel cs e) => Stencil Ix2 (Pixel cs e) (Pixel cs e)
blurStencil5x1f = makeUnsafeConvolutionStencil (Sz2 5 1) (2 :. 0) stencil
  where
    stencil f = f (-2 :. 0) 0.03467403390152031 .
                f (-1 :. 0) 0.23896796340399287 .
                f ( 0 :. 0) 0.45271600538897480 .
                f ( 1 :. 0) 0.23896796340399287 .
                f ( 2 :. 0) 0.03467403390152031
    {-# INLINE stencil #-}
{-# INLINE blurStencil5x1f #-}

内核中的值是使用非常准确的积分近似值和 σ=5/6 内核大小的最佳标准偏差计算的

为了看到实际效果,我们首先读取图像,该图像会自动转换为具有 Float 精度的 linear sRGB 色彩空间(仅供参考,问题中提供的原始图像是在 Y'CbCr 颜色空间,而不是 sRGB 与 alpha 通道)然后我们将模糊应用于它并与裁剪的原始连接在一起(应用没有填充的卷积减小了尺寸)。之后我们将构建的图像转换为 Y'CbCr 颜色空间并将其写入 JPEG 文件:

λ> img <- readImageAuto "4ZYKa.jpg" :: IO (Image S (SRGB 'Linear) Float)
λ> let imgBlurred = blurImageF img
λ> displayImage imgBlurred -- this will open the blurred image with default viewer
λ> imgCropped <- extractM (2 :. 2) (size imgBlurred) img
λ> imgBoth <- appendM 1 imgCropped imgBlurred
λ> let out = convertPixel <$> imgBoth :: Image DL (Y'CbCr SRGB) Word8
λ> writeImage "out.jpg" out

请注意,这在 GHCi 中会慢很多。它需要使用 -O2 -threaded -with-rtsopts=-N 标志进行编译以获得最佳性能。

enter image description here

相关问题