未验证 提交 f1ef3a0c 编写于 作者: W Walter 提交者: GitHub

Merge pull request #2183 from RainFrost1/index_management

add first version for index manager server
../../docs/zh_CN/inference_deployment/shitu_gallery_manager.md
\ No newline at end of file
# 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)
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))
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
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)
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)
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
此差异已折叠。
# -*- 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_())
# -*- 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_())
# -*- 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_())
# -*- 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_())
# -*- 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_())
# -*- 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_())
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)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddClassifyDialog</class>
<widget class="QDialog" name="AddClassifyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>286</width>
<height>127</height>
</rect>
</property>
<property name="windowTitle">
<string>添加分类</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>分类名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>11</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddClassifyDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddClassifyDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>414</width>
<height>415</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>编辑图像分类</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>原分类</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="oldLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>新分类</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="newLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="searchWordLineEdit"/>
</item>
<item>
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>查找</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="classifyListView">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>200</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>833</width>
<height>538</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>识图图像库管理</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QToolButton" name="appMenuBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="saveImageLibraryBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="imageScaleSlider">
<property name="maximumSize">
<size>
<width>400</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8</number>
</property>
<property name="pageStep">
<number>2</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="searchClassifyHistoryCmb">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="searchClassifyBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="classifyListView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="addImageBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeImageBtn">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="imageListWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QListWidget::Item:hover{background:skyblue;padding-top:0px; padding-bottom:0px;}
QListWidget::item:selected{background:rgb(245, 121, 0); color:red;}</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NewlibraryDialog</class>
<widget class="QDialog" name="NewlibraryDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>414</width>
<height>230</height>
</rect>
</property>
<property name="windowTitle">
<string>新建/重建 索引</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>索引方式</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="indexMethodCmb">
<property name="enabled">
<bool>true</bool>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text">
<string>HNSW32</string>
</property>
</item>
<item>
<property name="text">
<string>FLAT</string>
</property>
</item>
<item>
<property name="text">
<string>IVF</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QCheckBox" name="resetCheckBox">
<property name="text">
<string>重建索引,警告:会覆盖原索引</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>80</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NewlibraryDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NewlibraryDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RenameClassifyDialog</class>
<widget class="QDialog" name="RenameClassifyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>342</width>
<height>194</height>
</rect>
</property>
<property name="windowTitle">
<string>重命名分类</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="oldlabel">
<property name="text">
<string>原名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="oldNameLineEdit">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="newlabel">
<property name="text">
<string>新名称</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="newNameLineEdit"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>14</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>RenameClassifyDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>RenameClassifyDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WaitDialog</class>
<widget class="QDialog" name="WaitDialog">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>324</width>
<height>78</height>
</rect>
</property>
<property name="windowTitle">
<string>请等待</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="msgLabel">
<property name="text">
<string>正在更新索引库,请等待。。。</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册