From bc010ac82591cfc3e26b4cfd113dcc15a13e5e83 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:48:57 +0200 Subject: [PATCH 1/2] Validate camera projection matrix shapes --- src/pyrecest/models/sensor_models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pyrecest/models/sensor_models.py b/src/pyrecest/models/sensor_models.py index 1a6a77072..9162f7469 100644 --- a/src/pyrecest/models/sensor_models.py +++ b/src/pyrecest/models/sensor_models.py @@ -126,6 +126,13 @@ def _as_vector(value, length: int, name: str): return vector +def _as_matrix(value, shape: tuple[int, int], name: str): + matrix = asarray(value) + if tuple(matrix.shape) != shape: + raise ValueError(f"{name} must have shape {shape}") + return matrix + + def _as_sensor_positions(value, position_dim: int = 2): sensors = asarray(value) sensor_shape = tuple(sensors.shape) @@ -334,7 +341,7 @@ def camera_projection_measurement( are returned. """ position = _select(state, position_indices, 3, "position_indices") - rotation = asarray(rotation) if rotation is not None else None + rotation = None if rotation is None else _as_matrix(rotation, (3, 3), "rotation") translation = _as_vector(translation, 3, "translation") camera_position = position if rotation is None else matvec(rotation, position) camera_position = camera_position + translation @@ -348,7 +355,7 @@ def camera_projection_measurement( ) if camera_matrix is None: return stack([normalized[0], normalized[1]]) - homogeneous = matvec(asarray(camera_matrix), normalized) + homogeneous = matvec(_as_matrix(camera_matrix, (3, 3), "camera_matrix"), normalized) _validate_nonzero_scalar(homogeneous[2], "homogeneous camera scale") return stack([homogeneous[0] / homogeneous[2], homogeneous[1] / homogeneous[2]]) From 48dbe7763048e838c085bbdad31f089547fe4706 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:49:11 +0200 Subject: [PATCH 2/2] Add camera projection shape validation tests --- tests/test_camera_projection_validation.py | 48 ++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_camera_projection_validation.py diff --git a/tests/test_camera_projection_validation.py b/tests/test_camera_projection_validation.py new file mode 100644 index 000000000..f7b015e39 --- /dev/null +++ b/tests/test_camera_projection_validation.py @@ -0,0 +1,48 @@ +"""Regression tests for camera projection geometry validation.""" + +import unittest + +from pyrecest.backend import array, eye +from pyrecest.models import camera_projection_measurement + + +class TestCameraProjectionValidation(unittest.TestCase): + def test_camera_projection_rejects_malformed_rotation_shape(self): + state = array([1.0, 2.0, 4.0]) + + invalid_rotations = ( + array([1.0, 0.0, 0.0]), + array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]), + ) + for rotation in invalid_rotations: + with self.subTest(rotation_shape=tuple(rotation.shape)): + with self.assertRaisesRegex(ValueError, "rotation"): + camera_projection_measurement(state, rotation=rotation) + + def test_camera_projection_rejects_malformed_camera_matrix_shape(self): + state = array([1.0, 2.0, 4.0]) + + invalid_camera_matrices = ( + array([1.0, 0.0, 0.0]), + array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]), + ) + for camera_matrix in invalid_camera_matrices: + with self.subTest(camera_matrix_shape=tuple(camera_matrix.shape)): + with self.assertRaisesRegex(ValueError, "camera_matrix"): + camera_projection_measurement(state, camera_matrix=camera_matrix) + + def test_camera_projection_accepts_well_formed_rotation_and_camera_matrix(self): + state = array([2.0, 4.0, 2.0]) + camera_matrix = array([[2.0, 0.0, 10.0], [0.0, 3.0, 20.0], [0.0, 0.0, 1.0]]) + + projected = camera_projection_measurement( + state, + rotation=eye(3), + camera_matrix=camera_matrix, + ) + + self.assertEqual(tuple(projected.shape), (2,)) + + +if __name__ == "__main__": + unittest.main()