Skip to content

Commit bafa9e9

Browse files
test: expose camera geometry assertion failures (#143)
Use direct analytic assertions for camera parameters, projection, rays, matrices, and returned camera poses. Co-authored-by: Peter Corke <peter.i.corke@gmail.com>
1 parent 9a1e327 commit bafa9e9

1 file changed

Lines changed: 52 additions & 103 deletions

File tree

‎tests/test_camera.py‎

Lines changed: 52 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,12 @@ def test_parameters(self):
7373
self.assertEqual(c.v0, 300)
7474
nt.assert_array_almost_equal(c.pp, (200, 300))
7575

76-
# Test setting focal length
76+
# Setters must update both components, not silently fail.
7777
c = CentralCamera(f=0.015)
78-
try:
79-
c.f = 0.02
80-
self.assertAlmostEqual(c.f[0], 0.02)
81-
except:
82-
pass
83-
84-
# Test setting principal point
85-
try:
86-
c.pp = (400, 300)
87-
self.assertEqual(c.pp[0], 400)
88-
self.assertEqual(c.pp[1], 300)
89-
except:
90-
pass
78+
c.f = 0.02
79+
nt.assert_allclose(c.f, [0.02, 0.02])
80+
c.pp = (400, 300)
81+
nt.assert_array_equal(c.pp, [400, 300])
9182

9283
def test_pose(self):
9384

@@ -106,25 +97,16 @@ def test_pose(self):
10697
self.assertTrue(c.pose == y)
10798

10899
def test_project_point(self):
109-
"""Test projecting 3D points to image plane"""
100+
"""Check pinhole projection with points stored in columns."""
110101
c = CentralCamera(f=0.015, rho=10e-6, imagesize=[1280, 1024])
102+
p = c.project_point([0.1, 0.2, 3.0])
103+
self.assertEqual(p.shape, (2, 1))
104+
nt.assert_allclose(p, [[690], [612]])
111105

112-
# Project a single point
113-
try:
114-
P = np.array([0.1, 0.2, 3.0])
115-
p = c.project_point(P)
116-
self.assertEqual(p.shape, (2,))
117-
except:
118-
# Method might not exist
119-
pass
120-
121-
# Project multiple points
122-
try:
123-
P_multi = np.array([[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [3.0, 3.0, 3.0]]).T
124-
p_multi = c.project_point(P_multi)
125-
self.assertEqual(p_multi.shape[1], 3)
126-
except:
127-
pass
106+
P = np.array([[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [3.0, 3.0, 3.0]])
107+
p = c.project_point(P)
108+
self.assertEqual(p.shape, (2, 3))
109+
nt.assert_allclose(p, [[690, 740, 790], [612, 662, 712]])
128110

129111
def test_K_matrix(self):
130112
"""Test camera calibration matrix"""
@@ -155,50 +137,33 @@ def test_different_camera_types(self):
155137
c_persp = CentralCamera(f=0.015)
156138
self.assertEqual(c_persp.camtype, "perspective")
157139

158-
# Test camera with distortion parameters
159-
try:
160-
c_dist = CentralCamera(f=0.015, distortion=[0.1, 0.01])
161-
self.assertIsNotNone(c_dist.distortion)
162-
except:
163-
pass
140+
c_dist = CentralCamera(f=0.015, distortion=[0.1, 0.01])
141+
nt.assert_array_equal(c_dist.distortion, [0.1, 0.01])
164142

165143
def test_ray_direction(self):
166-
"""Test computing ray directions"""
167-
c = CentralCamera(f=0.015, rho=10e-6, imagesize=[1280, 1024])
168-
169-
try:
170-
# Test getting ray for a pixel
171-
ray = c.ray([640, 512])
172-
if ray is not None:
173-
self.assertEqual(len(ray), 3)
174-
except:
175-
# Method might not exist
176-
pass
144+
"""Rays contain the camera centre and follow the pixel direction."""
145+
c = CentralCamera(f=0.015, rho=10e-6, imagesize=[1280, 1024], pose=SE3(1, 2, 3))
146+
for pixel, direction in [([640, 512], [0, 0, 1]), ([790, 512], [0.1, 0, 1])]:
147+
with self.subTest(pixel=pixel):
148+
ray = c.ray(pixel)
149+
self.assertEqual(len(ray), 1)
150+
nt.assert_allclose(ray.w / ray.w[2], direction, atol=1e-12)
151+
self.assertTrue(ray.contains(c.centre))
152+
self.assertTrue(ray.contains(c.centre + np.array(direction)))
177153

178-
@unittest.skip("move method doesn't work as expected")
179154
def test_move_camera(self):
180-
"""Test moving camera in space"""
181-
c = CentralCamera()
182-
183-
# Move camera
184-
T = SE3(1, 2, 3)
185-
try:
186-
c.move(T)
187-
# Check that pose was updated
188-
self.assertIsNotNone(c.pose)
189-
except:
190-
# Method might not exist, just verify basic functionality
191-
pass
192-
c = CentralCamera(pose=x)
193-
self.assertTrue(c.pose == x)
194-
195-
c.pose = y
196-
self.assertTrue(c.pose == y)
197-
198-
c = CentralCamera()
199-
c2 = c.move(x)
200-
self.assertIsNot(c, c2)
201-
self.assertTrue(c2.pose == x)
155+
"""Move returns a new camera and leaves the original pose unchanged."""
156+
original_pose = SE3(1, 2, 3)
157+
c = CentralCamera(f=0.015, pose=original_pose)
158+
T = SE3(0.5, 0.25, 0)
159+
moved = c.move(T)
160+
self.assertIsNot(c, moved)
161+
nt.assert_allclose(moved.pose.A, T.A)
162+
nt.assert_allclose(c.pose.A, original_pose.A)
163+
nt.assert_allclose(moved.f, c.f)
164+
relative = c.move(T, relative=True)
165+
nt.assert_allclose(relative.pose.A, (original_pose * T).A)
166+
nt.assert_allclose(c.pose.A, original_pose.A)
202167

203168
def test_str(self):
204169
c = CentralCamera(f=0.123, imagesize=(2000, 3000), name="fred")
@@ -341,41 +306,25 @@ def test_camera_pose_transformations(self):
341306
self.assertTrue(c.pose == new_pose)
342307

343308
def test_camera_matrix_access(self):
344-
"""Test accessing camera matrices"""
345-
c = CentralCamera(f=0.015, rho=10e-6, pp=(640, 512))
346-
347-
try:
348-
# K matrix
349-
K = c.K
350-
self.assertEqual(K.shape, (3, 3))
351-
self.assertEqual(K[2, 2], 1.0)
352-
except:
353-
pass
354-
355-
try:
356-
# C matrix
357-
C = c.C()
358-
self.assertEqual(C.shape, (3, 4))
359-
except:
360-
pass
361-
362-
try:
363-
# P matrix
364-
P = c.P()
365-
self.assertEqual(P.shape, (3, 4))
366-
except:
367-
pass
309+
"""Check calibration and extrinsics against an analytic matrix."""
310+
c = CentralCamera(f=0.015, rho=10e-6, pp=(640, 512), pose=SE3(1, 2, 3))
311+
K = np.array([[1500, 0, 640], [0, 1500, 512], [0, 0, 1]])
312+
nt.assert_allclose(c.K, K)
313+
C = np.array([[1500, 0, 640, -3420], [0, 1500, 512, -4536], [0, 0, 1, -3]])
314+
nt.assert_allclose(c.C(), C)
315+
# C is the projection matrix; there is no separate P() API.
316+
P = np.array([1.1, 2.2, 6, 1])
317+
projected = C @ P
318+
nt.assert_allclose(c.project_point(P[:3]).ravel(), projected[:2] / projected[2])
368319

369320
def test_camera_clone_methods(self):
370-
"""Test camera cloning/copying"""
321+
"""A separately constructed camera can reuse a pose without sharing it."""
371322
c = CentralCamera(f=0.015, pose=SE3(1, 2, 3))
372-
373-
try:
374-
# Some way to clone the camera
375-
c_copy = CentralCamera(f=0.015, pose=c.pose)
376-
self.assertTrue(c_copy.pose == c.pose)
377-
except:
378-
pass
323+
c_copy = CentralCamera(f=0.015, pose=c.pose)
324+
self.assertIsNot(c, c_copy)
325+
nt.assert_allclose(c_copy.pose.A, c.pose.A)
326+
c_copy.pose = SE3(4, 5, 6)
327+
nt.assert_allclose(c.pose.t, [1, 2, 3])
379328

380329
def test_images2C(self):
381330
"""Checkerboard calibration end-to-end (regression: OpenCV 5's

0 commit comments

Comments
 (0)