Skip to content

Commit

Permalink
Merge pull request #109 from AllenNeuralDynamics/wip/pytest
Browse files Browse the repository at this point in the history
Wip/pytest
  • Loading branch information
jsiegle authored Oct 18, 2024
2 parents f73694c + 849e9c7 commit b5d5306
Show file tree
Hide file tree
Showing 47 changed files with 1,046 additions and 3 deletions.
2 changes: 1 addition & 1 deletion parallax/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os

__version__ = "1.0.1"
__version__ = "1.1.0"

# allow multiple OpenMP instances
os.environ["KMP_DUPLICATE_LIB_OK"] = "True"
11 changes: 11 additions & 0 deletions tests/test__setting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import pytest
from PyQt5.QtWidgets import QApplication

@pytest.fixture(scope="session")
def qapp():
app = QApplication([])
yield app
app.quit()

print("\nInitializing QApplication for tests...")

123 changes: 123 additions & 0 deletions tests/test_calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import pytest
from unittest.mock import MagicMock
from PyQt5.QtWidgets import QLineEdit, QComboBox
import numpy as np
from parallax.calculator import Calculator

@pytest.fixture
def setup_calculator(qtbot):
# Mocking the model and stage_controller
model = MagicMock()
reticle_selector = QComboBox()
stage_controller = MagicMock()

# Mock the model's stages and transforms
model.stages = {'stage1': MagicMock(), 'stage2': MagicMock()}
model.transforms = {'stage1': (np.eye(4), [1, 1, 1]), 'stage2': (None, None)}

# Initialize the Calculator widget
calculator = Calculator(model, reticle_selector, stage_controller)
qtbot.addWidget(calculator)
calculator.show()

return calculator, model, stage_controller

def test_set_current_reticle(setup_calculator, qtbot):
calculator, model, stage_controller = setup_calculator

# Simulate selecting a reticle in the dropdown
calculator.reticle_selector.addItem("Global coords (A)")
calculator.reticle_selector.addItem("Global coords (B)")
calculator.reticle_selector.setCurrentIndex(1)

# Trigger reticle change
calculator._setCurrentReticle()

assert calculator.reticle == "B", "Reticle should be set to 'B'"

@pytest.mark.parametrize("localX, localY, localZ, transM_LR, scale, expected_globalX, expected_globalY, expected_globalZ", [
# Case 1: Complex transformation with scaling (your initial case)
(10775.0, 6252.0, 6418.0,
np.array([[0.991319402, 0.079625596, -0.104621259, -10801.14725],
[-0.092116645, 0.988422741, -0.120561228, 6332.440016],
[0.093810271, 0.129152044, 0.987177483, 6122.5096],
[0, 0, 0, 1]]),
np.array([0.994494443, -0.988947511, -0.995326937]),
-2.5, 4.2, 23.1),
# Case 2: Basic translation vector without scaling
(100.0, 200.0, 300.0,
np.array([[1, 0, 0, 10],
[0, 1, 0, 20],
[0, 0, 1, 30],
[0, 0, 0, 1]]),
np.array([1, 1, 1]),
110.0, 220.0, 330.0)
])
def test_transform_local_to_global(setup_calculator, qtbot, localX, localY, localZ, transM_LR, scale, expected_globalX, expected_globalY, expected_globalZ):
calculator, model, stage_controller = setup_calculator

# Mock the QLineEdit fields for stage1
qtbot.keyClicks(calculator.findChild(QLineEdit, 'localX_stage1'), str(localX))
qtbot.keyClicks(calculator.findChild(QLineEdit, 'localY_stage1'), str(localY))
qtbot.keyClicks(calculator.findChild(QLineEdit, 'localZ_stage1'), str(localZ))

# Simulate conversion
calculator._convert('stage1', transM_LR, scale)

# Retrieve the global coordinates from the UI
globalX = calculator.findChild(QLineEdit, 'globalX_stage1').text()
globalY = calculator.findChild(QLineEdit, 'globalY_stage1').text()
globalZ = calculator.findChild(QLineEdit, 'globalZ_stage1').text()

# Convert text values to float for assertion
globalX = float(globalX)
globalY = float(globalY)
globalZ = float(globalZ)

