1 /* ============================================================
2 *
3 * Date : 2008-02-10
4 * Description : a tool to fix automatically camera lens aberrations
5 *
6 * Copyright (C) 2008 by Adrian Schroeter <adrian at suse dot de>
7 * Copyright (C) 2008-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
8 *
9 * This program is free software; you can redistribute it
10 * and/or modify it under the terms of the GNU General
11 * Public License as published by the Free Software Foundation;
12 * either version 2, or (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * ============================================================ */
20
21 #include "lensfuniface.h"
22
23 // Qt includes
24
25 #include <QStandardPaths>
26 #include <QFile>
27 #include <QDir>
28
29 // Local includes
30
31 #include "digikam_debug.h"
32
33 // Disable deprecated API from Lensfun.
34 #if defined(Q_CC_GNU)
35 # pragma GCC diagnostic push
36 # pragma GCC diagnostic ignored "-Wdeprecated-declarations"
37 #endif
38
39 #if defined(Q_CC_CLANG)
40 # pragma clang diagnostic push
41 # pragma clang diagnostic ignored "-Wdeprecated-declarations"
42 #endif
43
44 namespace Digikam
45 {
46
47 class Q_DECL_HIDDEN LensFunIface::Private
48 {
49 public:
50
Private()51 explicit Private()
52 : lfDb (nullptr),
53 lfCameras (nullptr),
54 usedLens (nullptr),
55 usedCamera(nullptr)
56 {
57 }
58
59 // To be used for modification
60 LensFunContainer settings;
61
62 // Database items
63 lfDatabase* lfDb;
64 const lfCamera* const* lfCameras;
65
66 QString makeDescription;
67 QString modelDescription;
68 QString lensDescription;
69
70 LensPtr usedLens;
71 DevicePtr usedCamera;
72 };
73
LensFunIface()74 LensFunIface::LensFunIface()
75 : d(new Private)
76 {
77 d->lfDb = lf_db_new();
78
79 // Lensfun host XML files in a dedicated sub-directory.
80
81 QString lensPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
82 QLatin1String("lensfun"),
83 QStandardPaths::LocateDirectory);
84
85 QDir lensDir;
86
87 // In first try to use last Lensfun version data dir.
88
89 lensDir = QDir(lensPath + QLatin1String("/version_2"), QLatin1String("*.xml"));
90
91 if (lensDir.entryList().isEmpty())
92 {
93 // Fail-back to revision 1.
94
95 lensDir = QDir(lensPath + QLatin1String("/version_1"), QLatin1String("*.xml"));
96
97 if (lensDir.entryList().isEmpty())
98 {
99 // Fail-back to revision 0 which host XML data in root data directory.
100
101 lensDir = QDir(lensPath, QLatin1String("*.xml"));
102 }
103 }
104
105 qCDebug(DIGIKAM_DIMG_LOG) << "Using root lens database dir: " << lensPath;
106
107 foreach (const QString& lens, lensDir.entryList())
108 {
109 qCDebug(DIGIKAM_DIMG_LOG) << "Load lens database file: " << lens;
110 d->lfDb->Load(QFile::encodeName(lensDir.absoluteFilePath(lens)).constData());
111 }
112
113 d->lfDb->Load();
114
115 d->lfCameras = d->lfDb->GetCameras();
116 }
117
~LensFunIface()118 LensFunIface::~LensFunIface()
119 {
120 lf_db_destroy(d->lfDb);
121 delete d;
122 }
123
setSettings(const LensFunContainer & other)124 void LensFunIface::setSettings(const LensFunContainer& other)
125 {
126 d->settings = other;
127 d->usedCamera = findCamera(d->settings.cameraMake, d->settings.cameraModel);
128 d->usedLens = findLens(d->settings.lensModel);
129 }
130
settings() const131 LensFunContainer LensFunIface::settings() const
132 {
133 return d->settings;
134 }
135
usedCamera() const136 LensFunIface::DevicePtr LensFunIface::usedCamera() const
137 {
138 return d->usedCamera;
139 }
140
setUsedCamera(DevicePtr cam)141 void LensFunIface::setUsedCamera(DevicePtr cam)
142 {
143 d->usedCamera = cam;
144 d->settings.cameraMake = d->usedCamera ? QLatin1String(d->usedCamera->Maker) : QString();
145 d->settings.cameraModel = d->usedCamera ? QLatin1String(d->usedCamera->Model) : QString();
146 d->settings.cropFactor = d->usedCamera ? d->usedCamera->CropFactor : -1.0;
147 }
148
usedLens() const149 LensFunIface::LensPtr LensFunIface::usedLens() const
150 {
151 return d->usedLens;
152 }
153
setUsedLens(LensPtr lens)154 void LensFunIface::setUsedLens(LensPtr lens)
155 {
156 d->usedLens = lens;
157 d->settings.lensModel = d->usedLens ? QLatin1String(d->usedLens->Model) : QString();
158 }
159
lensFunDataBase() const160 lfDatabase* LensFunIface::lensFunDataBase() const
161 {
162 return d->lfDb;
163 }
164
makeDescription() const165 QString LensFunIface::makeDescription() const
166 {
167 return d->makeDescription;
168 }
169
modelDescription() const170 QString LensFunIface::modelDescription() const
171 {
172 return d->modelDescription;
173 }
174
lensDescription() const175 QString LensFunIface::lensDescription() const
176 {
177 return d->lensDescription;
178 }
179
lensFunCameras() const180 const lfCamera* const* LensFunIface::lensFunCameras() const
181 {
182 return d->lfCameras;
183 }
184
setFilterSettings(const LensFunContainer & other)185 void LensFunIface::setFilterSettings(const LensFunContainer& other)
186 {
187 d->settings.filterCCA = other.filterCCA;
188 d->settings.filterVIG = other.filterVIG;
189 d->settings.filterDST = other.filterDST;
190 d->settings.filterGEO = other.filterGEO;
191 }
192
findCamera(const QString & make,const QString & model) const193 LensFunIface::DevicePtr LensFunIface::findCamera(const QString& make, const QString& model) const
194 {
195 const lfCamera* const* cameras = d->lfDb->GetCameras();
196
197 while (cameras && *cameras)
198 {
199 DevicePtr cam = *cameras;
200 // qCDebug(DIGIKAM_DIMG_LOG) << "Query camera:" << cam->Maker << "-" << cam->Model;
201
202 if ((QString::fromLatin1(cam->Maker).toLower() == make.toLower()) &&
203 (QString::fromLatin1(cam->Model).toLower() == model.toLower()))
204 {
205 qCDebug(DIGIKAM_DIMG_LOG) << "Search for camera " << make << "-" << model << " ==> true";
206 return cam;
207 }
208
209 ++cameras;
210 }
211
212 qCDebug(DIGIKAM_DIMG_LOG) << "Search for camera " << make << "-" << model << " ==> false";
213 return nullptr;
214 }
215
findLens(const QString & model) const216 LensFunIface::LensPtr LensFunIface::findLens(const QString& model) const
217 {
218 const lfLens* const* lenses = d->lfDb->GetLenses();
219
220 while (lenses && *lenses)
221 {
222 LensPtr lens = *lenses;
223
224 if (QString::fromLatin1(lens->Model) == model)
225 {
226 qCDebug(DIGIKAM_DIMG_LOG) << "Search for lens " << model << " ==> true";
227 return lens;
228 }
229
230 ++lenses;
231 }
232
233 qCDebug(DIGIKAM_DIMG_LOG) << "Search for lens " << model << " ==> false";
234 return nullptr;
235 }
236
findLenses(const lfCamera * const lfCamera,const QString & lensDesc,const QString & lensMaker) const237 LensFunIface::LensList LensFunIface::findLenses(const lfCamera* const lfCamera,
238 const QString& lensDesc,
239 const QString& lensMaker) const
240 {
241 LensList lensList;
242
243 if (lfCamera)
244 {
245 const char* const maker = lensMaker.isEmpty() ? nullptr : lensMaker.toLatin1().constData();
246 const char* const model = lensDesc.isEmpty() ? nullptr : lensDesc.toLatin1().constData();
247 const lfLens* const *lfLens = d->lfDb->FindLenses(lfCamera, maker, model);
248
249 while (lfLens && *lfLens)
250 {
251 lensList << (*lfLens);
252 ++lfLens;
253 }
254 }
255
256 return lensList;
257 }
258
findFromMetadata(DMetadata * const meta)259 LensFunIface::MetadataMatch LensFunIface::findFromMetadata(DMetadata* const meta)
260 {
261 MetadataMatch ret = MetadataNoMatch;
262 d->settings = LensFunContainer();
263 d->usedCamera = nullptr;
264 d->usedLens = nullptr;
265 d->lensDescription.clear();
266
267 if (!meta || meta->isEmpty())
268 {
269 qCDebug(DIGIKAM_DIMG_LOG) << "No metadata available";
270 return LensFunIface::MetadataUnavailable;
271 }
272
273 PhotoInfoContainer photoInfo = meta->getPhotographInformation();
274 d->makeDescription = photoInfo.make.trimmed();
275 d->modelDescription = photoInfo.model.trimmed();
276 bool exactMatch = true;
277
278 if (d->makeDescription.isEmpty())
279 {
280 qCDebug(DIGIKAM_DIMG_LOG) << "No camera maker info available";
281 exactMatch = false;
282 }
283 else
284 {
285 // NOTE: see bug #184156:
286 // Some rules to wrap unknown camera device from Lensfun database, which have equivalent in fact.
287
288 if (d->makeDescription == QLatin1String("Canon"))
289 {
290 if (d->modelDescription == QLatin1String("Canon EOS Kiss Digital X"))
291 {
292 d->modelDescription = QLatin1String("Canon EOS 400D DIGITAL");
293 }
294
295 if (d->modelDescription == QLatin1String("G1 X"))
296 {
297 d->modelDescription = QLatin1String("G1X");
298 }
299 }
300
301 if (d->makeDescription.contains(QLatin1String("olympus"), Qt::CaseInsensitive))
302 {
303 if (!findCamera(d->makeDescription, d->modelDescription))
304 {
305 QStringList olympusList;
306 olympusList << QLatin1String("Olympus Imaging Corp.");
307 olympusList << QLatin1String("Olympus Corporation");
308 olympusList << QLatin1String("Olympus");
309
310 while (!olympusList.isEmpty())
311 {
312 if (findCamera(olympusList.first(), d->modelDescription))
313 {
314 d->makeDescription = olympusList.first();
315 break;
316 }
317
318 olympusList.removeFirst();
319 }
320 }
321 }
322
323 d->lensDescription = photoInfo.lens.trimmed();
324
325 // ------------------------------------------------------------------------------------------------
326
327 DevicePtr lfCamera = findCamera(d->makeDescription, d->modelDescription);
328
329 if (lfCamera)
330 {
331 setUsedCamera(lfCamera);
332
333 qCDebug(DIGIKAM_DIMG_LOG) << "Camera maker : " << d->settings.cameraMake;
334 qCDebug(DIGIKAM_DIMG_LOG) << "Camera model : " << d->settings.cameraModel;
335
336 // ------------------------------------------------------------------------------------------------
337 // -- Performing lens description searches.
338
339 LensList lensMatches;
340
341 if (!d->lensDescription.isEmpty())
342 {
343 QString lensCutted;
344 LensList lensList;
345
346 // STAGE 1, search in LensFun database as well.
347
348 lensList = findLenses(d->usedCamera, d->lensDescription);
349 qCDebug(DIGIKAM_DIMG_LOG) << "* Check for lens by direct query (" << d->lensDescription << " : " << lensList.count() << ")";
350 lensMatches.append(lensList);
351
352 // STAGE 2, Adapt exiv2 strings to lensfun strings for Nikon.
353
354 lensCutted = d->lensDescription;
355
356 if (lensCutted.contains(QLatin1String("Nikon")))
357 {
358 lensCutted.remove(QLatin1String("Nikon "));
359 lensCutted.remove(QLatin1String("Zoom-"));
360 lensCutted.replace(QLatin1String("IF-ID"), QLatin1String("ED-IF"));
361 lensList = findLenses(d->usedCamera, lensCutted);
362 qCDebug(DIGIKAM_DIMG_LOG) << "* Check for Nikon lens (" << lensCutted << " : " << lensList.count() << ")";
363 lensMatches.append(lensList);
364 }
365
366 // TODO : Add here more specific lens maker rules.
367
368 // LAST STAGE, Adapt exiv2 strings to lensfun strings. Some lens description use something like that :
369 // "10.0 - 20.0 mm". This must be adapted like this : "10-20mm"
370
371 lensCutted = d->lensDescription;
372 lensCutted.replace(QRegExp(QLatin1String("\\.[0-9]")), QLatin1String("")); //krazy:exclude=doublequote_chars
373 lensCutted.replace(QLatin1String(" - "), QLatin1String("-"));
374 lensCutted.replace(QLatin1String(" mm"), QLatin1String("mn"));
375 lensList = findLenses(d->usedCamera, lensCutted);
376 qCDebug(DIGIKAM_DIMG_LOG) << "* Check for no maker lens (" << lensCutted << " : " << lensList.count() << ")";
377 lensMatches.append(lensList);
378
379 // Remove all duplicate lenses in the list by using QSet.
380
381 lensMatches = lensMatches.toSet().toList();
382 }
383 else
384 {
385 qCDebug(DIGIKAM_DIMG_LOG) << "Lens description string is empty";
386
387 const LensList lensList = findLenses(d->usedCamera, QString());
388
389 if (lensList.count() == 1)
390 {
391 // NOTE: see bug #407157
392
393 qCDebug(DIGIKAM_DIMG_LOG) << "For the camera " << d->settings.cameraModel
394 << " there is exactly one lens in the database: "
395 << lensList.first()->Model;
396 lensMatches.append(lensList);
397 }
398 else
399 {
400 exactMatch = false;
401 }
402 }
403
404 // Display the results.
405
406 if (lensMatches.isEmpty())
407 {
408 qCDebug(DIGIKAM_DIMG_LOG) << "lens matches : NOT FOUND";
409 exactMatch = false;
410 }
411 else if (lensMatches.count() == 1)
412 {
413 // Best case for an exact match is to have only one item returned by Lensfun searches.
414
415 setUsedLens(lensMatches.first());
416 qCDebug(DIGIKAM_DIMG_LOG) << "Lens found : " << d->settings.lensModel;
417 qCDebug(DIGIKAM_DIMG_LOG) << "Crop Factor : " << d->settings.cropFactor;
418 }
419 else
420 {
421 qCDebug(DIGIKAM_DIMG_LOG) << "lens matches : more than one...";
422 const lfLens* similar = nullptr;
423 double percent = 0.0;
424
425 foreach (const lfLens* const l, lensMatches)
426 {
427 double result = checkSimilarity(d->lensDescription, QLatin1String(l->Model));
428
429 if (result > percent)
430 {
431 percent = result;
432 similar = l;
433 }
434 }
435
436 if (similar)
437 {
438 qCDebug(DIGIKAM_DIMG_LOG) << "found similary match from" << lensMatches.count()
439 << "possibilities:" << similar->Model
440 << "similarity:" << percent;
441 setUsedLens(similar);
442 }
443 else
444 {
445 exactMatch = false;
446 }
447 }
448 }
449 else
450 {
451 qCDebug(DIGIKAM_DIMG_LOG) << "Cannot find Lensfun camera device for (" << d->makeDescription << " - " << d->modelDescription << ")";
452 exactMatch = false;
453 }
454 }
455
456 // ------------------------------------------------------------------------------------------------
457 // Performing Lens settings searches.
458
459 QString temp = photoInfo.focalLength;
460
461 if (temp.isEmpty())
462 {
463 qCDebug(DIGIKAM_DIMG_LOG) << "Focal Length : NOT FOUND";
464 exactMatch = false;
465 }
466
467 d->settings.focalLength = temp.mid(0, temp.length() - 3).toDouble(); // HACK: strip the " mm" at the end ...
468 qCDebug(DIGIKAM_DIMG_LOG) << "Focal Length : " << d->settings.focalLength;
469
470 // ------------------------------------------------------------------------------------------------
471
472 temp = photoInfo.aperture;
473
474 if (temp.isEmpty())
475 {
476 qCDebug(DIGIKAM_DIMG_LOG) << "Aperture : NOT FOUND";
477 exactMatch = false;
478 }
479
480 d->settings.aperture = temp.mid(1).toDouble();
481 qCDebug(DIGIKAM_DIMG_LOG) << "Aperture : " << d->settings.aperture;
482
483 // ------------------------------------------------------------------------------------------------
484 // Try to get subject distance value.
485
486 // From standard Exif.
487
488 temp = meta->getExifTagString("Exif.Photo.SubjectDistance");
489
490 if (temp.isEmpty())
491 {
492 // From standard XMP.
493
494 temp = meta->getXmpTagString("Xmp.exif.SubjectDistance");
495 }
496
497 if (temp.isEmpty())
498 {
499 // From Canon Makernote.
500
501 temp = meta->getExifTagString("Exif.CanonSi.SubjectDistance");
502 }
503
504 if (temp.isEmpty())
505 {
506 // From Nikon Makernote.
507
508 temp = meta->getExifTagString("Exif.NikonLd2.FocusDistance");
509 }
510
511 if (temp.isEmpty())
512 {
513 // From Nikon Makernote.
514
515 temp = meta->getExifTagString("Exif.NikonLd3.FocusDistance");
516 }
517
518 if (temp.isEmpty())
519 {
520 // From Olympus Makernote.
521
522 temp = meta->getExifTagString("Exif.OlympusFi.FocusDistance");
523 }
524
525 // TODO: Add here others Makernotes tags.
526
527 if (temp.isEmpty())
528 {
529 qCDebug(DIGIKAM_DIMG_LOG) << "Subject dist. : NOT FOUND : Use default value.";
530 temp = QLatin1String("1000");
531 }
532
533 temp = temp.remove(QLatin1String(" m"));
534 bool ok;
535 d->settings.subjectDistance = temp.toDouble(&ok);
536
537 if (!ok)
538 {
539 d->settings.subjectDistance = -1.0;
540 }
541
542 qCDebug(DIGIKAM_DIMG_LOG) << "Subject dist. : " << d->settings.subjectDistance;
543
544 // ------------------------------------------------------------------------------------------------
545
546 ret = exactMatch ? MetadataExactMatch : MetadataPartialMatch;
547
548 qCDebug(DIGIKAM_DIMG_LOG) << "Metadata match : " << metadataMatchDebugStr(ret);
549
550 return ret;
551 }
552
metadataMatchDebugStr(MetadataMatch val) const553 QString LensFunIface::metadataMatchDebugStr(MetadataMatch val) const
554 {
555 QString ret;
556
557 switch (val)
558 {
559 case MetadataNoMatch:
560 ret = QLatin1String("No Match");
561 break;
562
563 case MetadataPartialMatch:
564 ret = QLatin1String("Partial Match");
565 break;
566
567 default:
568 ret = QLatin1String("Exact Match");
569 break;
570 }
571
572 return ret;
573 }
574
supportsDistortion() const575 bool LensFunIface::supportsDistortion() const
576 {
577 if (!d->usedLens)
578 {
579 return false;
580 }
581
582 lfLensCalibDistortion res;
583
584 return d->usedLens->InterpolateDistortion(d->settings.focalLength, res);
585 }
586
supportsCCA() const587 bool LensFunIface::supportsCCA() const
588 {
589 if (!d->usedLens)
590 {
591 return false;
592 }
593
594 lfLensCalibTCA res;
595
596 return d->usedLens->InterpolateTCA(d->settings.focalLength, res);
597 }
598
supportsVig() const599 bool LensFunIface::supportsVig() const
600 {
601 if (!d->usedLens)
602 {
603 return false;
604 }
605
606 lfLensCalibVignetting res;
607
608 return d->usedLens->InterpolateVignetting(d->settings.focalLength,
609 d->settings.aperture,
610 d->settings.subjectDistance, res);
611 }
612
supportsGeometry() const613 bool LensFunIface::supportsGeometry() const
614 {
615 return supportsDistortion();
616 }
617
lensFunVersion()618 QString LensFunIface::lensFunVersion()
619 {
620 return QString::fromLatin1("%1.%2.%3-%4").arg(LF_VERSION_MAJOR)
621 .arg(LF_VERSION_MINOR)
622 .arg(LF_VERSION_MICRO)
623 .arg(LF_VERSION_BUGFIX);
624 }
625
626 // Inspired by https://www.qtcentre.org/threads/49601-String-similarity-check
627
checkSimilarity(const QString & a,const QString & b) const628 double LensFunIface::checkSimilarity(const QString& a, const QString& b) const
629 {
630 if (a.isEmpty() || b.isEmpty())
631 {
632 return 0.0;
633 }
634
635 const int chars = 3;
636 int counter = 0;
637
638 QString spaces = QString::fromLatin1(" ").repeated(chars - 1);
639 QString aa = spaces + a + spaces;
640 QString bb = spaces + b + spaces;
641
642 for (int i = 0 ; i < (aa.count() - (chars - 1)) ; ++i)
643 {
644 QString part = aa.mid(i, chars);
645
646 if (bb.contains(part, Qt::CaseInsensitive))
647 {
648 ++counter;
649 }
650 }
651
652 QString s = (aa.length() < bb.length()) ? aa : bb;
653
654 return (100.0 * counter / (s.length() - (chars - 1)));
655 }
656
657 // Restore warnings
658
659 #if defined(Q_CC_GNU)
660 # pragma GCC diagnostic pop
661 #endif
662
663 #if defined(Q_CC_CLANG)
664 # pragma clang diagnostic pop
665 #endif
666
667 } // namespace Digikam
668