Commit ffbadb9bf53d52e6a848c18f73594419819a5433
Merge pull request #210 from biometrics/Procrustes
Procrustes
Showing
1 changed file
with
185 additions
and
0 deletions
openbr/plugins/landmarks.cpp
| ... | ... | @@ -5,9 +5,11 @@ |
| 5 | 5 | #include "openbr/core/eigenutils.h" |
| 6 | 6 | #include <QString> |
| 7 | 7 | #include <Eigen/SVD> |
| 8 | +#include <Eigen/Dense> | |
| 8 | 9 | |
| 9 | 10 | using namespace std; |
| 10 | 11 | using namespace cv; |
| 12 | +using namespace Eigen; | |
| 11 | 13 | |
| 12 | 14 | namespace br |
| 13 | 15 | { |
| ... | ... | @@ -139,6 +141,189 @@ BR_REGISTER(Transform, ProcrustesTransform) |
| 139 | 141 | |
| 140 | 142 | /*! |
| 141 | 143 | * \ingroup transforms |
| 144 | + * \brief Improved procrustes alignment of points, to include a post processing scaling of points | |
| 145 | + * to faciliate subsequent texture mapping. | |
| 146 | + * \author Brendan Klare \cite bklare | |
| 147 | + */ | |
| 148 | +class ProcrustesAlignTransform : public Transform | |
| 149 | +{ | |
| 150 | + Q_OBJECT | |
| 151 | + | |
| 152 | + Q_PROPERTY(float width READ get_width WRITE set_width RESET reset_width STORED false) | |
| 153 | + Q_PROPERTY(float padding READ get_padding WRITE set_padding RESET reset_padding STORED false) | |
| 154 | + BR_PROPERTY(float, width, 80) | |
| 155 | + BR_PROPERTY(float, padding, 8) | |
| 156 | + | |
| 157 | + Eigen::MatrixXf referenceShape; | |
| 158 | + float minX; | |
| 159 | + float minY; | |
| 160 | + float maxX; | |
| 161 | + float maxY; | |
| 162 | + float aspectRatio; | |
| 163 | + | |
| 164 | + void init() { | |
| 165 | + minX = FLT_MAX, | |
| 166 | + minY = FLT_MAX, | |
| 167 | + maxX = -FLT_MAX, | |
| 168 | + maxY = -FLT_MAX; | |
| 169 | + aspectRatio = 0; | |
| 170 | + } | |
| 171 | + | |
| 172 | + static MatrixXf getRotation(MatrixXf ref, MatrixXf sample) { | |
| 173 | + MatrixXf R = ref.transpose() * sample; | |
| 174 | + JacobiSVD<MatrixXf> svd(R, ComputeFullU | ComputeFullV); | |
| 175 | + R = svd.matrixU() * svd.matrixV(); | |
| 176 | + return R; | |
| 177 | + } | |
| 178 | + | |
| 179 | + //Converts x y points in a single vector to two column matrix | |
| 180 | + static MatrixXf vectorToMatrix(MatrixXf vector) { | |
| 181 | + int n = vector.rows(); | |
| 182 | + MatrixXf matrix(n / 2, 2); | |
| 183 | + for (int i = 0; i < n / 2; i++) { | |
| 184 | + for (int j = 0; j < 2; j++) { | |
| 185 | + matrix(i, j) = vector(i * 2 + j); | |
| 186 | + } | |
| 187 | + } | |
| 188 | + return matrix; | |
| 189 | + } | |
| 190 | + | |
| 191 | + static MatrixXf matrixToVector(MatrixXf matrix) { | |
| 192 | + int n2 = matrix.rows(); | |
| 193 | + MatrixXf vector(n2 * 2, 1); | |
| 194 | + for (int i = 0; i < n2; i++) { | |
| 195 | + for (int j = 0; j < 2; j++) { | |
| 196 | + vector(i * 2 + j) = matrix(i, j); | |
| 197 | + } | |
| 198 | + } | |
| 199 | + return vector; | |
| 200 | + } | |
| 201 | + | |
| 202 | + void train(const TemplateList &data) | |
| 203 | + { | |
| 204 | + MatrixXf points(data[0].file.points().size() * 2, data.size()); | |
| 205 | + | |
| 206 | + // Normalize all sets of points | |
| 207 | + for (int j = 0; j < data.size(); j++) { | |
| 208 | + QList<QPointF> imagePoints = data[j].file.points(); | |
| 209 | + | |
| 210 | + float meanX = 0, | |
| 211 | + meanY = 0; | |
| 212 | + for (int i = 0; i < imagePoints.size(); i++) { | |
| 213 | + points(i * 2, j) = imagePoints[i].x(); | |
| 214 | + points(i * 2 + 1, j) = imagePoints[i].y(); | |
| 215 | + meanX += imagePoints[i].x(); | |
| 216 | + meanY += imagePoints[i].y(); | |
| 217 | + } | |
| 218 | + meanX /= imagePoints.size(); | |
| 219 | + meanY /= imagePoints.size(); | |
| 220 | + | |
| 221 | + for (int i = 0; i < imagePoints.size(); i++) { | |
| 222 | + points(i * 2, j) -= meanX; | |
| 223 | + points(i * 2 + 1, j) -= meanY; | |
| 224 | + } | |
| 225 | + } | |
| 226 | + | |
| 227 | + //normalize scale | |
| 228 | + for (int i = 0; i < points.cols(); i++) | |
| 229 | + points.col(i) = points.col(i) / points.col(i).norm(); | |
| 230 | + | |
| 231 | + //Normalize rotation | |
| 232 | + MatrixXf refPrev; | |
| 233 | + referenceShape = vectorToMatrix(points.rowwise().sum() / points.cols()); | |
| 234 | + float diff = FLT_MAX; | |
| 235 | + while (diff > 1e-5) {//iterate until reference shape is stable | |
| 236 | + refPrev = referenceShape; | |
| 237 | + | |
| 238 | + for (int j = 0; j < points.cols(); j++) { | |
| 239 | + MatrixXf p = vectorToMatrix(points.col(j)); | |
| 240 | + MatrixXf R = getRotation(referenceShape, p); | |
| 241 | + p = p * R.transpose(); | |
| 242 | + points.col(j) = matrixToVector(p); | |
| 243 | + } | |
| 244 | + referenceShape = vectorToMatrix(points.rowwise().sum() / points.cols()); | |
| 245 | + diff = (matrixToVector(referenceShape) - matrixToVector(refPrev)).norm(); | |
| 246 | + } | |
| 247 | + | |
| 248 | + referenceShape = vectorToMatrix(points.rowwise().sum() / points.cols()); | |
| 249 | + | |
| 250 | + //Choose crop boundaries and adjustments that captures all data | |
| 251 | + for (int i = 0; i < points.rows(); i++) { | |
| 252 | + for (int j = 0; j < points.cols(); j++) { | |
| 253 | + if (i % 2 == 0) { | |
| 254 | + if (points(i,j) > maxX) | |
| 255 | + maxX = points(i, j); | |
| 256 | + if (points(i,j) < minX) | |
| 257 | + minX = points(i, j); | |
| 258 | + } else { | |
| 259 | + if (points(i,j) > maxY) | |
| 260 | + maxY = points(i, j); | |
| 261 | + if (points(i,j) < minY) | |
| 262 | + minY = points(i, j); | |
| 263 | + } | |
| 264 | + } | |
| 265 | + } | |
| 266 | + aspectRatio = (maxX - minX) / (maxY - minY); | |
| 267 | + } | |
| 268 | + | |
| 269 | + void project(const Template &src, Template &dst) const | |
| 270 | + { | |
| 271 | + QList<QPointF> imagePoints = src.file.points(); | |
| 272 | + MatrixXf p(imagePoints.size() * 2, 1); | |
| 273 | + for (int i = 0; i < imagePoints.size(); i++) { | |
| 274 | + p(i * 2) = imagePoints[i].x(); | |
| 275 | + p(i * 2 + 1) = imagePoints[i].y(); | |
| 276 | + } | |
| 277 | + p = vectorToMatrix(p); | |
| 278 | + | |
| 279 | + //Normalize translation | |
| 280 | + p.col(0) = p.col(0) - MatrixXf::Ones(p.rows(),1) * (p.col(0).sum() / p.rows()); | |
| 281 | + p.col(1) = p.col(1) - MatrixXf::Ones(p.rows(),1) * (p.col(1).sum() / p.rows()); | |
| 282 | + | |
| 283 | + //Normalize scale | |
| 284 | + p /= matrixToVector(p).norm(); | |
| 285 | + | |
| 286 | + //Normalize rotation | |
| 287 | + MatrixXf R = getRotation(referenceShape, p); | |
| 288 | + p = p * R.transpose(); | |
| 289 | + | |
| 290 | + //Translate and scale into output space and store in output list | |
| 291 | + QList<QPointF> procrustesPoints; | |
| 292 | + for (int i = 0; i < p.rows(); i++) | |
| 293 | + procrustesPoints.append( QPointF( | |
| 294 | + (p(i, 0) - minX) / (maxX - minX) * (width - 1) + padding, | |
| 295 | + (p(i, 1) - minY) / (maxY - minY) * (qRound( width / aspectRatio) - 1) + padding)); | |
| 296 | + | |
| 297 | + dst = src; | |
| 298 | + dst.file.setList<QPointF>("ProcrustesPoints", procrustesPoints); | |
| 299 | + dst.file.set("ProcrustesBound", QRectF(0, 0, width + 2 * padding, (qRound(width / aspectRatio) + 2 * padding))); | |
| 300 | + } | |
| 301 | + | |
| 302 | + void store(QDataStream &stream) const | |
| 303 | + { | |
| 304 | + stream << referenceShape; | |
| 305 | + stream << minX; | |
| 306 | + stream << minY; | |
| 307 | + stream << maxX; | |
| 308 | + stream << maxY; | |
| 309 | + stream << aspectRatio; | |
| 310 | + } | |
| 311 | + | |
| 312 | + void load(QDataStream &stream) | |
| 313 | + { | |
| 314 | + stream >> referenceShape; | |
| 315 | + stream >> minX; | |
| 316 | + stream >> minY; | |
| 317 | + stream >> maxX; | |
| 318 | + stream >> maxY; | |
| 319 | + stream >> aspectRatio; | |
| 320 | + } | |
| 321 | +}; | |
| 322 | + | |
| 323 | +BR_REGISTER(Transform, ProcrustesAlignTransform) | |
| 324 | + | |
| 325 | +/*! | |
| 326 | + * \ingroup transforms | |
| 142 | 327 | * \brief Creates a Delaunay triangulation based on a set of points |
| 143 | 328 | * \author Scott Klum \cite sklum |
| 144 | 329 | */ | ... | ... |