Commit 21807b946114f4e0616a112e461d226061e99e80
1 parent
3b38780b
added some alignment transforms
Showing
1 changed file
with
613 additions
and
0 deletions
openbr/plugins/core/align.cpp
0 → 100644
| 1 | +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ||
| 2 | + * Copyright 2015 Noblis * | ||
| 3 | + * * | ||
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); * | ||
| 5 | + * you may not use this file except in compliance with the License. * | ||
| 6 | + * You may obtain a copy of the License at * | ||
| 7 | + * * | ||
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 * | ||
| 9 | + * * | ||
| 10 | + * Unless required by applicable law or agreed to in writing, software * | ||
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, * | ||
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * | ||
| 13 | + * See the License for the specific language governing permissions and * | ||
| 14 | + * limitations under the License. * | ||
| 15 | + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | ||
| 16 | + | ||
| 17 | +#include <opencv2/opencv.hpp> | ||
| 18 | +#include "openbr/plugins/openbr_internal.h" | ||
| 19 | +#include "openbr/core/qtutils.h" | ||
| 20 | +#include "openbr/core/opencvutils.h" | ||
| 21 | +#include "openbr/core/eigenutils.h" | ||
| 22 | +#include "openbr/core/common.h" | ||
| 23 | +#include <QString> | ||
| 24 | +#include <Eigen/SVD> | ||
| 25 | +#include <Eigen/Dense> | ||
| 26 | + | ||
| 27 | +using namespace std; | ||
| 28 | +using namespace cv; | ||
| 29 | +using namespace Eigen; | ||
| 30 | + | ||
| 31 | +namespace br | ||
| 32 | +{ | ||
| 33 | + | ||
| 34 | +/*! | ||
| 35 | + * \ingroup transforms | ||
| 36 | + * \brief Improved procrustes alignment of points, to include a post processing scaling of points | ||
| 37 | + * to faciliate subsequent texture mapping. | ||
| 38 | + * \author Brendan Klare \cite bklare | ||
| 39 | + * \param width Width of output coordinate space (before padding) | ||
| 40 | + * \param padding Amount of padding around the coordinate space | ||
| 41 | + * \param useFirst whether or not to use the first instance as the reference object | ||
| 42 | + */ | ||
| 43 | +class ProcrustesAlignTransform : public Transform | ||
| 44 | +{ | ||
| 45 | + Q_OBJECT | ||
| 46 | + | ||
| 47 | + Q_PROPERTY(float width READ get_width WRITE set_width RESET reset_width STORED false) | ||
| 48 | + Q_PROPERTY(float padding READ get_padding WRITE set_padding RESET reset_padding STORED false) | ||
| 49 | + Q_PROPERTY(bool useFirst READ get_useFirst WRITE set_useFirst RESET reset_useFirst STORED false) | ||
| 50 | + BR_PROPERTY(float, width, 80) | ||
| 51 | + BR_PROPERTY(float, padding, 8) | ||
| 52 | + BR_PROPERTY(bool, useFirst, false) | ||
| 53 | + | ||
| 54 | + | ||
| 55 | + Eigen::MatrixXf referenceShape; | ||
| 56 | + float minX; | ||
| 57 | + float minY; | ||
| 58 | + float maxX; | ||
| 59 | + float maxY; | ||
| 60 | + float aspectRatio; | ||
| 61 | + | ||
| 62 | + void init() { | ||
| 63 | + aspectRatio = 0; | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + static MatrixXf getRotation(MatrixXf ref, MatrixXf sample) { | ||
| 67 | + MatrixXf R = sample.transpose() * ref; | ||
| 68 | + JacobiSVD<MatrixXf> svd(R, ComputeFullU | ComputeFullV); | ||
| 69 | + R = svd.matrixU() * svd.matrixV().transpose(); | ||
| 70 | + return R; | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + //Converts x y points in a single vector to two column matrix | ||
| 74 | + static MatrixXf vectorToMatrix(MatrixXf vector) { | ||
| 75 | + int n = vector.rows(); | ||
| 76 | + MatrixXf matrix(n / 2, 2); | ||
| 77 | + for (int i = 0; i < n / 2; i++) { | ||
| 78 | + for (int j = 0; j < 2; j++) { | ||
| 79 | + matrix(i, j) = vector(i * 2 + j); | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + return matrix; | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + static MatrixXf matrixToVector(MatrixXf matrix) { | ||
| 86 | + int n2 = matrix.rows(); | ||
| 87 | + MatrixXf vector(n2 * 2, 1); | ||
| 88 | + for (int i = 0; i < n2; i++) { | ||
| 89 | + for (int j = 0; j < 2; j++) { | ||
| 90 | + vector(i * 2 + j) = matrix(i, j); | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + return vector; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + void train(const TemplateList &data) | ||
| 97 | + { | ||
| 98 | + MatrixXf points(data[0].file.points().size() * 2, data.size()); | ||
| 99 | + | ||
| 100 | + // Normalize all sets of points | ||
| 101 | + int skip = 0; | ||
| 102 | + for (int j = 0; j < data.size(); j++) { | ||
| 103 | + QList<QPointF> imagePoints = data[j].file.points(); | ||
| 104 | + if (imagePoints.size() == 0) { | ||
| 105 | + skip++; | ||
| 106 | + continue; | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + float meanX = 0, | ||
| 110 | + meanY = 0; | ||
| 111 | + for (int i = 0; i < imagePoints.size(); i++) { | ||
| 112 | + points(i * 2, j - skip) = imagePoints[i].x(); | ||
| 113 | + points(i * 2 + 1, j - skip) = imagePoints[i].y(); | ||
| 114 | + | ||
| 115 | + meanX += imagePoints[i].x(); | ||
| 116 | + meanY += imagePoints[i].y(); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + meanX /= imagePoints.size(); | ||
| 120 | + meanY /= imagePoints.size(); | ||
| 121 | + | ||
| 122 | + for (int i = 0; i < imagePoints.size(); i++) { | ||
| 123 | + points(i * 2, j - skip) -= meanX; | ||
| 124 | + points(i * 2 + 1, j - skip) -= meanY; | ||
| 125 | + } | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + points = MatrixXf(points.leftCols(data.size() - skip)); | ||
| 129 | + | ||
| 130 | + //normalize scale | ||
| 131 | + for (int i = 0; i < points.cols(); i++) | ||
| 132 | + points.col(i) = points.col(i) / points.col(i).norm(); | ||
| 133 | + | ||
| 134 | + //Normalize rotation | ||
| 135 | + if (!useFirst) { | ||
| 136 | + referenceShape = vectorToMatrix(points.rowwise().sum() / points.cols()); | ||
| 137 | + } else { | ||
| 138 | + referenceShape = vectorToMatrix(points.col(0)); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + for (int i = 0; i < points.cols(); i++) { | ||
| 142 | + MatrixXf p = vectorToMatrix(points.col(i)); | ||
| 143 | + MatrixXf R = getRotation(referenceShape, p); | ||
| 144 | + points.col(i) = matrixToVector(p * R); | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + //Choose crop boundaries and adjustments that captures most data | ||
| 148 | + MatrixXf minXs(points.cols(),1); | ||
| 149 | + MatrixXf minYs(points.cols(),1); | ||
| 150 | + MatrixXf maxXs(points.cols(),1); | ||
| 151 | + MatrixXf maxYs(points.cols(),1); | ||
| 152 | + for (int j = 0; j < points.cols(); j++) { | ||
| 153 | + minX = FLT_MAX, | ||
| 154 | + minY = FLT_MAX, | ||
| 155 | + maxX = -FLT_MAX, | ||
| 156 | + maxY = -FLT_MAX; | ||
| 157 | + for (int i = 0; i < points.rows(); i++) { | ||
| 158 | + if (i % 2 == 0) { | ||
| 159 | + if (points(i,j) > maxX) | ||
| 160 | + maxX = points(i, j); | ||
| 161 | + if (points(i,j) < minX) | ||
| 162 | + minX = points(i, j); | ||
| 163 | + } else { | ||
| 164 | + if (points(i,j) > maxY) | ||
| 165 | + maxY = points(i, j); | ||
| 166 | + if (points(i,j) < minY) | ||
| 167 | + minY = points(i, j); | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + minXs(j) = minX; | ||
| 172 | + maxXs(j) = maxX; | ||
| 173 | + minYs(j) = minY; | ||
| 174 | + maxYs(j) = maxY; | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + minX = minXs.mean() - 0 * EigenUtils::stddev(minXs); | ||
| 178 | + minY = minYs.mean() - 0 * EigenUtils::stddev(minYs); | ||
| 179 | + maxX = maxXs.mean() + 0 * EigenUtils::stddev(maxXs); | ||
| 180 | + maxY = maxYs.mean() + 0 * EigenUtils::stddev(maxYs); | ||
| 181 | + aspectRatio = (maxX - minX) / (maxY - minY); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + void project(const Template &src, Template &dst) const | ||
| 185 | + { | ||
| 186 | + QList<QPointF> imagePoints = src.file.points(); | ||
| 187 | + if (imagePoints.size() == 0) { | ||
| 188 | + dst.file.fte = true; | ||
| 189 | + qDebug() << "No points for file " << src.file.name; | ||
| 190 | + return; | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + MatrixXf p(imagePoints.size() * 2, 1); | ||
| 194 | + for (int i = 0; i < imagePoints.size(); i++) { | ||
| 195 | + p(i * 2) = imagePoints[i].x(); | ||
| 196 | + p(i * 2 + 1) = imagePoints[i].y(); | ||
| 197 | + } | ||
| 198 | + p = vectorToMatrix(p); | ||
| 199 | + | ||
| 200 | + //Normalize translation | ||
| 201 | + p.col(0) = p.col(0) - MatrixXf::Ones(p.rows(),1) * (p.col(0).sum() / p.rows()); | ||
| 202 | + p.col(1) = p.col(1) - MatrixXf::Ones(p.rows(),1) * (p.col(1).sum() / p.rows()); | ||
| 203 | + | ||
| 204 | + //Normalize scale | ||
| 205 | + p /= matrixToVector(p).norm(); | ||
| 206 | + | ||
| 207 | + //Normalize rotation | ||
| 208 | + MatrixXf R = getRotation(referenceShape, p); | ||
| 209 | + p = p * R; | ||
| 210 | + | ||
| 211 | + //Translate and scale into output space and store in output list | ||
| 212 | + QList<QPointF> procrustesPoints; | ||
| 213 | + for (int i = 0; i < p.rows(); i++) | ||
| 214 | + procrustesPoints.append( QPointF( | ||
| 215 | + (p(i, 0) - minX) / (maxX - minX) * (width - 1) + padding, | ||
| 216 | + (p(i, 1) - minY) / (maxY - minY) * (qRound( width / aspectRatio) - 1) + padding)); | ||
| 217 | + | ||
| 218 | + dst = src; | ||
| 219 | + dst.file.setList<QPointF>("ProcrustesPoints", procrustesPoints); | ||
| 220 | + dst.file.set("ProcrustesBound", QRectF(0, 0, width + 2 * padding, (qRound(width / aspectRatio) + 2 * padding))); | ||
| 221 | + dst.file.set("ProcrustesPadding", padding); | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + void store(QDataStream &stream) const | ||
| 225 | + { | ||
| 226 | + stream << referenceShape; | ||
| 227 | + stream << minX; | ||
| 228 | + stream << minY; | ||
| 229 | + stream << maxX; | ||
| 230 | + stream << maxY; | ||
| 231 | + stream << aspectRatio; | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + void load(QDataStream &stream) | ||
| 235 | + { | ||
| 236 | + stream >> referenceShape; | ||
| 237 | + stream >> minX; | ||
| 238 | + stream >> minY; | ||
| 239 | + stream >> maxX; | ||
| 240 | + stream >> maxY; | ||
| 241 | + stream >> aspectRatio; | ||
| 242 | + } | ||
| 243 | +}; | ||
| 244 | +BR_REGISTER(Transform, ProcrustesAlignTransform) | ||
| 245 | + | ||
| 246 | +/*! | ||
| 247 | + * \ingroup transforms | ||
| 248 | + * \brief Maps texture from one set of points to another. Assumes that points are rigidly transformed | ||
| 249 | + * \author Brendan Klare \cite bklare | ||
| 250 | + * \author Scott Klum \cite sklum | ||
| 251 | + */ | ||
| 252 | +class TextureMapTransform : public UntrainableTransform | ||
| 253 | +{ | ||
| 254 | + Q_OBJECT | ||
| 255 | + | ||
| 256 | +public: | ||
| 257 | + static QRectF getBounds(const QList<QPointF> &points, int dstPadding) | ||
| 258 | + { | ||
| 259 | + float srcMinX = FLT_MAX; | ||
| 260 | + float srcMinY = FLT_MAX; | ||
| 261 | + float srcMaxX = -FLT_MAX; | ||
| 262 | + float srcMaxY = -FLT_MAX; | ||
| 263 | + foreach (const QPointF &point, points) { | ||
| 264 | + if (point.x() < srcMinX) srcMinX = point.x(); | ||
| 265 | + if (point.y() < srcMinY) srcMinY = point.y(); | ||
| 266 | + if (point.x() > srcMaxX) srcMaxX = point.x(); | ||
| 267 | + if (point.y() > srcMaxY) srcMaxY = point.y(); | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + const float padding = (srcMaxX - srcMinX) / 80 * dstPadding; | ||
| 271 | + return QRectF(qRound(srcMinX - padding), qRound(srcMinY - padding), qRound(srcMaxX - srcMinX + 2 * padding), qRound(srcMaxY - srcMinY + 2 * padding)); | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + static int getVertexIndex(const QPointF &trianglePts, const QList<QPointF> &pts) | ||
| 275 | + { | ||
| 276 | + for (int i = 0; i < pts.size(); i++) | ||
| 277 | + // Check points using single precision accuracy to avoid potential rounding error | ||
| 278 | + if ((float(trianglePts.x()) == float(pts[i].x())) && (float(trianglePts.y()) == float(pts[i].y()))) | ||
| 279 | + return i; | ||
| 280 | + qFatal("Couldn't identify index of requested point!"); | ||
| 281 | + return -1; | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + static QList<QPointF> addBounds(QList<QPointF> points, const QRectF &bound) | ||
| 285 | + { | ||
| 286 | + points.append(bound.topLeft()); | ||
| 287 | + points.append(QPointF(bound.right() - 1, bound.top())); | ||
| 288 | + points.append(QPointF(bound.left(), bound.bottom() - 1)); | ||
| 289 | + points.append(QPointF(bound.right() - 1, bound.bottom() - 1)); | ||
| 290 | + return points; | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + static QList<QPointF> removeBounds(const QList<QPointF> &points) | ||
| 294 | + { | ||
| 295 | + return points.mid(0, points.size() - 4); | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + //Expand out bounds placed at end of point list by addBounds | ||
| 299 | + static QList<QPointF> expandBounds(QList<QPointF> points, int pad) | ||
| 300 | + { | ||
| 301 | + const int n = points.size(); | ||
| 302 | + points[n-4] = QPointF(points[n-4].x() - pad, points[n-4].y() - pad); | ||
| 303 | + points[n-3] = QPointF(points[n-3].x() + pad, points[n-3].y() - pad); | ||
| 304 | + points[n-2] = QPointF(points[n-2].x() - pad, points[n-2].y() + pad); | ||
| 305 | + points[n-1] = QPointF(points[n-1].x() + pad, points[n-1].y() + pad); | ||
| 306 | + return points; | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + //Contract in bounds placed at end of point list by addBounds | ||
| 310 | + static QList<QPointF> contractBounds(QList<QPointF> points, int pad) | ||
| 311 | + { | ||
| 312 | + const int n = points.size(); | ||
| 313 | + points[n-4] = QPointF(points[n-4].x() + pad, points[n-4].y() + pad); | ||
| 314 | + points[n-3] = QPointF(points[n-3].x() - pad, points[n-3].y() + pad); | ||
| 315 | + points[n-2] = QPointF(points[n-2].x() + pad, points[n-2].y() - pad); | ||
| 316 | + points[n-1] = QPointF(points[n-1].x() - pad, points[n-1].y() - pad); | ||
| 317 | + return points; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + static QList<QList<int> > getTriangulation(const QList<QPointF> &points, const QRectF &bound) | ||
| 321 | + { | ||
| 322 | + Subdiv2D subdiv(OpenCVUtils::toRect(bound)); | ||
| 323 | + foreach (const QPointF &point, points) { | ||
| 324 | + if (!bound.contains(point)) | ||
| 325 | + return QList<QList<int> >(); | ||
| 326 | + subdiv.insert(OpenCVUtils::toPoint(point)); | ||
| 327 | + } | ||
| 328 | + | ||
| 329 | + | ||
| 330 | + vector<Vec6f> triangleList; | ||
| 331 | + subdiv.getTriangleList(triangleList); | ||
| 332 | + | ||
| 333 | + QList<QList<int> > triangleIndices; | ||
| 334 | + foreach (const Vec6f &triangle, triangleList) { | ||
| 335 | + bool valid = true; | ||
| 336 | + const QPointF vertices[3] = { QPointF(triangle[0], triangle[1]), | ||
| 337 | + QPointF(triangle[2], triangle[3]), | ||
| 338 | + QPointF(triangle[4], triangle[5]) }; | ||
| 339 | + for (int j = 0; j < 3; j++) | ||
| 340 | + if (vertices[j].x() > bound.right() || vertices[j].y() > bound.bottom() || vertices[j].x() < bound.left() || vertices[j].y() < bound.top()) { | ||
| 341 | + valid = false; | ||
| 342 | + break; | ||
| 343 | + } | ||
| 344 | + | ||
| 345 | + if (valid) { | ||
| 346 | + QList<int> tri; | ||
| 347 | + for (int j = 0; j < 3; j++) | ||
| 348 | + tri.append(getVertexIndex(vertices[j], points)); | ||
| 349 | + triangleIndices.append(tri); | ||
| 350 | + } | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + return triangleIndices; | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | +private: | ||
| 357 | + void project(const Template &src, Template &dst) const | ||
| 358 | + { | ||
| 359 | + QList<QPointF> dstPoints = dst.file.getList<QPointF>("ProcrustesPoints"); | ||
| 360 | + QList<QPointF> srcPoints = dst.file.points(); | ||
| 361 | + if (dstPoints.empty() || srcPoints.empty()) { | ||
| 362 | + dst = src; | ||
| 363 | + if (Globals->verbose) { | ||
| 364 | + qWarning("Delauney triangulation failed because points or rects are empty."); | ||
| 365 | + dst.file.fte = true; | ||
| 366 | + } | ||
| 367 | + return; | ||
| 368 | + } | ||
| 369 | + | ||
| 370 | + QRectF dstBound = dst.file.get<QRectF>("ProcrustesBound"); | ||
| 371 | + dstPoints = addBounds(dstPoints, dstBound); | ||
| 372 | + | ||
| 373 | + /*Add a wider bound for triangulation to prevent border triangles from being missing*/ | ||
| 374 | + QRectF srcBound = getBounds(srcPoints, dst.file.get<int>("ProcrustesPadding") + 20); | ||
| 375 | + srcPoints = addBounds(srcPoints, srcBound); | ||
| 376 | + QList<QList<int> > triIndices = getTriangulation(srcPoints, srcBound); | ||
| 377 | + | ||
| 378 | + /*Remove wider bound for texture mapping*/ | ||
| 379 | + srcPoints = removeBounds(srcPoints); | ||
| 380 | + srcBound = getBounds(srcPoints, dst.file.get<int>("ProcrustesPadding")); | ||
| 381 | + srcPoints = addBounds(srcPoints, srcBound); | ||
| 382 | + | ||
| 383 | + int dstWidth = dstBound.width() + dstBound.x(); | ||
| 384 | + int dstHeight = dstBound.height() + dstBound.y(); | ||
| 385 | + dst.m() = Mat::zeros(dstHeight, dstWidth, src.m().type()); | ||
| 386 | + for (int i = 0; i < triIndices.size(); i++) { | ||
| 387 | + Point2f srcPoint1[3]; | ||
| 388 | + Point2f dstPoint1[3]; | ||
| 389 | + for (int j = 0; j < 3; j++) { | ||
| 390 | + srcPoint1[j] = OpenCVUtils::toPoint(srcPoints[triIndices[i][j]]); | ||
| 391 | + dstPoint1[j] = OpenCVUtils::toPoint(dstPoints[triIndices[i][j]]); | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + Mat buffer(dstHeight, dstWidth, src.m().type()); | ||
| 395 | + warpAffine(src.m(), buffer, getAffineTransform(srcPoint1, dstPoint1), Size(dstBound.width(), dstBound.height())); | ||
| 396 | + | ||
| 397 | + Mat mask = Mat::zeros(dstHeight, dstWidth, CV_8UC1); | ||
| 398 | + Point maskPoints[1][3]; | ||
| 399 | + maskPoints[0][0] = dstPoint1[0]; | ||
| 400 | + maskPoints[0][1] = dstPoint1[1]; | ||
| 401 | + maskPoints[0][2] = dstPoint1[2]; | ||
| 402 | + const Point* ppt = { maskPoints[0] }; | ||
| 403 | + fillConvexPoly(mask, ppt, 3, Scalar(255, 255, 255), 8); | ||
| 404 | + | ||
| 405 | + for (int i = 0; i < dstHeight; i++) { | ||
| 406 | + for (int j = 0; j < dstWidth; j++) { | ||
| 407 | + if (mask.at<uchar>(i,j) == 255) { | ||
| 408 | + if (dst.m().type() == CV_32FC3 || dst.m().type() == CV_8UC3) | ||
| 409 | + dst.m().at<cv::Vec3b>(i,j) = buffer.at<cv::Vec3b>(i,j); | ||
| 410 | + else if (dst.m().type() == CV_32F) | ||
| 411 | + dst.m().at<float>(i,j) = buffer.at<float>(i,j); | ||
| 412 | + else if (dst.m().type() == CV_8U) | ||
| 413 | + dst.m().at<uchar>(i,j) = buffer.at<uchar>(i,j); | ||
| 414 | + else | ||
| 415 | + qFatal("Unsupported pixel format."); | ||
| 416 | + } | ||
| 417 | + } | ||
| 418 | + } | ||
| 419 | + | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + dst.file = src.file; | ||
| 423 | + dst.file.clearPoints(); | ||
| 424 | + dst.file.clearRects(); | ||
| 425 | + dst.file.remove("ProcrustesPoints"); | ||
| 426 | + dst.file.remove("ProcrustesPadding"); | ||
| 427 | + dst.file.remove("ProcrustesBounds"); | ||
| 428 | + } | ||
| 429 | +}; | ||
| 430 | + | ||
| 431 | +BR_REGISTER(Transform, TextureMapTransform) | ||
| 432 | + | ||
| 433 | +// SynthesizePointsTransform helper class | ||
| 434 | +struct TriangleIndicies | ||
| 435 | +{ | ||
| 436 | + int indicies[3]; | ||
| 437 | + | ||
| 438 | + TriangleIndicies() | ||
| 439 | + { | ||
| 440 | + indicies[0] = 0; | ||
| 441 | + indicies[1] = 0; | ||
| 442 | + indicies[2] = 0; | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + TriangleIndicies(QList<int> indexList) | ||
| 446 | + { | ||
| 447 | + assert(indexList.size() == 3); | ||
| 448 | + qSort(indexList); | ||
| 449 | + indicies[0] = indexList[0]; | ||
| 450 | + indicies[1] = indexList[1]; | ||
| 451 | + indicies[2] = indexList[2]; | ||
| 452 | + } | ||
| 453 | +}; | ||
| 454 | + | ||
| 455 | +inline bool operator==(const TriangleIndicies &a, const TriangleIndicies &b) | ||
| 456 | +{ | ||
| 457 | + return (a.indicies[0] == b.indicies[0]) && (a.indicies[1] == b.indicies[1]) && (a.indicies[2] == b.indicies[2]); | ||
| 458 | +} | ||
| 459 | + | ||
| 460 | +inline uint qHash(const TriangleIndicies &key) | ||
| 461 | +{ | ||
| 462 | + return ::qHash(key.indicies[0]) ^ ::qHash(key.indicies[1]) ^ ::qHash(key.indicies[2]); | ||
| 463 | +} | ||
| 464 | + | ||
| 465 | +QDataStream &operator<<(QDataStream &stream, const TriangleIndicies &ti) | ||
| 466 | +{ | ||
| 467 | + return stream << ti.indicies[0] << ti.indicies[1] << ti.indicies[2]; | ||
| 468 | +} | ||
| 469 | + | ||
| 470 | +QDataStream &operator>>(QDataStream &stream, TriangleIndicies &ti) | ||
| 471 | +{ | ||
| 472 | + return stream >> ti.indicies[0] >> ti.indicies[1] >> ti.indicies[2]; | ||
| 473 | +} | ||
| 474 | + | ||
| 475 | +/*! | ||
| 476 | + * \ingroup transforms | ||
| 477 | + * \brief Synthesize additional points via triangulation. | ||
| 478 | + * \author Josh Klontz \cite jklontz | ||
| 479 | + */ | ||
| 480 | + class SynthesizePointsTransform : public MetadataTransform | ||
| 481 | + { | ||
| 482 | + Q_OBJECT | ||
| 483 | + Q_PROPERTY(float minRelativeDistance READ get_minRelativeDistance WRITE set_minRelativeDistance RESET reset_minRelativeDistance STORED false) | ||
| 484 | + BR_PROPERTY(float, minRelativeDistance, 0) // [0, 1] range controlling whether or not to nearby synthetic points. | ||
| 485 | + // 0 = keep all points, 1 = keep only the most distance point. | ||
| 486 | + | ||
| 487 | + QList<TriangleIndicies> triangles; | ||
| 488 | + | ||
| 489 | + void train(const TemplateList &data) | ||
| 490 | + { | ||
| 491 | + // Because not all triangulations are the same, we have to decide on a canonical set of triangles at training time. | ||
| 492 | + QHash<TriangleIndicies, int> counts; | ||
| 493 | + foreach (const Template &datum, data) { | ||
| 494 | + | ||
| 495 | + const QList<QPointF> points = datum.file.points(); | ||
| 496 | + if (points.size() == 0) | ||
| 497 | + continue; | ||
| 498 | + const QList< QList<int> > triangulation = TextureMapTransform::getTriangulation(points, TextureMapTransform::getBounds(points, 10)); | ||
| 499 | + if (triangulation.empty()) | ||
| 500 | + continue; | ||
| 501 | + | ||
| 502 | + foreach (const QList<int> &indicies, triangulation) | ||
| 503 | + counts[TriangleIndicies(indicies)]++; | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + triangles.clear(); | ||
| 507 | + QHash<TriangleIndicies, int>::const_iterator i = counts.constBegin(); | ||
| 508 | + while (i != counts.constEnd()) { | ||
| 509 | + if (3 * i.value() > data.size()) | ||
| 510 | + triangles.append(i.key()); // Keep triangles that occur in at least a third of the training instances | ||
| 511 | + ++i; | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + if (minRelativeDistance > 0) { // Discard relatively small triangles | ||
| 515 | + QVector<float> averageMinDistances(triangles.size()); | ||
| 516 | + foreach (const Template &datum, data) { | ||
| 517 | + File dst; | ||
| 518 | + projectMetadata(datum.file, dst); | ||
| 519 | + const QList<QPointF> points = dst.points(); | ||
| 520 | + | ||
| 521 | + QVector<float> minDistances(triangles.size()); | ||
| 522 | + for (int i=0; i<triangles.size(); i++) { | ||
| 523 | + // Work backwards so that we can also consider distances between synthetic points | ||
| 524 | + const QPointF &point = points[points.size()-1-i]; | ||
| 525 | + float minDistance = std::numeric_limits<float>::max(); | ||
| 526 | + for (int j=0; j<points.size()-1-i; j++) | ||
| 527 | + minDistance = min(minDistance, sqrtf(powf(point.x() - points[j].x(), 2.f) + powf(point.y() - points[j].y(), 2.f))); | ||
| 528 | + minDistances[triangles.size()-1-i] = minDistance; | ||
| 529 | + } | ||
| 530 | + | ||
| 531 | + const float maxMinDistance = Common::Max(minDistances); | ||
| 532 | + for (int i=0; i<minDistances.size(); i++) | ||
| 533 | + averageMinDistances[i] += (minDistances[i] / maxMinDistance); | ||
| 534 | + } | ||
| 535 | + | ||
| 536 | + const float maxAverageMinDistance = Common::Max(averageMinDistances); | ||
| 537 | + for (int i=averageMinDistances.size()-1; i>=0; i--) | ||
| 538 | + if (averageMinDistances[i] / maxAverageMinDistance < minRelativeDistance) | ||
| 539 | + triangles.removeAt(i); | ||
| 540 | + } | ||
| 541 | + | ||
| 542 | + if (Globals->verbose) | ||
| 543 | + qDebug() << "Kept" << triangles.size() << "of" << counts.size() << "triangles."; | ||
| 544 | + } | ||
| 545 | + | ||
| 546 | + void projectMetadata(const File &src, File &dst) const | ||
| 547 | + { | ||
| 548 | + QList<QPointF> points = src.points(); | ||
| 549 | + if (points.size() == 0) { | ||
| 550 | + dst.fte = true; | ||
| 551 | + return; | ||
| 552 | + } | ||
| 553 | + | ||
| 554 | + foreach (const TriangleIndicies &triangle, triangles) { | ||
| 555 | + const QPointF &p0 = points[triangle.indicies[0]]; | ||
| 556 | + const QPointF &p1 = points[triangle.indicies[1]]; | ||
| 557 | + const QPointF &p2 = points[triangle.indicies[2]]; | ||
| 558 | + points.append((p0 + p1 + p2) / 3 /* append the centroid of the triangle */); | ||
| 559 | + } | ||
| 560 | + dst.setPoints(points); | ||
| 561 | + } | ||
| 562 | + | ||
| 563 | + void store(QDataStream &stream) const | ||
| 564 | + { | ||
| 565 | + stream << triangles; | ||
| 566 | + } | ||
| 567 | + | ||
| 568 | + void load(QDataStream &stream) | ||
| 569 | + { | ||
| 570 | + stream >> triangles; | ||
| 571 | + } | ||
| 572 | + }; | ||
| 573 | + BR_REGISTER(Transform, SynthesizePointsTransform) | ||
| 574 | + | ||
| 575 | +/*! | ||
| 576 | + * \ingroup initializers | ||
| 577 | + * \brief Initialize Procrustes croppings | ||
| 578 | + * \author Brendan Klare \cite bklare | ||
| 579 | + */ | ||
| 580 | +class ProcrustesInitializer : public Initializer | ||
| 581 | +{ | ||
| 582 | + Q_OBJECT | ||
| 583 | + | ||
| 584 | + void initialize() const | ||
| 585 | + { | ||
| 586 | + Globals->abbreviations.insert("ProcrustesStasmFace","SelectPoints([17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76])+ProcrustesAlign(padding=6,width=120)+TextureMap+Resize(96,96)"); | ||
| 587 | + Globals->abbreviations.insert("ProcrustesStasmEyes","SelectPoints([28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])+ProcrustesAlign(padding=8)+TextureMap+Resize(24,48)"); | ||
| 588 | + Globals->abbreviations.insert("ProcrustesStasmPeriocular","SelectPoints([28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,16,17,18,19,20,21,22,23,24,25,26,27])+ProcrustesAlign(padding=10)+TextureMap+Resize(36,48)"); | ||
| 589 | + Globals->abbreviations.insert("ProcrustesStasmBrow","SelectPoints([16,17,18,19,20,21,22,23,24,25,26,27])+ProcrustesAlign(padding=8)+TextureMap+Resize(24,48)"); | ||
| 590 | + Globals->abbreviations.insert("ProcrustesStasmNose","SelectPoints([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58])+ProcrustesAlign(padding=12)+TextureMap+Resize(36,48)"); | ||
| 591 | + Globals->abbreviations.insert("ProcrustesStasmMouth","SelectPoints([59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76])+ProcrustesAlign(padding=10)+TextureMap+Resize(36,48)"); | ||
| 592 | + Globals->abbreviations.insert("ProcrustesStasmJaw", "SelectPoints([2,3,4,5,6,7,8,9,10])+ProcrustesAlign(padding=8)+TextureMap+Resize(36,48)"); | ||
| 593 | + | ||
| 594 | + Globals->abbreviations.insert("ProcrustesEyes","SelectPoints([19,20,21,22,23,24,25,26,27,28,29,30])+ProcrustesAlign(padding=8)+TextureMap+Resize(24,48)"); | ||
| 595 | + Globals->abbreviations.insert("ProcrustesNose","SelectPoints([12,13,14,15,16,17,18])+ProcrustesAlign(padding=30)+TextureMap+Resize(36,48)"); | ||
| 596 | + Globals->abbreviations.insert("ProcrustesMouth","SelectPoints([31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50])+ProcrustesAlign(padding=6)+TextureMap+Resize(36,48)"); | ||
| 597 | + Globals->abbreviations.insert("ProcrustesBrow","SelectPoints([0,1,2,3,4,5,6,7,8,9])+ProcrustesAlign(padding=6)+TextureMap+Resize(24,48)"); | ||
| 598 | + Globals->abbreviations.insert("ProcrustesFace","ProcrustesAlign(padding=6,width=120)+TextureMap+Resize(96,96)"); | ||
| 599 | + | ||
| 600 | + Globals->abbreviations.insert("ProcrustesLargeStasmFace","ProcrustesAlign(padding=18)+TextureMap+Resize(480,480)"); | ||
| 601 | + Globals->abbreviations.insert("ProcrustesLargeStasmEyes","SelectPoints([28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47])+ProcrustesAlign(padding=8)+TextureMap+Resize(240,480)"); | ||
| 602 | + Globals->abbreviations.insert("ProcrustesLargeStasmPeriocular","SelectPoints([28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,16,17,18,19,20,21,22,23,24,25,26,27])+ProcrustesAlign(padding=10)+TextureMap+Resize(360,480)"); | ||
| 603 | + Globals->abbreviations.insert("ProcrustesLargeStasmBrow","SelectPoints([16,17,18,19,20,21,22,23,24,25,26,27])+ProcrustesAlign(padding=8)+TextureMap+Resize(240,480)"); | ||
| 604 | + Globals->abbreviations.insert("ProcrustesLargeStasmNose","SelectPoints([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58])+ProcrustesAlign(padding=12)+TextureMap+Resize(360,480)"); | ||
| 605 | + Globals->abbreviations.insert("ProcrustesLargeStasmMouth","SelectPoints([59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76])+ProcrustesAlign(padding=20)+TextureMap+Resize(360,480)"); | ||
| 606 | + Globals->abbreviations.insert("ProcrustesLargeStasmJaw", "SelectPoints([2,3,4,5,6,7,8,9,10])+ProcrustesAlign(padding=8)+TextureMap+Resize(360,480)"); | ||
| 607 | + } | ||
| 608 | +}; | ||
| 609 | +BR_REGISTER(Initializer, ProcrustesInitializer) | ||
| 610 | + | ||
| 611 | +} // namespace br | ||
| 612 | + | ||
| 613 | +#include "align.moc" |