mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
imageTransforms_test.cpp
Go to the documentation of this file.
1/** \file imageTransforms_test.cpp
2 * \brief Tests image transformations.
3 */
4#include "../../catch2/catch.hpp"
5
6#include <array>
7#include <Eigen/Dense>
8#include <numbers>
9#include <vector>
10
11#define MX_NO_ERROR_REPORTS
12
14
17
19
20/** \cond
21 * Explicit instantiations compile-check the double-precision bilinear and cubic-convolution transform policies and
22 * emit their header-defined methods for coverage accounting in this test translation unit.
23 */
26/** \endcond */
27
28namespace
29{
30double imageMSE( const mx::improc::eigenImage<double> &a, const mx::improc::eigenImage<double> &b )
31{
32 return ( a - b ).square().mean();
33}
34} // namespace
35
36/** Verify direction and accuracy of various image shifts
37 *
38 * Tests image shifts by fractional pixels.
39 *
40 */
41/**
42 * \ingroup imageTransforms_unit_tests
43 */
44TEST_CASE( "Verify direction and accuracy of various image shifts", "[improc::imageTransforms]" )
45{
46 GIVEN( "a Gaussian image" )
47 {
48 WHEN( "shifting" )
49 {
50 mx::improc::eigenImage<double> im, shift, ref;
51
52 im.resize( 256, 256 );
53 shift.resize( im.rows(), im.cols() );
54 ref.resize( im.rows(), im.cols() );
55
56 // Use sigma = 8 to get a well oversampled image, making shifts more accurate
57 mx::math::func::gaussian2D<double>( im.data(), im.rows(), im.cols(), 0., 1.0, 127.5, 127.5, 8 );
58
60 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 127.0, 127.0, 8 );
61 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
62
64 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 128.0, 128.0, 8 );
65 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
66
68 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 128.5, 128.5, 8 );
69 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
70
72 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 128.0, 127.0, 8 );
73 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
74
76 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 127.2, 128.2, 8 );
77 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
78
80 mx::math::func::gaussian2D<double>( ref.data(), ref.rows(), ref.cols(), 0., 1.0, 128.8, 126.8, 8 );
81 REQUIRE_THAT( imageMSE( shift, ref ), Catch::Matchers::WithinAbs( 0.0, 1e-5 ) );
82 }
83 }
84}
85
86/** \brief Verifies the default single-precision cubic-convolution kernel and its complete image footprint.
87 *
88 * \ingroup imageTransforms_unit_tests
89 */
90TEST_CASE( "cubicConvolTransform produces normalized four-pixel kernels",
91 "[improc::cubicConvolTransform][improc::imageTransforms]" )
92{
94 constexpr Eigen::Index transformWidth = transformT::width;
95 constexpr Eigen::Index leftBuffer = transformT::lbuff;
96
97 transformT transform;
98
99 REQUIRE( transform.cubic == -0.5F );
100 REQUIRE( transformWidth == 4 );
101 REQUIRE( leftBuffer == 1 );
102
103 REQUIRE( transform.cubicConvolKernel( 0.0F ) == 1.0F );
104 REQUIRE( transform.cubicConvolKernel( 0.25F ) == 0.8671875F );
105 REQUIRE( transform.cubicConvolKernel( 0.5F ) == 0.5625F );
106 REQUIRE( transform.cubicConvolKernel( 0.75F ) == 0.2265625F );
107 REQUIRE( transform.cubicConvolKernel( 1.0F ) == 0.0F );
108 REQUIRE( transform.cubicConvolKernel( 1.25F ) == -0.0703125F );
109 REQUIRE( transform.cubicConvolKernel( 1.5F ) == -0.0625F );
110 REQUIRE( transform.cubicConvolKernel( 1.75F ) == -0.0234375F );
111 REQUIRE( transform.cubicConvolKernel( 2.0F ) == 0.0F );
112
113 const std::array<std::array<float, 2>, 4> phases{
114 { { 0.0F, 0.0F }, { 0.25F, 0.75F }, { 0.5F, 0.5F }, { 0.75F, 0.25F } } };
115
116 mx::improc::eigenImage<float> kernel( transformWidth, transformWidth );
117 for( const auto &phase : phases )
118 {
119 transform( kernel, phase[0], phase[1] );
120 REQUIRE( kernel.sum() == 1.0F );
121 }
122
123 const std::array<float, 4> quarterWeights{ -0.0703125F, 0.8671875F, 0.2265625F, -0.0234375F };
124 const std::array<float, 4> threeQuarterWeights{ -0.0234375F, 0.2265625F, 0.8671875F, -0.0703125F };
125
126 transform( kernel, 0.25F, 0.75F );
127 for( Eigen::Index row = 0; row < kernel.rows(); ++row )
128 {
129 for( Eigen::Index col = 0; col < kernel.cols(); ++col )
130 {
131 REQUIRE( kernel( row, col ) == quarterWeights[row] * threeQuarterWeights[col] );
132 }
133 }
134
136 const Eigen::Index firstAnchor = leftBuffer;
137 const Eigen::Index lastAnchor = constantImage.rows() - transformWidth + leftBuffer;
138 const std::array<Eigen::Index, 2> anchors{ firstAnchor, lastAnchor };
139
140 REQUIRE( firstAnchor - leftBuffer == 0 );
141 REQUIRE( lastAnchor - leftBuffer + transformWidth == constantImage.rows() );
142 REQUIRE( firstAnchor - 1 - leftBuffer < 0 );
143 REQUIRE( lastAnchor + 1 - leftBuffer + transformWidth > constantImage.rows() );
144
145 for( Eigen::Index rowAnchor : anchors )
146 {
147 for( Eigen::Index colAnchor : anchors )
148 {
149 transform( kernel, 0.25F, 0.75F );
150 const float sample = ( constantImage.block( rowAnchor - leftBuffer,
151 colAnchor - leftBuffer,
152 transformWidth,
153 transformWidth ) *
154 kernel )
155 .sum();
156 REQUIRE( sample == 3.25F );
157 }
158 }
159}
160
161/** \brief Verifies cubic-convolution image rotation direction, flux, allocation, and edge handling.
162 *
163 * \ingroup imageTransforms_unit_tests
164 */
165TEST_CASE( "imageRotate rotates an impulse counterclockwise", "[improc::imageRotate]" )
166{
168 image( 10, 16 ) = 1.0;
169
172 image,
173 0.5 * std::numbers::pi_v<double>,
175
176 REQUIRE( rotated.rows() == image.rows() );
177 REQUIRE( rotated.cols() == image.cols() );
178 REQUIRE( rotated( 16, 10 ) == Approx( 1.0 ) );
179 REQUIRE( rotated.sum() == Approx( 1.0 ).margin( 1e-12 ) );
180 REQUIRE( rotated( 0, 0 ) == 0.0 );
181}
An image cube with an Eigen API.
Declarations for utilities related to the Gaussian function.
Eigen::Array< scalarT, -1, -1 > eigenImage
Definition of the eigenImage type, which is an alias for Eigen::Array.
realT gaussian2D(const realT x, const realT y, const realT G0, const realT G, const realT x0, const realT y0, const realT sigma)
Find value at position (x,y) of the 2D arbitrarily-centered symmetric Gaussian.
Definition gaussian.hpp:126
TEST_CASE("Verify direction and accuracy of various image shifts", "[improc::imageTransforms]")
void imageRotate(arrT &transim, const arrT2 &im, floatT dq, transformT trans)
Rotate an image represented as an eigen array.
void imageShift(arrOutT &transim, const arrInT &im, floatT1 dx, floatT2 dy, transformT trans)
Shift an image.
Image interpolation and transformation.
Header for the image processing utilities.
Transformation by bi-linear interpolation.
Transformation by cubic convolution interpolation.