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"