eos 1.4.0
Loading...
Searching...
No Matches
linear_shape_fitting.hpp
1/*
2 * eos - A 3D Morphable Model fitting library written in modern C++11/14.
3 *
4 * File: include/eos/fitting/linear_shape_fitting.hpp
5 *
6 * Copyright 2014, 2015 Patrik Huber
7 *
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20#pragma once
21
22#ifndef EOS_LINEAR_SHAPE_FITTING_HPP
23#define EOS_LINEAR_SHAPE_FITTING_HPP
24
25#include "eos/morphablemodel/PcaModel.hpp"
26#include "eos/cpp17/optional.hpp"
27
28#include "Eigen/Core"
29#include "Eigen/QR"
30#include "Eigen/Sparse"
31
32#include <vector>
33#include <cstdint>
34#include <cassert>
35
36namespace eos {
37namespace fitting {
38
60inline std::vector<float> fit_shape_to_landmarks_linear(
61 const morphablemodel::PcaModel& shape_model, Eigen::Matrix<float, 3, 4> affine_camera_matrix,
62 const std::vector<Eigen::Vector2f>& landmarks, const std::vector<int>& vertex_ids,
63 Eigen::VectorXf base_face = Eigen::VectorXf(), float lambda = 3.0f,
64 cpp17::optional<int> num_coefficients_to_fit = cpp17::optional<int>(),
65 cpp17::optional<float> detector_standard_deviation = cpp17::optional<float>(),
66 cpp17::optional<float> model_standard_deviation = cpp17::optional<float>())
67{
68 assert(landmarks.size() == vertex_ids.size());
69
70 using Eigen::VectorXf;
71 using Eigen::MatrixXf;
72
73 const int num_coeffs_to_fit = num_coefficients_to_fit.value_or(shape_model.get_num_principal_components());
74 const int num_landmarks = static_cast<int>(landmarks.size());
75
76 if (base_face.size() == 0)
77 {
78 base_face = shape_model.get_mean();
79 }
80
81 // $\hat{V} \in R^{3N\times m-1}$, subselect the rows of the eigenvector matrix $V$ associated with the $N$ feature points
82 // And we insert a row of zeros after every third row, resulting in matrix $\hat{V}_h \in R^{4N\times m-1}$:
83 MatrixXf V_hat_h = MatrixXf::Zero(4 * num_landmarks, num_coeffs_to_fit);
84 int row_index = 0;
85 for (int i = 0; i < num_landmarks; ++i)
86 {
87 // In the paper, I'm not sure whether they use the orthonormal basis. It seems a bit messy/inconsistent even in the paper.
88 // Update PH 26.5.2014: I think the rescaled basis is fine/better!
89 const auto& basis_rows = shape_model.get_rescaled_pca_basis_at_point(vertex_ids[i]);
90 V_hat_h.block(row_index, 0, 3, V_hat_h.cols()) =
91 basis_rows.block(0, 0, basis_rows.rows(), num_coeffs_to_fit);
92 row_index += 4; // replace 3 rows and skip the 4th one, it has all zeros
93 }
94
95 // Form a block diagonal matrix $P \in R^{3N\times 4N}$ in which the camera matrix C (P_Affine, affine_camera_matrix) is placed on the diagonal:
96 Eigen::SparseMatrix<float> P(3 * num_landmarks, 4 * num_landmarks);
97 std::vector<Eigen::Triplet<float>> P_coefficients; // list of non-zeros coefficients
98 for (int i = 0; i < num_landmarks; ++i) { // Note: could make this the inner-most loop.
99 for (int x = 0; x < affine_camera_matrix.rows(); ++x) {
100 for (int y = 0; y < affine_camera_matrix.cols(); ++y) {
101 P_coefficients.push_back(
102 Eigen::Triplet<float>(3 * i + x, 4 * i + y, affine_camera_matrix(x, y)));
103 }
104 }
105 }
106 P.setFromTriplets(P_coefficients.begin(), P_coefficients.end());
107
108 // The variances: Add the 2D and 3D standard deviations.
109 // If the user doesn't provide them, we choose the following:
110 // 2D (detector) standard deviation: In pixel, we follow [1] and choose sqrt(3) as the default value.
111 // 3D (model) variance: 0.0f. It only makes sense to set it to something when we have a different variance
112 // for different vertices.
113 // The 3D variance has to be projected to 2D (for details, see paper [1]) so the units do match up.
114 const float sigma_squared_2D = std::pow(detector_standard_deviation.value_or(std::sqrt(3.0f)), 2) +
115 std::pow(model_standard_deviation.value_or(0.0f), 2);
116 // We use a VectorXf, and later use .asDiagonal():
117 const VectorXf Omega = VectorXf::Constant(3 * num_landmarks, 1.0f / sigma_squared_2D);
118 // Earlier, we set Sigma in a for-loop and then computed Omega, but it was really unnecessary:
119 // Sigma(i, i) = sqrt(sigma_squared_2D), but then Omega is Sigma.t() * Sigma (squares the diagonal) - so
120 // we just assign 1/sigma_squared_2D to Omega here.
121
122 // The landmarks in matrix notation (in homogeneous coordinates), $3N\times 1$
123 VectorXf y = VectorXf::Ones(3 * num_landmarks);
124 for (int i = 0; i < num_landmarks; ++i)
125 {
126 y(3 * i) = landmarks[i][0];
127 y((3 * i) + 1) = landmarks[i][1];
128 // y((3 * i) + 2) = 1; // already 1, stays (homogeneous coordinate)
129 }
130
131 // The mean, with an added homogeneous coordinate (x_1, y_1, z_1, 1, x_2, ...)^t
132 VectorXf v_bar = VectorXf::Ones(4 * num_landmarks);
133 for (int i = 0; i < num_landmarks; ++i)
134 {
135 v_bar(4 * i) = base_face(vertex_ids[i] * 3);
136 v_bar((4 * i) + 1) = base_face(vertex_ids[i] * 3 + 1);
137 v_bar((4 * i) + 2) = base_face(vertex_ids[i] * 3 + 2);
138 // v_bar((4 * i) + 3) = 1; // already 1, stays (homogeneous coordinate)
139 }
140
141 // Bring into standard regularised quadratic form with diagonal distance matrix Omega:
142 const MatrixXf A = P * V_hat_h; // camera matrix times the basis
143 const MatrixXf b = P * v_bar - y; // camera matrix times the mean, minus the landmarks
144 const MatrixXf AtOmegaAReg = A.transpose() * Omega.asDiagonal() * A +
145 lambda * Eigen::MatrixXf::Identity(num_coeffs_to_fit, num_coeffs_to_fit);
146 const MatrixXf rhs = -A.transpose() * Omega.asDiagonal() * b; // It's -A^t*Omega^t*b, but we don't need to
147 // transpose Omega, since it's a diagonal
148 // matrix, and Omega^t = Omega.
149
150 // c_s: The 'x' that we solve for. (The variance-normalised shape parameter vector, $c_s =
151 // [a_1/sigma_{s,1} , ..., a_m-1/sigma_{s,m-1}]^t$.)
152 // We get coefficients ~ N(0, 1), because we're fitting with the rescaled basis. The coefficients are not
153 // multiplied with their eigenvalues.
154 const VectorXf c_s = AtOmegaAReg.colPivHouseholderQr().solve(rhs);
155
156 return std::vector<float>(c_s.data(), c_s.data() + c_s.size());
157};
158
177inline std::vector<float>
179 const std::vector<Eigen::Matrix<float, 3, 4>>& affine_camera_matrices,
180 const std::vector<std::vector<Eigen::Vector2f>>& landmarks,
181 const std::vector<std::vector<int>>& vertex_ids,
182 std::vector<Eigen::VectorXf> base_faces = std::vector<Eigen::VectorXf>(),
183 float lambda = 3.0f,
184 cpp17::optional<int> num_coefficients_to_fit = cpp17::optional<int>(),
185 cpp17::optional<float> detector_standard_deviation = cpp17::optional<float>(),
186 cpp17::optional<float> model_standard_deviation = cpp17::optional<float>())
187{
188 assert(affine_camera_matrices.size() == landmarks.size() &&
189 landmarks.size() == vertex_ids.size()); // same number of instances (i.e. images/frames) for each of them
190 const int num_images = static_cast<int>(affine_camera_matrices.size());
191 for (int j = 0; j < num_images; ++j) {
192 assert(landmarks[j].size() == vertex_ids[j].size());
193 }
194
195 using Eigen::VectorXf;
196 using Eigen::MatrixXf;
197
198 const int num_coeffs_to_fit = num_coefficients_to_fit.value_or(shape_model.get_num_principal_components());
199
200 // the regularisation has to be adjusted when more than one image is given
201 lambda *= num_images;
202
203 std::size_t total_num_landmarks_dimension = 0;
204 for (const auto& l : landmarks) {
205 total_num_landmarks_dimension += l.size();
206 }
207
208 // $\hat{V} \in R^{3N\times m-1}$, subselect the rows of the eigenvector matrix $V$ associated with the $N$ feature points
209 // And we insert a row of zeros after every third row, resulting in matrix $\hat{V}_h \in R^{4N\times m-1}$:
210 MatrixXf V_hat_h = MatrixXf::Zero(4 * total_num_landmarks_dimension, num_coeffs_to_fit);
211 int V_hat_h_row_index = 0;
212 // Form a block diagonal matrix $P \in R^{3N\times 4N}$ in which the camera matrix C (P_Affine, affine_camera_matrix) is placed on the diagonal:
213 Eigen::SparseMatrix<float> P(3 * total_num_landmarks_dimension, 4 * total_num_landmarks_dimension);
214 std::vector<Eigen::Triplet<float>> P_coefficients; // list of non-zeros coefficients
215 int P_index = 0;
216 // The variances: Add the 2D and 3D standard deviations.
217 // If the user doesn't provide them, we choose the following:
218 // 2D (detector) standard deviation: In pixel, we follow [1] and choose sqrt(3) as the default value.
219 // 3D (model) variance: 0.0f. It only makes sense to set it to something when we have a different variance for different vertices.
220 // The 3D variance has to be projected to 2D (for details, see paper [1]) so the units do match up.
221 const float sigma_squared_2D = std::pow(detector_standard_deviation.value_or(std::sqrt(3.0f)), 2) +
222 std::pow(model_standard_deviation.value_or(0.0f), 2);
223 // We use a VectorXf, and later use .asDiagonal():
224 const VectorXf Omega = VectorXf::Constant(3 * total_num_landmarks_dimension, 1.0f / sigma_squared_2D);
225 // The landmarks in matrix notation (in homogeneous coordinates), $3N\times 1$
226 VectorXf y = VectorXf::Ones(3 * total_num_landmarks_dimension);
227 int y_index = 0; // also runs the same as P_index. Should rename to "running_index"?
228 // The mean, with an added homogeneous coordinate (x_1, y_1, z_1, 1, x_2, ...)^t
229 VectorXf v_bar = VectorXf::Ones(4 * total_num_landmarks_dimension);
230 int v_bar_index = 0; // also runs the same as P_index. But be careful, if I change it to be only 1 variable, only increment it once! :-)
231 // Well I think that would make it a bit messy since we need to increment inside the for (landmarks...) loop. Try to refactor some other way.
232
233 for (int k = 0; k < num_images; ++k)
234 {
235 // For each image we have, set up the equations and add it to the matrices:
236 assert(landmarks[k].size() == vertex_ids[k].size()); // has to be valid for each img
237
238 const int num_landmarks = static_cast<int>(landmarks[k].size());
239
240 if (base_faces[k].size() == 0)
241 {
242 base_faces[k] = shape_model.get_mean();
243 }
244
245 // $\hat{V} \in R^{3N\times m-1}$, subselect the rows of the eigenvector matrix $V$ associated with the $N$ feature points
246 // And we insert a row of zeros after every third row, resulting in matrix $\hat{V}_h \in R^{4N\times m-1}$:
247 //Mat V_hat_h = Mat::zeros(4 * num_landmarks, num_coeffs_to_fit, CV_32FC1);
248 for (int i = 0; i < num_landmarks; ++i)
249 {
250 const MatrixXf basis_rows_ = shape_model.get_rescaled_pca_basis_at_point(
251 vertex_ids[k][i]); // In the paper, the orthonormal basis might be used? I'm not sure, check it.
252 // It's even a mess in the paper. PH 26.5.2014: I think the rescaled basis is fine/better.
253 V_hat_h.block(V_hat_h_row_index, 0, 3, V_hat_h.cols()) =
254 basis_rows_.block(0, 0, basis_rows_.rows(), num_coeffs_to_fit);
255 V_hat_h_row_index += 4; // replace 3 rows and skip the 4th one, it has all zeros
256 }
257
258 // Form a block diagonal matrix $P \in R^{3N\times 4N}$ in which the camera matrix C (P_Affine, affine_camera_matrix) is placed on the diagonal:
259 for (int i = 0; i < num_landmarks; ++i) {
260 for (int x = 0; x < affine_camera_matrices[k].rows(); ++x) {
261 for (int y = 0; y < affine_camera_matrices[k].cols(); ++y) {
262 P_coefficients.push_back(Eigen::Triplet<float>(3 * P_index + x, 4 * P_index + y,
263 affine_camera_matrices[k](x, y)));
264 }
265 }
266 ++P_index;
267 }
268 // Fill P with coefficients:
269 P.setFromTriplets(P_coefficients.begin(), P_coefficients.end());
270
271 // The landmarks in matrix notation (in homogeneous coordinates), $3N\times 1$
272 //Mat y = Mat::ones(3 * num_landmarks, 1, CV_32FC1);
273 for (int i = 0; i < num_landmarks; ++i) {
274 y(3 * y_index) = landmarks[k][i][0];
275 y((3 * y_index) + 1) = landmarks[k][i][1];
276 //y((3 * i) + 2) = 1; // already 1, stays (homogeneous coordinate)
277 ++y_index;
278 }
279 // The mean, with an added homogeneous coordinate (x_1, y_1, z_1, 1, x_2, ...)^t
280 //Mat v_bar = Mat::ones(4 * num_landmarks, 1, CV_32FC1);
281 for (int i = 0; i < num_landmarks; ++i) {
282 v_bar(4 * v_bar_index) = base_faces[k](vertex_ids[k][i] * 3);
283 v_bar((4 * v_bar_index) + 1) = base_faces[k](vertex_ids[k][i] * 3 + 1);
284 v_bar((4 * v_bar_index) + 2) = base_faces[k](vertex_ids[k][i] * 3 + 2);
285 //v_bar.at<float>((4 * i) + 3) = 1; // already 1, stays (homogeneous coordinate)
286 ++v_bar_index;
287 }
288 }
289
290 // Bring into standard regularised quadratic form with diagonal distance matrix Omega:
291 const MatrixXf A = P * V_hat_h; // camera matrix times the basis
292 const MatrixXf b = P * v_bar - y; // camera matrix times the mean, minus the landmarks
293 const MatrixXf AtOmegaAReg = A.transpose() * Omega.asDiagonal() * A +
294 lambda * Eigen::MatrixXf::Identity(num_coeffs_to_fit, num_coeffs_to_fit);
295 const MatrixXf rhs = -A.transpose() * Omega.asDiagonal() * b; // It's -A^t*Omega^t*b, but we don't need to transpose Omega, since it's a diagonal matrix, and Omega^t = Omega.
296
297 // c_s: The 'x' that we solve for. (The variance-normalised shape parameter vector, $c_s = [a_1/sigma_{s,1} , ..., a_m-1/sigma_{s,m-1}]^t$.)
298 // We get coefficients ~ N(0, 1), because we're fitting with the rescaled basis. The coefficients are not multiplied with their eigenvalues.
299 const VectorXf c_s = AtOmegaAReg.colPivHouseholderQr().solve(rhs);
300
301 return std::vector<float>(c_s.data(), c_s.data() + c_s.size());
302};
303
304} /* namespace fitting */
305} /* namespace eos */
306
307#endif /* EOS_LINEAR_SHAPE_FITTING_HPP */
This class represents a PCA-model that consists of:
Definition: PcaModel.hpp:59
int get_num_principal_components() const
Definition: PcaModel.hpp:88
Eigen::MatrixXf get_rescaled_pca_basis_at_point(int vertex_id) const
Definition: PcaModel.hpp:223
const Eigen::VectorXf & get_mean() const
Definition: PcaModel.hpp:123
std::vector< float > fit_shape_to_landmarks_linear_multi(const morphablemodel::PcaModel &shape_model, const std::vector< Eigen::Matrix< float, 3, 4 > > &affine_camera_matrices, const std::vector< std::vector< Eigen::Vector2f > > &landmarks, const std::vector< std::vector< int > > &vertex_ids, std::vector< Eigen::VectorXf > base_faces=std::vector< Eigen::VectorXf >(), float lambda=3.0f, cpp17::optional< int > num_coefficients_to_fit=cpp17::optional< int >(), cpp17::optional< float > detector_standard_deviation=cpp17::optional< float >(), cpp17::optional< float > model_standard_deviation=cpp17::optional< float >())
Definition: linear_shape_fitting.hpp:178
std::vector< float > fit_shape_to_landmarks_linear(const morphablemodel::PcaModel &shape_model, Eigen::Matrix< float, 3, 4 > affine_camera_matrix, const std::vector< Eigen::Vector2f > &landmarks, const std::vector< int > &vertex_ids, Eigen::VectorXf base_face=Eigen::VectorXf(), float lambda=3.0f, cpp17::optional< int > num_coefficients_to_fit=cpp17::optional< int >(), cpp17::optional< float > detector_standard_deviation=cpp17::optional< float >(), cpp17::optional< float > model_standard_deviation=cpp17::optional< float >())
Definition: linear_shape_fitting.hpp:60
Namespace containing all of eos's 3D model fitting functionality.