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