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 | 850 | QStringList truthNames = File::get<QString>(truth, "name"); |
| 851 | 851 | |
| 852 | 852 | int skipped = 0; |
| 853 | - QList< QList<float> > pointErrors; | |
| 853 | + QList< QList<float> > pointErrorMagnitudes, pointErrorOrientations; | |
| 854 | 854 | QList<float> imageErrors; |
| 855 | 855 | QList<float> normalizedLengths; |
| 856 | 856 | for (int i=0; i<predicted.size(); i++) { |
| ... | ... | @@ -876,6 +876,7 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle |
| 876 | 876 | if (normalizationIndexA >= truthPoints.size()) qFatal("Normalization index A is out of range."); |
| 877 | 877 | if (normalizationIndexB >= truthPoints.size()) qFatal("Normalization index B is out of range."); |
| 878 | 878 | const float normalizedLength = QtUtils::euclideanLength(truthPoints[normalizationIndexB] - truthPoints[normalizationIndexA]); |
| 879 | + const float normalizedOrientation = QtUtils::orientation(truthPoints[normalizationIndexB], truthPoints[normalizationIndexA]); | |
| 879 | 880 | |
| 880 | 881 | if (predictedPoints.size() != truthPoints.size() || qIsNaN(normalizedLength)) { |
| 881 | 882 | predicted.removeAt(i); |
| ... | ... | @@ -886,8 +887,10 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle |
| 886 | 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 | 895 | // Want to know error for every image. |
| 893 | 896 | normalizedLengths.append(normalizedLength); |
| ... | ... | @@ -897,7 +900,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle |
| 897 | 900 | const float error = QtUtils::euclideanLength(predictedPoints[j] - truthPoints[j])/normalizedLength; |
| 898 | 901 | if (!qIsNaN(error)) { |
| 899 | 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 | 905 | totalCount++; |
| 902 | 906 | } |
| 903 | 907 | } |
| ... | ... | @@ -906,7 +910,47 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle |
| 906 | 910 | |
| 907 | 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 | 955 | QStringList lines; |
| 912 | 956 | lines.append("Plot,X,Y"); |
| ... | ... | @@ -948,8 +992,8 @@ float EvalLandmarking(const QString &predictedGallery, const QString &truthGalle |
| 948 | 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 | 997 | std::sort(pointError.begin(), pointError.end()); |
| 954 | 998 | averagePointErrors.append(Common::Mean(pointError)); |
| 955 | 999 | const int keep = qMin(Max_Points, pointError.size()); | ... | ... |
openbr/core/qtutils.cpp
| ... | ... | @@ -462,6 +462,11 @@ float euclideanLength(const QPointF &point) |
| 462 | 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 | 470 | float overlap(const QRectF &r, const QRectF &s) { |
| 466 | 471 | QRectF intersection = r & s; |
| 467 | 472 | ... | ... |
openbr/core/qtutils.h
| ... | ... | @@ -90,6 +90,7 @@ namespace QtUtils |
| 90 | 90 | |
| 91 | 91 | /**** Point Utilities ****/ |
| 92 | 92 | float euclideanLength(const QPointF &point); |
| 93 | + float orientation(const QPointF &pointA, const QPointF &pointB); | |
| 93 | 94 | |
| 94 | 95 | /**** Rect Utilities ****/ |
| 95 | 96 | float overlap(const QRectF &r, const QRectF &s); | ... | ... |