LensSerious 0.1
Lens-correction mathematics as data, not as a library of callbacks
Loading...
Searching...
No Matches
lensserious.c
Go to the documentation of this file.
1/*
2 LensSerious — implementation.
3
4 Copyright (C) 2026 Aurélien PIERRE.
5 Ported from Lensfun 0.3.4, Copyright (C) 2007 Andrew Zabolotny and contributors.
6 License: LGPL-3.0-or-later (see LICENSE).
7
8 PORTING RULE, load-bearing: every numeric convention below reproduces lensfun 0.3.4
9 behaviour ON PURPOSE, including the ones that look like bugs. The (width−1) sizing,
10 the one-sided Hermite tangents, evaluating interpolation weights in the coefficient
11 domain — a deviation here is not a cleanup, it silently moves every pixel of every
12 corrected image. tests/parity_lensfun.c is the arbiter; change nothing the harness
13 cannot re-verify.
14*/
15
16#include "lensserious.h"
17#include "lensserious_eval.h"
18
19#if defined(__GNUC__) || defined(__clang__)
20 #define LS_RESTRICT __restrict__
21#else
22 #define LS_RESTRICT
23#endif
24
25#include <float.h>
26#include <math.h>
27#include <string.h>
28
29/* ------------------------------------------------------------------------- */
30/* Interpolation — ported from auxfun.cpp:_lf_interpolate() and */
31/* lens.cpp:__insert_spline()/InterpolateDistortion()/InterpolateTCA(). */
32/* ------------------------------------------------------------------------- */
33
34/* Hermite with one-sided tangents at the ends: y1/y4 == FLT_MAX means "no outer
35 * neighbour", and the tangent degrades to the chord. Upstream auxfun.cpp:441. */
36static float _interpolate(float y1, float y2, float y3, float y4, float t)
37{
38 const float t2 = t * t;
39 const float t3 = t2 * t;
40 const float tg2 = (y1 == FLT_MAX) ? y3 - y2 : (y3 - y1) * 0.5f;
41 const float tg3 = (y4 == FLT_MAX) ? y3 - y2 : (y4 - y2) * 0.5f;
42 return (2.f * t3 - 3.f * t2 + 1.f) * y2 + (t3 - 2.f * t2 + t) * tg2
43 + (-2.f * t3 + 3.f * t2) * y3 + (t3 - t2) * tg3;
44}
45
46/* Keep the two nearest calibrations on each side of the target, by signed distance.
47 * Upstream lens.cpp:802. Indices: [0] far-below, [1] near-below, [2] near-above,
48 * [3] far-above. */
49typedef struct spline_t { const void *v[4]; float d[4]; } spline_t;
50
51static void _spline_init(spline_t *s)
52{
53 memset(s->v, 0, sizeof(s->v));
54 s->d[0] = s->d[1] = -FLT_MAX;
55 s->d[2] = s->d[3] = FLT_MAX;
56}
57
58static void _spline_insert(spline_t *s, float dist, const void *val)
59{
60 if(dist < 0)
61 {
62 if(dist > s->d[1]) { s->d[0] = s->d[1]; s->d[1] = dist; s->v[0] = s->v[1]; s->v[1] = val; }
63 else if(dist > s->d[0]) { s->d[0] = dist; s->v[0] = val; }
64 }
65 else
66 {
67 if(dist < s->d[2]) { s->d[3] = s->d[2]; s->d[2] = dist; s->v[3] = s->v[2]; s->v[2] = val; }
68 else if(dist < s->d[3]) { s->d[3] = dist; s->v[3] = val; }
69 }
70}
71
72/* Upstream interpolates COEFFICIENTS across focal, with per-term scaling hooks
73 * (__parameter_scales). For every model in the shipping database those scales are
74 * identity for distortion and index<2 of TCA uses focal-domain scaling — ported below
75 * exactly as the 0.3.4 code has it (lens.cpp:841): distortion: none; TCA terms 0..1:
76 * none (the switch body falls through empty for LINEAR/POLY3 v-terms in 0.3.4). */
77/* <real-focal-length>, interpolated the same way as everything else here: a Catmull-Rom
78 * spline over the four nearest points, an exact match short-circuiting it. The one
79 * difference from the distortion interpolation next door is that there is NO focal scaling
80 * -- upstream's __parameter_scales leaves real-focal alone, and rightly so: it is a length,
81 * not a coefficient whose magnitude tracks 1/focal. */
82static int _interp_real_focal(const ls_lens_t *lens, float focal, float *res)
83{
84 if(lens->n_real_focal == 0) return 0;
86
87 for(int i = 0; i < lens->n_real_focal; i++)
88 {
89 const ls_calib_real_focal_t *c = &lens->real_focal[i];
90 if(c->real_focal == 0.f) continue; /* upstream skips these outright */
91 const float df = focal - c->focal;
92 if(df == 0.0f) { *res = c->real_focal; return 1; }
93 _spline_insert(&s, df, c);
94 }
95 const ls_calib_real_focal_t *lo = (const ls_calib_real_focal_t *)s.v[1];
96 const ls_calib_real_focal_t *hi = (const ls_calib_real_focal_t *)s.v[2];
97 if(!lo || !hi)
98 {
99 if(lo) { *res = lo->real_focal; return 1; }
100 if(hi) { *res = hi->real_focal; return 1; }
101 return 0;
102 }
103 const ls_calib_real_focal_t *fl = (const ls_calib_real_focal_t *)s.v[0];
104 const ls_calib_real_focal_t *fh = (const ls_calib_real_focal_t *)s.v[3];
105 const float t = (focal - lo->focal) / (hi->focal - lo->focal);
106 *res = _interpolate(fl ? fl->real_focal : FLT_MAX, lo->real_focal, hi->real_focal,
107 fh ? fh->real_focal : FLT_MAX, t);
108 return 1;
109}
110
111/* Hugin's focal-length convention, as a multiplier on the nominal focal
112 * (modifier.cpp: get_hugin_focal_correction). It is derived from the distortion
113 * coefficients ALREADY interpolated at this focal, not from a separate fit. */
114static float _hugin_focal_correction(const ls_calib_dist_t *dist, int have_dist)
115{
116 if(!have_dist) return 1.f;
117 if(dist->model == LS_DIST_POLY3) return 1.f - dist->terms[0];
118 if(dist->model == LS_DIST_PTLENS)
119 return 1.f - dist->terms[0] - dist->terms[1] - dist->terms[2];
120 return 1.f;
121}
122
123static int _interp_dist(const ls_lens_t *lens, float focal, ls_calib_dist_t *res)
124{
125 if(lens->n_dist == 0) return 0;
126 spline_t s; _spline_init(&s);
128
129 for(int i = 0; i < lens->n_dist; i++)
130 {
131 const ls_calib_dist_t *c = &lens->dist[i];
132 if(c->model == LS_DIST_NONE) continue;
133 if(model == LS_DIST_NONE) model = c->model;
134 else if(model != c->model) continue; /* first model wins, upstream warning case */
135 const float df = focal - c->focal;
136 if(df == 0.0f) { *res = *c; return 1; }
137 _spline_insert(&s, df, c);
138 }
139 const ls_calib_dist_t *lo = (const ls_calib_dist_t *)s.v[1];
140 const ls_calib_dist_t *hi = (const ls_calib_dist_t *)s.v[2];
141 if(!lo || !hi)
142 {
143 if(lo) { *res = *lo; return 1; }
144 if(hi) { *res = *hi; return 1; }
145 return 0;
146 }
147 res->model = model;
148 res->focal = focal;
149 const float t = (focal - lo->focal) / (hi->focal - lo->focal);
150 const ls_calib_dist_t *fl = (const ls_calib_dist_t *)s.v[0];
151 const ls_calib_dist_t *fh = (const ls_calib_dist_t *)s.v[3];
152 /* Upstream __parameter_scales (lens.cpp:841) leaves the FOCALS in for every distortion
153 * model: coefficients are interpolated in the term×focal domain, then divided by the
154 * target focal. The first harness run against the raw-coefficient version measured up
155 * to 4.9 px of divergence at interpolated focals -- this scaling is load-bearing. */
156 for(int i = 0; i < 3; i++)
157 res->terms[i] = _interpolate(fl ? fl->terms[i] * fl->focal : FLT_MAX,
158 lo->terms[i] * lo->focal, hi->terms[i] * hi->focal,
159 fh ? fh->terms[i] * fh->focal : FLT_MAX, t) / focal;
160 return 1;
161}
162
163static int _interp_tca(const ls_lens_t *lens, float focal, ls_calib_tca_t *res)
164{
165 if(lens->n_tca == 0) return 0;
166 spline_t s; _spline_init(&s);
168
169 for(int i = 0; i < lens->n_tca; i++)
170 {
171 const ls_calib_tca_t *c = &lens->tca[i];
172 if(c->model == LS_TCA_NONE) continue;
173 if(model == LS_TCA_NONE) model = c->model;
174 else if(model != c->model) continue;
175 const float df = focal - c->focal;
176 if(df == 0.0f) { *res = *c; return 1; }
177 _spline_insert(&s, df, c);
178 }
179 const ls_calib_tca_t *lo = (const ls_calib_tca_t *)s.v[1];
180 const ls_calib_tca_t *hi = (const ls_calib_tca_t *)s.v[2];
181 if(!lo || !hi)
182 {
183 if(lo) { *res = *lo; return 1; }
184 if(hi) { *res = *hi; return 1; }
185 return 0;
186 }
187 res->model = model;
188 res->focal = focal;
189 const float t = (focal - lo->focal) / (hi->focal - lo->focal);
190 const ls_calib_tca_t *fl = (const ls_calib_tca_t *)s.v[0];
191 const ls_calib_tca_t *fh = (const ls_calib_tca_t *)s.v[3];
192 /* Same term×focal domain as distortion, EXCEPT terms 0..1 (vr/vb, kr/kb), which
193 * __parameter_scales exempts by setting the scales to 1.0. */
194 for(int i = 0; i < 6; i++)
195 {
196 const float sl0 = (i < 2) ? 1.f : (fl ? fl->focal : 1.f);
197 const float sl1 = (i < 2) ? 1.f : lo->focal;
198 const float sl2 = (i < 2) ? 1.f : hi->focal;
199 const float sl3 = (i < 2) ? 1.f : (fh ? fh->focal : 1.f);
200 const float sl4 = (i < 2) ? 1.f : focal;
201 res->terms[i] = _interpolate(fl ? fl->terms[i] * sl0 : FLT_MAX,
202 lo->terms[i] * sl1, hi->terms[i] * sl2,
203 fh ? fh->terms[i] * sl3 : FLT_MAX, t) / sl4;
204 }
205 return 1;
206}
207
208/* Vignetting — verbatim port of lfLens::InterpolateVignetting() and __vignetting_dist()
209 * (lens.cpp): inverse-distance weighting with p = 3.5 over ALL calibration points, in a
210 * space where focal is normalized by the lens's whole range, aperture enters as 4/A and
211 * distance as 0.1/D. An exact hit (< 1e-4) short-circuits; a nearest point further than
212 * 1.0 rejects the whole interpolation. The first version of this file used an ad-hoc
213 * metric here; the harness measured multiplier deltas up to 3.65 against upstream and
214 * this port is what removed them. */
215static float _vig_dist(const ls_lens_t *lens, const ls_calib_vig_t *c,
216 float focal, float aperture, float distance)
217{
218 float f1 = focal - lens->min_focal;
219 float f2 = c->focal - lens->min_focal;
220 const float df = lens->max_focal - lens->min_focal;
221 if(df != 0.f) { f1 /= df; f2 /= df; }
222 const float a1 = 4.f / aperture;
223 const float a2 = 4.f / c->aperture;
224 const float d1 = 0.1f / distance;
225 const float d2 = 0.1f / c->distance;
226 return sqrtf((f2 - f1) * (f2 - f1) + (a2 - a1) * (a2 - a1) + (d2 - d1) * (d2 - d1));
227}
228
229static int _interp_vig(const ls_lens_t *lens, float focal, float aperture, float distance,
230 ls_calib_vig_t *res)
231{
232 if(lens->n_vig == 0) return 0;
234 float total_weighting = 0.f;
235 float smallest = FLT_MAX;
236 float terms[3] = { 0.f, 0.f, 0.f };
237
238 for(int i = 0; i < lens->n_vig; i++)
239 {
240 const ls_calib_vig_t *c = &lens->vig[i];
241 if(c->model == LS_VIG_NONE) continue;
242 if(model == LS_VIG_NONE) model = c->model;
243 else if(model != c->model) continue;
244
245 const float dist = _vig_dist(lens, c, focal, aperture, distance);
246 if(dist < 0.0001f) { *res = *c; return 1; }
247 if(dist < smallest) smallest = dist;
248 /* Upstream computes the weight in DOUBLE (fabs(1.0 / pow(dist, 3.5))) and only then
249 * truncates: near an exact hit d^3.5 ~ 1e-14 and the two arithmetics disagree enough
250 * to change the mixture -- the harness measured 0.77 of multiplier drift for it. */
251 const float w = (float)fabs(1.0 / pow((double)dist, 3.5));
252 for(int k = 0; k < 3; k++) terms[k] += w * c->terms[k];
253 total_weighting += w;
254 }
255 if(smallest > 1.f) return 0;
256 if(total_weighting <= 0.f || smallest == FLT_MAX) return 0;
257
258 res->model = model;
259 res->focal = focal; res->aperture = aperture; res->distance = distance;
260 for(int k = 0; k < 3; k++) res->terms[k] = terms[k] / total_weighting;
261 return 1;
262}
263
264/* ------------------------------------------------------------------------- */
265/* Coordinate system — ported from lfModifier::lfModifier() (modifier.cpp:203) */
266/* ------------------------------------------------------------------------- */
267
269 float crop, int width, int height,
270 float focal, float aperture, float distance,
271 float scale, int target_type, int flags, int reverse)
272{
273 memset(mod, 0, sizeof(*mod));
274 if(!lens || crop <= 0.f) return 0;
275
276 /* "The '- 1' is due to the fact that Width and Height are measured at the pixel
277 * centres (they are actually transformed) instead of at their outer rims." */
278 const float w = (width >= 2) ? (float)(width - 1) : 1.f;
279 const float h = (height >= 2) ? (float)(height - 1) : 1.f;
280 mod->width = w; mod->height = h;
281
282 const float size = (w < h) ? w : h;
283 const float image_aspect_ratio = (w < h) ? h / w : w / h;
284
285 const float calibration_cropfactor = lens->crop_factor;
286 const float ar = (lens->aspect_ratio > 0.f) ? lens->aspect_ratio : 1.5f;
287 const float aspect_ratio_correction = sqrtf(ar * ar + 1.f);
288
289 const float coordinate_correction =
290 1.f / sqrtf(image_aspect_ratio * image_aspect_ratio + 1.f)
291 * calibration_cropfactor / crop * aspect_ratio_correction;
292
293 mod->norm_scale = 2.f / size * coordinate_correction;
294 mod->norm_unscale = size * 0.5f / coordinate_correction;
295 mod->aspect_ratio_correction = aspect_ratio_correction;
296 mod->center_x = (w / size + lens->center_x) * coordinate_correction;
297 mod->center_y = (h / size + lens->center_y) * coordinate_correction;
298
299 /* A projection change is radial for every type except panoramic and equirectangular,
300 * which treat the axes differently (see ls_eval_geometry()). Report those rather than
301 * approximate them. */
302 const int from = (int)lens->type;
303 const int to = (target_type == LS_LENS_UNKNOWN) ? from : target_type;
304 const int radial = (from != LS_LENS_PANORAMIC && from != LS_LENS_EQUIRECTANGULAR
306 && from != LS_LENS_UNKNOWN && to != LS_LENS_UNKNOWN);
307 mod->geom_from = from;
308 mod->geom_to = to;
309 /* The projection focal, in the same normalized units as a radius.
310 *
311 * MEASURED, not derived: liblensfun's own geometry callback was fitted over lenses
312 * spanning crop factors 1.0 to 7.66 and aspect ratios 1.143 to 1.5, and
313 *
314 * f_norm = focal * lens_crop_factor * sqrt(ar^2 + 1) / 21.633307
315 *
316 * reproduces all of them to five digits. The denominator is half the diagonal of a
317 * 36x24 frame, so the numerator is that diagonal reduced to the CALIBRATION sensor and
318 * then to its short side -- which is what normalized radius 1.0 means here.
319 *
320 * The SHOOTING crop deliberately does not appear: it is already inside norm_scale, and
321 * a first version that used a bare focal/12 (fitted on 3:2 lenses alone, where the two
322 * happen to coincide) was out by 284 px on a 4:3 compact. */
323 /* The focal the PROJECTION runs on, which is not always the one engraved on the barrel.
324 * lensfun's geometry callback uses GetRealFocalLength(focal) divided by
325 * get_hugin_focal_correction(focal) (modifier.cpp), and that resolves to three cases:
326 *
327 * - a lens carrying <real-focal-length> data: GetRealFocalLength returns it and returns
328 * EARLY, before the hugin multiplication -- so the division does not cancel and the
329 * geometry focal is real_focal / hugin;
330 * - a lens without it: GetRealFocalLength multiplies the hugin factor in and the caller
331 * divides it straight back out, leaving exactly the nominal focal. Measured across
332 * every fisheye in the database, ratio 1.0000.
333 *
334 * Getting this wrong is not subtle. On the Sigma 4.5mm circular fisheye the geometry
335 * focal is 0.4727x the nominal one, and using the nominal is 28 px out at the CENTRE of
336 * the frame and 135 px at the edge. Carrying the calibration points is what lets those
337 * lenses be corrected at all rather than declined. */
338 float geom_focal_mm = focal;
339 {
340 /* Unconditionally, and deliberately not from mod->dist: upstream derives the hugin
341 * factor from the distortion calibration whether or not the caller asked for
342 * distortion to be corrected, so gating it on the enable flags would change the
343 * projection depending on an unrelated request. */
344 ls_calib_dist_t hugin_dist;
345 const int have_dist = _interp_dist(lens, focal, &hugin_dist);
346 float real_focal = 0.f;
347 if(_interp_real_focal(lens, focal, &real_focal) && real_focal > 0.f)
348 geom_focal_mm = real_focal / _hugin_focal_correction(&hugin_dist, have_dist);
349 }
350
351 mod->geom_focal = geom_focal_mm * lens->crop_factor * aspect_ratio_correction
353 mod->geometry_unsupported = (from != to) && !radial;
354
355 /* The composition ORDER is settled and is the one below: lensfun's correction chain is
356 * scale (100), geometry (500), distortion (750, ModifyCoord_Dist_* -- the 250 UnDist_*
357 * variants are the reverse direction), then TCA as a subpixel callback after every
358 * coordinate callback. An earlier reading of this file's own header suggested the
359 * reverse and was wrong. */
360
361 mod->reverse = reverse ? 1 : 0;
362
363 /* The reverse direction swaps the projection endpoints (modifier.cpp: reverse ?
364 * AddCoordCallbackGeometry(targeom, lens->Type, ...) : the other way round). Everything
365 * downstream reads geom_from/geom_to, so doing it once here keeps the evaluator free of
366 * the distinction. */
367 if(mod->reverse)
368 {
369 const int swap = mod->geom_from;
370 mod->geom_from = mod->geom_to;
371 mod->geom_to = swap;
372 }
373
374 int enabled = 0;
375 if(from != to && radial && focal > 0.f) enabled |= LS_ENABLE_GEOMETRY;
376 if((flags & LS_ENABLE_DISTORTION) && _interp_dist(lens, focal, &mod->dist))
377 enabled |= LS_ENABLE_DISTORTION;
378 if((flags & LS_ENABLE_TCA) && _interp_tca(lens, focal, &mod->tca))
379 enabled |= LS_ENABLE_TCA;
380 if((flags & LS_ENABLE_VIGNETTING) && _interp_vig(lens, focal, aperture, distance, &mod->vig))
381 enabled |= LS_ENABLE_VIGNETTING;
382 if((flags & LS_ENABLE_SCALE) && scale != 1.0f && scale > 0.f)
383 {
384 /* Upstream stores 1/scale for the correction pass and `scale` itself for the reverse
385 * one, and moves the callback from priority 100 to 900 (mod-coord.cpp,
386 * AddCoordCallbackScale). Both halves of that matter; the order lives in ls_eval_map. */
387 mod->scale = mod->reverse ? scale : 1.f / scale;
388 enabled |= LS_ENABLE_SCALE;
389 }
390 else
391 mod->scale = 1.f;
392
393 /* Two models have no closed inverse at every coefficient, and upstream simply refuses
394 * the axis rather than producing something wrong: AddCoordCallbackDistortion returns
395 * false for poly3 with k1 = 0 (its reverse form is 1/k1), and AddSubpixelCallbackTCA
396 * returns false for linear TCA with a zero term (same reason). A refused axis is absent
397 * from lensfun's oflags, so it must be absent from ours. */
398 if(mod->reverse && (enabled & LS_ENABLE_DISTORTION)
399 && mod->dist.model == LS_DIST_POLY3 && mod->dist.terms[0] == 0.f)
400 enabled &= ~LS_ENABLE_DISTORTION;
401 if(mod->reverse && (enabled & LS_ENABLE_TCA) && mod->tca.model == LS_TCA_LINEAR
402 && (mod->tca.terms[0] == 0.f || mod->tca.terms[1] == 0.f))
403 enabled &= ~LS_ENABLE_TCA;
404
405 mod->enabled = enabled;
406 return enabled;
407}
408
409/* ------------------------------------------------------------------------- */
410/* The map. The closed forms themselves live in include/lensserious_eval.h, */
411/* which the OpenCL kernel includes verbatim: the CPU and the GPU evaluate */
412/* one source text, so they cannot drift apart between releases. */
413/* ------------------------------------------------------------------------- */
414
415/* ls_eval_from_modifier() casts the model enums straight to int, and the kernel compares
416 * the result against the LS_EVAL_* mirrors in lensserious_eval.h -- which cannot name the
417 * enums, since it must also compile as OpenCL C. Renumbering an enum without touching its
418 * mirror would make every GPU render evaluate the wrong model, silently and only on the
419 * GPU. Cheapest possible place to catch that is here, at compile time. */
420_Static_assert((int)LS_DIST_NONE == LS_EVAL_DIST_NONE, "distortion model mirror drifted");
421_Static_assert((int)LS_DIST_POLY3 == LS_EVAL_DIST_POLY3, "distortion model mirror drifted");
422_Static_assert((int)LS_DIST_POLY5 == LS_EVAL_DIST_POLY5, "distortion model mirror drifted");
423_Static_assert((int)LS_DIST_PTLENS == LS_EVAL_DIST_PTLENS, "distortion model mirror drifted");
424_Static_assert((int)LS_DIST_KNOTS == LS_EVAL_DIST_KNOTS, "distortion model mirror drifted");
425_Static_assert((int)LS_TCA_NONE == LS_EVAL_TCA_NONE, "TCA model mirror drifted");
426_Static_assert((int)LS_TCA_LINEAR == LS_EVAL_TCA_LINEAR, "TCA model mirror drifted");
427_Static_assert((int)LS_TCA_POLY3 == LS_EVAL_TCA_POLY3, "TCA model mirror drifted");
428_Static_assert((int)LS_VIG_NONE == LS_EVAL_VIG_NONE, "vignetting model mirror drifted");
429_Static_assert((int)LS_VIG_PA == LS_EVAL_VIG_PA, "vignetting model mirror drifted");
430_Static_assert((int)LS_VIG_KNOTS == LS_EVAL_VIG_KNOTS, "vignetting model mirror drifted");
431
432_Static_assert(LS_ENABLE_DISTORTION == LS_EVAL_ENABLE_DISTORTION, "enable bit mirror drifted");
433_Static_assert(LS_ENABLE_TCA == LS_EVAL_ENABLE_TCA, "enable bit mirror drifted");
434_Static_assert(LS_ENABLE_VIGNETTING == LS_EVAL_ENABLE_VIGNETTING, "enable bit mirror drifted");
435_Static_assert(LS_ENABLE_SCALE == LS_EVAL_ENABLE_SCALE, "enable bit mirror drifted");
436_Static_assert(LS_ENABLE_GEOMETRY == LS_EVAL_ENABLE_GEOMETRY, "enable bit mirror drifted");
437_Static_assert((int)LS_LENS_RECTILINEAR == LS_EVAL_LENS_RECTILINEAR, "lens type mirror drifted");
438_Static_assert((int)LS_LENS_FISHEYE_THOBY == LS_EVAL_LENS_FISHEYE_THOBY, "lens type mirror drifted");
439
440/* ls_eval_t crosses to the device as a by-value kernel argument, so host and device must
441 * lay it out identically. Scalars only is what guarantees that; this pins the consequence
442 * so adding a float2 (or a double, or a bool) fails to build instead of corrupting every
443 * field after it. */
444_Static_assert(sizeof(ls_eval_t) == 8 * sizeof(float) /* the coordinate system */
445 + 4 * sizeof(int) /* model ids and enable bits */
446 + 2 * sizeof(int) /* geom_from, geom_to */
447 + 2 * sizeof(float) /* geom_focal, reverse */
448 + 12 * sizeof(float) /* the terms */
449 + 3 * sizeof(int) /* knot_axes, knot_n, knot_vn */
450 + (6 * LS_MAX_KNOTS /* knot_r, knot_c */
451 + 2 * LS_MAX_KNOTS) /* knot_vr, knot_v */
452 * sizeof(float),
453 "ls_eval_t gained padding or a member: check it is still scalar-only");
454
455/* The whole point of the by-value transport is that it fits in a kernel argument list, and
456 * the smallest CL_DEVICE_MAX_PARAMETER_SIZE OpenCL 1.2 guarantees is 1024 bytes -- for ALL
457 * of a kernel's arguments together, not for this one. The consumer's widest kernel spends
458 * about 80 bytes on everything else (two images, six ints, two flags), so this leaves that
459 * headroom and still fails the build rather than the device if the tables ever grow. */
460_Static_assert(sizeof(ls_eval_t) <= 1024 - 128,
461 "ls_eval_t no longer fits a guaranteed OpenCL kernel argument list");
462_Static_assert(_Alignof(ls_eval_t) == _Alignof(float), "ls_eval_t alignment is no longer 4");
463
464/* How far outside the frame a point landed, as a signed distance: negative inside,
465 * positive outside, zero exactly on the edge. Upstream's AutoscaleResidualDistance(). */
466static float _autoscale_residual(const ls_eval_t *p, const float max_x, const float max_y,
467 const float x, const float y)
468{
469 float r = x - max_x;
470 float t = -max_x - x; if(t > r) r = t;
471 t = y - max_y; if(t > r) r = t;
472 t = -max_y - y; return (t > r) ? t : r;
473 (void)p;
474}
475
476/* What a point with no source pixel counts as, while SEARCHING. ls_eval_map() answers NaN
477 * there, which is the right answer for a renderer and a useless one for Newton: the search
478 * has to be able to tell "far outside" from "further outside" to walk back in. Upstream has
479 * no such split -- its geometry callbacks answer with this very constant
480 * (mod-coord.cpp: `if (theta >= M_PI / 2.0) rho = 1.6e16F`) and the search consumes it like
481 * any other coordinate. Using the same number here reproduces the same trajectory: the
482 * residual is proportional to ru, so the first step collapses ru towards zero and the
483 * iteration then walks back out to the edge. Aborting the search instead -- which is what
484 * this did first -- leaves the maximum to be set by whichever points did resolve, and
485 * measured 99.8 against upstream's 1.219 on the Canon EF 8-15mm reversed. */
486#define LS_GEOM_SENTINEL 1.6e16f
487
488/* The radius, along one direction, whose transformed point lands exactly on the frame edge.
489 * Newton with a NUMERIC derivative, because the chain has no closed inverse -- and with
490 * upstream's dx-doubling escape for when the two probes are too close to tell apart. */
491static float _autoscale_distance(const ls_eval_t *p, const float ca, const float sa,
492 const float dist, const float max_x, const float max_y)
493{
494 float ru = dist;
495 float dx = 1e-4f;
496
497 for(int countdown = 50; ; countdown--)
498 {
499 float x = ca * ru, y = sa * ru;
500 if(!ls_eval_coord_chain(p, 1, &x, &y)) { x = LS_GEOM_SENTINEL * ca * ru;
501 y = LS_GEOM_SENTINEL * sa * ru; }
502 const float rd = _autoscale_residual(p, max_x, max_y, x, y);
503 /* Upstream's NEWTON_EPS * 100. */
504 if(rd > -1e-3f && rd < 1e-3f) return ru;
505 if(!countdown) return -1.f; /* e.g. an ultrawide fisheye corner extending to infinity */
506
507 float x1 = ca * (ru + dx), y1 = sa * (ru + dx);
508 if(!ls_eval_coord_chain(p, 1, &x1, &y1)) { x1 = LS_GEOM_SENTINEL * ca * (ru + dx);
509 y1 = LS_GEOM_SENTINEL * sa * (ru + dx); }
510 const float rd1 = _autoscale_residual(p, max_x, max_y, x1, y1);
511
512 /* Too close to tell apart in this precision: widen the probe rather than divide by
513 * something that is mostly rounding. */
514 if(LS_FABS(rd1 - rd) < 1e-5f) { dx *= 2.f; continue; }
515
516 ru -= rd / ((rd1 - rd) / dx);
517 }
518}
519
520/* ------------------------------------------------------------------------- */
521/* Embedded maker profiles — the second source of correction data. */
522/* ------------------------------------------------------------------------- */
523
524/* A table is only usable if its radii ascend: the lookup walks them in order and stops at
525 * the first one it does not exceed, so an out-of-order axis reads the wrong segment rather
526 * than failing. Ties are refused too -- ls_eval_knot_lookup() returns the left value on a
527 * zero-width segment, which is defined but is not an interpolation.
528 *
529 * Worth checking rather than assuming, because the reverse direction CONSTRUCTS an axis
530 * (r*cor(r)) instead of receiving one, and that product is only monotone while the profile
531 * is physically sensible. A correction so strong it folds the image back on itself has no
532 * inverse to build, and the honest answer is to decline the axis. */
533static int _knot_axis_ascends(const float *xs, const int n)
534{
535 for(int i = 1; i < n; i++)
536 if(!(xs[i] > xs[i - 1])) return 0;
537 return 1;
538}
539
541 int width, int height, float scale, int flags, int reverse)
542{
543 if(!mod) return 0;
544 memset(mod, 0, sizeof(*mod));
545 if(!knots || width < 1 || height < 1) return 0;
546
547 /* The makers' coordinate system, adopted rather than converted: the distance from the
548 * image centre over half the image diagonal, so 1.0 lands on the far corner. Note the
549 * centre is at width/2, NOT at (width-1)/2 -- lensfun's pixel-rim convention is its own,
550 * and applying it to numbers measured under a different one would move every pixel by
551 * half of one.
552 *
553 * aspect_ratio_correction stays 1: it exists to reconcile lensfun's two normalizations
554 * (distortion against the short side, vignetting against the diagonal), and the makers
555 * index both of theirs the same way. */
556 const float w2 = (float)width * 0.5f;
557 const float h2 = (float)height * 0.5f;
558 const float rn = sqrtf(w2 * w2 + h2 * h2);
559 if(!(rn > 0.f)) return 0;
560
561 mod->width = (float)width;
562 mod->height = (float)height;
563 mod->norm_scale = 1.f / rn;
564 mod->norm_unscale = rn;
565 mod->aspect_ratio_correction = 1.f;
566 mod->center_x = w2 / rn;
567 mod->center_y = h2 / rn;
568
569 /* No projection change: the profile describes the lens as it shipped, in the projection
570 * it shipped with. Leaving both endpoints UNKNOWN keeps ls_eval_coord_chain()'s geometry
571 * stage switched off rather than running an identity through the fisheye transcendentals. */
574 mod->geom_focal = 0.f;
575 mod->reverse = reverse ? 1 : 0;
576
577 int enabled = 0;
578
579 if(scale != 1.f && scale > 0.f)
580 {
581 /* Same two halves as ls_modifier_init(): the stored factor is reciprocated for the
582 * correcting pass, and the stage moves from first to last in the reverse one. */
583 mod->scale = mod->reverse ? scale : 1.f / scale;
584 enabled |= LS_ENABLE_SCALE;
585 }
586 else
587 mod->scale = 1.f;
588
589 /* What the caller asked this table to serve. A caller wanting the table's geometry but
590 * another source's chromatic aberration passes DISTORTION alone, and the evaluator then
591 * runs every channel down the green curve. */
593
594 const int n = (knots->n > LS_MAX_KNOTS) ? LS_MAX_KNOTS : knots->n;
595 if((flags & LS_ENABLE_DISTORTION) && n > 0 && _knot_axis_ascends(knots->radius, n))
596 {
597 int usable = 1;
598 for(int c = 0; c < 3 && usable; c++)
599 {
600 for(int i = 0; i < n; i++)
601 {
602 const float r = knots->radius[i];
603 const float f = knots->cor_rgb[c][i];
604 if(!(f > 0.f)) { usable = 0; break; }
605
606 if(mod->reverse)
607 {
608 /* Reading the same curve the other way round. The forward map sends radius r to
609 * r*cor(r), so the point (r*cor(r), 1/cor(r)) is on the inverse -- the radius the
610 * point arrives at, and the factor that takes it back. No solver: this is the one
611 * thing a table gives that a polynomial does not.
612 *
613 * Exact AT the knots, and second-order between them, because a segment that is
614 * straight going forwards is not straight coming back. That is the same class of
615 * error the input already carries -- the maker's own samples are themselves a
616 * straight-line reading of a smooth curve. Measured rather than argued: 0.13 px
617 * worst case on a 6000x4000 frame, tests/knots.c. */
618 mod->knot_r[c][i] = r * f;
619 mod->knot_c[c][i] = 1.f / f;
620 }
621 else
622 {
623 mod->knot_r[c][i] = r;
624 mod->knot_c[c][i] = f;
625 }
626 }
627 /* Forwards this re-checks the axis already checked above, which is the point: the
628 * reverse axis is CONSTRUCTED, one per channel, and only monotone while the profile
629 * is physically sensible. A correction strong enough to fold the image back on itself
630 * has no inverse to build. */
631 if(usable && !_knot_axis_ascends(mod->knot_r[c], n)) usable = 0;
632 }
633
634 if(usable)
635 {
636 mod->dist.model = LS_DIST_KNOTS;
637 mod->knot_n = n;
638 enabled |= LS_ENABLE_DISTORTION;
639 }
640 }
641
642 const int vn = (knots->vn > LS_MAX_KNOTS) ? LS_MAX_KNOTS : knots->vn;
643 if((flags & LS_ENABLE_VIGNETTING) && vn > 0 && _knot_axis_ascends(knots->vig_radius, vn))
644 {
645 mod->vig.model = LS_VIG_KNOTS;
646 mod->knot_vn = vn;
647 for(int i = 0; i < vn; i++)
648 {
649 mod->knot_vr[i] = knots->vig_radius[i];
650 mod->knot_v[i] = knots->vig[i];
651 }
652 enabled |= LS_ENABLE_VIGNETTING;
653 }
654
655 /* No LS_ENABLE_TCA, ever, and not an oversight: the chromatic part of a maker's profile
656 * is inside the distortion table, one curve per channel. Reporting it as a separate axis
657 * would invite a caller to run a TCA stage that has no coefficients to run on. */
658 mod->enabled = enabled;
659 return enabled;
660}
661
663{
664 if(!mod) return 1.f;
665
666 /* TCA moves each channel's radius slightly past the green one the frame was measured on,
667 * so upstream reserves a flat permille for it. It is a subpixel callback, not a
668 * coordinate one, and so is not part of the transform measured below. */
669 const float subpixel_scale = (mod->enabled & LS_ENABLE_TCA) ? 1.001f : 1.f;
671 return subpixel_scale;
672
673 ls_eval_t p;
674 if(!ls_eval_from_modifier(mod, &p)) return subpixel_scale;
675
676 const float w = mod->width, h = mod->height;
677 const float max_x = w * 0.5f * mod->norm_scale;
678 const float max_y = h * 0.5f * mod->norm_scale;
679
680 /* 3 2 1
681 * 4 0 the four edge midpoints and the four corners
682 * 5 6 7 */
683 const float corner = atanf(h / w);
684 const float pi = 3.14159265f;
685 const float angles[8] = { 0.f, corner,
686 pi / 2.f, pi - corner,
687 pi, pi + corner,
688 3.f * pi / 2.f, 2.f * pi - corner };
689 const float diag = sqrtf(w * w + h * h) * 0.5f * mod->norm_scale;
690 const float dists[8] = { max_x, diag, max_y, diag, max_x, diag, max_y, diag };
691
692 float scale = 0.01f;
693 for(int i = 0; i < 8; i++)
694 {
695 const float landed = _autoscale_distance(&p, cosf(angles[i]), sinf(angles[i]),
696 dists[i], max_x, max_y);
697 if(landed <= 0.f) continue; /* not found; this point cannot raise the maximum */
698 const float point_scale = dists[i] / landed;
699 if(point_scale > scale) scale = point_scale;
700 }
701
702 /* "1 permille is our limit of accuracy (in rare cases, we may be even worse, depending on
703 * what happens between the test points), so assure that we really have no black borders
704 * left." -- upstream, and it is right: the eight points do not bound what happens between
705 * them. */
706 scale *= 1.001f;
707 scale *= subpixel_scale;
708
709 return mod->reverse ? 1.f / scale : scale;
710}
711
712int ls_modifier_set_projection(ls_modifier_t *mod, const int from_type, const int to_type,
713 const float focal_mm, const float crop_factor)
714{
715 if(!mod) return 0;
716
717 /* Same test as ls_modifier_init(): every pair is radially expressible except the two that
718 * are not functions of radius alone. */
719 const int radial = (from_type != LS_LENS_PANORAMIC && from_type != LS_LENS_EQUIRECTANGULAR
720 && to_type != LS_LENS_PANORAMIC && to_type != LS_LENS_EQUIRECTANGULAR
721 && from_type != LS_LENS_UNKNOWN && to_type != LS_LENS_UNKNOWN);
722
723 mod->geometry_unsupported = (from_type != to_type) && !radial;
724
725 if(!(from_type != to_type && radial && focal_mm > 0.f))
726 {
727 /* Nothing to do -- and the modifier is left exactly as it was found, endpoints and all.
728 * Writing them anyway would be harmless only in theory: with from == to the stage is an
729 * identity mathematically, but it is an identity computed by sending every radius
730 * through an arc-tangent and back, and the float round trip does not return the same
731 * number. That showed up as a whole frame differing by up to 30/65535 on a lens whose
732 * projection nobody had asked to change. */
733 mod->enabled &= ~LS_ENABLE_GEOMETRY;
734 return 0;
735 }
736
737 mod->geom_from = from_type;
738 mod->geom_to = to_type;
739 mod->geom_focal = focal_mm * crop_factor * mod->aspect_ratio_correction
742 return 1;
743}
744
746{
747 if(!mod || !out) return 0;
748 memset(out, 0, sizeof(*out));
749
750 out->norm_scale = mod->norm_scale;
751 out->norm_unscale = mod->norm_unscale;
752 out->center_x = mod->center_x;
753 out->center_y = mod->center_y;
754 {
755 const float arc = (mod->aspect_ratio_correction > 0.f) ? mod->aspect_ratio_correction : 1.f;
756 const float inv_arc = 1.f / arc;
757 out->vig_scale = mod->norm_scale * inv_arc;
758 out->vig_center_x = mod->center_x * inv_arc;
759 out->vig_center_y = mod->center_y * inv_arc;
760 }
761 out->scale = mod->scale;
762 out->enabled = mod->enabled;
763
764 out->dist_model = (int)mod->dist.model;
765 out->tca_model = (int)mod->tca.model;
766 out->vig_model = (int)mod->vig.model;
767 out->geom_from = mod->geom_from;
768 out->geom_to = mod->geom_to;
769 out->geom_focal = mod->geom_focal;
770
771 for(int i = 0; i < 3; i++) out->dist_terms[i] = mod->dist.terms[i];
772 for(int i = 0; i < 6; i++) out->tca_terms[i] = mod->tca.terms[i];
773 for(int i = 0; i < 3; i++) out->vig_terms[i] = mod->vig.terms[i];
774
775 /* A maker's table, if this is one. Already per channel and already facing the requested
776 * direction -- ls_modifier_init_knots() did both -- so this is a copy and nothing else.
777 * Whichever kind of modifier this is, the other kind's fields stay at the memset zero,
778 * and dist_model/vig_model are what the evaluator reads to tell them apart. */
779 out->knot_axes = mod->knot_axes;
780 out->knot_n = mod->knot_n;
781 for(int c = 0; c < 3; c++)
782 for(int i = 0; i < mod->knot_n; i++)
783 {
784 out->knot_r[c][i] = mod->knot_r[c][i];
785 out->knot_c[c][i] = mod->knot_c[c][i];
786 }
787 out->knot_vn = mod->knot_vn;
788 for(int i = 0; i < mod->knot_vn; i++)
789 {
790 out->knot_vr[i] = mod->knot_vr[i];
791 out->knot_v[i] = mod->knot_v[i];
792 }
793
794 out->reverse = mod->reverse;
795 if(mod->reverse)
796 {
797 /* Two models are stored in a different FORM for the reverse pass, because upstream
798 * bakes the reciprocal into the callback's data block rather than taking it per pixel
799 * (mod-coord.cpp AddCoordCallbackDistortion, mod-subpix.cpp AddSubpixelCallbackTCA).
800 * Doing the same here keeps the per-pixel path free of a divide, and keeps the kernel
801 * -- which sees only this block -- free of the distinction.
802 *
803 * The zero cases cannot arrive: ls_modifier_init() has already cleared the enable bit
804 * for exactly the coefficients that would divide by zero here. */
805 /* Distortion terms stay as-is: the reverse solver uses the UNSCALED equation, not
806 * upstream's monic form, because dividing poly3 through by k1 makes the residual
807 * unresolvable in float for the small k1 real lenses have (see ls_eval_undist_factor). */
808 if(out->tca_model == LS_EVAL_TCA_LINEAR && (out->enabled & LS_ENABLE_TCA))
809 {
810 out->tca_terms[0] = 1.f / mod->tca.terms[0];
811 out->tca_terms[1] = 1.f / mod->tca.terms[1];
812 }
813 }
814
815 return 1;
816}
817
819{
820 if(!dst || !src) return 0;
821
822 /* Every field the vignetting evaluator reads, and no other. Listed rather than memcpy'd
823 * from an offset range because the two halves are interleaved in the struct: a range copy
824 * would work today and silently take a coordinate field the day someone reorders it. */
825 dst->vig_scale = src->vig_scale;
826 dst->vig_center_x = src->vig_center_x;
827 dst->vig_center_y = src->vig_center_y;
828 dst->vig_model = src->vig_model;
829 for(int i = 0; i < 3; i++) dst->vig_terms[i] = src->vig_terms[i];
830
831 dst->knot_vn = src->knot_vn;
832 for(int i = 0; i < src->knot_vn && i < LS_MAX_KNOTS; i++)
833 {
834 dst->knot_vr[i] = src->knot_vr[i];
835 dst->knot_v[i] = src->knot_v[i];
836 }
837
838 /* The enable bit follows the data, or the evaluator would run a model that is not there. */
839 dst->enabled = (dst->enabled & ~LS_EVAL_ENABLE_VIGNETTING)
841 return 1;
842}
843
845 float xu, float yu, int width, int height,
846 float *res)
847{
848 if(!mod || !res || width <= 0 || height <= 0) return 0;
849
850 ls_eval_t p;
851 ls_eval_from_modifier(mod, &p);
852
853 /* Exact per-pixel evaluation, identical to the OpenCL kernel because it IS the OpenCL
854 * kernel's code, and deliberately NOT bit-matched to upstream's row walker: lensfun's
855 * SSE path computes sqrt as _mm_rcp_ps(_mm_rsqrt_ps(r2)) -- two chained 12-bit
856 * approximations with no Newton step (mod-coord-sse.cpp) -- and disagrees with its own
857 * scalar math by up to 0.19 px at the end of a 6016-wide row. Matching that would mean
858 * reimplementing an approximation error. LensSerious matches upstream's SCALAR semantics
859 * to < 0.01 px; against upstream's SSE rows the residual is bounded by upstream's own
860 * approximation, and the parity harness asserts both bounds separately. */
861 for(int row = 0; row < height; row++)
862 {
863 float *out = res + (size_t)row * width * 6;
864 const float y = yu + (float)row;
865 for(int col = 0; col < width; col++, out += 6)
866 ls_eval_map(&p, xu + (float)col, y, out);
867 }
868 return 1;
869}
870
872 float xu, float yu, int width, int height,
873 float *rgba, int row_stride_bytes)
874{
875 if(!mod || !rgba || width <= 0 || height <= 0) return 0;
876 if(!(mod->enabled & LS_ENABLE_VIGNETTING)) return 0;
877
878 ls_eval_t p;
879 ls_eval_from_modifier(mod, &p);
880
881 const size_t stride = row_stride_bytes ? (size_t)row_stride_bytes / sizeof(float)
882 : (size_t)width * 4;
883 const float vs = p.vig_scale;
884 const float vcx = p.vig_center_x;
885
886 /* Two passes over a block, and the reason is the divide.
887 *
888 * Written as one loop -- derive the multiplier, then scale the pixel -- the compiler
889 * vectorises only the four-component store, because the store is what looks like a
890 * vector. The 1/c stays scalar, one divide per pixel, and a single-precision divide is
891 * the most expensive thing in this function by a wide margin.
892 *
893 * Computing the multipliers for a block FIRST gives a loop with no stores to the image
894 * in it, which vectorises across PIXELS: four divides per instruction. The second pass
895 * is then pure multiply-and-store. Measured on 24 Mpx, this is what took the function
896 * from 1.8x slower than lensfun's hand-written SSE2 to parity with it.
897 *
898 * The block is sized to stay in L1 alongside the pixels it is about to scale. */
899 enum { LS_VIG_BLOCK = 256 };
900 float mbuf[LS_VIG_BLOCK];
901
902 for(int row = 0; row < height; row++)
903 {
904 float *LS_RESTRICT px = rgba + (size_t)row * stride;
905
906 /* Everything constant along the row is computed once. The x coordinate is still
907 * derived from the absolute column rather than stepped incrementally, so this stays
908 * bit-identical to what ls_eval_vignette_factor() produces in a kernel -- upstream
909 * steps r2 by a recurrence, which is cheaper still and not reproducible per work-item. */
910 const float y = (yu + (float)row) * vs - p.vig_center_y;
911 const float yy = y * y;
912
913 for(int col0 = 0; col0 < width; col0 += LS_VIG_BLOCK)
914 {
915 const int n = (width - col0 < LS_VIG_BLOCK) ? (width - col0) : LS_VIG_BLOCK;
916
917 for(int i = 0; i < n; i++)
918 {
919 const float x = (xu + (float)(col0 + i)) * vs - vcx;
920 mbuf[i] = ls_eval_vignette_from_r2(&p, x * x + yy);
921 }
922
923 for(int i = 0; i < n; i++, px += 4)
924 {
925 const float m = mbuf[i];
926 /* Only THREE components change, but all four are multiplied and the fourth put
927 * back. The fourth is not a colour and there is no falloff in it to remove -- a
928 * lens darkens light, not coverage -- so scaling it corrupts whatever the caller
929 * keeps there: in a raw pipeline that is routinely a mask, and for premultiplied
930 * alpha it would break the invariant outright.
931 *
932 * Written as three multiplies and a skip, the store is a 3-of-4 masked write and
933 * the compiler falls back to scalar. Multiplying the whole pixel keeps it a single
934 * aligned 4-wide operation, and restoring one lane afterwards is one scalar store.
935 *
936 * Upstream lensfun scales all four, under LF_CR_4(RED, GREEN, BLUE, UNKNOWN) -- but
937 * only in its SSE2 path; its own scalar DeVignetting leaves the fourth alone. The
938 * two disagree, so there is no upstream behaviour here to be faithful to, and
939 * tests/parity_lensfun.c has always excluded alpha from the comparison for that
940 * reason. It asserts this instead. */
941 const float a = px[3];
942 px[0] *= m;
943 px[1] *= m;
944 px[2] *= m;
945 px[3] *= m;
946 px[3] = a;
947 }
948 }
949 }
950
951 return 1;
952}
static int _interp_real_focal(const ls_lens_t *lens, float focal, float *res)
Definition lensserious.c:82
int ls_eval_from_modifier(const ls_modifier_t *mod, ls_eval_t *out)
Flatten a resolved modifier into the scalar block a kernel can take by value.
float ls_modifier_autoscale(const ls_modifier_t *mod)
The scale that just removes the black borders a correction leaves behind.
static float _hugin_focal_correction(const ls_calib_dist_t *dist, int have_dist)
#define LS_RESTRICT
Definition lensserious.c:22
static void _spline_init(spline_t *s)
Definition lensserious.c:51
static int _interp_tca(const ls_lens_t *lens, float focal, ls_calib_tca_t *res)
#define LS_GEOM_SENTINEL
int ls_modifier_init_knots(ls_modifier_t *mod, const ls_knots_t *knots, int width, int height, float scale, int flags, int reverse)
Resolve a maker's embedded profile, in place of a database lens.
int ls_modifier_apply_vignetting(const ls_modifier_t *mod, float xu, float yu, int width, int height, float *rgba, int row_stride_bytes)
pa vignetting, multiplied in place over RGBA float rows. Contract of lfModifier::ApplyColorModificati...
static int _knot_axis_ascends(const float *xs, const int n)
static float _autoscale_residual(const ls_eval_t *p, const float max_x, const float max_y, const float x, const float y)
int ls_modifier_init(ls_modifier_t *mod, const ls_lens_t *lens, float crop, int width, int height, float focal, float aperture, float distance, float scale, int target_type, int flags, int reverse)
Resolve a lens at one (crop, geometry, focal, aperture, distance, scale).
static void _spline_insert(spline_t *s, float dist, const void *val)
Definition lensserious.c:58
static int _interp_dist(const ls_lens_t *lens, float focal, ls_calib_dist_t *res)
static float _interpolate(float y1, float y2, float y3, float y4, float t)
Definition lensserious.c:36
int ls_modifier_apply_subpixel_geometry(const ls_modifier_t *mod, float xu, float yu, int width, int height, float *res)
The geometry map: for count output pixels starting at (xu, yu), write 6 floats per pixel — source coo...
static float _vig_dist(const ls_lens_t *lens, const ls_calib_vig_t *c, float focal, float aperture, float distance)
static int _interp_vig(const ls_lens_t *lens, float focal, float aperture, float distance, ls_calib_vig_t *res)
static float _autoscale_distance(const ls_eval_t *p, const float ca, const float sa, const float dist, const float max_x, const float max_y)
struct spline_t spline_t
int ls_modifier_set_projection(ls_modifier_t *mod, const int from_type, const int to_type, const float focal_mm, const float crop_factor)
Add a projection change to an already-resolved modifier.
int ls_eval_adopt_vignetting(ls_eval_t *dst, const ls_eval_t *src)
Move src's vignetting into dst, leaving dst's geometry untouched.
What this is, and what it deliberately is not.
#define LS_ENABLE_SCALE
ls_tca_model_t
Definition lensserious.h:71
@ LS_TCA_LINEAR
Definition lensserious.h:73
@ LS_TCA_POLY3
Definition lensserious.h:74
@ LS_TCA_NONE
Definition lensserious.h:72
#define LS_ENABLE_VIGNETTING
#define LS_ENABLE_DISTORTION
#define LS_ENABLE_TCA
ls_vig_model_t
Definition lensserious.h:78
@ LS_VIG_NONE
Definition lensserious.h:79
@ LS_VIG_PA
Definition lensserious.h:80
@ LS_VIG_KNOTS
Definition lensserious.h:81
@ LS_LENS_RECTILINEAR
Definition lensserious.h:97
@ LS_LENS_EQUIRECTANGULAR
@ LS_LENS_UNKNOWN
Definition lensserious.h:96
@ LS_LENS_FISHEYE_THOBY
@ LS_LENS_PANORAMIC
Definition lensserious.h:99
ls_dist_model_t
Definition lensserious.h:62
@ LS_DIST_NONE
Definition lensserious.h:63
@ LS_DIST_PTLENS
Definition lensserious.h:66
@ LS_DIST_POLY5
Definition lensserious.h:65
@ LS_DIST_KNOTS
Definition lensserious.h:67
@ LS_DIST_POLY3
Definition lensserious.h:64
#define LS_ENABLE_GEOMETRY
The closed forms, written once, compiled as C99 and as OpenCL C.
#define LS_EVAL_VIG_NONE
#define LS_EVAL_DIST_POLY3
#define LS_EVAL_TCA_POLY3
static int ls_eval_coord_chain(const ls_eval_t *p, const int c, float *x, float *y)
The coordinate chain: scale, projection and distortion, in direction order.
#define LS_EVAL_ENABLE_DISTORTION
#define LS_EVAL_DIST_NONE
#define LS_EVAL_DIST_PTLENS
#define LS_EVAL_VIG_PA
#define LS_EVAL_TCA_NONE
static float ls_eval_vignette_from_r2(const ls_eval_t *p, const float r2)
The vignetting multiplier for ONE output pixel. Multiply the pixel by it.
#define LS_EVAL_ENABLE_VIGNETTING
#define LS_EVAL_LENS_FISHEYE_THOBY
#define LS_EVAL_LENS_RECTILINEAR
#define LS_EVAL_ENABLE_SCALE
#define LS_FABS(x)
#define LS_EVAL_DIST_KNOTS
#define LS_EVAL_VIG_KNOTS
#define LS_EVAL_FULL_FRAME_HALF_DIAG_MM
#define LS_EVAL_DIST_POLY5
#define LS_EVAL_ENABLE_GEOMETRY
static void ls_eval_map(const ls_eval_t *p, float xu, float yu, float *out)
The map for ONE output pixel: six floats, source coordinates for R, G, B.
struct ls_eval_t ls_eval_t
One lens resolved at one shooting configuration, as a flat block of scalars.
#define LS_EVAL_ENABLE_TCA
#define LS_EVAL_TCA_LINEAR
#define LS_MAX_KNOTS
ls_dist_model_t model
Definition lensserious.h:85
ls_tca_model_t model
Definition lensserious.h:86
float terms[6]
Definition lensserious.h:86
ls_vig_model_t model
Definition lensserious.h:87
float terms[3]
Definition lensserious.h:87
One lens resolved at one shooting configuration, as a flat block of scalars.
float knot_v[16]
float vig_terms[3]
float knot_c[3][16]
float knot_r[3][16]
float dist_terms[3]
float tca_terms[6]
float knot_vr[16]
A lens correction the camera maker measured and wrote into the file, as knots.
float cor_rgb[3][16]
float vig_radius[16]
float vig[16]
float radius[16]
float crop_factor
ls_lens_type_t type
ls_calib_vig_t vig[LS_MAX_CALIB]
float center_x
ls_calib_real_focal_t real_focal[LS_MAX_CALIB]
ls_calib_tca_t tca[LS_MAX_CALIB]
float min_focal
float center_y
ls_calib_dist_t dist[LS_MAX_CALIB]
float aspect_ratio
float max_focal
int n_real_focal
A modifier: the lens resolved at one shooting configuration.
float knot_c[3][16]
ls_calib_tca_t tca
float knot_vr[16]
ls_calib_dist_t dist
float knot_v[16]
ls_calib_vig_t vig
float knot_r[3][16]
int geometry_unsupported
float aspect_ratio_correction
float d[4]
Definition lensserious.c:49
const void * v[4]
Definition lensserious.c:49