# Check if the transformed values match expected values
assert globalX == pytest.approx(expected_globalX, abs=5), f"Expected globalX to be {expected_globalX}, got {globalX}"
assert globalY == pytest.approx(expected_globalY, abs=5), f"Expected globalY to be {expected_globalY}, got {globalY}"
assert globalZ == pytest.approx(expected_globalZ, abs=5), f"Expected globalZ to be {expected_globalZ}, got {globalZ}"

def test_transform_global_to_local(setup_calculator, qtbot):
calculator, model, stage_controller = setup_calculator

# Mock the QLineEdit fields for stage1
qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalX_stage1'), "15.0")
qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalY_stage1'), "25.0")
qtbot.keyClicks(calculator.findChild(QLineEdit, 'globalZ_stage1'), "35.0")

# Mock transformation matrix and scale for stage1
transM_LR = np.array([[1, 0, 0, 5], [0, 1, 0, 5], [0, 0, 1, 5], [0, 0, 0, 1]])
scale = np.array([1, 1, 1])

# Simulate conversion
calculator._convert('stage1', transM_LR, scale)

localX = calculator.findChild(QLineEdit, 'localX_stage1').text()
localY = calculator.findChild(QLineEdit, 'localY_stage1').text()
localZ = calculator.findChild(QLineEdit, 'localZ_stage1').text()

assert localX == "10.00", f"Expected localX to be 10.00, got {localX}"
assert localY == "20.00", f"Expected localY to be 20.00, got {localY}"
assert localZ == "30.00", f"Expected localZ to be 30.00, got {localZ}"

def test_clear_fields(setup_calculator, qtbot):
calculator, model, stage_controller = setup_calculator

# Set some values in the QLineEdit fields
calculator.findChild(QLineEdit, 'localX_stage1').setText("10.0")
calculator.findChild(QLineEdit, 'localY_stage1').setText("20.0")
calculator.findChild(QLineEdit, 'localZ_stage1').setText("30.0")

# Clear the fields
calculator._clear_fields('stage1')

localX = calculator.findChild(QLineEdit, 'localX_stage1').text()
localY = calculator.findChild(QLineEdit, 'localY_stage1').text()
localZ = calculator.findChild(QLineEdit, 'localZ_stage1').text()

assert localX == "", "Expected localX to be cleared"
assert localY == "", "Expected localY to be cleared"
assert localZ == "", "Expected localZ to be cleared"
147 changes: 147 additions & 0 deletions tests/test_calibration_camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import pytest
import numpy as np
from unittest.mock import MagicMock, patch
from parallax.calibration_camera import CalibrationStereo, OBJPOINTS

@pytest.fixture
def mock_model():
"""Fixture to create a mock model for testing."""
model = MagicMock()
return model

@pytest.fixture
def intrinsic_data():
"""Fixture for mock intrinsic data for both cameras."""
intrinsicA = [
np.array([[1.54e+04, 0.0e+00, 2.0e+03],
[0.0e+00, 1.54e+04, 1.5e+03],
[0.0e+00, 0.0e+00, 1.0e+00]]),
np.array([[0., 0., 0., 0., 0.]]), # Distortion coefficients
(np.array([[-2.88], [-0.08], [-0.47]]),), # rvecA
(np.array([[1.08], [1.01], [58.70]]),) # tvecA
]
intrinsicB = [
np.array([[1.54e+04, 0.0e+00, 2.0e+03],
[0.0e+00, 1.54e+04, 1.5e+03],
[0.0e+00, 0.0e+00, 1.0e+00]]),
np.array([[0., 0., 0., 0., 0.]]), # Distortion coefficients
(np.array([[2.64], [-0.29], [0.35]]),), # rvecB
(np.array([[1.24], [0.33], [56.22]]),) # tvecB
]
return intrinsicA, intrinsicB

