vspline 1.1.0
Generic C++11 Code for Uniform B-Splines
ca_correct.cc
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 2017 - 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/// ca_correct.cc
33///
34/// Perform correction of chromatic aberration using a cubic polynomial
35/// This uses panotools-compatible parameters. Currently only processes
36/// 8bit RGB, processing is done on the sRGB data without conversion
37/// to linear and back, images with alpha channel won't compute.
38/// To see how the panotools lens correction model functions,
39/// please refer to https://wiki.panotools.org/Lens_correction_model
40///
41/// compile with:
42/// clang++ -std=c++11 -march=native -o ca_correct -O3 -pthread -DUSE_VC ca_correct.cc -lvigraimpex -lVc
43///
44/// you can also use g++ instead of clang++. If you don't have Vc on
45/// your system, omit '-DUSE_VC' and '-lVc'. To use highway instead of
46/// Vc, use -DUSE_HWY and -lhwy, and to use std::simd, use -DUSE_STSIMD,
47/// and compile with c++17 standard. Newer versions of gcc provide a std::simd
48/// implementation, if that is missing you need to install it separately.
49///
50/// invoke with ca_correct <image> ar br cr dr ag bg cg dg ab bb cb db d e
51/// where 'ar' stands for 'parameter a for red channel' etc., and the
52/// trailing d and e are center shift in x and y direction in pixels.
53///
54/// The purpose here is more to demonstrate how to implement the maths
55/// using vspline, but the program could easily be fleshed out to
56/// become really usable:
57///
58/// - add alpha channel treatment
59/// - add processing of 16bit data
60/// - add colour space management and internal linear processing
61
62// TODO: handle incoming alpha channel
63// TODO: differentiate between 8/16 bit
64// TODO: operate in linear RGB
65// TODO: may produce transparent output where coordinate is out-of-range
66// TODO might allow to parametrize normalization (currently using PT's way)
67// TODO: while emulating the panotools way of ca correction is nice
68// to have, try implementing shift-curve based correction: with
69// pixel's radial distance r to origin (normalized to [0,1])
70// access a 1D b-spline over M data points, so that
71// res = ev ( r * ( M - 1 ) )
72// and picking up from source at res instead of r.
73// the coefficients of the shift curve can be generated by sampling the
74// 'normal' method or any other model, it's methodically neutral, within
75// the fidelity of the spline to the approximated signal, which can be
76// taken arbitrarily high.
77// TODO: consider two more shift curves Cx and Cy defined over [-1,1]
78// then, if the incoming coordinate is(x,y), let
79// x' = Cx ( x ) and y' = Cy ( y ). This would introduce a tensor-
80// based component, usable for sensor tilt compensation (check!)
81
82#include <iostream>
83
84// <vspline/vspline.h> pulls in all of vspline's functionality
85
86#include <vspline/vspline.h>
87
88// in addition to <vigra/tinyvector.hxx> and <vigra/multi_array.hxx>,
89// which are necessarily included by vspline, we want to use vigra's
90// import/export facilities to load and store images:
91
92#include <vigra/stdimage.hxx>
93#include <vigra/imageinfo.hxx>
94#include <vigra/impex.hxx>
95
96// we'll be working with float data, so we set up a program-wide
97// constant for the vector width appropriate for float data
98
100
101// we silently assume we have a colour image
102
103typedef vigra::RGBValue<float,0,1,2> pixel_type;
104
105// coordinate_type is a 2D coordinate
106
107typedef vigra::TinyVector < float , 2 > coordinate_type ;
108
109// type of b-spline object used as interpolator
110
112
113// target_type is a 2D array of pixels
114
115typedef vigra::MultiArray < 2 , pixel_type > target_type ;
116
117// type of b-spline evaluator producing single floats
118
119typedef vspline::evaluator < coordinate_type , // incoming coordinate's type
120 float // processing single channel data
122
123// gate type to force singular coordinates to a range
124// we are using mirror boundary conditions. Note that this may
125// produce artifacts in the margins.
126
128
129// mapper uses two gates, for x and y component
130
133
134// we start out by coding the functor implementing
135// the coordinate transformation for a single channel.
136// we inherit from vspline::unary_functor so that our coordinate
137// transformation fits well into vspline's functional processing scheme
138
140: public vspline::unary_functor < float , float >
141{
142 // incoming coordinates are shifted by dx and dy. These values are expected
143 // in image coordinates. The shift values should be so that a pixel which
144 // is located at the intersection of the sensor with the optical axis comes
145 // out as (0,0). If the optical system is perfectly centered, dx and dy will
146 // be the coordinates of the image center, so if the image is size X * Y,
147 // dx = ( X - 1 ) / 2
148 // dy = ( Y - 1 ) / 2
149
150 const float dx , dy ;
151
152 // next we have a scaling factor. Once the coordinates are shifted to
153 // coordinates relative to the optical axis, we apply a scaling factor
154 // which 'normalizes' the pixel's distance from the optical axis.
155 // A typical scaling factor would be the distance of the image center
156 // from the top left corner at (-0.5,-0.5). With dx and dy as above
157 // dxc = dx - -0.5 ;
158 // dyc = dy - -0.5 ;
159 // scale = 1 / sqrt ( dx * dx + dy * dy ) ;
160 // Here we use a different choice to be compatible with panotools:
161 // we use the vertical distance from image center to top/bottom
162 // margin.
163 // Since we'll be using a polynomial over the radial distance, picking
164 // scale values larger or smaller than this 'typical' value can be used
165 // to affect the precise effect of the radial function.
166 // rscale is simply the reciprocal value for faster computation.
167
168 const float scale ;
169 const float rscale ;
170
171 // After applying the scale, we have a normalized coordinate. The functor will
172 // use this normalized coordinate to calculate the normalized distance from
173 // the optical axis. The resulting distance is the argument to the radial
174 // correction function. For the radial correction function, we use a cubic
175 // polynomial, which needs four coefficients:
176
177 const float a , b , c , d ;
178
179 // finally we have the PT d and e values, which we label x_shift and y_shift
180 // to avoid confusion with the fourth coefficient of the polynomial.
181
182 const float x_shift , y_shift ;
183
184 // we use two static functions to concisely initialize some of the
185 // constant values above
186
187 static double d_from_extent ( double d )
188 {
189 return ( d - 1.0 ) / 2.0 ;
190 }
191
192 static double rscale_from_wh ( double w , double h )
193 {
194 double dx = d_from_extent ( w ) ;
195 double dy = d_from_extent ( h ) ;
196 // I'd normalize to the corner, but to be compatible with panotools,
197 // I use normalization to top margin center instead.
198 // return sqrt ( dx * dx + dy * dy ) ;
199 return sqrt ( dy * dy ) ;
200 }
201
202 // here's the constructor for the radial correction functor, taking all
203 // the values passed from main() and initializing the constants
204
205 ev_radial_correction ( const double & _width ,
206 const double & _height ,
207 const double & _x_shift ,
208 const double & _y_shift ,
209 const double & _a ,
210 const double & _b ,
211 const double & _c ,
212 const double & _d )
213 : dx ( d_from_extent ( _width ) ) ,
214 dy ( d_from_extent ( _height ) ) ,
215 x_shift ( _x_shift ) ,
216 y_shift ( _y_shift ) ,
217 rscale ( rscale_from_wh ( _width , _height ) ) ,
218 scale ( 1.0 / rscale_from_wh ( _width , _height ) ) ,
219 a ( _a ) ,
220 b ( _b ) ,
221 c ( _c ) ,
222 d ( _d )
223 {
224 // we echo the internal state
225 std::cout << "dx: " << dx << std::endl ;
226 std::cout << "dy: " << dy << std::endl ;
227 std::cout << "scale: " << scale << std::endl ;
228 std::cout << "rscale: " << rscale << std::endl ;
229 std::cout << "a: " << a << std::endl ;
230 std::cout << "b: " << b << std::endl ;
231 std::cout << "c: " << c << std::endl ;
232 std::cout << "d: " << d << std::endl ;
233 } ;
234
235 // now we provide evaluation code for the functor.
236 // since the code is the same for vectorized and unvectorized
237 // operation, we can write a template, In words:
238 // eval is a function template with the coordinate type as it's
239 // template argument. eval receives it's argument as a const
240 // reference to a coordinate and deposits it's result to a reference
241 // to a coordinate. This function will not change the state of the
242 // functor (hence the const) - the functor does not have mutable state
243 // anyway. Note how CRD can be a single coordinate_type, or it's
244 // vectorized equivalent.
245
246 template < class CRD >
247 void eval ( const CRD & in ,
248 CRD & result ) const
249 {
250 // set up coordinate-type variable to work on, copy input to it.
251
252 CRD cc ( in ) ;
253
254 // shift and scale
255 // TODO: is it right to add the shift here, or should I subtract
256
257 cc[0] -= ( dx + x_shift ) ;
258 cc[0] *= scale ;
259 cc[1] -= ( dy + y_shift ) ;
260 cc[1] *= scale ;
261
262 // calculate distance from center (this is normalized due to scaled cc)
263
264 auto r = sqrt ( cc[0] * cc[0] + cc[1] * cc[1] ) ;
265
266 // apply polynomial to obtain the scaling factor.
267
268 auto rr = a * r * r * r + b * r * r + c * r + d ;
269
270 // use rr to scale cc - this is the radial correction
271
272 cc[0] *= rr ;
273 cc[1] *= rr ;
274
275 // apply rscale to revert to centered image coordinates
276
277 cc[0] *= rscale ;
278 cc[1] *= rscale ;
279
280 // reverse initial shift to arrive at UL-based image coordinates
281
282 cc[0] += ( dx + x_shift ) ;
283 cc[1] += ( dy + y_shift ) ;
284
285 // assign to result
286
287 result = cc ;
288 }
289} ;
290
291// next we set up the functor processing all three channels. This functor
292// receives three ev_radial_correction functors and three channel views:
293
295: public vspline::unary_functor < coordinate_type , pixel_type >
296{
297 // these three functors hold the radial corrections for the three
298 // colour channels
299
303
304 // these three functors hold interpolators for the colour channels
305
309
310 // and this object deals with out-of-bounds coordinates
311
313
314 // the constructor receives all the functors we'll use. Note how we
315 // can simply copy-construct the functors.
316
318 const ev_radial_correction & _rc_green ,
319 const ev_radial_correction & _rc_blue ,
320 const ev_type & _ev_red ,
321 const ev_type & _ev_green ,
322 const ev_type & _ev_blue ,
323 const mapper_type & _m )
324 : rc_red ( _rc_red ) ,
325 rc_green ( _rc_green ) ,
326 rc_blue ( _rc_blue ) ,
327 ev_red ( _ev_red ) ,
328 ev_green ( _ev_green ) ,
329 ev_blue ( _ev_blue ) ,
330 m ( _m )
331 { } ;
332
333 // the eval routine is simple, it simply applies the coordinate
334 // transformation, applies the mapper to force the transformed
335 // coordinate into the range, an then picks the interpolated value
336 // using the interpolator for the channel. This is done for all
337 // channels in turn.
338
339 // since the code is the same for vectorized and unvectorized
340 // operation, we can again write a template:
341
342 template < class IN , class OUT >
343 void eval ( const IN & c ,
344 OUT & result ) const
345 {
346 // work variable containing a (possibly vectorized) 2D coordinate
347
348 IN cc ;
349
350 // apply the radial correction to the incoming coordinate in c,
351 // storing result to cc. Note that c contains the 'target' coordinate:
352 // The coordinate of the pixel in the target which we want to compute
353
354 rc_red.eval ( c , cc ) ;
355
356 // force coordinate into the defined range (here we use mirroring)
357
358 m.eval ( cc , cc ) ;
359
360 // evaluate channel view at corrected coordinate, storing result
361 // to the red channel of 'result'
362
363 ev_red.eval ( cc , result[0] ) ;
364
365 // ditto, for the remaining channels
366
367 rc_green.eval ( c , cc ) ;
368 m.eval ( cc , cc ) ;
369 ev_green.eval ( cc , result[1] ) ;
370
371 rc_blue.eval ( c , cc ) ;
372 m.eval ( cc , cc ) ;
373 ev_blue.eval ( cc , result[2] ) ;
374 }
375
376} ;
377
378int main ( int argc , char * argv[] )
379{
380 if ( argc < 11 )
381 {
382 std::cerr << "pass a colour image file as first argument" << std::endl ;
383 std::cerr << "followed by a, b, c for red, green, blue" << std::endl ;
384 std::cerr << "and the horizontal and vertical shift" << std::endl ;
385 std::cerr << "like ca_correct xx.jpg 0.0001411 -0.0005236 0.0008456 1.0002093 0 0 0 1 0.0002334 -0.0007607 0.0011446 0.9996757 176 116"
386 << std::endl ;
387 exit( -1 ) ;
388 }
389
390 double ar = atof ( argv[2] ) ;
391 double br = atof ( argv[3] ) ;
392 double cr = atof ( argv[4] ) ;
393 double dr = atof ( argv[5] ) ;
394 double ag = atof ( argv[6] ) ;
395 double bg = atof ( argv[7] ) ;
396 double cg = atof ( argv[8] ) ;
397 double dg = atof ( argv[9] ) ;
398 double ab = atof ( argv[10] ) ;
399 double bb = atof ( argv[11] ) ;
400 double cb = atof ( argv[12] ) ;
401 double db = atof ( argv[13] ) ;
402 double x_shift = atof ( argv[14] ) ; // aka panotools 'd'
403 double y_shift = atof ( argv[15] ) ; // aka panotools 'e'
404
405 // get the image file name
406
407 vigra::ImageImportInfo imageInfo ( argv[1] ) ;
408
409 // we want a b-spline with natural boundary conditions
410
411 vigra::TinyVector < vspline::bc_code , 2 > bcv ( vspline::NATURAL ) ;
412
413 // create cubic 2D b-spline object containing the image data
414 // TODO allow passing in arbitrary spline order
415
416 spline_type bspl ( imageInfo.shape() , // the shape of the data for the spline
417 5 , // degree 5 == quintic spline
418 bcv // specifies natural BCs along both axes
419 ) ;
420
421 // load the image data into the b-spline's core. This is a common idiom:
422 // the spline's 'core' is a MultiArrayView to that part of the spline's
423 // data container which precisely corresponds with the input data.
424 // This saves loading the image to some memory first and then transferring
425 // the data into the spline. Since the core is a vigra::MultiarrayView,
426 // we can pass it to importImage as the desired target for loading the
427 // image from disk.
428
429 std::cout << "reading image " << argv[1] << " from disk" << std::endl ;
430
431 vigra::importImage ( imageInfo , bspl.core ) ;
432
433 // prefilter the b-spline
434
435 std::cout << "setting up b-spline interpolator for image data" << std::endl ;
436
437 bspl.prefilter() ;
438
439 // this is where the result should go:
440
441 target_type target ( imageInfo.shape() ) ;
442
443 // process the image metrics
444
445 float width = imageInfo.width() ;
446 float height = imageInfo.height() ;
447
448 // set up the radial transformation functors
449
450 std::cout << "setting up radial correction for red channel:" << std::endl ;
451
453 ( width , height , x_shift , y_shift , ar , br , cr , dr ) ;
454
455 std::cout << "setting up radial correction for green channel:" << std::endl ;
456
457 ev_radial_correction ca_green
458 ( width , height , x_shift , y_shift , ag , bg , cg , dg ) ;
459
460 std::cout << "setting up radial correction for blue channel:" << std::endl ;
461
463 ( width , height , x_shift , y_shift , ab , bb , cb , db ) ;
464
465 // here we create the channel views.
466
467 auto red_channel = bspl.get_channel_view ( 0 ) ;
468 auto green_channel = bspl.get_channel_view ( 1 ) ;
469 auto blue_channel = bspl.get_channel_view ( 2 ) ;
470
471 // and set up the per-channel interpolators
472
473 ev_type red_ev ( red_channel ) ;
474 ev_type green_ev ( green_channel ) ;
475 ev_type blue_ev ( blue_channel ) ;
476
477 // next we set up coordinate mapping to the defined range
478
479 gate_type g_x ( 0.0 , width - 1.0 ) ;
480 gate_type g_y ( 0.0 , height - 1.0 ) ;
481 mapper_type m ( g_x , g_y ) ;
482
483 // using vspline's factory functions to create the 'gates' and the
484 // 'mapper' applying them, we could instead create m like this:
485 // (Note how we have to be explicit about using 'float'
486 // for the arguments to the gates - using double arguments would not
487 // work here unless we'd also specify the vector width.)
488
489// auto m = vspline::mapper < coordinate_type >
490// ( vspline::mirror ( 0.0f , width - 1.0f ) ,
491// vspline::mirror ( 0.0f , height - 1.0f ) ) ;
492
493 // finally, we create the top-level functor, passing in the three
494 // radial correction functors, the channel-wise evaluators and the
495 // mapper object
496
497 ev_ca_correct correct ( ca_red , ca_green , ca_blue ,
498 red_ev , green_ev , blue_ev ,
499 m ) ;
500
501 // now we obtain the result by performing a vspline::transform. this transform
502 // successively passes discrete coordinates into the target to the functor
503 // it's invoked with, storing the result of the functor's evaluation
504 // at the self-same coordinates in it's target, so for each coordinate
505 // (X,Y), target[(X,Y)] = correct(X,Y)
506
507 std::cout << "rendering the target image" << std::endl ;
508
509 vspline::transform ( correct , target ) ;
510
511 // store the result with vigra impex
512
513 std::cout << "storing the target image as 'ca_correct.tif'" << std::endl ;
514
515 vigra::ImageExportInfo eximageInfo ( "ca_correct.tif" );
516
517 vigra::exportImage ( target ,
518 eximageInfo
519 .setPixelType("UINT8")
520 .setForcedRangeMapping ( 0 , 255 , 0 , 255 ) ) ;
521
522 std::cout << "done" << std::endl ;
523}
int main(int argc, char *argv[])
Definition: ca_correct.cc:378
const int VSIZE
ca_correct.cc
Definition: ca_correct.cc:99
vigra::RGBValue< float, 0, 1, 2 > pixel_type
Definition: ca_correct.cc:103
vspline::map_functor< coordinate_type, VSIZE, gate_type, gate_type > mapper_type
Definition: ca_correct.cc:132
vspline::mirror_gate< float > gate_type
Definition: ca_correct.cc:127
vigra::TinyVector< float, 2 > coordinate_type
Definition: ca_correct.cc:107
vspline::evaluator< coordinate_type, float > ev_type
Definition: ca_correct.cc:121
vspline::bspline< pixel_type, 2 > spline_type
Definition: ca_correct.cc:111
vigra::MultiArray< 2, pixel_type > target_type
Definition: ca_correct.cc:115
class evaluator encodes evaluation of a spline-like object. This is a generalization of b-spline eval...
Definition: eval.h:1718
void eval(const typename base_type::in_type &_coordinate, typename base_type::out_type &_result) const
unvectorized evaluation function. this is delegated to 'feed' above, which reinterprets the arguments...
Definition: eval.h:1822
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
@ NATURAL
Definition: common.h:75
mapper_type m
Definition: ca_correct.cc:312
ev_ca_correct(const ev_radial_correction &_rc_red, const ev_radial_correction &_rc_green, const ev_radial_correction &_rc_blue, const ev_type &_ev_red, const ev_type &_ev_green, const ev_type &_ev_blue, const mapper_type &_m)
Definition: ca_correct.cc:317
ev_radial_correction rc_green
Definition: ca_correct.cc:301
ev_type ev_green
Definition: ca_correct.cc:307
ev_type ev_red
Definition: ca_correct.cc:306
ev_type ev_blue
Definition: ca_correct.cc:308
ev_radial_correction rc_red
Definition: ca_correct.cc:300
ev_radial_correction rc_blue
Definition: ca_correct.cc:302
void eval(const IN &c, OUT &result) const
Definition: ca_correct.cc:343
const float rscale
Definition: ca_correct.cc:169
static double d_from_extent(double d)
Definition: ca_correct.cc:187
static double rscale_from_wh(double w, double h)
Definition: ca_correct.cc:192
const float x_shift
Definition: ca_correct.cc:182
ev_radial_correction(const double &_width, const double &_height, const double &_x_shift, const double &_y_shift, const double &_a, const double &_b, const double &_c, const double &_d)
Definition: ca_correct.cc:205
const float y_shift
Definition: ca_correct.cc:182
void eval(const CRD &in, CRD &result) const
Definition: ca_correct.cc:247
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
channel_view_type get_channel_view(const int &channel)
get a bspline object for a single channel of the data. This is lightweight and requires the viewed da...
Definition: bspline.h:730
finally we define class mapper which is initialized with a set of gate objects (of arbitrary type) wh...
Definition: map.h:468
void eval(const in_type &in, out_type &out) const
Definition: map.h:547
mirror gate 'folds' coordinates into the range. From the infinite number of mirror images resulting f...
Definition: map.h:295
class unary_functor provides a functor object which offers a system of types for concrete unary funct...
with the definition of 'simd_traits', we can proceed to implement 'vector_traits': struct vector_trai...
Definition: vector.h:344
includes all headers from vspline (most of them indirectly)