1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2011-08-08
7  * Description : Accessing 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  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "facetags.h"
26 
27 // KDE includes
28 
29 #include <klocalizedstring.h>
30 
31 // Local includes
32 
33 #include "digikam_debug.h"
34 #include "coredbconstants.h"
35 #include "tagscache.h"
36 #include "tagregion.h"
37 #include "tagproperties.h"
38 
39 namespace Digikam
40 {
41 
42 // --- FaceIfacePriv ---
43 
44 class Q_DECL_HIDDEN FaceTagsHelper
45 {
46 public:
47 
48     static QString tagPath(const QString& name, int parentId);
49     static void    makeFaceTag(int tagId, const QString& fullName);
50     static int     findFirstTagWithProperty(const QString& property, const QString& value = QString());
51     static int     tagForName(const QString& name, int tagId, int parentId,
52                               const QString& givenFullName, bool convert, bool create);
53 };
54 
55 // --- Private methods ---
56 
findFirstTagWithProperty(const QString & property,const QString & value)57 int FaceTagsHelper::findFirstTagWithProperty(const QString& property, const QString& value)
58 {
59     QList<int> candidates = TagsCache::instance()->tagsWithProperty(property, value);
60 
61     if (!candidates.isEmpty())
62     {
63         return candidates.first();
64     }
65 
66     return 0;
67 }
68 
tagPath(const QString & name,int parentId)69 QString FaceTagsHelper::tagPath(const QString& name, int parentId)
70 {
71     QString faceParentTagName = TagsCache::instance()->tagName(parentId);
72 
73     if ((faceParentTagName).contains(QRegExp(QLatin1String("(_Digikam_root_tag_/|/_Digikam_root_tag_|_Digikam_root_tag_)"))))
74     {
75         return  QLatin1Char('/') + name;
76     }
77     else
78     {
79         return faceParentTagName + QLatin1Char('/') + name;
80     }
81 }
82 
makeFaceTag(int tagId,const QString & fullName)83 void FaceTagsHelper::makeFaceTag(int tagId, const QString& fullName)
84 {
85     QString faceEngineName  = fullName;
86     /*
87      *    // find a unique FacesEngineId
88      *    for (int i=0; d->findFirstTagWithProperty(TagPropertyName::FacesEngineId(), FacesEngineId); ++i)
89      *    {
90      *        FacesEngineId = fullName + QString::fromUtf8(" (%1)").arg(i);
91      *    }
92      */
93     TagProperties props(tagId);
94     props.setProperty(TagPropertyName::person(),         fullName);
95     props.setProperty(TagPropertyName::faceEngineName(), faceEngineName);
96 }
97 
tagForName(const QString & name,int tagId,int parentId,const QString & givenFullName,bool convert,bool create)98 int FaceTagsHelper::tagForName(const QString& name, int tagId, int parentId, const QString& givenFullName,
99                                bool convert, bool create)
100 {
101     if (name.isEmpty() && givenFullName.isEmpty() && !tagId)
102     {
103         return FaceTags::unknownPersonTagId();
104     }
105 
106     QString fullName = givenFullName.isNull() ? name : givenFullName;
107 
108     if (tagId)
109     {
110         if      (FaceTags::isPerson(tagId))
111         {
112 /*
113             qCDebug(DIGIKAM_DATABASE_LOG) << "Proposed tag is already a person";
114 */
115             return tagId;
116         }
117         else if (convert)
118         {
119             if (fullName.isNull())
120             {
121                 fullName = TagsCache::instance()->tagName(tagId);
122             }
123 
124             qCDebug(DIGIKAM_DATABASE_LOG) << "Converting proposed tag to person, full name" << fullName;
125             makeFaceTag(tagId, fullName);
126 
127             return tagId;
128         }
129 
130         return 0;
131     }
132 
133     // First attempt: Find by full name in "person" attribute
134 
135     QList<int> candidates = TagsCache::instance()->tagsWithProperty(TagPropertyName::person(), fullName);
136 
137     foreach (int id, candidates)
138     {
139         qCDebug(DIGIKAM_DATABASE_LOG) << "Candidate with set full name:" << id << fullName;
140 
141         if (parentId == -1)
142         {
143             return id;
144         }
145         else if (TagsCache::instance()->parentTag(id) == parentId)
146         {
147             return id;
148         }
149     }
150 
151     // Second attempt: Find by tag name
152 
153     if (parentId == -1)
154     {
155         candidates = TagsCache::instance()->tagsForName(name);
156     }
157     else
158     {
159         tagId = TagsCache::instance()->tagForName(name, parentId);
160         candidates.clear();
161 
162         if (tagId)
163         {
164             candidates << tagId;
165         }
166     }
167 
168     foreach (int id, candidates)
169     {
170         // Is this tag already a person tag?
171 
172         if (FaceTags::isPerson(id))
173         {
174             qCDebug(DIGIKAM_DATABASE_LOG) << "Found tag with name" << name << "is already a person." << id;
175             return id;
176         }
177         else if (convert)
178         {
179             qCDebug(DIGIKAM_DATABASE_LOG) << "Converting tag with name" << name << "to a person." << id;
180             makeFaceTag(id, fullName);
181             return id;
182         }
183     }
184 
185     // Third: If desired, create a new tag
186 
187     if (create)
188     {
189         qCDebug(DIGIKAM_DATABASE_LOG) << "Creating new tag for name" << name << "fullName" << fullName;
190 
191         if (parentId == -1)
192         {
193             parentId = FaceTags::personParentTag();
194         }
195 
196         tagId = TagsCache::instance()->getOrCreateTag(tagPath(name, parentId));
197         makeFaceTag(tagId, fullName);
198 
199         return tagId;
200     }
201 
202     return 0;
203 }
204 
205 // --- public methods ---
206 
allPersonNames()207 QList<QString> FaceTags::allPersonNames()
208 {
209     return TagsCache::instance()->tagNames(allPersonTags());
210 }
211 
allPersonPaths()212 QList<QString> FaceTags::allPersonPaths()
213 {
214     return TagsCache::instance()->tagPaths(allPersonTags());
215 }
216 
tagForPerson(const QString & name,int parentId,const QString & fullName)217 int FaceTags::tagForPerson(const QString& name, int parentId, const QString& fullName)
218 {
219     return FaceTagsHelper::tagForName(name, 0, parentId, fullName, false, false);
220 }
221 
getOrCreateTagForPerson(const QString & name,int parentId,const QString & fullName)222 int FaceTags::getOrCreateTagForPerson(const QString& name, int parentId, const QString& fullName)
223 {
224     return FaceTagsHelper::tagForName(name, 0, parentId, fullName, true, true);
225 }
226 
ensureIsPerson(int tagId,const QString & fullName)227 void FaceTags::ensureIsPerson(int tagId, const QString& fullName)
228 {
229     FaceTagsHelper::tagForName(QString(), tagId, 0, fullName, true, false);
230 }
231 
isPerson(int tagId)232 bool FaceTags::isPerson(int tagId)
233 {
234     return TagsCache::instance()->hasProperty(tagId, TagPropertyName::person());
235 }
236 
isTheUnknownPerson(int tagId)237 bool FaceTags::isTheUnknownPerson(int tagId)
238 {
239     return TagsCache::instance()->hasProperty(tagId, TagPropertyName::unknownPerson());
240 }
241 
isTheUnconfirmedPerson(int tagId)242 bool FaceTags::isTheUnconfirmedPerson(int tagId)
243 {
244     return TagsCache::instance()->hasProperty(tagId, TagPropertyName::unconfirmedPerson());
245 }
246 
isTheIgnoredPerson(int tagId)247 bool FaceTags::isTheIgnoredPerson(int tagId)
248 {
249     return TagsCache::instance()->hasProperty(tagId, TagPropertyName::ignoredPerson());
250 }
251 
allPersonTags()252 QList<int> FaceTags::allPersonTags()
253 {
254     return TagsCache::instance()->tagsWithProperty(TagPropertyName::person());
255 }
256 
scannedForFacesTagId()257 int FaceTags::scannedForFacesTagId()
258 {
259     return TagsCache::instance()->getOrCreateInternalTag(InternalTagName::scannedForFaces()); // no i18n
260 }
261 
identityAttributes(int tagId)262 QMap<QString, QString> FaceTags::identityAttributes(int tagId)
263 {
264     QMap<QString, QString> attributes;
265     QString uuid = TagsCache::instance()->propertyValue(tagId, TagPropertyName::faceEngineUuid());
266 
267     if (!uuid.isEmpty())
268     {
269         attributes[QLatin1String("uuid")] = uuid;
270     }
271 
272     QString fullName = TagsCache::instance()->propertyValue(tagId, TagPropertyName::person());
273 
274     if (!fullName.isEmpty())
275     {
276         attributes[QLatin1String("fullName")] = fullName;
277     }
278 
279     QString faceEngineName = TagsCache::instance()->propertyValue(tagId, TagPropertyName::person());
280     QString tagName        = TagsCache::instance()->tagName(tagId);
281 
282     if (tagName != faceEngineName)
283     {
284         attributes.insertMulti(QLatin1String("name"), faceEngineName);
285         attributes.insertMulti(QLatin1String("name"), tagName);
286     }
287     else
288     {
289         attributes[QLatin1String("name")] = tagName;
290     }
291 
292     return attributes;
293 }
294 
applyTagIdentityMapping(int tagId,const QMap<QString,QString> & attributes)295 void FaceTags::applyTagIdentityMapping(int tagId, const QMap<QString, QString>& attributes)
296 {
297     TagProperties props(tagId);
298 
299     if (attributes.contains(QLatin1String("fullName")))
300     {
301         props.setProperty(TagPropertyName::person(), attributes.value(QLatin1String("fullName")));
302     }
303 
304     // we do not change the digikam tag name at this point, but we have this extra tag property
305 
306     if (attributes.contains(QLatin1String("name")))
307     {
308         props.setProperty(TagPropertyName::faceEngineName(), attributes.value(QLatin1String("name")));
309     }
310 
311     props.setProperty(TagPropertyName::faceEngineUuid(), attributes.value(QLatin1String("uuid")));
312 }
313 
getOrCreateTagForIdentity(const QMap<QString,QString> & attributes)314 int FaceTags::getOrCreateTagForIdentity(const QMap<QString, QString>& attributes)
315 {
316     // Attributes from FacesEngine's Identity object.
317     // The text constants are defines in FacesEngine's API docs
318 
319     if (attributes.isEmpty())
320     {
321         return FaceTags::unknownPersonTagId();
322     }
323 
324     int tagId;
325 
326     // First, look for UUID
327 
328     if (!attributes.value(QLatin1String("uuid")).isEmpty())
329     {
330         if ((tagId = FaceTagsHelper::findFirstTagWithProperty(TagPropertyName::faceEngineUuid(), attributes.value(QLatin1String("uuid")))))
331         {
332             return tagId;
333         }
334     }
335 
336     // Second, look for full name
337 
338     if (!attributes.value(QLatin1String("fullName")).isEmpty())
339     {
340         if ((tagId = FaceTagsHelper::findFirstTagWithProperty(TagPropertyName::person(), attributes.value(QLatin1String("fullName")))))
341         {
342             return tagId;
343         }
344     }
345 
346     // Third, look for either name or full name
347     // TODO: better support for "fullName"
348 
349     QString name = attributes.value(QLatin1String("name"));
350 
351     if (name.isEmpty())
352     {
353         name = attributes.value(QLatin1String("fullName"));
354     }
355 
356     if (name.isEmpty())
357     {
358         return FaceTags::unknownPersonTagId();
359     }
360 
361     if ((tagId = FaceTagsHelper::findFirstTagWithProperty(TagPropertyName::faceEngineName(), name)))
362     {
363         return tagId;
364     }
365 
366     if ((tagId = FaceTagsHelper::findFirstTagWithProperty(TagPropertyName::person(), name)))
367     {
368         return tagId;
369     }
370 
371     // identity is in FacesEngine's database, but not in ours, so create.
372 
373     tagId = FaceTagsHelper::tagForName(name, 0, -1, attributes.value(QLatin1String("fullName")), true, true);
374     applyTagIdentityMapping(tagId, attributes);
375 
376     return tagId;
377 }
378 
faceNameForTag(int tagId)379 QString FaceTags::faceNameForTag(int tagId)
380 {
381     if (!TagsCache::instance()->hasTag(tagId))
382     {
383         return QString();
384     }
385 
386     QString id = TagsCache::instance()->propertyValue(tagId, TagPropertyName::person());
387 
388     if (id.isNull())
389     {
390         id = TagsCache::instance()->tagName(tagId);
391     }
392 
393     return id;
394 }
395 
personParentTag()396 int FaceTags::personParentTag()
397 {
398     // check default
399 
400     QString i18nName = i18nc("People on your photos", "People");
401     int tagId        = TagsCache::instance()->tagForPath(i18nName);
402 
403     if (tagId)
404     {
405         return tagId;
406     }
407 
408     // employ a heuristic
409 
410     QList<int> personTags = allPersonTags();
411 
412     if (!personTags.isEmpty())
413     {
414         // we find the most toplevel parent tag of a person tag
415 
416         QMultiMap<int, int> tiers;
417 
418         foreach (int tid, personTags)
419         {
420             tiers.insert(TagsCache::instance()->parentTags(tid).size(), tid);
421         }
422 
423         QList<int> mosttoplevelTags = tiers.values(tiers.begin().key());
424 
425         // as a pretty weak criterion, take the largest id which usually corresponds to the latest tag creation.
426 
427         std::sort(mosttoplevelTags.begin(), mosttoplevelTags.end());
428 
429         return TagsCache::instance()->parentTag(mosttoplevelTags.last());
430     }
431 
432     // create default
433 
434     return TagsCache::instance()->getOrCreateTag(i18nName);
435 }
436 
unknownPersonTagId()437 int FaceTags::unknownPersonTagId()
438 {
439     QList<int> ids = TagsCache::instance()->tagsWithPropertyCached(TagPropertyName::unknownPerson());
440 
441     if (!ids.isEmpty())
442     {
443         return ids.first();
444     }
445 
446     int unknownPersonTagId = TagsCache::instance()->getOrCreateTag(
447                                         FaceTagsHelper::tagPath(
448                                         i18nc("The list of detected faces from the collections but not recognized", "Unknown"),
449                                         personParentTag()));
450     TagProperties props(unknownPersonTagId);
451     props.setProperty(TagPropertyName::person(),        QString()); // no name associated
452     props.setProperty(TagPropertyName::unknownPerson(), QString()); // special property
453 
454     return unknownPersonTagId;
455 }
456 
unconfirmedPersonTagId()457 int FaceTags::unconfirmedPersonTagId()
458 {
459     QList<int> ids = TagsCache::instance()->tagsWithPropertyCached(TagPropertyName::unconfirmedPerson());
460 
461     if (!ids.isEmpty())
462     {
463         return ids.first();
464     }
465 
466     int unknownPersonTagId = TagsCache::instance()->getOrCreateTag(
467                                         FaceTagsHelper::tagPath(
468                                         i18nc("The list of recognized faces from the collections but not confirmed", "Unconfirmed"),
469                                         personParentTag()));
470     TagProperties props(unknownPersonTagId);
471     props.setProperty(TagPropertyName::person(),            QString()); // no name associated
472     props.setProperty(TagPropertyName::unconfirmedPerson(), QString()); // special property
473 
474     return unknownPersonTagId;
475 }
476 
ignoredPersonTagId()477 int FaceTags::ignoredPersonTagId()
478 {
479     QList<int> ids = TagsCache::instance()->tagsWithPropertyCached(TagPropertyName::ignoredPerson());
480 
481     if (!ids.isEmpty())
482     {
483         return ids.first();
484     }
485 
486     int ignoredPersonTagId = TagsCache::instance()->getOrCreateTag(
487                                         FaceTagsHelper::tagPath(
488                                         i18nc("List of detected faces that need not be recognized", "Ignored"),
489                                         personParentTag()));
490     TagProperties props(ignoredPersonTagId);
491     props.setProperty(TagPropertyName::person(),        QString());
492     props.setProperty(TagPropertyName::ignoredPerson(), QString());
493 
494     return ignoredPersonTagId;
495 }
496 
existsIgnoredPerson()497 bool FaceTags::existsIgnoredPerson()
498 {
499     QList<int> ids = TagsCache::instance()->tagsWithPropertyCached(TagPropertyName::ignoredPerson());
500 
501     if (!ids.isEmpty())
502     {
503         return true;
504     }
505 
506     return false;
507 }
508 
509 } // Namespace Digikam
510