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"