Commit bf17972881c559482e56cb89ffc898df48eaeaab
Merge pull request #283 from biometrics/evaluation_updates
Updates to eval_detection
Showing
5 changed files
with
110 additions
and
56 deletions
app/br/br.cpp
| ... | ... | @@ -160,8 +160,8 @@ public: |
| 160 | 160 | check((parc >= 2) && (parc <= 3), "Incorrect parameter count for 'evalClustering'."); |
| 161 | 161 | br_eval_clustering(parv[0], parv[1], parc == 3 ? parv[2] : ""); |
| 162 | 162 | } else if (!strcmp(fun, "evalDetection")) { |
| 163 | - check((parc >= 2) && (parc <= 5), "Incorrect parameter count for 'evalDetection'."); | |
| 164 | - br_eval_detection(parv[0], parv[1], parc >= 3 ? parv[2] : "", parc >= 4 ? atoi(parv[3]) : 0, parc == 5 ? atoi(parv[4]) : 0); | |
| 163 | + check((parc >= 2) && (parc <= 6), "Incorrect parameter count for 'evalDetection'."); | |
| 164 | + br_eval_detection(parv[0], parv[1], parc >= 3 ? parv[2] : "", parc >= 4 ? atoi(parv[3]) : 0, parc >= 5 ? atoi(parv[4]) : 0, parc == 6 ? atoi(parv[5]) : 0); | |
| 165 | 165 | } else if (!strcmp(fun, "evalLandmarking")) { |
| 166 | 166 | check((parc >= 2) && (parc <= 5), "Incorrect parameter count for 'evalLandmarking'."); |
| 167 | 167 | br_eval_landmarking(parv[0], parv[1], parc >= 3 ? parv[2] : "", parc >= 4 ? atoi(parv[3]) : 0, parc >= 5 ? atoi(parv[4]) : 1); | ... | ... |
openbr/core/eval.cpp
| ... | ... | @@ -666,10 +666,15 @@ struct Detection |
| 666 | 666 | { |
| 667 | 667 | QRectF boundingBox; |
| 668 | 668 | float confidence; |
| 669 | + // The ignore flag is useful when certain faces in an image should be ignored | |
| 670 | + // and should not effect detection performance. Predicted detections that overlap | |
| 671 | + // with an ignored truth detection will not count as a true positive, false positive, | |
| 672 | + // true negative, or false negative, it will simply be ignored. | |
| 673 | + bool ignore; | |
| 669 | 674 | |
| 670 | 675 | Detection() {} |
| 671 | - Detection(const QRectF &boundingBox_, float confidence_ = -1) | |
| 672 | - : boundingBox(boundingBox_), confidence(confidence_) {} | |
| 676 | + Detection(const QRectF &boundingBox_, float confidence_ = -1, bool ignore_ = false) | |
| 677 | + : boundingBox(boundingBox_), confidence(confidence_), ignore(ignore_) {} | |
| 673 | 678 | |
| 674 | 679 | float area() const { return boundingBox.width() * boundingBox.height(); } |
| 675 | 680 | float overlap(const Detection &other) const |
| ... | ... | @@ -679,6 +684,16 @@ struct Detection |
| 679 | 684 | } |
| 680 | 685 | }; |
| 681 | 686 | |
| 687 | +struct SortedDetection | |
| 688 | +{ | |
| 689 | + int truth_idx, predicted_idx; | |
| 690 | + float overlap; | |
| 691 | + SortedDetection() : truth_idx(-1), predicted_idx(-1), overlap(-1) {} | |
| 692 | + SortedDetection(int truth_idx_, int predicted_idx_, float overlap_) | |
| 693 | + : truth_idx(truth_idx_), predicted_idx(predicted_idx_), overlap(overlap_) {} | |
| 694 | + inline bool operator<(const SortedDetection &other) const { return overlap > other.overlap; } | |
| 695 | +}; | |
| 696 | + | |
| 682 | 697 | struct Detections |
| 683 | 698 | { |
| 684 | 699 | QList<Detection> predicted, truth; |
| ... | ... | @@ -704,6 +719,7 @@ static QStringList computeDetectionResults(const QList<ResolvedDetection> &detec |
| 704 | 719 | { |
| 705 | 720 | QList<DetectionOperatingPoint> points; |
| 706 | 721 | float TP = 0, FP = 0, prevFP = -1; |
| 722 | + | |
| 707 | 723 | for (int i=0; i<detections.size(); i++) { |
| 708 | 724 | const ResolvedDetection &detection = detections[i]; |
| 709 | 725 | if (discrete) { |
| ... | ... | @@ -716,6 +732,13 @@ static QStringList computeDetectionResults(const QList<ResolvedDetection> &detec |
| 716 | 732 | } |
| 717 | 733 | if ((i == detections.size()-1) || (detection.confidence > detections[i+1].confidence)) { |
| 718 | 734 | if (FP > prevFP || (i == detections.size()-1)) { |
| 735 | + if (prevFP / numImages < 0.1 && FP / numImages > 0.1 && discrete) { | |
| 736 | + qDebug("TAR @ FAR => %f : 0.1", TP / totalTrueDetections); | |
| 737 | + qDebug("Confidence: %f", detection.confidence); | |
| 738 | + } else if (prevFP / numImages < 0.01 && FP / numImages > 0.01 && discrete) { | |
| 739 | + qDebug("TAR @ FAR => %f : 0.01", TP / totalTrueDetections); | |
| 740 | + qDebug("Confidence: %f", detection.confidence); | |
| 741 | + } | |
| 719 | 742 | points.append(DetectionOperatingPoint(TP, FP, totalTrueDetections, numImages)); |
| 720 | 743 | prevFP = FP; |
| 721 | 744 | } |
| ... | ... | @@ -803,7 +826,7 @@ static QList<Detection> getDetections(const DetectionKey &key, const File &f, bo |
| 803 | 826 | dets.append(Detection(f.get<QRectF>(key), isTruth ? -1 : f.get<float>("Confidence", -1))); |
| 804 | 827 | } else if (key.type == DetectionKey::XYWidthHeight) { |
| 805 | 828 | const QRectF rect(f.get<float>(key+"_X"), f.get<float>(key+"_Y"), f.get<float>(key+"_Width"), f.get<float>(key+"_Height")); |
| 806 | - dets.append(Detection(rect, isTruth ? -1 : f.get<float>("Confidence", -1))); | |
| 829 | + dets.append(Detection(rect, isTruth ? -1 : f.get<float>("Confidence", -1), f.get<bool>("Ignore", false))); | |
| 807 | 830 | } |
| 808 | 831 | return dets; |
| 809 | 832 | } |
| ... | ... | @@ -827,21 +850,16 @@ static QMap<QString, Detections> getDetections(const File &predictedGallery, con |
| 827 | 850 | qPrintable(predictedDetectKey == truthDetectKey ? QString() : "/"+truthDetectKey)); |
| 828 | 851 | |
| 829 | 852 | QMap<QString, Detections> allDetections; |
| 830 | - foreach (const File &f, predicted) | |
| 831 | - allDetections[f.baseName()].predicted.append(getDetections(predictedDetectKey, f, false)); | |
| 832 | 853 | foreach (const File &f, truth) |
| 833 | - allDetections[f.baseName()].truth.append(getDetections(truthDetectKey, f, true)); | |
| 854 | + allDetections[f.name].truth.append(getDetections(truthDetectKey, f, true)); | |
| 855 | + foreach (const File &f, predicted) | |
| 856 | + if (allDetections.contains(f.name)) allDetections[f.name].predicted.append(getDetections(predictedDetectKey, f, false)); | |
| 834 | 857 | return allDetections; |
| 835 | 858 | } |
| 836 | 859 | |
| 837 | -static int getNumberOfImages(const File &truthGallery) | |
| 838 | -{ | |
| 839 | - const FileList files = FileList::fromGallery(truthGallery); | |
| 840 | - | |
| 841 | - QSet<QString> names; | |
| 842 | - foreach(const File &file, files) | |
| 843 | - names.insert(file.fileName()); | |
| 844 | - return names.size(); | |
| 860 | +static inline int getNumberOfImages(const QMap<QString, Detections> detections) | |
| 861 | +{ | |
| 862 | + return detections.keys().size(); | |
| 845 | 863 | } |
| 846 | 864 | |
| 847 | 865 | static int associateGroundTruthDetections(QList<ResolvedDetection> &resolved, QList<ResolvedDetection> &falseNegative, QMap<QString, Detections> &all, QRectF &offsets) |
| ... | ... | @@ -852,13 +870,13 @@ static int associateGroundTruthDetections(QList<ResolvedDetection> &resolved, QL |
| 852 | 870 | foreach (Detections detections, all.values()) { |
| 853 | 871 | totalTrueDetections += detections.truth.size(); |
| 854 | 872 | // Try to associate ground truth detections with predicted detections |
| 855 | - while (!detections.truth.isEmpty() && !detections.predicted.isEmpty()) { | |
| 856 | - const Detection truth = detections.truth.takeFirst(); // Take removes the detection | |
| 857 | - int bestIndex = -1; | |
| 858 | - float bestOverlap = -std::numeric_limits<float>::max(); | |
| 859 | - // Find the nearest predicted detection to this ground truth detection | |
| 860 | - for (int i=0; i<detections.predicted.size(); i++) { | |
| 861 | - Detection predicted = detections.predicted[i]; | |
| 873 | + | |
| 874 | + QList<SortedDetection> sortedDetections; sortedDetections.reserve(detections.truth.size() * detections.predicted.size()); | |
| 875 | + for (int t = 0; t < detections.truth.size(); t++) { | |
| 876 | + const Detection truth = detections.truth[t]; | |
| 877 | + for (int p = 0; p < detections.predicted.size(); p++) { | |
| 878 | + Detection predicted = detections.predicted[p]; | |
| 879 | + | |
| 862 | 880 | float predictedWidth = predicted.boundingBox.width(); |
| 863 | 881 | float x, y, width, height; |
| 864 | 882 | x = predicted.boundingBox.x() + offsets.x()*predictedWidth; |
| ... | ... | @@ -868,35 +886,44 @@ static int associateGroundTruthDetections(QList<ResolvedDetection> &resolved, QL |
| 868 | 886 | Detection newPredicted(QRectF(x, y, width, height), 0.0); |
| 869 | 887 | |
| 870 | 888 | const float overlap = truth.overlap(newPredicted); |
| 871 | - if (overlap > bestOverlap) { | |
| 872 | - bestOverlap = overlap; | |
| 873 | - bestIndex = i; | |
| 874 | - } | |
| 889 | + if (overlap > 0) | |
| 890 | + sortedDetections.append(SortedDetection(t, p, overlap)); | |
| 875 | 891 | } |
| 876 | - // Removing the detection prevents us from considering it twice. | |
| 877 | - // We don't want to associate two ground truth detections with the | |
| 878 | - // same prediction, over vice versa. | |
| 879 | - const Detection predicted = detections.predicted.takeAt(bestIndex); | |
| 880 | - resolved.append(ResolvedDetection(predicted.confidence, bestOverlap)); | |
| 881 | - | |
| 882 | - if (offsets.x() == 0) { | |
| 883 | - // Add side differences to total only for pairs that meet the overlap threshold. | |
| 884 | - if (bestOverlap > 0.3) { | |
| 885 | - count++; | |
| 886 | - float width = predicted.boundingBox.width(); | |
| 887 | - dLeftTotal += (truth.boundingBox.left() - predicted.boundingBox.left()) / width; | |
| 888 | - dRightTotal += (truth.boundingBox.right() - predicted.boundingBox.right()) / width; | |
| 889 | - dTopTotal += (truth.boundingBox.top() - predicted.boundingBox.top()) / width; | |
| 890 | - dBottomTotal += (truth.boundingBox.bottom() - predicted.boundingBox.bottom()) / width; | |
| 891 | - } | |
| 892 | + } | |
| 893 | + | |
| 894 | + std::sort(sortedDetections.begin(), sortedDetections.end()); | |
| 895 | + | |
| 896 | + QList<int> removedTruth; | |
| 897 | + QList<int> removedPredicted; | |
| 898 | + | |
| 899 | + foreach (const SortedDetection &detection, sortedDetections) { | |
| 900 | + if (removedTruth.contains(detection.truth_idx) || removedPredicted.contains(detection.predicted_idx)) | |
| 901 | + continue; | |
| 902 | + | |
| 903 | + const Detection truth = detections.truth[detection.truth_idx]; | |
| 904 | + const Detection predicted = detections.predicted[detection.predicted_idx]; | |
| 905 | + | |
| 906 | + if (!truth.ignore) resolved.append(ResolvedDetection(predicted.confidence, detection.overlap)); | |
| 907 | + | |
| 908 | + removedTruth.append(detection.truth_idx); | |
| 909 | + removedPredicted.append(detection.predicted_idx); | |
| 910 | + | |
| 911 | + if (offsets.x() == 0 && detection.overlap > 0.3) { | |
| 912 | + count++; | |
| 913 | + float width = predicted.boundingBox.width(); | |
| 914 | + dLeftTotal += (truth.boundingBox.left() - predicted.boundingBox.left()) / width; | |
| 915 | + dRightTotal += (truth.boundingBox.right() - predicted.boundingBox.right()) / width; | |
| 916 | + dTopTotal += (truth.boundingBox.top() - predicted.boundingBox.top()) / width; | |
| 917 | + dBottomTotal += (truth.boundingBox.bottom() - predicted.boundingBox.bottom()) / width; | |
| 892 | 918 | } |
| 893 | 919 | } |
| 894 | 920 | |
| 895 | - foreach (const Detection &detection, detections.predicted) | |
| 896 | - resolved.append(ResolvedDetection(detection.confidence, 0)); | |
| 897 | - for (int i=0; i<detections.truth.size(); i++) | |
| 898 | - falseNegative.append(ResolvedDetection(-std::numeric_limits<float>::max(), 0)); | |
| 921 | + for (int i = 0; i < detections.predicted.size(); i++) | |
| 922 | + if (!removedPredicted.contains(i)) resolved.append(ResolvedDetection(detections.predicted[i].confidence, 0)); | |
| 923 | + for (int i = 0; i < detections.truth.size(); i++) | |
| 924 | + if (!removedTruth.contains(i) && !detections.truth[i].ignore) falseNegative.append(ResolvedDetection(-std::numeric_limits<float>::max(), 0)); | |
| 899 | 925 | } |
| 926 | + | |
| 900 | 927 | if (offsets.x() == 0) { |
| 901 | 928 | // Calculate average differences in each direction |
| 902 | 929 | float dRight = dRightTotal / count; |
| ... | ... | @@ -914,7 +941,7 @@ static int associateGroundTruthDetections(QList<ResolvedDetection> &resolved, QL |
| 914 | 941 | return totalTrueDetections; |
| 915 | 942 | } |
| 916 | 943 | |
| 917 | -float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv, bool normalize, int minSize) | |
| 944 | +float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv, bool normalize, int minSize, int maxSize) | |
| 918 | 945 | { |
| 919 | 946 | qDebug("Evaluating detection of %s against %s", qPrintable(predictedGallery), qPrintable(truthGallery)); |
| 920 | 947 | // Organized by file, QMap used to preserve order |
| ... | ... | @@ -923,12 +950,13 @@ float EvalDetection(const QString &predictedGallery, const QString &truthGallery |
| 923 | 950 | // Remove any bounding boxes with a side smaller than minSize |
| 924 | 951 | if (minSize > 0) { |
| 925 | 952 | qDebug("Removing boxes smaller than %d\n", minSize); |
| 953 | + QMap<QString, Detections> allFilteredDetections; | |
| 926 | 954 | foreach (QString key, allDetections.keys()) { |
| 927 | 955 | Detections detections = allDetections[key]; |
| 928 | 956 | Detections filteredDetections; |
| 929 | 957 | for (int i = 0; i < detections.predicted.size(); i++) { |
| 930 | 958 | QRectF box = detections.predicted[i].boundingBox; |
| 931 | - if (min(box.width(), box.height()) > minSize) { | |
| 959 | + if (min(box.width(), box.height()) > sqrt(0.5 * pow(minSize, 2))) { | |
| 932 | 960 | filteredDetections.predicted.append(detections.predicted[i]); |
| 933 | 961 | } |
| 934 | 962 | } |
| ... | ... | @@ -939,8 +967,34 @@ float EvalDetection(const QString &predictedGallery, const QString &truthGallery |
| 939 | 967 | filteredDetections.truth.append(detections.truth[i]); |
| 940 | 968 | } |
| 941 | 969 | } |
| 942 | - allDetections.insert(key, filteredDetections); | |
| 970 | + if (!filteredDetections.truth.empty()) allFilteredDetections[key] = filteredDetections; | |
| 971 | + } | |
| 972 | + allDetections = allFilteredDetections; | |
| 973 | + } | |
| 974 | + | |
| 975 | + // Remove any bounding boxes with no side smaller than maxSize | |
| 976 | + if (maxSize > 0) { | |
| 977 | + qDebug("Removing boxes larger than %d\n", maxSize); | |
| 978 | + QMap<QString, Detections> allFilteredDetections; | |
| 979 | + foreach (QString key, allDetections.keys()) { | |
| 980 | + Detections detections = allDetections[key]; | |
| 981 | + Detections filteredDetections; | |
| 982 | + for (int i = 0; i < detections.predicted.size(); i++) { | |
| 983 | + QRectF box = detections.predicted[i].boundingBox; | |
| 984 | + if (min(box.width(), box.height()) < sqrt(0.5 * pow(maxSize, 2))) { | |
| 985 | + filteredDetections.predicted.append(detections.predicted[i]); | |
| 986 | + } | |
| 987 | + } | |
| 988 | + | |
| 989 | + for (int i = 0; i < detections.truth.size(); i++) { | |
| 990 | + QRectF box = detections.truth[i].boundingBox; | |
| 991 | + if (min(box.width(), box.height()) < maxSize) { | |
| 992 | + filteredDetections.truth.append(detections.truth[i]); | |
| 993 | + } | |
| 994 | + } | |
| 995 | + if (!filteredDetections.truth.empty()) allFilteredDetections[key] = filteredDetections; | |
| 943 | 996 | } |
| 997 | + allDetections = allFilteredDetections; | |
| 944 | 998 | } |
| 945 | 999 | |
| 946 | 1000 | QList<ResolvedDetection> resolvedDetections, falseNegativeDetections; |
| ... | ... | @@ -963,8 +1017,8 @@ float EvalDetection(const QString &predictedGallery, const QString &truthGallery |
| 963 | 1017 | std::sort(resolvedDetections.begin(), resolvedDetections.end()); |
| 964 | 1018 | QStringList lines; |
| 965 | 1019 | lines.append("Plot, X, Y"); |
| 966 | - lines.append(computeDetectionResults(resolvedDetections, totalTrueDetections, getNumberOfImages(truthGallery), true)); | |
| 967 | - lines.append(computeDetectionResults(resolvedDetections, totalTrueDetections, getNumberOfImages(truthGallery), false)); | |
| 1020 | + lines.append(computeDetectionResults(resolvedDetections, totalTrueDetections, getNumberOfImages(allDetections), true)); | |
| 1021 | + lines.append(computeDetectionResults(resolvedDetections, totalTrueDetections, getNumberOfImages(allDetections), false)); | |
| 968 | 1022 | |
| 969 | 1023 | float averageOverlap; |
| 970 | 1024 | { // Overlap Density | ... | ... |
openbr/core/eval.h
| ... | ... | @@ -30,7 +30,7 @@ namespace br |
| 30 | 30 | float InplaceEval(const QString & simmat, const QString & target, const QString & query, const QString & csv = ""); |
| 31 | 31 | |
| 32 | 32 | void EvalClassification(const QString &predictedGallery, const QString &truthGallery, QString predictedProperty = "", QString truthProperty = ""); |
| 33 | - float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv = "", bool normalize = false, int minSize = 0); // Return average overlap | |
| 33 | + float EvalDetection(const QString &predictedGallery, const QString &truthGallery, const QString &csv = "", bool normalize = false, int minSize = 0, int maxSize = 0); // Return average overlap | |
| 34 | 34 | float EvalLandmarking(const QString &predictedGallery, const QString &truthGallery, const QString &csv = "", int normalizationIndexA = 0, int normalizationIndexB = 1); // Return average error |
| 35 | 35 | void EvalRegression(const QString &predictedGallery, const QString &truthGallery, QString predictedProperty = "", QString truthProperty = ""); |
| 36 | 36 | } | ... | ... |
openbr/openbr.cpp
| ... | ... | @@ -129,9 +129,9 @@ void br_eval_clustering(const char *csv, const char *gallery, const char *truth_ |
| 129 | 129 | EvalClustering(csv, gallery, truth_property); |
| 130 | 130 | } |
| 131 | 131 | |
| 132 | -float br_eval_detection(const char *predicted_gallery, const char *truth_gallery, const char *csv, bool normalize, int minSize) | |
| 132 | +float br_eval_detection(const char *predicted_gallery, const char *truth_gallery, const char *csv, bool normalize, int minSize, int maxSize) | |
| 133 | 133 | { |
| 134 | - return EvalDetection(predicted_gallery, truth_gallery, csv, normalize, minSize); | |
| 134 | + return EvalDetection(predicted_gallery, truth_gallery, csv, normalize, minSize, maxSize); | |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 137 | float br_eval_landmarking(const char *predicted_gallery, const char *truth_gallery, const char *csv, int normalization_index_a, int normalization_index_b) | ... | ... |
openbr/openbr.h
| ... | ... | @@ -205,7 +205,7 @@ BR_EXPORT void br_eval_clustering(const char *csv, const char *gallery, const ch |
| 205 | 205 | * \param normalize Optional \c bool flag to normalize predicted bounding boxes for improved detection. |
| 206 | 206 | * \return Average detection bounding box overlap. |
| 207 | 207 | */ |
| 208 | -BR_EXPORT float br_eval_detection(const char *predicted_gallery, const char *truth_gallery, const char *csv = "", bool normalize = false, int minSize = 0); | |
| 208 | +BR_EXPORT float br_eval_detection(const char *predicted_gallery, const char *truth_gallery, const char *csv = "", bool normalize = false, int minSize = 0, int maxSize = 0); | |
| 209 | 209 | |
| 210 | 210 | /*! |
| 211 | 211 | * \brief Evaluates and prints landmarking accuracy to terminal. | ... | ... |