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 + + + + + + + + +