Bladeren bron

Add: Accept Image file and Folder drop.

Satoshi Yoneda 3 weken geleden
bovenliggende
commit
9d4e6795da
3 gewijzigde bestanden met toevoegingen van 145 en 21 verwijderingen
  1. 21 15
      include/MainWindow.hpp
  2. 11 2
      src/ImageHasher.cpp
  3. 113 4
      src/MainWindow.cpp

+ 21 - 15
include/MainWindow.hpp

@@ -1,15 +1,15 @@
 #pragma once
 #include "DatabaseManager.hpp"
-#include "ResultListModel.hpp"
-#include "ResultItemDelegate.hpp"
 #include "ResultFilterProxyModel.hpp"
-#include <QLineEdit>
-#include <unordered_set>
+#include "ResultItemDelegate.hpp"
+#include "ResultListModel.hpp"
 #include <QCheckBox>
 #include <QCloseEvent>
+#include <QDropEvent>
 #include <QEvent>
 #include <QFutureWatcher>
 #include <QLabel>
+#include <QLineEdit>
 #include <QListView>
 #include <QListWidget>
 #include <QMainWindow>
@@ -19,6 +19,7 @@
 #include <QSlider>
 #include <QTimer>
 #include <memory>
+#include <unordered_set>
 #include <vector>
 
 // アプリケーションのメインウィンドウ(GUI)を管理するクラス
@@ -35,15 +36,16 @@ private slots:
   void onRemoveDirectory(); // リストからディレクトリ除外ボタン押下
   void onStartScan();       // 重複検索スキャン開始ボタン押下
   void onDeleteSelected();  // チェックされた重複画像を削除するボタン押下
-  void onThresholdChanged(int value); // 類似度しきい値スライダー変更時
-  void onStrictChanged(int state);    // Strictモードチェックボックス変更時
-  void onScanFinished();              // ディレクトリの一括スキャン完了時
-  void performAsyncSearch();          // 非同期での類似画像群の抽出実行
-  void onSearchFinished();            // 類似画像の検索完了時
-  void onClearResults();              // 結果表示のクリア
+  void onThresholdChanged(int value);    // 類似度しきい値スライダー変更時
+  void onStrictChanged(int state);       // Strictモードチェックボックス変更時
+  void onScanFinished();                 // ディレクトリの一括スキャン完了時
+  void performAsyncSearch();             // 非同期での類似画像群の抽出実行
+  void onSearchFinished();               // 類似画像の検索完了時
+  void onClearResults();                 // 結果表示のクリア
   void removeGroupFromView(int groupId); // 指定したグループをリストから除外
-  void onContextMenuRequested(const std::string& path, int groupId, const QPoint& globalPos);
-  void onFileDoubleClicked(const std::string& path);
+  void onContextMenuRequested(const std::string &path, int groupId,
+                              const QPoint &globalPos);
+  void onFileDoubleClicked(const std::string &path);
   void onSearchTextChanged(const QString &text);
 
 private:
@@ -51,8 +53,9 @@ private:
   void setupUi();      // UIのレイアウトと初期化
   void loadSettings(); // INIファイルからの設定値復元
   void saveSettings(); // INIファイルへの設定値保存
-  void updateResultGrid(const std::vector<DuplicateGroup>
-                            &groups, bool preserveState = false); // 結果グリッドへサムネイルを並べる
+  void updateResultGrid(
+      const std::vector<DuplicateGroup> &groups,
+      bool preserveState = false); // 結果グリッドへサムネイルを並べる
   std::vector<ImageData>
   getFilteredImages(); // 現在リストにあるディレクトリの画像のみ抽出
   bool
@@ -60,6 +63,8 @@ private:
               QEvent *event) override; // 右クリックメニュー等のイベントフィルタ
   void
   closeEvent(QCloseEvent *event) override; // ウィンドウ終了時の保存処理など
