vspline 1.1.0
Generic C++11 Code for Uniform B-Splines
bls.cpp
Go to the documentation of this file.
1/************************************************************************/
2/* */
3/* vspline - a set of generic tools for creation and evaluation */
4/* of uniform b-splines */
5/* */
6/* Copyright 2018 - 2023 by Kay F. Jahnke */
7/* */
8/* Permission is hereby granted, free of charge, to any person */
9/* obtaining a copy of this software and associated documentation */
10/* files (the "Software"), to deal in the Software without */
11/* restriction, including without limitation the rights to use, */
12/* copy, modify, merge, publish, distribute, sublicense, and/or */
13/* sell copies of the Software, and to permit persons to whom the */
14/* Software is furnished to do so, subject to the following */
15/* conditions: */
16/* */
17/* The above copyright notice and this permission notice shall be */
18/* included in all copies or substantial portions of the */
19/* Software. */
20/* */
21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND */
22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES */
23/* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND */
24/* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT */
25/* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, */
26/* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING */
27/* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR */
28/* OTHER DEALINGS IN THE SOFTWARE. */
29/* */
30/************************************************************************/
31
32/// \file bls.cpp
33///
34/// \brief fidelity test
35///
36/// This is a test to see how much a signal degrades when it is submitted
37/// to a sequence of operations:
38///
39/// - create a periodic b-spline over the signal
40/// - evaluate the spline at unit-spaced locations with an arbitrary offset
41/// - yielding a shifted signal, for which the process is repeated
42///
43/// Finally, a last shift is performed which samples the penultimate version
44/// of the signal at points coinciding with coordinates 0...N-1 of the
45/// original signal. This last iteration should ideally recreate the
46/// original sequence, and you'll see a cumulated offset of zero for this
47/// last iteration.
48///
49/// The test is done with a periodic signal to avoid margin effects.
50/// The initial sequence is created by performing an IFFT on random
51/// data up to a given cutoff frequency, producing a band-limited signal.
52/// Alternatively, by modifying the initial values which are put into
53/// the frequency-domain representation of the test signal, more specific
54/// scenarios can be investigated (try setting the values to 1 uniformly etc)
55/// With the cutoff factor approaching 1.0, the signal contains more and
56/// higher high frequencies, making it ever more difficult for the spline
57/// to represent the data faithfully. With a cutoff of <= .5, fidelity is
58/// very high for high-degree b-splines; even after many iterations
59/// the spline is nearly unchanged and faithfully represents the
60/// original signal.
61///
62/// There are two factors to observe: high-degree splines cope better with
63/// high frequency components, but they suffer from the large magnitude of
64/// coefficient values, making the evaluation error larger - especially since
65/// high-degree splines have wide support.
66///
67/// An important conclusion is that even though all input signals produced
68/// with this test are band-limited by design, this quality is not sufficient
69/// to produce a b-spline from them which is 'stable' in the sense of being
70/// immune to the multiple shifting operation, unless the signal is free from
71/// frequencies above *half the Nyquist frequency*.
72///
73/// If the input contains high frequencies, and yet a faithful b-spline
74/// representation has to be provided which is immune to shifting, one
75/// solution is to create a high-degree spline over the signal, sample it
76/// at half unit steps and use the resulting signal to create the 'working'
77/// spline. By creating the high-order 'super-spline', a signal is created
78/// which is low in high-frequency content and can be faithfully represented
79/// by the working spline. The upsampling can also be done in any other
80/// way, like FFT/IFFT. See n_shift.cc for an implementation of upsampling
81/// using a high-degree b-spline as 'super-spline'.
82///
83/// An aside: this program shows that we can easily shift and resample a
84/// high-degree periodic spline for thousands of times without much signal
85/// degradation, which is quite remarkable, but requires the signal to be
86/// band-limited to *half the Nyquist frequency*. But if we look at the
87/// fourier transformation of each of the shifted variants of the signal,
88/// we don't - as we might expect - get precisely the same magnitudes for
89/// the partial frequencies. I'm not quite sure what to make of this.
90///
91/// note that this example requires fftw3.
92///
93/// compile with: clang++ -O3 -std=c++11 -obls bls.cpp \
94/// -pthread -lvigraimpex -lfftw3
95///
96/// to use highway, add -DUSE_HWY and -lhwy
97/// to use Vc, add -DUSE_VC and -lVc
98/// to use std::simd, use -DUSE_STDSIMD and --std=c++17
99// TODO: comes out wrong with clang++ and std::simd on my debian11 system
100///
101/// invoke with: bls <spline degree> <number of iterations> \
102/// [ <frequency cutoff> ]
103///
104/// Try different spline degrees and cutoff frequencies. With cutoff
105/// frequencies of 0.5 or less you'll notice that even with relatively
106/// low spline degree, the 'immunity' to resampling is quite good: even
107/// with thousands of iterations, the original signal can be regained
108/// with minimal error. Raising the spline degree, the cutoff frequency
109/// can even be raised above 0.5 and the spline remains quite 'immune'.
110
111#include <random>
112#include <vigra/accumulator.hxx>
113#include <vigra/multi_math.hxx>
114#include <vspline/vspline.h>
115#include <vigra/multi_fft.hxx>
116
117// a functor to add an offset to an incoming value. This is
118// used further down to create a combined functor producing
119// resamplings of the initial signal.
120
121struct offset_f : public vspline::unary_functor < double >
122{
123 const double offset ;
124
125 offset_f ( const double & _offset )
126 : offset ( _offset )
127 { }
128
129 template < typename IN , typename OUT >
130 void eval ( const IN & in , OUT & out ) const
131 {
132 out = in + offset ;
133 }
134} ;
135
136int main ( int argc , char * argv[] )
137{
138 std::cout << std::fixed << std::showpoint
139 << std::setprecision
140 (std::numeric_limits<double>::max_digits10) ;
141
142 if ( argc < 3 )
143 {
144 std::cerr << "pass the spline's degree and the number of iterations"
145 << std::endl
146 << "and, optionally, the cutoff frequency"
147 << std::endl ;
148 exit ( -1 ) ;
149 }
150
151 int degree = std::atoi ( argv[1] ) ;
152
153 assert ( degree >= 0 && degree <= vspline_constants::max_degree ) ;
154
155 int iterations = std::max ( 1 , std::atoi ( argv[2] ) ) ;
156
157 double f_cutoff = .5 ;
158 if ( argc == 4 )
159 {
160 f_cutoff = std::atof ( argv[3] ) ;
161 std::cout << "using frequency cutoff " << f_cutoff << std::endl ;
162 }
163
164 const std::size_t sz = 1024 ;
165
166 vigra::MultiArray < 1 , double > signal ( sz ) ;
167 vigra::MultiArray < 1 , double > target ( sz ) ;
168
169 vigra::MultiArray < 1 , vigra::FFTWComplex<double> >
170 fourier ( signal.shape() / 2 + 1 ) ;
171
172 std::random_device rd ;
173 std::mt19937 gen ( rd() ) ;
174 gen.seed ( 42 ) ; // level playing field
175 std::uniform_real_distribution<> dis ( -1 , 1 ) ;
176
177 // we fill the coefficients in, filling in random values, until
178 // we reach the cutoff point, then set the higher ones to zero.
179
180 int fill = 0 ;
181 for ( auto & e : fourier )
182 {
183 if ( fill < f_cutoff * sz / 2.0 )
184 e = vigra::FFTWComplex<double> ( dis(gen) , dis(gen) ) ;
185 else
186 e = vigra::FFTWComplex<double> ( 0.0 , 0.0 ) ;
187 ++fill ;
188 }
189
190 // an inverse FFT produces the signal we'll run the test on
191
192 vigra::fourierTransformInverse ( fourier , signal ) ;
193
194 // now we set up the working spline
195
196 vspline::bspline < double , // spline's data type
197 1 > // one dimension
198 bspw ( sz , // sz values
199 degree , // degree as per command line
200 vspline::PERIODIC , // periodic boundary conditions
201 0.0 ) ; // no tolerance
202
203 // we 'pull in' the signal data we've just generated via the spline's
204 // 'prefilter' function which accepts an array of knot points
205
206 bspw.prefilter ( signal ) ;
207
208 // the next few lines are just statistics to better judge what's going on
209
210 using namespace vigra::multi_math ;
211 using namespace vigra::acc;
212
213 vigra::MultiArray < 1 , double > error_array
214 ( vigra::multi_math::squaredNorm ( bspw.core ) ) ;
215
216 AccumulatorChain < double , Select < Mean, Maximum > > ac ;
217 extractFeatures ( error_array.begin() , error_array.end() , ac ) ;
218 {
219 std::cout << "coefficients Mean: "
220 << sqrt(get<Mean>(ac)) << std::endl;
221 std::cout << "coefficients Maximum: "
222 << sqrt(get<Maximum>(ac)) << std::endl;
223 }
224
225 // now we create an evaluator to obtain interpolated values,
226 // using 'make_safe_evaluator' which will create a periodized
227 // spline because we've specified PERIODIC boundary conditions.
228 // So the resulting evaluator will accept arbitrary coordinates
229 // and map them to the defined range.
230
231 auto ev = vspline::make_safe_evaluator ( bspw ) ;
232
233 // we cumulate the offsets so we can 'undo' the cumulated offset
234 // in the last iteration
235
236 double cumulated_offset = 0.0 ;
237
238 for ( int n = 0 ; n < iterations ; n++ )
239 {
240 // use a random, largish offset (+/- 1000). any offset
241 // will do, since we have a periodic spline, mapping the
242 // coordinates for evaluation into the spline's range
243
244 double offset = 1000.0 * dis ( gen ) ;
245
246 // with the last iteration, we shift back to the original
247 // 0-based locations. This last shift should recreate the
248 // original signal as best as a spline of this degree can
249 // do after so many iterations.
250
251 if ( n == iterations - 1 )
252 offset = - cumulated_offset ;
253
254 cumulated_offset += offset ;
255
256 if ( n > ( iterations - 10 ) )
257 std::cout << "iteration " << n << " offset " << offset
258 << " cumulated offset " << cumulated_offset << std::endl ;
259
260 // we evaluate the spline at unit-stepped offsetted locations,
261 // so, 0 + offset , 1 + offset ... in the last iteration, this
262 // should ideally reproduce the original signal. Note the functional
263 // composition: offset_f creates the offsetting functor, adding the
264 // given offset to incoming discrete coordinates, ev is the periodic
265 // spline functor we made before, and 'chaining' the two with '+'
266 // creates a combined functor which feeds the first functor's output
267 // to the second functor's input. So the resulting functor 'f' combines
268 // three steps: add an offset to an incoming coordinate, map the
269 // result into the spline's defined range, and evaluate it there.
270
271 auto f = offset_f ( offset ) + ev ;
272
273 // use vspline::transform to 'roll out' the combined functor.
274 // vspline::transform with a single array argument 'feeds' discrete
275 // coordinates to the functor. This is such a common operation that
276 // vspline::transform has a specific overload for the purpose. The
277 // discrete coordinates are simply those coordinates at which the
278 // values calculated by the functor should be stored.
279
280 vspline::transform ( f , target ) ;
281
282 // now we create a new spline over 'target', reusing bspw
283 // note how this merely changes the coefficients of the spline,
284 // the container for the coefficients is reused, and therefore
285 // the evaluator (ev) will look at the new set of coefficients.
286 // So we don't need to create a new evaluator.
287
288 bspw.prefilter ( target ) ;
289
290 // to convince ourselves that we really are working on a different
291 // sampling of the signal - and to see how close we get to the
292 // original signal after n iterations, when we use a last offset to get
293 // the sampling locations back to 0, 1, ..., before the 'final' result,
294 // we echo the statistics for the last ten iterations. The final
295 // iteration resamples the result of the penultimate one so that it
296 // should recreate the original signal, if the spline is indeed
297 // 'immune' to resampling. This will be the case if the spline is
298 // of sufficiently large degree, and the signal is band-limited to half
299 // the Nyquist frequency.
300
301 vigra::MultiArray < 1 , double > error_array
302 ( vigra::multi_math::squaredNorm ( target - signal ) ) ;
303
304 AccumulatorChain < double , Select < Mean, Maximum > > ac ;
305 extractFeatures ( error_array.begin() , error_array.end() , ac ) ;
306
307 if ( n > ( iterations - 10 ) )
308 {
309 if ( n == iterations - 1 )
310 std::cout << "final result, evaluating at signal unit steps" << std::endl ;
311 std::cout << "signal difference Mean: "
312 << sqrt(get<Mean>(ac)) << std::endl;
313 std::cout << "signal difference Maximum: "
314 << sqrt(get<Maximum>(ac)) << std::endl;
315 }
316 }
317}
int main(int argc, char *argv[])
Definition: bls.cpp:136
void transform(const unary_functor_type &functor, const vigra::MultiArrayView< dimension, typename unary_functor_type::in_type > &input, vigra::MultiArrayView< dimension, typename unary_functor_type::out_type > &output, int njobs=vspline::default_njobs, vspline::atomic< bool > *p_cancel=0)
implementation of two-array transform using wielding::coupled_wield.
Definition: transform.h:211
vspline::grok_type< bspl_coordinate_type< spline_type, rc_type >, result_type, _vsize > make_safe_evaluator(const spline_type &bspl, vigra::TinyVector< int, spline_type::dimension > dspec=vigra::TinyVector< int, spline_type::dimension >(0), int shift=0)
make_safe_evaluator is a factory function, producing a functor which provides safe access to an evalu...
Definition: eval.h:2390
@ PERIODIC
Definition: common.h:73
invoke with: bls <spline degree> <number of iterations> \ [ <frequency cutoff> ]
Definition: bls.cpp:122
offset_f(const double &_offset)
Definition: bls.cpp:125
const double offset
Definition: bls.cpp:123
void eval(const IN &in, OUT &out) const
Definition: bls.cpp:130
class bspline now builds on class bspline_base, adding coefficient storage, while bspline_base provid...
Definition: bspline.h:499
view_type core
Definition: bspline.h:566
void prefilter(vspline::xlf_type boost=vspline::xlf_type(1), int njobs=vspline::default_njobs)
prefilter converts the knot point data in the 'core' area into b-spline coefficients....
Definition: bspline.h:815
class unary_functor provides a functor object which offers a system of types for concrete unary funct...
includes all headers from vspline (most of them indirectly)