Commit 04c4024d authored by Gilles Caulier's avatar Gilles Caulier 🗼
Browse files

Apply patches #102120 and #102121 from Mario Frank

102120: Extended the duplicates search list view. Now, the average
similarity of the found duplicates (excluding the original image) is shown as
table column. Sorting the result set by the average similarity is thus
possible. To implement this feature, the haariface had to be modified. It
returns a map of average similarities to a map of image ids to the set of
similar images instead of the map of image ids to the set of similar images.
Communicating the average similarity to the search list view was not possible
via slots and signals and this would have lead to sending a map of image ids
to average similarities and then distributing the appropriate average
similarity to the correct FindDuplicateAlbumItem. Instead, the average
similarity is communicated via the SearchXml-query as a field of the group.
This way, the correct item gets the correct similarity automatically. The
evaluation of the new field by an SQL query is surpressed by the introduction
of noEffect fields which need to have a prefix "noeffect_". So, the log is
not polluted by unnecessary debug information.

102121: The items in the FindDuplicatesAlbum were sorted by
lexicographic order which does not make sense for the average similarity
column (e.g. 100.00 is not correctly sorted). Thus, the less than operator
was adopted such that for the average similarity column, arithmetic order is
used. To make the code more stable against regressions due to reordering the
columns, an enum was introduced.

