diff --git a/include/usgscsm/Distortion.h b/include/usgscsm/Distortion.h index 990f513617f0beda9ebd431719e9e02ce164477d..10c56691840b3a856255ee7727ac756786802d92 100644 --- a/include/usgscsm/Distortion.h +++ b/include/usgscsm/Distortion.h @@ -6,22 +6,24 @@ #include <tuple> #include <vector> -enum DistortionType { RADIAL, TRANSVERSE, KAGUYALISM, DAWNFC, LROLROCNAC, CAHVOR }; +// This should be synched with the enum in ale/Distortion.h +enum DistortionType { RADIAL, TRANSVERSE, KAGUYALISM, DAWNFC, LROLROCNAC, CAHVOR, + LUNARORBITER, RADTAN }; -// Transverse Distortion -void distortionJacobian(double x, double y, double *jacobian, - const std::vector<double> opticalDistCoeffs); +// Transverse distortion Jacobian +void transverseDistortionJacobian(double x, double y, double *jacobian, + std::vector<double> const& opticalDistCoeffs); void computeTransverseDistortion(double ux, double uy, double &dx, double &dy, - const std::vector<double> opticalDistCoeffs); + std::vector<double> const& opticalDistCoeffs); void removeDistortion(double dx, double dy, double &ux, double &uy, - const std::vector<double> opticalDistCoeffs, + std::vector<double> const& opticalDistCoeffs, DistortionType distortionType, const double tolerance = 1.0E-6); void applyDistortion(double ux, double uy, double &dx, double &dy, - const std::vector<double> opticalDistCoeffs, + std::vector<double> const& opticalDistCoeffs, DistortionType distortionType, const double desiredPrecision = 1.0E-6, const double tolerance = 1.0E-6); diff --git a/include/usgscsm/Utilities.h b/include/usgscsm/Utilities.h index f1106299a48f361967f5e81c213e2f2c37b5a229..7247560973806c7233592436e9a7450670a22004 100644 --- a/include/usgscsm/Utilities.h +++ b/include/usgscsm/Utilities.h @@ -65,6 +65,15 @@ void lagrangeInterp(const int &numTime, const double *valueArray, double brentRoot(double lowerBound, double upperBound, std::function<double(double)> func, double epsilon = 1e-10); +// Use the Newton-Raphson method undistort a pixel (dx, dy), producing (ux, uy). +void newtonRaphson(double dx, double dy, double &ux, double &uy, + std::vector<double> const& opticalDistCoeffs, + DistortionType distortionType, const double tolerance, + std::function<void(double, double, double &, double &, + std::vector<double> const&)> distortionFunction, + std::function<void(double, double, double *, + std::vector<double> const&)> distortionJacobian); + // Evaluate a polynomial function. // Coefficients should be ordered least order to greatest I.E. {1, 2, 3} is 1 + // 2x + 3x^2 diff --git a/src/Distortion.cpp b/src/Distortion.cpp index 9684822a0dc35ce04add4056036f4c1e601f6788..38f397ac7d45176f326e2587989ca3333b040abe 100644 --- a/src/Distortion.cpp +++ b/src/Distortion.cpp @@ -1,10 +1,12 @@ #include "Distortion.h" +#include "Utilities.h" #include <Error.h> #include <string> -void distortionJacobian(double x, double y, double *jacobian, - const std::vector<double> opticalDistCoeffs) { +// Jacobian for Transverse distortion +void transverseDistortionJacobian(double x, double y, double *jacobian, + std::vector<double> const& opticalDistCoeffs) { double d_dx[10]; d_dx[0] = 0; d_dx[1] = 1; @@ -57,7 +59,7 @@ void distortionJacobian(double x, double y, double *jacobian, * tuple */ void computeTransverseDistortion(double ux, double uy, double &dx, double &dy, - const std::vector<double> opticalDistCoeffs) { + std::vector<double> const& opticalDistCoeffs) { double f[10]; f[0] = 1; f[1] = ux; @@ -82,8 +84,77 @@ void computeTransverseDistortion(double ux, double uy, double &dx, double &dy, } } +// Compute distorted focal plane coordinates given undistorted coordinates. Use +// the radial-tangential distortion model with 5 coefficients (k1, k2, k3 for +// radial distortion, and p1, p2 for tangential distortion). This was tested to +// give the same results as the OpenCV distortion model, by invoking the +// function cv::projectPoints() (with zero rotation, zero translation, and +// identity camera matrix). The parameters are stored in opticalDistCoeffs +// in the order: [k1, k2, p1, p2, k3]. +void computeRadTanDistortion(double ux, double uy, double &dx, double &dy, + std::vector<double> const& opticalDistCoeffs) { + + if (opticalDistCoeffs.size() != 5) { + csm::Error::ErrorType errorType = csm::Error::INDEX_OUT_OF_RANGE; + std::string message = + "Distortion coefficients for the radtan distortion model must be of size 5, " + "in the order k1, k2, p1, p2, k3. Got: " + std::to_string(opticalDistCoeffs.size()); + std::string function = "computeRadTanDistortion"; + throw csm::Error(errorType, message, function); + } + + // Shorten notation + double x = ux, y = uy; + double k1 = opticalDistCoeffs[0]; + double k2 = opticalDistCoeffs[1]; + double p1 = opticalDistCoeffs[2]; + double p2 = opticalDistCoeffs[3]; + double k3 = opticalDistCoeffs[4]; + + double r2 = (x * x) + (y * y); + + dx = x * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)); + + dy = y * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y); +} + +// Compute the jacobian for radtan distortion +void radTanDistortionJacobian(double x, double y, double *jacobian, + std::vector<double> const& opticalDistCoeffs) { + + double k1 = opticalDistCoeffs[0]; + double k2 = opticalDistCoeffs[1]; + double p1 = opticalDistCoeffs[2]; + double p2 = opticalDistCoeffs[3]; + double k3 = opticalDistCoeffs[4]; + + double r2 = x * x + y * y; + double dr2dx = 2.0 * x; + double dr2dy = 2.0 * y; + + // dfx / dx + jacobian[0] = (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + x * (k1 * dr2dx + k2 * dr2dx * 2.0 * r2 + k3 * dr2dx * 3.0 * r2 * r2) + + 2.0 * p1 * y + p2 * (dr2dx + 4.0 * x); + + // dfx / dy + jacobian[1] = x * (k1 * dr2dy + k2 * dr2dy * 2.0 * r2 + k3 * dr2dy * 3.0 * r2 * r2) + + 2.0 * p1 * x + p2 * dr2dy; + + // dfy / dx + jacobian[2] = y * (k1 * dr2dx + k2 * dr2dx * 2.0 * r2 + k3 * dr2dx * 3.0 * r2 * r2) + + (p1 * dr2dx + 2.0 * p2 * y); + + // dfy / dy + jacobian[3] = (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + y * (k1 * dr2dy + k2 * dr2dy * 2.0 * r2 + k3 * dr2dy * 3.0 * r2 * r2) + + p1 * (dr2dy + 4.0 * y) + 2.0 * p2 * x; +} + void removeDistortion(double dx, double dy, double &ux, double &uy, - const std::vector<double> opticalDistCoeffs, + std::vector<double> const& opticalDistCoeffs, DistortionType distortionType, const double tolerance) { ux = dx; uy = dy; @@ -109,55 +180,8 @@ void removeDistortion(double dx, double dy, double &ux, double &uy, // Solve the distortion equation using the Newton-Raphson method. // Set the error tolerance to about one millionth of a NAC pixel. // The maximum number of iterations of the Newton-Raphson method. - const int maxTries = 20; - - double x; - double y; - double fx; - double fy; - double jacobian[4]; - - // Initial guess at the root - x = dx; - y = dy; - - computeTransverseDistortion(x, y, fx, fy, opticalDistCoeffs); - - for (int count = 1; - ((fabs(fx) + fabs(fy)) > tolerance) && (count < maxTries); count++) { - computeTransverseDistortion(x, y, fx, fy, opticalDistCoeffs); - - fx = dx - fx; - fy = dy - fy; - - distortionJacobian(x, y, jacobian, opticalDistCoeffs); - - // Jxx * Jyy - Jxy * Jyx - double determinant = - jacobian[0] * jacobian[3] - jacobian[1] * jacobian[2]; - if (fabs(determinant) < 1E-6) { - ux = x; - uy = y; - // - // Near-zero determinant. Add error handling here. - // - //-- Just break out and return with no convergence - return; - } - - x = x + (jacobian[3] * fx - jacobian[1] * fy) / determinant; - y = y + (jacobian[0] * fy - jacobian[2] * fx) / determinant; - } - - if ((fabs(fx) + fabs(fy)) <= tolerance) { - // The method converged to a root. - ux = x; - uy = y; - - return; - } - // Otherwise method did not converge to a root within the maximum - // number of iterations + newtonRaphson(dx, dy, ux, uy, opticalDistCoeffs, distortionType, tolerance, + computeTransverseDistortion, transverseDistortionJacobian); } break; case KAGUYALISM: { @@ -314,11 +338,22 @@ void removeDistortion(double dx, double dy, double &ux, double &uy, } } break; + + // Compute undistorted focal plane coordinate given distorted coordinates + // with the radtan model. See computeRadTanDistortion() for more details. + case RADTAN: + { + newtonRaphson(dx, dy, ux, uy, opticalDistCoeffs, distortionType, tolerance, + computeRadTanDistortion, radTanDistortionJacobian); + + } + break; + } } void applyDistortion(double ux, double uy, double &dx, double &dy, - const std::vector<double> opticalDistCoeffs, + std::vector<double> const& opticalDistCoeffs, DistortionType distortionType, const double desiredPrecision, const double tolerance) { dx = ux; @@ -451,9 +486,9 @@ void applyDistortion(double ux, double uy, double &dx, double &dy, } } break; - // The dawn distortion model is "reversed" from other distortion models so - // the apply function computes distorted coordinates as a - // fn(undistorted coordinates) + // The dawn distortion model is "reversed" from other distortion models. + // The apply function computes distorted coordinates as a + // function of undistorted coordinates. case DAWNFC: { double r2; @@ -594,5 +629,14 @@ void applyDistortion(double ux, double uy, double &dx, double &dy, } } break; + + // Compute distorted focal plane coordinate given undistorted coordinates + // with the RADTAN model. See computeRadTanDistortion() for more details. + case RADTAN: + { + computeRadTanDistortion(ux, uy, dx, dy, opticalDistCoeffs); + } + break; + } } diff --git a/src/Utilities.cpp b/src/Utilities.cpp index 46be2632bc17e161842817c755119dd3779073c5..035d46060f25e9e4ea55e7b4e916166ffee9cdf9 100644 --- a/src/Utilities.cpp +++ b/src/Utilities.cpp @@ -401,6 +401,57 @@ double brentRoot(double lowerBound, double upperBound, return nextPoint; } +// Use the Newton-Raphson method undistort a pixel (dx, dy), producing (ux, uy). +void newtonRaphson(double dx, double dy, double &ux, double &uy, + std::vector<double> const& opticalDistCoeffs, + DistortionType distortionType, const double tolerance, + std::function<void(double, double, double &, double &, + std::vector<double> const&)> distortionFunction, + std::function<void(double, double, double *, + std::vector<double> const&)> distortionJacobian) { + + const int maxTries = 20; + + double x, y, fx, fy, jacobian[4]; + + // Initial guess for the root + x = dx; + y = dy; + + distortionFunction(x, y, fx, fy, opticalDistCoeffs); + + for (int count = 1; + ((fabs(fx) + fabs(fy)) > tolerance) && (count < maxTries); count++) { + distortionFunction(x, y, fx, fy, opticalDistCoeffs); + + fx = dx - fx; + fy = dy - fy; + + distortionJacobian(x, y, jacobian, opticalDistCoeffs); + + // Jxx * Jyy - Jxy * Jyx + double determinant = + jacobian[0] * jacobian[3] - jacobian[1] * jacobian[2]; + if (fabs(determinant) < 1e-6) { + ux = x; + uy = y; + // Near-zero determinant. Cannot continue. Return most recent result. + return; + } + + x = x + (jacobian[3] * fx - jacobian[1] * fy) / determinant; + y = y + (jacobian[0] * fy - jacobian[2] * fx) / determinant; + } + + if ((fabs(fx) + fabs(fy)) <= tolerance) { + // The method converged to a root. + ux = x; + uy = y; + + return; + } +} + double evaluatePolynomial(const std::vector<double> &coeffs, double x) { if (coeffs.empty()) { throw std::invalid_argument("Polynomial coeffs must be non-empty."); @@ -1009,9 +1060,9 @@ double getSemiMinorRadius(json isd, csm::WarningList *list) { // type. Defaults to transverse DistortionType getDistortionModel(json isd, csm::WarningList *list) { try { - json distoriton_subset = isd.at("optical_distortion"); + json distortion_subset = isd.at("optical_distortion"); - json::iterator it = distoriton_subset.begin(); + json::iterator it = distortion_subset.begin(); std::string distortion = (std::string)it.key(); @@ -1025,6 +1076,12 @@ DistortionType getDistortionModel(json isd, csm::WarningList *list) { return DistortionType::DAWNFC; } else if (distortion.compare("lrolrocnac") == 0) { return DistortionType::LROLROCNAC; + } else if (distortion.compare("cahvor") == 0) { + return DistortionType::CAHVOR; + } else if (distortion.compare("lunarorbiter") == 0) { + return DistortionType::LUNARORBITER; + } else if (distortion.compare("radtan") == 0) { + return DistortionType::RADTAN; } } catch (...) { if (list) { @@ -1054,6 +1111,10 @@ DistortionType getDistortionModel(int aleDistortionModel, return DistortionType::LROLROCNAC; }else if (aleDistortionType == ale::DistortionType::CAHVOR) { return DistortionType::CAHVOR; + }else if (aleDistortionType == ale::DistortionType::LUNARORBITER) { + return DistortionType::LUNARORBITER; + }else if (aleDistortionType == ale::DistortionType::RADTAN) { + return DistortionType::RADTAN; } } catch (...) { if (list) { @@ -1212,7 +1273,26 @@ std::vector<double> getDistortionCoeffs(json isd, csm::WarningList *list) { coefficients = std::vector<double>(6, 0.0); } } break; + case DistortionType::RADTAN: { + try { + coefficients = isd.at("optical_distortion") + .at("radtan") + .at("coefficients") + .get<std::vector<double>>(); + + return coefficients; + } catch (...) { + if (list) { + list->push_back(csm::Warning( + csm::Warning::DATA_NOT_AVAILABLE, + "Could not parse the radtan distortion model coefficients.", + "Utilities::getDistortion()")); + } + coefficients = std::vector<double>(5, 0.0); + } + } break; } + if (list) { list->push_back( csm::Warning(csm::Warning::DATA_NOT_AVAILABLE, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8922583af27f9c0071be37c0469f47aa16e08684..a60dfc7887207b60bfb19bba9fe1184787121bb9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,7 +33,6 @@ add_test(NAME test_usgscsm_cam_test_load_state COMMAND usgscsm_cam_test --model model_state.json --output-model-state model_state2.json WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests) - # 3. Save model state from .sup file add_test(NAME test_usgscsm_cam_test_save_sup_state COMMAND usgscsm_cam_test --model data/gxp_model_file.sup --output-model-state gxp_model_state.json @@ -43,4 +42,9 @@ add_test(NAME test_usgscsm_cam_test_load_sup_state COMMAND usgscsm_cam_test --model gxp_model_state.json --output-model-state gxp_model_state2.json WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests) +# 5. Test the radtan model (this distortion model gets added to a DawnFC sensor) +add_test(NAME test_usgscsm_cam_test_radtan + COMMAND usgscsm_cam_test --model data/dawnfc_radtan.json --sample-rate 100 --output-model-state dawnfc_radtan_model_state.json + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests) + gtest_discover_tests(runCSMCameraModelTests WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests) diff --git a/tests/DistortionTests.cpp b/tests/DistortionTests.cpp index bfee70e4eb58e1a91f7466c911aa91f458f19dc1..bda373bd24c1e89c64e4347e8c431a34c9b15a62 100644 --- a/tests/DistortionTests.cpp +++ b/tests/DistortionTests.cpp @@ -19,8 +19,8 @@ TEST_P(ImageCoordParameterizedTest, JacobianTest) { csm::ImageCoord imagePt = GetParam(); double jacobian[4]; - distortionJacobian(imagePt.samp, imagePt.line, jacobian, - transverseDistortionCoeffs); + transverseDistortionJacobian(imagePt.samp, imagePt.line, jacobian, + transverseDistortionCoeffs); // Jxx * Jyy - Jxy * Jyx double determinant = @@ -36,8 +36,8 @@ TEST(Transverse, Jacobian1) { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0}; double jacobian[4]; - distortionJacobian(imagePt.samp, imagePt.line, jacobian, - transverseDistortionCoeffs); + transverseDistortionJacobian(imagePt.samp, imagePt.line, jacobian, + transverseDistortionCoeffs); EXPECT_NEAR(jacobian[0], 56.25, 1e-8); EXPECT_NEAR(jacobian[1], 112.5, 1e-8); @@ -389,3 +389,28 @@ TEST_P(CoeffOffsetParameterizedTest, InverseOnesCoeffsCahvorTest) EXPECT_NEAR(dx, 4, 1e-8); EXPECT_NEAR(dy, 0, 1e-8); } + +INSTANTIATE_TEST_SUITE_P(RadTanInversionTest, RadTanTest, + ::testing::Values(std::vector<double>(0, 0))); + +TEST_P(RadTanTest, RadTanInversionTest) +{ + + // Initialize radtan distortion coefficients (k1, k2, p1, p2, k3) + std::vector<double> distCoeffs= {0.000031, -0.000056, 1.3e-5, -1.7e-6, 2.9e-8}; + + double ux = 5.0, uy = 6.0; + + // Compute distortion + double dx, dy; + applyDistortion(ux, uy, dx, dy, distCoeffs, DistortionType::RADTAN, 1e-8, 1e-8); + + // Remove distortion (undistort) + double ux2, uy2; + removeDistortion(dx, dy, ux2, uy2, distCoeffs, DistortionType::RADTAN, 1e-8); + + EXPECT_NEAR(dx, 4.0010785450000004, 1e-8); + EXPECT_NEAR(dy, 4.8022116940000013, 1e-8); + EXPECT_NEAR(ux2, ux, 1e-8); + EXPECT_NEAR(uy2, uy, 1e-8); +} diff --git a/tests/Fixtures.h b/tests/Fixtures.h index 2b5b053c537467c052dab6d212a1bde8f3c6ed7f..36c5e0ad6d2fbcf7e5aa226bff70933174e95182 100644 --- a/tests/Fixtures.h +++ b/tests/Fixtures.h @@ -180,6 +180,9 @@ class ImageCoordParameterizedTest class CoeffOffsetParameterizedTest : public ::testing::TestWithParam<std::vector<double>> {}; +class RadTanTest + : public ::testing::TestWithParam<std::vector<double>> {}; + class FramerParameterizedTest : public ::testing::TestWithParam<csm::ImageCoord> { protected: diff --git a/tests/data/dawnfc_radtan.json b/tests/data/dawnfc_radtan.json new file mode 100644 index 0000000000000000000000000000000000000000..22a5d9f8e3bf12f059cd2fdb36310016a755a039 --- /dev/null +++ b/tests/data/dawnfc_radtan.json @@ -0,0 +1,272 @@ +{ + "isis_camera_version": 2, + "image_lines": 1024, + "image_samples": 1024, + "name_platform": "DAWN", + "name_sensor": "FRAMING CAMERA 2", + "reference_height": { + "maxheight": 1000, + "minheight": -1000, + "unit": "m" + }, + "name_model": "USGS_ASTRO_FRAME_SENSOR_MODEL", + "center_ephemeris_time": 366389046.66804266, + "radii": { + "semimajor": 289.0, + "semiminor": 229.0, + "unit": "km" + }, + "body_rotation": { + "time_dependent_frames": [ + 2000004, + 1 + ], + "ck_table_start_time": 366389046.66804266, + "ck_table_end_time": 366389046.66804266, + "ck_table_original_size": 1, + "ephemeris_times": [ + 366389046.66804266 + ], + "quaternions": [ + [ + -0.6516990183788688, + 0.4029902779213469, + -0.03888879053873084, + 0.641385131816547 + ] + ], + "angular_velocities": [ + [ + 0.00015233084023562592, + -0.00018790495501866476, + 0.00021960595804307437 + ] + ], + "reference_frame": 1 + }, + "instrument_pointing": { + "time_dependent_frames": [ + -203120, + -203000, + 1 + ], + "ck_table_start_time": 366389046.66804266, + "ck_table_end_time": 366389046.66804266, + "ck_table_original_size": 1, + "ephemeris_times": [ + 366389046.66804266 + ], + "quaternions": [ + [ + 0.23666091236048983, + -0.2808008347085259, + -0.9120764329484655, + 0.18237073298010004 + ] + ], + "angular_velocities": [ + [ + -2.4291478000490343e-05, + -8.625645496100273e-06, + 8.501230573671002e-06 + ] + ], + "reference_frame": 1, + "constant_frames": [ + -203121, + -203120 + ], + "constant_rotation": [ + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + "naif_keywords": { + "BODY2000004_RADII": [ + 289.0, + 280.0, + 229.0 + ], + "BODY_FRAME_CODE": 2000004, + "BODY_CODE": 2000004, + "FRAME_-203121_CLASS_ID": -203121.0, + "TKFRAME_-203121_ANGLES": [ + 0.0, + 0.0, + 0.0 + ], + "INS-203121_PIXEL_SAMPLES": 1024.0, + "FRAME_-203121_CENTER": -203.0, + "INS-203121_FOV_ANGULAR_SIZE": [ + 0.09548, + 0.09542 + ], + "INS-203121_RAD_DIST_COEFF": 8.4e-06, + "INS-203121_FOV_ANGLE_UNITS": "DEGREES", + "TKFRAME_-203121_AXES": [ + 1.0, + 2.0, + 3.0 + ], + "TKFRAME_-203121_SPEC": "ANGLES", + "INS-203121_IFOV": [ + 9.3242e-05, + 9.3184e-05 + ], + "FRAME_-203121_NAME": "DAWN_FC2_FILTER_1", + "INS-203121_F/RATIO": 7.5, + "INS-203121_EFF_WAVELENGTH": 732.0, + "INS-203121_PIXEL_SIZE": [ + 14.004, + 13.995 + ], + "INS-203121_BANDCENTER": 735.0, + "INS-203121_FOV_CLASS_SPEC": "ANGLES", + "FRAME_-203121_CLASS": 4.0, + "INS-203121_FOV_REF_VECTOR": [ + 1.0, + 0.0, + 0.0 + ], + "INS-203121_FOV_CROSS_ANGLE": 2.7335816, + "INS-203121_FOV_REF_ANGLE": 2.7353005, + "INS-203121_BANDWIDTH": 682.0, + "INS-203121_CCD_CENTER": [ + 511.5, + 511.5 + ], + "TKFRAME_-203121_UNITS": "DEGREES", + "INS-203121_BORESIGHT": [ + 0.0, + 0.0, + 150.07 + ], + "INS-203121_PIXEL_LINES": 1024.0, + "INS-203121_FOV_SHAPE": "RECTANGLE", + "INS-203121_FOCAL_LENGTH": 150.07, + "INS-203121_ITRANSL": [ + 0.0, + 0.0, + 71.42857142857143 + ], + "INS-203121_TRANSX": [ + 0.0, + 0.0140088, + 0.0 + ], + "INS-203121_TRANSY": [ + 0.0, + 0.0, + 0.014 + ], + "INS-203121_ITRANSS": [ + 0.0, + 71.38370167323397, + 0.0 + ], + "TKFRAME_-203121_RELATIVE": "DAWN_FC2", + "INS-203121_FOV_FRAME": "DAWN_FC2", + "BODY2000004_LONG_AXIS": 0.0, + "FRAME_2000004_CENTER": 2000004.0, + "FRAME_2000004_CLASS_ID": 2000004.0, + "BODY2000004_POLE_DEC": [ + 42.235, + 0.0, + 0.0 + ], + "BODY2000004_PM": [ + 285.39, + 1617.3329428, + 0.0 + ], + "BODY2000004_POLE_RA": [ + 309.031, + 0.0, + 0.0 + ], + "OBJECT_2000004_FRAME": "VESTA_FIXED", + "FRAME_2000004_CLASS": 2.0, + "FRAME_2000004_NAME": "VESTA_FIXED" + }, + "detector_sample_summing": 1, + "detector_line_summing": 1, + "focal_length_model": { + "focal_length": 150.07 + }, + "detector_center": { + "line": 512.0, + "sample": 512.0 + }, + "starting_detector_line": 0, + "starting_detector_sample": 0, + "focal2pixel_lines": [ + 0.0, + 0.0, + 71.40816909454442 + ], + "focal2pixel_samples": [ + 0.0, + 71.40816909454442, + 0.0 + ], + "optical_distortion": { + "radtan": { + "coefficients": [ + 8.4e-06, 0, 0, 0, 0 + ] + } + }, + "instrument_position": { + "spk_table_start_time": 366389046.66804266, + "spk_table_end_time": 366389046.66804266, + "spk_table_original_size": 1, + "ephemeris_times": [ + 366389046.66804266 + ], + "positions": [ + [ + -980.0261618498736, + 1388.7080325894044, + 2466.5530852201077 + ] + ], + "velocities": [ + [ + -0.038273210022520744, + 0.04959477604204062, + -0.04302704368708845 + ] + ], + "reference_frame": 1 + }, + "sun_position": { + "spk_table_start_time": 366389046.66804266, + "spk_table_end_time": 366389046.66804266, + "spk_table_original_size": 1, + "ephemeris_times": [ + 366389046.66804266 + ], + "positions": [ + [ + -235084739.21033305, + 210948757.04546872, + 114748421.64433399 + ] + ], + "velocities": [ + [ + -15.345047951161861, + -12.985834298998117, + -3.1660155049823224 + ] + ], + "reference_frame": 1 + } +}