File indexing completed on 2025-01-19 03:59:30

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2010-07-18
0007  * Description : batch face detection
0008  *
0009  * SPDX-FileCopyrightText: 2010      by Aditya Bhatt <adityabhatt1991 at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2012      by Andi Clemens <andi dot clemens at gmail dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "facesdetector.h"
0018 
0019 // Qt includes
0020 
0021 #include <QClipboard>
0022 #include <QVBoxLayout>
0023 #include <QTimer>
0024 #include <QIcon>
0025 #include <QPushButton>
0026 #include <QApplication>
0027 #include <QTextEdit>
0028 #include <QHash>
0029 
0030 // KDE includes
0031 
0032 #include <kconfiggroup.h>
0033 #include <klocalizedstring.h>
0034 #include <ksharedconfig.h>
0035 
0036 // Local includes
0037 
0038 #include "facialrecognition_wrapper.h"
0039 #include "digikam_debug.h"
0040 #include "coredb.h"
0041 #include "album.h"
0042 #include "albummanager.h"
0043 #include "albumpointer.h"
0044 #include "facepipeline.h"
0045 #include "facescansettings.h"
0046 #include "iteminfojob.h"
0047 #include "facetags.h"
0048 
0049 namespace Digikam
0050 {
0051 
0052 class Q_DECL_HIDDEN BenchmarkMessageDisplay : public QWidget
0053 {
0054     Q_OBJECT
0055 
0056 public:
0057 
0058     explicit BenchmarkMessageDisplay(const QString& richText)
0059         : QWidget(nullptr)
0060     {
0061         setAttribute(Qt::WA_DeleteOnClose);
0062 
0063         QVBoxLayout* const vbox     = new QVBoxLayout;
0064         QTextEdit* const edit       = new QTextEdit;
0065         vbox->addWidget(edit, 1);
0066         QPushButton* const okButton = new QPushButton(i18n("OK"));
0067         vbox->addWidget(okButton, 0, Qt::AlignRight);
0068 
0069         setLayout(vbox);
0070 
0071         connect(okButton, SIGNAL(clicked()),
0072                 this, SLOT(close()));
0073 
0074         edit->setHtml(richText);
0075         QApplication::clipboard()->setText(edit->toPlainText());
0076 
0077         resize(500, 400);
0078         show();
0079         raise();
0080     }
0081 
0082 private:
0083 
0084     // Disable
0085     BenchmarkMessageDisplay(QWidget*);
0086 };
0087 
0088 // --------------------------------------------------------------------------
0089 
0090 class Q_DECL_HIDDEN FacesDetector::Private
0091 {
0092 public:
0093 
0094     explicit Private()
0095       : source   (FacesDetector::Albums),
0096         benchmark(false)
0097     {
0098     }
0099 
0100     FacesDetector::InputSource source;
0101     bool                       benchmark;
0102 
0103     AlbumPointerList<>         albumTodoList;
0104     ItemInfoList               infoTodoList;
0105     QList<qlonglong>           idsTodoList;
0106 
0107     ItemInfoJob                albumListing;
0108     FacePipeline               pipeline;
0109 };
0110 
0111 FacesDetector::FacesDetector(const FaceScanSettings& settings, ProgressItem* const parent)
0112     : MaintenanceTool(QLatin1String("FacesDetector"), parent),
0113       d              (new Private)
0114 {
0115     if      (settings.task == FaceScanSettings::RetrainAll)
0116     {
0117         // clear all training data in the database
0118         FacialRecognitionWrapper().clearAllTraining(QLatin1String("digikam"));
0119         d->pipeline.plugRetrainingDatabaseFilter();
0120         d->pipeline.plugTrainer();
0121         d->pipeline.construct();
0122     }
0123     else if (settings.task == FaceScanSettings::BenchmarkDetection)
0124     {
0125         d->benchmark = true;
0126         d->pipeline.plugDatabaseFilter(FacePipeline::ScanAll);
0127         d->pipeline.plugFacePreviewLoader();
0128 
0129         if (settings.useFullCpu)
0130         {
0131             d->pipeline.plugParallelFaceDetectors();
0132         }
0133         else
0134         {
0135             d->pipeline.plugFaceDetector();
0136         }
0137 
0138         d->pipeline.plugDetectionBenchmarker();
0139         d->pipeline.construct();
0140     }
0141     else if (settings.task == FaceScanSettings::BenchmarkRecognition)
0142     {
0143         d->benchmark = true;
0144         d->pipeline.plugRetrainingDatabaseFilter();
0145         d->pipeline.plugFaceRecognizer();
0146         d->pipeline.plugRecognitionBenchmarker();
0147         d->pipeline.construct();
0148     }
0149     else if ((settings.task == FaceScanSettings::DetectAndRecognize) ||
0150              (settings.task == FaceScanSettings::Detect))
0151     {
0152         FacePipeline::FilterMode filterMode;
0153         FacePipeline::WriteMode  writeMode;
0154 
0155         if      (settings.alreadyScannedHandling == FaceScanSettings::Skip)
0156         {
0157             filterMode = FacePipeline::SkipAlreadyScanned;
0158             writeMode  = FacePipeline::NormalWrite;
0159         }
0160         else if (settings.alreadyScannedHandling == FaceScanSettings::Rescan)
0161         {
0162             filterMode = FacePipeline::ScanAll;
0163             writeMode  = FacePipeline::OverwriteUnconfirmed;
0164         }
0165         else if (settings.alreadyScannedHandling == FaceScanSettings::ClearAll)
0166         {
0167             filterMode = FacePipeline::ScanAll;
0168             writeMode  = FacePipeline::OverwriteAllFaces;
0169         }
0170         else // FaceScanSettings::Merge
0171         {
0172             filterMode = FacePipeline::ScanAll;
0173             writeMode  = FacePipeline::NormalWrite;
0174         }
0175 
0176         d->pipeline.plugDatabaseFilter(filterMode);
0177         d->pipeline.plugFacePreviewLoader();
0178 
0179         if (settings.useFullCpu)
0180         {
0181             d->pipeline.plugParallelFaceDetectors();
0182         }
0183         else
0184         {
0185             d->pipeline.plugFaceDetector();
0186         }
0187 
0188         if (settings.task == FaceScanSettings::DetectAndRecognize)
0189         {
0190             //d->pipeline.plugRerecognizingDatabaseFilter();
0191             d->pipeline.plugFaceRecognizer();
0192         }
0193 
0194         d->pipeline.plugDatabaseWriter(writeMode);
0195         d->pipeline.setAccuracyAndModel(settings.accuracy,
0196                                         settings.useYoloV3);
0197         d->pipeline.construct();
0198     }
0199     else // FaceScanSettings::RecognizeMarkedFaces
0200     {
0201         d->pipeline.plugRerecognizingDatabaseFilter();
0202         d->pipeline.plugFaceRecognizer();
0203         d->pipeline.plugDatabaseWriter(FacePipeline::NormalWrite);
0204         d->pipeline.setAccuracyAndModel(settings.accuracy,
0205                                         settings.useYoloV3);
0206         d->pipeline.construct();
0207     }
0208 
0209     connect(&d->albumListing, SIGNAL(signalItemsInfo(ItemInfoList)),
0210             this, SLOT(slotItemsInfo(ItemInfoList)));
0211 
0212     connect(&d->albumListing, SIGNAL(signalCompleted()),
0213             this, SLOT(slotContinueAlbumListing()));
0214 
0215     connect(&d->pipeline, SIGNAL(finished()),
0216             this, SLOT(slotContinueAlbumListing()));
0217 
0218     connect(&d->pipeline, SIGNAL(processed(FacePipelinePackage)),
0219             this, SLOT(slotShowOneDetected(FacePipelinePackage)));
0220 
0221     connect(&d->pipeline, SIGNAL(skipped(QList<ItemInfo>)),
0222             this, SLOT(slotImagesSkipped(QList<ItemInfo>)));
0223 
0224     connect(this, SIGNAL(progressItemCanceled(ProgressItem*)),
0225             this, SLOT(slotCancel()));
0226 
0227     if      (settings.wholeAlbums &&
0228              (settings.task == FaceScanSettings::RecognizeMarkedFaces))
0229     {
0230         d->idsTodoList = CoreDbAccess().db()->
0231             getImagesWithImageTagProperty(FaceTags::unknownPersonTagId(),
0232                                           ImageTagPropertyName::autodetectedFace());
0233 
0234         d->source = FacesDetector::Ids;
0235     }
0236     else if (settings.task == FaceScanSettings::RetrainAll)
0237     {
0238         d->idsTodoList = CoreDbAccess().db()->
0239             getImagesWithProperty(ImageTagPropertyName::tagRegion());
0240 
0241         d->source = FacesDetector::Ids;
0242     }
0243     else if (settings.albums.isEmpty() && settings.infos.isEmpty())
0244     {
0245         d->albumTodoList = AlbumManager::instance()->allPAlbums();
0246         d->source        = FacesDetector::Albums;
0247     }
0248     else if (!settings.albums.isEmpty())
0249     {
0250         d->albumTodoList = settings.albums;
0251         d->source        = FacesDetector::Albums;
0252     }
0253     else
0254     {
0255         d->infoTodoList = settings.infos;
0256         d->source       = FacesDetector::Infos;
0257     }
0258 }
0259 
0260 FacesDetector::~FacesDetector()
0261 {
0262     delete d;
0263 }
0264 
0265 void FacesDetector::slotStart()
0266 {
0267     MaintenanceTool::slotStart();
0268 
0269     setThumbnail(QIcon::fromTheme(QLatin1String("edit-image-face-show")).pixmap(22));
0270 
0271     // set label depending on settings
0272 
0273     if (d->albumTodoList.size() > 0)
0274     {
0275         if (d->albumTodoList.size() == 1)
0276         {
0277             setLabel(i18n("Scan for faces in album: %1", d->albumTodoList.first()->title()));
0278         }
0279         else
0280         {
0281             setLabel(i18n("Scan for faces in %1 albums", d->albumTodoList.size()));
0282         }
0283     }
0284     else if (d->infoTodoList.size() > 0)
0285     {
0286         if (d->infoTodoList.size() == 1)
0287         {
0288             setLabel(i18n("Scan for faces in image: %1", d->infoTodoList.first().name()));
0289         }
0290         else
0291         {
0292             setLabel(i18n("Scan for faces in %1 images", d->infoTodoList.size()));
0293         }
0294     }
0295     else
0296     {
0297         setLabel(i18n("Updating faces database"));
0298     }
0299 
0300     ProgressManager::addProgressItem(this);
0301 
0302     if      (d->source == FacesDetector::Infos)
0303     {
0304         int total = d->infoTodoList.count();
0305         qCDebug(DIGIKAM_GENERAL_LOG) << "Total is" << total;
0306 
0307         setTotalItems(total);
0308 
0309         if (d->infoTodoList.isEmpty())
0310         {
0311             slotDone();
0312             return;
0313         }
0314 
0315         slotItemsInfo(d->infoTodoList);
0316         return;
0317     }
0318     else if (d->source == FacesDetector::Ids)
0319     {
0320         ItemInfoList itemInfos(d->idsTodoList);
0321 
0322         int total = itemInfos.count();
0323         qCDebug(DIGIKAM_GENERAL_LOG) << "Total is" << total;
0324 
0325         setTotalItems(total);
0326 
0327         if (itemInfos.isEmpty())
0328         {
0329             slotDone();
0330             return;
0331         }
0332 
0333         slotItemsInfo(itemInfos);
0334         return;
0335     }
0336 
0337     setUsesBusyIndicator(true);
0338 
0339     // get total count, cached by AlbumManager
0340 
0341     QHash<int, int> palbumCounts;
0342     QHash<int, int> talbumCounts;
0343     bool hasPAlbums = false;
0344     bool hasTAlbums = false;
0345 
0346     Q_FOREACH (Album* const album, d->albumTodoList)
0347     {
0348         if (album->type() == Album::PHYSICAL)
0349         {
0350             hasPAlbums = true;
0351         }
0352         else
0353         {
0354             hasTAlbums = true;
0355         }
0356     }
0357 
0358     palbumCounts = AlbumManager::instance()->getPAlbumsCount();
0359     talbumCounts = AlbumManager::instance()->getTAlbumsCount();
0360 
0361     if (palbumCounts.isEmpty() && hasPAlbums)
0362     {
0363         QApplication::setOverrideCursor(Qt::WaitCursor);
0364         palbumCounts = CoreDbAccess().db()->getNumberOfImagesInAlbums();
0365         QApplication::restoreOverrideCursor();
0366     }
0367 
0368     if (talbumCounts.isEmpty() && hasTAlbums)
0369     {
0370         QApplication::setOverrideCursor(Qt::WaitCursor);
0371         talbumCounts = CoreDbAccess().db()->getNumberOfImagesInTags();
0372         QApplication::restoreOverrideCursor();
0373     }
0374 
0375     // first, we use the progressValueMap map to store absolute counts
0376 
0377     QHash<Album*, int> progressValueMap;
0378 
0379     Q_FOREACH (Album* const album, d->albumTodoList)
0380     {
0381         if (album->type() == Album::PHYSICAL)
0382         {
0383             progressValueMap[album] = palbumCounts.value(album->id());
0384         }
0385         else
0386         {
0387             // this is possibly broken of course because we do not know if images have multiple tags,
0388             // but there's no better solution without expensive operation
0389 
0390             progressValueMap[album] = talbumCounts.value(album->id());
0391         }
0392     }
0393 
0394     // second, calculate (approximate) overall sum
0395 
0396     int total = 0;
0397 
0398     Q_FOREACH (int count, progressValueMap)
0399     {
0400         // cppcheck-suppress useStlAlgorithm
0401         total += count;
0402     }
0403 
0404     total = qMax(1, total);
0405     qCDebug(DIGIKAM_GENERAL_LOG) << "Total is" << total;
0406 
0407     setUsesBusyIndicator(false);
0408     setTotalItems(total);
0409 
0410     slotContinueAlbumListing();
0411 }
0412 
0413 void FacesDetector::slotContinueAlbumListing()
0414 {
0415     if (d->source != FacesDetector::Albums)
0416     {
0417         slotDone();
0418         return;
0419     }
0420 
0421     qCDebug(DIGIKAM_GENERAL_LOG) << d->albumListing.isRunning() << !d->pipeline.hasFinished();
0422 
0423     // we get here by the finished signal from both, and want both to have finished to continue
0424 
0425     if (d->albumListing.isRunning() || !d->pipeline.hasFinished())
0426     {
0427         return;
0428     }
0429 
0430     // list can have null pointer if album was deleted recently
0431 
0432     Album* album = nullptr;
0433 
0434     do
0435     {
0436         if (d->albumTodoList.isEmpty())
0437         {
0438             slotDone();
0439             return;
0440         }
0441 
0442         album = d->albumTodoList.takeFirst();
0443     }
0444     while (!album);
0445 
0446     d->albumListing.allItemsFromAlbum(album);
0447 }
0448 
0449 void FacesDetector::slotItemsInfo(const ItemInfoList& items)
0450 {
0451     d->pipeline.process(items);
0452 }
0453 
0454 void FacesDetector::slotDone()
0455 {
0456     if (d->benchmark)
0457     {
0458         new BenchmarkMessageDisplay(d->pipeline.benchmarkResult());
0459     }
0460 
0461     // Switch on scanned for faces flag on digiKam config file.
0462 
0463     KSharedConfig::openConfig()->group(QLatin1String("General Settings"))
0464                                        .writeEntry("Face Scanner First Run", true);
0465 
0466     MaintenanceTool::slotDone();
0467 }
0468 
0469 void FacesDetector::slotCancel()
0470 {
0471     d->pipeline.shutDown();
0472     MaintenanceTool::slotCancel();
0473 }
0474 
0475 void FacesDetector::slotImagesSkipped(const QList<ItemInfo>& infos)
0476 {
0477     advance(infos.size());
0478 }
0479 
0480 void FacesDetector::slotShowOneDetected(const FacePipelinePackage& /*package*/)
0481 {
0482     advance(1);
0483 }
0484 
0485 } // namespace Digikam
0486 
0487 #include "facesdetector.moc"
0488 
0489 #include "moc_facesdetector.cpp"