BUGS: 372217
FIXED-IN: 5.4.0
CCMAIL: frank@uni-potsdam.de
parent a5031724
......@@ -423,7 +423,7 @@ QList<qlonglong> HaarIface::bestMatchesForImage(qlonglong imageid, int numberOfR
return bestMatches(&sig, numberOfResults, type);
}
QList<qlonglong> HaarIface::bestMatchesForImageWithThreshold(qlonglong imageid, double requiredPercentage,
QPair<double,QList<qlonglong>> HaarIface::bestMatchesForImageWithThreshold(qlonglong imageid, double requiredPercentage,
double maximumPercentage, SketchType type)
{
if ( !d->useSignatureCache || (d->signatureCache->isEmpty() && d->useSignatureCache) )
......@@ -432,7 +432,7 @@ QList<qlonglong> HaarIface::bestMatchesForImageWithThreshold(qlonglong imageid,
if (!retrieveSignatureFromDB(imageid, &sig))
{
return QList<qlonglong>();
return QPair<double,QList<qlonglong>>();
}
return bestMatchesWithThreshold(imageid, &sig, requiredPercentage, maximumPercentage, type);
......@@ -526,7 +526,7 @@ QList<qlonglong> HaarIface::bestMatches(Haar::SignatureData* const querySig, int
return bestMatches.values();
}
QList<qlonglong> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::SignatureData* const querySig, double requiredPercentage, double maximumPercentage, SketchType type)
QPair<double,QList<qlonglong>> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::SignatureData* const querySig, double requiredPercentage, double maximumPercentage, SketchType type)
{
QMap<qlonglong, double> scores = searchDatabase(querySig, type);
double lowest, highest;
......@@ -541,7 +541,8 @@ QList<qlonglong> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::Sig
double requiredScore = lowest + scoreRange * percentageRange;
QMultiMap<double, qlonglong> bestMatches;
double score, percentage;
double score, percentage, avgPercentage = 0.0;
QPair<double,QList<qlonglong>> result;
qlonglong id;
for (QMap<qlonglong, double>::const_iterator it = scores.constBegin(); it != scores.constEnd(); ++it)
......@@ -555,6 +556,10 @@ QList<qlonglong> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::Sig
// If the found image is the original one (check by id) or the percentage is below the maximum.
if ((id == imageid) || (percentage <= maximumPercentage)){
bestMatches.insert(percentage, id);
// If the current image is not the original, use the images similarity for the average percentage
if (id != imageid){
avgPercentage += percentage;
}
}
}
}
......@@ -562,6 +567,11 @@ QList<qlonglong> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::Sig
// Debug output
if (bestMatches.count() > 1)
{
// The average percentage is the sum of all percentages
// (without the original picture) divided by the count of pictures -1.
// Subtracting 1 is necessary since the original picture is not used for the calculation.
avgPercentage = avgPercentage / (bestMatches.count() - 1);
qCDebug(DIGIKAM_DATABASE_LOG) << "Duplicates with id and score:";
for (QMultiMap<double, qlonglong>::const_iterator it = bestMatches.constBegin(); it != bestMatches.constEnd(); ++it)
......@@ -569,9 +579,9 @@ QList<qlonglong> HaarIface::bestMatchesWithThreshold(qlonglong imageid,Haar::Sig
qCDebug(DIGIKAM_DATABASE_LOG) << it.value() << QString::number(it.key() * 100) + QLatin1Char('%');
}
}
// We may want to return the map itself, or a list with pairs id - percentage
return bestMatches.values();
result.first = avgPercentage;
result.second = bestMatches.values();
return result;
}
/// This method is the core functionality: It assigns a score to every image in the db
......@@ -758,22 +768,32 @@ void HaarIface::rebuildDuplicatesAlbums(const QList<int>& albums2Scan, const QLi
double requiredPercentage, double maximumPercentage, HaarProgressObserver* const observer)
{
// Carry out search. This takes long.
QMap< qlonglong, QList<qlonglong> > results = findDuplicatesInAlbumsAndTags(albums2Scan, tags2Scan, requiredPercentage, maximumPercentage, observer);
QMap< double,QMap< qlonglong,QList<qlonglong> > > results = findDuplicatesInAlbumsAndTags(albums2Scan, tags2Scan, requiredPercentage, maximumPercentage, observer);
// Build search XML from the results. Store list of ids of similar images.
QMap<QString, QString> queries;
for (QMap< qlonglong, QList<qlonglong> >::const_iterator it = results.constBegin(); it != results.constEnd(); ++it)
{
SearchXmlWriter writer;
writer.writeGroup();
writer.writeField(QLatin1String("imageid"), SearchXml::OneOf);
writer.writeValue(it.value());
writer.finishField();
writer.finishGroup();
writer.finish();
// Use the id of the first duplicate as name of the search
queries.insert(QString::number(it.key()), writer.xml());
// Iterate over the similarity
for (QMap< double,QMap< qlonglong,QList<qlonglong> > >::const_iterator similarity_it = results.constBegin(); similarity_it != results.constEnd(); ++similarity_it)
{
double similarity = similarity_it.key() * 100;
QMap<qlonglong,QList<qlonglong>> sameSimilarityMap = similarity_it.value();
// Iterate ofer
for (QMap< qlonglong,QList<qlonglong> >::const_iterator it = sameSimilarityMap.constBegin(); it != sameSimilarityMap.constEnd(); ++it){
SearchXmlWriter writer;
writer.writeGroup();
writer.writeField(QLatin1String("imageid"), SearchXml::OneOf);
writer.writeValue(it.value());
writer.finishField();
// Add the average similarity as field
writer.writeField(QLatin1String("noeffect_avgsim"), SearchXml::Equal);
writer.writeValue(similarity);
writer.finishField();
writer.finishGroup();
writer.finish();
// Use the id of the first duplicate as name of the search
queries.insert(QString::number(it.key()), writer.xml());
}
}
// Write search albums to database
......@@ -792,7 +812,7 @@ void HaarIface::rebuildDuplicatesAlbums(const QList<int>& albums2Scan, const QLi
}
}
QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicatesInAlbums(const QList<int>& albums2Scan,
QMap< double,QMap< qlonglong,QList<qlonglong> > > HaarIface::findDuplicatesInAlbums(const QList<int>& albums2Scan,
double requiredPercentage,
double maximumPercentage,
HaarProgressObserver* const observer)
......@@ -808,7 +828,7 @@ QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicatesInAlbums(const QLis
return findDuplicates(idList, requiredPercentage, maximumPercentage, observer);
}
QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicatesInAlbumsAndTags(const QList<int>& albums2Scan,
QMap< double,QMap< qlonglong,QList<qlonglong> > > HaarIface::findDuplicatesInAlbumsAndTags(const QList<int>& albums2Scan,
const QList<int>& tags2Scan,
double requiredPercentage,
double maximumPercentage,
......@@ -831,14 +851,15 @@ QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicatesInAlbumsAndTags(con
return findDuplicates(idList, requiredPercentage, maximumPercentage, observer);
}
QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicates(const QSet<qlonglong>& images2Scan,
QMap< double,QMap< qlonglong,QList<qlonglong> > > HaarIface::findDuplicates(const QSet<qlonglong>& images2Scan,
double requiredPercentage,
double maximumPercentage,
HaarProgressObserver* const observer)
{
QMap< qlonglong, QList<qlonglong> > resultsMap;
QMap<double,QMap<qlonglong,QList<qlonglong>>> resultsMap;
QMap<double,QMap<qlonglong,QList<qlonglong>>>::iterator similarity_it;
QSet<qlonglong>::const_iterator it;
QList<qlonglong> bestMatchesList;
QPair<double,QList<qlonglong>> bestMatches;
QSet<qlonglong> resultsCandidates;
int total = 0;
......@@ -860,16 +881,25 @@ QMap< qlonglong, QList<qlonglong> > HaarIface::findDuplicates(const QSet<qlonglo
if (!resultsCandidates.contains(*it))
{
// find images with required similarity
bestMatchesList = bestMatchesForImageWithThreshold(*it, requiredPercentage, maximumPercentage, ScannedSketch);
if (!bestMatchesList.isEmpty())
bestMatches = bestMatchesForImageWithThreshold(*it, requiredPercentage, maximumPercentage, ScannedSketch);
if (!bestMatches.second.isEmpty())
{
// the list will usually contain one image: the original. Filter out.
if (!(bestMatchesList.count() == 1 && bestMatchesList.first() == *it))
if (!(bestMatches.second.count() == 1 && bestMatches.second.first() == *it))
{
resultsMap.insert(*it, bestMatchesList);
// make a lookup for the average similarity
similarity_it = resultsMap.find(bestMatches.first);
// If there is an entry for this similarity, add the result set. Else, create a new similarity entry.
if (similarity_it != resultsMap.end()){
similarity_it->insert(*it,bestMatches.second);
} else {
QMap<qlonglong,QList<qlonglong>> result;
result.insert(*it, bestMatches.second);
resultsMap.insert(bestMatches.first,result);
}
resultsCandidates << *it;
resultsCandidates.unite(bestMatchesList.toSet());
resultsCandidates.unite(bestMatches.second.toSet());
}
}
}
......
......@@ -97,7 +97,7 @@ public:
* All matches with a similarity in a given threshold interval are returned.
* The threshold is in the range requiredPercentage..maximumPercentage.
*/
QList<qlonglong> bestMatchesForImageWithThreshold(qlonglong imageid,
QPair<double,QList<qlonglong>> bestMatchesForImageWithThreshold(qlonglong imageid,
double requiredPercentage, double maximumPercentage, SketchType type=ScannedSketch);
/** Calculates the Haar signature, bring it in a form as stored in the DB,
......@@ -116,15 +116,15 @@ public:
* All images are referenced by id from database.
* The threshold is in the range 0..1, with 1 meaning identical signature.
*/
QMap< qlonglong, QList<qlonglong> > findDuplicates(const QSet<qlonglong>& images2Scan, double requiredPercentage,
QMap< double,QMap< qlonglong,QList<qlonglong> > > findDuplicates(const QSet<qlonglong>& images2Scan, double requiredPercentage,
double maximumPercentage, HaarProgressObserver* const observer = 0);
/** Calls findDuplicates with all images in the given album ids */
QMap< qlonglong, QList<qlonglong> > findDuplicatesInAlbums(const QList<int>& albums2Scan, double requiredPercentage,
QMap< double,QMap< qlonglong,QList<qlonglong> > > findDuplicatesInAlbums(const QList<int>& albums2Scan, double requiredPercentage,
double maximumPercentage, HaarProgressObserver* const observer = 0);
/** Calls findDuplicates with all images in the given album and tag ids */
QMap< qlonglong, QList<qlonglong> > findDuplicatesInAlbumsAndTags(const QList<int>& albums2Scan,
QMap< double,QMap< qlonglong,QList<qlonglong> > > findDuplicatesInAlbumsAndTags(const QList<int>& albums2Scan,
const QList<int>& tags2Scan,
double requiredPercentage,
double maximumPercentage,
......@@ -154,7 +154,7 @@ private:
bool indexImage(qlonglong imageid);
QList<qlonglong> bestMatches(Haar::SignatureData* const data, int numberOfResults, SketchType type);
QList<qlonglong> bestMatchesWithThreshold(qlonglong imageid,Haar::SignatureData* const querySig,
QPair<double,QList<qlonglong>> bestMatchesWithThreshold(qlonglong imageid,Haar::SignatureData* const querySig,
double requiredPercentage, double maximumPercentage, SketchType type);
QMap<qlonglong, double> searchDatabase(Haar::SignatureData* const data, SketchType type);
......
......@@ -808,7 +808,7 @@ void ImageLister::listHaarSearch(ImageListerReceiver* const receiver, const QStr
iface.setAlbumRootsToSearch(albumRootsToList());
}
list = iface.bestMatchesForImageWithThreshold(id, threshold,maxThreshold, sketchType);
list = iface.bestMatchesForImageWithThreshold(id, threshold,maxThreshold, sketchType).second;
}
listFromIdList(receiver, list);
......
......@@ -785,7 +785,11 @@ bool ImageQueryBuilder::buildField(QString& sql, SearchXmlCachingReader& reader,
SearchXml::Relation relation = reader.fieldRelation();
FieldQueryBuilder fieldQuery(sql, reader, boundValues, hooks, relation);
if (name == QLatin1String("albumid"))
// First catch all noeffect fields. Those are only used for message passing when no Signal-Slot-communication is possible
if (name.startsWith(QLatin1String("noeffect_"))){
return false;
}
else if (name == QLatin1String("albumid"))
{
if (relation == SearchXml::Equal || relation == SearchXml::Unequal)
{
......
......@@ -69,10 +69,11 @@ FindDuplicatesAlbum::FindDuplicatesAlbum(QWidget* const parent)
setAllColumnsShowFocus(true);
setIconSize(QSize(d->iconSize, d->iconSize));
setSortingEnabled(true);
setColumnCount(2);
setHeaderLabels(QStringList() << i18n("Ref. images") << i18n("Items"));
setColumnCount(3);
setHeaderLabels(QStringList() << i18n("Ref. images") << i18n("Items") << i18n("Avg. similarity"));
header()->setSectionResizeMode(0, QHeaderView::Stretch);
header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
setWhatsThis(i18n("This shows all found duplicate items."));
connect(d->thumbLoadThread, SIGNAL(signalThumbnailLoaded(LoadingDescription,QPixmap)),
......
......@@ -34,6 +34,7 @@
#include "album.h"
#include "coredbsearchxml.h"
namespace Digikam
{
......@@ -63,13 +64,26 @@ FindDuplicatesAlbumItem::FindDuplicatesAlbumItem(QTreeWidget* const parent, SAlb
if (d->album)
{
d->refImgInfo = ImageInfo(d->album->title().toLongLong());
setText(0, d->refImgInfo.name());
setText(Column::REFERENCE_IMAGE, d->refImgInfo.name());
SearchXmlReader reader(d->album->query());
reader.readToFirstField();
QList<int> list;
list << reader.valueToIntList();
setText(1, QString::number(list.count()));
setText(Column::RESULT_COUNT, QString::number(list.count()));
double avgSim = 0.00;
SearchXml::Element element;
while ( (element = reader.readNext()) != SearchXml::End )
{
if ( (element == SearchXml::Field) && (reader.fieldName().compare("noeffect_avgsim") == 0) )
{
avgSim = reader.valueToDouble();
}
}
setText(Column::AVG_SIMILARITY,QString::number(avgSim,'f',2));
}
setThumb(QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(parent->iconSize().width(), QIcon::Disabled), false);
......@@ -102,7 +116,7 @@ void FindDuplicatesAlbumItem::setThumb(const QPixmap& pix, bool hasThumb)
icon.addPixmap(pixmap, QIcon::Active, QIcon::Off);
icon.addPixmap(pixmap, QIcon::Normal, QIcon::On);
icon.addPixmap(pixmap, QIcon::Normal, QIcon::Off);
setIcon(0, icon);
setIcon(Column::REFERENCE_IMAGE, icon);
d->hasThumb = hasThumb;
}
......@@ -120,7 +134,16 @@ QUrl FindDuplicatesAlbumItem::refUrl() const
bool FindDuplicatesAlbumItem::operator<(const QTreeWidgetItem& other) const
{
int column = treeWidget()->sortColumn();
int result = QCollator().compare(text(column), other.text(column));
int result = 0;
if (column == Column::AVG_SIMILARITY)
{
result = ( text(column).toDouble() < other.text(column).toDouble() ) ? -1 : 0;
}
else
{
result = QCollator().compare(text(column), other.text(column));
}
if (result < 0)
{
......
......@@ -42,6 +42,15 @@ class SAlbum;
class FindDuplicatesAlbumItem : public QTreeWidgetItem
{
public:
enum Column
{
REFERENCE_IMAGE = 0,
RESULT_COUNT = 1,
AVG_SIMILARITY = 2
};
public:
FindDuplicatesAlbumItem(QTreeWidget* const parent, SAlbum* const album);
......
......@@ -80,7 +80,7 @@ public:
QSpinBox* minSimilarity;
QSpinBox* maxSimilarity;
QPushButton* scanDuplicatesBtn;
QPushButton* updateFingerPrtBtn;
......@@ -128,10 +128,10 @@ FindDuplicatesView::FindDuplicatesView(QWidget* const parent)
d->maxSimilarity->setValue(100);
d->maxSimilarity->setSingleStep(1);
d->maxSimilarity->setSuffix(QLatin1String("%"));
d->similarityLabel = new QLabel(i18n("Similarity:"));
d->similarityLabel->setBuddy(d->minSimilarity);
d->similarityIntervalLabel = new QLabel("-");
// ---------------------------------------------------------------
......@@ -179,7 +179,7 @@ FindDuplicatesView::FindDuplicatesView(QWidget* const parent)
connect(AlbumManager::instance(), SIGNAL(signalAlbumsCleared()),
this, SLOT(slotClear()));
connect(d->minSimilarity, SIGNAL(valueChanged(int)),this,SLOT(slotMinimumChanged(int)));
}
......
......@@ -75,8 +75,6 @@
#include "dcolorvalueselector.h"
#include "dexpanderbox.h"
namespace Digikam
{
......@@ -207,7 +205,8 @@ const QString FuzzySearchView::Private::configSimilarsMaxThresholdEntry(QLatin1S
FuzzySearchView::FuzzySearchView(SearchModel* const searchModel,
SearchModificationHelper* const searchModificationHelper,
QWidget* const parent)
: QScrollArea(parent), StateSavingObject(this),
: QScrollArea(parent),
StateSavingObject(this),
d(new Private)
{
const int spacing = QApplication::style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing);
......@@ -880,11 +879,11 @@ void FuzzySearchView::dropEvent(QDropEvent* e)
if (DItemDrag::canDecode(e->mimeData()))
{
QList<QUrl> urls;
QList<QUrl> kioURLs;
QList<QUrl> ioURLs;
QList<int> albumIDs;
QList<qlonglong> imageIDs;
if (!DItemDrag::decode(e->mimeData(), urls, kioURLs, albumIDs, imageIDs))
if (!DItemDrag::decode(e->mimeData(), urls, ioURLs, albumIDs, imageIDs))
{
return;
}
......@@ -922,9 +921,12 @@ void FuzzySearchView::slotMaxLevelImageChanged(int newValue)
void FuzzySearchView::slotLevelImageChanged(int newValue)
{
d->maxLevelImage->setMinimum(newValue);
if (newValue > d->maxLevelImage->value()){
if (newValue > d->maxLevelImage->value())
{
d->maxLevelImage->setValue(newValue);
}
if (d->timerImage)
{
d->timerImage->stop();
......@@ -939,6 +941,7 @@ void FuzzySearchView::slotLevelImageChanged(int newValue)
d->timerImage->setSingleShot(true);
d->timerImage->setInterval(500);
}
d->timerImage->start();
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment