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