diff --git a/src/clearex/registration/linear.py b/src/clearex/registration/linear.py index 0e9483d..7b1d202 100644 --- a/src/clearex/registration/linear.py +++ b/src/clearex/registration/linear.py @@ -264,8 +264,9 @@ def _extract_shear(affine_matrix): affine_matrix: np.ndarray 4x4 affine transform matrix. """ - rotation, scaling_shear = polar(a=affine_matrix[:3, :3]) - _, shear = rq(a=scaling_shear) + # Extract shear directly from the 3x3 matrix using RQ decomposition + # This gives us the "raw" shear without removing rotation first + shear, _ = rq(a=affine_matrix[:3, :3]) # Use shear coefficients: sx, sy, sz = np.diag(v=shear) @@ -311,11 +312,19 @@ def _extract_rotation(affine_matrix): """ rotation, _ = polar(a=affine_matrix[:3, :3]) - # Create a rotation object - r = Rotation.from_matrix(matrix=rotation) - - # Extract Euler angles (XYZ order) in degrees - euler_angles_deg = r.as_euler(seq="xyz", degrees=True) + # Check for reflection (negative determinant) + det = np.linalg.det(rotation) + if det < 0: + logger.warning("Transform contains reflection (negative determinant). Reporting zero rotation.") + print("Warning: Transform contains reflection (negative determinant).") + # For reflection transforms, report zero rotation + euler_angles_deg = np.array([0.0, 0.0, 0.0]) + else: + # Create a rotation object + r = Rotation.from_matrix(matrix=rotation) + + # Extract Euler angles (XYZ order) in degrees + euler_angles_deg = r.as_euler(seq="xyz", degrees=True) # Print angles clearly axis_labels: list[str] = ["X (roll)", "Y (pitch)", "Z (yaw)"] diff --git a/tests/registration/test_image_registration.py b/tests/registration/test_image_registration.py index 09afbce..d5a0811 100644 --- a/tests/registration/test_image_registration.py +++ b/tests/registration/test_image_registration.py @@ -46,45 +46,40 @@ class TestImageRegistration: def test_initialization_with_defaults(self): """Test that ImageRegistration initializes with default values.""" - reg = ImageRegistration() - assert reg.fixed_image_path is None - assert reg.moving_image_path is None - assert reg.save_directory is None - assert reg.imaging_round == 0 - assert reg.crop is False - assert reg.enable_logging is True - assert reg._log is None - assert reg._image_opener is not None + with tempfile.TemporaryDirectory() as tmpdir: + # Create dummy image files + fixed_path = os.path.join(tmpdir, "fixed.npy") + moving_path = os.path.join(tmpdir, "moving.npy") + + # Create simple 3D arrays + fixed_arr = np.random.rand(10, 10, 10).astype(np.float32) + moving_arr = np.random.rand(10, 10, 10).astype(np.float32) + + np.save(fixed_path, fixed_arr) + np.save(moving_path, moving_arr) + + reg = ImageRegistration( + fixed_image_path=fixed_path, + moving_image_path=moving_path, + save_directory=tmpdir, + ) + assert reg.fixed_image_path == fixed_path + assert reg.moving_image_path == moving_path + assert reg.save_directory == tmpdir + assert reg.imaging_round == 0 + assert reg.crop is False + assert reg.force_override is False + assert reg._log is not None + assert reg._image_opener is not None def test_initialization_with_custom_values(self): """Test that ImageRegistration initializes with custom values.""" - reg = ImageRegistration( - fixed_image_path="fixed.tif", - moving_image_path="moving.tif", - save_directory="/tmp/output", - imaging_round=5, - crop=True, - enable_logging=False, - ) - assert reg.fixed_image_path == "fixed.tif" - assert reg.moving_image_path == "moving.tif" - assert reg.save_directory == "/tmp/output" - assert reg.imaging_round == 5 - assert reg.crop is True - assert reg.enable_logging is False - - def test_register_missing_required_parameters(self): - """Test that register raises ValueError when required parameters are missing.""" - reg = ImageRegistration() - with pytest.raises(ValueError, match="fixed_image_path, moving_image_path, and save_directory"): - reg.register() - - def test_register_uses_instance_attributes(self): - """Test that register uses instance attributes when parameters not provided.""" with tempfile.TemporaryDirectory() as tmpdir: # Create dummy image files fixed_path = os.path.join(tmpdir, "fixed.npy") moving_path = os.path.join(tmpdir, "moving.npy") + output_dir = os.path.join(tmpdir, "output") + os.makedirs(output_dir, exist_ok=True) # Create simple 3D arrays fixed_arr = np.random.rand(10, 10, 10).astype(np.float32) @@ -96,22 +91,42 @@ def test_register_uses_instance_attributes(self): reg = ImageRegistration( fixed_image_path=fixed_path, moving_image_path=moving_path, - save_directory=tmpdir, - imaging_round=1, + save_directory=output_dir, + imaging_round=5, + crop=True, enable_logging=False, ) + assert reg.fixed_image_path == fixed_path + assert reg.moving_image_path == moving_path + assert reg.save_directory == output_dir + assert reg.imaging_round == 5 + assert reg.crop is True + assert reg.force_override is False + + def test_initialization_validates_required_parameters(self): + """Test that ImageRegistration __init__ validates required parameters.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create dummy image files + fixed_path = os.path.join(tmpdir, "fixed.npy") + moving_path = os.path.join(tmpdir, "moving.npy") - # Mock the internal methods to avoid actual registration - with patch.object(reg, '_perform_linear_registration', return_value=MagicMock()): - with patch.object(reg, '_perform_nonlinear_registration'): - with patch('clearex.registration.crop_data', return_value=moving_arr): - reg.register() + # Create simple 3D arrays + fixed_arr = np.random.rand(10, 10, 10).astype(np.float32) + moving_arr = np.random.rand(10, 10, 10).astype(np.float32) - # Verify that logging was initialized - assert reg._log is not None + np.save(fixed_path, fixed_arr) + np.save(moving_path, moving_arr) + + # Test initialization succeeds with all required parameters + reg = ImageRegistration( + fixed_image_path=fixed_path, + moving_image_path=moving_path, + save_directory=tmpdir, + ) + assert reg is not None - def test_register_uses_provided_parameters(self): - """Test that register prefers provided parameters over instance attributes.""" + def test_register_uses_instance_attributes(self): + """Test that register uses instance attributes when parameters not provided.""" with tempfile.TemporaryDirectory() as tmpdir: # Create dummy image files fixed_path = os.path.join(tmpdir, "fixed.npy") @@ -125,25 +140,21 @@ def test_register_uses_provided_parameters(self): np.save(moving_path, moving_arr) reg = ImageRegistration( - fixed_image_path="wrong_fixed.tif", - moving_image_path="wrong_moving.tif", - save_directory="/wrong/path", - imaging_round=99, + fixed_image_path=fixed_path, + moving_image_path=moving_path, + save_directory=tmpdir, + imaging_round=1, enable_logging=False, ) # Mock the internal methods to avoid actual registration - with patch.object(reg, '_perform_linear_registration', return_value=MagicMock()): - with patch.object(reg, '_perform_nonlinear_registration'): - with patch('clearex.registration.crop_data', return_value=moving_arr): - reg.register( - fixed_image_path=fixed_path, - moving_image_path=moving_path, - save_directory=tmpdir, - imaging_round=1, - ) - - # The method should have run successfully with provided params + mock_image = MagicMock() + mock_mask = MagicMock() + with patch.object(reg, '_perform_linear_registration', return_value=(mock_image, mock_mask)): + with patch.object(reg, '_perform_nonlinear_registration', return_value=(mock_image, mock_mask)): + reg.register() + + # Verify that logging was initialized assert reg._log is not None