Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/clearex/registration/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)"]
Expand Down
127 changes: 69 additions & 58 deletions tests/registration/test_image_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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


Expand Down