Skip to content

Commit 7964324

Browse files
committed
test|doc: Add Vision tests, fix documentation bugs.
1 parent 888be21 commit 7964324

5 files changed

Lines changed: 271 additions & 13 deletions

File tree

test/ArrayFire/ArithSpec.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import GHC.Stack
1515
import Test.HUnit.Lang (FailureReason (..), HUnitFailure (..))
1616
import Test.Hspec
1717
import Test.Hspec.QuickCheck
18+
import Test.QuickCheck ((==>))
1819
import Prelude hiding (div)
1920

2021
compareWith :: (HasCallStack, Show a) => (a -> a -> Bool) -> a -> a -> Expectation
@@ -40,8 +41,10 @@ instance HasEpsilon Double where
4041
approxWith :: (Ord a, Num a) => a -> a -> a -> a -> Bool
4142
approxWith rtol atol a b = abs (a - b) <= Prelude.max atol (rtol * Prelude.max (abs a) (abs b))
4243

44+
-- | Relative + absolute tolerance check at machine-epsilon scale.
45+
-- Tolerance = max(4*eps, 2*eps * max(|a|,|b|)).
4346
approx :: (Ord a, HasEpsilon a) => a -> a -> Bool
44-
approx a b = approxWith (2 * eps * Prelude.max (abs a) (abs b)) (4 * eps) a b
47+
approx a b = approxWith (2 * eps) (4 * eps) a b
4548

4649
shouldBeApprox :: (Ord a, HasEpsilon a, Show a) => a -> a -> Expectation
4750
shouldBeApprox = compareWith approx
@@ -93,7 +96,9 @@ spec =
9396
matrix @Int (2, 2) [[1, 1], [1, 1]] + matrix @Int (2, 2) [[1, 1], [1, 1]]
9497
`shouldBe` matrix @Int (2, 2) [[2, 2], [2, 2]]
9598
prop "Should take cubed root" $ \(x :: Double) ->
96-
evalf (ArrayFire.cbrt (scalar (x * x * x))) `shouldBeApprox` x
99+
let x3 = x * x * x
100+
in not (isNaN x3 || isInfinite x3) ==>
101+
evalf (ArrayFire.cbrt (scalar x3)) `shouldBeApprox` x
97102

98103
it "Should lte Array" $ do
99104
2 `ArrayFire.le` (3 :: Array Double) `shouldBe` 1

test/ArrayFire/DeviceSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Test.Hspec
77

88
spec :: Spec
99
spec =
10-
describe "Algorithm tests" $ do
10+
describe "Device tests" $ do
1111
it "Should show device info" $ do
1212
A.info `shouldReturn` ()
1313
it "Should show device init" $ do

test/ArrayFire/FeaturesSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Test.Hspec
77

88
spec :: Spec
99
spec =
10-
describe "Feautures tests" $ do
10+
describe "Features tests" $ do
1111
it "Should get features number an array" $ do
1212
let feats = createFeatures 10
1313
getFeaturesNum feats `shouldBe` 10

test/ArrayFire/LAPACKSpec.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ spec =
8686
A.choleskyInplace a False `shouldBe` 0
8787

8888
it "Should solve Ax=b using solveLU" $ do
89-
-- A = | 2 1 | b = | 5 | => x = | 2 |
89+
-- A = | 2 1 | b = | 5 | => x = | 1 |
9090
-- | 1 3 | | 10| | 3 |
9191
-- Column-major A: [2,1,1,3], b: [5,10]
9292
let a = A.mkArray @Double [2,2] [2,1,1,3]

test/ArrayFire/VisionSpec.hs

