|
2 | 2 | {-# LANGUAGE TypeApplications #-} |
3 | 3 | module ArrayFire.ImageSpec where |
4 | 4 |
|
5 | | -import Control.Exception |
6 | | -import Data.Complex |
7 | | -import Data.Word |
8 | | -import Foreign.C.Types |
9 | | -import GHC.Int |
10 | | -import Test.Hspec |
| 5 | +import qualified ArrayFire as A |
| 6 | +import Test.Hspec |
| 7 | +import Test.Hspec.ApproxExpect |
11 | 8 |
|
12 | | -import ArrayFire |
| 9 | +-- | A 4×4 single-channel constant image. |
| 10 | +gray :: A.Array Float |
| 11 | +gray = A.constant @Float [4,4] 1.0 |
| 12 | + |
| 13 | +-- | A 4×4×3 three-channel (RGB) constant image. |
| 14 | +rgb :: A.Array Float |
| 15 | +rgb = A.constant @Float [4,4,3] 1.0 |
13 | 16 |
|
14 | 17 | spec :: Spec |
15 | | -spec = |
16 | | - describe "Image tests" $ do |
17 | | - it "Should test if Image I/O is available" $ do |
18 | | - isImageIOAvailable `shouldReturn` True |
| 18 | +spec = describe "Image spec" $ do |
| 19 | + |
| 20 | + describe "isImageIOAvailable" $ |
| 21 | + it "reports whether FreeImage support was compiled in" $ |
| 22 | + -- value is build-dependent; we only assert the call succeeds & is Bool |
| 23 | + (A.isImageIOAvailable >>= (`shouldSatisfy` (`elem` [True, False]))) |
| 24 | + |
| 25 | + describe "gaussianKernel" $ do |
| 26 | + it "produces a kernel of the requested dimensions" $ |
| 27 | + A.getDims (A.gaussianKernel @Float 3 5 0 0) `shouldBe` (3,5,1,1) |
| 28 | + |
| 29 | + it "is normalized to sum ~1" $ |
| 30 | + sum (A.toList (A.gaussianKernel @Float 5 5 0 0)) `shouldBeApprox` (1.0 :: Float) |
| 31 | + |
| 32 | + it "has only non-negative weights" $ |
| 33 | + A.toList (A.gaussianKernel @Float 5 5 0 0) `shouldSatisfy` all (>= 0) |
| 34 | + |
| 35 | + describe "resize" $ do |
| 36 | + it "upsamples to the requested dimensions" $ |
| 37 | + A.getDims (A.resize gray 8 8 A.Nearest) `shouldBe` (8,8,1,1) |
| 38 | + |
| 39 | + it "downsamples to the requested dimensions" $ |
| 40 | + A.getDims (A.resize gray 2 2 A.Bilinear) `shouldBe` (2,2,1,1) |
| 41 | + |
| 42 | + it "preserves a constant image under bilinear resize" $ |
| 43 | + A.toList (A.resize gray 8 8 A.Bilinear) `shouldSatisfy` all (`approx` 1.0) |
| 44 | + |
| 45 | + describe "colorspace conversion" $ do |
| 46 | + it "rgb2gray collapses the channel dimension" $ |
| 47 | + A.getDims (A.rgb2gray rgb 0.3 0.59 0.11) `shouldBe` (4,4,1,1) |
| 48 | + |
| 49 | + it "rgb2gray of a constant image yields the weighted intensity" $ |
| 50 | + A.toList (A.rgb2gray rgb 0.3 0.59 0.11) `shouldSatisfy` all (`approx` 1.0) |
| 51 | + |
| 52 | + it "gray2rgb expands to three channels" $ |
| 53 | + A.getDims (A.gray2rgb gray 1 1 1) `shouldBe` (4,4,3,1) |
| 54 | + |
| 55 | + it "rgb2ycbcr / ycbcr2rgb preserve image dimensions" $ do |
| 56 | + let ycbcr = A.rgb2ycbcr rgb A.Ycc601 |
| 57 | + A.getDims ycbcr `shouldBe` (4,4,3,1) |
| 58 | + A.getDims (A.ycbcr2rgb ycbcr A.Ycc601) `shouldBe` (4,4,3,1) |
| 59 | + |
| 60 | + describe "morphology" $ do |
| 61 | + it "dilation with an all-ones mask leaves a constant image unchanged" $ do |
| 62 | + let mask = A.constant @Float [3,3] 1.0 |
| 63 | + A.toList (A.dilate gray mask) `shouldSatisfy` all (`approx` 1.0) |
| 64 | + |
| 65 | + it "erosion with an all-ones mask leaves a constant image unchanged" $ do |
| 66 | + let mask = A.constant @Float [3,3] 1.0 |
| 67 | + A.toList (A.erode gray mask) `shouldSatisfy` all (`approx` 1.0) |
| 68 | + |
| 69 | + describe "histogram" $ do |
| 70 | + it "has one element per requested bin" $ |
| 71 | + A.getElements (A.histogram gray 16 0 1) `shouldBe` 16 |
| 72 | + |
| 73 | + it "produces a u32 array" $ |
| 74 | + A.getType (A.histogram gray 16 0 1) `shouldBe` A.U32 |
| 75 | + |
| 76 | + it "accumulates every pixel across all bins" $ |
| 77 | + sum (map fromIntegral (A.toList (A.histogram gray 16 0 1))) |
| 78 | + `shouldBe` (16 :: Int) -- 4×4 pixels |
| 79 | + |
| 80 | + describe "gradient" $ |
| 81 | + it "of a constant image is zero in both directions" $ do |
| 82 | + let (gx, gy) = A.gradient gray |
| 83 | + A.toList gx `shouldSatisfy` all (`approx` 0.0) |
| 84 | + A.toList gy `shouldSatisfy` all (`approx` 0.0) |
| 85 | + |
| 86 | + describe "summed area table (sat)" $ do |
| 87 | + it "preserves the image dimensions" $ |
| 88 | + A.getDims (A.sat gray) `shouldBe` (4,4,1,1) |
| 89 | + |
| 90 | + it "bottom-right cell holds the total sum" $ |
| 91 | + -- column-major: last element is the integral over the whole image |
| 92 | + last (A.toList (A.sat gray)) `shouldBeApprox` (16.0 :: Float) |
| 93 | + |
| 94 | + describe "moments" $ |
| 95 | + it "M00 of a constant image equals its total intensity (area)" $ |
| 96 | + A.momentsAll gray A.M00 `shouldBeApprox` (16.0 :: Double) |
| 97 | + |
| 98 | + where |
| 99 | + -- relative+absolute tolerance check, returning Bool for use with `all` |
| 100 | + approx :: Float -> Float -> Bool |
| 101 | + approx x e = abs (x - e) <= 1e-8 + 1e-5 * max (abs x) (abs e) |
0 commit comments