diff --git a/deploy/shitu_index_manager/README.md b/deploy/shitu_index_manager/README.md
new file mode 120000
index 0000000000000000000000000000000000000000..2e801b61cd70669dac3795e4e8ecb16ca2238b2a
--- /dev/null
+++ b/deploy/shitu_index_manager/README.md
@@ -0,0 +1 @@
+../../docs/zh_CN/inference_deployment/shitu_gallery_manager.md
\ No newline at end of file
diff --git a/deploy/shitu_index_manager/index_manager.py b/deploy/shitu_index_manager/index_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..97e3eec561cf7a45476bd750624d721dfd85fdb9
--- /dev/null
+++ b/deploy/shitu_index_manager/index_manager.py
@@ -0,0 +1,349 @@
+# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import os
+import sys
+from PyQt5 import QtCore, QtGui, QtWidgets
+import mod.mainwindow
+
+from paddleclas.deploy.utils import config, logger
+from paddleclas.deploy.python.predict_rec import RecPredictor
+from fastapi import FastAPI
+import uvicorn
+import numpy as np
+import faiss
+from typing import List
+import pickle
+import cv2
+import socket
+import json
+import operator
+from multiprocessing import Process
+"""
+完整的index库如下:
+root_path/ # 库存储目录
+|-- image_list.txt # 图像列表,每行:image_path label。由前端生成及修改。后端只读
+|-- features.pkl # 建库之后,保存的embedding向量,后端生成,前端无需操作
+|-- images # 图像存储目录,由前端生成及增删查等操作。后端只读
+| |-- md5.jpg
+| |-- md5.jpg
+| |-- ……
+|-- index # 真正的生成的index库存储目录,后端生成及操作,前端无需操作。
+| |-- vector.index # faiss生成的索引库
+| |-- id_map.pkl # 索引文件
+"""
+
+
+class ShiTuIndexManager(object):
+
+ def __init__(self, config):
+ self.root_path = None
+ self.image_list_path = "image_list.txt"
+ self.image_dir = "images"
+ self.index_path = "index/vector.index"
+ self.id_map_path = "index/id_map.pkl"
+ self.features_path = "features.pkl"
+ self.index = None
+ self.id_map = None
+ self.features = None
+ self.config = config
+ self.predictor = RecPredictor(config)
+
+ def _load_pickle(self, path):
+ if os.path.exists(path):
+ return pickle.load(open(path, 'rb'))
+ else:
+ return None
+
+ def _save_pickle(self, path, data):
+ if not os.path.exists(os.path.dirname(path)):
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'wb') as fd:
+ pickle.dump(data, fd)
+
+ def _load_index(self):
+ self.index = faiss.read_index(
+ os.path.join(self.root_path, self.index_path))
+ self.id_map = self._load_pickle(
+ os.path.join(self.root_path, self.id_map_path))
+ self.features = self._load_pickle(
+ os.path.join(self.root_path, self.features_path))
+
+ def _save_index(self, index, id_map, features):
+ faiss.write_index(index, os.path.join(self.root_path, self.index_path))
+ self._save_pickle(os.path.join(self.root_path, self.id_map_path),
+ id_map)
+ self._save_pickle(os.path.join(self.root_path, self.features_path),
+ features)
+
+ def _update_path(self, root_path, image_list_path=None):
+ if root_path == self.root_path:
+ pass
+ else:
+ self.root_path = root_path
+ if not os.path.exists(os.path.join(root_path, "index")):
+ os.mkdir(os.path.join(root_path, "index"))
+ if image_list_path is not None:
+ self.image_list_path = image_list_path
+
+ def _cal_featrue(self, image_list):
+ batch_images = []
+ featrures = None
+ cnt = 0
+ for idx, image_path in enumerate(image_list):
+ image = cv2.imread(image_path)
+ if image is None:
+ return "{} is broken or not exist. Stop"
+ else:
+ image = image[:, :, ::-1]
+ batch_images.append(image)
+ cnt += 1
+ if cnt % self.config["Global"]["batch_size"] == 0 or (
+ idx + 1) == len(image_list):
+ if len(batch_images) == 0:
+ continue
+ batch_results = self.predictor.predict(batch_images)
+ featrures = batch_results if featrures is None else np.concatenate(
+ (featrures, batch_results), axis=0)
+ batch_images = []
+ return featrures
+
+ def _split_datafile(self, data_file, image_root):
+ '''
+ data_file: image path and info, which can be splitted by spacer
+ image_root: image path root
+ delimiter: delimiter
+ '''
+ gallery_images = []
+ gallery_docs = []
+ gallery_ids = []
+ with open(data_file, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+ for _, ori_line in enumerate(lines):
+ line = ori_line.strip().split()
+ text_num = len(line)
+ assert text_num >= 2, f"line({ori_line}) must be splitted into at least 2 parts, but got {text_num}"
+ image_file = os.path.join(image_root, line[0])
+
+ gallery_images.append(image_file)
+ gallery_docs.append(ori_line.strip())
+ gallery_ids.append(os.path.basename(line[0]).split(".")[0])
+
+ return gallery_images, gallery_docs, gallery_ids
+
+ def create_index(self,
+ image_list: str,
+ index_method: str = "HNSW32",
+ image_root: str = None):
+ if not os.path.exists(image_list):
+ return "{} is not exist".format(image_list)
+ if index_method.lower() not in ['hnsw32', 'ivf', 'flat']:
+ return "The index method Only support: HNSW32, IVF, Flat"
+ self._update_path(os.path.dirname(image_list), image_list)
+
+ # get image_paths
+ image_root = image_root if image_root is not None else self.root_path
+ gallery_images, gallery_docs, image_ids = self._split_datafile(
+ image_list, image_root)
+
+ # gernerate index
+ if index_method == "IVF":
+ index_method = index_method + str(
+ min(max(int(len(gallery_images) // 32), 2), 65536)) + ",Flat"
+ index = faiss.index_factory(
+ self.config["IndexProcess"]["embedding_size"], index_method,
+ faiss.METRIC_INNER_PRODUCT)
+ self.index = faiss.IndexIDMap2(index)
+ features = self._cal_featrue(gallery_images)
+ self.index.train(features)
+ index_ids = np.arange(0, len(gallery_images)).astype(np.int64)
+ self.index.add_with_ids(features, index_ids)
+
+ self.id_map = dict()
+ for i, d in zip(list(index_ids), gallery_docs):
+ self.id_map[i] = d
+
+ self.features = {
+ "features": features,
+ "index_method": index_method,
+ "image_ids": image_ids,
+ "index_ids": index_ids.tolist()
+ }
+ self._save_index(self.index, self.id_map, self.features)
+
+ def open_index(self, root_path: str, image_list_path: str) -> str:
+ self._update_path(root_path)
+ _, _, image_ids = self._split_datafile(image_list_path, root_path)
+ if os.path.exists(os.path.join(self.root_path, self.index_path)) and \
+ os.path.exists(os.path.join(self.root_path, self.id_map_path)) and \
+ os.path.exists(os.path.join(self.root_path, self.features_path)):
+ self._update_path(root_path)
+ self._load_index()
+ if operator.eq(set(image_ids), set(self.features['image_ids'])):
+ return ""
+ else:
+ return "The image list is different from index, Please update index"
+ else:
+ return "File not exist: features.pkl, vector.index, id_map.pkl"
+
+ def update_index(self, image_list: str, image_root: str = None) -> str:
+ if self.index and self.id_map and self.features:
+ image_paths, image_docs, image_ids = self._split_datafile(
+ image_list,
+ image_root if image_root is not None else self.root_path)
+
+ # for add image
+ add_ids = list(
+ set(image_ids).difference(set(self.features["image_ids"])))
+ add_indexes = [i for i, x in enumerate(image_ids) if x in add_ids]
+ add_image_paths = [image_paths[i] for i in add_indexes]
+ add_image_docs = [image_docs[i] for i in add_indexes]
+ add_image_ids = [image_ids[i] for i in add_indexes]
+ self._add_index(add_image_paths, add_image_docs, add_image_ids)
+
+ # delete images
+ delete_ids = list(
+ set(self.features["image_ids"]).difference(set(image_ids)))
+ self._delete_index(delete_ids)
+ self._save_index(self.index, self.id_map, self.features)
+ return ""
+ else:
+ return "Failed. Please create or open index first"
+
+ def _add_index(self, image_list: List, image_docs: List, image_ids: List):
+ if len(image_ids) == 0:
+ return
+ featrures = self._cal_featrue(image_list)
+ index_ids = (np.arange(0, len(image_list)) + max(self.id_map.keys()) +
+ 1).astype(np.int64)
+ self.index.add_with_ids(featrures, index_ids)
+
+ for i, d in zip(index_ids, image_docs):
+ self.id_map[i] = d
+
+ self.features['features'] = np.concatenate(
+ [self.features['features'], featrures], axis=0)
+ self.features['image_ids'].extend(image_ids)
+ self.features['index_ids'].extend(index_ids.tolist())
+
+ def _delete_index(self, image_ids: List):
+ if len(image_ids) == 0:
+ return
+ indexes = [
+ i for i, x in enumerate(self.features['image_ids'])
+ if x in image_ids
+ ]
+ self.features["features"] = np.delete(self.features["features"],
+ indexes,
+ axis=0)
+ self.features["image_ids"] = np.delete(np.asarray(
+ self.features["image_ids"]),
+ indexes,
+ axis=0).tolist()
+ index_ids = np.delete(np.asarray(self.features["index_ids"]),
+ indexes,
+ axis=0).tolist()
+ id_map_values = [self.id_map[i] for i in index_ids]
+ self.index.reset()
+ ids = np.arange(0, len(id_map_values)).astype(np.int64)
+ self.index.add_with_ids(self.features['features'], ids)
+ self.id_map.clear()
+ for i, d in zip(ids, id_map_values):
+ self.id_map[i] = d
+ self.features["index_ids"] = ids
+
+
+app = FastAPI()
+
+
+@app.get("/new_index")
+def new_index(image_list_path: str,
+ index_method: str = "HNSW32",
+ index_root_path: str = None,
+ force: bool = False):
+ result = ""
+ try:
+ if index_root_path is not None:
+ image_list_path = os.path.join(index_root_path, image_list_path)
+ index_path = os.path.join(index_root_path, "index", "vector.index")
+ id_map_path = os.path.join(index_root_path, "index", "id_map.pkl")
+
+ if not (os.path.exists(index_path)
+ and os.path.exists(id_map_path)) or force:
+ manager.create_index(image_list_path, index_method, index_root_path)
+ else:
+ result = "There alrealy has index in {}".format(index_root_path)
+ except Exception as e:
+ result = e.__str__()
+ data = {"error_message": result}
+ return json.dumps(data).encode()
+
+
+@app.get("/open_index")
+def open_index(index_root_path: str, image_list_path: str):
+ result = ""
+ try:
+ image_list_path = os.path.join(index_root_path, image_list_path)
+ result = manager.open_index(index_root_path, image_list_path)
+ except Exception as e:
+ result = e.__str__()
+
+ data = {"error_message": result}
+ return json.dumps(data).encode()
+
+
+@app.get("/update_index")
+def update_index(image_list_path: str, index_root_path: str = None):
+ result = ""
+ try:
+ if index_root_path is not None:
+ image_list_path = os.path.join(index_root_path, image_list_path)
+ result = manager.update_index(image_list=image_list_path,
+ image_root=index_root_path)
+ except Exception as e:
+ result = e.__str__()
+ data = {"error_message": result}
+ return json.dumps(data).encode()
+
+
+def FrontInterface(server_process=None):
+ front = QtWidgets.QApplication([])
+ main_window = mod.mainwindow.MainWindow(process=server_process)
+ main_window.showMaximized()
+ sys.exit(front.exec_())
+
+
+def Server(args):
+ [app, host, port] = args
+ uvicorn.run(app, host=host, port=port)
+
+
+if __name__ == '__main__':
+ args = config.parse_args()
+ model_config = config.get_config(args.config,
+ overrides=args.override,
+ show=True)
+ manager = ShiTuIndexManager(model_config)
+ try:
+ ip = socket.gethostbyname(socket.gethostname())
+ except:
+ ip = '127.0.0.1'
+ port = 8000
+ p_server = Process(target=Server, args=([app, ip, port],))
+ p_server.start()
+ # p_client = Process(target=FrontInterface, args=())
+ # p_client.start()
+ # p_client.join()
+ FrontInterface(p_server)
+ p_server.terminate()
+ sys.exit(0)
diff --git a/deploy/shitu_index_manager/mod/__init__.py b/deploy/shitu_index_manager/mod/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/deploy/shitu_index_manager/mod/classify_ui_context.py b/deploy/shitu_index_manager/mod/classify_ui_context.py
new file mode 100644
index 0000000000000000000000000000000000000000..e244532e2c6b9a4c47f879218faac33982bf566a
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/classify_ui_context.py
@@ -0,0 +1,144 @@
+import os
+
+from PyQt5 import QtCore, QtWidgets
+from mod import image_list_manager as imglistmgr
+from mod import utils
+from mod import ui_addclassifydialog
+from mod import ui_renameclassifydialog
+
+
+class ClassifyUiContext(QtCore.QObject):
+ # 分类界面相关业务
+ selected = QtCore.pyqtSignal(str) # 选择分类信号
+
+ def __init__(self, ui: QtWidgets.QListView, parent: QtWidgets.QMainWindow,
+ image_list_mgr: imglistmgr.ImageListManager):
+ super(ClassifyUiContext, self).__init__()
+ self.__ui = ui
+ self.__parent = parent
+ self.__imageListMgr = image_list_mgr
+ self.__menu = QtWidgets.QMenu()
+ self.__initMenu()
+ self.__initUi()
+ self.__connectSignal()
+
+ @property
+ def ui(self):
+ return self.__ui
+
+ @property
+ def parent(self):
+ return self.__parent
+
+ @property
+ def imageListManager(self):
+ return self.__imageListMgr
+
+ @property
+ def menu(self):
+ return self.__menu
+
+ def __initUi(self):
+ """初始化分类界面"""
+ self.__ui.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+
+ def __connectSignal(self):
+ """连接信号"""
+ self.__ui.clicked.connect(self.uiClicked)
+ self.__ui.doubleClicked.connect(self.uiDoubleClicked)
+
+ def __initMenu(self):
+ """初始化分类界面菜单"""
+ utils.setMenu(self.__menu, "添加分类", self.addClassify)
+ utils.setMenu(self.__menu, "移除分类", self.removeClassify)
+ utils.setMenu(self.__menu, "重命名分类", self.renemeClassify)
+
+ self.__ui.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.__ui.customContextMenuRequested.connect(self.__showMenu)
+
+ def __showMenu(self, pos):
+ """显示分类界面菜单"""
+ if len(self.__imageListMgr.filePath) > 0:
+ self.__menu.exec_(self.__ui.mapToGlobal(pos))
+
+ def setClassifyList(self, classify_list):
+ """设置分类列表"""
+ list_model = QtCore.QStringListModel(classify_list)
+ self.__ui.setModel(list_model)
+
+ def uiClicked(self, index):
+ """分类列表点击"""
+ if not self.__ui.currentIndex().isValid():
+ return
+ txt = index.data()
+ self.selected.emit(txt)
+
+ def uiDoubleClicked(self, index):
+ """分类列表双击"""
+ if not self.__ui.currentIndex().isValid():
+ return
+ ole_name = index.data()
+ dlg = QtWidgets.QDialog(parent=self.parent)
+ ui = ui_renameclassifydialog.Ui_RenameClassifyDialog()
+ ui.setupUi(dlg)
+ ui.oldNameLineEdit.setText(ole_name)
+ result = dlg.exec_()
+ new_name = ui.newNameLineEdit.text()
+ if result == QtWidgets.QDialog.Accepted:
+ mgr_result = self.__imageListMgr.renameClassify(ole_name, new_name)
+ if not mgr_result:
+ QtWidgets.QMessageBox.warning(self.parent, "重命名分类", "重命名分类错误")
+ else:
+ self.setClassifyList(self.__imageListMgr.classifyList)
+ self.__imageListMgr.writeFile()
+
+ def addClassify(self):
+ """添加分类"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self.__parent, "提示",
+ "请先打开正确的图像库")
+ return
+ dlg = QtWidgets.QDialog(parent=self.parent)
+ ui = ui_addclassifydialog.Ui_AddClassifyDialog()
+ ui.setupUi(dlg)
+ result = dlg.exec_()
+ txt = ui.lineEdit.text()
+ if result == QtWidgets.QDialog.Accepted:
+ mgr_result = self.__imageListMgr.addClassify(txt)
+ if not mgr_result:
+ QtWidgets.QMessageBox.warning(self.parent, "添加分类", "添加分类错误")
+ else:
+ self.setClassifyList(self.__imageListMgr.classifyList)
+
+ def removeClassify(self):
+ """移除分类"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self.__parent, "提示",
+ "请先打开正确的图像库")
+ return
+ if not self.__ui.currentIndex().isValid():
+ return
+ classify = self.__ui.currentIndex().data()
+ result = QtWidgets.QMessageBox.information(
+ self.parent,
+ "移除分类",
+ "确定移除分类: {}".format(classify),
+ buttons=QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
+ defaultButton=QtWidgets.QMessageBox.Cancel)
+ if result == QtWidgets.QMessageBox.Ok:
+ if len(self.__imageListMgr.imageList(classify)) > 0:
+ QtWidgets.QMessageBox.warning(self.parent, "移除分类",
+ "分类下存在图片,请先移除图片")
+ else:
+ self.__imageListMgr.removeClassify(classify)
+ self.setClassifyList(self.__imageListMgr.classifyList())
+
+ def renemeClassify(self):
+ """重命名分类"""
+ idx = self.__ui.currentIndex()
+ if idx.isValid():
+ self.uiDoubleClicked(idx)
+
+ def searchClassify(self, classify):
+ """查找分类"""
+ self.setClassifyList(self.__imageListMgr.findLikeClassify(classify))
diff --git a/deploy/shitu_index_manager/mod/image_list_manager.py b/deploy/shitu_index_manager/mod/image_list_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a662114691115612b92cf2f0a4cd391f13bc181
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/image_list_manager.py
@@ -0,0 +1,236 @@
+import os
+
+
+class ImageListManager:
+ """
+ 图像列表文件管理器
+ """
+ def __init__(self, file_path="", encoding="utf-8"):
+ self.__filePath = ""
+ self.__dirName = ""
+ self.__dataList = {}
+ self.__findLikeClassifyResult = []
+ if file_path != "":
+ self.readFile(file_path, encoding)
+
+ @property
+ def filePath(self):
+ return self.__filePath
+
+ @property
+ def dirName(self):
+ return self.__dirName
+
+ @dirName.setter
+ def dirName(self, value):
+ self.__dirName = value
+
+ @property
+ def dataList(self):
+ return self.__dataList
+
+ @property
+ def classifyList(self):
+ return self.__dataList.keys()
+
+ @property
+ def findLikeClassifyResult(self):
+ return self.__findLikeClassifyResult
+
+ def imageList(self, classify: str):
+ """
+ 获取分类下的图片列表
+
+ Args:
+ classify (str): 分类名称
+
+ Returns:
+ list: 图片列表
+ """
+ return self.__dataList[classify]
+
+ def readFile(self, file_path: str, encoding="utf-8"):
+ """
+ 读取文件内容
+
+ Args:
+ file_path (str): 文件路径
+ encoding (str, optional): 文件编码. 默认 "utf-8".
+
+ Raises:
+ Exception: 文件不存在
+ """
+ if not os.path.exists(file_path):
+ raise Exception("文件不存在:{}".format(file_path))
+ self.__filePath = file_path
+ self.__dirName = os.path.dirname(self.__filePath)
+ self.__readData(file_path, encoding)
+
+ def __readData(self, file_path: str, encoding="utf-8"):
+ """
+ 读取文件内容
+
+ Args:
+ file_path (str): 文件路径
+ encoding (str, optional): 文件编码. 默认 "utf-8".
+ """
+ with open(file_path, "r", encoding=encoding) as f:
+ self.__dataList.clear()
+ for line in f:
+ line = line.rstrip("\n")
+ data = line.split("\t")
+ self.__appendData(data)
+
+ def __appendData(self, data: list):
+ """
+ 添加数据
+
+ Args:
+ data (list): 数据
+ """
+ if data[1] not in self.__dataList:
+ self.__dataList[data[1]] = []
+ self.__dataList[data[1]].append(data[0])
+
+ def writeFile(self, file_path="", encoding="utf-8"):
+ """
+ 写入文件
+
+ Args:
+ file_path (str, optional): 文件路径. 默认 "".
+ encoding (str, optional): 文件编码. 默认 "utf-8".
+ """
+ if file_path == "":
+ file_path = self.__filePath
+ if not os.path.exists(file_path):
+ return False
+ self.__dirName = os.path.dirname(self.__filePath)
+ lines = []
+ for classify in self.__dataList.keys():
+ for path in self.__dataList[classify]:
+ lines.append("{}\t{}\n".format(path, classify))
+ with open(file_path, "w", encoding=encoding) as f:
+ f.writelines(lines)
+ return True
+
+ def realPath(self, image_path: str):
+ """
+ 获取真实路径
+
+ Args:
+ image_path (str): 图片路径
+ """
+ return os.path.join(self.__dirName, image_path)
+
+ def realPathList(self, classify: str):
+ """
+ 获取分类下的真实路径列表
+
+ Args:
+ classify (str): 分类名称
+
+ Returns:
+ list: 真实路径列表
+ """
+ if classify not in self.classifyList:
+ return []
+ paths = self.__dataList[classify]
+ if len(paths) == 0:
+ return []
+ for i in range(len(paths)):
+ paths[i] = os.path.join(self.__dirName, paths[i])
+ return paths
+
+ def findLikeClassify(self, name: str):
+ """
+ 查找类似的分类名称
+
+ Args:
+ name (str): 分类名称
+
+ Returns:
+ list: 类似的分类名称列表
+ """
+ self.__findLikeClassifyResult.clear()
+ for classify in self.__dataList.keys():
+ word = str(name)
+ if (word in classify):
+ self.__findLikeClassifyResult.append(classify)
+ return self.__findLikeClassifyResult
+
+ def addClassify(self, classify: str):
+ """
+ 添加分类
+
+ Args:
+ classify (str): 分类名称
+
+ Returns:
+ bool: 如果分类名称已经存在,返回False,否则添加分类并返回True
+ """
+ if classify in self.__dataList:
+ return False
+ self.__dataList[classify] = []
+ return True
+
+ def removeClassify(self, classify: str):
+ """
+ 移除分类
+
+ Args:
+ classify (str): 分类名称
+
+ Returns:
+ bool: 如果分类名称不存在,返回False,否则移除分类并返回True
+ """
+ if classify not in self.__dataList:
+ return False
+ self.__dataList.pop(classify)
+ return True
+
+ def renameClassify(self, old_classify: str, new_classify: str):
+ """
+ 重命名分类名称
+
+ Args:
+ old_classify (str): 原分类名称
+ new_classify (str): 新分类名称
+
+ Returns:
+ bool: 如果原分类名称不存在,或者新分类名称已经存在,返回False,否则重命名分类名称并返回True
+ """
+ if old_classify not in self.__dataList:
+ return False
+ if new_classify in self.__dataList:
+ return False
+ self.__dataList[new_classify] = self.__dataList[old_classify]
+ self.__dataList.pop(old_classify)
+ return True
+
+ def allClassfiyNotEmpty(self):
+ """
+ 检查所有分类是否都有图片
+
+ Returns:
+ bool: 如果有一个分类没有图片,返回False,否则返回True
+ """
+ for classify in self.__dataList.keys():
+ if len(self.__dataList[classify]) == 0:
+ return False
+ return True
+
+ def resetImageList(self, classify: str, image_list: list):
+ """
+ 重置图片列表
+
+ Args:
+ classify (str): 分类名称
+ image_list (list): 图片相对路径列表
+
+ Returns:
+ bool: 如果分类名称不存在,返回False,否则重置图片列表并返回True
+ """
+ if classify not in self.__dataList:
+ return False
+ self.__dataList[classify] = image_list
+ return True
diff --git a/deploy/shitu_index_manager/mod/image_list_ui_context.py b/deploy/shitu_index_manager/mod/image_list_ui_context.py
new file mode 100644
index 0000000000000000000000000000000000000000..6d5206194a79137ebff7d7d0f5d61c28ba300bb5
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/image_list_ui_context.py
@@ -0,0 +1,231 @@
+import os
+from stat import filemode
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+from mod import image_list_manager as imglistmgr
+from mod import utils
+from mod import ui_renameclassifydialog
+from mod import imageeditclassifydialog
+
+# 图像缩放基数
+BASE_IMAGE_SIZE = 64
+
+
+class ImageListUiContext(QtCore.QObject):
+ # 图片列表界面相关业务,style sheet 在 MainWindow.ui 相应的 ImageListWidget 中设置
+ listCount = QtCore.pyqtSignal(int) # 图像列表图像的数量
+ selectedCount = QtCore.pyqtSignal(int) # 图像列表选择图像的数量
+
+ def __init__(self, ui: QtWidgets.QListWidget,
+ parent: QtWidgets.QMainWindow,
+ image_list_mgr: imglistmgr.ImageListManager):
+ super(ImageListUiContext, self).__init__()
+ self.__ui = ui
+ self.__parent = parent
+ self.__imageListMgr = image_list_mgr
+ self.__initUi()
+ self.__menu = QtWidgets.QMenu()
+ self.__initMenu()
+ self.__connectSignal()
+ self.__selectedClassify = ""
+ self.__imageScale = 1
+
+ @property
+ def ui(self):
+ return self.__ui
+
+ @property
+ def parent(self):
+ return self.__parent
+
+ @property
+ def imageListManager(self):
+ return self.__imageListMgr
+
+ @property
+ def menu(self):
+ return self.__menu
+
+ def __initUi(self):
+ """初始化图片列表样式"""
+ self.__ui.setViewMode(QtWidgets.QListView.IconMode)
+ self.__ui.setSpacing(15)
+ self.__ui.setMovement(QtWidgets.QListView.Static)
+ self.__ui.setSelectionMode(
+ QtWidgets.QAbstractItemView.ExtendedSelection)
+
+ def __initMenu(self):
+ """初始化图片列表界面菜单"""
+ utils.setMenu(self.__menu, "添加图片", self.addImage)
+ utils.setMenu(self.__menu, "移除图片", self.removeImage)
+ utils.setMenu(self.__menu, "编辑图片分类", self.editImageClassify)
+ self.__menu.addSeparator()
+ utils.setMenu(self.__menu, "选择全部图片", self.selectAllImage)
+ utils.setMenu(self.__menu, "反向选择图片", self.reverseSelectImage)
+ utils.setMenu(self.__menu, "取消选择图片", self.cancelSelectImage)
+
+ self.__ui.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.__ui.customContextMenuRequested.connect(self.__showMenu)
+
+ def __showMenu(self, pos):
+ """显示图片列表界面菜单"""
+ if len(self.__imageListMgr.filePath) > 0:
+ self.__menu.exec_(self.__ui.mapToGlobal(pos))
+
+ def __connectSignal(self):
+ """连接信号与槽"""
+ self.__ui.itemSelectionChanged.connect(self.onSelectionChanged)
+
+ def setImageScale(self, scale: int):
+ """设置图片大小"""
+ self.__imageScale = scale
+ size = QtCore.QSize(scale * BASE_IMAGE_SIZE, scale * BASE_IMAGE_SIZE)
+ self.__ui.setIconSize(size)
+ for i in range(self.__ui.count()):
+ item = self.__ui.item(i)
+ item.setSizeHint(size)
+
+ def setImageList(self, classify: str):
+ """设置图片列表"""
+ size = QtCore.QSize(self.__imageScale * BASE_IMAGE_SIZE,
+ self.__imageScale * BASE_IMAGE_SIZE)
+ self.__selectedClassify = classify
+ image_list = self.__imageListMgr.imageList(classify)
+ self.__ui.clear()
+ count = 0
+ for i in image_list:
+ item = QtWidgets.QListWidgetItem(self.__ui)
+ item.setIcon(QtGui.QIcon(self.__imageListMgr.realPath(i)))
+ item.setData(QtCore.Qt.UserRole, i)
+ item.setSizeHint(size)
+ self.__ui.addItem(item)
+ count += 1
+ self.listCount.emit(count)
+
+ def clear(self):
+ """清除图片列表"""
+ self.__ui.clear()
+
+ def addImage(self):
+ """添加图片"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self.__parent, "提示",
+ "请先打开正确的图像库")
+ return
+ filter = "图片 (*.png *.jpg *.jpeg *.PNG *.JPG *.JPEG);;所有文件(*.*)"
+ dlg = QtWidgets.QFileDialog(self.__parent)
+ dlg.setFileMode(QtWidgets.QFileDialog.ExistingFiles) # 多选文件
+ dlg.setViewMode(QtWidgets.QFileDialog.Detail) # 详细模式
+ file_paths = dlg.getOpenFileNames(filter=filter)[0]
+ if len(file_paths) == 0:
+ return
+ image_list_dir = self.__imageListMgr.dirName
+ file_list = []
+ for path in file_paths:
+ if not os.path.exists(path):
+ continue
+ new_file = self.__copyToImagesDir(path)
+ if new_file != "" and image_list_dir in new_file:
+ # 去掉 image_list_dir 的路径和斜杠
+ begin = len(image_list_dir) + 1
+ file_list.append(new_file[begin:])
+ if len(file_list) > 0:
+ if self.__selectedClassify == "":
+ QtWidgets.QMessageBox.warning(self.__parent, "提示", "请先选择分类")
+ return
+ new_list = self.__imageListMgr.imageList(
+ self.__selectedClassify) + file_list
+ self.__imageListMgr.resetImageList(self.__selectedClassify,
+ new_list)
+ self.setImageList(self.__selectedClassify)
+ self.__imageListMgr.writeFile()
+
+ def __copyToImagesDir(self, image_path: str):
+ md5 = utils.fileMD5(image_path)
+ file_ext = utils.fileExtension(image_path)
+ to_dir = os.path.join(self.__imageListMgr.dirName, "images")
+ new_path = os.path.join(to_dir, md5 + file_ext)
+ if os.path.exists(to_dir):
+ utils.copyFile(image_path, new_path)
+ return new_path
+ else:
+ return ""
+
+ def removeImage(self):
+ """移除图片"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self.__parent, "提示",
+ "请先打开正确的图像库")
+ return
+ path_list = []
+ image_list = self.__ui.selectedItems()
+ if len(image_list) == 0:
+ return
+ question = QtWidgets.QMessageBox.question(self.__parent, "移除图片",
+ "确定移除所选图片吗?")
+ if question == QtWidgets.QMessageBox.No:
+ return
+ for i in range(self.__ui.count()):
+ item = self.__ui.item(i)
+ img_path = item.data(QtCore.Qt.UserRole)
+ if not item.isSelected():
+ path_list.append(img_path)
+ else:
+ # 从磁盘上删除图片
+ utils.removeFile(
+ os.path.join(self.__imageListMgr.dirName, img_path))
+ self.__imageListMgr.resetImageList(self.__selectedClassify, path_list)
+ self.setImageList(self.__selectedClassify)
+ self.__imageListMgr.writeFile()
+
+ def editImageClassify(self):
+ """编辑图片分类"""
+ old_classify = self.__selectedClassify
+ dlg = imageeditclassifydialog.ImageEditClassifyDialog(
+ parent=self.__parent,
+ old_classify=old_classify,
+ classify_list=self.__imageListMgr.classifyList)
+ result = dlg.exec_()
+ new_classify = dlg.newClassify
+ if result == QtWidgets.QDialog.Accepted \
+ and new_classify != old_classify \
+ and new_classify != "":
+ self.__moveImage(old_classify, new_classify)
+ self.__imageListMgr.writeFile()
+
+ def __moveImage(self, old_classify, new_classify):
+ """移动图片"""
+ keep_list = []
+ is_selected = False
+ move_list = self.__imageListMgr.imageList(new_classify)
+ for i in range(self.__ui.count()):
+ item = self.__ui.item(i)
+ txt = item.data(QtCore.Qt.UserRole)
+ if item.isSelected():
+ move_list.append(txt)
+ is_selected = True
+ else:
+ keep_list.append(txt)
+ if is_selected:
+ self.__imageListMgr.resetImageList(new_classify, move_list)
+ self.__imageListMgr.resetImageList(old_classify, keep_list)
+ self.setImageList(old_classify)
+
+ def selectAllImage(self):
+ """选择所有图片"""
+ self.__ui.selectAll()
+
+ def reverseSelectImage(self):
+ """反向选择图片"""
+ for i in range(self.__ui.count()):
+ item = self.__ui.item(i)
+ item.setSelected(not item.isSelected())
+
+ def cancelSelectImage(self):
+ """取消选择图片"""
+ self.__ui.clearSelection()
+
+ def onSelectionChanged(self):
+ """选择图像该变,发送选择的数量信号"""
+ count = len(self.__ui.selectedItems())
+ self.selectedCount.emit(count)
diff --git a/deploy/shitu_index_manager/mod/imageeditclassifydialog.py b/deploy/shitu_index_manager/mod/imageeditclassifydialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..ee8def38767a42871b621be2f8ce965a124ba409
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/imageeditclassifydialog.py
@@ -0,0 +1,52 @@
+import os
+from PyQt5 import QtCore, QtGui, QtWidgets
+from mod import image_list_manager
+from mod import ui_imageeditclassifydialog
+from mod import utils
+
+
+class ImageEditClassifyDialog(QtWidgets.QDialog):
+ """图像编辑分类对话框"""
+ def __init__(self, parent, old_classify, classify_list):
+ super(ImageEditClassifyDialog, self).__init__(parent)
+ self.ui = mod.ui_imageeditclassifydialog.Ui_Dialog()
+ self.ui.setupUi(self) # 初始化主窗口界面
+ self.__oldClassify = old_classify
+ self.__classifyList = classify_list
+ self.__newClassify = ""
+ self.__searchResult = []
+ self.__initUi()
+ self.__connectSignal()
+
+ @property
+ def newClassify(self):
+ return self.__newClassify
+
+ def __initUi(self):
+ self.ui.oldLineEdit.setText(self.__oldClassify)
+ self.__setClassifyList(self.__classifyList)
+ self.ui.classifyListView.setEditTriggers(
+ QtWidgets.QAbstractItemView.NoEditTriggers)
+
+ def __connectSignal(self):
+ self.ui.classifyListView.clicked.connect(self.selectedListView)
+ self.ui.searchButton.clicked.connect(self.searchClassify)
+
+ def __setClassifyList(self, classify_list):
+ list_model = QtCore.QStringListModel(classify_list)
+ self.ui.classifyListView.setModel(list_model)
+
+ def selectedListView(self, index):
+ if not self.ui.classifyListView.currentIndex().isValid():
+ return
+ txt = index.data()
+ self.ui.newLineEdit.setText(txt)
+ self.__newClassify = txt
+
+ def searchClassify(self):
+ txt = self.ui.searchWordLineEdit.text()
+ self.__searchResult.clear()
+ for classify in self.__classifyList:
+ if txt in classify:
+ self.__searchResult.append(classify)
+ self.__setClassifyList(self.__searchResult)
diff --git a/deploy/shitu_index_manager/mod/index_http_client.py b/deploy/shitu_index_manager/mod/index_http_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b9353e22150b062c105eb2ae0ea4a322657d001
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/index_http_client.py
@@ -0,0 +1,60 @@
+import json
+import os
+import urllib3
+import urllib.parse
+
+
+class IndexHttpClient():
+ """索引库客户端,使用 urllib3 连接,使用 urllib.parse 进行 url 编码"""
+ def __init__(self, host: str, port: int):
+ self.__host = host
+ self.__port = port
+ self.__http = urllib3.PoolManager()
+ self.__headers = {"Content-type": "application/json"}
+
+ def url(self):
+ return "http://{}:{}".format(self.__host, self.__port)
+
+ def new_index(self,
+ image_list_path: str,
+ index_root_path: str,
+ index_method="HNSW32",
+ force=False):
+ """新建 重建 库"""
+ if index_method not in ["HNSW32", "FLAT", "IVF"]:
+ raise Exception(
+ "index_method 必须是 HNSW32, FLAT, IVF,实际值为:{}".format(
+ index_method))
+ params = {"image_list_path":image_list_path, \
+ "index_root_path":index_root_path, \
+ "index_method":index_method, \
+ "force":force}
+ return self.__post(self.url() + "/new_index?", params)
+
+ def open_index(self, index_root_path: str, image_list_path: str):
+ """打开库"""
+ params = {
+ "index_root_path": index_root_path,
+ "image_list_path": image_list_path
+ }
+ return self.__post(self.url() + "/open_index?", params)
+
+ def update_index(self, image_list_path: str, index_root_path: str):
+ """更新索引库"""
+ params = {"image_list_path":image_list_path, \
+ "index_root_path":index_root_path}
+ return self.__post(self.url() + "/update_index?", params)
+
+ def __post(self, url: str, params: dict):
+ """发送 url 并接收数据"""
+ http = self.__http
+ encode_params = urllib.parse.urlencode(params)
+ get_url = url + encode_params
+ req = http.request("GET", get_url, headers=self.__headers)
+ result = json.loads(req.data)
+ if isinstance(result, str):
+ result = eval(result)
+ msg = result["error_message"]
+ if msg != None and len(msg) == 0:
+ msg = None
+ return msg
diff --git a/deploy/shitu_index_manager/mod/mainwindow.py b/deploy/shitu_index_manager/mod/mainwindow.py
new file mode 100644
index 0000000000000000000000000000000000000000..40d11f6c480619b537cb0c738e99ede89a8fe50c
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/mainwindow.py
@@ -0,0 +1,492 @@
+from multiprocessing.dummy import active_children
+from multiprocessing import Process
+import os
+import sys
+import socket
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+from mod import ui_mainwindow
+from mod import image_list_manager
+from mod import classify_ui_context
+from mod import image_list_ui_context
+from mod import ui_newlibrarydialog
+from mod import index_http_client
+from mod import utils
+from mod import ui_waitdialog
+import threading
+
+TOOL_BTN_ICON_SIZE = 64
+TOOL_BTN_ICON_SMALL = 48
+
+try:
+ DEFAULT_HOST = socket.gethostbyname(socket.gethostname())
+except:
+ DEFAULT_HOST = '127.0.0.1'
+
+# DEFAULT_HOST = "localhost"
+DEFAULT_PORT = 8000
+PADDLECLAS_DOC_URL = "https://gitee.com/paddlepaddle/PaddleClas/docs/zh_CN/inference_deployment/shitu_gallery_manager.md"
+
+
+class MainWindow(QtWidgets.QMainWindow):
+ """主窗口"""
+ newIndexMsg = QtCore.pyqtSignal(str) # 新建索引库线程信号
+ openIndexMsg = QtCore.pyqtSignal(str) # 打开索引库线程信号
+ updateIndexMsg = QtCore.pyqtSignal(str) # 更新索引库线程信号
+ importImageCount = QtCore.pyqtSignal(int) # 导入图像数量信号
+
+ def __init__(self, process=None):
+ super(MainWindow, self).__init__()
+ self.server_process = process
+ self.ui = ui_mainwindow.Ui_MainWindow()
+ self.ui.setupUi(self) # 初始化主窗口界面
+
+ self.__imageListMgr = image_list_manager.ImageListManager()
+
+ self.__appMenu = QtWidgets.QMenu() # 应用菜单
+ self.__libraryAppendMenu = QtWidgets.QMenu() # 图像库附加功能菜单
+ self.__initAppMenu() # 初始化应用菜单
+
+ self.__pathBar = QtWidgets.QLabel(self) # 路径
+ self.__classifyCountBar = QtWidgets.QLabel(self) # 分类数量
+ self.__imageCountBar = QtWidgets.QLabel(self) # 图像列表数量
+ self.__imageSelectedBar = QtWidgets.QLabel(self) # 图像列表选择数量
+ self.__spaceBar1 = QtWidgets.QLabel(self) # 空格间隔栏
+ self.__spaceBar2 = QtWidgets.QLabel(self) # 空格间隔栏
+ self.__spaceBar3 = QtWidgets.QLabel(self) # 空格间隔栏
+
+ # 分类界面相关业务
+ self.__classifyUiContext = classify_ui_context.ClassifyUiContext(
+ ui=self.ui.classifyListView,
+ parent=self,
+ image_list_mgr=self.__imageListMgr)
+
+ # 图片列表界面相关业务
+ self.__imageListUiContext = image_list_ui_context.ImageListUiContext(
+ ui=self.ui.imageListWidget,
+ parent=self,
+ image_list_mgr=self.__imageListMgr)
+
+ # 搜索的历史记录回车快捷键
+ self.__historyCmbShortcut = QtWidgets.QShortcut(
+ QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ self.ui.searchClassifyHistoryCmb)
+
+ self.__waitDialog = QtWidgets.QDialog() # 等待对话框
+ self.__waitDialogUi = ui_waitdialog.Ui_WaitDialog() # 等待对话框界面
+ self.__initToolBtn()
+ self.__connectSignal()
+ self.__initUI()
+ self.__initWaitDialog()
+
+ def __initUI(self):
+ """初始化界面"""
+ # 窗口图标
+ self.setWindowIcon(QtGui.QIcon("./resource/app_icon.png"))
+
+ # 初始化分割窗口
+ self.ui.splitter.setStretchFactor(0, 20)
+ self.ui.splitter.setStretchFactor(1, 80)
+
+ # 初始化图像缩放
+ self.ui.imageScaleSlider.setValue(4)
+
+ # 状态栏界面设置
+ space_bar = " " # 间隔16空格
+ self.__spaceBar1.setText(space_bar)
+ self.__spaceBar2.setText(space_bar)
+ self.__spaceBar3.setText(space_bar)
+ self.ui.statusbar.addWidget(self.__pathBar)
+ self.ui.statusbar.addWidget(self.__spaceBar1)
+ self.ui.statusbar.addWidget(self.__classifyCountBar)
+ self.ui.statusbar.addWidget(self.__spaceBar2)
+ self.ui.statusbar.addWidget(self.__imageCountBar)
+ self.ui.statusbar.addWidget(self.__spaceBar3)
+ self.ui.statusbar.addWidget(self.__imageSelectedBar)
+
+ def __initToolBtn(self):
+ """初始化工具按钮"""
+ self.__setToolButton(self.ui.appMenuBtn, "应用菜单",
+ "./resource/app_menu.png", TOOL_BTN_ICON_SIZE)
+
+ self.__setToolButton(self.ui.saveImageLibraryBtn, "保存图像库",
+ "./resource/save_image_Library.png",
+ TOOL_BTN_ICON_SIZE)
+ self.ui.saveImageLibraryBtn.clicked.connect(self.saveImageLibrary)
+
+ self.__setToolButton(self.ui.addClassifyBtn, "添加分类",
+ "./resource/add_classify.png",
+ TOOL_BTN_ICON_SIZE)
+ self.ui.addClassifyBtn.clicked.connect(
+ self.__classifyUiContext.addClassify)
+
+ self.__setToolButton(self.ui.removeClassifyBtn, "移除分类",
+ "./resource/remove_classify.png",
+ TOOL_BTN_ICON_SIZE)
+ self.ui.removeClassifyBtn.clicked.connect(
+ self.__classifyUiContext.removeClassify)
+
+ self.__setToolButton(self.ui.searchClassifyBtn, "查找分类",
+ "./resource/search_classify.png",
+ TOOL_BTN_ICON_SMALL)
+ self.ui.searchClassifyBtn.clicked.connect(
+ self.__classifyUiContext.searchClassify)
+
+ self.__setToolButton(self.ui.addImageBtn, "添加图片",
+ "./resource/add_image.png", TOOL_BTN_ICON_SMALL)
+ self.ui.addImageBtn.clicked.connect(self.__imageListUiContext.addImage)
+
+ self.__setToolButton(self.ui.removeImageBtn, "移除图片",
+ "./resource/remove_image.png",
+ TOOL_BTN_ICON_SMALL)
+ self.ui.removeImageBtn.clicked.connect(
+ self.__imageListUiContext.removeImage)
+
+ self.ui.searchClassifyHistoryCmb.setToolTip("查找分类历史")
+ self.ui.imageScaleSlider.setToolTip("图片缩放")
+
+ def __setToolButton(self, button, tool_tip: str, icon_path: str,
+ icon_size: int):
+ """设置工具按钮"""
+ button.setToolTip(tool_tip)
+ button.setIcon(QtGui.QIcon(icon_path))
+ button.setIconSize(QtCore.QSize(icon_size, icon_size))
+
+ def __initAppMenu(self):
+ """初始化应用菜单"""
+ utils.setMenu(self.__appMenu, "新建图像库", self.newImageLibrary)
+ utils.setMenu(self.__appMenu, "打开图像库", self.openImageLibrary)
+ utils.setMenu(self.__appMenu, "保存图像库", self.saveImageLibrary)
+
+ self.__libraryAppendMenu.setTitle("导入图像")
+ utils.setMenu(self.__libraryAppendMenu, "导入 image_list 图像",
+ self.importImageListImage)
+ utils.setMenu(self.__libraryAppendMenu, "导入多文件夹图像",
+ self.importDirsImage)
+ self.__appMenu.addMenu(self.__libraryAppendMenu)
+
+ self.__appMenu.addSeparator()
+ utils.setMenu(self.__appMenu, "新建/重建 索引库", self.newIndexLibrary)
+ utils.setMenu(self.__appMenu, "更新索引库", self.updateIndexLibrary)
+ self.__appMenu.addSeparator()
+ utils.setMenu(self.__appMenu, "帮助", self.showHelp)
+ utils.setMenu(self.__appMenu, "关于", self.showAbout)
+ utils.setMenu(self.__appMenu, "退出", self.exitApp)
+
+ self.ui.appMenuBtn.setMenu(self.__appMenu)
+ self.ui.appMenuBtn.setPopupMode(QtWidgets.QToolButton.InstantPopup)
+
+ def __initWaitDialog(self):
+ """初始化等待对话框"""
+ self.__waitDialogUi.setupUi(self.__waitDialog)
+ self.__waitDialog.setWindowFlags(QtCore.Qt.Dialog
+ | QtCore.Qt.FramelessWindowHint)
+
+ def __startWait(self, msg: str):
+ """开始显示等待对话框"""
+ self.setEnabled(False)
+ self.__waitDialogUi.msgLabel.setText(msg)
+ self.__waitDialog.setWindowFlags(QtCore.Qt.Dialog
+ | QtCore.Qt.FramelessWindowHint
+ | QtCore.Qt.WindowStaysOnTopHint)
+ self.__waitDialog.show()
+ self.__waitDialog.repaint()
+
+ def __stopWait(self):
+ """停止显示等待对话框"""
+ self.setEnabled(True)
+ self.__waitDialogUi.msgLabel.setText("执行完毕!")
+ self.__waitDialog.setWindowFlags(QtCore.Qt.Dialog
+ | QtCore.Qt.FramelessWindowHint
+ | QtCore.Qt.CustomizeWindowHint)
+ self.__waitDialog.close()
+
+ def __connectSignal(self):
+ """连接信号与槽"""
+ self.__classifyUiContext.selected.connect(
+ self.__imageListUiContext.setImageList)
+ self.ui.searchClassifyBtn.clicked.connect(self.searchClassify)
+ self.ui.imageScaleSlider.valueChanged.connect(
+ self.__imageListUiContext.setImageScale)
+ self.__imageListUiContext.listCount.connect(self.__setImageCountBar)
+ self.__imageListUiContext.selectedCount.connect(
+ self.__setImageSelectedCountBar)
+ self.__historyCmbShortcut.activated.connect(self.searchClassify)
+ self.newIndexMsg.connect(self.__onNewIndexMsg)
+ self.openIndexMsg.connect(self.__onOpenIndexMsg)
+ self.updateIndexMsg.connect(self.__onUpdateIndexMsg)
+ self.importImageCount.connect(self.__onImportImageCount)
+
+ def newImageLibrary(self):
+ """新建图像库"""
+ dir_path = self.__openDirDialog("新建图像库")
+ if dir_path == None:
+ return
+ if not utils.isEmptyDir(dir_path):
+ QtWidgets.QMessageBox.warning(self, "错误", "该目录不为空,请选择空目录")
+ return
+ if not utils.initLibrary(dir_path):
+ QtWidgets.QMessageBox.warning(self, "错误", "新建图像库失败")
+ return
+ QtWidgets.QMessageBox.information(self, "提示", "新建图像库成功")
+ self.__reload(os.path.join(dir_path, "image_list.txt"), dir_path)
+
+ def __openDirDialog(self, title: str):
+ """打开目录对话框"""
+ dlg = QtWidgets.QFileDialog(self)
+ dlg.setWindowTitle(title)
+ dlg.setOption(QtWidgets.QFileDialog.ShowDirsOnly, True)
+ dlg.setFileMode(QtWidgets.QFileDialog.Directory)
+ dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptOpen)
+ if dlg.exec_() == QtWidgets.QDialog.Accepted:
+ dir_path = dlg.selectedFiles()[0]
+ return dir_path
+ return None
+
+ def openImageLibrary(self):
+ """打开图像库"""
+ dir_path = self.__openDirDialog("打开图像库")
+ if dir_path != None:
+ image_list_path = os.path.join(dir_path, "image_list.txt")
+ if os.path.exists(image_list_path) \
+ and os.path.exists(os.path.join(dir_path, "images")):
+ self.__reload(image_list_path, dir_path)
+ self.openIndexLibrary()
+
+ def __reload(self, image_list_path: str, msg: str):
+ """重新加载图像库"""
+ self.__imageListMgr.readFile(image_list_path)
+ self.__imageListUiContext.clear()
+ self.__classifyUiContext.setClassifyList(
+ self.__imageListMgr.classifyList)
+ self.__setPathBar(msg)
+ self.__setClassifyCountBar(len(self.__imageListMgr.classifyList))
+ self.__setImageCountBar(0)
+ self.__setImageSelectedCountBar(0)
+
+ def saveImageLibrary(self):
+ """保存图像库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.warning(self, "错误", "请先打开正确的图像库")
+ return
+ self.__imageListMgr.writeFile()
+ self.__reload(self.__imageListMgr.filePath,
+ self.__imageListMgr.dirName)
+ hint_str = "为保证图片准确识别,请在修改图片库后更新索引库。\n\
+如果是新建图像库或者没有索引库,请新建索引库。"
+
+ QtWidgets.QMessageBox.information(self, "提示", hint_str)
+
+ def __onImportImageCount(self, count: int):
+ """导入图像槽"""
+ self.__stopWait()
+ if count == -1:
+ QtWidgets.QMessageBox.warning(self, "错误", "导入到当前图像库错误")
+ return
+ QtWidgets.QMessageBox.information(self, "提示",
+ "导入图像库成功,导入图像:{}".format(count))
+ self.__reload(self.__imageListMgr.filePath,
+ self.__imageListMgr.dirName)
+
+ def __importImageListImageThread(self, from_path: str, to_path: str):
+ """导入 image_list 图像 线程"""
+ count = utils.oneKeyImportFromFile(from_path=from_path,
+ to_path=to_path)
+ if count == None:
+ count = -1
+ self.importImageCount.emit(count)
+
+ def importImageListImage(self):
+ """导入 image_list 图像 到当前图像库,建议当前库是新建的空库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self, "提示", "请先打开正确的图像库")
+ return
+ from_path = QtWidgets.QFileDialog.getOpenFileName(
+ caption="导入 image_list 图像", filter="txt (*.txt)")[0]
+ if not os.path.exists(from_path):
+ QtWidgets.QMessageBox.information(self, "提示", "打开的文件不存在")
+ return
+ from_mgr = image_list_manager.ImageListManager(from_path)
+ self.__startWait("正在导入图像,请等待。。。")
+ thread = threading.Thread(target=self.__importImageListImageThread,
+ args=(from_mgr.filePath,
+ self.__imageListMgr.filePath))
+ thread.start()
+
+ def __importDirsImageThread(self, from_dir: str, to_image_list_path: str):
+ """导入多文件夹图像 线程"""
+ count = utils.oneKeyImportFromDirs(
+ from_dir=from_dir, to_image_list_path=to_image_list_path)
+ if count == None:
+ count = -1
+ self.importImageCount.emit(count)
+
+ def importDirsImage(self):
+ """导入 多文件夹图像 到当前图像库,建议当前库是新建的空库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self, "提示", "请先打开正确的图像库")
+ return
+ dir_path = self.__openDirDialog("导入多文件夹图像")
+ if dir_path == None:
+ return
+ if not os.path.exists(dir_path):
+ QtWidgets.QMessageBox.information(self, "提示", "打开的目录不存在")
+ return
+ self.__startWait("正在导入图像,请等待。。。")
+ thread = threading.Thread(target=self.__importDirsImageThread,
+ args=(dir_path,
+ self.__imageListMgr.filePath))
+ thread.start()
+
+ def __newIndexThread(self, index_root_path: str, image_list_path: str,
+ index_method: str, force: bool):
+ """新建重建索引库线程"""
+ try:
+ client = index_http_client.IndexHttpClient(
+ DEFAULT_HOST, DEFAULT_PORT)
+ err_msg = client.new_index(image_list_path=image_list_path,
+ index_root_path=index_root_path,
+ index_method=index_method,
+ force=force)
+ if err_msg == None:
+ err_msg = ""
+ self.newIndexMsg.emit(err_msg)
+ except Exception as e:
+ self.newIndexMsg.emit(str(e))
+
+ def __onNewIndexMsg(self, err_msg):
+ """新建重建索引库槽"""
+ self.__stopWait()
+ if err_msg == "":
+ QtWidgets.QMessageBox.information(self, "提示", "新建/重建 索引库成功")
+ else:
+ QtWidgets.QMessageBox.warning(self, "错误", err_msg)
+
+ def newIndexLibrary(self):
+ """新建重建索引库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self, "提示", "请先打开正确的图像库")
+ return
+ dlg = QtWidgets.QDialog(self)
+ ui = ui_newlibrarydialog.Ui_NewlibraryDialog()
+ ui.setupUi(dlg)
+ result = dlg.exec_()
+ index_method = ui.indexMethodCmb.currentText()
+ force = ui.resetCheckBox.isChecked()
+ if result == QtWidgets.QDialog.Accepted:
+ self.__startWait("正在 新建/重建 索引库,请等待。。。")
+ thread = threading.Thread(target=self.__newIndexThread,
+ args=(self.__imageListMgr.dirName,
+ "image_list.txt", index_method,
+ force))
+ thread.start()
+
+ def __openIndexThread(self, index_root_path: str, image_list_path: str):
+ """打开索引库线程"""
+ try:
+ client = index_http_client.IndexHttpClient(
+ DEFAULT_HOST, DEFAULT_PORT)
+ err_msg = client.open_index(index_root_path=index_root_path,
+ image_list_path=image_list_path)
+ if err_msg == None:
+ err_msg = ""
+ self.openIndexMsg.emit(err_msg)
+ except Exception as e:
+ self.openIndexMsg.emit(str(e))
+
+ def __onOpenIndexMsg(self, err_msg):
+ """打开索引库槽"""
+ self.__stopWait()
+ if err_msg == "":
+ QtWidgets.QMessageBox.information(self, "提示", "打开索引库成功")
+ else:
+ QtWidgets.QMessageBox.warning(self, "错误", err_msg)
+
+ def openIndexLibrary(self):
+ """打开索引库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self, "提示", "请先打开正确的图像库")
+ return
+ self.__startWait("正在打开索引库,请等待。。。")
+ thread = threading.Thread(target=self.__openIndexThread,
+ args=(self.__imageListMgr.dirName,
+ "image_list.txt"))
+ thread.start()
+
+ def __updateIndexThread(self, index_root_path: str, image_list_path: str):
+ """更新索引库线程"""
+ try:
+ client = index_http_client.IndexHttpClient(
+ DEFAULT_HOST, DEFAULT_PORT)
+ err_msg = client.update_index(image_list_path=image_list_path,
+ index_root_path=index_root_path)
+ if err_msg == None:
+ err_msg = ""
+ self.updateIndexMsg.emit(err_msg)
+ except Exception as e:
+ self.updateIndexMsg.emit(str(e))
+
+ def __onUpdateIndexMsg(self, err_msg):
+ """更新索引库槽"""
+ self.__stopWait()
+ if err_msg == "":
+ QtWidgets.QMessageBox.information(self, "提示", "更新索引库成功")
+ else:
+ QtWidgets.QMessageBox.warning(self, "错误", err_msg)
+
+ def updateIndexLibrary(self):
+ """更新索引库"""
+ if not os.path.exists(self.__imageListMgr.filePath):
+ QtWidgets.QMessageBox.information(self, "提示", "请先打开正确的图像库")
+ return
+ self.__startWait("正在更新索引库,请等待。。。")
+ thread = threading.Thread(target=self.__updateIndexThread,
+ args=(self.__imageListMgr.dirName,
+ "image_list.txt"))
+ thread.start()
+
+ def searchClassify(self):
+ """查找分类"""
+ if len(self.__imageListMgr.classifyList) == 0:
+ return
+ cmb = self.ui.searchClassifyHistoryCmb
+ txt = cmb.currentText()
+ is_has = False
+ if txt != "":
+ for i in range(cmb.count()):
+ if cmb.itemText(i) == txt:
+ is_has = True
+ break
+ if not is_has:
+ cmb.addItem(txt)
+ self.__classifyUiContext.searchClassify(txt)
+
+ def showHelp(self):
+ """显示帮助"""
+ QtGui.QDesktopServices.openUrl(QtCore.QUrl(PADDLECLAS_DOC_URL))
+
+ def showAbout(self):
+ """显示关于对话框"""
+ QtWidgets.QMessageBox.information(self, "关于", "识图图像库管理 V1.0.0")
+
+ def exitApp(self):
+ """退出应用"""
+ if isinstance(self.server_process, Process):
+ self.server_process.terminate()
+ # os.kill(self.server_pid)
+ sys.exit(0)
+
+ def __setPathBar(self, msg: str):
+ """设置路径状态栏信息"""
+ self.__pathBar.setText("图像库路径:{}".format(msg))
+
+ def __setClassifyCountBar(self, msg: str):
+ self.__classifyCountBar.setText("分类总数量:{}".format(msg))
+
+ def __setImageCountBar(self, count: int):
+ """设置图像数量状态栏信息"""
+ self.__imageCountBar.setText("当前图像数量:{}".format(count))
+
+ def __setImageSelectedCountBar(self, count: int):
+ """设置选择图像数量状态栏信息"""
+ self.__imageSelectedBar.setText("选择图像数量:{}".format(count))
diff --git a/deploy/shitu_index_manager/mod/ui_addclassifydialog.py b/deploy/shitu_index_manager/mod/ui_addclassifydialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c824e5f62936c5d9f61aff9c603f3b377e385d8
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_addclassifydialog.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/AddClassifyDialog.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_AddClassifyDialog(object):
+ def setupUi(self, AddClassifyDialog):
+ AddClassifyDialog.setObjectName("AddClassifyDialog")
+ AddClassifyDialog.resize(286, 127)
+ AddClassifyDialog.setModal(True)
+ self.verticalLayout = QtWidgets.QVBoxLayout(AddClassifyDialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.label = QtWidgets.QLabel(AddClassifyDialog)
+ self.label.setObjectName("label")
+ self.verticalLayout.addWidget(self.label)
+ self.lineEdit = QtWidgets.QLineEdit(AddClassifyDialog)
+ self.lineEdit.setObjectName("lineEdit")
+ self.verticalLayout.addWidget(self.lineEdit)
+ spacerItem = QtWidgets.QSpacerItem(20, 11,
+ QtWidgets.QSizePolicy.Minimum,
+ QtWidgets.QSizePolicy.Expanding)
+ self.verticalLayout.addItem(spacerItem)
+ self.buttonBox = QtWidgets.QDialogButtonBox(AddClassifyDialog)
+ self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+ self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
+ | QtWidgets.QDialogButtonBox.Ok)
+ self.buttonBox.setObjectName("buttonBox")
+ self.verticalLayout.addWidget(self.buttonBox)
+
+ self.retranslateUi(AddClassifyDialog)
+ self.buttonBox.accepted.connect(AddClassifyDialog.accept)
+ self.buttonBox.rejected.connect(AddClassifyDialog.reject)
+ QtCore.QMetaObject.connectSlotsByName(AddClassifyDialog)
+
+ def retranslateUi(self, AddClassifyDialog):
+ _translate = QtCore.QCoreApplication.translate
+ AddClassifyDialog.setWindowTitle(
+ _translate("AddClassifyDialog", "添加分类"))
+ self.label.setText(_translate("AddClassifyDialog", "分类名称"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ AddClassifyDialog = QtWidgets.QDialog()
+ ui = Ui_AddClassifyDialog()
+ ui.setupUi(AddClassifyDialog)
+ AddClassifyDialog.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/ui_imageeditclassifydialog.py b/deploy/shitu_index_manager/mod/ui_imageeditclassifydialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..cce943c0fcb524f2fc9ff2e61e4ca73c3f7d6e29
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_imageeditclassifydialog.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/ImageEditClassifyDialog.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_Dialog(object):
+ def setupUi(self, Dialog):
+ Dialog.setObjectName("Dialog")
+ Dialog.resize(414, 415)
+ Dialog.setMinimumSize(QtCore.QSize(0, 0))
+ self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.label = QtWidgets.QLabel(Dialog)
+ self.label.setObjectName("label")
+ self.verticalLayout.addWidget(self.label)
+ self.oldLineEdit = QtWidgets.QLineEdit(Dialog)
+ self.oldLineEdit.setEnabled(False)
+ self.oldLineEdit.setObjectName("oldLineEdit")
+ self.verticalLayout.addWidget(self.oldLineEdit)
+ self.label_2 = QtWidgets.QLabel(Dialog)
+ self.label_2.setObjectName("label_2")
+ self.verticalLayout.addWidget(self.label_2)
+ self.newLineEdit = QtWidgets.QLineEdit(Dialog)
+ self.newLineEdit.setEnabled(False)
+ self.newLineEdit.setObjectName("newLineEdit")
+ self.verticalLayout.addWidget(self.newLineEdit)
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.searchWordLineEdit = QtWidgets.QLineEdit(Dialog)
+ self.searchWordLineEdit.setObjectName("searchWordLineEdit")
+ self.horizontalLayout.addWidget(self.searchWordLineEdit)
+ self.searchButton = QtWidgets.QPushButton(Dialog)
+ self.searchButton.setObjectName("searchButton")
+ self.horizontalLayout.addWidget(self.searchButton)
+ self.verticalLayout.addLayout(self.horizontalLayout)
+ self.classifyListView = QtWidgets.QListView(Dialog)
+ self.classifyListView.setEnabled(True)
+ self.classifyListView.setMinimumSize(QtCore.QSize(400, 200))
+ self.classifyListView.setObjectName("classifyListView")
+ self.verticalLayout.addWidget(self.classifyListView)
+ self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
+ self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+ self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
+ | QtWidgets.QDialogButtonBox.Ok)
+ self.buttonBox.setObjectName("buttonBox")
+ self.verticalLayout.addWidget(self.buttonBox)
+
+ self.retranslateUi(Dialog)
+ self.buttonBox.accepted.connect(Dialog.accept)
+ self.buttonBox.rejected.connect(Dialog.reject)
+ QtCore.QMetaObject.connectSlotsByName(Dialog)
+
+ def retranslateUi(self, Dialog):
+ _translate = QtCore.QCoreApplication.translate
+ Dialog.setWindowTitle(_translate("Dialog", "编辑图像分类"))
+ self.label.setText(_translate("Dialog", "原分类"))
+ self.label_2.setText(_translate("Dialog", "新分类"))
+ self.searchButton.setText(_translate("Dialog", "查找"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ Dialog = QtWidgets.QDialog()
+ ui = Ui_Dialog()
+ ui.setupUi(Dialog)
+ Dialog.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/ui_mainwindow.py b/deploy/shitu_index_manager/mod/ui_mainwindow.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f544ce1152bd947fd59c36595122fe4bc9fd5f0
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_mainwindow.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/MainWindow.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_MainWindow(object):
+ def setupUi(self, MainWindow):
+ MainWindow.setObjectName("MainWindow")
+ MainWindow.resize(833, 538)
+ MainWindow.setMinimumSize(QtCore.QSize(0, 0))
+ self.centralwidget = QtWidgets.QWidget(MainWindow)
+ self.centralwidget.setObjectName("centralwidget")
+ self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.centralwidget)
+ self.verticalLayout_3.setObjectName("verticalLayout_3")
+ self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_3.setObjectName("horizontalLayout_3")
+ self.appMenuBtn = QtWidgets.QToolButton(self.centralwidget)
+ self.appMenuBtn.setObjectName("appMenuBtn")
+ self.horizontalLayout_3.addWidget(self.appMenuBtn)
+ self.saveImageLibraryBtn = QtWidgets.QToolButton(self.centralwidget)
+ self.saveImageLibraryBtn.setObjectName("saveImageLibraryBtn")
+ self.horizontalLayout_3.addWidget(self.saveImageLibraryBtn)
+ self.addClassifyBtn = QtWidgets.QToolButton(self.centralwidget)
+ self.addClassifyBtn.setObjectName("addClassifyBtn")
+ self.horizontalLayout_3.addWidget(self.addClassifyBtn)
+ self.removeClassifyBtn = QtWidgets.QToolButton(self.centralwidget)
+ self.removeClassifyBtn.setObjectName("removeClassifyBtn")
+ self.horizontalLayout_3.addWidget(self.removeClassifyBtn)
+ spacerItem = QtWidgets.QSpacerItem(40, 20,
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_3.addItem(spacerItem)
+ self.imageScaleSlider = QtWidgets.QSlider(self.centralwidget)
+ self.imageScaleSlider.setMaximumSize(QtCore.QSize(400, 16777215))
+ self.imageScaleSlider.setMinimum(1)
+ self.imageScaleSlider.setMaximum(8)
+ self.imageScaleSlider.setPageStep(2)
+ self.imageScaleSlider.setOrientation(QtCore.Qt.Horizontal)
+ self.imageScaleSlider.setObjectName("imageScaleSlider")
+ self.horizontalLayout_3.addWidget(self.imageScaleSlider)
+ self.verticalLayout_3.addLayout(self.horizontalLayout_3)
+ self.splitter = QtWidgets.QSplitter(self.centralwidget)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(
+ self.splitter.sizePolicy().hasHeightForWidth())
+ self.splitter.setSizePolicy(sizePolicy)
+ self.splitter.setOrientation(QtCore.Qt.Horizontal)
+ self.splitter.setObjectName("splitter")
+ self.widget = QtWidgets.QWidget(self.splitter)
+ self.widget.setObjectName("widget")
+ self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.widget)
+ self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout_2.setObjectName("verticalLayout_2")
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ self.searchClassifyHistoryCmb = QtWidgets.QComboBox(self.widget)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Fixed)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(
+ self.searchClassifyHistoryCmb.sizePolicy().hasHeightForWidth())
+ self.searchClassifyHistoryCmb.setSizePolicy(sizePolicy)
+ self.searchClassifyHistoryCmb.setEditable(True)
+ self.searchClassifyHistoryCmb.setObjectName("searchClassifyHistoryCmb")
+ self.horizontalLayout.addWidget(self.searchClassifyHistoryCmb)
+ self.searchClassifyBtn = QtWidgets.QToolButton(self.widget)
+ self.searchClassifyBtn.setObjectName("searchClassifyBtn")
+ self.horizontalLayout.addWidget(self.searchClassifyBtn)
+ self.verticalLayout_2.addLayout(self.horizontalLayout)
+ self.classifyListView = QtWidgets.QListView(self.widget)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(
+ self.classifyListView.sizePolicy().hasHeightForWidth())
+ self.classifyListView.setSizePolicy(sizePolicy)
+ self.classifyListView.setMinimumSize(QtCore.QSize(200, 0))
+ self.classifyListView.setEditTriggers(
+ QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.classifyListView.setObjectName("classifyListView")
+ self.verticalLayout_2.addWidget(self.classifyListView)
+ self.widget1 = QtWidgets.QWidget(self.splitter)
+ self.widget1.setObjectName("widget1")
+ self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1)
+ self.verticalLayout.setContentsMargins(0, 0, 0, 0)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ self.addImageBtn = QtWidgets.QToolButton(self.widget1)
+ self.addImageBtn.setObjectName("addImageBtn")
+ self.horizontalLayout_2.addWidget(self.addImageBtn)
+ self.removeImageBtn = QtWidgets.QToolButton(self.widget1)
+ self.removeImageBtn.setObjectName("removeImageBtn")
+ self.horizontalLayout_2.addWidget(self.removeImageBtn)
+ spacerItem1 = QtWidgets.QSpacerItem(40, 20,
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_2.addItem(spacerItem1)
+ self.verticalLayout.addLayout(self.horizontalLayout_2)
+ self.imageListWidget = QtWidgets.QListWidget(self.widget1)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(
+ self.imageListWidget.sizePolicy().hasHeightForWidth())
+ self.imageListWidget.setSizePolicy(sizePolicy)
+ self.imageListWidget.setMinimumSize(QtCore.QSize(200, 0))
+ self.imageListWidget.setStyleSheet(
+ "QListWidget::Item:hover{background:skyblue;padding-top:0px; padding-bottom:0px;}\n"
+ "QListWidget::item:selected{background:rgb(245, 121, 0); color:red;}"
+ )
+ self.imageListWidget.setObjectName("imageListWidget")
+ self.verticalLayout.addWidget(self.imageListWidget)
+ self.verticalLayout_3.addWidget(self.splitter)
+ MainWindow.setCentralWidget(self.centralwidget)
+ self.statusbar = QtWidgets.QStatusBar(MainWindow)
+ self.statusbar.setObjectName("statusbar")
+ MainWindow.setStatusBar(self.statusbar)
+
+ self.retranslateUi(MainWindow)
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
+
+ def retranslateUi(self, MainWindow):
+ _translate = QtCore.QCoreApplication.translate
+ MainWindow.setWindowTitle(_translate("MainWindow", "识图图像库管理"))
+ self.appMenuBtn.setText(_translate("MainWindow", "..."))
+ self.saveImageLibraryBtn.setText(_translate("MainWindow", "..."))
+ self.addClassifyBtn.setText(_translate("MainWindow", "..."))
+ self.removeClassifyBtn.setText(_translate("MainWindow", "..."))
+ self.searchClassifyBtn.setText(_translate("MainWindow", "..."))
+ self.addImageBtn.setText(_translate("MainWindow", "..."))
+ self.removeImageBtn.setText(_translate("MainWindow", "..."))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ MainWindow = QtWidgets.QMainWindow()
+ ui = Ui_MainWindow()
+ ui.setupUi(MainWindow)
+ MainWindow.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/ui_newlibrarydialog.py b/deploy/shitu_index_manager/mod/ui_newlibrarydialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..dcfad9e1b217f0632c71049a2898a8c782644325
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_newlibrarydialog.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/NewlibraryDialog.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_NewlibraryDialog(object):
+ def setupUi(self, NewlibraryDialog):
+ NewlibraryDialog.setObjectName("NewlibraryDialog")
+ NewlibraryDialog.resize(414, 230)
+ self.verticalLayout = QtWidgets.QVBoxLayout(NewlibraryDialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.label = QtWidgets.QLabel(NewlibraryDialog)
+ self.label.setObjectName("label")
+ self.verticalLayout.addWidget(self.label)
+ self.indexMethodCmb = QtWidgets.QComboBox(NewlibraryDialog)
+ self.indexMethodCmb.setEnabled(True)
+ self.indexMethodCmb.setObjectName("indexMethodCmb")
+ self.indexMethodCmb.addItem("")
+ self.indexMethodCmb.addItem("")
+ self.indexMethodCmb.addItem("")
+ self.verticalLayout.addWidget(self.indexMethodCmb)
+ self.resetCheckBox = QtWidgets.QCheckBox(NewlibraryDialog)
+ self.resetCheckBox.setObjectName("resetCheckBox")
+ self.verticalLayout.addWidget(self.resetCheckBox)
+ spacerItem = QtWidgets.QSpacerItem(20, 80,
+ QtWidgets.QSizePolicy.Minimum,
+ QtWidgets.QSizePolicy.Expanding)
+ self.verticalLayout.addItem(spacerItem)
+ self.buttonBox = QtWidgets.QDialogButtonBox(NewlibraryDialog)
+ self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+ self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
+ | QtWidgets.QDialogButtonBox.Ok)
+ self.buttonBox.setObjectName("buttonBox")
+ self.verticalLayout.addWidget(self.buttonBox)
+
+ self.retranslateUi(NewlibraryDialog)
+ self.indexMethodCmb.setCurrentIndex(0)
+ self.buttonBox.accepted.connect(NewlibraryDialog.accept)
+ self.buttonBox.rejected.connect(NewlibraryDialog.reject)
+ QtCore.QMetaObject.connectSlotsByName(NewlibraryDialog)
+
+ def retranslateUi(self, NewlibraryDialog):
+ _translate = QtCore.QCoreApplication.translate
+ NewlibraryDialog.setWindowTitle(
+ _translate("NewlibraryDialog", "新建/重建 索引"))
+ self.label.setText(_translate("NewlibraryDialog", "索引方式"))
+ self.indexMethodCmb.setItemText(
+ 0, _translate("NewlibraryDialog", "HNSW32"))
+ self.indexMethodCmb.setItemText(1,
+ _translate("NewlibraryDialog", "FLAT"))
+ self.indexMethodCmb.setItemText(2, _translate("NewlibraryDialog",
+ "IVF"))
+ self.resetCheckBox.setText(
+ _translate("NewlibraryDialog", "重建索引,警告:会覆盖原索引"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ NewlibraryDialog = QtWidgets.QDialog()
+ ui = Ui_NewlibraryDialog()
+ ui.setupUi(NewlibraryDialog)
+ NewlibraryDialog.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/ui_renameclassifydialog.py b/deploy/shitu_index_manager/mod/ui_renameclassifydialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..b4d1ab32a640b6919fd0f35f25d4627460347e14
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_renameclassifydialog.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/RenameClassifyDialog.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_RenameClassifyDialog(object):
+ def setupUi(self, RenameClassifyDialog):
+ RenameClassifyDialog.setObjectName("RenameClassifyDialog")
+ RenameClassifyDialog.resize(342, 194)
+ self.verticalLayout = QtWidgets.QVBoxLayout(RenameClassifyDialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.oldlabel = QtWidgets.QLabel(RenameClassifyDialog)
+ self.oldlabel.setObjectName("oldlabel")
+ self.verticalLayout.addWidget(self.oldlabel)
+ self.oldNameLineEdit = QtWidgets.QLineEdit(RenameClassifyDialog)
+ self.oldNameLineEdit.setEnabled(False)
+ self.oldNameLineEdit.setObjectName("oldNameLineEdit")
+ self.verticalLayout.addWidget(self.oldNameLineEdit)
+ self.newlabel = QtWidgets.QLabel(RenameClassifyDialog)
+ self.newlabel.setObjectName("newlabel")
+ self.verticalLayout.addWidget(self.newlabel)
+ self.newNameLineEdit = QtWidgets.QLineEdit(RenameClassifyDialog)
+ self.newNameLineEdit.setObjectName("newNameLineEdit")
+ self.verticalLayout.addWidget(self.newNameLineEdit)
+ spacerItem = QtWidgets.QSpacerItem(20, 14,
+ QtWidgets.QSizePolicy.Minimum,
+ QtWidgets.QSizePolicy.Expanding)
+ self.verticalLayout.addItem(spacerItem)
+ self.buttonBox = QtWidgets.QDialogButtonBox(RenameClassifyDialog)
+ self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
+ self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
+ | QtWidgets.QDialogButtonBox.Ok)
+ self.buttonBox.setObjectName("buttonBox")
+ self.verticalLayout.addWidget(self.buttonBox)
+
+ self.retranslateUi(RenameClassifyDialog)
+ self.buttonBox.accepted.connect(RenameClassifyDialog.accept)
+ self.buttonBox.rejected.connect(RenameClassifyDialog.reject)
+ QtCore.QMetaObject.connectSlotsByName(RenameClassifyDialog)
+
+ def retranslateUi(self, RenameClassifyDialog):
+ _translate = QtCore.QCoreApplication.translate
+ RenameClassifyDialog.setWindowTitle(
+ _translate("RenameClassifyDialog", "重命名分类"))
+ self.oldlabel.setText(_translate("RenameClassifyDialog", "原名称"))
+ self.newlabel.setText(_translate("RenameClassifyDialog", "新名称"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ RenameClassifyDialog = QtWidgets.QDialog()
+ ui = Ui_RenameClassifyDialog()
+ ui.setupUi(RenameClassifyDialog)
+ RenameClassifyDialog.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/ui_waitdialog.py b/deploy/shitu_index_manager/mod/ui_waitdialog.py
new file mode 100644
index 0000000000000000000000000000000000000000..921ba9b75d64edc4cfaadf4a393abf7fba2a3e17
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/ui_waitdialog.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'ui/WaitDialog.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.5
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_WaitDialog(object):
+ def setupUi(self, WaitDialog):
+ WaitDialog.setObjectName("WaitDialog")
+ WaitDialog.setWindowModality(QtCore.Qt.NonModal)
+ WaitDialog.resize(324, 78)
+ self.verticalLayout = QtWidgets.QVBoxLayout(WaitDialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.msgLabel = QtWidgets.QLabel(WaitDialog)
+ self.msgLabel.setObjectName("msgLabel")
+ self.verticalLayout.addWidget(self.msgLabel)
+ self.progressBar = QtWidgets.QProgressBar(WaitDialog)
+ self.progressBar.setMaximum(0)
+ self.progressBar.setProperty("value", -1)
+ self.progressBar.setObjectName("progressBar")
+ self.verticalLayout.addWidget(self.progressBar)
+ spacerItem = QtWidgets.QSpacerItem(20, 1,
+ QtWidgets.QSizePolicy.Minimum,
+ QtWidgets.QSizePolicy.Expanding)
+ self.verticalLayout.addItem(spacerItem)
+
+ self.retranslateUi(WaitDialog)
+ QtCore.QMetaObject.connectSlotsByName(WaitDialog)
+
+ def retranslateUi(self, WaitDialog):
+ _translate = QtCore.QCoreApplication.translate
+ WaitDialog.setWindowTitle(_translate("WaitDialog", "请等待"))
+ self.msgLabel.setText(_translate("WaitDialog", "正在更新索引库,请等待。。。"))
+
+
+if __name__ == "__main__":
+ import sys
+ app = QtWidgets.QApplication(sys.argv)
+ WaitDialog = QtWidgets.QDialog()
+ ui = Ui_WaitDialog()
+ ui.setupUi(WaitDialog)
+ WaitDialog.show()
+ sys.exit(app.exec_())
diff --git a/deploy/shitu_index_manager/mod/utils.py b/deploy/shitu_index_manager/mod/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..2886522654998429cae3fc720d39343db41db75b
--- /dev/null
+++ b/deploy/shitu_index_manager/mod/utils.py
@@ -0,0 +1,142 @@
+import os
+import sys
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+import hashlib
+import shutil
+from mod import image_list_manager
+
+
+def setMenu(menu: QtWidgets.QMenu, text: str, triggered):
+ """设置菜单"""
+ action = menu.addAction(text)
+ action.triggered.connect(triggered)
+
+
+def fileMD5(file_path: str):
+ """计算文件的MD5值"""
+ md5 = hashlib.md5()
+ with open(file_path, 'rb') as f:
+ md5.update(f.read())
+ return md5.hexdigest().lower()
+
+
+def copyFile(from_path: str, to_path: str):
+ """复制文件"""
+ shutil.copyfile(from_path, to_path)
+ return os.path.exists(to_path)
+
+
+def removeFile(file_path: str):
+ """删除文件"""
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ return not os.path.exists(file_path)
+
+
+def fileExtension(file_path: str):
+ """获取文件的扩展名"""
+ return os.path.splitext(file_path)[1]
+
+
+def copyImageToDir(self, from_image_path: str, to_dir_path: str):
+ """复制图像文件到目标目录"""
+ if not os.path.exists(from_image_path) and not os.path.exists(to_dir_path):
+ return None
+ md5 = fileMD5(from_image_path)
+ file_ext = fileExtension(from_image_path)
+ new_path = os.path.join(to_dir_path, md5 + file_ext)
+ copyFile(from_image_path, new_path)
+ return new_path
+
+
+def oneKeyImportFromFile(from_path: str, to_path: str):
+ """从其它图像库 from_path {image_list.txt} 导入到图像库 to_path {image_list.txt}"""
+ if not os.path.exists(from_path) or not os.path.exists(to_path):
+ return None
+ if from_path == to_path:
+ return None
+ from_mgr = image_list_manager.ImageListManager(file_path=from_path)
+ to_mgr = image_list_manager.ImageListManager(file_path=to_path)
+ return oneKeyImport(from_mgr=from_mgr, to_mgr=to_mgr)
+
+
+def oneKeyImportFromDirs(from_dir: str, to_image_list_path: str):
+ """从其它图像库 from_dir 搜索子目录 导入到图像库 to_image_list_path"""
+ if not os.path.exists(from_dir) or not os.path.exists(to_image_list_path):
+ return None
+ if from_dir == os.path.dirname(to_image_list_path):
+ return None
+ from_mgr = image_list_manager.ImageListManager()
+ to_mgr = image_list_manager.ImageListManager(
+ file_path=to_image_list_path)
+ from_mgr.dirName = from_dir
+ sub_dir_list = os.listdir(from_dir)
+ for sub_dir in sub_dir_list:
+ real_sub_dir = os.path.join(from_dir, sub_dir)
+ if not os.path.isdir(real_sub_dir):
+ continue
+ img_list = os.listdir(real_sub_dir)
+ img_path = []
+ for img in img_list:
+ real_img = os.path.join(real_sub_dir, img)
+ if not os.path.isfile(real_img):
+ continue
+ img_path.append("{}/{}".format(sub_dir, img))
+ if len(img_path) == 0:
+ continue
+ from_mgr.addClassify(sub_dir)
+ from_mgr.resetImageList(sub_dir, img_path)
+ return oneKeyImport(from_mgr=from_mgr, to_mgr=to_mgr)
+
+
+def oneKeyImport(from_mgr: image_list_manager.ImageListManager,
+ to_mgr: image_list_manager.ImageListManager):
+ """一键导入"""
+ count = 0
+ for classify in from_mgr.classifyList:
+ img_list = from_mgr.realPathList(classify)
+ to_mgr.addClassify(classify)
+ to_img_list = to_mgr.imageList(classify)
+ new_img_list = []
+ for img in img_list:
+ from_image_path = img
+ to_dir_path = os.path.join(to_mgr.dirName, "images")
+ md5 = fileMD5(from_image_path)
+ file_ext = fileExtension(from_image_path)
+ new_path = os.path.join(to_dir_path, md5 + file_ext)
+ if os.path.exists(new_path):
+ # 如果新文件 MD5 重复跳过后面的复制文件操作
+ continue
+ copyFile(from_image_path, new_path)
+ new_img_list.append("images/" + md5 + file_ext)
+ count += 1
+ to_img_list += new_img_list
+ to_mgr.resetImageList(classify, to_img_list)
+ to_mgr.writeFile()
+ return count
+
+
+def newFile(file_path: str):
+ """创建文件"""
+ if os.path.exists(file_path):
+ return False
+ else:
+ with open(file_path, 'w') as f:
+ pass
+ return True
+
+
+def isEmptyDir(dir_path: str):
+ """判断目录是否为空"""
+ return not os.listdir(dir_path)
+
+
+def initLibrary(dir_path: str):
+ """初始化库"""
+ images_dir = os.path.join(dir_path, "images")
+ if not os.path.exists(images_dir):
+ os.makedirs(images_dir)
+ image_list_path = os.path.join(dir_path, "image_list.txt")
+ newFile(image_list_path)
+ return os.path.exists(dir_path)
diff --git a/deploy/shitu_index_manager/resource/add_classify.png b/deploy/shitu_index_manager/resource/add_classify.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ba0d1d3f58654b3e52dbe8f4d537a15066670f0
Binary files /dev/null and b/deploy/shitu_index_manager/resource/add_classify.png differ
diff --git a/deploy/shitu_index_manager/resource/add_image.png b/deploy/shitu_index_manager/resource/add_image.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a7493f79ecd36271501ebccbc21c56271510ee9
Binary files /dev/null and b/deploy/shitu_index_manager/resource/add_image.png differ
diff --git a/deploy/shitu_index_manager/resource/app_icon.png b/deploy/shitu_index_manager/resource/app_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..0991f667fb93676e9e1a882099813fe585d4a48c
Binary files /dev/null and b/deploy/shitu_index_manager/resource/app_icon.png differ
diff --git a/deploy/shitu_index_manager/resource/app_menu.png b/deploy/shitu_index_manager/resource/app_menu.png
new file mode 100644
index 0000000000000000000000000000000000000000..d46180f45d184c5c3d88d58796d0096fead1b976
Binary files /dev/null and b/deploy/shitu_index_manager/resource/app_menu.png differ
diff --git a/deploy/shitu_index_manager/resource/remove_classify.png b/deploy/shitu_index_manager/resource/remove_classify.png
new file mode 100644
index 0000000000000000000000000000000000000000..51efb8a2a00c5767e45d6b532547ac963321cfbf
Binary files /dev/null and b/deploy/shitu_index_manager/resource/remove_classify.png differ
diff --git a/deploy/shitu_index_manager/resource/remove_image.png b/deploy/shitu_index_manager/resource/remove_image.png
new file mode 100644
index 0000000000000000000000000000000000000000..057d3c20405754bbf51d38d73de36259e8ac12a4
Binary files /dev/null and b/deploy/shitu_index_manager/resource/remove_image.png differ
diff --git a/deploy/shitu_index_manager/resource/save_image_Library.png b/deploy/shitu_index_manager/resource/save_image_Library.png
new file mode 100644
index 0000000000000000000000000000000000000000..67e0a394a9aea56b83eb563f1b38ec36a9c724bc
Binary files /dev/null and b/deploy/shitu_index_manager/resource/save_image_Library.png differ
diff --git a/deploy/shitu_index_manager/resource/search_classify.png b/deploy/shitu_index_manager/resource/search_classify.png
new file mode 100644
index 0000000000000000000000000000000000000000..bdd75d8556c8bd05a4df60ac000d2da332dd722c
Binary files /dev/null and b/deploy/shitu_index_manager/resource/search_classify.png differ
diff --git a/deploy/shitu_index_manager/ui/AddClassifyDialog.ui b/deploy/shitu_index_manager/ui/AddClassifyDialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..bdc06f3aa942257d562f426bea5eb035812c5d91
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/AddClassifyDialog.ui
@@ -0,0 +1,90 @@
+
+
+ AddClassifyDialog
+
+
+
+ 0
+ 0
+ 286
+ 127
+
+
+
+ 添加分类
+
+
+ true
+
+
+ -
+
+
+ 分类名称
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 11
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ AddClassifyDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ AddClassifyDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/deploy/shitu_index_manager/ui/ImageEditClassifyDialog.ui b/deploy/shitu_index_manager/ui/ImageEditClassifyDialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..d21624fd2defb41e676b1927a50fc66605f977a1
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/ImageEditClassifyDialog.ui
@@ -0,0 +1,125 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 414
+ 415
+
+
+
+
+ 0
+ 0
+
+
+
+ 编辑图像分类
+
+
+ -
+
+
+ 原分类
+
+
+
+ -
+
+
+ false
+
+
+
+ -
+
+
+ 新分类
+
+
+
+ -
+
+
+ false
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ 查找
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 400
+ 200
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/deploy/shitu_index_manager/ui/MainWindow.ui b/deploy/shitu_index_manager/ui/MainWindow.ui
new file mode 100644
index 0000000000000000000000000000000000000000..8d8808b36e80e0f90d1f398c8bba39ff3fa181b4
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/MainWindow.ui
@@ -0,0 +1,212 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 833
+ 538
+
+
+
+
+ 0
+ 0
+
+
+
+ 识图图像库管理
+
+
+
+ -
+
+
-
+
+
+ ...
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 400
+ 16777215
+
+
+
+ 1
+
+
+ 8
+
+
+ 2
+
+
+ Qt::Horizontal
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::Horizontal
+
+
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+
+ -
+
+
+ ...
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 0
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ ...
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 200
+ 0
+
+
+
+ QListWidget::Item:hover{background:skyblue;padding-top:0px; padding-bottom:0px;}
+QListWidget::item:selected{background:rgb(245, 121, 0); color:red;}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/deploy/shitu_index_manager/ui/NewlibraryDialog.ui b/deploy/shitu_index_manager/ui/NewlibraryDialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..0df94eae2f0fd660d65eb7ef93fba99950629fb6
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/NewlibraryDialog.ui
@@ -0,0 +1,116 @@
+
+
+ NewlibraryDialog
+
+
+
+ 0
+ 0
+ 414
+ 230
+
+
+
+ 新建/重建 索引
+
+
+ -
+
+
+ 索引方式
+
+
+
+ -
+
+
+ true
+
+
+ 0
+
+
-
+
+ HNSW32
+
+
+ -
+
+ FLAT
+
+
+ -
+
+ IVF
+
+
+
+
+ -
+
+
+ 重建索引,警告:会覆盖原索引
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 80
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ NewlibraryDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ NewlibraryDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/deploy/shitu_index_manager/ui/RenameClassifyDialog.ui b/deploy/shitu_index_manager/ui/RenameClassifyDialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..53ba8606a42ff7e541317001719e89b753805633
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/RenameClassifyDialog.ui
@@ -0,0 +1,101 @@
+
+
+ RenameClassifyDialog
+
+
+
+ 0
+ 0
+ 342
+ 194
+
+
+
+ 重命名分类
+
+
+ -
+
+
+ 原名称
+
+
+
+ -
+
+
+ false
+
+
+
+ -
+
+
+ 新名称
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 14
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ RenameClassifyDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ RenameClassifyDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/deploy/shitu_index_manager/ui/WaitDialog.ui b/deploy/shitu_index_manager/ui/WaitDialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..eaf62fde565a92ae2f4ca3c06010f6d7c42d4848
--- /dev/null
+++ b/deploy/shitu_index_manager/ui/WaitDialog.ui
@@ -0,0 +1,54 @@
+
+
+ WaitDialog
+
+
+ Qt::NonModal
+
+
+
+ 0
+ 0
+ 324
+ 78
+
+
+
+ 请等待
+
+
+ -
+
+
+ 正在更新索引库,请等待。。。
+
+
+
+ -
+
+
+ 0
+
+
+ -1
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 1
+
+
+
+
+
+
+
+
+