From d5c49d98b07b285246c42de6a4bc895d46b19129 Mon Sep 17 00:00:00 2001 From: Oleg Alexandrov <oleg.alexandrov@gmail.com> Date: Mon, 6 Nov 2023 18:21:37 -0800 Subject: [PATCH] Implement radtan distortion and undistortion --- include/usgscsm/Distortion.h | 8 +- src/Distortion.cpp | 222 +++++++++++++++++++++++++++++++++-- tests/DistortionTests.cpp | 33 +++++- tests/Fixtures.h | 3 + 4 files changed, 248 insertions(+), 18 deletions(-) diff --git a/include/usgscsm/Distortion.h b/include/usgscsm/Distortion.h index 990f513..7561816 100644 --- a/include/usgscsm/Distortion.h +++ b/include/usgscsm/Distortion.h @@ -6,11 +6,11 @@ #include <tuple> #include <vector> -enum DistortionType { RADIAL, TRANSVERSE, KAGUYALISM, DAWNFC, LROLROCNAC, CAHVOR }; +enum DistortionType { RADIAL, TRANSVERSE, KAGUYALISM, DAWNFC, LROLROCNAC, CAHVOR, 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, + const std::vector<double> opticalDistCoeffs); void computeTransverseDistortion(double ux, double uy, double &dx, double &dy, const std::vector<double> opticalDistCoeffs); diff --git a/src/Distortion.cpp b/src/Distortion.cpp index a40ae12..5971501 100644 --- a/src/Distortion.cpp +++ b/src/Distortion.cpp @@ -3,8 +3,9 @@ #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, + const std::vector<double> opticalDistCoeffs) { double d_dx[10]; d_dx[0] = 0; d_dx[1] = 1; @@ -82,6 +83,138 @@ 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. " + "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; + +#if 0 +// Check the partial derivatives with numerical differentiation + { + double eps = 1e-4; + double y0 = y; + y = y0 + eps; + r2 = (x * x) + (y * y); + + double dx1 = x * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)); + + double dy1 = y * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y); + + y = y0 - eps; + r2 = (x * x) + (y * y); + + double dx2 = x * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)); + + double dy2 = y * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y); + + std::cout << "--numerical1 is " << (dx1-dx2)/(2*eps) << " " << jacobian[1] << std::endl; + std::cout << "diff1 is " << (dx1-dx2)/(2*eps) - jacobian[1] << std::endl; + + std::cout << "--numerical3 is " << (dy1-dy2)/(2*eps) << " " << jacobian[3] << std::endl; + std::cout << "diff3 is " << (dy1-dy2)/(2*eps) - jacobian[3] << std::endl; + + y = y0; + } + + { + double eps = 1e-4; + double x0 = x; + x = x0 + eps; + r2 = (x * x) + (y * y); + + double dx1 = x * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)); + + double dy1 = y * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y); + + x = x0 - eps; + r2 = (x * x) + (y * y); + + double dx2 = x * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (2.0 * p1 * x * y + p2 * (r2 + 2.0 * x * x)); + + double dy2 = y * (1.0 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2) + + (p1 * (r2 + 2.0 * y * y) + 2.0 * p2 * x * y); + + std::cout << "--numerical0 is " << (dx1-dx2)/(2*eps) << " " << jacobian[0] << std::endl; + std::cout << "diff0 is " << (dx1-dx2)/(2*eps) - jacobian[0] << std::endl; + + std::cout << "--numerical2 is " << (dy1-dy2)/(2*eps) << " " << jacobian[2] << std::endl; + std::cout << "diff2 is " << (dy1-dy2)/(2*eps) - jacobian[2] << std::endl; + + x = x0; + } +#endif +} + void removeDistortion(double dx, double dy, double &ux, double &uy, const std::vector<double> opticalDistCoeffs, DistortionType distortionType, const double tolerance) { @@ -132,7 +265,7 @@ void removeDistortion(double dx, double dy, double &ux, double &uy, fx = dx - fx; fy = dy - fy; - distortionJacobian(x, y, jacobian, opticalDistCoeffs); + transverseDistortionJacobian(x, y, jacobian, opticalDistCoeffs); // Jxx * Jyy - Jxy * Jyx double determinant = @@ -316,6 +449,66 @@ 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: + { + // 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; + + computeRadTanDistortion(x, y, fx, fy, opticalDistCoeffs); + + for (int count = 1; + ((fabs(fx) + fabs(fy)) > tolerance) && (count < maxTries); count++) { + computeRadTanDistortion(x, y, fx, fy, opticalDistCoeffs); + + fx = dx - fx; + fy = dy - fy; + + radTanDistortionJacobian(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 + } + break; + } } @@ -327,7 +520,7 @@ void applyDistortion(double ux, double uy, double &dx, double &dy, dy = uy; switch (distortionType) { - // Compute undistorted focal plane coordinate given a distorted + // Compute distorted focal plane coordinate given undistorted // focal plane coordinate. This case works by iteratively adding distortion // until the new distorted point, r, undistorts to within a tolerance of the // original point, rp. @@ -454,9 +647,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; @@ -468,7 +661,7 @@ void applyDistortion(double ux, double uy, double &dx, double &dy, // The LRO LROC NAC distortion model uses an iterative approach to go from // undistorted x,y to distorted x,y - // Algorithum adapted from ISIS3 LRONarrowAngleDistortionMap.cpp + // Algorithm adapted from ISIS3 LRONarrowAngleDistortionMap.cpp case LROLROCNAC: { double yt = uy; @@ -491,9 +684,9 @@ void applyDistortion(double ux, double uy, double &dx, double &dy, double dk1 = opticalDistCoeffs[0]; - // Owing to the odd distotion model employed in this senser if |y| is > + // Owing to the odd distortion model employed in this sensor if |y| is > // 116.881145553046 then there is no root to find. Further, the greatest - // y that any measure on the sensor will acutally distort to is less + // y that any measure on the sensor will actually distort to is less // than 20. Thus, if any distorted measure is greater that that skip the // iterations. The points isn't in the cube, and exactly how far outside // the cube is irrelevant. Just let the camera model know its not in the @@ -597,5 +790,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/tests/DistortionTests.cpp b/tests/DistortionTests.cpp index 1c44432..3cbec40 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); @@ -380,3 +380,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 2b5b053..36c5e0a 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: -- GitLab