From d5c57a2402816a46852a62e37cc9609f3fc45ff8 Mon Sep 17 00:00:00 2001 From: copper Date: Tue, 3 May 2022 13:16:39 +0800 Subject: [PATCH] init --- .gitattributes | 1 + .gitignore | 212 +++++++++++++++++++ ReadMe.md | 12 ++ __init__.py | 0 actions/actions.py | 341 ++++++++++++++++++++++++++++++ gui/about.py | 42 ++++ gui/layertree.py | 54 +++++ gui/license.py | 74 +++++++ gui/mainwindow.py | 113 ++++++++++ gui/mapcanvas.py | 491 +++++++++++++++++++++++++++++++++++++++++++ gui/messagebox.py | 70 ++++++ gui/project.py | 72 +++++++ gui/result.py | 41 ++++ gui/setting.py | 0 icons/assessment.svg | 1 + icons/cancel.svg | 1 + icons/clear.svg | 1 + icons/edit.svg | 1 + icons/exit.svg | 1 + icons/export.svg | 1 + icons/font.svg | 1 + icons/full.svg | 1 + icons/load.svg | 1 + icons/logo.svg | 1 + icons/model.svg | 1 + icons/ok.svg | 1 + icons/outline.svg | 1 + icons/paint.svg | 1 + icons/pan.svg | 1 + icons/qt.svg | 67 ++++++ icons/settings.svg | 1 + icons/splash.png | Bin 0 -> 95653 bytes icons/start.svg | 1 + icons/vector.svg | 1 + icons/zoomin.svg | 1 + icons/zoomout.svg | 1 + lic/license.lic | 1 + license.lic | 1 + log.txt | 0 main.py | 9 + mul/mulexport.py | 32 +++ mul/mulgrubcut.py | 19 ++ mul/mulstart.py | 63 ++++++ res.qrc | 26 +++ test/tree.py | 132 ++++++++++++ utils/__init__.py | 2 + utils/license.py | 143 +++++++++++++ utils/menu.py | 11 + utils/project.py | 121 +++++++++++ utils/setting.py | 109 ++++++++++ 50 files changed, 2279 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 ReadMe.md create mode 100644 __init__.py create mode 100644 actions/actions.py create mode 100644 gui/about.py create mode 100644 gui/layertree.py create mode 100644 gui/license.py create mode 100644 gui/mainwindow.py create mode 100644 gui/mapcanvas.py create mode 100644 gui/messagebox.py create mode 100644 gui/project.py create mode 100644 gui/result.py create mode 100644 gui/setting.py create mode 100644 icons/assessment.svg create mode 100644 icons/cancel.svg create mode 100644 icons/clear.svg create mode 100644 icons/edit.svg create mode 100644 icons/exit.svg create mode 100644 icons/export.svg create mode 100644 icons/font.svg create mode 100644 icons/full.svg create mode 100644 icons/load.svg create mode 100644 icons/logo.svg create mode 100644 icons/model.svg create mode 100644 icons/ok.svg create mode 100644 icons/outline.svg create mode 100644 icons/paint.svg create mode 100644 icons/pan.svg create mode 100644 icons/qt.svg create mode 100644 icons/settings.svg create mode 100644 icons/splash.png create mode 100644 icons/start.svg create mode 100644 icons/vector.svg create mode 100644 icons/zoomin.svg create mode 100644 icons/zoomout.svg create mode 100644 lic/license.lic create mode 100644 license.lic create mode 100644 log.txt create mode 100644 main.py create mode 100644 mul/mulexport.py create mode 100644 mul/mulgrubcut.py create mode 100644 mul/mulstart.py create mode 100644 res.qrc create mode 100644 test/tree.py create mode 100644 utils/__init__.py create mode 100644 utils/license.py create mode 100644 utils/menu.py create mode 100644 utils/project.py create mode 100644 utils/setting.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0bb75f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.onnx filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed028b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# C++ objects and libs +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.so.* +*.dll +*.dylib + +# Qt-es +object_script.*.Release +object_script.*.Debug +*_plugin_import.cpp +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h +*.qmlc +*.jsc +Makefile* +*build-* +*.qm +*.prl + +# Qt unit tests +target_wrapper.* + +# QtCreator +*.autosave + +# QtCreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCreator CMake +CMakeLists.txt.user* + +# QtCreator 4.8< compilation database +compile_commands.json + +# QtCreator local machine specific files for imported projects +*creator.user* + +*_qmlcache.qrc + +# custom +output* +.vscode/settings.json +package +rc.py +*.aux.xml + +model/ \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..0d5dbc1 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,12 @@ +# 变化检测 +王铜 +CVEO团队 + +# 环境 +Python3 + PyQt + QGIS? + +# 当前依赖 +pyqt qgis gdal numpy + +# 打包方式 +comming soon \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/actions/actions.py b/actions/actions.py new file mode 100644 index 0000000..4b2d4d4 --- /dev/null +++ b/actions/actions.py @@ -0,0 +1,341 @@ +import logging +import os +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import QAction, QActionGroup, QLabel, QFileDialog +from gui.about import AboutDialog +from gui.project import Create +from utils.project import Project + +class ActionManager(QtCore.QObject): + + def __init__(self, + double_map, + layer_tree, + follow_box, + result_box, + message_box, + parent=None): + super().__init__(parent) + self.w_parent = parent + self.actions = {} + self.action_groups = {} + self.action_group_actions = {} + + self.double_map = double_map + self.layer_tree = layer_tree + self.follow_box = follow_box + self.result_box = result_box + self.message_box = message_box + + self.allways_enable = [] + self.init_enable = [] + self.dataload_enable = [] + + self.toolbar = None + self.menubar = None + self.status_bar = None + + def set_menus(self, menubar): + self.menubar = menubar + self.file_menu = menubar.addMenu('&文件') + # self.view_menu = menubar.addMenu('&视图') + self.basic_menu = menubar.addMenu('&基本工具') + self.preop_menu = menubar.addMenu('&预处理') + self.change_detection_menu = menubar.addMenu('&变化检测') + self.special_chagne_detec_menu = menubar.addMenu('&专题变化检测') + self.postop_menu = menubar.addMenu('&后处理') + self.eval_menu = menubar.addMenu('&评估') + self.help_menu = menubar.addMenu('&帮助') + + def set_toolbar(self, toolbar): + self.toolbar = toolbar + self.toolbar.setIconSize(QtCore.QSize(24, 24)) + + def set_status_bar(self, status_bar): + self.status_bar = status_bar + + def set_actions(self): + ''' + File menu + ''' + project_create = self.add_action(QAction('&工程创建', self.w_parent), 'File') + project_open = self.add_action(QAction('&打开工程', self.w_parent), 'File') + project_save = self.add_action(QAction('&保存工程', self.w_parent), 'File') + data_load = self.add_action(QAction('&数据加载', self.w_parent), 'File') + view_setting = self.add_action(QAction('&界面定制', self.w_parent), 'File') + exit_app = self.add_action(QAction('&退出', self.w_parent), 'File') + project_create.triggered.connect(self.project_create) + project_open.triggered.connect(self.project_open) + project_save.triggered.connect(self.project_save) + data_load.triggered.connect(self.data_load) + view_setting.triggered.connect(self.view_setting) + exit_app.triggered.connect(self.w_parent.close) + + self.allways_enable.append(project_create, project_open, exit_app, view_setting) + self.init_enable.append(project_save, data_load) + + self.file_menu.addAction(project_create) + self.file_menu.addAction(project_open) + self.file_menu.addAction(project_save) + self.file_menu.addAction(data_load) + self.file_menu.addAction(view_setting) + self.file_menu.addAction(exit_app) + + if self.toolbar is not None: + self.toolbar.addAction(project_create) + self.toolbar.addAction(project_open) + self.toolbar.addAction(project_save) + + ''' + Basic menu + ''' + grid_line = self.add_action(QAction('&网格线', self.w_parent), 'Basic') + grid_line.setCheckable(True) + grid_line.setChecked(True) + + zomm_in = self.add_action(QAction('&放大', self.w_parent), 'Basic') + zomm_out = self.add_action(QAction('&缩小', self.w_parent), 'Basic') + pan = self.add_action(QAction('&漫游', self.w_parent), 'Basic') + locate = self.add_action(QAction('&定位', self.w_parent), 'Basic') + + + + self.basic_menu.addAction(grid_line) + self.basic_menu.addAction(zomm_in) + self.basic_menu.addAction(zomm_out) + self.basic_menu.addAction(pan) + self.basic_menu.addAction(locate) + + ''' + Preop menu + ''' + morphology_filter = self.add_action(QAction('&形态学滤波', self.w_parent), 'filter') + lee_filter = self.add_action(QAction('&Lee滤波', self.w_parent), 'filter') + auto_filter = self.add_action(QAction('&自适应滤波-自主', self.w_parent), 'filter') + auto_filter_no_params = self.add_action(QAction('自动滤波(无参自适应滤波)-自主', self.w_parent), 'filter') + double_filter = self.add_action(QAction('&双边滤波', self.w_parent), 'filter') + align_action = self.add_action(QAction('&配准', self.w_parent), 'align') + filter_action_group = self.get_action_group('filter') + + filter_menu = self.preop_menu.addMenu('&滤波') + for action in filter_action_group.actions(): + filter_menu.addAction(action) + + # self.preop_menu.addActionGroup(filter_action_group) + self.preop_menu.addAction(align_action) + + if self.toolbar is not None: + self.toolbar.addAction(morphology_filter) + self.toolbar.addAction(lee_filter) + self.toolbar.addAction(auto_filter) + self.toolbar.addAction(auto_filter_no_params) + self.toolbar.addAction(double_filter) + + ''' + Change detection menu + ''' + diff_method = self.add_action(QAction('&差分法', self.w_parent), 'unsuper_change_detection') + log_diff = self.add_action(QAction('&对数差分法', self.w_parent), 'unsuper_change_detection') + lsts_ = self.add_action(QAction('&LSTS法', self.w_parent), 'unsuper_change_detection') + lhba = self.add_action(QAction('&LHBA法', self.w_parent), 'unsuper_change_detection') + aht = self.add_action(QAction('&AHT法', self.w_parent), 'unsuper_change_detection') + kpvd = self.add_action(QAction('&KPVD法', self.w_parent), 'unsuper_change_detection') + mohd = self.add_action(QAction('&MOHD法', self.w_parent), 'unsuper_change_detection') + sh = self.add_action(QAction('&SH法', self.w_parent), 'unsuper_change_detection') + cva = self.add_action(QAction('&CVA法', self.w_parent), 'unsuper_change_detection') + mls = self.add_action(QAction('&MLS法', self.w_parent), 'unsuper_change_detection') + pca_kmean = self.add_action(QAction('&PCA-KMean法', self.w_parent), 'unsuper_change_detection') + semi_fcm = self.add_action(QAction('&Semi-FCM法', self.w_parent), 'unsuper_change_detection') + mls_svm = self.add_action(QAction('&MLS-SVM法', self.w_parent), 'unsuper_change_detection') + cva_fcm = self.add_action(QAction('&CVA-FCM法', self.w_parent), 'unsuper_change_detection') + cva_emgmm = self.add_action(QAction('&CVA-EMGMM法', self.w_parent), 'unsuper_change_detection') + gwdm = self.add_action(QAction('&GWDM法', self.w_parent), 'unsuper_change_detection') + + mrf = self.add_action(QAction('&MRF法', self.w_parent), 'super_change_detection') + mad = self.add_action(QAction('&MAD法', self.w_parent), 'super_change_detection') + irmad = self.add_action(QAction('&IRMAD法', self.w_parent), 'super_change_detection') + + dcva = self.add_action(QAction('&DCVA法', self.w_parent), 'ai_change_detection') + dp_fcn = self.add_action(QAction('&DP-FCN法', self.w_parent), 'ai_change_detection') + rcnn = self.add_action(QAction('&RCNN法', self.w_parent), 'ai_change_detection') + + if self.toolbar is not None: + self.toolbar.addAction(diff_method) + self.toolbar.addAction(log_diff) + self.toolbar.addAction(lsts_) + self.toolbar.addAction(lhba) + + + unsuper_change_detection = self.get_action_group('unsuper_change_detection') + super_change_detection = self.get_action_group('super_change_detection') + ai_change_detection = self.get_action_group('ai_change_detection') + unsuper_menu = self.change_detection_menu.addMenu('&非监督') + for action in unsuper_change_detection.actions(): + unsuper_menu.addAction(action) + super_menu = self.change_detection_menu.addMenu('&监督') + for action in super_change_detection.actions(): + super_menu.addAction(action) + ai_menu = self.change_detection_menu.addMenu('&AI') + for action in ai_change_detection.actions(): + ai_menu.addAction(action) + + # self.change_detection_menu.addActionGroup(super_change_detection) + # self.change_detection_menu.addActionGroup(ai_change_detection) + + ''' + Special change detection menu + ''' + + water_change = self.add_action(QAction('&水体变化', self.w_parent), 'special_change_detection') + vegetation_change = self.add_action(QAction('&植被变化', self.w_parent), 'special_change_detection') + build_change = self.add_action(QAction('&房屋变化', self.w_parent), 'special_change_detection') + + self.special_chagne_detec_menu.addAction(water_change) + self.special_chagne_detec_menu.addAction(vegetation_change) + self.special_chagne_detec_menu.addAction(build_change) + + ''' + Postop menu + ''' + slide_window = self.add_action(QAction('&滑动窗口法', self.w_parent), 'noise_reduction') + density = self.add_action(QAction('&密度法', self.w_parent), 'noise_reduction') + + raster_export = self.add_action(QAction('&二值栅格数据导出', self.w_parent), 'export') + txt_pos_export = self.add_action(QAction('&兼容ArcMap的坐标Txt文件', self.w_parent), 'export') + render_export = self.add_action(QAction('&渲染图像导出', self.w_parent), 'export') + + noise_reduction = self.get_action_group('noise_reduction') + export = self.get_action_group('export') + + noise_reduction_menu = self.postop_menu.addMenu('&噪声抑制') + for action in noise_reduction.actions(): + noise_reduction_menu.addAction(action) + export_menu = self.postop_menu.addMenu('&导出') + for action in export.actions(): + export_menu.addAction(action) + + # self.postop_menu.addActionGroup(noise_reduction) + # self.postop_menu.addActionGroup(export) + + ''' + Evaluation menu + ''' + + evaluation = self.add_action(QAction('&评估', self.w_parent), 'evaluation') + self.eval_menu.addAction(evaluation) + + ''' + Help menu + ''' + about = self.add_action(QAction('&关于', self.w_parent), 'about') + about.triggered.connect(lambda : AboutDialog(self.w_parent).show()) + self.help_menu.addAction(about) + + self.message_box.info('Menu init finished') + self.message_box.info(self.actions.keys()) + for group in self.action_groups.keys(): + self.message_box.info('%s:' % (group)) + for action in self.action_groups[group].actions(): + action.setEnabled(False) + self.message_box.info('\t%s' % (action.text())) + + ''' + Enabled actions + ''' + about.setEnabled(True) + project_create.setEnabled(True) + project_open.setEnabled(True) + + Project().project_init.connect(self.project_init) + + if self.status_bar is not None: + corr_widget = QLabel(self.status_bar) + # corr_widget.setLineWidth(200) + corr_widget.setFixedWidth(200) + self.status_bar.addWidget(corr_widget) + scale_widget = QLabel(self.status_bar) + scale_widget.setFixedWidth(200) + self.status_bar.addWidget(scale_widget) + self.double_map.corr_changed.connect(corr_widget.setText) + self.double_map.scale_changed.connect(scale_widget.setText) + + def project_create(self): + project = Project() + if project.is_init: + project.save() + + projec_create = Create(self.w_parent) + if(projec_create.exec_()): + project.setup(os.path.join(projec_create.file, projec_create.name + '.prj')) + project.is_init = True + project.cell_size = projec_create.cell_size + + def project_init(self, state): + self.message_box.info('Project init') + for group in self.action_groups.keys(): + # self.message_box.info('%s:' % (group)) + for action in self.action_groups[group].actions(): + action.setEnabled(state) + # self.message_box.info('\t%s' % (action.text())) + + self.message_box.info('Project init finished') + + def project_open(self): + if Project().is_init: + Project().save() + + project_file = QFileDialog.getOpenFileName(self.w_parent, '打开工程', '.', '*.prj') + if project_file[0] != '': + Project().clear() + Project().setup(project_file[0]) + + def project_save(self): + if Project().is_init: + Project().save() + + def data_load(self): + if Project().is_init: + Project().save() + + def view_setting(self): + pass + + + def add_action(self, action, group=None): + if group is None: + self.actions[action.text()] = action + else: + if group not in self.action_groups: + self.action_groups[group] = QActionGroup(self.w_parent) + self.action_groups[group].setExclusive(True) + self.action_groups[group].addAction(action) + self.action_group_actions[group] = action + return action + + def get_action(self, action_name, group_name=None): + if action_name in self.actions: + return self.actions[action_name] + else: + if group_name is None: + return None + else: + if group_name not in self.action_group_actions: + return None + else: + group = self.action_group_actions[group_name] + for action in group.actions(): + if action.text() == action_name: + return action + return None + + def get_action_group(self, group_name): + return self.action_groups[group_name] + + def get_action_group_action(self, group_name): + return self.action_group_actions[group_name] + + def get_action_group_actions(self, group_name): + return self.action_group_actions[group_name].actions() + + def get_actions(self): + return self.actions \ No newline at end of file diff --git a/gui/about.py b/gui/about.py new file mode 100644 index 0000000..718dee8 --- /dev/null +++ b/gui/about.py @@ -0,0 +1,42 @@ +from PyQt5.QtWidgets import QDialog, QApplication, QLabel, QTextEdit, QVBoxLayout +from PyQt5.QtCore import Qt + + +class AboutDialog(QDialog): + def __init__(self, parent=None): + super(AboutDialog, self).__init__(parent) + self.setWindowTitle("About") + self.setFixedSize(800, 400) + + self.label = QLabel("

