Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip/pytest #109

Merged
merged 18 commits into from
Oct 18, 2024
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
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
Loading