1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2010-08-08
7  * Description : FaceEngine database interface allowing easy manipulation of face tags
8  *
9  * Copyright (C) 2010-2011 by Aditya Bhatt <adityabhatt1991 at gmail dot com>
10  * Copyright (C) 2010-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
11  * Copyright (C) 2012-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
12  *
13  * This program is free software; you can redistribute it
14  * and/or modify it under the terms of the GNU General
15  * Public License as published by the Free Software Foundation;
16  * either version 2, or (at your option)
17  * any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * ============================================================ */
25 
26 #include "faceutils.h"
27 
28 // Qt includes
29 
30 #include <QImage>
31 #include <QTimer>
32 
33 // Local includes
34 
35 #include "digikam_debug.h"
36 #include "coredbaccess.h"
37 #include "coredbconstants.h"
38 #include "coredboperationgroup.h"
39 #include "coredb.h"
40 #include "dimg.h"
41 #include "facetags.h"
42 #include "iteminfo.h"
43 #include "itemtagpair.h"
44 #include "fileactionmngr.h"
45 #include "tagproperties.h"
46 #include "tagscache.h"
47 #include "tagregion.h"
48 #include "thumbnailloadthread.h"
49 #include "albummanager.h"
50 
51 namespace Digikam
52 {
53 
54 // --- Constructor / Destructor -------------------------------------------------------------------------------------
55 
FaceUtils(QObject * const parent)56 FaceUtils::FaceUtils(QObject* const parent)
57     : QObject(parent)
58 {
59 }
60 
~FaceUtils()61 FaceUtils::~FaceUtils()
62 {
63 }
64 
65 // --- Mark for scanning and training -------------------------------------------------------------------------------
66 
hasBeenScanned(qlonglong imageid) const67 bool FaceUtils::hasBeenScanned(qlonglong imageid) const
68 {
69     return hasBeenScanned(ItemInfo(imageid));
70 }
71 
hasBeenScanned(const ItemInfo & info) const72 bool FaceUtils::hasBeenScanned(const ItemInfo& info) const
73 {
74     return info.tagIds().contains(FaceTags::scannedForFacesTagId());
75 }
76 
markAsScanned(qlonglong imageid,bool hasBeenScanned) const77 void FaceUtils::markAsScanned(qlonglong imageid, bool hasBeenScanned) const
78 {
79     markAsScanned(ItemInfo(imageid), hasBeenScanned);
80 }
81 
markAsScanned(const ItemInfo & info,bool hasBeenScanned) const82 void FaceUtils::markAsScanned(const ItemInfo& info, bool hasBeenScanned) const
83 {
84     if (hasBeenScanned)
85     {
86         ItemInfo(info).setTag(FaceTags::scannedForFacesTagId());
87     }
88     else
89     {
90         ItemInfo(info).removeTag(FaceTags::scannedForFacesTagId());
91     }
92 }
93 
94 // --- Convert between FacesEngine results and FaceTagsIface ---
95 
toFaceTagsIfaces(qlonglong imageid,const QList<QRectF> & detectedFaces,const QList<Identity> & recognitionResults,const QSize & fullSize) const96 QList<FaceTagsIface> FaceUtils::toFaceTagsIfaces(qlonglong imageid,
97                                                  const QList<QRectF>& detectedFaces,
98                                                  const QList<Identity>& recognitionResults,
99                                                  const QSize& fullSize) const
100 {
101     QList<FaceTagsIface> faces;
102 
103     for (int i = 0 ; i < detectedFaces.size() ; ++i)
104     {
105         Identity identity;
106 
107         if (!recognitionResults.isEmpty())
108         {
109             identity = recognitionResults[i];
110         }
111 
112         // We'll get the unknownPersonTagId if the identity is null
113 
114         int tagId                = FaceTags::getOrCreateTagForIdentity(identity.attributesMap());
115         QRect fullSizeRect       = TagRegion::relativeToAbsolute(detectedFaces[i], fullSize);
116         FaceTagsIface::Type type = identity.isNull() ? FaceTagsIface::UnknownName : FaceTagsIface::UnconfirmedName;
117 
118         if (!tagId || !fullSizeRect.isValid())
119         {
120             faces << FaceTagsIface();
121             continue;
122         }
123 /*
124         qCDebug(DIGIKAM_GENERAL_LOG) << "New Entry" << fullSizeRect << tagId;
125 */
126         faces << FaceTagsIface(type, imageid, tagId, TagRegion(fullSizeRect));
127     }
128 
129     return faces;
130 }
131 
132 // --- Images in faces and thumbnails ---
133 
storeThumbnails(ThumbnailLoadThread * const thread,const QString & filePath,const QList<FaceTagsIface> & databaseFaces,const DImg & image)134 void FaceUtils::storeThumbnails(ThumbnailLoadThread* const thread,
135                                 const QString& filePath,
136                                 const QList<FaceTagsIface>& databaseFaces,
137                                 const DImg& image)
138 {
139     foreach (const FaceTagsIface& face, databaseFaces)
140     {
141         QList<QRect> rects;
142         rects << face.region().toRect();
143         const int margin = faceRectDisplayMargin(face.region().toRect());
144         rects << face.region().toRect().adjusted(-margin, -margin, margin, margin);
145 
146         foreach (const QRect& rect, rects)
147         {
148             QRect mapped  = TagRegion::mapFromOriginalSize(image, rect);
149             QImage detail = image.copyQImage(mapped);
150             thread->storeDetailThumbnail(filePath, rect, detail, true);
151         }
152     }
153 }
154 
155 // --- Face detection: merging results ------------------------------------------------------------------------------------
156 
writeUnconfirmedResults(qlonglong imageid,const QList<QRectF> & detectedFaces,const QList<Identity> & recognitionResults,const QSize & fullSize)157 QList<FaceTagsIface> FaceUtils::writeUnconfirmedResults(qlonglong imageid,
158                                                         const QList<QRectF>& detectedFaces,
159                                                         const QList<Identity>& recognitionResults,
160                                                         const QSize& fullSize)
161 {
162     // Build list of new entries
163 
164     QList<FaceTagsIface> newFaces = toFaceTagsIfaces(imageid, detectedFaces, recognitionResults, fullSize);
165 
166     if (newFaces.isEmpty())
167     {
168         return newFaces;
169     }
170 
171     // list of existing entries
172 
173     QList<FaceTagsIface> currentFaces = databaseFaces(imageid);
174 
175     // merge new with existing entries
176 
177     for (int i = 0 ; i < newFaces.size() ; ++i)
178     {
179         FaceTagsIface& newFace = newFaces[i];
180         QList<FaceTagsIface> overlappingEntries;
181 
182         foreach (const FaceTagsIface& oldFace, currentFaces)
183         {
184             double minOverlap = oldFace.isConfirmedName() ? 0.25 : 0.5;
185 
186             if (oldFace.region().intersects(newFace.region(), minOverlap))
187             {
188                 overlappingEntries << oldFace;
189                 qCDebug(DIGIKAM_GENERAL_LOG) << "Entry" << oldFace.region() << oldFace.tagId()
190                                              << "overlaps" << newFace.region() << newFace.tagId() << ", skipping";
191             }
192         }
193 
194         // The purpose if the next scope is to merge entries:
195         // A confirmed face will never be overwritten.
196         // If a name is set to an old face, it will only be replaced by a new face with a name.
197 
198         if (!overlappingEntries.isEmpty())
199         {
200             if (newFace.isUnknownName())
201             {
202                 // we have no name in the new face. Do we have one in the old faces?
203 
204                 for (int j = 0 ; j < overlappingEntries.size() ; ++j)
205                 {
206                     const FaceTagsIface& oldFace = overlappingEntries.at(j);
207 
208                     if (oldFace.isUnknownName())
209                     {
210                         // remove old face
211                     }
212                     else
213                     {
214                         // skip new entry if any overlapping face has a name, and we do not
215 
216                         newFace = FaceTagsIface();
217                         break;
218                     }
219                 }
220             }
221             else
222             {
223                 // we have a name in the new face. Do we have names in overlapping faces?
224 
225                 for (int j = 0 ; j < overlappingEntries.size() ; ++j)
226                 {
227                     FaceTagsIface& oldFace = overlappingEntries[j];
228 
229                     if      (oldFace.isUnknownName())
230                     {
231                         // remove old face
232                     }
233                     else if (oldFace.isUnconfirmedName())
234                     {
235                         if (oldFace.tagId() == newFace.tagId())
236                         {
237                             // remove smaller face
238 
239                             if (oldFace.region().intersects(newFace.region(), 1))
240                             {
241                                 newFace = FaceTagsIface();
242                                 break;
243                             }
244 
245                             // else remove old face
246                         }
247                         else
248                         {
249                             // assume new recognition is more trained, remove older face
250                         }
251                     }
252                     else if (oldFace.isConfirmedName())
253                     {
254                         // skip new entry, confirmed has of course priority
255 
256                         newFace = FaceTagsIface();
257                     }
258                 }
259             }
260         }
261 
262         // if we did not decide to skip this face, add is to the db now
263 
264         if (!newFace.isNull())
265         {
266             // list will contain all old entries that should still be removed
267 
268             removeFaces(overlappingEntries);
269 
270             ItemTagPair pair(imageid, newFace.tagId());
271 
272             // UnconfirmedName and UnknownName have the same attribute
273 
274             addFaceAndTag(pair, newFace, FaceTagsIface::attributesForFlags(FaceTagsIface::UnconfirmedName), false);
275 
276             // If the face is unconfirmed and the tag is not the unknown person tag, set the unconfirmed person property.
277 
278             if (newFace.isUnconfirmedType() && !FaceTags::isTheUnknownPerson(newFace.tagId()))
279             {
280                 ItemTagPair unconfirmedPair(imageid, FaceTags::unconfirmedPersonTagId());
281                 unconfirmedPair.addProperty(ImageTagPropertyName::autodetectedPerson(),newFace.getAutodetectedPersonString());
282             }
283         }
284     }
285 
286     return newFaces;
287 }
288 
identityForTag(int tagId,FacialRecognitionWrapper & recognizer) const289 Identity FaceUtils::identityForTag(int tagId, FacialRecognitionWrapper& recognizer) const
290 {
291     QMap<QString, QString> attributes = FaceTags::identityAttributes(tagId);
292     Identity identity                 = recognizer.findIdentity(attributes);
293 
294     if (!identity.isNull())
295     {
296         qCDebug(DIGIKAM_GENERAL_LOG) << "Found FacesEngine identity" << identity.id() << "for tag" << tagId;
297         return identity;
298     }
299 
300     qCDebug(DIGIKAM_GENERAL_LOG) << "Adding new FacesEngine identity with attributes" << attributes;
301     identity                          = recognizer.addIdentity(attributes);
302 
303     FaceTags::applyTagIdentityMapping(tagId, identity.attributesMap());
304 
305     return identity;
306 }
307 
tagForIdentity(const Identity & identity) const308 int FaceUtils::tagForIdentity(const Identity& identity) const
309 {
310     return FaceTags::getOrCreateTagForIdentity(identity.attributesMap());
311 }
312 
313 // --- Editing normal tags, reimplemented with FileActionMngr ---
314 
addNormalTag(qlonglong imageId,int tagId)315 void FaceUtils::addNormalTag(qlonglong imageId, int tagId)
316 {
317     FileActionMngr::instance()->assignTag(ItemInfo(imageId), tagId);
318 
319     /**
320      * Implementation for automatic assigning of face as
321      * Tag Icon, if no icon exists currently.
322      * Utilising a QTimer to ensure that a new TAlbum
323      * is given time to be created, before assigning Icon.
324      */
325     QTimer::singleShot(200, [=]()
326         {
327             if (
328                 !FaceTags::isTheIgnoredPerson(tagId)  &&
329                 !FaceTags::isTheUnknownPerson(tagId)  &&
330                 !FaceTags::isTheUnconfirmedPerson(tagId)
331                )
332             {
333                 TAlbum* const album = AlbumManager::instance()->findTAlbum(tagId);
334 
335                 // If Icon is NULL, set the newly added Face as the Icon.
336 
337                 if (album && (album->iconId() == 0))
338                 {
339                     QString err;
340 
341                     if (!AlbumManager::instance()->updateTAlbumIcon(album, QString(),
342                                                                     imageId, err))
343                     {
344                         qCDebug(DIGIKAM_GENERAL_LOG) << err ;
345                     }
346                 }
347             }
348         }
349     );
350 }
351 
removeNormalTag(qlonglong imageId,int tagId)352 void FaceUtils::removeNormalTag(qlonglong imageId, int tagId)
353 {
354     FileActionMngr::instance()->removeTag(ItemInfo(imageId), tagId);
355 
356     if (
357         !FaceTags::isTheIgnoredPerson(tagId)  &&
358         !FaceTags::isTheUnknownPerson(tagId)  &&
359         !FaceTags::isTheUnconfirmedPerson(tagId)
360        )
361     {
362         int count = CoreDbAccess().db()->getNumberOfImagesInTagProperties(tagId,
363                                          ImageTagPropertyName::tagRegion());
364 
365         /**
366          * If the face just removed was the final face
367          * associated with that Tag, reset Tag Icon.
368          */
369         if (count == 0)
370         {
371             TAlbum* const album = AlbumManager::instance()->findTAlbum(tagId);
372 
373             if (album && (album->iconId() != 0))
374             {
375                 QString err;
376 
377                 if (!AlbumManager::instance()->updateTAlbumIcon(album, QString(),
378                                                                 0, err))
379                 {
380                     qCDebug(DIGIKAM_GENERAL_LOG) << err ;
381                 }
382             }
383         }
384     }
385 }
386 
removeNormalTags(qlonglong imageId,const QList<int> & tagIds)387 void FaceUtils::removeNormalTags(qlonglong imageId, const QList<int>& tagIds)
388 {
389     FileActionMngr::instance()->removeTags(ItemInfo(imageId), tagIds);
390 }
391 
392 // --- Utilities ---
393 
rotateFaces(const ItemInfo & info,int newOrientation,int oldOrientation)394 QSize FaceUtils::rotateFaces(const ItemInfo& info,
395                              int newOrientation,
396                              int oldOrientation)
397 {
398     /**
399      *  Get all faces from database and rotate them
400      */
401     QList<FaceTagsIface> facesList = databaseFaces(info.id());
402 
403     if (facesList.isEmpty())
404     {
405         return QSize();
406     }
407 
408     QSize newSize = info.dimensions();
409 
410     foreach (const FaceTagsIface& dface, facesList)
411     {
412         QRect faceRect = dface.region().toRect();
413 
414         TagRegion::reverseToOrientation(faceRect,
415                                         oldOrientation,
416                                         info.dimensions());
417 
418         newSize = TagRegion::adjustToOrientation(faceRect,
419                                                  newOrientation,
420                                                  info.dimensions());
421 
422         changeRegion(dface, TagRegion(faceRect));
423     }
424 
425     return newSize;
426 }
427 
faceRectDisplayMargin(const QRect & rect)428 int FaceUtils::faceRectDisplayMargin(const QRect& rect)
429 {
430     /*
431      * Do not change that value unless you know what you do.
432      * There are a lot of pregenerated thumbnails in user's databases,
433      * expensive to regenerate, depending on this very value.
434      */
435     int margin = qMax(rect.width(), rect.height());
436     margin    /= 10;
437 
438     return margin;
439 }
440 
441 } // Namespace Digikam
442