@pytest.fixture
def imgpoints_data():
"""Fixture for mock image points for both cameras."""
imgpointsA = [
np.array([
[1786, 1755], [1837, 1756], [1887, 1757], [1937, 1758], [1987, 1759],
[2036, 1760], [2086, 1761], [2136, 1762], [2186, 1763], [2235, 1764],
[2285, 1765], [2334, 1766], [2383, 1767], [2432, 1768], [2481, 1769],
[2530, 1770], [2579, 1771], [2628, 1772], [2676, 1773], [2725, 1774],
[2773, 1775]
], dtype=np.float32),
np.array([
[2233, 2271], [2238, 2220], [2244, 2170], [2249, 2120], [2254, 2069],
[2259, 2019], [2264, 1968], [2269, 1918], [2275, 1866], [2280, 1816],
[2285, 1765], [2290, 1714], [2295, 1663], [2300, 1612], [2306, 1561],
[2311, 1509], [2316, 1459], [2321, 1407], [2327, 1355], [2332, 1304],
[2337, 1252]
], dtype=np.float32)
]
imgpointsB = [
np.array([
[1822, 1677], [1875, 1668], [1927, 1660], [1979, 1651], [2031, 1643],
[2083, 1635], [2135, 1626], [2187, 1618], [2239, 1609], [2290, 1601],
[2341, 1593], [2392, 1584], [2443, 1576], [2494, 1568], [2545, 1559],
[2596, 1551], [2647, 1543], [2698, 1535], [2748, 1526], [2799, 1518],
[2850, 1510]
], dtype=np.float32),
np.array([
[2494, 2081], [2478, 2031], [2463, 1982], [2448, 1933], [2432, 1884],
[2417, 1835], [2402, 1786], [2387, 1738], [2371, 1689], [2356, 1640],
[2341, 1593], [2326, 1544], [2311, 1496], [2296, 1449], [2281, 1401],
[2267, 1354], [2252, 1306], [2237, 1259], [2222, 1212], [2208, 1165],
[2193, 1118]
], dtype=np.float32)
]
return imgpointsA, imgpointsB

@pytest.fixture
def setup_calibration_stereo(mock_model, intrinsic_data, imgpoints_data):
"""Fixture to set up a CalibrationStereo instance."""
camA = "22517664"
camB = "22468054"
imgpointsA, imgpointsB = imgpoints_data
intrinsicA, intrinsicB = intrinsic_data

calib_stereo = CalibrationStereo(
model=mock_model,
camA=camA,
imgpointsA=imgpointsA,
intrinsicA=intrinsicA,
camB=camB,
imgpointsB=imgpointsB,
intrinsicB=intrinsicB
)
return calib_stereo

def test_calibrate_stereo(setup_calibration_stereo):
"""Test the stereo calibration process."""
calib_stereo = setup_calibration_stereo

# Mock the cv2.stereoCalibrate method with expected results
mock_retval = 0.4707333710438937
mock_R_AB = np.array([
[0.92893564, 0.32633462, 0.17488367],
[-0.3667657, 0.7465183, 0.55515165],
[0.05061134, -0.57984149, 0.81315579]
])
mock_T_AB = np.array([[-10.35397621], [-32.59591834], [9.0524749]])
mock_E_AB = np.array([
[1.67041403, 12.14262756, -31.53105623],
[8.93319518, -3.04952897, 10.00252579],
[34.07699343, 2.90774401, -0.04753311]
])
mock_F_AB = np.array([
[-5.14183388e-09, -3.73771847e-08, 1.56104641e-03],
[-2.74979764e-08, 9.38699691e-09, -4.33243885e-04],
[-1.56385384e-03, -7.71647099e-05, 1.00000000e+00]
])

with patch('cv2.stereoCalibrate', return_value=(mock_retval, None, None, None, None, mock_R_AB, mock_T_AB, mock_E_AB, mock_F_AB)) as mock_stereo_calibrate:
retval, R_AB, T_AB, E_AB, F_AB = calib_stereo.calibrate_stereo()

# Use np.allclose() to check that the values match the expected ones.
assert np.isclose(retval, mock_retval, atol=1e-6)
assert np.allclose(R_AB, mock_R_AB, atol=1e-6)
assert np.allclose(T_AB, mock_T_AB, atol=1e-6)
assert np.allclose(E_AB, mock_E_AB, atol=1e-6)
assert np.allclose(F_AB, mock_F_AB, atol=1e-6)

def test_triangulation(setup_calibration_stereo, imgpoints_data):
"""Test the triangulation function for consistency."""
calib_stereo = setup_calibration_stereo
imgpointsA, imgpointsB = imgpoints_data

