diff --git a/ale/base/type_sensor.py b/ale/base/type_sensor.py index 0e060b1b47fcd7470c395f08f42347465cf5133f..9fa789215cb76611f1483ba6cf80e094aac6233b 100644 --- a/ale/base/type_sensor.py +++ b/ale/base/type_sensor.py @@ -1,4 +1,10 @@ +import math + import numpy as np +from scipy.spatial.transform import Rotation + +from ale.transformation import FrameChain +from ale.transformation import ConstantRotation class LineScanner(): """ @@ -362,3 +368,174 @@ class RollingShutter(): : array """ raise NotImplementedError + + +class Cahvor(): + """ + Mixin for largely ground based sensors to add an + extra step in the frame chain to go from ground camera to + the Camera + """ + + @property + def cahvor_camera_dict(self): + """ + This function extracts and returns the elements for the + CAHVOR camera model from a concrete driver as a dictionary. + See the MSL MASTCAM Cahvor, Framer, Pds3Label, NaifSpice, Driver + """ + raise NotImplementedError + + def compute_h_c(self): + """ + Computes the h_c element of a cahvor model for the conversion + to a photogrametric model + + Returns + ------- + : float + Dot product of A and H vectors + """ + return np.dot(self.cahvor_camera_dict['A'], self.cahvor_camera_dict['H']) + + def compute_h_s(self): + """ + Computes the h_s element of a cahvor model for the conversion + to a photogrametric model + + Returns + ------- + : float + Norm of the cross product of A and H vectors + """ + return np.linalg.norm(np.cross(self.cahvor_camera_dict['A'], self.cahvor_camera_dict['H'])) + + def compute_v_c(self): + """ + Computes the v_c element of a cahvor model for the conversion + to a photogrametric model + + Returns + ------- + : float + Dot product of A and V vectors + """ + return np.dot(self.cahvor_camera_dict['A'], self.cahvor_camera_dict['V']) + + def compute_v_s(self): + """ + Computes the v_s element of a cahvor model for the conversion + to a photogrametric model + + Returns + ------- + : float + Norm of the cross product of A and V vectors + """ + return np.linalg.norm(np.cross(self.cahvor_camera_dict['A'], self.cahvor_camera_dict['V'])) + + @property + def cahvor_rotation_matrix(self): + """ + Computes the cahvor rotation matrix for the instrument to Rover frame + + Returns + ------- + : array + Rotation Matrix as a 2D numpy array + """ + if not hasattr(self, "_cahvor_rotation_matrix"): + h_c = self.compute_h_c() + h_s = self.compute_h_s() + v_c = self.compute_v_c() + v_s = self.compute_v_s() + H_prime = (self.cahvor_camera_dict['H'] - h_c * self.cahvor_camera_dict['A'])/h_s + V_prime = (self.cahvor_camera_dict['V'] - v_c * self.cahvor_camera_dict['A'])/v_s + r_matrix = np.array([H_prime, -V_prime, -self.cahvor_camera_dict['A']]) + + phi = math.asin(r_matrix[2][0]) + w = - math.asin(r_matrix[2][1] / math.cos(phi)) + k = math.acos(r_matrix[0][0] / math.cos(phi)) + + w = math.degrees(w) + phi = math.degrees(phi) + k = math.degrees(k) + + # Rotational Matrix M generation + cahvor_rotation_matrix = np.zeros((3, 3)) + cahvor_rotation_matrix[0, 0] = math.cos(phi) * math.cos(k) + cahvor_rotation_matrix[0, 1] = math.sin(w) * math.sin(phi) * math.cos(k) + \ + math.cos(w) * math.sin(k) + cahvor_rotation_matrix[0, 2] = - math.cos(w) * math.sin(phi) * math.cos(k) + \ + math.sin(w) * math.sin(k) + cahvor_rotation_matrix[1, 0] = - math.cos(phi) * math.sin(k) + cahvor_rotation_matrix[1, 1] = - math.sin(w) * math.sin(phi) * math.sin(k) + \ + math.cos(w) * math.cos(k) + cahvor_rotation_matrix[1, 2] = math.cos(w) * math.sin(phi) * math.sin(k) + \ + math.sin(w) * math.cos(k) + cahvor_rotation_matrix[2, 0] = math.sin(phi) + cahvor_rotation_matrix[2, 1] = - math.sin(w) * math.cos(phi) + cahvor_rotation_matrix[2, 2] = math.cos(w) * math.cos(phi) + self._cahvor_rotation_matrix = cahvor_rotation_matrix + return self._cahvor_rotation_matrix + + @property + def frame_chain(self): + """ + Returns a modified frame chain with the cahvor models extra rotation + added into the model + + Returns + ------- + : object + A networkx frame chain object + """ + if not hasattr(self, '_frame_chain'): + self._frame_chain = FrameChain.from_spice(sensor_frame=self.ikid, + target_frame=self.target_frame_id, + center_ephemeris_time=self.center_ephemeris_time, + ephemeris_times=self.ephemeris_time, + nadir=False, exact_ck_times=False) + cahvor_quats = Rotation.from_matrix(self.cahvor_rotation_matrix).as_quat() + cahvor_rotation = ConstantRotation(cahvor_quats, self.sensor_frame_id, self.ikid) + self._frame_chain.add_edge(rotation = cahvor_rotation) + return self._frame_chain + + @property + def detector_center_line(self): + """ + Computes the detector center line using the cahvor model. + Equation for computation comes from MSL instrument kernels + + Returns + ------- + : float + The detector center line/boresight center line + """ + return self.compute_v_c() + + @property + def detector_center_sample(self): + """ + Computes the detector center sample using the cahvor model. + Equation for computation comes from MSL instrument kernels + + Returns + ------- + : float + The detector center sample/boresight center sample + """ + return self.compute_h_c() + + @property + def pixel_size(self): + """ + Computes the pixel size given the focal length from spice kernels + or other sources + + Returns + ------- + : float + Focal length of a cahvor model instrument + """ + return self.focal_length/self.compute_h_s() diff --git a/ale/drivers/msl_drivers.py b/ale/drivers/msl_drivers.py index 37d53fefd61772a8bf581109f59e1358f20f0efb..25ef1d8ca0f4e0dccb3c765494a258af54b4c0f0 100644 --- a/ale/drivers/msl_drivers.py +++ b/ale/drivers/msl_drivers.py @@ -5,9 +5,10 @@ from ale.base.data_naif import NaifSpice from ale.base.label_pds3 import Pds3Label from ale.base.type_sensor import Framer from ale.base.type_distortion import NoDistortion +from ale.base.type_sensor import Cahvor from ale.base.base import Driver -class MslMastcamPds3NaifSpiceDriver(Framer, Pds3Label, NaifSpice, NoDistortion, Driver): +class MslMastcamPds3NaifSpiceDriver(Cahvor, Framer, Pds3Label, NaifSpice, NoDistortion, Driver): @property def spacecraft_name(self): """ diff --git a/tests/pytests/test_cahvor_mixin.py b/tests/pytests/test_cahvor_mixin.py new file mode 100644 index 0000000000000000000000000000000000000000..7bd38f645446f98c5cf426816e749a37b7203fbf --- /dev/null +++ b/tests/pytests/test_cahvor_mixin.py @@ -0,0 +1,65 @@ +import unittest +from unittest.mock import PropertyMock, patch +import pytest +import pvl +import numpy as np + +import ale + +from ale.base.type_sensor import Cahvor + +def cahvor_camera_dict(): + camera_dict = {} + camera_dict['C'] = np.array([6.831825e-01, 5.243722e-01, -1.955875e+00]) + camera_dict['A'] = np.array([-3.655151e-01, 5.396012e-01, 7.584387e-01]) + camera_dict['H'] = np.array([-1.156881e+04, -7.518712e+03, 6.618359e+02]) + camera_dict['V'] = np.array([5.843885e+03, -8.213856e+03, 9.438374e+03]) + return camera_dict + + +class test_cahvor_sensor(unittest.TestCase): + + def setUp(self): + self.driver = Cahvor() + self.driver.focal_length = 100 + self.driver.ikid = -76220 + self.driver.sensor_frame_id = -76562 + self.driver.target_frame_id = 10014 + self.driver.center_ephemeris_time = 0 + self.driver.ephemeris_time = [0] + + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_compute_functions(self, cahvor_camera_dict): + np.testing.assert_almost_equal(self.driver.compute_h_s(), 13796.844341513603) + np.testing.assert_almost_equal(self.driver.compute_h_c(), 673.4306859859296) + np.testing.assert_almost_equal(self.driver.compute_v_s(), 13796.847423351614) + np.testing.assert_almost_equal(self.driver.compute_v_c(), 590.1933422831007) + + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_cahvor_model_elements(self, cahvor_camera_dict): + cahvor_matrix = self.driver.cahvor_rotation_matrix + np.testing.assert_allclose(cahvor_matrix, [[-0.42447558, -0.7572992, -0.49630475], + [ 0.73821222, 0.02793007, -0.67399009], + [ 0.52427398, -0.65247056, 0.54719189]]) + + @patch('ale.transformation.FrameChain.from_spice', return_value=ale.transformation.FrameChain()) + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_cahvor_frame_chain(self, cahvor_camera_dict, from_spice): + frame_chain = self.driver.frame_chain + assert len(frame_chain.nodes()) == 2 + assert -76220 in frame_chain.nodes() + assert -76562 in frame_chain.nodes() + from_spice.assert_called_with(center_ephemeris_time=0, ephemeris_times=[0], sensor_frame=-76220, target_frame=10014, nadir=False, exact_ck_times=False) + np.testing.assert_allclose(frame_chain[-76562][-76220]['rotation'].quat, [0.0100307131, -0.4757136116, 0.6970899144, 0.5363409323]) + + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_cahvor_detector_center_line(self, cahvor_camera_dict): + np.testing.assert_almost_equal(self.driver.detector_center_line, 590.1933422831007) + + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_cahvor_detector_center_sample(self, cahvor_camera_dict): + np.testing.assert_almost_equal(self.driver.detector_center_sample, 673.4306859859296) + + @patch("ale.base.type_sensor.Cahvor.cahvor_camera_dict", new_callable=PropertyMock, return_value=cahvor_camera_dict()) + def test_cahvor_pixel_size(self, cahvor_camera_dict): + assert self.driver.pixel_size == 0.007248034226138798 \ No newline at end of file diff --git a/tests/pytests/test_msl_drivers.py b/tests/pytests/test_msl_drivers.py index 4053cb269279dbb26bf99e5817a3ea0949118a92..7b58c1a3c10b13ba6c7c691cfc7e3aff8bdc84f2 100644 --- a/tests/pytests/test_msl_drivers.py +++ b/tests/pytests/test_msl_drivers.py @@ -1,7 +1,6 @@ import numpy as np import unittest -import ale from ale.drivers.msl_drivers import MslMastcamPds3NaifSpiceDriver from conftest import get_image_label @@ -33,10 +32,17 @@ class test_mastcam_pds_naif(unittest.TestCase): with patch('ale.drivers.msl_drivers.spice.bods2c', return_value=-76562) as bods2c: assert self.driver.sensor_frame_id == -76562 bods2c.assert_called_with("MSL_SITE_62") - - # uncomment once cahvor mixin is merged - # def test_focal2pixel_lines(self): - # np.testing.assert_allclose(self.driver.focal2pixel_lines, [0, 137968.44341513602, 0]) - - # def test_focal2pixel_samples(self): - # np.testing.assert_allclose(self.driver.focal2pixel_samples, [137968.44341513602, 0, 0]) + + def test_focal2pixel_lines(self): + with patch('ale.drivers.msl_drivers.spice.bods2c', new_callable=PropertyMock, return_value=-76220) as bods2c, \ + patch('ale.drivers.msl_drivers.spice.gdpool', new_callable=PropertyMock, return_value=[100]) as gdpool: + np.testing.assert_allclose(self.driver.focal2pixel_lines, [0, 137.96844341513602, 0]) + bods2c.assert_called_with('MSL_MASTCAM_RIGHT') + gdpool.assert_called_with('INS-76220_FOCAL_LENGTH', 0, 1) + + def test_focal2pixel_samples(self): + with patch('ale.drivers.msl_drivers.spice.bods2c', new_callable=PropertyMock, return_value=-76220) as bods2c, \ + patch('ale.drivers.msl_drivers.spice.gdpool', new_callable=PropertyMock, return_value=[100]) as gdpool: + np.testing.assert_allclose(self.driver.focal2pixel_samples, [137.96844341513602, 0, 0]) + bods2c.assert_called_with('MSL_MASTCAM_RIGHT') + gdpool.assert_called_with('INS-76220_FOCAL_LENGTH', 0, 1)