Lines changed: 261 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,267 @@
1-
{-# LANGUAGE TypeApplications #-}
1+
{-# LANGUAGE ScopedTypeVariables #-}
2+
{-# LANGUAGE TypeApplications #-}
23
module ArrayFire.VisionSpec where
34

4-
import qualified ArrayFire as A
5+
import qualified ArrayFire as A
6+
import Control.Exception (SomeException, evaluate, try)
7+
import Control.Monad (when)
58
import Test.Hspec
69

10+
-- | 100×100 constant-intensity Float image. No edges or corners.
11+
-- FAST / Harris / SUSAN must produce 0 features on this image.
12+
flatImg :: A.Array Float
13+
flatImg = A.constant @Float [100, 100] 0.5
14+
15+
-- | 100×100 image composed of four 50×50 quadrants with alternating
16+
-- intensities (0.0 / 1.0), creating a strong corner at the centre.
17+
quadrantImg :: A.Array Float
18+
quadrantImg =
19+
let tl = A.constant @Float [50, 50] 0.0
20+
tr = A.constant @Float [50, 50] 1.0
21+
bl = A.constant @Float [50, 50] 1.0
22+
br = A.constant @Float [50, 50] 0.0
23+
in A.join 0 (A.join 1 tl tr) (A.join 1 bl br)
24+
25+
xpos, ypos, score, orient, size_ :: A.Features -> A.Array Float
26+
xpos = A.getFeaturesXPos
27+
ypos = A.getFeaturesYPos
28+
score = A.getFeaturesScore
29+
orient = A.getFeaturesOrientation
30+
size_ = A.getFeaturesSize
31+
732
spec :: Spec
8-
spec =
9-
describe "Vision spec" $ do
10-
it "Should construct Features for fast feature detection" $ do
11-
let arr = A.vector @Int 30000 [1..]
12-
let feats = A.fast arr 1.0 9 False 1.0 3
13-
(1 + 1) `shouldBe` 2
33+
spec = describe "Vision spec" $ do
34+
35+
-- ------------------------------------------------------------------ --
36+
-- FAST
37+
-- ------------------------------------------------------------------ --
38+
describe "fast" $ do
39+
it "detects 0 features on a flat image" $
40+
A.getFeaturesNum (A.fast flatImg 0.05 9 False 1.0 3) `shouldBe` 0
41+
42+
it "all accessor arrays are consistent with getFeaturesNum" $ do
43+
let feats = A.fast quadrantImg 0.1 9 False 1.0 3
44+
n = A.getFeaturesNum feats
45+
A.getElements (xpos feats) `shouldBe` n
46+
A.getElements (ypos feats) `shouldBe` n
47+
A.getElements (score feats) `shouldBe` n
48+
A.getElements (orient feats) `shouldBe` n
49+
A.getElements (size_ feats) `shouldBe` n
50+
51+
it "detected x-coordinates lie in [0, 100)" $ do
52+
let feats = A.fast quadrantImg 0.1 9 False 1.0 3
53+
A.toList (xpos feats) `shouldSatisfy` all (\x -> x >= (0 :: Float) && x < 100)
54+
55+
it "detected y-coordinates lie in [0, 100)" $ do
56+
let feats = A.fast quadrantImg 0.1 9 False 1.0 3
57+
A.toList (ypos feats) `shouldSatisfy` all (\y -> y >= (0 :: Float) && y < 100)
58+
59+
it "all feature scores are non-negative" $ do
60+
let feats = A.fast quadrantImg 0.1 9 False 1.0 3
61+
A.toList (score feats) `shouldSatisfy` all (>= (0 :: Float))
62+
63+
-- ------------------------------------------------------------------ --
64+
-- Harris
65+
-- ------------------------------------------------------------------ --
66+
describe "harris" $ do
67+
it "detects 0 corners on a flat image" $
68+
A.getFeaturesNum (A.harris flatImg 500 1e-3 1.0 0 0.04) `shouldBe` 0
69+
70+
it "all accessor arrays are consistent with getFeaturesNum" $ do
71+
let feats = A.harris quadrantImg 500 1e-3 1.0 0 0.04
72+
n = A.getFeaturesNum feats
73+
A.getElements (xpos feats) `shouldBe` n
74+
A.getElements (ypos feats) `shouldBe` n
75+
A.getElements (score feats) `shouldBe` n
76+
77+
it "detected x-coordinates lie in [0, 100)" $ do
78+
let feats = A.harris quadrantImg 500 1e-3 1.0 0 0.04
79+
A.toList (xpos feats) `shouldSatisfy` all (\x -> x >= (0 :: Float) && x < 100)
80+
81+
it "detected y-coordinates lie in [0, 100)" $ do
82+
let feats = A.harris quadrantImg 500 1e-3 1.0 0 0.04
83+
A.toList (ypos feats) `shouldSatisfy` all (\y -> y >= (0 :: Float) && y < 100)
84+
85+
-- ------------------------------------------------------------------ --
86+
-- ORB
87+
-- ------------------------------------------------------------------ --
88+
describe "orb" $ do
89+
it "descriptor row count equals getFeaturesNum" $ do
90+
let (feats, descs) = A.orb quadrantImg 0.1 500 1.5 4 False
91+
n = A.getFeaturesNum feats
92+
(d0, _, _, _) = A.getDims (descs :: A.Array Float)
93+
d0 `shouldBe` n
94+
95+
it "all coordinate arrays are consistent with getFeaturesNum" $ do
96+
let (feats, _) = A.orb quadrantImg 0.1 500 1.5 4 False
97+
n = A.getFeaturesNum feats
98+
A.getElements (xpos feats) `shouldBe` n
99+
A.getElements (ypos feats) `shouldBe` n
100+
A.getElements (score feats) `shouldBe` n
101+
A.getElements (orient feats) `shouldBe` n
102+
A.getElements (size_ feats) `shouldBe` n
103+
104+
-- ------------------------------------------------------------------ --
105+
-- SUSAN
106+
-- ------------------------------------------------------------------ --
107+
describe "susan" $ do
108+
it "detects 0 corners on a flat image" $
109+
A.getFeaturesNum (A.susan flatImg 3 0.1 0.5 0.05 3) `shouldBe` 0
110+
111+
it "all accessor arrays are consistent with getFeaturesNum" $ do
112+
let feats = A.susan quadrantImg 3 0.1 0.5 0.05 3
113+
n = A.getFeaturesNum feats
114+
A.getElements (xpos feats) `shouldBe` n
115+
A.getElements (ypos feats) `shouldBe` n
116+
A.getElements (score feats) `shouldBe` n
117+
118+
it "detected x-coordinates lie in [0, 100)" $ do
119+
let feats = A.susan quadrantImg 3 0.1 0.5 0.05 3
120+
A.toList (xpos feats) `shouldSatisfy` all (\x -> x >= (0 :: Float) && x < 100)
121+
122+
-- ------------------------------------------------------------------ --
123+
-- Difference of Gaussians
124+
-- ------------------------------------------------------------------ --
125+
describe "dog" $ do
126+
it "output has the same dimensions as the input image" $
127+
A.getDims (A.dog flatImg 1 2) `shouldBe` (100, 100, 1, 1)
128+
129+
it "DoG of a constant image has zero interior values" $ do
130+
-- Border pixels are non-zero due to Gaussian zero-padding; the interior
131+
-- (at least 2 pixels from each edge for kernel radius=2) must be zero.
132+
let result = A.dog (A.constant @Float [20, 20] 0.5) 1 2
133+
interior = result A.! (A.range 2 17, A.range 2 17)
134+
A.toList @Float interior `shouldSatisfy` all (\v -> abs v < 1e-5)
135+
136+
it "different radii produce different results on a non-constant image" $ do
137+
let dog12 = A.dog quadrantImg 1 2
138+
dog13 = A.dog quadrantImg 1 3
139+
(dog12 == dog13) `shouldBe` False
140+
141+
-- ------------------------------------------------------------------ --
142+
-- matchTemplate
143+
-- ------------------------------------------------------------------ --
144+
describe "matchTemplate" $ do
145+
it "output has the same dimensions as the search image" $ do
146+
let img = A.constant @Float [20, 20] 1.0
147+
tmpl = A.constant @Float [5, 5] 1.0
148+
A.getDims (A.matchTemplate img tmpl A.MatchTypeSAD) `shouldBe` (20, 20, 1, 1)
149+
150+
it "SAD of a zero image against a zero template is zero everywhere" $ do
151+
let img = A.constant @Float [10, 10] 0.0
152+
tmpl = A.constant @Float [3, 3] 0.0
153+
result = A.matchTemplate img tmpl A.MatchTypeSAD
154+
A.toList @Float result `shouldSatisfy` all (< 1e-5)
155+
156+
it "SSD of a zero image against a zero template is zero everywhere" $ do
157+
let img = A.constant @Float [10, 10] 0.0
158+
tmpl = A.constant @Float [3, 3] 0.0
159+
result = A.matchTemplate img tmpl A.MatchTypeSSD
160+
A.toList @Float result `shouldSatisfy` all (< 1e-5)
161+
162+
-- ------------------------------------------------------------------ --
163+
-- hammingMatcher
164+
-- ------------------------------------------------------------------ --
165+
describe "hammingMatcher" $ do
166+
it "identical descriptors produce 0 Hamming distances" $ do
167+
-- 4 features, each 4 uint32 components; dim 0 = feature length
168+
let desc = A.mkArray @A.Word32 [4, 4] (replicate 16 0xDEADBEEF)
169+
(_idxs, dists) = A.hammingMatcher desc desc 0 1
170+
A.toList @A.Word32 dists `shouldBe` replicate 4 0
171+
172+
it "result arrays have one entry per query feature (n_dist = 1)" $ do
173+
let query = A.mkArray @A.Word32 [4, 3] (replicate 12 0x00000000)
174+
train = A.mkArray @A.Word32 [4, 5] (replicate 20 0xFFFFFFFF)
175+
(idxs, dists) = A.hammingMatcher query train 0 1
176+
A.getElements @A.Word32 idxs `shouldBe` 3
177+
A.getElements @A.Word32 dists `shouldBe` 3
178+
179+
it "returned indices are within training-set bounds" $ do
180+
let query = A.mkArray @A.Word32 [4, 3] (replicate 12 0x00000000)
181+
train = A.mkArray @A.Word32 [4, 5] (replicate 20 0x00000000)
182+
(idxs, _dists) = A.hammingMatcher query train 0 1
183+
A.toList @A.Word32 idxs `shouldSatisfy` all (< 5)
184+
185+
-- ------------------------------------------------------------------ --
186+
-- nearestNeighbor
187+
-- ------------------------------------------------------------------ --
188+
describe "nearestNeighbor" $ do
189+
it "identical descriptors produce 0 SAD distances" $ do
190+
let desc = A.mkArray @Float [4, 4] (replicate 16 1.0)
191+
(_idxs, dists) = A.nearestNeighbor desc desc 0 1 A.MatchTypeSAD
192+
A.toList @Float dists `shouldBe` replicate 4 0.0
193+
194+
it "identical descriptors produce 0 SSD distances" $ do
195+
let desc = A.mkArray @Float [4, 4] (replicate 16 1.0)
196+
(_idxs, dists) = A.nearestNeighbor desc desc 0 1 A.MatchTypeSSD
197+
A.toList @Float dists `shouldBe` replicate 4 0.0
198+
199+
it "result count matches number of query features" $ do
200+
let query = A.mkArray @Float [4, 3] (replicate 12 0.0)
201+
train = A.mkArray @Float [4, 5] (replicate 20 1.0)
202+
(idxs, dists) = A.nearestNeighbor query train 0 1 A.MatchTypeSAD
203+
A.getElements @Float idxs `shouldBe` 3
204+
A.getElements @Float dists `shouldBe` 3
205+
206+
it "returned indices are within training-set bounds" $ do
207+
let query = A.mkArray @Float [4, 3] (replicate 12 0.0)
208+
train = A.mkArray @Float [4, 5] (replicate 20 1.0)
209+
(idxs, _) = A.nearestNeighbor query train 0 1 A.MatchTypeSAD
210+
A.toList @Float idxs `shouldSatisfy` all (< 5)
211+
212+
-- ------------------------------------------------------------------ --
213+
-- homography
214+
-- ------------------------------------------------------------------ --
215+
describe "homography" $ do
216+
it "returns a 3×3 homography matrix" $ do
217+
-- 4 exact correspondences: unit square → 2× scaled square
218+
let sx = A.vector @Float 4 [0, 1, 0, 1]
219+
sy = A.vector @Float 4 [0, 0, 1, 1]
220+
dx = A.vector @Float 4 [0, 2, 0, 2]
221+
dy = A.vector @Float 4 [0, 0, 2, 2]
222+
(_, h) = A.homography sx sy dx dy A.RANSAC 1.0 1000
223+
A.getDims h `shouldBe` (3, 3, 1, 1)
224+
225+
it "inlier count is non-negative" $ do
226+
let sx = A.vector @Float 4 [0, 1, 0, 1]
227+
sy = A.vector @Float 4 [0, 0, 1, 1]
228+
(inliers, _) = A.homography sx sy sx sy A.RANSAC 1.0 1000
229+
inliers `shouldSatisfy` (>= 0)
230+
231+
it "identity correspondences yield at least 4 inliers" $ do
232+
let sx = A.vector @Float 4 [0, 1, 0, 1]
233+
sy = A.vector @Float 4 [0, 0, 1, 1]
234+
(inliers, _) = A.homography sx sy sx sy A.RANSAC 10.0 1000
235+
inliers `shouldSatisfy` (>= 4)
236+
237+
-- ------------------------------------------------------------------ --
238+
-- SIFT (may not be compiled into every ArrayFire build)
239+
-- ------------------------------------------------------------------ --
240+
describe "sift" $ do
241+
it "descriptor row count equals getFeaturesNum; width is 128 when features found" $ do
242+
result <- try $ evaluate $
243+
A.sift quadrantImg 3 0.04 10.0 1.6 False (1.0 / 256.0) 0.05
244+
case (result :: Either SomeException (A.Features, A.Array Float)) of
245+
Left _ -> pendingWith "SIFT not available in this ArrayFire build"
246+
Right (feats, descs) -> do
247+
let n = A.getFeaturesNum feats
248+
(d0, d1, _, _) = A.getDims descs
249+
d0 `shouldBe` n
250+
-- AF returns (0,0) when no features are found rather than (0,128),
251+
-- so only assert the column width when at least one feature exists.
252+
when (n > 0) $ d1 `shouldBe` 128
14253

254+
-- ------------------------------------------------------------------ --
255+
-- GLOH (may not be compiled into every ArrayFire build)
256+
-- ------------------------------------------------------------------ --
257+
describe "gloh" $ do
258+
it "descriptor row count equals getFeaturesNum; width is 272 when features found" $ do
259+
result <- try $ evaluate $
260+
A.gloh quadrantImg 3 0.04 10.0 1.6 False (1.0 / 256.0) 0.05
261+
case (result :: Either SomeException (A.Features, A.Array Float)) of
262+
Left _ -> pendingWith "GLOH not available in this ArrayFire build"
263+
Right (feats, descs) -> do
264+
let n = A.getFeaturesNum feats
265+
(d0, d1, _, _) = A.getDims descs
266+
d0 `shouldBe` n
267+
when (n > 0) $ d1 `shouldBe` 272

0 commit comments

Comments
 (0)