我正在尝试使用 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))
看起来像高斯模糊矩阵中1/256
系数的蓝色区域有点少,但如何完全摆脱它们?
FIX:我猜是因为 Pixel8
类型 0-255 和 Int,但我只是通过将内核中心的 36
更改为 35
来修复整个问题,因此矩阵的总和系数是(我相信)255/255 = 1。
答案 0 :(得分:3)
你写
| p1 + p2 > 255 = 255
| p1 + p2 < 0 = 0
防止溢出,但这些条件永远不会成立。 Word8
值始终介于 0
和 255
之间。检查溢出的正确方法如下所示:
| 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)
复杂度,其中 m
和 n
是图像维度,k
是内核端,实际实现的复杂度是 O(m*n*k^4)
。这放弃了列表的其他性能限制,例如盒装元素和缓存不友好性。255
。最好在提供图像模糊功能之前完全删除它,或者使用原始图像的透明度值。1x5
核应用到行然后 5x1 应用到列会更快。这种方法将复杂性降低到 O(m*n*k)
Word8
和 Double
会导致过滤器的质量比实际情况差得多。换句话说,由于过滤器中的错误值,您会得到一个错误,然后在求和过程中该错误会进一步放大,因为舍入应该在对浮点值求和之后而不是之前发生。JuicyPixels
是一个很棒的编码/解码图像的包,但它绝对不适合更复杂的图像处理任务。您需要的是一个实际的图像处理库或一个已经实现了卷积算法的数组操作库。我有一个库 HIP
是专门为这类东西设计的,但它正在经历一次重大的重写,所以我将提供一个数组库 massiv
和 massiv-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
标志进行编译以获得最佳性能。