File indexing completed on 2025-01-19 03:57:55

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-09-03
0007  * Description : Face detection benchmarker
0008  *
0009  * SPDX-FileCopyrightText: 2010-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2012-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "detectionbenchmarker.h"
0017 
0018 // Local includes
0019 
0020 #include "digikam_debug.h"
0021 #include "tagscache.h"
0022 
0023 namespace Digikam
0024 {
0025 
0026 DetectionBenchmarker::DetectionBenchmarker(FacePipeline::Private* const d)
0027     : totalImages           (0),
0028       faces                 (0),
0029       totalPixels           (0),
0030       facePixels            (0),
0031       trueNegativeImages    (0),
0032       falsePositiveImages   (0),
0033       truePositiveFaces     (0),
0034       falseNegativeFaces    (0),
0035       falsePositiveFaces    (0),
0036       d                     (d)
0037 {
0038 }
0039 
0040 void DetectionBenchmarker::process(const FacePipelineExtendedPackage::Ptr& package)
0041 {
0042     if (package->databaseFaces.isEmpty())
0043     {
0044         // Detection / Recognition
0045 
0046         qCDebug(DIGIKAM_GENERAL_LOG) << "Benchmarking image" << package->info.name();
0047 
0048         FaceUtils utils;
0049         QList<FaceTagsIface> groundTruth = utils.databaseFaces(package->info.id());
0050 
0051         QList<FaceTagsIface> testedFaces = utils.toFaceTagsIfaces(package->info.id(),
0052                                                                   package->detectedFaces,
0053                                                                   package->recognitionResults,
0054                                                                   package->image.originalSize());
0055 
0056         QList<FaceTagsIface> unmatchedTrueFaces   = groundTruth;
0057         QList<FaceTagsIface> unmatchedTestedFaces = testedFaces;
0058         QList<FaceTagsIface> matchedTrueFaces;
0059 
0060         int trueFaces                             = groundTruth.size();
0061         const double minOverlap                   = 0.75;
0062 
0063         qCDebug(DIGIKAM_GENERAL_LOG) << "There are" << trueFaces << "faces to be detected. The detector found" << testedFaces.size();
0064 
0065         ++totalImages;
0066         faces       += trueFaces;
0067         totalPixels += package->image.originalSize().width() * package->image.originalSize().height();
0068 
0069         Q_FOREACH (const FaceTagsIface& trueFace, groundTruth)
0070         {
0071             ++faces;
0072             QRect rect  = trueFace.region().toRect();
0073             facePixels += rect.width() * rect.height();
0074 
0075             Q_FOREACH (const FaceTagsIface& testedFace, testedFaces)
0076             {
0077                 if (trueFace.region().intersects(testedFace.region(), minOverlap))
0078                 {   // cppcheck-suppress useStlAlgorithm
0079                     matchedTrueFaces << trueFace;
0080                     unmatchedTrueFaces.removeOne(trueFace);
0081                     break;
0082                 }
0083             }
0084         }
0085 
0086         Q_FOREACH (const FaceTagsIface& testedFace, testedFaces)
0087         {
0088             Q_FOREACH (const FaceTagsIface& trueFace, groundTruth)
0089             {
0090                 if (trueFace.region().intersects(testedFace.region(), minOverlap))
0091                 {   // cppcheck-suppress useStlAlgorithm
0092                     unmatchedTestedFaces.removeOne(testedFace);
0093                     break;
0094                 }
0095             }
0096         }
0097 
0098         if (groundTruth.isEmpty())
0099         {
0100             if (testedFaces.isEmpty())
0101             {
0102                 ++trueNegativeImages;
0103             }
0104             else
0105             {
0106                 qCDebug(DIGIKAM_GENERAL_LOG) << "The image, truly without faces, is false-positive";
0107                 ++falsePositiveImages;
0108             }
0109         }
0110 
0111         truePositiveFaces  += matchedTrueFaces.size();
0112         falseNegativeFaces += unmatchedTrueFaces.size();
0113         falsePositiveFaces += unmatchedTestedFaces.size();
0114         qCDebug(DIGIKAM_GENERAL_LOG) << "Faces detected correctly:"
0115                                      << matchedTrueFaces.size()
0116                                      << ", faces missed:"
0117                                      << unmatchedTrueFaces.size()
0118                                      << ", faces falsely detected:"
0119                                      << unmatchedTestedFaces.size();
0120     }
0121 
0122     package->processFlags  |= FacePipelinePackage::WrittenToDatabase;
0123     Q_EMIT processed(package);
0124 }
0125 
0126 /**
0127  * NOTE: Bench performance code. No need i18n here
0128  */
0129 QString DetectionBenchmarker::result() const
0130 {
0131     qCDebug(DIGIKAM_GENERAL_LOG) << "Per-image:"
0132                                  << trueNegativeImages
0133                                  << falsePositiveFaces;
0134     qCDebug(DIGIKAM_GENERAL_LOG) << "Per-face:"
0135                                  << truePositiveFaces
0136                                  << falseNegativeFaces
0137                                  << falsePositiveFaces; // 26 7 1
0138 
0139     int negativeImages = trueNegativeImages + falsePositiveImages;
0140     int trueFaces      = truePositiveFaces  + falseNegativeFaces;
0141     QString specificityWarning, sensitivityWarning;
0142 
0143     if (negativeImages < (0.2 * totalImages))
0144     {
0145         specificityWarning = QString::fromUtf8("<p><b>Note:</b><br/> "
0146                                      "Only %1 of the %2 test images have <i>no</i> depicted faces. "
0147                                      "This means the result is cannot be representative; "
0148                                      "it can only be used to compare preselected collections, "
0149                                      "and the specificity and false-positive rate have little meaning. </p>")
0150                                      .arg(negativeImages).arg(totalImages);
0151         negativeImages     = qMax(negativeImages, 1);
0152     }
0153 
0154     if (trueFaces == 0)
0155     {
0156         sensitivityWarning = QString::fromUtf8("<p><b>Note:</b><br/> "
0157                                      "No picture in the test collection contained a face. "
0158                                      "This means that sensitivity and PPV have no meaning and will be zero. </p>");
0159         trueFaces          = 1;
0160     }
0161 
0162     // collection properties
0163     double pixelCoverage     = facePixels                  / totalPixels;
0164     // per-image
0165     double specificity       = double(trueNegativeImages)  / negativeImages;
0166     double falsePositiveRate = double(falsePositiveImages) / negativeImages;
0167     // per-face
0168     double sensitivity       = double(truePositiveFaces)   / trueFaces;
0169     double ppv               = double(truePositiveFaces)   / (truePositiveFaces + falsePositiveFaces);
0170 
0171     return QString::fromUtf8("<p>"
0172                              "<u>Collection Properties:</u><br/>"
0173                              "%1 Images <br/>"
0174                              "%2 Faces <br/>"
0175                              "%3% of pixels covered by faces <br/>"
0176                              "</p>"
0177                              "%8"
0178                              "%9"
0179                              "<p>"
0180                              "<u>Per-Image Performance:</u> <br/>"
0181                              "Specificity: %4% <br/>"
0182                              "False-Positive Rate: %5%"
0183                              "</p>"
0184                              "<p>"
0185                              "<u>Per-Face Performance:</u> <br/>"
0186                              "Sensitivity: %6% <br/>"
0187                              "Positive Predictive Value: %7% <br/>"
0188                              "</p>"
0189                              "<p>"
0190                              "In other words, if a face is detected as face, it will "
0191                              "with a probability of %7% truly be a face. "
0192                              "Of all true faces, %6% will be detected. "
0193                              "Given face with no images on it, the detector will with a probability "
0194                              "of %5% falsely find a face on it. "
0195                              "</p>")
0196                              .arg(totalImages).arg(faces).arg(pixelCoverage * 100, 0, 'f', 1)
0197                              .arg(specificity * 100, 0, 'f', 1).arg(falsePositiveRate * 100, 0, 'f', 1)
0198                              .arg(sensitivity * 100, 0, 'f', 1).arg(ppv * 100, 0, 'f', 1)
0199                              .arg(specificityWarning).arg(sensitivityWarning);
0200 }
0201 
0202 } // namespace Digikam
0203 
0204 #include "moc_detectionbenchmarker.cpp"