mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
imageFilters_test.cpp
Go to the documentation of this file.
1/** \file imageFilters_test.cpp
2 * \brief Tests of image filtering and smoothing utilities.
3 */
4#include "../../catch2/catch.hpp"
5
6#include <cmath>
7#include <limits>
8#include <vector>
9#include <Eigen/Dense>
10
13
14/** \cond
15 * Explicit instantiations compile-check the default Gaussian and azimuthal box kernels with the canonical
16 * double-precision Eigen image and emit their header-defined methods for coverage accounting in this test
17 * translation unit.
18 */
21/** \endcond */
22
23namespace unitTest
24{
25namespace improcTest
26{
27namespace imageFiltersTest
28{
29
30/** \cond */
31template <typename arrayT>
32bool arrayAllFinite( const arrayT &array )
33{
34 for( int column = 0; column < array.cols(); ++column )
35 {
36 for( int row = 0; row < array.rows(); ++row )
37 {
38 if( !mx::math::isFinite( array( row, column ) ) )
39 {
40 return false;
41 }
42 }
43 }
44 return true;
45}
46/** \endcond */
47
48/// Verify precalcKernel returns the same Gaussian and azimuthal kernels as their production generators.
49/** Exercises mx::improc::precalcKernel::setKernel against mx::improc::gaussKernel::setKernel and
50 * mx::improc::azBoxKernel::setKernel.
51 */
52/**
53 * \ingroup imageFilters_unit_tests
54 */
55TEST_CASE( "precalcKernel caches production kernel values", "[improc::precalcKernel]" )
56{
57 SECTION( "with Gaussian kernel" )
58 {
60
61 kernelT kernel( 2 );
62 mx::improc::precalcKernel<kernelT> pck( kernel, 8, 8, 3.5, 3.5 );
63
64 REQUIRE( pck.m_kernels.size() == 8 * 8 );
65
66 mx::improc::eigenImage<float> generatedKernel, cachedKernel;
67 for( int column = 0; column < 8; ++column )
68 {
69 for( int row = 0; row < 8; ++row )
70 {
71 REQUIRE( kernel.setKernel( row - 3.5, column - 3.5, generatedKernel ) == mx::error_t::noerror );
72 REQUIRE( pck.setKernel( row - 3.5, column - 3.5, cachedKernel ) == mx::error_t::noerror );
73 REQUIRE( ( generatedKernel == cachedKernel ).all() );
74 }
75 }
76 }
77
78 SECTION( "with azBoxKernel kernel" )
79 {
81
82 kernelT kernel( 3, 5 );
83 mx::improc::precalcKernel<kernelT> pck( kernel, 8, 8, 3.5, 3.5 );
84
85 REQUIRE( pck.m_kernels.size() == 8 * 8 );
86
87 mx::improc::eigenImage<float> generatedKernel, cachedKernel;
88 for( int column = 0; column < 8; ++column )
89 {
90 for( int row = 0; row < 8; ++row )
91 {
92 REQUIRE( kernel.setKernel( row - 3.5, column - 3.5, generatedKernel ) == mx::error_t::noerror );
93 REQUIRE( pck.setKernel( row - 3.5, column - 3.5, cachedKernel ) == mx::error_t::noerror );
94 REQUIRE( ( generatedKernel == cachedKernel ).all() );
95 }
96 }
97 }
98}
99
100/// Verify azBoxKernel produces finite normalized kernels at image centers and neighboring pixels.
101/** Exercises mx::improc::azBoxKernel::setKernel for exact, half-pixel, radial, and azimuthal center-relative
102 * coordinates. The exact image center consistently uses the positive x-axis as its radial orientation.
103 */
104/**
105 * \ingroup imageFilters_unit_tests
106 */
107TEST_CASE( "azBoxKernel is finite at centers and neighboring pixels", "[improc::azBoxKernel]" )
108{
109 auto requireFiniteNormalized = []( const mx::improc::eigenImage<double> &kernel )
110 {
111 REQUIRE( kernel.size() > 0 );
112 REQUIRE( arrayAllFinite( kernel ) );
113 REQUIRE( kernel.sum() == Approx( 1.0 ).margin( 1e-12 ) );
114 };
115
116 SECTION( "exact center uses the positive x-axis and configured half-widths" )
117 {
119 kernelT kernelGenerator( 3.0, 1.0 );
121
122 REQUIRE( kernelGenerator.setKernel( 0.0, 0.0, kernel ) == mx::error_t::noerror );
123 requireFiniteNormalized( kernel );
124 REQUIRE( kernelGenerator.maxWidth() == 4 );
125 REQUIRE( kernel.rows() == 8 );
126 REQUIRE( kernel.cols() == 4 );
127 }
128
129 SECTION( "an angular limit retains the output pixel" )
130 {
132 kernelT kernelGenerator( 1.0, 2.0, 30.0 );
134
135 REQUIRE( kernelGenerator.setKernel( 0.0, 0.0, kernel ) == mx::error_t::noerror );
136 requireFiniteNormalized( kernel );
137 REQUIRE( kernel.rows() % 2 == 1 );
138 REQUIRE( kernel.cols() % 2 == 1 );
139 REQUIRE( kernel( kernel.rows() / 2, kernel.cols() / 2 ) > 0 );
140
141 REQUIRE( kernelGenerator.setKernel( 0.0, -1.0, kernel ) == mx::error_t::noerror );
142 requireFiniteNormalized( kernel );
143 REQUIRE( kernel.rows() % 2 == 1 );
144 REQUIRE( kernel.cols() % 2 == 1 );
145 REQUIRE( kernel( kernel.rows() / 2, kernel.cols() / 2 ) > 0 );
146 }
147
148 SECTION( "an odd kernel includes its finite center sample" )
149 {
151 kernelT kernelGenerator( 2.0, 2.0 );
152 mx::improc::eigenImage<double> centerKernel, radialNeighbor, azimuthalNeighbor, halfPixelKernel;
153
154 REQUIRE( kernelGenerator.setKernel( 0.0, 0.0, centerKernel ) == mx::error_t::noerror );
155 REQUIRE( kernelGenerator.setKernel( 1.0, 0.0, radialNeighbor ) == mx::error_t::noerror );
156 REQUIRE( kernelGenerator.setKernel( 0.0, 1.0, azimuthalNeighbor ) == mx::error_t::noerror );
157 REQUIRE( kernelGenerator.setKernel( 0.5, 0.5, halfPixelKernel ) == mx::error_t::noerror );
158
159 requireFiniteNormalized( centerKernel );
160 requireFiniteNormalized( radialNeighbor );
161 requireFiniteNormalized( azimuthalNeighbor );
162 requireFiniteNormalized( halfPixelKernel );
163 REQUIRE( kernelGenerator.maxWidth() == 1 );
164 REQUIRE( centerKernel.rows() % 2 == 1 );
165 REQUIRE( centerKernel.cols() % 2 == 1 );
166 REQUIRE( centerKernel( centerKernel.rows() / 2, centerKernel.cols() / 2 ) > 0 );
167 REQUIRE( radialNeighbor( radialNeighbor.rows() / 2, radialNeighbor.cols() / 2 ) > 0 );
168 }
169
170 SECTION( "radial and azimuthal neighbors rotate an anisotropic kernel" )
171 {
173 kernelT kernelGenerator( 3.0, 1.0 );
174 mx::improc::eigenImage<double> radialKernel, azimuthalKernel;
175
176 REQUIRE( kernelGenerator.setKernel( 2.0, 0.0, radialKernel ) == mx::error_t::noerror );
177 REQUIRE( kernelGenerator.setKernel( 0.0, 2.0, azimuthalKernel ) == mx::error_t::noerror );
178
179 requireFiniteNormalized( radialKernel );
180 requireFiniteNormalized( azimuthalKernel );
181 REQUIRE( radialKernel.rows() == azimuthalKernel.cols() );
182 REQUIRE( radialKernel.cols() == azimuthalKernel.rows() );
183 }
184}
185
186/// Verify azBoxKernel honors maximum azimuth and validates degenerate configuration values.
187/** Exercises mx::improc::azBoxKernel::setKernel normalization, zero-width identity behavior, absolute half-widths,
188 * finite-value validation, and maximum-angle limiting.
189 */
190/**
191 * \ingroup imageFilters_unit_tests
192 */
193TEST_CASE( "azBoxKernel validates widths coordinates and maxAz", "[improc::azBoxKernel]" )
194{
197
198 SECTION( "zero half-widths produce an identity kernel" )
199 {
200 kernelT identityKernel( 0.0, 0.0 );
201 REQUIRE( identityKernel.maxWidth() == 0 );
202 REQUIRE( identityKernel.setKernel( 0.0, 0.0, kernel ) == mx::error_t::noerror );
203 REQUIRE( kernel.rows() == 1 );
204 REQUIRE( kernel.cols() == 1 );
205 REQUIRE( kernel( 0, 0 ) == 1.0 );
206 REQUIRE( identityKernel.setKernel( 4.0, -3.0, kernel ) == mx::error_t::noerror );
207 REQUIRE( kernel( 0, 0 ) == 1.0 );
208 }
209
210 SECTION( "subpixel half-widths support a one-pixel odd kernel" )
211 {
213 subpixelKernelT subpixelKernel( 0.25, 0.25 );
214
215 REQUIRE( subpixelKernel.maxWidth() == 0 );
216 REQUIRE( subpixelKernel.setKernel( 0.0, 0.0, kernel ) == mx::error_t::noerror );
217 REQUIRE( kernel.rows() == 1 );
218 REQUIRE( kernel.cols() == 1 );
219 REQUIRE( kernel( 0, 0 ) == 1.0 );
220 }
221
222 SECTION( "negative half-widths retain their absolute-value contract" )
223 {
224 kernelT positiveWidths( 2.0, 4.0 );
225 kernelT negativeWidths( -2.0, -4.0 );
226 mx::improc::eigenImage<double> positiveKernel, negativeKernel;
227
228 REQUIRE( positiveWidths.setKernel( 3.0, 1.0, positiveKernel ) == mx::error_t::noerror );
229 REQUIRE( negativeWidths.setKernel( 3.0, 1.0, negativeKernel ) == mx::error_t::noerror );
230 REQUIRE( ( positiveKernel == negativeKernel ).all() );
231 }
232
233 SECTION( "nonfinite widths maxAz and coordinates are rejected" )
234 {
235 const double nan = std::numeric_limits<double>::quiet_NaN();
236 const double infinity = std::numeric_limits<double>::infinity();
237 kernelT invalidRadialWidth( nan, 1.0 );
238 kernelT invalidAzimuthalWidth( 1.0, infinity );
239 kernelT invalidMaxAz( 1.0, 1.0, nan );
240 kernelT validKernel( 1.0, 1.0 );
241
242 REQUIRE( invalidRadialWidth.setKernel( 0.0, 0.0, kernel ) == mx::error_t::invalidconfig );
243 REQUIRE( kernel.size() == 0 );
244 REQUIRE( invalidAzimuthalWidth.setKernel( 0.0, 0.0, kernel ) == mx::error_t::invalidconfig );
245 REQUIRE( invalidMaxAz.setKernel( 0.0, 0.0, kernel ) == mx::error_t::invalidconfig );
246 REQUIRE( validKernel.setKernel( nan, 0.0, kernel ) == mx::error_t::invalidarg );
247 REQUIRE( validKernel.setKernel( 0.0, infinity, kernel ) == mx::error_t::invalidarg );
248 }
249
250 SECTION( "maxAz limits support around the positive x-axis at image center" )
251 {
252 kernelT unlimitedKernel( 2.0, 5.0 );
253 kernelT equivalentUnlimitedKernel( 2.0, 5.0, 180.0 );
254 kernelT limitedKernel( 2.0, 5.0, 30.0 );
255 kernelT negativeLimitKernel( 2.0, 5.0, -30.0 );
256 mx::improc::eigenImage<double> unlimited, equivalentUnlimited, limited, negativeLimit;
257
258 REQUIRE( unlimitedKernel.setKernel( 0.0, 0.0, unlimited ) == mx::error_t::noerror );
259 REQUIRE( equivalentUnlimitedKernel.setKernel( 0.0, 0.0, equivalentUnlimited ) == mx::error_t::noerror );
260 REQUIRE( limitedKernel.setKernel( 0.0, 0.0, limited ) == mx::error_t::noerror );
261 REQUIRE( negativeLimitKernel.setKernel( 0.0, 0.0, negativeLimit ) == mx::error_t::noerror );
262
263 REQUIRE( arrayAllFinite( unlimited ) );
264 REQUIRE( arrayAllFinite( limited ) );
265 REQUIRE( unlimited.sum() == Approx( 1.0 ).margin( 1e-12 ) );
266 REQUIRE( limited.sum() == Approx( 1.0 ).margin( 1e-12 ) );
267 REQUIRE( ( unlimited == equivalentUnlimited ).all() );
268 REQUIRE( ( limited == negativeLimit ).all() );
269 REQUIRE( ( limited != 0.0 ).count() < ( unlimited != 0.0 ).count() );
270
271 const double xCenter = 0.5 * ( limited.rows() - 1.0 );
272 const double yCenter = 0.5 * ( limited.cols() - 1.0 );
273 const double maxAngle = mx::math::dtor( 30.0 );
274 for( int column = 0; column < limited.cols(); ++column )
275 {
276 for( int row = 0; row < limited.rows(); ++row )
277 {
278 if( limited( row, column ) > 0 )
279 {
280 REQUIRE( std::fabs( std::atan2( column - yCenter, row - xCenter ) ) <= maxAngle + 1e-12 );
281 }
282 }
283 }
284 }
285
286 SECTION( "finite dimension overflow is rejected" )
287 {
288 kernelT oversizedKernel( std::numeric_limits<double>::max(), 1.0 );
289
290 REQUIRE( oversizedKernel.maxWidth() == 0 );
291 REQUIRE( oversizedKernel.setKernel( 1.0, 0.0, kernel ) == mx::error_t::invalidconfig );
292 REQUIRE( kernel.size() == 0 );
293 }
294
295 SECTION( "inconsistent cached width bounds are rejected" )
296 {
297 kernelT widthKernel( 3.0, 1.0 );
298 widthKernel.m_maxWidth = 0;
299 REQUIRE( widthKernel.setKernel( 1.0, 0.0, kernel ) == mx::error_t::sizeerr );
300
301 kernelT heightKernel( 1.0, 3.0 );
302 heightKernel.m_maxWidth = 2;
303 REQUIRE( heightKernel.setKernel( 1.0, 0.0, kernel ) == mx::error_t::sizeerr );
304 }
305
306 SECTION( "a kernel with no accepted samples is rejected" )
307 {
308 kernelT emptyKernel( 1.0, 1.0 );
309 emptyKernel.m_radWidth = -1.0;
310 REQUIRE( emptyKernel.setKernel( 1.0, 0.0, kernel ) == mx::error_t::invalidconfig );
311 REQUIRE( kernel.size() == 0 );
312 }
313
314 SECTION( "maxAz is evaluated away from the image center" )
315 {
316 kernelT limitedKernel( 2.0, 5.0, 30.0 );
317 REQUIRE( limitedKernel.setKernel( 3.0, 2.0, kernel ) == mx::error_t::noerror );
318 REQUIRE( arrayAllFinite( kernel ) );
319 REQUIRE( kernel.sum() == Approx( 1.0 ).margin( 1e-12 ) );
320 }
321}
322
323/// Verify precalcKernel handles odd, even, and non-square image centers without invalid cached kernels.
324/** Exercises mx::improc::precalcKernel construction and mx::improc::precalcKernel::setKernel at exact and half-pixel
325 * image centers.
326 */
327/**
328 * \ingroup imageFilters_unit_tests
329 */
330TEST_CASE( "precalcKernel supports odd even and non-square image centers", "[improc::precalcKernel]" )
331{
333 kernelT kernelGenerator( 2.0, 2.0 );
334 mx::improc::precalcKernel<kernelT> oddKernelCache( kernelGenerator, 5, 7, 2.0, 3.0 );
335 mx::improc::precalcKernel<kernelT> evenKernelCache( kernelGenerator, 4, 6, 1.5, 2.5 );
336
337 REQUIRE( oddKernelCache.m_kernels.size() == 5 * 7 );
338 REQUIRE( evenKernelCache.m_kernels.size() == 4 * 6 );
339
340 for( const auto &cachedKernel : oddKernelCache.m_kernels )
341 {
342 REQUIRE( cachedKernel.size() > 0 );
343 REQUIRE( arrayAllFinite( cachedKernel ) );
344 REQUIRE( cachedKernel.sum() == Approx( 1.0 ).margin( 1e-12 ) );
345 }
346 for( const auto &cachedKernel : evenKernelCache.m_kernels )
347 {
348 REQUIRE( cachedKernel.size() > 0 );
349 REQUIRE( arrayAllFinite( cachedKernel ) );
350 REQUIRE( cachedKernel.sum() == Approx( 1.0 ).margin( 1e-12 ) );
351 }
352
354 REQUIRE( oddKernelCache.setKernel( 0.0, 0.0, centerKernel ) == mx::error_t::noerror );
355 REQUIRE( arrayAllFinite( centerKernel ) );
356 REQUIRE( evenKernelCache.setKernel( 0.5, 0.5, centerKernel ) == mx::error_t::noerror );
357 REQUIRE( arrayAllFinite( centerKernel ) );
358}
359
360/// Verify precalcKernel propagates generation errors and rejects invalid lookup coordinates.
361/** Exercises mx::improc::precalcKernel construction error propagation and mx::improc::precalcKernel::setKernel
362 * coordinate bounds.
363 */
364/**
365 * \ingroup imageFilters_unit_tests
366 */
367TEST_CASE( "precalcKernel propagates errors and validates lookup bounds", "[improc::precalcKernel]" )
368{
370 const double nan = std::numeric_limits<double>::quiet_NaN();
371
372 try
373 {
374 kernelT invalidKernel( nan, 1.0 );
375 mx::improc::precalcKernel<kernelT> invalidCache( invalidKernel, 3, 3, 1.0, 1.0 );
376 static_cast<void>( invalidCache );
377 FAIL( "precalcKernel ignored a setKernel failure" );
378 }
379 catch( const mx::exception<mx::verbose::o> &error )
380 {
381 REQUIRE( error.code() == mx::error_t::invalidconfig );
382 }
383
384 kernelT validKernel( 2.0, 2.0 );
385 mx::improc::precalcKernel<kernelT> cache( validKernel, 3, 5, 1.0, 2.0 );
387
388 REQUIRE( cache.setKernel( -1.0, -2.0, kernel ) == mx::error_t::noerror );
389 REQUIRE( cache.setKernel( 1.0, 2.0, kernel ) == mx::error_t::noerror );
390 REQUIRE( cache.setKernel( -2.0, 0.0, kernel ) == mx::error_t::invalidarg );
391 REQUIRE( cache.setKernel( 2.0, 0.0, kernel ) == mx::error_t::invalidarg );
392 REQUIRE( cache.setKernel( 0.0, -3.0, kernel ) == mx::error_t::invalidarg );
393 REQUIRE( cache.setKernel( 0.0, 3.0, kernel ) == mx::error_t::invalidarg );
394 REQUIRE( cache.setKernel( 0.25, 0.0, kernel ) == mx::error_t::invalidarg );
395 REQUIRE( cache.setKernel( nan, 0.0, kernel ) == mx::error_t::invalidarg );
396
397 mx::improc::precalcKernel<kernelT> emptyCache( validKernel, 0, 0, 0.0, 0.0 );
398 REQUIRE( emptyCache.setKernel( 0.0, 0.0, kernel ) == mx::error_t::invalidconfig );
399}
400
401/** \brief Verifies the radial-profile subtraction used by HCI preprocessing.
402 *
403 * Exercises mx::improc::radprofim with its centered-radius overload and subtraction enabled, followed by the
404 * mx::improc::zeroNaNs cleanup used by HCI.
405 *
406 * \ingroup imageFilters_unit_tests
407 */
408TEST_CASE( "radprofim subtracts a centered radial background", "[improc::radprofim][hciReduce]" )
409{
410 mx::improc::eigenImage<double> image( 9, 9 );
411 image.setConstant( 5.0 );
413
414 mx::improc::radprofim( profile, image, true );
415 mx::improc::zeroNaNs( image );
416
417 REQUIRE( profile.rows() == image.rows() );
418 REQUIRE( profile.cols() == image.cols() );
419 REQUIRE( arrayAllFinite( image ) );
420 REQUIRE( image.abs().maxCoeff() < 1e-12 );
421}
422
423/** \brief Verifies that masked pixels are excluded from and preserved by radial-profile subtraction.
424 *
425 * Exercises the masked mx::improc::radprofim overload used when preprocessing must omit invalid pixels.
426 *
427 * \ingroup imageFilters_unit_tests
428 */
429TEST_CASE( "radprofim excludes masked pixels", "[improc::radprofim][hciReduce]" )
430{
431 mx::improc::eigenImage<double> image( 9, 9 );
432 image.setConstant( 5.0 );
433
434 mx::improc::eigenImage<double> radius( 9, 9 );
435 mx::improc::radiusImage( radius );
436
438 mask.setOnes();
439 mask( 4, 4 ) = 0;
440
442 mx::improc::radprofim( profile, image, radius, &mask, true );
443
444 REQUIRE( profile.rows() == image.rows() );
445 REQUIRE( profile.cols() == image.cols() );
446 REQUIRE( profile( 4, 4 ) == 0.0 );
447 REQUIRE( image( 4, 4 ) == 5.0 );
448
449 mx::improc::zeroNaNs( image );
450 REQUIRE( image( 4, 5 ) == Approx( 0.0 ).margin( 1e-12 ) );
451}
452
453/** \brief Verifies HCI azimuthal median filtering through a precomputed azimuthal kernel.
454 *
455 * Exercises mx::improc::medianFilterImage with mx::improc::precalcKernel and mx::improc::azBoxKernel.
456 *
457 * \ingroup imageFilters_unit_tests
458 */
459TEST_CASE( "medianFilterImage handles HCI azimuthal kernels", "[improc::medianFilterImage][hciReduce]" )
460{
462
463 kernelT kernelGenerator( 2.0, 2.0 );
464 mx::improc::precalcKernel<kernelT> kernelCache( kernelGenerator, 9, 9, 4.0, 4.0 );
465
466 mx::improc::eigenImage<double> image( 9, 9 );
467 image.setConstant( 5.0 );
468 image( 4, 4 ) = 100.0;
469
471 mx::improc::medianFilterImage( filtered, image, kernelCache );
472
473 REQUIRE( filtered.rows() == image.rows() );
474 REQUIRE( filtered.cols() == image.cols() );
475 REQUIRE( arrayAllFinite( filtered ) );
476 REQUIRE( filtered( 4, 4 ) == 5.0 );
477 REQUIRE( filtered( 0, 0 ) == 5.0 );
478}
479
480/// Verify that filterImage preserves non-square dimensions with a Gaussian kernel.
481/**
482 * \ingroup imageFilters_unit_tests
483 */
484TEST_CASE( "Gaussian filter handles non-square image dimensions", "[improc::imageFilters]" )
485{
488
489 im.resize( 640, 480 );
490 for( int r = 0; r < im.rows(); ++r )
491 {
492 for( int c = 0; c < im.cols(); ++c )
493 {
494 im( r, c ) = static_cast<float>( ( r + 2 * c ) % 17 );
495 }
496 }
497
499 auto err = mx::improc::filterImage( fim, im, kernel );
500
501 REQUIRE( err == mx::error_t::noerror );
502 REQUIRE( fim.rows() == im.rows() );
503 REQUIRE( fim.cols() == im.cols() );
504 REQUIRE( std::isfinite( fim( 0, 0 ) ) );
505 REQUIRE( std::isfinite( fim( fim.rows() - 1, fim.cols() - 1 ) ) );
506 REQUIRE( std::isfinite( fim( fim.rows() / 2, fim.cols() / 2 ) ) );
507}
508
509/// Verify that medianSmooth uses exactly the configured odd or even square window.
510/** Exercises both mx::improc::medianSmooth overloads, including the arithmetic mean of the two central samples for an
511 * even-sized window and the caller-owned untouched edge pixels.
512 */
513/**
514 * \ingroup imageFilters_unit_tests
515 */
516TEST_CASE( "medianSmooth honors exact odd and even full widths", "[improc::medianSmooth]" )
517{
518 SECTION( "even width uses width squared samples" )
519 {
520 mx::improc::eigenImage<double> input( 4, 4 );
521 mx::improc::eigenImage<double> output( 4, 4 );
522 for( int column = 0; column < input.cols(); ++column )
523 {
524 for( int row = 0; row < input.rows(); ++row )
525 {
526 input( row, column ) = row + column * input.rows();
527 }
528 }
529 output.setConstant( -99.0 );
530
531 int xMax = 17;
532 int yMax = 19;
533 double pMax = 23.0;
534 const int result = mx::improc::medianSmooth( output, xMax, yMax, pMax, input, 4 );
535
536 REQUIRE( result == 0 );
537 REQUIRE( output( 2, 2 ) == Approx( 7.5 ) );
538 REQUIRE( xMax == 2 );
539 REQUIRE( yMax == 2 );
540 REQUIRE( pMax == Approx( 7.5 ) );
541 for( int column = 0; column < output.cols(); ++column )
542 {
543 for( int row = 0; row < output.rows(); ++row )
544 {
545 if( row != 2 || column != 2 )
546 {
547 REQUIRE( output( row, column ) == -99.0 );
548 }
549 }
550 }
551 }
552
553 SECTION( "odd width retains its centered footprint" )
554 {
555 mx::improc::eigenImage<double> input( 3, 3 );
556 mx::improc::eigenImage<double> output( 3, 3 );
557 for( int column = 0; column < input.cols(); ++column )
558 {
559 for( int row = 0; row < input.rows(); ++row )
560 {
561 input( row, column ) = row + column * input.rows();
562 }
563 }
564 output.setConstant( -99.0 );
565
566 REQUIRE( mx::improc::medianSmooth( output, input, 3 ) == 0 );
567 REQUIRE( output( 1, 1 ) == Approx( 4.0 ) );
568 REQUIRE( output( 0, 0 ) == -99.0 );
569 REQUIRE( output( 2, 2 ) == -99.0 );
570 }
571
572 SECTION( "unit width is an identity operation" )
573 {
574 mx::improc::eigenImage<double> input( 2, 3 );
575 input << 1.0, 2.0, 3.0, 4.0, 5.0, 6.0;
576 mx::improc::eigenImage<double> output( 2, 3 );
577 output.setZero();
578
579 REQUIRE( mx::improc::medianSmooth( output, input, 1 ) == 0 );
580 REQUIRE( ( output == input ).all() );
581 }
582}
583
584/// Verify that medianSmooth rejects invalid dimensions without modifying caller-owned pixels.
585/** Exercises mx::improc::medianSmooth validation and deterministic maximum outputs. */
586/**
587 * \ingroup imageFilters_unit_tests
588 */
589TEST_CASE( "medianSmooth validates widths and output dimensions", "[improc::medianSmooth]" )
590{
591 mx::improc::eigenImage<double> input( 3, 4 );
592 mx::improc::eigenImage<double> output( 3, 4 );
593 input.setZero();
594 output.setConstant( -42.0 );
595
596 int xMax = 17;
597 int yMax = 19;
598 double pMax = 23.0;
599
600 REQUIRE( mx::improc::medianSmooth( output, xMax, yMax, pMax, input, 0 ) == -1 );
601 REQUIRE( xMax == -1 );
602 REQUIRE( yMax == -1 );
603 REQUIRE( pMax == std::numeric_limits<double>::lowest() );
604 REQUIRE( ( output == -42.0 ).all() );
605
606 REQUIRE( mx::improc::medianSmooth( output, xMax, yMax, pMax, input, -1 ) == -1 );
607 REQUIRE( mx::improc::medianSmooth( output, xMax, yMax, pMax, input, 4 ) == -1 );
608 REQUIRE( ( output == -42.0 ).all() );
609
610 mx::improc::eigenImage<double> wrongSize( 2, 4 );
611 wrongSize.setConstant( -42.0 );
612 REQUIRE( mx::improc::medianSmooth( wrongSize, xMax, yMax, pMax, input, 3 ) == -1 );
613 REQUIRE( ( wrongSize == -42.0 ).all() );
614}
615
616/// Verify that meanSmooth uses an exact even-width footprint.
617/** Exercises both mx::improc::meanSmooth overloads with the same even-window placement used by medianSmooth. */
618/**
619 * \ingroup imageFilters_unit_tests
620 */
621TEST_CASE( "meanSmooth honors exact even full widths", "[improc::meanSmooth]" )
622{
623 mx::improc::eigenImage<double> input( 4, 4 );
624 mx::improc::eigenImage<double> output( 4, 4 );
625 for( int column = 0; column < input.cols(); ++column )
626 {
627 for( int row = 0; row < input.rows(); ++row )
628 {
629 input( row, column ) = row + column * input.rows();
630 }
631 }
632 output.setConstant( -99.0 );
633
634 REQUIRE( mx::improc::meanSmooth( output, input, 4 ) == 0 );
635 REQUIRE( output( 2, 2 ) == Approx( 7.5 ) );
636 REQUIRE( output( 1, 1 ) == -99.0 );
637
638 output.setConstant( -99.0 );
639 int xMax = 17;
640 int yMax = 19;
641 double pMax = 23.0;
642 REQUIRE( mx::improc::meanSmooth( output, xMax, yMax, pMax, input, 4, true ) == 0 );
643 REQUIRE( output( 2, 2 ) == Approx( 7.5 ) );
644 REQUIRE( xMax == 2 );
645 REQUIRE( yMax == 2 );
646 REQUIRE( pMax == Approx( 7.5 ) );
647
648 SECTION( "reject extrema in an all-positive window" )
649 {
650 mx::improc::eigenImage<double> positiveInput( 2, 2 );
651 positiveInput << 1.0, 2.0, 3.0, 100.0;
652 mx::improc::eigenImage<double> positiveOutput( 2, 2 );
653 positiveOutput.setConstant( -99.0 );
654
655 REQUIRE( mx::improc::meanSmooth( positiveOutput, positiveInput, 2, true ) == 0 );
656 REQUIRE( positiveOutput( 1, 1 ) == Approx( 2.5 ) );
657 REQUIRE( positiveOutput( 0, 0 ) == -99.0 );
658 }
659
660 SECTION( "reject extrema in an all-negative window" )
661 {
662 mx::improc::eigenImage<double> negativeInput( 2, 2 );
663 negativeInput << -100.0, -3.0, -2.0, -1.0;
664 mx::improc::eigenImage<double> negativeOutput( 2, 2 );
665 negativeOutput.setConstant( -99.0 );
666
667 xMax = 17;
668 yMax = 19;
669 pMax = 23.0;
670 REQUIRE( mx::improc::meanSmooth( negativeOutput, xMax, yMax, pMax, negativeInput, 2, true ) == 0 );
671 REQUIRE( negativeOutput( 1, 1 ) == Approx( -2.5 ) );
672 REQUIRE( negativeOutput( 0, 0 ) == -99.0 );
673 REQUIRE( xMax == 1 );
674 REQUIRE( yMax == 1 );
675 REQUIRE( pMax == Approx( -2.5 ) );
676 }
677}
678
679/// Verify that vectorSmoothMedian uses no more than the configured even width.
680/** Exercises mx::math::vectorSmoothMedian at interior and truncated edge locations. */
681/**
682 * \ingroup vectorUtils_unit_tests
683 */
684TEST_CASE( "vectorSmoothMedian honors exact even full widths", "[math::vectorSmoothMedian]" )
685{
686 std::vector<double> input{ 0.0, 1.0, 2.0, 3.0, 100.0 };
687 const std::vector<double> original = input;
688 std::vector<double> output;
689
690 REQUIRE( mx::math::vectorSmoothMedian( output, input, 4 ) == 0 );
691 REQUIRE( input == original );
692 REQUIRE( output.size() == input.size() );
693 REQUIRE( output[0] == Approx( 0.5 ) );
694 REQUIRE( output[1] == Approx( 1.0 ) );
695 REQUIRE( output[2] == Approx( 1.5 ) );
696 REQUIRE( output[3] == Approx( 2.5 ) );
697 REQUIRE( output[4] == Approx( 3.0 ) );
698
699 output.assign( 1, -99.0 );
700 REQUIRE( mx::math::vectorSmoothMedian( output, input, 0 ) == -1 );
701 REQUIRE( output == std::vector<double>{ -99.0 } );
702}
703
704/// Verify stddevImage centers the signal using the annular mean before calculating S/N.
705/** Exercises mx::improc::stddevImage with a masked reference sample and a non-zero radial mean. */
706/**
707 * \ingroup imageFilters_unit_tests
708 */
709TEST_CASE( "stddevImage subtracts the annular mean for S/N", "[improc::stddevImage]" )
710{
711 mx::improc::eigenImage<double> image( 8, 8 );
712 mx::improc::eigenImage<double> radius( 8, 8 );
714 mask.setOnes();
715
716 for( int column = 0; column < image.cols(); ++column )
717 {
718 for( int row = 0; row < image.rows(); ++row )
719 {
720 radius( row, column ) = column + 0.5;
721 image( row, column ) = 10.0 * column + ( row % 2 == 0 ? 2.0 : 4.0 );
722 }
723 }
724 mask( 0, 1 ) = 0;
725
727 mx::improc::stddevImage( stddev, image, radius, mask, 0.0, 6.0, true );
728
729 const double mean = 92.0 / 7.0;
730 const double variance = ( 3.0 * std::pow( 12.0 - mean, 2 ) + 4.0 * std::pow( 14.0 - mean, 2 ) ) / 6.0;
731 REQUIRE( stddev( 2, 1 ) == Approx( ( 12.0 - mean ) / std::sqrt( variance ) ) );
732
733 SECTION( "negative masked radii are skipped before binning" )
734 {
735 radius( 0, 0 ) = -0.5;
736 mask.setOnes();
737
738 mx::improc::stddevImage( stddev, image, radius, mask, 0.0, 6.0, true );
739
740 REQUIRE( std::isfinite( stddev( 2, 1 ) ) );
741 }
742
743 SECTION( "a masked infinite radius finishes the bins" )
744 {
745 radius( 0, 7 ) = std::numeric_limits<double>::infinity();
746 mask( 0, 7 ) = 0;
747
748 mx::improc::stddevImage( stddev, image, radius, mask, 0.0, 6.0, true );
749
750 REQUIRE( std::isfinite( stddev( 2, 1 ) ) );
751 }
752}
753
754} // namespace imageFiltersTest
755} // namespace improcTest
756} // namespace unitTest
Augments an exception with the source file and line.
Definition exception.hpp:42
Eigen::Array< scalarT, -1, -1 > eigenImage
Definition of the eigenImage type, which is an alias for Eigen::Array.
void zeroNaNs(imageT &im, valueT val)
Zero any NaNs in an image.
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ sizeerr
A size was invalid or calculated incorrectly.
Definition error_t.hpp:35
@ invalidconfig
A config setting was invalid.
Definition error_t.hpp:30
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
bool isFinite(realT value)
Test whether a floating-point value is finite, including under finite-math-only optimization.
realT dtor(realT q)
Convert from degrees to radians.
Definition geo.hpp:133
TEST_CASE("precalcKernel caches production kernel values", "[improc::precalcKernel]")
Verify precalcKernel returns the same Gaussian and azimuthal kernels as their production generators.
int meanSmooth(imageTout &imOut, const imageTin &imIn, int meanFullWidth, bool rejectMinMax=false)
Smooth an image using the mean in a rectangular box, optionally rejecting the highest and lowest valu...
int medianSmooth(imageTout &imOut, int &xMax, int &yMax, typename imageTout::Scalar &pMax, const imageTin &imIn, int medianFullWidth)
error_t filterImage(imageOutT &fim, imageInT im, const kernelT &kernel, int maxr=0)
Filter an image with a mean kernel.
void medianFilterImage(imageOutT &fim, imageInT im, const kernelT &kernel, int maxr=0, int maxrproc=1)
Filter an image with a median kernel.
void radiusImage(eigenT &m, typename eigenT::Scalar xc, typename eigenT::Scalar yc, typename eigenT::Scalar scale=1)
Fills in the cells of an Eigen 2D Array with their radius from the center.
void radprofim(radprofT &radprofIm, eigenImT1 &im, const eigenImT2 &rad, const eigenImT3 *mask, bool subtract, bool mean=false)
Form a radial profile image, and optionally subtract it from the input.
void stddevImage(eigenImT &stdIm, const eigenImT1 &im, const eigenImT2 &rad, const eigenImT3 &mask, typename eigenImT::Scalar minRad, typename eigenImT::Scalar maxRad, bool divide)
Form a standard deviation image, and optionally normalize the input relative to the local mean to for...
int vectorSmoothMedian(std::vector< realT > &smVec, std::vector< realT > &vec, int win)
Smooth a vector using the median in a window specified by its full width.
Image filters (smoothing, radial profiles, etc.).
Header for the image processing utilities.
Azimuthally variable boxcar kernel.
Symetric Gaussian smoothing kernel.
A kernel that is pre-calculated for the entire image, useful for repeated applications.
error_t setKernel(arithT x, arithT y, arrayT &kernel) const
Retrieve the cached kernel at an integral image-relative coordinate.
std::vector< arrayT > m_kernels
generated kernels in column-major image-coordinate order.
Verbosity level 0, no reports are generated or printed to stderr.
Definition error.hpp:147