"+ QApplication.applicationName() + "

") + self.label.setAlignment(Qt.AlignCenter) + self.label.setStyleSheet("font-size: 20px;") + + self.label2 = QLabel("

Version: " + QApplication.applicationVersion() + "

") + self.label2.setAlignment(Qt.AlignCenter) + self.label2.setStyleSheet("font-size: 15px;") + + self.label3 = QLabel("

" + QApplication.organizationName() + "

") + self.label3.setAlignment(Qt.AlignCenter) + self.label3.setStyleSheet("font-size: 15px;") + + self.label4 = QLabel("

Copyright (c) 2020

") + self.label4.setAlignment(Qt.AlignCenter) + self.label4.setStyleSheet("font-size: 10px;") + + self.text = QTextEdit() + self.text.setReadOnly(True) + self.text.setText(''' + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + ''') + + self.layout = QVBoxLayout() + self.layout.addWidget(self.label) + self.layout.addWidget(self.label2) + self.layout.addWidget(self.label3) + self.layout.addWidget(self.label4) + self.layout.addWidget(self.text) + self.setLayout(self.layout) diff --git a/gui/layertree.py b/gui/layertree.py new file mode 100644 index 0000000..347b545 --- /dev/null +++ b/gui/layertree.py @@ -0,0 +1,54 @@ + +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtCore import Qt,QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import (QTreeView, QTreeWidgetItem, QAbstractItemView, QHeaderView, QStyleFactory) + +from utils.project import PairLayer + +class LayerTree(QtWidgets.QWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.tree_view = QTreeView(self) + self.tree = QtWidgets.QTreeWidget(self) + self.tree.setColumnCount(1) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.right_menu_show) + self.root=QTreeWidgetItem(self.tree) + self.tree.setHeaderHidden(True) + # self.tree.setHeaderLabels(['图层']) + self.root.setText(0,'Root') + + child1=QTreeWidgetItem() + child1.setText(0,'child1') + child1.setCheckState(0,Qt.Checked) + + self.root.addChild(child1) + self.tree.expandAll() + + self.tree.addTopLevelItem(self.root) + + self.tree.clicked.connect(self.onClicked) + + layout = QtWidgets.QGridLayout() + layout.addWidget(self.tree) + self.setLayout(layout) + self.setLayoutDirection(Qt.LeftToRight) + + def onClicked(self,index): + print(index.row()) + + def add_layer(self, layer:PairLayer): + pass + + def right_menu_show(self, position): + rightMenu = QtWidgets.QMenu(self) + # QAction = QtWidgets.QAction(self.menuBar1) + self.actionreboot = QtWidgets.QAction('zhangji') + self.actionreboot.setObjectName("actionreboot") + self.actionreboot.setText('aaa') + rightMenu.addAction(self.actionreboot) + + rightMenu.exec_(self.mapToGlobal(position)) + diff --git a/gui/license.py b/gui/license.py new file mode 100644 index 0000000..31b161d --- /dev/null +++ b/gui/license.py @@ -0,0 +1,74 @@ +import shutil +from PyQt5 import QtWidgets +from PyQt5 import QtCore +from PyQt5.QtGui import QIcon +import os + +from utils.license import LicenseHelper + +class License(QtWidgets.QDialog): + + def __init__(self, parent = None, flags = QtCore.Qt.WindowFlags() ) -> None: + super().__init__(parent, flags) + self.setWindowTitle("License") + self.setWindowIcon(QIcon(os.path.join("gui", "icons", "license.png"))) + self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint) + self.setFixedSize(600, 400) + + self.text = QtWidgets.QLineEdit() + self.text.setReadOnly(False) + self.label = QtWidgets.QLabel() + self.label.setText("License File Path: ") + + self.setModal(True) + + self.btn_open = QtWidgets.QPushButton("Open") + self.btn_open.clicked.connect(self.open_file) + + hlayout = QtWidgets.QHBoxLayout() + hlayout.addWidget(self.label, 0, alignment=QtCore.Qt.AlignTop) + hlayout.addWidget(self.text, 0, alignment=QtCore.Qt.AlignTop) + hlayout.addWidget(self.btn_open, 0, alignment=QtCore.Qt.AlignTop) + + self.btn_ok = QtWidgets.QPushButton("OK") + self.btn_ok.clicked.connect(self.ok_clicked) + + hlayout2 = QtWidgets.QHBoxLayout() + hlayout2.addWidget(self.btn_ok, alignment = QtCore.Qt.AlignRight, stretch= 0) + + vlayout = QtWidgets.QVBoxLayout() + + infobox = QtWidgets.QTextEdit(self) + infobox.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + infobox.setPlainText(""" + This program is NOT free software: you can NOT redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Copy the MAC address to the clipboard and send it to the developer. + MAC address: {}""".format(LicenseHelper().get_mac_address())) + + vlayout.addLayout(hlayout) + vlayout.addWidget(infobox) + vlayout.addLayout(hlayout2) + + self.setLayout(vlayout) + + def open_file(self) -> None: + file_path = QtWidgets.QFileDialog.getOpenFileName(self, "Open File", "", "License Files (*.*)") + if file_path[0]: + self.text.setText(file_path[0]) + # self.label.setText("License File Path: " + file_path[0]) + + def ok_clicked(self) -> None: + if self.text.text() == "": + QtWidgets.QMessageBox.warning(self, "Warning", "Please select a license file.") + else: + pth = self.text.text() + if not os.path.exists(pth): + QtWidgets.QMessageBox.warning(self, "Warning", "The selected file does not exist.") + else: + shutil.copy(pth, os.path.join("lic", "license.lic")) + self.accept() + self.close() \ No newline at end of file diff --git a/gui/mainwindow.py b/gui/mainwindow.py new file mode 100644 index 0000000..d2ec7d3 --- /dev/null +++ b/gui/mainwindow.py @@ -0,0 +1,113 @@ +import pdb +from PyQt5.QtWidgets import QWidget, QApplication, QMainWindow +from PyQt5.QtCore import Qt, QSize +from PyQt5.QtGui import QIcon +from PyQt5 import QtGui +from PyQtAds import QtAds +from actions.actions import ActionManager +from gui.layertree import LayerTree +from gui.mapcanvas import DoubleCanvas +from gui.messagebox import MessageBox +from gui.result import ResultTable +from utils import Settings +from utils.project import Project + +class MainWindow(QMainWindow): + + def __init__(self, parent=None, **kargs): + super().__init__(parent) + self.current_instance = kargs.get('current_instance', 0) + if self.current_instance > 0: + self.setWindowTitle(QApplication.applicationName() + ' ' + str(self.current_instance)) + else: + self.setWindowTitle(QApplication.applicationName()) + self.setWindowIcon(QIcon(":/icons/logo.svg")) + + self.setAcceptDrops(True) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.set_toolbar() + self.set_pannels() + Project(self).connect( + self.double_map, + self.layer_tree, + self.message_box, + self.result_box) + self.action_manager = ActionManager( + self.double_map, + self.layer_tree, + self.follow_box, + self.result_box, + self.message_box, self) + self.action_manager.set_menus(self.menuBar()) + self.action_manager.set_toolbar(self.toolbar) + self.action_manager.set_status_bar(self.statusBar()) + + self.action_manager.set_actions() + + self.resize(*Settings.General().size) + + + def set_toolbar(self): + self.toolbar = self.addToolBar('Toolbar') + self.toolbar.setMovable(False) + self.toolbar.setFloatable(False) + self.toolbar.setIconSize(QSize(16, 16)) + self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu) + self.toolbar.setLayoutDirection(Qt.LeftToRight) + pass + + + def set_pannels(self): + + self.dock_manager = QtAds.CDockManager(self) + self.dock_manager.setContextMenuPolicy(Qt.CustomContextMenu) + + self.double_map = DoubleCanvas(self) + central_dock_widget = QtAds.CDockWidget(self.tr("Canvas")) + central_dock_widget.setWidget(self.double_map) + central_dock_area = self.dock_manager.setCentralWidget(central_dock_widget) + central_dock_area.setAllowedAreas(QtAds.DockWidgetArea.OuterDockAreas) + + self.double_map.setContextMenuPolicy(Qt.CustomContextMenu) + + self.layer_tree = LayerTree(self) + # self.layer_tree.setContextMenuPolicy(Qt.CustomContextMenu) + + def set_docker_fixed(docker): + docker.setFeature(QtAds.ads.CDockWidget.DockWidgetFeature.DockWidgetClosable , False) + docker.setFeature(QtAds.ads.CDockWidget.DockWidgetFeature.DockWidgetMovable , False) + docker.setFeature(QtAds.ads.CDockWidget.DockWidgetFeature.DockWidgetFloatable , False) + + self.layer_tree_dock = QtAds.CDockWidget(self.tr("图层树"), self) + + self.layer_tree_dock.setWidget(self.layer_tree) + left_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.LeftDockWidgetArea, self.layer_tree_dock, central_dock_area) + self.left_arre = left_area + self.follow_dock = QtAds.CDockWidget(self.tr("流程")) + self.follow_box = QWidget(self) + self.follow_dock.setWidget(self.follow_box) + self.dock_manager.addDockWidget(QtAds.DockWidgetArea.BottomDockWidgetArea, self.follow_dock, left_area) + + self.result_dock = QtAds.CDockWidget(self.tr("结果")) + self.result_box = ResultTable(self) + self.result_dock.setWidget(self.result_box) + bottom_area = self.dock_manager.addDockWidget(QtAds.DockWidgetArea.BottomDockWidgetArea, self.result_dock, central_dock_area) + self.message_dock = QtAds.CDockWidget(self.tr("消息")) + self.message_box = MessageBox(self, MessageBox.INFO) + self.message_dock.setWidget(self.message_box) + self.dock_manager.addDockWidget(QtAds.DockWidgetArea.RightDockWidgetArea, self.message_dock, bottom_area) + # bottom_area.setCurrentDockWidget(self.result_dock) + self.bottom_area = bottom_area + + set_docker_fixed(self.layer_tree_dock) + set_docker_fixed(self.follow_dock) + set_docker_fixed(self.result_dock) + set_docker_fixed(self.message_dock) + + def closeEvent(self, event): + pass + + def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + Settings.General().size = (a0.size().width(), a0.size().height()) + return super().resizeEvent(a0) \ No newline at end of file diff --git a/gui/mapcanvas.py b/gui/mapcanvas.py new file mode 100644 index 0000000..0a51fcf --- /dev/null +++ b/gui/mapcanvas.py @@ -0,0 +1,491 @@ + +# from alg.utils import random_color +# from mul.mulgrubcut import GrabCut +import multiprocessing +# from alg.grubcut import grubcut +# from gui.layerselect import LayerSelect +# from gui.default import get_default_category_colors, get_default_category_keys +# from os import truncate +from pathlib import Path +from PyQt5.QtCore import QSettings, QUrl, pyqtSignal, Qt, QVariant +from PyQt5.QtWidgets import QMessageBox, QWidget, QHBoxLayout +from PyQt5.QtGui import QColor, QDragEnterEvent, QDropEvent + +from qgis.core import QgsPointXY, QgsRasterLayer, QgsVectorLayer, QgsFeature, QgsGeometry, QgsCategorizedSymbolRenderer, QgsRendererCategory, QgsFillSymbol, QgsPalLayerSettings, QgsRuleBasedLabeling, QgsTextFormat +from qgis.gui import QgsMapCanvas +from qgis.core import QgsVectorLayerExporter, QgsVectorFileWriter, QgsProject, QgsField, QgsRasterFileWriter, QgsRasterPipe +import threading +import tempfile +import cv2 +import os + +class DoubleCanvas(QWidget): + corr_changed = pyqtSignal(str) + scale_changed = pyqtSignal(str) + + def __init__(self, parent = None) -> None: + super().__init__(parent) + self.setAcceptDrops(False) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.mapcanva1 = CanvasWidget(self) + self.mapcanva2 = CanvasWidget(self) + self.mapcanva1.setCanvasColor(QColor(255, 255, 255)) + self.mapcanva2.setCanvasColor(QColor(255, 255, 255)) + + self.mapcanva1.update_coordinates_text.connect(self.corr_changed) + self.mapcanva2.update_coordinates_text.connect(self.corr_changed) + + self.mapcanva1.update_scale_text.connect(self.scale_changed) + self.mapcanva2.update_scale_text.connect(self.scale_changed) + + layout = QHBoxLayout(self) + layout.addWidget(self.mapcanva1) + layout.addWidget(self.mapcanva2) + + self.setLayout(layout) + +class CanvasWidget(QgsMapCanvas): + update_coordinates_text = pyqtSignal(str) + update_scale_text = pyqtSignal(str) + + def __init__(self, parent): + super().__init__(parent) + self.current_raster_layer = None + self.current_vector_layer = None + + self.setCanvasColor(Qt.white) + self.enableAntiAliasing(True) + self.setAcceptDrops(False) + + # coordinates updated + def coordinates2text(pt:QgsPointXY): + return self.update_coordinates_text.emit("X: {:.5f}, Y: {:.5f}".format(pt.x(), pt.y())) + self.xyCoordinates.connect(coordinates2text) + self.scaleChanged.connect(lambda _ : self.update_scale_text.emit("1 : {:.3f}".format(self.scale()))) + + self.total_f = 0 + self.start_extract = False + self.label_pal = None + # self.result_layers = [] + + def dragEnterEvent(self, e:QDragEnterEvent) -> None: + ''' + Can drag + ''' + candidates = [".tif", ".tiff", ".jpg", ".jpeg", ".bmp", ".png"] + + if e.mimeData().hasUrls(): + if Path(e.mimeData().urls()[0].toLocalFile()).suffix in candidates: + e.accept() + return + + e.ignore() + + def dropEvent(self, e:QDropEvent) -> None: + ''' + Drop image to the canvas + ''' + url_path = e.mimeData().urls()[0] + image_path = QUrl(url_path).toLocalFile() + self.load_image(image_path) + + def load_image(self, path) -> None: + if not Path(path).exists(): + return + + raster_layer = QgsRasterLayer(path, Path(path).name) + if not raster_layer.isValid(): + print("栅格图层加载失败!") + raster_layer.file_path = path + + # self.layers.insert(0, raster_layer) + # self.layers.insert(0, vector_layer) + # if self.current_raster_layer: + # del self.current_raster_layer + # if self.current_vector_layer: + # del self.current_vector_layer + + QgsProject.instance().addMapLayer(raster_layer) + self.current_raster_layer = raster_layer + # self.current_vector_layer = vector_layer + self.setExtent(raster_layer.extent()) + # self.setLayers([vector_layer, raster_layer]) + self.zoomToFeatureExtent(raster_layer.extent()) + self.have_current_image.emit(True) + + def load_result_from_txt(self, path) -> None: + if not Path(path).exists(): + return + # vector_layer = QgsVectorLayer("Polygon?field=category:string(20)&field=confidence:double", Path(path).name, "memory") + vector_layer = QgsVectorLayer("Polygon?field=category:string(20)&field=confidence:double&field=renderkey:string(32)&field=isman:boolean&field=isauto:boolean&field=label:string(64)", Path(path).name + ' outline', "memory") + + if not vector_layer.isValid(): + print("矢量图层加载失败!") + vector_layer.setLabelsEnabled(True) + lyr = QgsPalLayerSettings() + lyr.enabled = True + lyr.fieldName = 'label' # default in data sources + # lyr.textFont = self._TestFont + lyr.textNamedStyle = 'Medium' + text_format = QgsTextFormat() + text_format.color = QColor('#ffffff') + text_format.background().color = QColor('#000000') + text_format.buffer().setEnabled(True) + text_format.buffer().setSize(1) + text_format.buffer().setOpacity(0.5) + lyr.setFormat(text_format) + self.label_pal = lyr + root = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings()) + rule = QgsRuleBasedLabeling.Rule(lyr) + rule.setDescription('label') + root.appendChild(rule) + #Apply label configuration + rules = QgsRuleBasedLabeling(root) + vector_layer.setLabeling(rules) + vector_layer.triggerRepaint() + # lyr.writeToLayer(vector_layer) + vector_layer.setRenderer(self.__get_categorical_renderer("renderkey")) + QgsProject.instance().addMapLayer(vector_layer) + self.current_vector_layer = vector_layer + # provider = self.current_vector_layer.dataProvider() + # provider.truncate() + self.current_vector_layer.startEditing() + # objects = [] + features = [] + with open(path) as f: + for line in f.readlines(): + item_data = line.split("\n")[0].split(" ") + if len(item_data) == 1 + 4 * 2: + cls_name = item_data[0] + item_data[2] = -1.0 * float(item_data[2]) + item_data[4] = -1.0 * float(item_data[4]) + item_data[6] = -1.0 * float(item_data[6]) + item_data[8] = -1.0 * float(item_data[8]) + wkt = "POLYGON (({} {}, {} {}, {} {}, {} {}))".format(*item_data[1:]) + conf = 1.0 + else: + cls_name = item_data[8] + # print(cls_name) + # print(cls_name[0]) + # print(cls_name[0].isalpha()) + if cls_name[0].isalpha(): + item_data[1] = -1.0 * float(item_data[1]) + item_data[3] = -1.0 * float(item_data[3]) + item_data[5] = -1.0 * float(item_data[5]) + item_data[7] = -1.0 * float(item_data[7]) + conf = 1.0 + wkt = "POLYGON (({} {}, {} {}, {} {}, {} {}))".format(*item_data[:8]) + else: + cls_name = item_data[0] + conf = float(item_data[1]) + item_data[3] = -1.0 * float(item_data[3]) + item_data[5] = -1.0 * float(item_data[5]) + item_data[7] = -1.0 * float(item_data[7]) + item_data[9] = -1.0 * float(item_data[9]) + wkt = "POLYGON (({} {}, {} {}, {} {}, {} {}))".format(*item_data[2:]) + + feat = QgsFeature(self.current_vector_layer.fields()) + feat.setGeometry(QgsGeometry.fromWkt(wkt)) + feat.setAttribute('category', cls_name) + feat.setAttribute('confidence', conf) + feat.setAttribute('renderkey', cls_name) + feat.setAttribute('isman', False) + feat.setAttribute('isauto', True) + feat.setAttribute('label', f'{ cls_name},{conf:.3f}') + features.append(feat) + # objects.append({ + # "category": item_data[0], + # "confidence": item_data[1], + # "fid": feat.id() + # }) + self.current_vector_layer.addFeatures(features) + self.current_vector_layer.commitChanges() + self.have_current_vector.emit(True) + self.layer_update() + + def clear_vector(self): + if self.current_vector_layer is not None: + provider = self.current_vector_layer.dataProvider() + provider.truncate() + self.layer_update() + + def change_current_vector_layer(self, vector_layer): + if self.current_vector_layer is not None: + self.current_vector_layer.removeSelection() + self.current_vector_layer = vector_layer + + self.layer_update() + + def layer_update(self): + if self.current_vector_layer is None: + self.object_updated.emit([]) + return + self.current_vector_layer.updateExtents() + self.refresh() + objects = [] + for feature in self.current_vector_layer.getFeatures(): + objects.append({ + "category": feature['category'], + "confidence": feature['confidence'], + "renderkey": feature['renderkey'], + 'isman': feature['isman'], + 'isauto': feature['isauto'], + "fid": feature.id() + }) + self.object_updated.emit(objects) + + def selectd_changed(self, items:list): + if len(items) == 0: + self.current_vector_layer.removeSelection() + else: + self.current_vector_layer.selectByIds(list(item['fid'] for item in items)) + + def item_change(self, items:list): + self.current_vector_layer.startEditing() + features = list(self.current_vector_layer.getFeatures()) + for f in features: + has_f = False + for item in items: + if f.id() == item['fid']: + # f = QgsFeature(f) + has_f = True + f.setAttribute('category', item['category']) + f.setAttribute('confidence', item['confidence']) + f.setAttribute('renderkey', item['renderkey']) + f.setAttribute('isman', item['isman']) + f.setAttribute('isauto', item['isauto']) + self.current_vector_layer.updateFeature(f) + break + if has_f: + continue + + self.current_vector_layer.deleteFeature(f.id()) + + self.current_vector_layer.commitChanges() + self.current_vector_layer.updateExtents() + # print(self.current_vector_layer.fields()) + + self.refresh() + + def zoom_to_full_extent(self) -> None: + if self.current_raster_layer: + self.zoomToFeatureExtent(self.current_raster_layer.extent()) + + def __get_categorical_renderer(self, fieldname:str) -> QgsCategorizedSymbolRenderer: + settings = QSettings(self) + + category_keys = settings.value("keys", get_default_category_keys()) + category_colors = settings.value("colors", get_default_category_colors()) + + settings.beginGroup("Category") + if len(category_colors) < len(category_keys): + for _ in range(len(category_keys) - len(category_colors)): + category_colors.append(random_color()) + settings.setValue('colors', category_colors) + settings.endGroup() + categorized_renderer = QgsCategorizedSymbolRenderer() + for key, color in zip(category_keys, category_colors): + fill_color = QColor(color) + fill_color.setAlphaF(0.3) + categorized_renderer.addCategory(\ + QgsRendererCategory( + key, + QgsFillSymbol.createSimple( + {"color":fill_color.name(QColor.HexArgb),"outline_color":color, "outline_width":"1"}), '')) + categorized_renderer.setClassAttribute(fieldname) + return categorized_renderer + + def export_to_raster(self, path) -> None: + if self.current_vector_layer is None: + return + + def load_extract_result(self, res): + r = self.current_raster_layer + vector_layer = QgsVectorLayer("Polygon?field=category:string(20)&field=confidence:double&field=renderkey:string(32)&field=isman:boolean&field=isauto:boolean", Path(r.file_path).name + ' outline', "memory") + # vector_layer = QgsVectorLayer(tempfile) + if not vector_layer.isValid(): + print("矢量图层加载失败!") + + vector_layer.setRenderer(self.__get_categorical_renderer("renderkey")) + lyr = QgsPalLayerSettings() + lyr.enabled = True + lyr.fieldName = 'label' # default in data sources + # lyr.textFont = self._TestFont + lyr.textNamedStyle = 'Medium' + text_format = QgsTextFormat() + text_format.color = QColor('#ffffff') + text_format.background().color = QColor('#000000') + text_format.buffer().setEnabled(True) + text_format.buffer().setSize(1) + text_format.buffer().setOpacity(0.5) + lyr.setFormat(text_format) + self.label_pal = lyr + root = QgsRuleBasedLabeling.Rule(QgsPalLayerSettings()) + rule = QgsRuleBasedLabeling.Rule(lyr) + rule.setDescription('label') + root.appendChild(rule) + #Apply label configuration + rules = QgsRuleBasedLabeling(root) + vector_layer.setLabeling(rules) + vector_layer.triggerRepaint() + vector_layer.startEditing() + features = [] + for f in res: + pts = f[0] + prop = f[1] + # pts = grubcut(img_path, pts, False, True, False ) + pts = list( f'{p[0]} {p[1]}' for p in pts ) + wkt = f'POLYGON (( {",".join(pts)} ))' + # geometry = QgsGeometry.fromWkt(wkt) + feat = QgsFeature(vector_layer.fields()) + feat.setGeometry(QgsGeometry.fromWkt(wkt)) + feat.setAttribute('category', prop['category']) + feat.setAttribute('confidence', prop['confidence']) + feat.setAttribute('renderkey', prop['category']) + feat.setAttribute('isman', False) + feat.setAttribute('isauto', True) + features.append(feat) + + vector_layer.addFeatures(features) + vector_layer.commitChanges() + QgsProject.instance().addMapLayer(vector_layer) + self.process_end.emit() + r.has_extract = True + self.start_extract = False + r.extract_layer = vector_layer + self.layer_update() + + def run_thread(self, conn, pp): + all_ok = False + # print(pp.is_alive) + while pp.is_alive: + r = conn.recv() + # print(r) + if all_ok: + self.extract_end.emit(r) + break + if int(r) == self.total_f - 1: + all_ok = True + self.process_update.emit(r) + # print(conn.recv()) + def grubcut(self, v, r): + # for f in v.getFeatures(): + if self.start_extract: + return + self.current_raster_layer = r + img_path = r.file_path + if getattr(r, 'has_extract', False): + vector_layer = r.extract_layer + try: + QgsProject.instance().removeMapLayer(vector_layer) + except: + pass + + # self.current_vector_layer = vector_layer + features = [] + points = [] + for f in v.getFeatures(): + pts = f.geometry().vertices() + pts = list([ vr.x(), vr.y() ] for vr in pts) + points.append(pts) + features.append({ + 'category': f['category'], + 'confidence': f['confidence'] + }) + self.total_f = len(points) + self.start_extract = True + self.process_start.emit([0, self.total_f]) + parent_conn, child_conn = multiprocessing.Pipe() + t = GrabCut(child_conn, img_path, points, features) + p = threading.Thread(target=self.run_thread, args=(parent_conn,t)) + t.start() + p.start() + + def export_to(self, path, filter_name) -> None: + if filter_name == 'Shp (*.shp)': + if self.current_vector_layer is None: + return + ls = LayerSelect(self) + ls.show() + ls.exec() + if ls.result() == LayerSelect.OK: + save_options = QgsVectorFileWriter.SaveVectorOptions() + save_options.driverName = "ESRI Shapefile" + save_options.fileEncoding = "UTF-8" + transform_context = QgsProject.instance().transformContext() + error = QgsVectorFileWriter.writeAsVectorFormatV2(ls.value, + path, + transform_context, + save_options) + if error[0] == QgsVectorFileWriter.NoError: + print("又成功了!") + else: + print(error) + + if filter_name == 'JPEG Images(*.jpg)': + file_name = path + '.tif' + extent = self.current_raster_layer.extent() + width, height = self.current_raster_layer.width(), self.current_raster_layer.height() + + pipe = QgsRasterPipe() + provider = self.current_raster_layer.dataProvider() + pipe.set(provider.clone()) + + file_writer = QgsRasterFileWriter(file_name) + error = file_writer.writeRaster(pipe, + width, + height, + extent, + self.current_raster_layer.crs()) + + target_img = cv2.imread(file_name) + jpg_file_name = path + '.jpg' + cv2.imwrite(jpg_file_name, target_img) + os.remove(file_name) + if error == QgsRasterFileWriter.NoError: + QMessageBox.about(self, 'Export Files', '导出JPEG图像成功!') + else: + QMessageBox.about(self, 'Export Files', '导出JPEG图像失败!') + + if filter_name == 'TIFF Images(*.tif)': + file_name = path + '.tif' + extent = self.current_raster_layer.extent() + width, height = self.current_raster_layer.width(), self.current_raster_layer.height() + + pipe = QgsRasterPipe() + provider = self.current_raster_layer.dataProvider() + pipe.set(provider.clone()) + + file_writer = QgsRasterFileWriter(file_name) + error = file_writer.writeRaster(pipe, + width, + height, + extent, + self.current_raster_layer.crs()) + if error == QgsRasterFileWriter.NoError: + QMessageBox.about(self, 'Export Files', '导出TIFF图像成功!') + else: + QMessageBox.about(self, 'Export Files', '导出TIFF图像失败!') + if filter_name == 'PNG Images(*.png)': + file_name = path + '.tif' + extent = self.current_raster_layer.extent() + width, height = self.current_raster_layer.width(), self.current_raster_layer.height() + + pipe = QgsRasterPipe() + provider = self.current_raster_layer.dataProvider() + pipe.set(provider.clone()) + + file_writer = QgsRasterFileWriter(file_name) + error = file_writer.writeRaster(pipe, + width, + height, + extent, + self.current_raster_layer.crs()) + target_img = cv2.imread(file_name) + jpg_file_name = path + '.png' + cv2.imwrite(jpg_file_name, target_img) + os.remove(file_name) + if error == QgsRasterFileWriter.NoError: + QMessageBox.about(self, 'Export Files', '导出PNG图像成功!') + else: + QMessageBox.about(self, 'Export Files', '导出PNG图像失败!') \ No newline at end of file diff --git a/gui/messagebox.py b/gui/messagebox.py new file mode 100644 index 0000000..56da67e --- /dev/null +++ b/gui/messagebox.py @@ -0,0 +1,70 @@ +from PyQt5.QtWidgets import QTextEdit +from PyQt5 import QtWidgets +from PyQt5.QtGui import QTextCursor +from PyQt5.QtCore import Qt +from datetime import datetime, time +class MessageBox(QTextEdit): + + INFO=0 + WARNING=1 + DEBUG=2 + ERROR=3 + + def __init__(self, parent=None, level=0): + super().__init__(parent) + self.setReadOnly(True) + self.setTextInteractionFlags(Qt.TextSelectableByMouse) + self.setStyleSheet("QTextEdit { background-color: #f0f0f0; }") + self.msg = '' + self.level = level + + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.right_menu_show) + + def right_menu_show(self, position): + rightMenu = QtWidgets.QMenu(self) + # QAction = QtWidgets.QAction(self.menuBar1) + action = QtWidgets.QAction('清空') + action.triggered.connect(self.clear) + rightMenu.addAction(action) + + rightMenu.exec_(self.mapToGlobal(position)) + + def append(self, text): + self.msg += '
' + text + self.setText(self.msg) + cursor = self.textCursor() + # QTextCursor + cursor.movePosition(QTextCursor.End) + self.setTextCursor(cursor) + + def info(self, text): + if self.level <= self.INFO: + timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.append('[INFO] %s
'%timestr + str(text)) + # self.append(text) + # self.append(text) + + def warning(self, text): + if self.level <= self.WARNING: + timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.append('[WARNING] %s
'%timestr + str(text)) + + def debug(self, text): + if self.level <= self.DEBUG: + timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.append('[DEBUG] %s
'%timestr + str(text)) + # self.append(text) + # self.append(text) + + def error(self, text): + if self.level <= self.ERROR: + timestr = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self.append('[ERROR] %s
'%timestr + str(text)) + # self.append(text) + # self.append(text) + + def clear(self): + self.msg = '' + self.setText(self.msg) + \ No newline at end of file diff --git a/gui/project.py b/gui/project.py new file mode 100644 index 0000000..9d28ae5 --- /dev/null +++ b/gui/project.py @@ -0,0 +1,72 @@ +from PyQt5.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import Qt +from utils.setting import Settings +class Create(QDialog): + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.setWindowTitle('Create Project') + self.setWindowIcon(QIcon(":/icons/logo.svg")) + + self.file = str(Settings.General().root) + self.name = '未命名' + self.max_memory = Settings.Project().max_memory + self.cell_size = Settings.Project().cell_size + + file_label = QLabel('Project Dir:') + file_label.setFixedWidth(100) + file_input = QLineEdit() + file_input.setPlaceholderText('Project Dir') + file_input.setToolTip('Project Dir') + file_input.setReadOnly(True) + file_input.setText(self.file) + + name_label = QLabel('Project Name:') + name_label.setFixedWidth(100) + name_input = QLineEdit() + name_input.setPlaceholderText('Project Name') + name_input.setToolTip('Project Name') + + name_input_layout = QHBoxLayout() + name_input_layout.addWidget(name_label) + name_input_layout.addWidget(name_input) + + file_input_layout = QHBoxLayout() + file_input_layout.addWidget(file_label) + file_input_layout.addWidget(file_input) + + cell_size_label = QLabel('Cell Size:') + cell_size_label.setFixedWidth(100) + cell_size_x_label = QLabel('X:') + cell_size_y_label = QLabel('Y:') + cell_size_x_input = QLineEdit() + cell_size_y_input = QLineEdit() + cell_size_x_input.setPlaceholderText('Cell Size X') + cell_size_y_input.setPlaceholderText('Cell Size Y') + cell_size_x_input.setToolTip('Cell Size X') + cell_size_y_input.setToolTip('Cell Size Y') + + cell_input_layout = QHBoxLayout() + cell_input_layout.addWidget(cell_size_label) + cell_input_layout.addWidget(cell_size_x_label) + cell_input_layout.addWidget(cell_size_x_input) + cell_input_layout.addWidget(cell_size_y_label) + cell_input_layout.addWidget(cell_size_y_input) + + ok_button = QPushButton('OK') + cancel_button = QPushButton('Cancel') + + button_layout = QHBoxLayout() + button_layout.setDirection(QHBoxLayout.RightToLeft) + button_layout.addWidget(ok_button, 0, Qt.AlignRight) + button_layout.addWidget(cancel_button, 0, Qt.AlignRight) + + main_layout = QVBoxLayout() + main_layout.addLayout(file_input_layout) + main_layout.addLayout(name_input_layout) + main_layout.addLayout(cell_input_layout) + main_layout.addLayout(button_layout) + + self.setLayout(main_layout) + diff --git a/gui/result.py b/gui/result.py new file mode 100644 index 0000000..81fd948 --- /dev/null +++ b/gui/result.py @@ -0,0 +1,41 @@ + +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtCore import Qt,QModelIndex +from PyQt5.QtGui import QStandardItemModel, QStandardItem +from PyQt5.QtWidgets import (QTreeView, QTreeWidgetItem, QAbstractItemView, QHeaderView, QStyleFactory) + +from utils.project import PairLayer + +class ResultTable(QtWidgets.QWidget): + + def __init__(self, parent=None): + super(ResultTable, self).__init__(parent) + self.tree_view = QTreeView(self) + self.tree = QtWidgets.QTreeWidget(self) + self.tree.setColumnCount(1) + self.tree.setColumnWidth(0,150) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.root=QTreeWidgetItem(self.tree) + self.tree.setHeaderLabels(['图层']) + self.root.setText(0,'Root') + + child1=QTreeWidgetItem() + child1.setText(0,'child1') + child1.setCheckState(0,Qt.Checked) + + self.root.addChild(child1) + self.tree.expandAll() + + self.tree.addTopLevelItem(self.root) + + self.tree.clicked.connect(self.onClicked) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.tree) + self.setLayout(layout) + + def onClicked(self,index): + print(index.row()) + + def add_layer(self, layer:PairLayer): + pass diff --git a/gui/setting.py b/gui/setting.py new file mode 100644 index 0000000..e69de29 diff --git a/icons/assessment.svg b/icons/assessment.svg new file mode 100644 index 0000000..7c9bd45 --- /dev/null +++ b/icons/assessment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/cancel.svg b/icons/cancel.svg new file mode 100644 index 0000000..f5be325 --- /dev/null +++ b/icons/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/clear.svg b/icons/clear.svg new file mode 100644 index 0000000..72bc3ec --- /dev/null +++ b/icons/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/edit.svg b/icons/edit.svg new file mode 100644 index 0000000..2c23b9e --- /dev/null +++ b/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/exit.svg b/icons/exit.svg new file mode 100644 index 0000000..6008f00 --- /dev/null +++ b/icons/exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/export.svg b/icons/export.svg new file mode 100644 index 0000000..359844e --- /dev/null +++ b/icons/export.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/font.svg b/icons/font.svg new file mode 100644 index 0000000..f92ba9d --- /dev/null +++ b/icons/font.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/full.svg b/icons/full.svg new file mode 100644 index 0000000..6be915f --- /dev/null +++ b/icons/full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/load.svg b/icons/load.svg new file mode 100644 index 0000000..5968cdd --- /dev/null +++ b/icons/load.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/logo.svg b/icons/logo.svg new file mode 100644 index 0000000..4ecf1b3 --- /dev/null +++ b/icons/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/model.svg b/icons/model.svg new file mode 100644 index 0000000..128a1d8 --- /dev/null +++ b/icons/model.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/ok.svg b/icons/ok.svg new file mode 100644 index 0000000..58e528c --- /dev/null +++ b/icons/ok.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/outline.svg b/icons/outline.svg new file mode 100644 index 0000000..0d6e64b --- /dev/null +++ b/icons/outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/paint.svg b/icons/paint.svg new file mode 100644 index 0000000..041f3e8 --- /dev/null +++ b/icons/paint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/pan.svg b/icons/pan.svg new file mode 100644 index 0000000..15759f0 --- /dev/null +++ b/icons/pan.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/qt.svg b/icons/qt.svg new file mode 100644 index 0000000..d5aab41 --- /dev/null +++ b/icons/qt.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/icons/settings.svg b/icons/settings.svg new file mode 100644 index 0000000..ea7e8e8 --- /dev/null +++ b/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/splash.png b/icons/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..82266d9767bb4b7848a520797b68dbbf6dda55c8 GIT binary patch literal 95653 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 DxSt?7 literal 0 HcmV?d00001 diff --git a/icons/start.svg b/icons/start.svg new file mode 100644 index 0000000..19160dd --- /dev/null +++ b/icons/start.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/vector.svg b/icons/vector.svg new file mode 100644 index 0000000..4f88c95 --- /dev/null +++ b/icons/vector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/zoomin.svg b/icons/zoomin.svg new file mode 100644 index 0000000..f830872 --- /dev/null +++ b/icons/zoomin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/zoomout.svg b/icons/zoomout.svg new file mode 100644 index 0000000..aa18703 --- /dev/null +++ b/icons/zoomout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lic/license.lic b/lic/license.lic new file mode 100644 index 0000000..175477c --- /dev/null +++ b/lic/license.lic @@ -0,0 +1 @@ +pGZJMmJtule8fwDCz4mnyHoQa7N6pl5GRdLqfoXREBqG4Xb1jbvgf7RmC8f1+sNpiCFSIt7NgvU362tKhB5UBXn/vUAadG1lOGC70dUhprGzBoqJN7VkAHkNGg0XjoE8H0SCVynr8To7ciwcnmK6HJXre6i+mBdTjACmKseTMlWp480XOt7uHysltORbTA3J \ No newline at end of file diff --git a/license.lic b/license.lic new file mode 100644 index 0000000..175477c --- /dev/null +++ b/license.lic @@ -0,0 +1 @@ +pGZJMmJtule8fwDCz4mnyHoQa7N6pl5GRdLqfoXREBqG4Xb1jbvgf7RmC8f1+sNpiCFSIt7NgvU362tKhB5UBXn/vUAadG1lOGC70dUhprGzBoqJN7VkAHkNGg0XjoE8H0SCVynr8To7ciwcnmK6HJXre6i+mBdTjACmKseTMlWp480XOt7uHysltORbTA3J \ No newline at end of file diff --git a/log.txt b/log.txt new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..549e839 --- /dev/null +++ b/main.py @@ -0,0 +1,9 @@ +from mul.mulstart import MulStart +import logging + +logging.basicConfig(level=logging.INFO, filename='log.txt', filemode='w', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +if __name__ == '__main__': + + t = MulStart() + t.run() \ No newline at end of file diff --git a/mul/mulexport.py b/mul/mulexport.py new file mode 100644 index 0000000..f05d1fa --- /dev/null +++ b/mul/mulexport.py @@ -0,0 +1,32 @@ +import multiprocessing +from alg.txt_export_to import export_to_shp +import os +import glob +from qgis.core import * +class ExportToSHP(multiprocessing.Process): + + def __init__(self, conn, result_path, output_dir): + super(ExportToSHP, self).__init__() + self.conn = conn + self.result_path = result_path + self.result_list = self.get_result_list() + self.output_dir = output_dir + os.makedirs(self.output_dir, exist_ok=True) + + def get_result_list(self): + fl = list(glob.glob(os.path.join(self.result_path, '*.txt'))) + return fl + + def run(self): + # result = [] + self.conn.send(len(self.result_list)) + qgs = QgsApplication([], False) + QgsApplication.initQgis() + for i, p in enumerate(self.result_list): + o = os.path.basename(p) + o = os.path.splitext(o)[0] + r = export_to_shp(p, os.path.join(self.output_dir, o + '.shp')) + # result.append([r, self.feats[i]]) + self.conn.send([i, r, o]) + # self.conn.send(result) + diff --git a/mul/mulgrubcut.py b/mul/mulgrubcut.py new file mode 100644 index 0000000..181a884 --- /dev/null +++ b/mul/mulgrubcut.py @@ -0,0 +1,19 @@ +import multiprocessing +from alg.grubcut import grubcut + +class GrabCut(multiprocessing.Process): + + def __init__(self, conn, img_path, pts = [], feats = []): + super(GrabCut, self).__init__() + self.conn = conn + self.pts = pts + self.feats = feats + self.img_path = img_path + def run(self): + result = [] + for i, p in enumerate(self.pts): + r = grubcut(self.img_path, p, False, True, False) + result.append([r, self.feats[i]]) + self.conn.send(i) + self.conn.send(result) + diff --git a/mul/mulstart.py b/mul/mulstart.py new file mode 100644 index 0000000..a132671 --- /dev/null +++ b/mul/mulstart.py @@ -0,0 +1,63 @@ +import sys +import time +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont, QPixmap +from PyQt5.QtWidgets import QSplashScreen, QProgressBar, QStyleFactory, QMessageBox +from qgis.core import QgsApplication +# from qgis.core import +from gui.mainwindow import MainWindow +import multiprocessing +from gui import license +from utils.setting import Settings + +class MulStart: + + def __init__(self, **kargs) -> None: + super(MulStart, self).__init__() + self.kargs = kargs + def run(self): + + QgsApplication.setAttribute(Qt.AA_EnableHighDpiScaling) + QgsApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) + QgsApplication.setOrganizationName("西安理工大学-ImgSciGroup") + QgsApplication.setApplicationName("Easy Change Detection") + QgsApplication.setApplicationVersion("v0.0.1") + QgsApplication.setFont(QFont("Segoe UI", 10)) + QgsApplication.setStyle(QStyleFactory.create("Fusion")) + + + # pyrcc5 res.qrc -o rc.py + import rc + + app = QgsApplication([], True) + QgsApplication.initQgis() + while not Settings.General().license: + QMessageBox.warning(None, "Warning", "Please select a license file.") + if(license.License().exec_() == license.License.Accepted): + continue + else: + sys.exit(0) + # Create and display the splash screen + splash_pix = QPixmap(':/icons/splash.png') + + splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) + progressBar = QProgressBar(splash) + progressBar.setMaximum(10) + progressBar.setTextVisible(False) + progressBar.setGeometry(46, splash_pix.height() - 60, splash_pix.width()-92, 10) + + splash.show() + for i in range(1, 11): + progressBar.setValue(i) + t = time.time() + while time.time() < t + 0.05: + app.processEvents() + + ex = MainWindow(**self.kargs) + # ex.canvas.load_image(r'data\100001678.jpg') + # ex.canvas.load_result_from_txt(r'data\100001678.txt') + # ex.showMaximized() + ex.show() + splash.finish(ex) + sys.exit(app.exec_()) + \ No newline at end of file diff --git a/res.qrc b/res.qrc new file mode 100644 index 0000000..07a812c --- /dev/null +++ b/res.qrc @@ -0,0 +1,26 @@ + + + icons/splash.png + icons/logo.svg + icons/load.svg + icons/settings.svg + icons/exit.svg + icons/vector.svg + icons/zoomin.svg + icons/zoomout.svg + icons/pan.svg + icons/full.svg + icons/start.svg + icons/clear.svg + icons/edit.svg + icons/export.svg + icons/model.svg + icons/assessment.svg + icons/paint.svg + icons/font.svg + icons/qt.svg + icons/ok.svg + icons/cancel.svg + icons/outline.svg + + diff --git a/test/tree.py b/test/tree.py new file mode 100644 index 0000000..2106dde --- /dev/null +++ b/test/tree.py @@ -0,0 +1,132 @@ +from PyQt5 import QtCore +from PyQt5.QtWidgets import QDesktopWidget +import sys +from PyQt5.QtWidgets import * + +# import tree # tree.py文件 + + +class myTreeWidget: + def __init__(self, objTree): + self.myTree = objTree + # 设置列数 + self.myTree.setColumnCount(1) + # 设置树形控件头部的标题 + self.myTree.setHeaderLabels(['机构列表']) + + # 设置根节点 + self.root = QTreeWidgetItem(self.myTree) + self.root.setText(0, '本单位') + + # 设置树形控件的列的宽度 + self.myTree.setColumnWidth(0, 100) + + # 设置子节点1 + child1 = QTreeWidgetItem(self.root) + child1.setText(0, '市场部') + self.root.addChild(child1) + + # 设置子节点11 + child11 = QTreeWidgetItem(child1) + child11.setText(0, '销售班') + + # 设置子节点2 + child2 = QTreeWidgetItem(self.root) + child2.setText(0, '财务部') + + # 设置子节点21 + child21 = QTreeWidgetItem(child2) + child21.setText(0, '财务一班') + + # 加载根节点的所有属性与子控件 + self.myTree.addTopLevelItem(self.root) + + # TODO 优化2 给节点添加响应事件 + self.myTree.clicked.connect(self.onClicked) + + # 节点全部展开 + self.myTree.expandAll() + + def onClicked(self): + item = self.myTree.currentItem() + print('Key=%s' % (item.text(0))) + + +class MyPyQTMainForm(QMainWindow): + """ + 主界面 + """ + + def __init__(self): + """ + 初始化 + """ + super(MyPyQTMainForm, self).__init__() + # self.setupUi(self) + # 创建树控件对象 + layout = QVBoxLayout() + + tree_widget = QTreeWidget() + # layout.addWidget(tree_widget) + # self.setLayout(layout) + self.layout().addWidget(tree_widget) + self.myTreeTest = myTreeWidget(tree_widget) + # self.myTreeTest = myTreeWidget(self.treeWidget) + + def center(self): + """ + 定义一个函数使得窗口居中显示 + """ + # 获取屏幕坐标系 + screen = QDesktopWidget().screenGeometry() + # 获取窗口坐标系 + size = self.geometry() + newLeft = (screen.width() - size.width()) / 2 + newTop = (screen.height() - size.height()) / 2 + self.move(int(newLeft), int(newTop)) + + def addNode(self): + """ + 添加节点 + """ + print('--- addTreeNode ---') + item = self.myTreeTest.myTree.currentItem() + node = QTreeWidgetItem(item) + node.setText(0, '后勤部') + + def deleteNode(self): + """ + 删除节点 + """ + print('--- delTreeNode ---') + item = self.myTreeTest.myTree.currentItem() + root = self.myTreeTest.myTree.invisibleRootItem() + for item in self.myTreeTest.myTree.selectedItems(): + (item.parent() or root).removeChild(item) + + def modifyNode(self): + """ + 修改节点 + """ + print('--- modifyTreeNode ---') + item = self.myTreeTest.myTree.currentItem() + item.setText(0, '办公室') + + +""" +主函数 +""" +if __name__ == '__main__': + app = QApplication(sys.argv) + myPyMainForm = MyPyQTMainForm() + # 主窗口显示在屏幕中间 + myPyMainForm.center() + + # 禁止最大化按钮 + myPyMainForm.setWindowFlags(QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint) + # 禁止拉伸窗口大小 + myPyMainForm.setFixedSize(myPyMainForm.width(), myPyMainForm.height()) + + # 显示主界面 + myPyMainForm.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..267231c --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,2 @@ +from .setting import Settings +from .license import LicenseHelper \ No newline at end of file diff --git a/utils/license.py b/utils/license.py new file mode 100644 index 0000000..c731d90 --- /dev/null +++ b/utils/license.py @@ -0,0 +1,143 @@ +import base64 +from Crypto.Cipher import AES +import uuid +import hashlib +import datetime + +# + +class AESHelper(object): + def __init__(self, password, iv): + self.password = bytes(password, encoding='utf-8') + self.iv = bytes(iv, encoding='utf-8') + + def pkcs7padding(self, text): + """ + 明文使用PKCS7填充 + 最终调用AES加密方法时,传入的是一个byte数组,要求是16的整数倍,因此需要对明文进行处理 + :param text: 待加密内容(明文) + :return: + """ + bs = AES.block_size # 16 + length = len(text) + bytes_length = len(bytes(text, encoding='utf-8')) + # tips:utf-8编码时,英文占1个byte,而中文占3个byte + padding_size = length if(bytes_length == length) else bytes_length + padding = bs - padding_size % bs + # tips:chr(padding)看与其它语言的约定,有的会使用'\0' + padding_text = chr(padding) * padding + return text + padding_text + + def pkcs7unpadding(self, text): + """ + 处理使用PKCS7填充过的数据 + :param text: 解密后的字符串 + :return: + """ + length = len(text) + unpadding = ord(text[length-1]) + return text[0:length-unpadding] + + def encrypt(self, content): + """ + AES加密 + 模式cbc + 填充pkcs7 + :param key: 密钥 + :param content: 加密内容 + :return: + """ + cipher = AES.new(self.password, AES.MODE_CBC, self.iv) + content_padding = self.pkcs7padding(content) + encrypt_bytes = cipher.encrypt(bytes(content_padding, encoding='utf-8')) + result = str(base64.b64encode(encrypt_bytes), encoding='utf-8') + return result + + def decrypt(self, content): + """ + AES解密 + 模式cbc + 去填充pkcs7 + :param key: + :param content: + :return: + """ + cipher = AES.new(self.password, AES.MODE_CBC, self.iv) + encrypt_bytes = base64.b64decode(content) + decrypt_bytes = cipher.decrypt(encrypt_bytes) + result = str(decrypt_bytes, encoding='utf-8') + result = self.pkcs7unpadding(result) + return result + +def get_aes(): + # AES_SECRET和AES_IV分别为密钥和偏移量 + aes_helper = AESHelper( + 'ao234esorGFSFGubh#$^&@gihdfjl$@4', + 'dergbdzbfdsdrt$g') + return aes_helper + +class LicenseHelper(object): + def generate_license(self, end_date, mac_addr): + print("Received end_date: {}, mac_addr: {}".format(end_date, mac_addr)) + psw = self.hash_msg('smartant' + str(mac_addr)) + license_str = {} + license_str['mac'] = mac_addr + license_str['time_str'] = end_date + license_str['psw'] = psw + s = str(license_str) + licence_result = get_aes().encrypt(s) + return licence_result + + def get_mac_address(self): + mac = uuid.UUID(int=uuid.getnode()).hex[-12:] + return ":".join([mac[e:e + 2] for e in range(0, 11, 2)]) + + def hash_msg(self, msg): + sha256 = hashlib.sha256() + sha256.update(msg.encode('utf-8')) + res = sha256.hexdigest() + return res + + def read_license(self, license_result): + lic_msg = bytes(license_result, encoding="utf8") + license_str = get_aes().decrypt(lic_msg) + license_dic = eval(license_str) + return license_dic + + def check_license_date(self, lic_date): + current_time = datetime.datetime.strftime(datetime.datetime.now() ,"%Y-%m-%d %H:%M:%S") + current_time_array = datetime.datetime.strptime(current_time,"%Y-%m-%d %H:%M:%S") + lic_date_array = datetime.datetime.strptime(lic_date, "%Y-%m-%d %H:%M:%S") + remain_days = lic_date_array - current_time_array + remain_days = remain_days.days + print('lic data:{}'.format(lic_date)) + print('remain_days: {}'.format(remain_days)) + if remain_days < 0 or remain_days == 0: + return False + else: + return True + + def check_license_psw(self, psw): + mac_addr = self.get_mac_address() + hashed_msg = self.hash_msg('smartant' + str(mac_addr)) + if psw == hashed_msg: + return True + else: + return False + + +if __name__ == '__main__': + + lic = LicenseHelper().generate_license('2022-12-31 00:00:00', LicenseHelper().get_mac_address()) + + with open('license.lic', 'w') as f: + f.write(lic[::-1]) + + with open('license.lic', 'r') as f: + license_result = f.read()[::-1] + + license_dic = LicenseHelper().read_license(license_result) + + print(license_dic) + + \ No newline at end of file diff --git a/utils/menu.py b/utils/menu.py new file mode 100644 index 0000000..fc0eefb --- /dev/null +++ b/utils/menu.py @@ -0,0 +1,11 @@ +from functools import wraps + +def as_menu(D, name, icon=None, shortcut=None, tip=None, checkable=False, signal=None, callback=None, enabled=True): + + def func(f): + @wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + D.addMenu(name, icon, shortcut, tip, checkable, signal, callback, enabled, wrapper) + return f + return func \ No newline at end of file diff --git a/utils/project.py b/utils/project.py new file mode 100644 index 0000000..59a0e73 --- /dev/null +++ b/utils/project.py @@ -0,0 +1,121 @@ +import os +from pathlib import Path +from osgeo import gdal, gdal_array +from utils.setting import Settings +from qgis.core import QgsRasterLayer +from PyQt5.QtCore import QObject, pyqtSignal + +def singleton(cls): + _instance = {} + + def inner(*args, **kargs): + if cls not in _instance: + _instance[cls] = cls(*args, **kargs) + return _instance[cls] + return inner + +@singleton +class Project(QObject): + + project_init = pyqtSignal(bool) + + def __init__(self, + parent=None): + super().__init__(parent) + self.is_init = False + self.cell_size = Settings.Project().cell_size + self.max_memory = Settings.Project().max_memory + self.max_threads = Settings.Project().max_threads + self.root = Settings.General().root + + def connect(self, pair_canvas, + layer_tree, + message_box, + result_table): + self.pair_canvas = pair_canvas + self.layer_tree = layer_tree + self.message_box = message_box + self.result_table = result_table + + def setup(self, file=None): + self.is_init = True + + if file is None: + self.file = Path(self.root)/'project'/'untitled.cdp' + dir_name = os.path.dirname(self.file) + if not os.path.exists(dir_name): + os.makedirs(dir_name, exist_ok=True) + if not self.file.exists(): + f = self.file.open('w') + f.close() + else: + self.load() + # self.project_created.emit() + self.project_init.emit(True) + + def save(self): + pass + + def clear(self): + ''' + clear all layers + ''' + self.layer_tree.clear() + self.pair_canvas.clear() + self.message_box.clear() + self.result_table.clear() + + def load(self): + pass + + def add_layer(self, pth1, pth2): + player = PairLayer(pth1, pth2) + if player.check(): + self.layer_tree.add_layer(player) + else: + self.message_box.show_message(player.msg) + +class VectorLayer: + pass + +class GridLayer: + pass + +class PairLayer: + + def __init__(self, pth1, pth2) -> None: + self.pth1 = pth1 + self.pth2 = pth2 + + self.l1_name = os.path.basename(pth1) + self.l2_name = os.path.basename(pth2) + + self.grid_layer = GridLayer() + + self.msg = '' + + def check(self): + if not os.path.exists(self.pth1): + self.msg = '图层1不存在' + return False + if not os.path.exists(self.pth2): + self.msg = '图层2不存在' + return False + + ds1 = gdal.Open(self.pth1) + ds2 = gdal.Open(self.pth2) + if ds1 is None or ds2 is None: + self.msg = '图层打开失败' + return False + + if ds1.RasterXSize != ds2.RasterXSize or ds1.RasterYSize != ds2.RasterYSize: + self.msg = '图层尺寸不一致' + return False + + del ds1 + del ds2 + + self.l1 = QgsRasterLayer(self.pth1, self.l1_name) + self.l2 = QgsRasterLayer(self.pth2, self.l2_name) + + return True \ No newline at end of file diff --git a/utils/setting.py b/utils/setting.py new file mode 100644 index 0000000..3dd71a1 --- /dev/null +++ b/utils/setting.py @@ -0,0 +1,109 @@ +from datetime import datetime +import os +from typing import Tuple +from PyQt5.QtCore import QSettings +from utils.license import LicenseHelper + +class Settings(QSettings): + + def __init__(self, key): + super().__init__() + self.key = key + def __enter__(self): + self.beginGroup(self.key) + return self + + def __exit__(self, *args, **kargs): + self.endGroup() + + class Project: + + PRE= 'project' + + @property + def cell_size(self) -> Tuple[int, int]: + with Settings(self.PRE) as s: + return s.value('cell_size', (100, 100)) + + @cell_size.setter + def cell_size(self, value:Tuple[int, int]): + with Settings(self.PRE) as s: + s.setValue('cell_size', value) + + @property + def max_memory(self): + with Settings(self.PRE) as s: + return s.value('max_memory', 100) + + @max_memory.setter + def max_memory(self, value): + with Settings(self.PRE) as s: + s.setValue('max_memory', value) + + @property + def max_threads(self): + with Settings(self.PRE) as s: + return s.value('max_threads', 4) + + @max_threads.setter + def max_threads(self, value): + with Settings(self.PRE) as s: + s.setValue('max_threads', value) + + class General: + + PRE='general' + + @property + def size(self): + with Settings(Settings.General.PRE) as s: + return s.value('size', (800, 600)) + + @size.setter + def size(self, value): + with Settings(Settings.General.PRE) as s: + s.setValue('size', value) + + @property + def end_date(self): + if not os.path.exists('lic/license.lic'): + return datetime.now() + + with open('lic/license.lic', 'r') as f: + lic = f.read()[::-1] + + lic_helper = LicenseHelper() + try: + lic_dic = lic_helper.read_license(lic) + + if lic_helper.check_license_date(lic_dic['time_str']) and lic_helper.check_license_psw(lic_dic['psw']): + return lic_dic['time_str'] + else: + return datetime.now() + except: + return datetime.now() + + @property + def license(self): + if not os.path.exists('lic/license.lic'): + return False + + with open('lic/license.lic', 'r') as f: + lic = f.read()[::-1] + + lic_helper = LicenseHelper() + try: + lic_dic = lic_helper.read_license(lic) + + if lic_helper.check_license_date(lic_dic['time_str']) and lic_helper.check_license_psw(lic_dic['psw']): + return True + else: + return False + except: + return False + + @property + def root(self): + with Settings(Settings.General.PRE) as s: + return s.value('root', './') + \ No newline at end of file