# Use the OBJPOINTS as the expected points.
expected_points_3d = OBJPOINTS

# Mock the triangulation result to match the expected object points.
homogeneous_coords = np.hstack([expected_points_3d, np.ones((expected_points_3d.shape[0], 1))])

with patch('cv2.triangulatePoints', return_value=homogeneous_coords.T) as mock_triangulate:
points_3d_hom = calib_stereo.triangulation(
calib_stereo.P_A, calib_stereo.P_B, imgpointsA[0], imgpointsB[0]
)

# Prevent division by zero by ensuring the last component isn't zero.
valid_indices = points_3d_hom[:, -1] != 0
points_3d_hom = points_3d_hom[valid_indices]
points_3d_hom = points_3d_hom / points_3d_hom[:, -1].reshape(-1, 1)

# Only compare valid points with the expected object points.
expected_valid_points = expected_points_3d[valid_indices]

# Verify that the triangulation result is similar to the expected object points.
assert np.allclose(points_3d_hom[:, :3], expected_valid_points, atol=1e-2)
61 changes: 61 additions & 0 deletions tests/test_coords_transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import numpy as np
import pytest
from parallax.coords_transformation import RotationTransformation

@pytest.fixture
def transformer():
return RotationTransformation()

def test_roll(transformer):
# Test roll rotation around the x-axis
input_matrix = np.identity(3)
roll_angle = np.pi / 4 # 45 degrees
expected_output = np.array([[1, 0, 0],
[0, np.sqrt(2) / 2, -np.sqrt(2) / 2],
[0, np.sqrt(2) / 2, np.sqrt(2) / 2]])
output = transformer.roll(input_matrix, roll_angle)
assert np.allclose(output, expected_output), "Roll transformation failed."

def test_pitch(transformer):
# Test pitch rotation around the y-axis
input_matrix = np.identity(3)
pitch_angle = np.pi / 6 # 30 degrees
expected_output = np.array([[np.sqrt(3) / 2, 0, 0.5],
[0, 1, 0],
[-0.5, 0, np.sqrt(3) / 2]])
output = transformer.pitch(input_matrix, pitch_angle)
assert np.allclose(output, expected_output), "Pitch transformation failed."

def test_yaw(transformer):
# Test yaw rotation around the z-axis
input_matrix = np.identity(3)
yaw_angle = np.pi / 3 # 60 degrees
expected_output = np.array([[0.5, -np.sqrt(3) / 2, 0],
[np.sqrt(3) / 2, 0.5, 0],
[0, 0, 1]])
output = transformer.yaw(input_matrix, yaw_angle)
assert np.allclose(output, expected_output), "Yaw transformation failed."

def test_extract_angles(transformer):
# Test extraction of roll, pitch, yaw from rotation matrix
rotation_matrix = transformer.combineAngles(np.pi / 4, np.pi / 6, np.pi / 3)
roll, pitch, yaw = transformer.extractAngles(rotation_matrix)

assert np.isclose(roll, np.pi / 4), f"Expected roll to be {np.pi / 4}, got {roll}"
assert np.isclose(pitch, np.pi / 6), f"Expected pitch to be {np.pi / 6}, got {pitch}"
assert np.isclose(yaw, np.pi / 3), f"Expected yaw to be {np.pi / 3}, got {yaw}"

def test_fit_params(transformer):
# Test fitting parameters for transformation
measured_pts = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
global_pts = np.array([[2, 3, 4], [5, 6, 7], [8, 9, 10], [11, 12, 13]])

origin, rotation_matrix, scale, avg_err = transformer.fit_params(measured_pts, global_pts)

# Expected values based on the simplified test data
expected_origin = np.array([1, 1, 1])
expected_scale = np.array([1, 1, 1])

assert np.allclose(origin, expected_origin), f"Expected origin {expected_origin}, got {origin}"
assert np.allclose(scale, expected_scale), f"Expected scale {expected_scale}, got {scale}"
assert avg_err < 1e-7, f"Expected avg error to be near 0, got {avg_err}"
Loading

0 comments on commit b5d5306

Please sign in to comment.