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-08-08
0007  * Description : FaceEngine database interface allowing easy manipulation of face tags
0008  *
0009  * SPDX-FileCopyrightText: 2010-2011 by Aditya Bhatt <adityabhatt1991 at gmail dot com>
0010  * SPDX-FileCopyrightText: 2010-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0011  * SPDX-FileCopyrightText: 2012-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "faceutils.h"
0018 
0019 // Qt includes
0020 
0021 #include <QImage>
0022 
0023 // Local includes
0024 
0025 #include "digikam_debug.h"
0026 #include "coredbaccess.h"
0027 #include "coredbconstants.h"
0028 #include "coredboperationgroup.h"
0029 #include "coredb.h"
0030 #include "dimg.h"
0031 #include "facetags.h"
0032 #include "itemtagpair.h"
0033 #include "tagproperties.h"
0034 #include "tagscache.h"
0035 #include "tagregion.h"
0036 #include "thumbnailloadthread.h"
0037 #include "albummanager.h"
0038 #include "scancontroller.h"
0039 #include "metadatahub.h"
0040 
0041 namespace Digikam
0042 {
0043 
0044 // --- Constructor / Destructor -------------------------------------------------------------------------------------
0045 
0046 FaceUtils::FaceUtils(QObject* const parent)
0047     : QObject(parent)
0048 {
0049 }
0050 
0051 FaceUtils::~FaceUtils()
0052 {
0053 }
0054 
0055 // --- Mark for scanning and training -------------------------------------------------------------------------------
0056 
0057 bool FaceUtils::hasBeenScanned(qlonglong imageid) const
0058 {
0059     return hasBeenScanned(ItemInfo(imageid));
0060 }
0061 
0062 bool FaceUtils::hasBeenScanned(const ItemInfo& info) const
0063 {
0064     return info.tagIds().contains(FaceTags::scannedForFacesTagId());
0065 }
0066 
0067 void FaceUtils::markAsScanned(qlonglong imageid, bool hasBeenScanned) const
0068 {
0069     markAsScanned(ItemInfo(imageid), hasBeenScanned);
0070 }
0071 
0072 void FaceUtils::markAsScanned(const ItemInfo& info, bool hasBeenScanned) const
0073 {
0074     if (hasBeenScanned)
0075     {
0076         ItemInfo(info).setTag(FaceTags::scannedForFacesTagId());
0077     }
0078     else
0079     {
0080         ItemInfo(info).removeTag(FaceTags::scannedForFacesTagId());
0081     }
0082 }
0083 
0084 // --- Convert between FacesEngine results and FaceTagsIface ---
0085 
0086 QList<FaceTagsIface> FaceUtils::toFaceTagsIfaces(qlonglong imageid,
0087                                                  const QList<QRectF>& detectedFaces,
0088                                                  const QList<Identity>& recognitionResults,
0089                                                  const QSize& fullSize) const
0090 {
0091     QList<FaceTagsIface> faces;
0092 
0093     for (int i = 0 ; i < detectedFaces.size() ; ++i)
0094     {
0095         Identity identity;
0096 
0097         if (!recognitionResults.isEmpty())
0098         {
0099             identity = recognitionResults[i];
0100         }
0101 
0102         // We'll get the unknownPersonTagId if the identity is null
0103 
0104         int tagId                = FaceTags::getOrCreateTagForIdentity(identity.attributesMap());
0105         QRect fullSizeRect       = TagRegion::relativeToAbsolute(detectedFaces[i], fullSize);
0106         FaceTagsIface::Type type = identity.isNull() ? FaceTagsIface::UnknownName : FaceTagsIface::UnconfirmedName;
0107 
0108         if (!tagId || !fullSizeRect.isValid())
0109         {
0110             faces << FaceTagsIface();
0111             continue;
0112         }
0113 /*
0114         qCDebug(DIGIKAM_GENERAL_LOG) << "New Entry" << fullSizeRect << tagId;
0115 */
0116         faces << FaceTagsIface(type, imageid, tagId, TagRegion(fullSizeRect));
0117     }
0118 
0119     return faces;
0120 }
0121 
0122 // --- Images in faces and thumbnails ---
0123 
0124 void FaceUtils::storeThumbnails(ThumbnailLoadThread* const thread,
0125                                 const QString& filePath,
0126                                 const QList<FaceTagsIface>& databaseFaces,
0127                                 const DImg& image)
0128 {
0129     Q_FOREACH (const FaceTagsIface& face, databaseFaces)
0130     {
0131         QList<QRect> rects;
0132         QRect orgRect = face.region().toRect();
0133         rects << orgRect;
0134         rects << faceRectToDisplayRect(orgRect);
0135 
0136         Q_FOREACH (const QRect& rect, rects)
0137         {
0138             QRect mapped  = TagRegion::mapFromOriginalSize(image, rect);
0139             QImage detail = image.copyQImage(mapped);
0140             thread->storeDetailThumbnail(filePath, rect, detail, true);
0141         }
0142     }
0143 }
0144 
0145 // --- Face detection: merging results ------------------------------------------------------------------------------------
0146 
0147 QList<FaceTagsIface> FaceUtils::writeUnconfirmedResults(qlonglong imageid,
0148                                                         const QList<QRectF>& detectedFaces,
0149                                                         const QList<Identity>& recognitionResults,
0150                                                         const QSize& fullSize)
0151 {
0152     // Build list of new entries
0153 
0154     QList<FaceTagsIface> newFaces = toFaceTagsIfaces(imageid, detectedFaces, recognitionResults, fullSize);
0155 
0156     if (newFaces.isEmpty())
0157     {
0158         return newFaces;
0159     }
0160 
0161     // list of existing entries
0162 
0163     QList<FaceTagsIface> currentFaces = databaseFaces(imageid);
0164 
0165     // merge new with existing entries
0166 
0167     for (int i = 0 ; i < newFaces.size() ; ++i)
0168     {
0169         FaceTagsIface& newFace = newFaces[i];
0170         QList<FaceTagsIface> overlappingEntries;
0171 
0172         Q_FOREACH (const FaceTagsIface& oldFace, currentFaces)
0173         {
0174             double minOverlap = oldFace.isConfirmedName() ? 0.25 : 0.5;
0175 
0176             if (oldFace.region().intersects(newFace.region(), minOverlap))
0177             {
0178                 overlappingEntries << oldFace;
0179                 qCDebug(DIGIKAM_GENERAL_LOG) << "Entry" << oldFace.region() << oldFace.tagId()
0180                                              << "overlaps" << newFace.region() << newFace.tagId() << ", skipping";
0181             }
0182         }
0183 
0184         // The purpose if the next scope is to merge entries:
0185         // A confirmed face will never be overwritten.
0186         // If a name is set to an old face, it will only be replaced by a new face with a name.
0187 
0188         if (!overlappingEntries.isEmpty())
0189         {
0190             if (newFace.isUnknownName())
0191             {
0192                 // we have no name in the new face. Do we have one in the old faces?
0193 
0194                 for (int j = 0 ; j < overlappingEntries.size() ; ++j)
0195                 {
0196                     const FaceTagsIface& oldFace = overlappingEntries.at(j);
0197 
0198                     if (oldFace.isUnknownName())
0199                     {
0200                         // remove old face
0201                     }
0202                     else
0203                     {
0204                         // skip new entry if any overlapping face has a name, and we do not
0205 
0206                         newFace = FaceTagsIface();
0207                         break;
0208                     }
0209                 }
0210             }
0211             else
0212             {
0213                 // we have a name in the new face. Do we have names in overlapping faces?
0214 
0215                 for (int j = 0 ; j < overlappingEntries.size() ; ++j)
0216                 {
0217                     FaceTagsIface& oldFace = overlappingEntries[j];
0218 
0219                     if      (oldFace.isUnknownName())
0220                     {
0221                         // remove old face
0222                     }
0223                     else if (oldFace.isUnconfirmedName())
0224                     {
0225                         if (oldFace.tagId() == newFace.tagId())
0226                         {
0227                             // remove smaller face
0228 
0229                             if (oldFace.region().intersects(newFace.region(), 1))
0230                             {
0231                                 newFace = FaceTagsIface();
0232                                 break;
0233                             }
0234 
0235                             // else remove old face
0236                         }
0237                         else
0238                         {
0239                             // assume new recognition is more trained, remove older face
0240                         }
0241                     }
0242                     else if (oldFace.isConfirmedName())
0243                     {
0244                         // skip new entry, confirmed has of course priority
0245 
0246                         newFace = FaceTagsIface();
0247                     }
0248                 }
0249             }
0250         }
0251 
0252         // if we did not decide to skip this face, add is to the db now
0253 
0254         if (!newFace.isNull())
0255         {
0256             // list will contain all old entries that should still be removed
0257 
0258             removeFaces(overlappingEntries);
0259 
0260             ItemTagPair pair(imageid, newFace.tagId());
0261 
0262             // UnconfirmedName and UnknownName have the same attribute
0263 
0264             addFaceAndTag(pair, newFace, FaceTagsIface::attributesForFlags(FaceTagsIface::UnconfirmedName), false);
0265 
0266             // If the face is unconfirmed and the tag is not the unknown person tag, set the unconfirmed person property.
0267 
0268             if (newFace.isUnconfirmedType() && !FaceTags::isTheUnknownPerson(newFace.tagId()))
0269             {
0270                 ItemTagPair unconfirmedPair(imageid, FaceTags::unconfirmedPersonTagId());
0271                 unconfirmedPair.addProperty(ImageTagPropertyName::autodetectedPerson(),
0272                                             newFace.getAutodetectedPersonString());
0273             }
0274         }
0275     }
0276 
0277     return newFaces;
0278 }
0279 
0280 Identity FaceUtils::identityForTag(int tagId, FacialRecognitionWrapper& recognizer) const
0281 {
0282     QMultiMap<QString, QString> attributes = FaceTags::identityAttributes(tagId);
0283     Identity identity                      = recognizer.findIdentity(attributes);
0284 
0285     if (!identity.isNull())
0286     {
0287         qCDebug(DIGIKAM_GENERAL_LOG) << "Found FacesEngine identity" << identity.id() << "for tag" << tagId;
0288         return identity;
0289     }
0290 
0291     qCDebug(DIGIKAM_GENERAL_LOG) << "Adding new FacesEngine identity with attributes" << attributes;
0292     identity                               = recognizer.addIdentity(attributes);
0293 
0294     FaceTags::applyTagIdentityMapping(tagId, identity.attributesMap());
0295 
0296     return identity;
0297 }
0298 
0299 int FaceUtils::tagForIdentity(const Identity& identity) const
0300 {
0301     return FaceTags::getOrCreateTagForIdentity(identity.attributesMap());
0302 }
0303 
0304 // --- Editing normal tags, reimplemented with MetadataHub ---
0305 
0306 void FaceUtils::addNormalTag(qlonglong imageId, int tagId)
0307 {
0308     FaceTagsEditor::addNormalTag(imageId, tagId);
0309 
0310     ItemInfo info(imageId);
0311     MetadataHub hub;
0312     hub.load(info);
0313 
0314     ScanController::FileMetadataWrite writeScope(info);
0315     writeScope.changed(hub.writeToMetadata(info, MetadataHub::WRITE_TAGS));
0316 }
0317 
0318 void FaceUtils::removeNormalTag(qlonglong imageId, int tagId)
0319 {
0320     FaceTagsEditor::removeNormalTag(imageId, tagId);
0321 
0322     ItemInfo info(imageId);
0323     MetadataHub hub;
0324     hub.load(info);
0325 
0326     ScanController::FileMetadataWrite writeScope(info);
0327     writeScope.changed(hub.writeToMetadata(info, MetadataHub::WRITE_TAGS));
0328 
0329     if (
0330         !FaceTags::isTheIgnoredPerson(tagId)  &&
0331         !FaceTags::isTheUnknownPerson(tagId)  &&
0332         !FaceTags::isTheUnconfirmedPerson(tagId)
0333        )
0334     {
0335         qlonglong faceItemId = CoreDbAccess().db()->getFirstItemWithFaceTag(tagId);
0336 
0337         /**
0338          * If the face just removed was the final face
0339          * associated with that Tag, reset Tag Icon.
0340          */
0341         if (faceItemId == -1)
0342         {
0343             TAlbum* const album = AlbumManager::instance()->findTAlbum(tagId);
0344 
0345             if (album && (album->iconId() != 0))
0346             {
0347                 QString err;
0348 
0349                 if (!AlbumManager::instance()->updateTAlbumIcon(album, QString(),
0350                                                                 0, err))
0351                 {
0352                     qCDebug(DIGIKAM_GENERAL_LOG) << err ;
0353                 }
0354             }
0355         }
0356     }
0357 }
0358 
0359 void FaceUtils::removeNormalTags(qlonglong imageId, const QList<int>& tagIds)
0360 {
0361     FaceTagsEditor::removeNormalTags(imageId, tagIds);
0362 
0363     ItemInfo info(imageId);
0364     MetadataHub hub;
0365     hub.load(info);
0366 
0367     ScanController::FileMetadataWrite writeScope(info);
0368     writeScope.changed(hub.writeToMetadata(info, MetadataHub::WRITE_TAGS));
0369 }
0370 
0371 // --- Utilities ---
0372 
0373 QRect FaceUtils::faceRectToDisplayRect(const QRect& rect)
0374 {
0375     /*
0376      * Do not change that value unless you know what you do.
0377      * There are a lot of pregenerated thumbnails in user's databases,
0378      * expensive to regenerate, depending on this very value.
0379      */
0380     int margin = qMax(rect.width(), rect.height());
0381     margin    /= 10;
0382 
0383     return rect.adjusted(-margin, -margin, margin, margin);
0384 }
0385 
0386 } // Namespace Digikam
0387 
0388 #include "moc_faceutils.cpp"