-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #109 from AllenNeuralDynamics/wip/pytest
Wip/pytest
- Loading branch information
Showing
47 changed files
with
1,046 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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...") | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" |
Oops, something went wrong.