@@ -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