Calibration Library 1.0.0
A C++ library for camera calibration and vision-related geometric transformations
Loading...
Searching...
No Matches
zhang.cpp
Go to the documentation of this file.
2
3// std
4#include <iostream>
5
6namespace calib {
7
9static auto zhang_bmtx(const Eigen::VectorXd& b) -> Eigen::Matrix3d {
10 Eigen::Matrix3d bmtx;
11 bmtx << b(0), b(1), b(3), b(1), b(2), b(4), b(3), b(4), b(5);
12 return 0.5 * (bmtx + bmtx.transpose()); // symmetrize
13}
14
15static void check_conic_decomposition(const Eigen::Matrix3d& bmtx, const Eigen::Matrix3d& kmtx) {
16 // Optional self-check: B ≈ K^{-T} K^{-1} up to scale (should be exact after our normalization)
17 const Eigen::Matrix3d bhat = kmtx.inverse().transpose() * kmtx.inverse();
18 if (bhat.allFinite()) {
19 // Compare relative Frobenius error after aligning scale by a single element
20 double s = 1.0;
21 if (std::abs(bhat(2, 2)) > 0) {
22 s = bmtx(2, 2) / bhat(2, 2);
23 }
24 const double rel_err = (bmtx - s * bhat).norm() / std::max(1e-12, bmtx.norm());
25 if (!std::isfinite(rel_err) || rel_err > 1e-6) {
26 std::cerr << "Zhang: B consistency warning, rel_err=" << rel_err << "\n";
27 }
28 }
29}
30
31// b is homogeneous: recover K from B = K^{-T} K^{-1} via Cholesky of B
32static auto kmtx_from_dual_conic(const Eigen::VectorXd& bv) -> std::optional<Eigen::Matrix3d> {
33 // Expect a 6-vector: [b11, b12, b22, b13, b23, b33]^T
34 if (bv.size() != 6) {
35 std::cerr << "Zhang: dual-conic vector must have size 6, got " << bv.size() << "\n";
36 return std::nullopt;
37 }
38
39 const auto try_factor = [](const Eigen::Matrix3d& bmtx) -> std::optional<Eigen::Matrix3d> {
40 // B should be symmetric positive-definite (omega = K^{-T} K^{-1})
41 if (!bmtx.allFinite()) {
42 return std::nullopt;
43 }
44
45 // Fast SPD check via LLT; also gives us the factor in one shot
46 Eigen::LLT<Eigen::Matrix3d> llt(bmtx);
47 if (llt.info() != Eigen::Success) {
48 return std::nullopt;
49 }
50
51 // Cholesky: B = U^T * U, with U upper-triangular, diag > 0
52 const Eigen::Matrix3d umtx = llt.matrixU();
53
54 // Since B = (K^{-1})^T (K^{-1}), we have U = K^{-1} (same triangular form)
55 Eigen::Matrix3d kmtx = umtx.inverse();
56 if (!kmtx.allFinite()) {
57 return std::nullopt;
58 }
59
60 // Normalize so K(2,2) = 1
61 const double k22 = kmtx(2, 2);
62 if (std::abs(k22) < 1e-15) {
63 return std::nullopt;
64 }
65 kmtx /= k22;
66
67 // Ensure a conventional calibration matrix: positive focal lengths
68 if (kmtx(0, 0) <= 0.0 || kmtx(1, 1) <= 0.0) {
69 // Flip sign uniformly if needed (numerical edge cases)
70 kmtx = -kmtx;
71 }
72
73 check_conic_decomposition(bmtx, kmtx);
74 return kmtx;
75 };
76
77 // Rebuild symmetric B from the 6-vector
78 const Eigen::Matrix3d bmtx = zhang_bmtx(bv);
79
80 // Try as-is, then the opposite sign (b is homogeneous)
81 if (auto kmtx = try_factor(bmtx)) {
82 return kmtx;
83 }
84 if (auto kmtx = try_factor(-bmtx)) {
85 return kmtx;
86 }
87
88 std::cerr << "Zhang: failed to recover K from dual conic (both signs).\n";
89 return std::nullopt;
90}
91
92// ---------- Zhang: recover K from homographies ----------
93static auto v_ij(const Eigen::Matrix3d& hmtx, int i, int j) -> Eigen::Matrix<double, 1, 6> {
94 assert(0 <= i && i < 3 && 0 <= j && j < 3);
95 const double h0i = hmtx(0, i);
96 const double h1i = hmtx(1, i);
97 const double h2i = hmtx(2, i);
98 const double h0j = hmtx(0, j);
99 const double h1j = hmtx(1, j);
100 const double h2j = hmtx(2, j);
101
102 Eigen::Matrix<double, 1, 6> v;
103 v << h0i * h0j, h0i * h1j + h1i * h0j, h1i * h1j, h0i * h2j + h2i * h0j, h1i * h2j + h2i * h1j,
104 h2i * h2j;
105 return v;
106}
107
108// Safe per-homography normalization for Zhang
109// - enforce a consistent sign (h33 >= 0) and try to set h33 = 1 when possible
110// - otherwise fall back to a Frobenius scaling to bring entries to O(1)
111// This uses a SINGLE scalar per H (critical for Zhang).
112static auto normalize_hmtx(const Eigen::Matrix3d& hmtx) -> Eigen::Matrix3d {
113 Eigen::Matrix3d hmtx_norm = hmtx;
114 if (!hmtx_norm.allFinite()) {
115 return hmtx_norm;
116 }
117
118 // Make sign consistent across views: prefer h33 >= 0
119 if (hmtx_norm(2, 2) < 0.0) {
120 hmtx_norm = -hmtx_norm;
121 }
122
123 // Primary: set h33 = 1 if well-conditioned
124 const double h33 = hmtx_norm(2, 2);
125 if (std::abs(h33) > 1e-12) {
126 hmtx_norm /= h33; // h33 becomes +1
127 return hmtx_norm;
128 }
129
130 // Fallback: scale by Frobenius norm to keep numbers ~ O(1)
131 const double nf = hmtx_norm.norm();
132 if (nf > 1e-12) {
133 hmtx_norm /= nf;
134 }
135
136 return hmtx_norm;
137}
138
139static auto make_zhang_design_matrix(const std::vector<HomographyResult>& hs)
140 -> std::optional<Eigen::MatrixXd> {
141 const int m = static_cast<int>(hs.size());
142 if (m < 4) {
143 std::cerr << "Zhang method requires at least 4 views\n";
144 return std::nullopt;
145 }
146
147 Eigen::MatrixXd vmtx(2 * m, 6);
148 for (Eigen::Index k = 0; k < m; ++k) {
149 Eigen::Matrix3d hmtx = normalize_hmtx(hs[static_cast<size_t>(k)].hmtx); // <-- important
150 Eigen::Matrix<double, 1, 6> v12 = v_ij(hmtx, 0, 1);
151 Eigen::Matrix<double, 1, 6> v11 = v_ij(hmtx, 0, 0);
152 Eigen::Matrix<double, 1, 6> v22 = v_ij(hmtx, 1, 1);
153
154 auto normalize_row = [](Eigen::Matrix<double, 1, 6>& r) {
155 double s = r.norm();
156 if (s > 0) {
157 r /= s;
158 }
159 };
160
161 normalize_row(v12);
162 Eigen::Matrix<double, 1, 6> vr = v11 - v22;
163 normalize_row(vr);
164
165 vmtx.row(2 * k) = v12;
166 vmtx.row(2 * k + 1) = vr;
167 }
168 return vmtx;
169}
170
171auto zhang_intrinsics_from_hs(const std::vector<HomographyResult>& hs)
172 -> std::optional<CameraMatrix> {
173 const auto vmtx_opt = make_zhang_design_matrix(hs);
174 if (!vmtx_opt.has_value()) {
175 std::cout << "Zhang design matrix creation failed.\n";
176 return std::nullopt;
177 }
178
179 Eigen::JacobiSVD<Eigen::MatrixXd> svd(vmtx_opt.value(), Eigen::ComputeFullV);
180
181 // The null space should correspond to the smallest singular value
182 Eigen::VectorXd bvec = svd.matrixV().col(5); // smallest singular value
183 Eigen::VectorXd resid = vmtx_opt.value() * bvec;
184 double rms = std::sqrt(resid.squaredNorm() / static_cast<double>(resid.size()));
185 if (rms > 1e-3) {
186 std::cout << "Zhang warning: large residual in solving for b: " << rms << '\n';
187 } else {
188 std::cout << "Zhang: solved for b with residual " << rms << '\n';
189 }
190
191 // Try both signs of the b vector
192 std::optional<Eigen::Matrix3d> kmtx_opt = kmtx_from_dual_conic(bvec);
193 if (!kmtx_opt.has_value()) {
194 std::cout << "Zhang kmtx_from_dual_conic failed for one sign, trying the other.\n";
195 bvec *= -1;
196 kmtx_opt = kmtx_from_dual_conic(bvec);
197 if (!kmtx_opt.has_value()) {
198 std::cout << "Zhang kmtx_from_dual_conic failed for both signs.\n";
199 return std::nullopt;
200 }
201 }
202
203 return CameraMatrix{.fx = kmtx_opt.value()(0, 0),
204 .fy = kmtx_opt.value()(1, 1),
205 .cx = kmtx_opt.value()(0, 2),
206 .cy = kmtx_opt.value()(1, 2),
207 .skew = kmtx_opt.value()(0, 1)};
208}
209
210} // namespace calib
Linear multi-camera extrinsics initialisation (DLT)
Eigen::Matrix3d skew(const Eigen::Vector3d &vec)
Definition se3_utils.h:21
static auto make_zhang_design_matrix(const std::vector< HomographyResult > &hs) -> std::optional< Eigen::MatrixXd >
Definition zhang.cpp:139
static auto v_ij(const Eigen::Matrix3d &hmtx, int i, int j) -> Eigen::Matrix< double, 1, 6 >
Definition zhang.cpp:93
static auto zhang_bmtx(const Eigen::VectorXd &b) -> Eigen::Matrix3d
Constructs B = K^{-T} K^{-1} from the 6-vector b.
Definition zhang.cpp:9
static auto normalize_hmtx(const Eigen::Matrix3d &hmtx) -> Eigen::Matrix3d
Definition zhang.cpp:112
auto zhang_intrinsics_from_hs(const std::vector< HomographyResult > &hs) -> std::optional< CameraMatrix >
Definition zhang.cpp:171
static auto kmtx_from_dual_conic(const Eigen::VectorXd &bv) -> std::optional< Eigen::Matrix3d >
Definition zhang.cpp:32
static void check_conic_decomposition(const Eigen::Matrix3d &bmtx, const Eigen::Matrix3d &kmtx)
Definition zhang.cpp:15