Commit 1bff5f2f8623f1c1355ca90228940def28ab5944
1 parent
2929d5f1
EvalLandmarking now corrects for systematic landmark bias
Showing
3 changed files
with
57 additions
and
7 deletions
openbr/core/eval.cpp
| @@ -850,7 +850,7 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -850,7 +850,7 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 850 | QStringList truthNames = File::get<QString>(truth, "name"); | 850 | QStringList truthNames = File::get<QString>(truth, "name"); |
| 851 | 851 | ||
| 852 | int skipped = 0; | 852 | int skipped = 0; |
| 853 | - QList< QList<float> > pointErrors; | 853 | + QList< QList<float> > pointErrorMagnitudes, pointErrorOrientations; |
| 854 | QList<float> imageErrors; | 854 | QList<float> imageErrors; |
| 855 | QList<float> normalizedLengths; | 855 | QList<float> normalizedLengths; |
| 856 | for (int i=0; i<predicted.size(); i++) { | 856 | for (int i=0; i<predicted.size(); i++) { |
| @@ -876,6 +876,7 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -876,6 +876,7 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 876 | if (normalizationIndexA >= truthPoints.size()) qFatal("Normalization index A is out of range."); | 876 | if (normalizationIndexA >= truthPoints.size()) qFatal("Normalization index A is out of range."); |
| 877 | if (normalizationIndexB >= truthPoints.size()) qFatal("Normalization index B is out of range."); | 877 | if (normalizationIndexB >= truthPoints.size()) qFatal("Normalization index B is out of range."); |
| 878 | const float normalizedLength = QtUtils::euclideanLength(truthPoints[normalizationIndexB] - truthPoints[normalizationIndexA]); | 878 | const float normalizedLength = QtUtils::euclideanLength(truthPoints[normalizationIndexB] - truthPoints[normalizationIndexA]); |
| 879 | + const float normalizedOrientation = QtUtils::orientation(truthPoints[normalizationIndexB], truthPoints[normalizationIndexA]); | ||
| 879 | 880 | ||
| 880 | if (predictedPoints.size() != truthPoints.size() || qIsNaN(normalizedLength)) { | 881 | if (predictedPoints.size() != truthPoints.size() || qIsNaN(normalizedLength)) { |
| 881 | predicted.removeAt(i); | 882 | predicted.removeAt(i); |
| @@ -886,8 +887,10 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -886,8 +887,10 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 886 | continue; | 887 | continue; |
| 887 | } | 888 | } |
| 888 | 889 | ||
| 889 | - while (pointErrors.size() < predictedPoints.size()) | ||
| 890 | - pointErrors.append(QList<float>()); | 890 | + while (pointErrorMagnitudes.size() < predictedPoints.size()) { |
| 891 | + pointErrorMagnitudes.append(QList<float>()); | ||
| 892 | + pointErrorOrientations.append(QList<float>()); | ||
| 893 | + } | ||
| 891 | 894 | ||
| 892 | // Want to know error for every image. | 895 | // Want to know error for every image. |
| 893 | normalizedLengths.append(normalizedLength); | 896 | normalizedLengths.append(normalizedLength); |
| @@ -897,7 +900,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -897,7 +900,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 897 | const float error = QtUtils::euclideanLength(predictedPoints[j] - truthPoints[j])/normalizedLength; | 900 | const float error = QtUtils::euclideanLength(predictedPoints[j] - truthPoints[j])/normalizedLength; |
| 898 | if (!qIsNaN(error)) { | 901 | if (!qIsNaN(error)) { |
| 899 | totalError += error; | 902 | totalError += error; |
| 900 | - pointErrors[j].append(error); | 903 | + pointErrorMagnitudes[j].append(error); |
| 904 | + pointErrorOrientations[j].append(QtUtils::orientation(predictedPoints[j], truthPoints[j]) - normalizedOrientation); | ||
| 901 | totalCount++; | 905 | totalCount++; |
| 902 | } | 906 | } |
| 903 | } | 907 | } |
| @@ -906,7 +910,47 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -906,7 +910,47 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 906 | 910 | ||
| 907 | qDebug() << "Skipped" << skipped << "files due to point size mismatch or NaN normalized length."; | 911 | qDebug() << "Skipped" << skipped << "files due to point size mismatch or NaN normalized length."; |
| 908 | 912 | ||
| 909 | - QList<float> averagePointErrors; averagePointErrors.reserve(pointErrors.size()); | 913 | + // Adjust the point error to not penalize for systematic biases... |
| 914 | + // ... by first calculating the average bias for each point | ||
| 915 | + QList<QPointF> averagePointBiases; | ||
| 916 | + for (int i=0; i<pointErrorMagnitudes.size(); i++) { | ||
| 917 | + const QList<float> &magnitudes = pointErrorMagnitudes[i]; | ||
| 918 | + const QList<float> &orientations = pointErrorOrientations[i]; | ||
| 919 | + QPointF cumulativePointBias; | ||
| 920 | + for (int j=0; j<magnitudes.size(); j++) { | ||
| 921 | + const float m = magnitudes[j]; | ||
| 922 | + const float o = orientations[j]; | ||
| 923 | + cumulativePointBias += QPointF(m*cos(o), m*sin(o)); | ||
| 924 | + } | ||
| 925 | + averagePointBiases.append(cumulativePointBias / magnitudes.size()); | ||
| 926 | + } | ||
| 927 | + | ||
| 928 | + // ... and then subtracting the average bias from each individual error. | ||
| 929 | + for (int i=0; i<pointErrorMagnitudes.size(); i++) { | ||
| 930 | + QList<float> &magnitudes = pointErrorMagnitudes[i]; | ||
| 931 | + QList<float> &orientations = pointErrorOrientations[i]; | ||
| 932 | + const QPointF &bias = averagePointBiases[i]; | ||
| 933 | + for (int j=0; j<magnitudes.size(); j++) { | ||
| 934 | + float &m = magnitudes[j]; | ||
| 935 | + float &o = orientations[j]; | ||
| 936 | + QPointF error(m*cos(o), m*sin(o)); | ||
| 937 | + error -= bias; | ||
| 938 | + // At this point if we added up all the `error` vectors for a | ||
| 939 | + // landmark they would sum to zero. Josh confirmed this when | ||
| 940 | + // implementing the bias normalization correction, but removed it | ||
| 941 | + // from the final implementation. | ||
| 942 | + | ||
| 943 | + // Update the error magnitude for reporting MAE | ||
| 944 | + // m = QtUtils::euclideanLength(error); | ||
| 945 | + | ||
| 946 | + // We don't need to update orientation because we don't use it | ||
| 947 | + // again, but we do so anyway in the interest of pedantic | ||
| 948 | + // correctness. | ||
| 949 | + o = QtUtils::orientation(QPointF(0.f,0.f), error); | ||
| 950 | + } | ||
| 951 | + } | ||
| 952 | + | ||
| 953 | + QList<float> averagePointErrors; averagePointErrors.reserve(pointErrorMagnitudes.size()); | ||
| 910 | 954 | ||
| 911 | QStringList lines; | 955 | QStringList lines; |
| 912 | lines.append("Plot,X,Y"); | 956 | lines.append("Plot,X,Y"); |
| @@ -948,8 +992,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | @@ -948,8 +992,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle | ||
| 948 | lines.append("EXP,"+filePath+":"+predicted[exampleIndices[i].second].file.name+","+QString::number(exampleIndices[i].first)); | 992 | lines.append("EXP,"+filePath+":"+predicted[exampleIndices[i].second].file.name+","+QString::number(exampleIndices[i].first)); |
| 949 | } | 993 | } |
| 950 | 994 | ||
| 951 | - for (int i=0; i<pointErrors.size(); i++) { | ||
| 952 | - QList<float> &pointError = pointErrors[i]; | 995 | + for (int i=0; i<pointErrorMagnitudes.size(); i++) { |
| 996 | + QList<float> &pointError = pointErrorMagnitudes[i]; | ||
| 953 | std::sort(pointError.begin(), pointError.end()); | 997 | std::sort(pointError.begin(), pointError.end()); |
| 954 | averagePointErrors.append(Common::Mean(pointError)); | 998 | averagePointErrors.append(Common::Mean(pointError)); |
| 955 | const int keep = qMin(Max_Points, pointError.size()); | 999 | const int keep = qMin(Max_Points, pointError.size()); |
openbr/core/qtutils.cpp
| @@ -462,6 +462,11 @@ float euclideanLength(const QPointF &point) | @@ -462,6 +462,11 @@ float euclideanLength(const QPointF &point) | ||
| 462 | return sqrt(pow(point.x(), 2) + pow(point.y(), 2)); | 462 | return sqrt(pow(point.x(), 2) + pow(point.y(), 2)); |
| 463 | } | 463 | } |
| 464 | 464 | ||
| 465 | +float orientation(const QPointF &pointA, const QPointF &pointB) | ||
| 466 | +{ | ||
| 467 | + return atan2(pointB.y() - pointA.y(), pointB.x() - pointA.x()); | ||
| 468 | +} | ||
| 469 | + | ||
| 465 | float overlap(const QRectF &r, const QRectF &s) { | 470 | float overlap(const QRectF &r, const QRectF &s) { |
| 466 | QRectF intersection = r & s; | 471 | QRectF intersection = r & s; |
| 467 | 472 |
openbr/core/qtutils.h
| @@ -90,6 +90,7 @@ namespace QtUtils | @@ -90,6 +90,7 @@ namespace QtUtils | ||
| 90 | 90 | ||
| 91 | /**** Point Utilities ****/ | 91 | /**** Point Utilities ****/ |
| 92 | float euclideanLength(const QPointF &point); | 92 | float euclideanLength(const QPointF &point); |
| 93 | + float orientation(const QPointF &pointA, const QPointF &pointB); | ||
| 93 | 94 | ||
| 94 | /**** Rect Utilities ****/ | 95 | /**** Rect Utilities ****/ |
| 95 | float overlap(const QRectF &r, const QRectF &s); | 96 | float overlap(const QRectF &r, const QRectF &s); |