diff --git a/include/schaapcommon/fitters/gaussianfitter.h b/include/schaapcommon/fitters/gaussianfitter.h index 8e088d035efc4baa9dd04da443ca01f4f1581a1b..8dcf774262f7f6af689f57cb81a1c771a434130d 100644 --- a/include/schaapcommon/fitters/gaussianfitter.h +++ b/include/schaapcommon/fitters/gaussianfitter.h @@ -8,6 +8,23 @@ namespace schaapcommon { namespace fitters { + +struct Ellipse { + double major; + double minor; + double position_angle; +}; + +/** + * Deconvolve two Gaussian kernels. The result convolved with @p smallest + * results again in @p largest. If @p smallest is larger than @p largest, + * an ellipse is returned with all parameters set to NaN. + * + * As the output axes are returned with the same units as the input axes, + * the input may be both in FWHM or both in sigma. + */ +Ellipse DeconvolveGaussian(const Ellipse& largest, const Ellipse& smallest); + class GaussianFitter { public: void Fit2DGaussianCentred(const float* image, size_t width, size_t height, diff --git a/src/fitters/gaussianfitter.cc b/src/fitters/gaussianfitter.cc index 573e05375f441867083aff4500db9375347f4e66..f94a219b3cdbba58ab5f3ac02059ace8e2a66b19 100644 --- a/src/fitters/gaussianfitter.cc +++ b/src/fitters/gaussianfitter.cc @@ -5,6 +5,7 @@ #include <cmath> #include <iostream> +#include <limits> #include <gsl/gsl_vector.h> #include <gsl/gsl_multifit_nlin.h> @@ -22,7 +23,7 @@ namespace { const long double kSigmaToBeam = 2.0L * std::sqrt(2.0L * std::log(2.0L)); void ToAnglesAndFwhm(double sx, double sy, double beta, double& ellipse_major, - double& ellipse_minor, double& ellipse_phase_angle) { + double& ellipse_minor, double& ellipse_position_angle) { const double beta_factor = 1.0 - beta * beta; double cov[4]; cov[0] = sx * sx / beta_factor; @@ -40,11 +41,11 @@ void ToAnglesAndFwhm(double sx, double sy, double beta, double& ellipse_major, vec1[0] = vec2[0]; vec1[1] = vec2[1]; } - ellipse_phase_angle = -std::atan2(vec1[0], vec1[1]); + ellipse_position_angle = -std::atan2(vec1[0], vec1[1]); } else { ellipse_major = std::sqrt(std::fabs(sx)) * kSigmaToBeam; ellipse_minor = std::sqrt(std::fabs(sx)) * kSigmaToBeam; - ellipse_phase_angle = 0.0; + ellipse_position_angle = 0.0; } } @@ -54,7 +55,7 @@ void ToAnglesAndFwhm(double sx, double sy, double beta, double& ellipse_major, * @param y Y coordinate. * @param sx Sigma value for the x direction. * @param sy Sigma value for the y direction. - * @param beta Beta value. + * @param beta Beta value; beta = 2 cov(x, y) / (sx * sy) */ double GaussCentered(double x, double y, double sx, double sy, double beta) { return std::exp(-x * x / (2.0 * sx * sx) + beta * x * y / (sx * sy) - @@ -787,5 +788,58 @@ void GaussianFitter::Fit2DGaussianWithAmplitudeWithFloor( beam_major *= scale_factor_; beam_minor *= scale_factor_; } + +Ellipse DeconvolveGaussian(const Ellipse& a, const Ellipse& b) { + /** + * These calculations can be derived from the following three steps necessary + * to deconvolve two Gaussian kernels. First, calculating the covariance + * matrix of both ellipses, then applying the formula for the variance of the + * sum of two Gaussians a = b + c (solving for the covariance of c), followed + * by converting the covariance matrix back to ellipse parameters. + * These steps can then be compacted to avoid having to first calculate the + * full parameters of a and b. The covariance can be calculated as: + * var_x = semiMajorAxis² * cos(phi)² + semiMinorAxis² * sin(phi)² + * var_y = semiMajorAxis² * sin(phi)² + semiMinorAxis² * cos(phi)² + * cov_xy = (semiMajorAxis² - semiMinorAxis²) * sin(phi) * cos(phi) + * beta = r = cov_xy / (sxsy) + */ + const double cos_phi_a = std::cos(a.position_angle); + const double cos_square_a = cos_phi_a * cos_phi_a; + const double sin_phi_a = std::sin(a.position_angle); + const double sin_square_a = sin_phi_a * sin_phi_a; + const double cos_phi_b = std::cos(b.position_angle); + const double cos_square_b = cos_phi_b * cos_phi_b; + const double sin_phi_b = std::sin(b.position_angle); + const double sin_square_b = sin_phi_b * sin_phi_b; + const double a_maj_squared = a.major * a.major; + const double a_min_squared = a.minor * a.minor; + const double b_maj_squared = b.major * b.major; + const double b_min_squared = b.minor * b.minor; + const double cov_x = + (a_maj_squared * cos_square_a) + (a_min_squared * sin_square_a) - + (b_maj_squared * cos_square_b) - (b_min_squared * sin_square_b); + const double cov_y = + (a_maj_squared * sin_square_a) + (a_min_squared * cos_square_a) - + (b_maj_squared * sin_square_b) - (b_min_squared * cos_square_b); + const double cov_xy = + 2.0 * ((a_min_squared - a_maj_squared) * sin_phi_a * cos_phi_a - + (b_min_squared - b_maj_squared) * sin_phi_b * cos_phi_b); + const double d = cov_x + cov_y; + const double r = + std::sqrt((cov_x - cov_y) * (cov_x - cov_y) + cov_xy * cov_xy); + Ellipse result; + const double epsilon = (std::fabs(cov_x) + std::fabs(cov_y)) * 1e-5; + if (cov_x < -epsilon || cov_y < -epsilon || d < r) { + result.major = std::numeric_limits<double>::quiet_NaN(); + result.minor = std::numeric_limits<double>::quiet_NaN(); + result.position_angle = std::numeric_limits<double>::quiet_NaN(); + } else { + result.major = std::sqrt(0.5 * (d + r)); + result.minor = std::sqrt(0.5 * (d - r)); + result.position_angle = 0.5 * std::atan2(-cov_xy, cov_x - cov_y); + } + return result; +} + } // namespace fitters } // namespace schaapcommon diff --git a/src/fitters/test/tgaussianfitter.cc b/src/fitters/test/tgaussianfitter.cc index 73d99272adb4bfa2af7cbfe2d4a4890c97785607..0f8e3a6916cd84d27e61ea0074be406b87e2e00c 100644 --- a/src/fitters/test/tgaussianfitter.cc +++ b/src/fitters/test/tgaussianfitter.cc @@ -143,4 +143,80 @@ BOOST_AUTO_TEST_CASE(fit_small_beam) { 1.0e-4); } +BOOST_AUTO_TEST_CASE(deconvolve) { + using schaapcommon::fitters::DeconvolveGaussian; + using schaapcommon::fitters::Ellipse; + Ellipse result; + + // Evaluate all four kwadrants. + for (double phi = 0.0; phi < 2.0 * M_PI; phi += M_PI / 4.0) { + // Simple case: equal PAs and zero minor axis for second ellipse, basically + // a 1D deconvolution. + const double input_phi = 0.5 + phi; + result = DeconvolveGaussian(Ellipse{2.0, 1.0, input_phi}, + Ellipse{0.2, 0.0, input_phi}); + BOOST_CHECK_CLOSE_FRACTION(result.major, std::sqrt(2.0 * 2.0 - 0.2 * 0.2), + 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, 1.0, 1e-5); + double expected = input_phi; + while (expected > M_PI * 0.5) expected -= M_PI; + BOOST_CHECK_CLOSE_FRACTION(result.position_angle, expected, 1e-5); + + // Same as above, but rotate second ellipse by 180 deg (which should not + // change anything). + result = DeconvolveGaussian(Ellipse{2.0, 1.0, input_phi}, + Ellipse{0.2, 0.0, input_phi + M_PI}); + BOOST_CHECK_CLOSE_FRACTION(result.major, std::sqrt(2.0 * 2.0 - 0.2 * 0.2), + 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, 1.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.position_angle, expected, 1e-5); + } + + // Deconvolve with zero size ellipse: should return first ellipse. + result = DeconvolveGaussian(Ellipse{2.0, 1.0, 0.5}, Ellipse{0.0, 0.0, 0.1}); + BOOST_CHECK_CLOSE_FRACTION(result.major, std::sqrt(2.0 * 2.0 - 0.0 * 0.0), + 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, 1.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.position_angle, 0.5, 1e-5); + + // Equal sizes: should return zero-sized ellipse. + result = DeconvolveGaussian(Ellipse{2.0, 1.0, 0.5}, Ellipse{2.0, 1.0, 0.5}); + BOOST_CHECK_LT(std::abs(result.major), 1e-5); + BOOST_CHECK_LT(std::abs(result.minor), 1e-5); + // position angle is somewhat undefined, but should still be finite... + BOOST_CHECK(std::isfinite(result.position_angle)); + // ...and within bounds. + BOOST_CHECK_LE(result.position_angle, 2.0 * M_PI); + BOOST_CHECK_GE(result.position_angle, -2.0 * M_PI); + + // 90 degree different position angle. + result = + DeconvolveGaussian(Ellipse{5.0, 2.0, 0.0}, Ellipse{1.0, 0.0, M_PI * 0.5}); + BOOST_CHECK_CLOSE_FRACTION(result.major, 5.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, std::sqrt(2.0 * 2.0 - 1.0 * 1.0), + 1e-5); + BOOST_CHECK_LT(std::abs(result.position_angle), 1e-5); + + // Circular deconvolution. + result = DeconvolveGaussian(Ellipse{5.0, 5.0, -0.3}, Ellipse{3.0, 3.0, 0.7}); + BOOST_CHECK_CLOSE_FRACTION(result.major, 4.0, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, 4.0, 1e-5); + BOOST_CHECK(std::isfinite(result.position_angle)); + BOOST_CHECK_LE(result.position_angle, 2.0 * M_PI); + BOOST_CHECK_GE(result.position_angle, -2.0 * M_PI); + + // A complex case (numbers were calculated using the code, assuming it is + // correct). + result = DeconvolveGaussian(Ellipse{10.0, 8.0, 0.3}, Ellipse{7.0, 5.0, 1.4}); + BOOST_CHECK_CLOSE_FRACTION(result.major, 8.477876113222349, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.minor, 4.2574190079030156, 1e-5); + BOOST_CHECK_CLOSE_FRACTION(result.position_angle, 0.11532393547063115, 1e-5); + + // Overflow situation. + result = DeconvolveGaussian(Ellipse{3.0, 3.0, 0.0}, Ellipse{4.0, 0.0, 0.0}); + BOOST_CHECK(!std::isfinite(result.major)); + BOOST_CHECK(!std::isfinite(result.major)); + BOOST_CHECK(!std::isfinite(result.major)); +} + BOOST_AUTO_TEST_SUITE_END()