Calibration Library 1.0.0
A C++ library for camera calibration and vision-related geometric transformations
Loading...
Searching...
No Matches
distortion.h
Go to the documentation of this file.
1
13#pragma once
14
15// std
16#include <algorithm>
17#include <concepts>
18#include <optional>
19#include <span>
20#include <stdexcept>
21#include <vector>
22
23// eigen
24#include <Eigen/Core>
25#include <Eigen/Dense>
26
29
30namespace calib {
31
47template <typename D>
49 requires(const D& distortion, const Eigen::Matrix<typename D::Scalar, 2, 1>& point2d) {
50 {
51 distortion.template distort<typename D::Scalar>(point2d)
52 } -> std::same_as<Eigen::Matrix<typename D::Scalar, 2, 1>>;
53 {
54 distortion.template undistort<typename D::Scalar>(point2d)
55 } -> std::same_as<Eigen::Matrix<typename D::Scalar, 2, 1>>;
56 };
57
68template <typename T>
69struct Observation final {
70 T x, y;
71 T u, v;
72};
73
91template <typename T>
92[[nodiscard]]
93auto apply_distortion(const Eigen::Matrix<T, 2, 1>& norm_xy,
94 const Eigen::Matrix<T, Eigen::Dynamic, 1>& coeffs) -> Eigen::Matrix<T, 2, 1> {
95 if (coeffs.size() < 2) {
96 throw std::runtime_error("Insufficient distortion coefficients");
97 }
98
99 const int num_radial_coeffs = static_cast<int>(coeffs.size()) - 2;
100 const T& x_coord = norm_xy.x();
101 const T& y_coord = norm_xy.y();
102 T r2_val = x_coord * x_coord + y_coord * y_coord;
103 T radial = T(1);
104 T rpow = r2_val;
105 for (int i = 0; i < num_radial_coeffs; ++i) {
106 radial += T(coeffs[i]) * rpow;
107 rpow *= r2_val;
108 }
109 T tangential1 = T(coeffs[num_radial_coeffs]);
110 T tangential2 = T(coeffs[num_radial_coeffs + 1]);
111 T x_distorted = x_coord * radial + T(2) * tangential1 * x_coord * y_coord +
112 tangential2 * (r2_val + T(2) * x_coord * x_coord);
113 T y_distorted = y_coord * radial + tangential1 * (r2_val + T(2) * y_coord * y_coord) +
114 T(2) * tangential2 * x_coord * y_coord;
115 return {x_distorted, y_distorted};
116}
117
118// Normalize and undistort pixel coordinates
119template <typename T>
120[[nodiscard]]
121auto undistort(Eigen::Matrix<T, 2, 1> norm_xy, const Eigen::Matrix<T, Eigen::Dynamic, 1>& coeffs)
122 -> Eigen::Matrix<T, 2, 1> {
123 if (coeffs.size() < 2) {
124 throw std::runtime_error("Insufficient distortion coefficients");
125 }
126
127 constexpr int k_num_undistort_iters = 5;
128 Eigen::Matrix<T, 2, 1> undistorted_xy = norm_xy;
129 for (int iter = 0; iter < k_num_undistort_iters; ++iter) {
130 Eigen::Matrix<T, 2, 1> distorted_xy = apply_distortion(undistorted_xy, coeffs);
131 undistorted_xy += norm_xy - distorted_xy;
132 }
133 return undistorted_xy;
134}
135
136template <typename T>
138 Eigen::Matrix<T, Eigen::Dynamic, 1> distortion;
139 Eigen::Matrix<T, Eigen::Dynamic, 1> residuals;
140};
141
142template <typename Scalar_>
143struct BrownConrady final {
144 using Scalar = Scalar_;
145 Eigen::Matrix<Scalar, Eigen::Dynamic, 1> coeffs;
146
147 BrownConrady() = default;
148 template <typename Derived>
149 explicit BrownConrady(const Eigen::MatrixBase<Derived>& coeffs_in) : coeffs(coeffs_in) {}
150
151 template <typename T>
152 [[nodiscard]]
153 auto distort(const Eigen::Matrix<T, 2, 1>& norm_xy) const -> Eigen::Matrix<T, 2, 1> {
154 return apply_distortion(norm_xy, coeffs.template cast<T>().eval());
155 }
156
157 template <typename T>
158 [[nodiscard]]
159 auto undistort(const Eigen::Matrix<T, 2, 1>& distorted_xy) const -> Eigen::Matrix<T, 2, 1> {
160 return calib::undistort(distorted_xy, coeffs.template cast<T>().eval());
161 }
162};
164
165template <typename Scalar>
166inline auto invert_brown_conrady(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& forward)
167 -> Eigen::Matrix<Scalar, Eigen::Dynamic, 1> {
168 if (forward.size() < 2) {
169 throw std::runtime_error("Insufficient distortion coefficients");
170 }
171 int num_radial = static_cast<int>(forward.size()) - 2;
172
173 constexpr int grid = 21;
174 constexpr double lim = 1.0;
175 std::vector<Observation<Scalar>> obs;
176 obs.reserve(grid * grid);
177 constexpr Scalar k_two = Scalar(2.0);
178 for (int i = 0; i < grid; ++i) {
179 Scalar x_coord =
180 -lim + k_two * lim * static_cast<Scalar>(i) / static_cast<Scalar>(grid - 1);
181 for (int j = 0; j < grid; ++j) {
182 Scalar y_coord =
183 -lim + k_two * lim * static_cast<Scalar>(j) / static_cast<Scalar>(grid - 1);
184 Eigen::Matrix<Scalar, 2, 1> und(x_coord, y_coord);
185 Eigen::Matrix<Scalar, 2, 1> dst = apply_distortion(und, forward);
186 obs.push_back({dst.x(), dst.y(), x_coord, y_coord});
187 }
188 }
189
190 auto inv_opt = fit_distortion_full(obs, {1.0, 1.0, 0.0, 0.0, 0.0}, num_radial);
191 if (inv_opt.has_value()) {
192 return inv_opt->distortion;
193 }
194 return Eigen::Matrix<Scalar, Eigen::Dynamic, 1>::Zero(forward.size());
195}
196
197template <typename Scalar_>
198struct DualBrownConrady final {
199 using Scalar = Scalar_;
200 Eigen::Matrix<Scalar, Eigen::Dynamic, 1> forward;
201 Eigen::Matrix<Scalar, Eigen::Dynamic, 1> inverse;
202
203 DualBrownConrady() = default;
204
205 explicit DualBrownConrady(const Eigen::Matrix<Scalar, Eigen::Dynamic, 1>& coeffs)
206 : forward(coeffs), inverse(invert_brown_conrady(coeffs)) {}
207
208 template <typename T>
209 [[nodiscard]]
210 auto distort(const Eigen::Matrix<T, 2, 1>& norm_xy) const -> Eigen::Matrix<T, 2, 1> {
211 return apply_distortion(norm_xy, forward.template cast<T>().eval());
212 }
213
214 template <typename T>
215 [[nodiscard]]
216 auto undistort(const Eigen::Matrix<T, 2, 1>& distorted_xy) const -> Eigen::Matrix<T, 2, 1> {
217 return apply_distortion(distorted_xy, inverse.template cast<T>().eval());
218 }
219};
220
222
227
228// TODO: refactor for camera_model as a template parameter
229template <typename T>
230[[nodiscard]]
231auto fit_distortion_full(const std::vector<Observation<T>>& observations,
232 const CameraMatrixT<T>& intrinsics, int num_radial = 2,
233 std::span<const int> fixed_indices = {},
234 std::span<const T> fixed_values = {})
235 -> std::optional<DistortionWithResiduals<T>> {
236 constexpr int k_min_observations = 8;
237 if (observations.size() < k_min_observations) {
238 return std::nullopt;
239 }
240
241 const int num_coeffs = num_radial + 2; // radial + tangential coeffs
242 const int num_obs = static_cast<int>(observations.size());
243 const int num_rows = num_obs * 2;
244
245 Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic> design_matrix(num_rows, num_coeffs);
246 Eigen::Matrix<T, Eigen::Dynamic, 1> rhs(num_rows);
247
248 design_matrix.setZero();
249 rhs.setZero();
250
251 const int idx_p1 = num_radial;
252 const int idx_p2 = num_radial + 1;
253
254 for (int obs_idx = 0; obs_idx < num_obs; ++obs_idx) {
255 const T x_coord = T(observations[obs_idx].x);
256 const T y_coord = T(observations[obs_idx].y);
257 const T r2_val = x_coord * x_coord + y_coord * y_coord;
258
259 const T undistorted_u = intrinsics.fx * x_coord + intrinsics.skew * y_coord + intrinsics.cx;
260 const T undistorted_v = intrinsics.fy * y_coord + intrinsics.cy;
261
262 const T residual_u = T(observations[obs_idx].u) - undistorted_u;
263 const T residual_v = T(observations[obs_idx].v) - undistorted_v;
264
265 const int row_u = 2 * obs_idx;
266 const int row_v = row_u + 1;
267
268 // Radial terms
269 T rpow = r2_val;
270 for (int j = 0; j < num_radial; ++j) {
271 design_matrix(row_u, j) =
272 intrinsics.fx * x_coord * rpow + intrinsics.skew * y_coord * rpow;
273 design_matrix(row_v, j) = intrinsics.fy * y_coord * rpow;
274 rpow *= r2_val;
275 }
276
277 // Tangential terms
278 design_matrix(row_u, idx_p1) = intrinsics.fx * (T(2.0) * x_coord * y_coord) +
279 intrinsics.skew * (r2_val + T(2.0) * y_coord * y_coord);
280 design_matrix(row_u, idx_p2) = intrinsics.fx * (r2_val + T(2.0) * x_coord * x_coord) +
281 intrinsics.skew * (T(2.0) * x_coord * y_coord);
282 design_matrix(row_v, idx_p1) = intrinsics.fy * (r2_val + T(2.0) * y_coord * y_coord);
283 design_matrix(row_v, idx_p2) = intrinsics.fy * (T(2.0) * x_coord * y_coord);
284
285 rhs(row_u) = residual_u;
286 rhs(row_v) = residual_v;
287 }
288
289 if (fixed_indices.empty()) {
290 Eigen::JacobiSVD<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic>> svd(
291 design_matrix, Eigen::ComputeThinU | Eigen::ComputeThinV);
292 auto alpha = svd.solve(rhs);
293 Eigen::Matrix<T, Eigen::Dynamic, 1> residuals = design_matrix * alpha - rhs;
294 return DistortionWithResiduals<T>{alpha, residuals};
295 }
296
297 std::vector<std::pair<int, T>> fixed_pairs;
298 fixed_pairs.reserve(fixed_indices.size());
299 for (size_t i = 0; i < fixed_indices.size(); ++i) {
300 int idx = fixed_indices[i];
301 T value = T(0);
302 if (i < fixed_values.size()) {
303 value = fixed_values[i];
304 }
305 fixed_pairs.emplace_back(idx, value);
306 }
307 std::sort(fixed_pairs.begin(), fixed_pairs.end(),
308 [](const auto& a, const auto& b) { return a.first < b.first; });
309 fixed_pairs.erase(std::unique(fixed_pairs.begin(), fixed_pairs.end(),
310 [](const auto& a, const auto& b) { return a.first == b.first; }),
311 fixed_pairs.end());
312
313 std::vector<int> fixed_only;
314 fixed_only.reserve(fixed_pairs.size());
315 for (const auto& [idx, value] : fixed_pairs) {
316 if (idx < 0 || idx >= num_coeffs) {
317 throw std::invalid_argument("Fixed distortion index out of range");
318 }
319 fixed_only.push_back(idx);
320 }
321
322 Eigen::Matrix<T, Eigen::Dynamic, 1> alpha =
323 Eigen::Matrix<T, Eigen::Dynamic, 1>::Zero(num_coeffs);
324 for (const auto& [idx, value] : fixed_pairs) {
325 alpha(idx) = value;
326 }
327
328 Eigen::Matrix<T, Eigen::Dynamic, 1> rhs_adjusted = rhs;
329 for (const auto& [idx, value] : fixed_pairs) {
330 rhs_adjusted -= design_matrix.col(idx) * value;
331 }
332
333 std::vector<int> free_indices;
334 free_indices.reserve(num_coeffs - static_cast<int>(fixed_pairs.size()));
335 for (int idx = 0; idx < num_coeffs; ++idx) {
336 if (!std::binary_search(fixed_only.begin(), fixed_only.end(), idx)) {
337 free_indices.push_back(idx);
338 }
339 }
340
341 Eigen::Matrix<T, Eigen::Dynamic, 1> residuals;
342 if (free_indices.empty()) {
343 residuals = design_matrix * alpha - rhs;
344 return DistortionWithResiduals<T>{alpha, residuals};
345 }
346
347 Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic> free_design(
348 design_matrix.rows(), static_cast<int>(free_indices.size()));
349 for (int col = 0; col < static_cast<int>(free_indices.size()); ++col) {
350 free_design.col(col) = design_matrix.col(free_indices[col]);
351 }
352
353 Eigen::JacobiSVD<Eigen::Matrix<T, Eigen::Dynamic, Eigen::Dynamic>> svd(
354 free_design, Eigen::ComputeThinU | Eigen::ComputeThinV);
355 Eigen::Matrix<T, Eigen::Dynamic, 1> free_alpha = svd.solve(rhs_adjusted);
356
357 for (int col = 0; col < static_cast<int>(free_indices.size()); ++col) {
358 alpha(free_indices[col]) = free_alpha(col);
359 }
360
361 residuals = design_matrix * alpha - rhs;
362 return DistortionWithResiduals<T>{std::move(alpha), std::move(residuals)};
363}
364
365template <typename T>
366auto fit_distortion(const std::vector<Observation<T>>& observations,
367 const CameraMatrixT<T>& intrinsics, int num_radial = 2,
368 std::span<const int> fixed_indices = {}, std::span<const T> fixed_values = {})
369 -> std::optional<DistortionWithResiduals<T>> {
370 return fit_distortion_full(observations, intrinsics, num_radial, fixed_indices, fixed_values);
371}
372
373inline auto fit_distortion_dual(const std::vector<Observation<double>>& observations,
374 const CameraMatrix& intrinsics, int num_radial = 2,
375 std::span<const int> fixed_indices = {},
376 std::span<const double> fixed_values = {})
377 -> std::optional<DualDistortionWithResiduals> {
378 auto forward =
379 fit_distortion_full(observations, intrinsics, num_radial, fixed_indices, fixed_values);
380 if (!forward) {
381 return std::nullopt;
382 }
383
384 std::vector<Observation<double>> inv_observations;
385 inv_observations.reserve(observations.size());
386 for (const auto& obs : observations) {
387 double y_dist = (obs.v - intrinsics.cy) / intrinsics.fy;
388 double x_dist = (obs.u - intrinsics.cx - intrinsics.skew * y_dist) / intrinsics.fx;
389 double u_undist = intrinsics.fx * obs.x + intrinsics.skew * obs.y + intrinsics.cx;
390 double v_undist = intrinsics.fy * obs.y + intrinsics.cy;
391 inv_observations.push_back({x_dist, y_dist, u_undist, v_undist});
392 }
393
394 auto inverse =
395 fit_distortion_full(inv_observations, intrinsics, num_radial, fixed_indices, fixed_values);
396 if (!inverse) {
397 return std::nullopt;
398 }
399
400 DualDistortionWithResiduals out;
401 out.distortion.forward = forward->distortion;
402 out.distortion.inverse = inverse->distortion;
403 out.residuals = forward->residuals;
404 return out;
405}
406
407inline void to_json(nlohmann::json& j, const DualDistortion& d) {
408 j = {{"forward", d.forward}, {"inverse", d.inverse}};
409}
410
411inline void from_json(const nlohmann::json& j, DualDistortion& d) {
412 if (j.contains("forward")) d.forward = j.at("forward").get<Eigen::VectorXd>();
413 if (j.contains("inverse")) d.inverse = j.at("inverse").get<Eigen::VectorXd>();
414}
415
416inline void to_json(nlohmann::json& j, const BrownConradyd& d) { j = {{"coeffs", d.coeffs}}; }
417
418inline void from_json(const nlohmann::json& j, BrownConradyd& d) {
419 if (j.contains("coeffs")) d.coeffs = j.at("coeffs").get<Eigen::VectorXd>();
420}
421
422} // namespace calib
Concept defining the interface for lens distortion models.
Definition distortion.h:48
Linear multi-camera extrinsics initialisation (DLT)
auto fit_distortion_dual(const std::vector< Observation< double > > &observations, const CameraMatrix &intrinsics, int num_radial=2, std::span< const int > fixed_indices={}, std::span< const double > fixed_values={}) -> std::optional< DualDistortionWithResiduals >
Definition distortion.h:373
void from_json(const nlohmann::json &j, T &value)
Definition json.h:89
auto fit_distortion(const std::vector< Observation< T > > &observations, const CameraMatrixT< T > &intrinsics, int num_radial=2, std::span< const int > fixed_indices={}, std::span< const T > fixed_values={}) -> std::optional< DistortionWithResiduals< T > >
Definition distortion.h:366
auto apply_distortion(const Eigen::Matrix< T, 2, 1 > &norm_xy, const Eigen::Matrix< T, Eigen::Dynamic, 1 > &coeffs) -> Eigen::Matrix< T, 2, 1 >
Apply lens distortion to normalized coordinates.
Definition distortion.h:93
auto undistort(Eigen::Matrix< T, 2, 1 > norm_xy, const Eigen::Matrix< T, Eigen::Dynamic, 1 > &coeffs) -> Eigen::Matrix< T, 2, 1 >
Definition distortion.h:121
void to_json(nlohmann::json &j, const T &value)
Definition json.h:49
auto fit_distortion_full(const std::vector< Observation< T > > &observations, const CameraMatrixT< T > &intrinsics, int num_radial=2, std::span< const int > fixed_indices={}, std::span< const T > fixed_values={}) -> std::optional< DistortionWithResiduals< T > >
Definition distortion.h:231
auto invert_brown_conrady(const Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > &forward) -> Eigen::Matrix< Scalar, Eigen::Dynamic, 1 >
Definition distortion.h:166
auto distort(const Eigen::Matrix< T, 2, 1 > &norm_xy) const -> Eigen::Matrix< T, 2, 1 >
Definition distortion.h:153
BrownConrady(const Eigen::MatrixBase< Derived > &coeffs_in)
Definition distortion.h:149
BrownConrady()=default
auto undistort(const Eigen::Matrix< T, 2, 1 > &distorted_xy) const -> Eigen::Matrix< T, 2, 1 >
Definition distortion.h:159
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > coeffs
Definition distortion.h:145
Eigen::Matrix< T, Eigen::Dynamic, 1 > distortion
Definition distortion.h:138
Eigen::Matrix< T, Eigen::Dynamic, 1 > residuals
Definition distortion.h:139
DualBrownConrady(const Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > &coeffs)
Definition distortion.h:205
auto undistort(const Eigen::Matrix< T, 2, 1 > &distorted_xy) const -> Eigen::Matrix< T, 2, 1 >
Definition distortion.h:216
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > inverse
Coefficients for undistortion.
Definition distortion.h:201
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > forward
Coefficients for distortion.
Definition distortion.h:200
auto distort(const Eigen::Matrix< T, 2, 1 > &norm_xy) const -> Eigen::Matrix< T, 2, 1 >
Definition distortion.h:210
Observation structure for distortion parameter estimation.
Definition distortion.h:69
T y
Normalized undistorted coordinates.
Definition distortion.h:70
T v
Observed distorted pixel coordinates.
Definition distortion.h:71