+  void dropEvent(QDropEvent *e) override; // ドロップを受け付ける
+  void dragEnterEvent(QDragEnterEvent *event) override; // ドロップを受け付ける
 
   // データベースマネージャーのインスタンス
   std::unique_ptr<DatabaseManager> m_dbManager;
@@ -100,5 +105,6 @@ private:
   QStringList m_loadedDirs; // 起動時にロードされたディレクトリ群
 
   std::vector<DuplicateGroup> m_currentGroups; // 現在表示中のグループ一覧
-  std::unordered_set<std::string> m_ignoredPaths; // セッション中に除外した画像のパス
+  std::unordered_set<std::string>
+      m_ignoredPaths; // セッション中に除外した画像のパス
 };

+ 11 - 2
src/ImageHasher.cpp

@@ -1,6 +1,8 @@
 #include "ImageHasher.hpp"
 #include <bit> // C++20 std::popcount
 #include <opencv2/imgproc.hpp>
+#include <QFile>
+#include <QByteArray>
 
 // 画像の輝度勾配(隣り合うピクセルの明暗)からハッシュを計算する(Difference
 // Hash) 処理が軽く、単純なリサイズ等に強い特徴がある
@@ -70,8 +72,15 @@ int ImageHasher::hammingDistance(uint64_t h1, uint64_t h2) {
 */
 
 cv::Mat ImageHasher::loadImage(const std::string &path, int targetSize) {
-  // Load with IMREAD_COLOR
-  cv::Mat img = cv::imread(path, cv::IMREAD_COLOR);
+  // Windowsでの日本語パス対応のため、QFileで読み込んでからimdecodeする
+  QFile file(QString::fromStdString(path));
+  if (!file.open(QIODevice::ReadOnly))
+    return cv::Mat();
+
+  QByteArray data = file.readAll();
+  cv::Mat img = cv::imdecode(cv::Mat(1, data.size(), CV_8U, data.data()),
+                             cv::IMREAD_COLOR);
+
   if (img.empty())
     return cv::Mat();
 

+ 113 - 4
src/MainWindow.cpp

@@ -8,6 +8,7 @@
 #include <QHBoxLayout>
 #include <QMessageBox>
 #include <QPixmap>
+#include <QUrl>
 #include <QVBoxLayout>
 #include <chrono>
 #include <filesystem>
@@ -20,6 +21,7 @@
 #include <QDesktopServices>
 #include <QImageReader>
 #include <QMenu>
+#include <QMimeData>
 #include <QPointer>
 #include <QSettings>
 #include <QStandardPaths>
@@ -175,6 +177,9 @@ void MainWindow::setupUi() {
 
   setCentralWidget(central);
 
+  // ドロップを受け付ける
+  setAcceptDrops(true);
+
   // Connections
   connect(m_addDirBtn, &QPushButton::clicked, this,
           &MainWindow::onAddDirectory);
@@ -248,6 +253,105 @@ void MainWindow::closeEvent(QCloseEvent *event) {
   QMainWindow::closeEvent(event);
 }
 
+void MainWindow::dragEnterEvent(QDragEnterEvent *event) {
+  if (event->mimeData()->hasUrls()) {
+    event->acceptProposedAction();
+  }
+}
+
+// ドロップを受け付ける
+void MainWindow::dropEvent(QDropEvent *e) {
+  const QMimeData *mimeData = e->mimeData();
+  if (!mimeData->hasUrls())
+    return;
+
+  std::vector<DuplicateGroup> results;
+  auto candidates = getFilteredImages();
+  bool dirAdded = false;
+
+  for (const QUrl &url : mimeData->urls()) {
+    QString localPath = url.toLocalFile();
+    if (localPath.isEmpty())
+      continue;
+
+    QFileInfo fileInfo(localPath);
+    if (fileInfo.isDir()) {
+      // 重複チェック
+      bool exists = false;
+      for (int i = 0; i < m_dirList->count(); ++i) {
+        if (m_dirList->item(i)->text() == localPath) {
+          exists = true;
+          break;
+        }
+      }
+      if (!exists) {
+        auto *item = new QListWidgetItem(localPath, m_dirList);
+        item->setToolTip(localPath);
+        dirAdded = true;
+      }
+    } else if (fileInfo.isFile()) {
+      cv::Mat img = ImageHasher::loadImage(localPath.toStdString());
+      if (img.empty())
+        continue;
+      
+      // ここでクリア
+      m_model->clear();
+
+      uint64_t dhash = ImageHasher::calculateDHash(img);
+      uint64_t phash = ImageHasher::calculatePHash(img);
+
+      DuplicateGroup foundGroup;
+      // ドロップされた画像自身を最初に追加
+      ImageData dropped;
+      dropped.path = localPath.toStdString();
+      dropped.dhash = dhash;
+      dropped.phash = phash;
+      dropped.file_size = fileInfo.size();
+      dropped.timestamp = fileInfo.lastModified().toSecsSinceEpoch();
+      foundGroup.images.push_back(dropped);
+
+      for (const auto &cand : candidates) {
+        // 自分自身(パスが同じ)は除外
+        if (cand.path == dropped.path)
+          continue;
+
+        bool match = false;
+        if (m_strictMode) {
+          if (ImageHasher::hammingDistance(dhash, cand.dhash) <=
+                  m_currentThreshold &&
+              ImageHasher::hammingDistance(phash, cand.phash) <=
+                  m_currentThreshold) {
+            match = true;
+          }
+        } else {
+          if (ImageHasher::hammingDistance(dhash, cand.dhash) <=
+                  m_currentThreshold ||
+              ImageHasher::hammingDistance(phash, cand.phash) <=
+                  m_currentThreshold) {
+            match = true;
+          }
+        }
+        if (match) {
+          foundGroup.images.push_back(cand);
+        }
+      }
+
+      // 重複が見つかった場合のみ追加(自分1枚だけなら追加しない)
+      if (foundGroup.images.size() > 1) {
+        results.push_back(foundGroup);
+      }
+    }
+  }
+
+  if (!results.empty()) {
+    updateResultGrid(results);
+  }
+
+  if (dirAdded) {
+    saveSettings();
+  }
+}
+
 void MainWindow::onAddDirectory() {
   QString dir =
       QFileDialog::getExistingDirectory(this, tr("Select Directory to Scan"));
@@ -348,7 +452,8 @@ void MainWindow::onStartScan() {
     auto results = QtConcurrent::blockingMapped(allFiles, processFunc);
 
     // 4. DBへの一括保存 (メインスレッドの管理外のDB接続で行う)
-    QString dataPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
+    QString dataPath =
+        QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
     QString dbPath = dataPath + "/dupfind_cache.db";
     DatabaseManager db(dbPath.toStdString());
     if (db.open()) {
@@ -568,7 +673,8 @@ void MainWindow::onDeleteSelected() {
   if (allCheckedInSomeGroup) {
     auto warnRes = QMessageBox::warning(
         this, tr("Warning: All images selected"),
-        tr("一部の類似画像グループで、すべての画像が削除対象としてチェックされてい"
+        tr("一部の類似画像グループで、すべての画像が削除対象としてチェックされ"
+           "てい"
            "ます。\n"
            "このまま削除すると、それらの画像ファイルはすべて失われます。\n\n"
            "本当に削除を実行しますか?"),
@@ -592,10 +698,13 @@ void MainWindow::onDeleteSelected() {
         failures.push_back(qPath);
       }
     }
+    // 壊れたサムネイルが表示される現象を抑えるため、ここでいったんCOMMITする
+    m_dbManager->commitTransaction();
 
     if (!failures.empty()) {
-      QString msg = tr("The following files could not be moved to trash and were "
-                       "NOT deleted:\n\n");
+      QString msg =
+          tr("The following files could not be moved to trash and were "
+             "NOT deleted:\n\n");
       for (const auto &f : failures) {
         msg += f + "\n";
       }