CmdMain.cpp 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #include <QCoreApplication>
  2. #include <QDirIterator>
  3. #include <QProcess>
  4. #include <QSettings>
  5. #include <QSharedMemory>
  6. #include <QStringList>
  7. #include <QThread>
  8. #include <chrono>
  9. #include <filesystem>
  10. #include <iostream>
  11. #include <unordered_map>
  12. #include "DatabaseManager.hpp"
  13. #include "ImageHasher.hpp"
  14. #include <opencv2/core/utils/logger.hpp>
  15. void workerAdd(const QStringList &dirs);
  16. void cmdAdd(const QStringList &dirs);
  17. void cmdTh(int thValue);
  18. void cmdSearch(const QString &imageFile);
  19. void cmdStrict(bool strict);
  20. int main(int argc, char *argv[]) {
  21. // OpenCVの不要なINFOログ(並列バックエンド読み込み失敗など)を抑制する
  22. cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
  23. QCoreApplication app(argc, argv);
  24. QStringList args = app.arguments();
  25. if (args.size() < 2) {
  26. std::cerr << "Usage: DupFindCmd add <dir1> <dir2> ...\n"
  27. << " DupFindCmd th <N>\n"
  28. << " DupFindCmd search <image_file>\n"
  29. << " DupFindCmd strict [on|off]\n";
  30. return 1;
  31. }
  32. QString command = args[1];
  33. if (command == "--worker-add") {
  34. QStringList dirs = args.mid(2);
  35. workerAdd(dirs);
  36. return 0;
  37. } else if (command == "add") {
  38. if (args.size() < 3) {
  39. std::cerr << "Error: 'add' Requires at least one directory.\n";
  40. return 1;
  41. }
  42. cmdAdd(args.mid(2));
  43. return 0;
  44. } else if (command == "th") {
  45. if (args.size() != 3) {
  46. std::cerr << "Error: 'th' Requires exactly one integer.\n";
  47. return 1;
  48. }
  49. bool ok;
  50. int val = args[2].toInt(&ok);
  51. if (!ok || val < 0 || val > 32) {
  52. std::cerr << "Error: N must be an integer between 0 and 32.\n";
  53. return 1;
  54. }
  55. cmdTh(val);
  56. return 0;
  57. } else if (command == "search") {
  58. if (args.size() != 3) {
  59. std::cerr << "Error: 'search' Requires exactly one image file.\n";
  60. return 1;
  61. }
  62. cmdSearch(args[2]);
  63. return 0;
  64. } else if (command == "strict") {
  65. if (args.size() != 3) {
  66. std::cerr << "Error: 'strict' Requires exactly one argument.\n";
  67. return 1;
  68. }
  69. bool val = (args[2] == "on");
  70. cmdStrict(val);
  71. return 0;
  72. } else {
  73. std::cerr << "Unknown command: " << command.toStdString() << "\n";
  74. return 1;
  75. }
  76. }
  77. void cmdTh(int thValue) {
  78. QString iniPath = QCoreApplication::applicationDirPath() + "/DupFind.ini";
  79. QSettings settings(iniPath, QSettings::IniFormat);
  80. settings.setValue("threshold", thValue);
  81. std::cout << "Successfully updated threshold to " << thValue << ".\n";
  82. }
  83. // GUI以外からディレクトリ追加を指示するコマンド
  84. // 設定ファイル(INI)を更新した上で、バックグラウンドのワーカプロセスをデタッチ起動する
  85. void cmdAdd(const QStringList &dirs) {
  86. QString iniPath = QCoreApplication::applicationDirPath() + "/DupFind.ini";
  87. QSettings settings(iniPath, QSettings::IniFormat);
  88. QStringList existingDirs = settings.value("directories").toStringList();
  89. bool changed = false;
  90. for (const QString &dir : dirs) {
  91. if (!existingDirs.contains(
  92. dir, Qt::CaseInsensitive)) { // 念のためWindowsなど考慮
  93. existingDirs.append(dir);
  94. changed = true;
  95. }
  96. }
  97. if (changed) {
  98. settings.setValue("directories", existingDirs);
  99. }
  100. std::cout << "Started background scan.\n";
  101. // デタッチしてバックグラウンドプロセスを起動
  102. QString program = QCoreApplication::applicationFilePath();
  103. QStringList workerArgs;
  104. workerArgs << "--worker-add" << dirs;
  105. QProcess::startDetached(program, workerArgs);
  106. }
  107. // バックグラウンドで画像ファイルのハッシュ値(dHash, pHash)を計算し、DBへ書き込む処理
  108. // GUI動作との競合を避ける配慮などが実装されている
  109. void workerAdd(const QStringList &dirs) {
  110. // バックグラウンド処理のためCPU優先度を下げる
  111. QThread::currentThread()->setPriority(QThread::IdlePriority);
  112. // GUIが起動しているか確認するための共有メモリ
  113. QSharedMemory sharedMem("DupFind_GUI_Instance");
  114. DatabaseManager dbManager("dupfind_cache.db");
  115. if (!dbManager.open())
  116. return;
  117. auto cachedList = dbManager.getAllImages();
  118. std::unordered_map<std::string, ImageData> cache;
  119. for (const auto &img : cachedList) {
  120. cache[img.path] = img;
  121. }
  122. const QStringList filters = {"*.jpg", "*.png", "*.jpeg", "*.bmp",
  123. "*.webp", "*.tiff", "*.heic", "*.heif"};
  124. int count = 0;
  125. for (const QString &dirPath : dirs) {
  126. QDirIterator it(dirPath, filters, QDir::Files | QDir::NoSymLinks,
  127. QDirIterator::Subdirectories);
  128. while (it.hasNext()) {
  129. std::string stdPath = it.next().toStdString();
  130. // 少し粒度を荒くしてGUI起動チェック (10ファイルごと)
  131. if (count % 10 == 0) {
  132. if (sharedMem.attach()) {
  133. // GUIが共有メモリを確保している=起動中
  134. sharedMem.detach();
  135. return; // 即座に終了
  136. }
  137. }
  138. count++;
  139. try {
  140. std::error_code ec;
  141. auto currentSize = std::filesystem::file_size(stdPath, ec);
  142. auto currentMtime = std::chrono::duration_cast<std::chrono::seconds>(
  143. std::filesystem::last_write_time(stdPath, ec)
  144. .time_since_epoch())
  145. .count();
  146. auto cacheIt = cache.find(stdPath);
  147. if (cacheIt != cache.end()) {
  148. if (cacheIt->second.file_size == static_cast<int64_t>(currentSize) &&
  149. cacheIt->second.timestamp == static_cast<int64_t>(currentMtime)) {
  150. continue; // すでに最新ハッシュ計算済み
  151. }
  152. }
  153. cv::Mat img = ImageHasher::loadImage(stdPath);
  154. if (img.empty())
  155. continue;
  156. ImageData data;
  157. data.path = stdPath;
  158. data.dhash = ImageHasher::calculateDHash(img);
  159. data.phash = ImageHasher::calculatePHash(img);
  160. data.timestamp = currentMtime;
  161. data.file_size = currentSize;
  162. data.is_searched = false;
  163. dbManager.addImage(data);
  164. } catch (...) {
  165. // ファイルIOエラー等はスキップ
  166. }
  167. }
  168. }
  169. }
  170. // 指定された1つの画像ファイルと類似する「DB上の全ての画像」を検索してパスを出力するコマンド
  171. void cmdSearch(const QString &imageFile) {
  172. std::string stdPath = imageFile.toStdString();
  173. if (!std::filesystem::exists(stdPath)) {
  174. return;
  175. }
  176. cv::Mat img = ImageHasher::loadImage(stdPath);
  177. if (img.empty()) {
  178. return;
  179. }
  180. uint64_t dhash = ImageHasher::calculateDHash(img);
  181. uint64_t phash = ImageHasher::calculatePHash(img);
  182. QString iniPath = QCoreApplication::applicationDirPath() + "/DupFind.ini";
  183. QSettings settings(iniPath, QSettings::IniFormat);
  184. int threshold = settings.value("threshold", 10).toInt();
  185. bool strict = settings.value("strict_mode", false).toBool();
  186. DatabaseManager dbManager("dupfind_cache.db");
  187. if (!dbManager.open())
  188. return;
  189. auto allImages = dbManager.getAllImages();
  190. for (const auto &imgData : allImages) {
  191. int distD = ImageHasher::hammingDistance(dhash, imgData.dhash);
  192. int distP = ImageHasher::hammingDistance(phash, imgData.phash);
  193. bool similar = strict ? (distD <= threshold && distP <= threshold)
  194. : (distD <= threshold || distP <= threshold);
  195. if (similar) {
  196. std::cout << imgData.path << "\n";
  197. }
  198. }
  199. }
  200. void cmdStrict(bool strict) {
  201. QString iniPath = QCoreApplication::applicationDirPath() + "/DupFind.ini";
  202. QSettings settings(iniPath, QSettings::IniFormat);
  203. settings.setValue("strict_mode", strict);
  204. std::cout << "Successfully updated strict mode to " << (strict ? "on" : "off")
  205. << ".\n";
  206. }