mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
Dive pictures: find moved pictures based on filename
Users might have edited their pictures. Therefore, instead of identifying pictures by the hash of the file-content, use the file path. The match between original and new filename is graded by a score. Currently, this is the number of path components that match, starting from the filename. Camparison is case-insensitive. After having identified the matching images, write the caches so that they are saved even if the user doesn't cleanly quit the application. Since the new code uses significantly less resources, it can be run in a single background thread. Thus, the multi-threading can be simplified. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
This commit is contained in:
parent
08962cb38d
commit
0646b41275
5 changed files with 80 additions and 34 deletions
|
@ -1277,37 +1277,95 @@ QStringList imageExtensionFilters() {
|
||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works on a copy of the string, because it runs in asynchronous context
|
// Compare two full paths and return the number of matching levels, starting from the filename.
|
||||||
static void learnImage(QString filename)
|
// String comparison is case-insensitive.
|
||||||
|
static int matchFilename(const QString &path1, const QString &path2)
|
||||||
{
|
{
|
||||||
|
QFileInfo f1(path1);
|
||||||
|
QFileInfo f2(path2);
|
||||||
|
|
||||||
|
int score = 0;
|
||||||
|
for (;;) {
|
||||||
|
QString fn1 = f1.fileName();
|
||||||
|
QString fn2 = f2.fileName();
|
||||||
|
if (fn1.isEmpty() || fn2.isEmpty())
|
||||||
|
break;
|
||||||
|
if (fn1 == ".") {
|
||||||
|
f1 = QFileInfo(f1.path());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (fn2 == ".") {
|
||||||
|
f2 = QFileInfo(f2.path());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (QString::compare(fn1, fn2, Qt::CaseInsensitive) != 0)
|
||||||
|
break;
|
||||||
|
f1 = QFileInfo(f1.path());
|
||||||
|
f2 = QFileInfo(f2.path());
|
||||||
|
++score;
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImageMatch {
|
||||||
|
QString localFilename;
|
||||||
|
int score;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void learnImage(const QString &filename, QMap<QString, ImageMatch> &matches)
|
||||||
|
{
|
||||||
|
// Find the original filenames with the highest match-score
|
||||||
|
QStringList newMatches;
|
||||||
QByteArray hash = hashFile(filename);
|
QByteArray hash = hashFile(filename);
|
||||||
// TODO: This is inefficient: we search the hash map by value. But firstly,
|
int bestScore = 1;
|
||||||
// this is running in asynchronously, so it doesn't block the UI. Secondly,
|
|
||||||
// we might not want to learn pictures by hash anyway (the user might have
|
|
||||||
// edited the picture, which changes the hash.
|
|
||||||
for (auto it = hashOf.cbegin(); it != hashOf.cend(); ++it) {
|
for (auto it = hashOf.cbegin(); it != hashOf.cend(); ++it) {
|
||||||
if (it.value() == hash)
|
int score = matchFilename(filename, it.key());
|
||||||
learnPictureFilename(it.key(), filename);
|
if (score < bestScore)
|
||||||
|
continue;
|
||||||
|
if (score > bestScore)
|
||||||
|
newMatches.clear();
|
||||||
|
newMatches.append(it.key());
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new original filenames to the list of matches, if the score is higher than previously
|
||||||
|
for (const QString &originalFilename: newMatches) {
|
||||||
|
auto it = matches.find(originalFilename);
|
||||||
|
if (it == matches.end())
|
||||||
|
matches.insert(originalFilename, { filename, bestScore });
|
||||||
|
else if (it->score < bestScore)
|
||||||
|
*it = { filename, bestScore };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void learnImages(const QDir dir, int max_recursions)
|
void learnImages(const QStringList &dirNames, int max_recursions)
|
||||||
{
|
{
|
||||||
QStringList files;
|
|
||||||
QStringList filters = imageExtensionFilters();
|
QStringList filters = imageExtensionFilters();
|
||||||
|
QMap<QString, ImageMatch> matches;
|
||||||
|
|
||||||
if (max_recursions) {
|
QVector<QStringList> stack; // Use a stack to recurse into directories
|
||||||
foreach (QString dirname, dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs)) {
|
stack.reserve(max_recursions + 1);
|
||||||
learnImages(QDir(dir.filePath(dirname)), max_recursions - 1);
|
stack.append(dirNames);
|
||||||
|
while (!stack.isEmpty()) {
|
||||||
|
if (stack.last().isEmpty()) {
|
||||||
|
stack.removeLast();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QDir dir(stack.last().takeLast());
|
||||||
|
|
||||||
|
for (const QString &file: dir.entryList(filters, QDir::Files))
|
||||||
|
learnImage(dir.absoluteFilePath(file), matches);
|
||||||
|
if (stack.size() <= max_recursions) {
|
||||||
|
stack.append(QStringList());
|
||||||
|
for (const QString &dirname: dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs))
|
||||||
|
stack.last().append(dir.filePath(dirname));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto it = matches.begin(); it != matches.end(); ++it)
|
||||||
|
learnPictureFilename(it.key(), it->localFilename);
|
||||||
|
|
||||||
foreach (QString file, dir.entryList(filters, QDir::Files)) {
|
write_hashes();
|
||||||
files.append(dir.absoluteFilePath(file));
|
|
||||||
}
|
|
||||||
|
|
||||||
QtConcurrent::blockingMap(files, learnImage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" const char *local_file_path(struct picture *picture)
|
extern "C" const char *local_file_path(struct picture *picture)
|
||||||
|
|
|
@ -31,7 +31,7 @@ void updateHash(struct picture *picture);
|
||||||
QByteArray hashFile(const QString &filename);
|
QByteArray hashFile(const QString &filename);
|
||||||
QString hashString(const char *filename);
|
QString hashString(const char *filename);
|
||||||
QString thumbnailFileName(const QString &filename);
|
QString thumbnailFileName(const QString &filename);
|
||||||
void learnImages(const QDir dir, int max_recursions);
|
void learnImages(const QStringList &dirNames, int max_recursions);
|
||||||
void learnPictureFilename(const QString &originalName, const QString &localName);
|
void learnPictureFilename(const QString &originalName, const QString &localName);
|
||||||
void hashPicture(QString filename);
|
void hashPicture(QString filename);
|
||||||
extern "C" char *hashstring(const char *filename);
|
extern "C" char *hashstring(const char *filename);
|
||||||
|
|
|
@ -701,13 +701,10 @@ void MainWindow::on_actionCloudOnline_triggered()
|
||||||
updateCloudOnlineStatus();
|
updateCloudOnlineStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void learnImageDirs(QStringList dirnames)
|
static void learnImageDirs(QStringList dirnames)
|
||||||
{
|
{
|
||||||
QList<QFuture<void> > futures;
|
learnImages(dirnames, 10);
|
||||||
foreach (QString dir, dirnames) {
|
DivePictureModel::instance()->updateDivePictures();
|
||||||
futures << QtConcurrent::run(learnImages, QDir(dir), 10);
|
|
||||||
}
|
|
||||||
DivePictureModel::instance()->updateDivePicturesWhenDone(futures);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionHash_images_triggered()
|
void MainWindow::on_actionHash_images_triggered()
|
||||||
|
|
|
@ -23,14 +23,6 @@ DivePictureModel::DivePictureModel() : rowDDStart(0),
|
||||||
this, &DivePictureModel::updateThumbnail, Qt::QueuedConnection);
|
this, &DivePictureModel::updateThumbnail, Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DivePictureModel::updateDivePicturesWhenDone(QList<QFuture<void>> futures)
|
|
||||||
{
|
|
||||||
Q_FOREACH (QFuture<void> f, futures) {
|
|
||||||
f.waitForFinished();
|
|
||||||
}
|
|
||||||
updateDivePictures();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DivePictureModel::setZoomLevel(int level)
|
void DivePictureModel::setZoomLevel(int level)
|
||||||
{
|
{
|
||||||
zoomLevel = level / 10.0;
|
zoomLevel = level / 10.0;
|
||||||
|
|
|
@ -21,7 +21,6 @@ public:
|
||||||
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||||
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||||
virtual void updateDivePictures();
|
virtual void updateDivePictures();
|
||||||
void updateDivePicturesWhenDone(QList<QFuture<void>>);
|
|
||||||
void removePictures(const QVector<QString> &fileUrls);
|
void removePictures(const QVector<QString> &fileUrls);
|
||||||
int rowDDStart, rowDDEnd;
|
int rowDDStart, rowDDEnd;
|
||||||
void updateDivePictureOffset(const QString &filename, int offsetSeconds);
|
void updateDivePictureOffset(const QString &filename, int offsetSeconds);
|
||||||
|
|
Loading…
Add table
Reference in a new issue