Repository: PaddleCV-SIG/EISeg Branch: develop Commit: bbb42e97ccbe Files: 98 Total size: 461.0 KB Directory structure: gitextract_x7oiqy62/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── --bug.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── README_EN.md ├── docs/ │ ├── medical.md │ ├── medical_en.md │ ├── remote_sensing.md │ ├── remote_sensing_en.md │ └── tools.md ├── eiseg/ │ ├── __init__.py │ ├── __main__.py │ ├── app.py │ ├── config/ │ │ ├── colormap.txt │ │ └── config.yaml │ ├── controller.py │ ├── exe.py │ ├── inference/ │ │ ├── __init__.py │ │ ├── clicker.py │ │ ├── predictor/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── ops.py │ │ └── transforms/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── crops.py │ │ ├── flip.py │ │ ├── limit_longest_side.py │ │ └── zoom_in.py │ ├── models.py │ ├── plugin/ │ │ ├── __init__.py │ │ ├── medical/ │ │ │ ├── __init__.py │ │ │ └── med.py │ │ ├── n2grid/ │ │ │ ├── __init__.py │ │ │ ├── grid.py │ │ │ └── rs_grid.py │ │ └── remotesensing/ │ │ ├── __init__.py │ │ ├── imgtools.py │ │ ├── raster.py │ │ └── shape.py │ ├── run.py │ ├── ui.py │ ├── util/ │ │ ├── __init__.py │ │ ├── coco/ │ │ │ ├── __init__.py │ │ │ ├── _mask.pyx │ │ │ ├── coco.py │ │ │ ├── cocoeval.py │ │ │ ├── common/ │ │ │ │ ├── gason.cpp │ │ │ │ ├── gason.h │ │ │ │ ├── maskApi.c │ │ │ │ └── maskApi.h │ │ │ └── mask.py │ │ ├── coco.py.bk │ │ ├── colormap.py │ │ ├── config.py │ │ ├── exp_imports/ │ │ │ └── default.py │ │ ├── label.py │ │ ├── language.py │ │ ├── manager.py │ │ ├── misc.py │ │ ├── opath.py │ │ ├── polygon.py │ │ ├── qt.py │ │ ├── regularization/ │ │ │ ├── __init__.py │ │ │ ├── cal_line.py │ │ │ ├── cal_point.py │ │ │ ├── rdp_alg.py │ │ │ ├── rotate_ang.py │ │ │ └── rs_regularization.py │ │ ├── serialization.py │ │ ├── translate/ │ │ │ ├── Arabic.qm │ │ │ └── English.qm │ │ └── vis.py │ └── widget/ │ ├── __init__.py │ ├── bbox.py │ ├── create.py │ ├── grip.py │ ├── line.py │ ├── loading.py │ ├── polygon.py │ ├── scene.py │ ├── shortcut.py │ ├── table.py │ └── view.py ├── init.sh ├── requirements-med.txt ├── requirements-rs.txt ├── requirements.txt ├── setup.py └── tool/ ├── baidu_translate.py ├── pypi.sh ├── semantic2instance.py ├── translate.pro ├── translateUI.py ├── ts/ │ ├── Arabic.ts │ └── English.ts └── update_md5.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/--bug.md ================================================ --- name: 反馈bug about: 反馈使用过程中出现的各种错误 title: '' labels: bug assignees: '' --- **bug描述** 请大致描述出错的现象,在什么情况下或操作过程中遇到,在上述条件下是否总是出现等。 **复现方法** 我们可以如何操作重现这个bug 如: 1. 启动软件 2. 点击.... 3. 打开.... 4. 出现问题 .... **截屏** 如果条件允许可以添加针对问题的截屏,可以帮助我们理解问题。 **运行环境(请尽量填写,这可以帮助我们定位问题):** - 系统: [e.g. Windows/Mac os/Linux] - 安装方式:[e.g. 克隆代码/pip/Windows exe] - 软件版本:[运行时显示在软件最上方] ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files test/ __pycache__/ *.py[cod] *$py.class dzq* .vscode .vscode/ vis_temp.py test.txt *.pdparams output/ temp* temp/ # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ 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/ # 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 target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .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/ # vscode .vscode/* # pycharm .idea/* # exe out/ eiseg/requirements_with_opt.txt # test test/ test_output/ *.npy # qsetting *.ini # mask_sm tool/mask.png # static_weights *.pdiparams *.pdiparams.info *.pdmodel ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ================================================ FILE: MANIFEST.in ================================================ include eiseg/config/* include eiseg/resource/* include eiseg/util/translate/* ================================================ FILE: README.md ================================================ # EISeg [![Python 3.6](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/release/python-360/) [![PaddlePaddle 2.2](https://img.shields.io/badge/paddlepaddle-2.2-blue.svg)](https://www.python.org/downloads/release/python-360/) [![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) [![Downloads](https://pepy.tech/badge/eiseg)](https://pepy.tech/project/eiseg) 简体中文 | [English](README_EN.md) ## 最新动向 - [2022.7.20] 为了减少重复工作以集中精力做好后续的开发和维护工作,**后续EISeg将仅在飞桨官方仓库[PaddlePaddle/PaddeSeg](https://github.com/PaddlePaddle/PaddleSeg/tree/release/2.6/EISeg)下进行更新,现有代码仓库将不再更新**。非常感谢大家的一路相伴与支持,EISeg将竭诚为大家提供更多更好用的标注功能,欢迎大家去新的地址体验,也希望大家继续支持。 ================================================ FILE: README_EN.md ================================================ # EISeg [![Python 3.6](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org/downloads/release/python-360/) [![PaddlePaddle 2.2](https://img.shields.io/badge/paddlepaddle-2.2-blue.svg)](https://www.python.org/downloads/release/python-360/) [![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) [![Downloads](https://pepy.tech/badge/eiseg)](https://pepy.tech/project/eiseg) [Chinese (Simplified)](README.md) | English ## Latest Developments - [2022.7.20] To reduce repetitive work and focus on development and maintenance in later versions, **EISeg will only be updated on the official repository [PaddlePaddle/PaddeSeg](https://github.com/PaddlePaddle/PaddleSeg/tree/release/2.6/EISeg) later, and this repository will not be updated**. Thank you very much for your company and support all the way. EISeg will do its best to provide you with more and better annotation functions. Welcome to experience the new address and hope you will continue to support me. ================================================ FILE: docs/medical.md ================================================ # 医疗相关 以下内容为EISeg中医疗垂类相关的文档,主要包括环境配置和功能介绍。 ## 1 环境配置 使用医疗组件需要额外安装SimpleITK包用于读取医学影像,安装方式如下: ```shell pip install SimpleITK ``` ## 2 功能介绍 目前EISeg支持打开**单层的Dicom格式图像**,对Nitfi格式和多张Dicom的支持正在开发中。EISeg通过图像拓展名判断图像格式。打开单张图像时需要在右下角类型下拉菜单中选择医疗图像,如下图所示 打开文件夹时和自然图像过程相同。打开 .dcm 后缀的图像后会询问是否开启医疗组件。 ![med-prompt](https://linhandev.github.io/assets/img/post/Med/med-prompt.png) 点击确定后会出现图像窗宽窗位设置面板 ![med-widget](https://linhandev.github.io/assets/img/post/Med/med-widget.png) 窗宽窗位的作用是聚焦一定的强度区间,方便观察CT扫描。CT扫描中每个像素点存储的数值代表人体在该位置的密度,密度越高数值越大,图像的数据范围通常为-1024~1024。不过查看扫描时人眼无法分辨2048个灰度,因此通常选择一个更小的强度范围,将这一区间内图像的灰度差异拉大,从而方便观察。具体的操作是取扫描中强度范围在 窗位-窗宽/2~窗位+窗宽/2 的部分,将这一部分数据放入256灰度的图片中展示给用户。 推理方面,目前EISeg针对医疗场景提供[肝脏分割预训练模型](https://paddleseg.bj.bcebos.com/eiseg/0.4/static_hrnet18s_ocr48_lits.zip),推荐窗宽窗位400, 0。该模型用于肝脏分割效果最佳,也可以用于其他组织或器官的分割。 ================================================ FILE: docs/medical_en.md ================================================ # Medical Treatment This part presents documents related to medical treatment in EISeg, including its environment configuration and functions. ## 1 Environment Configuration The SimpleITK package should be additionally installed for image reading, please try the following: ``` pip install SimpleITK ``` ## 2 Functions EISeg can open **single-layer Dicom format images**, while the support for Nitfi format and multiple Dicom is under development. EISeg fines the image format by its expansion name. To open a single image you need to select Medical Image in the drop-down menu of type at the bottom right corner, as shown below. The folder and natural image share the same process. When opening an image with a .dcm suffix, you will be asked whether to turn on the medical component. [![med-prompt](https://camo.githubusercontent.com/ba9ab11d3e602ae61769d2926bd6774d1dfa633346cc483ab04bf4c89e65d2d0/68747470733a2f2f6c696e68616e6465762e6769746875622e696f2f6173736574732f696d672f706f73742f4d65642f6d65642d70726f6d70742e706e67)](https://camo.githubusercontent.com/ba9ab11d3e602ae61769d2926bd6774d1dfa633346cc483ab04bf4c89e65d2d0/68747470733a2f2f6c696e68616e6465762e6769746875622e696f2f6173736574732f696d672f706f73742f4d65642f6d65642d70726f6d70742e706e67) Click Yes and there appears the setting panel of the image window width and position. [![med-widget](https://camo.githubusercontent.com/05e9c84842f9b18ad94d5a9d7610642607f569d3ef6a9d97fd445a60df9ece46/68747470733a2f2f6c696e68616e6465762e6769746875622e696f2f6173736574732f696d672f706f73742f4d65642f6d65642d7769646765742e706e67)](https://camo.githubusercontent.com/05e9c84842f9b18ad94d5a9d7610642607f569d3ef6a9d97fd445a60df9ece46/68747470733a2f2f6c696e68616e6465762e6769746875622e696f2f6173736574732f696d672f706f73742f4d65642f6d65642d7769646765742e706e67) The window width and position serve to limit the intensity range for easy observation of the CT scanning. The value stored at each pixel point in the CT scan represents the density of the human body at that location, so the higher the density the larger the value. The data range of the image is usually -1024 to 1024. However, the human eye cannot distinguish 2048 shades of gray when viewing the scan, so a smaller intensity range is usually adopted to increase the grayscale differences of the images within, thus facilitating the observation. This is done by selecting the section ranging from Window - Window Width/2 to Window + Window Width/2, and presenting the data in a 256-grayscale image. For inference, EISeg provides the [pre-trained model for liver segmentation](https://paddleseg.bj.bcebos.com/eiseg/0.4/static_hrnet18s_ocr48_lits.zip) for medical scenarios, with recommended window widths of 400, 0. This model performs best for liver segmentation and can also be used for other tissues or organs. ================================================ FILE: docs/remote_sensing.md ================================================ # 遥感相关 以下内容为EISeg中遥感垂类相关的文档,主要包括环境配置和功能介绍两大方面。 ## 1 环境配置 EISeg中对遥感数据的支持来自GDAL/OGR,GDAL是一个在X/MIT许可协议下的开源栅格空间数据转换库,OGR与其功能类似但主要提供对矢量数据的支持。 ### 1.1 依赖安装 关于GDAL的安装,可参考如下安装方式: #### 1.1.1 Windows Windows用户可以通过[这里](https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal)下载对应Python和系统版本的二进制文件(*.whl)到本地,以GDAL‑3.3.3‑cp39‑cp39‑win_amd64.whl为例,进入下载目录: ```shell cd download ``` 安装依赖: ```shell pip install GDAL‑3.3.3‑cp39‑cp39‑win_amd64.whl ``` #### 1.1.2 Linux/Mac安装 Mac用户建议利用conda安装,如下: ```shell script conda install gdal ``` ## 2 功能介绍 目前EISeg中的遥感垂类功能建设还比较简单,基本完成了GTiff类数据加载、大幅遥感影像切片与合并、地理栅格/矢量数据(GTiff/ESRI Shapefile)导出。并基于各类建筑提取数据集40余万张数据训练了一个建筑分割的交互式模型。 ### 2.1 数据加载 目前EISeg仅支持了*.tif/tiff图像后缀的的遥感影像读取,由于训练数据都是来自于RGB三通道的遥感图像切片,因此交互分割也仅在RGB三通道上完成,也就表示EISeg支持多波段数据的波段选择。 当使用EISeg打开GTiff图像时,会获取当前波段数,可通过波段设置的下拉列表进行设置。默认为[b1, b1, b1]。下例展示的是天宫一号多光谱数据设置真彩色: ![yd6fa-hqvvb](https://user-images.githubusercontent.com/71769312/141137443-a327309e-0987-4b2a-88fd-f698e08d3294.gif) ### 2.2 大幅数据切片 目前EISeg对于大幅遥感图像(目前最大尝试为900M,17000*10000大小三通道图像),支持切片预测后合并,其中切片的重叠区域overlap为24。 ![140916007-86076366-62ce-49ba-b1d9-18239baafc90](https://user-images.githubusercontent.com/71769312/141139282-854dcb4f-bcab-4ccc-aa3c-577cc52ca385.png) 下面是一副来自谷歌地球的重庆部分地区的切片演示: ![7kevx-q90hv](https://user-images.githubusercontent.com/71769312/141137447-60b305b1-a8ef-4b06-a45e-6db0b1ef2516.gif) ### 2.3 地理数据保存 当打开标注的GTiff图像带有地理参考,可设置EISeg保存时保存为带有地理参考的GTiff和ESRI Shapefile。 - GTiff:已成为GIS和卫星遥感应用的行业图像标准文件。 - ESRI Shapefile:是最常见的的矢量数据格式,Shapefile文件是美国环境系统研究所(ESRI)所研制的GIS文件系统格式文件,是工业标准的矢量数据文件。 所有的商业和开源GIS软件都支持。无处不在的它已成为行业标准。 ![82jlu-no59o](https://user-images.githubusercontent.com/71769312/141137726-76457454-5e9c-4ad0-85d6-d03f658ee63c.gif) ### 2.4 遥感标注模型选择 建筑物标注建议使用[static_hrnet18_ocr48_rsbuilding_instance](https://paddleseg.bj.bcebos.com/eiseg/0.4/static_hrnet18_ocr48_rsbuilding_instance.zip) ================================================ FILE: docs/remote_sensing_en.md ================================================ # Remote Sensing 以下内容为EISeg中遥感垂类相关的文档,主要包括环境配置和功能介绍两大方面。 This part presents documents related to remote sensing in EISeg, including its environment configuration and functions. ## 1 Environment Configuration EISeg supports remote sensing data with GDAL and OGR. The former is a translator library for raster spatial data formats under the X/MIT style Open Source License, while the latter has similar functions but mainly supports vector data. ### 1.1 Install Dependencies GDAL can be installed as follows: #### 1.1.1 Windows Windows users can download the corresponding binaries (*.whl) of Python and system versions [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#gdal). Here we take GDAL-3.3.3 -cp39-cp39-win_amd64.whl as an example, go to the download directory: ``` cd download ``` Install the dependencies: ``` pip install GDAL‑3.3.3‑cp39‑cp39‑win_amd64.whl ``` #### 1.1.2 Linux/Mac Mac users are recommended to install with conda: ``` conda install gdal ``` ## 2 Functions At present, functions of remote sensing in EISeg are relatively simple including GTiff class data loading, large remote sensing image slicing and merging, and geographic raster/vector data (GTiff/ESRI Shapefile) export. What's more, an interactive model of building segmentation is trained based on more than 400,000 data from various building datasets. ### 2.1 Data Loading For the moment, EISeg can only read remote sensing images with *.tif/tiff suffix. Since the training data are all remote sensing image slices of RGB three-channel, the interactive segmentation shares the same basis, which means EISeg supports band selection of multi-band data. When adopting EISeg to open the GTiff image, the current number of bands is obtained, which can be set by the drop-down list of band settings. The default is [b1, b1, b1]. The following example shows the true color setting of Tiangong-1 multispectral data. [![yd6fa-hqvvb](https://user-images.githubusercontent.com/71769312/141137443-a327309e-0987-4b2a-88fd-f698e08d3294.gif)](https://user-images.githubusercontent.com/71769312/141137443-a327309e-0987-4b2a-88fd-f698e08d3294.gif) ### 2.2 large Image Slicing EISeg supports the post-prediction merging of sliced large remote sensing images (the latest attempt is 900M three-channel images with a size of 17000*10000), in which the overlap (overlapping area) of slices is 24. [![140916007-86076366-62ce-49ba-b1d9-18239baafc90](https://user-images.githubusercontent.com/71769312/141139282-854dcb4f-bcab-4ccc-aa3c-577cc52ca385.png)](https://user-images.githubusercontent.com/71769312/141139282-854dcb4f-bcab-4ccc-aa3c-577cc52ca385.png) The following demonstrates the slicing of some districts in Chongqing from Google Earth: [![7kevx-q90hv](https://user-images.githubusercontent.com/71769312/141137447-60b305b1-a8ef-4b06-a45e-6db0b1ef2516.gif)](https://user-images.githubusercontent.com/71769312/141137447-60b305b1-a8ef-4b06-a45e-6db0b1ef2516.gif) ### 2.3 Geographic Data Saving When the GTiff images to be labeled are accompanied by georeferencing, you can set EISeg to save them as GTiff with georeferencing or ESRI Shapefile. - GTiff: A standard image file for industries of GIS and satellite remote sensing. - ESRI Shapefile: The most common vector data format.The Shapefile file is a GIS file format developed by the U.S. Environmental Systems Research Institute (ESRI) and is the industry-standard vector data file. It is supported by all commercial and open source GIS software and now represents the industry standard. [![82jlu-no59o](https://user-images.githubusercontent.com/71769312/141137726-76457454-5e9c-4ad0-85d6-d03f658ee63c.gif)](https://user-images.githubusercontent.com/71769312/141137726-76457454-5e9c-4ad0-85d6-d03f658ee63c.gif) ### 2.4 Labeling Model for Remote Sensing [static_hrnet18_ocr48_rsbuilding_instance](https://paddleseg.bj.bcebos.com/eiseg/0.4/static_hrnet18_ocr48_rsbuilding_instance.zip) are recommended for building labeling. ================================================ FILE: docs/tools.md ================================================ # 脚本工具相关 以下内容为EISeg中的相关工具使用。位置位于EISeg/tool ## 语义标签转实例标签 语义分割标签转实例分割标签(原标签为0/255),结果为单通道图像采用调色板调色。通过`tool`中的`semantic2instance`,可以将EISeg标注好的语义分割数据转为实例分割数据。使用以下方法: ``` shell python semantic2instance.py -o label_path -d save_path ``` 其中: - `label_path`: 语义标签存放路径,必填 - `save_path`: 实例标签保存路径,必填 ![68747470733a2f2f73332e626d702e6f76682f696d67732f323032312f30392f303038633562373638623765343737612e706e67](https://user-images.githubusercontent.com/71769312/141392781-d99ec177-f445-4336-9ab2-0ba7ae75d664.png) ================================================ FILE: eiseg/__init__.py ================================================ # Copyright (c) 2021 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 sys import os import os.path as osp import logging from datetime import datetime from qtpy import QtCore import cv2 __APPNAME__ = "EISeg" __VERSION__ = "0.5.0" pjpath = osp.dirname(osp.realpath(__file__)) sys.path.append(pjpath) for k, v in os.environ.items(): if k.startswith("QT_") and "cv2" in v: del os.environ[k] # log settings = QtCore.QSettings( osp.join(pjpath, "config/setting.ini"), QtCore.QSettings.IniFormat ) logFolder = settings.value("logFolder") logLevel = bool(settings.value("log")) logDays = settings.value("logDays") if logFolder is None or len(logFolder) == 0: logFolder = osp.normcase(osp.join(pjpath, "log")) if not osp.exists(logFolder): os.makedirs(logFolder) if logLevel: logLevel = logging.DEBUG else: logLevel = logging.CRITICAL if logDays: logDays = int(logDays) else: logDays = 7 # TODO: 删除大于logDays 的 log t = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") logger = logging.getLogger("EISeg Logger") handler = logging.FileHandler(osp.normcase(osp.join(logFolder, f"eiseg-{t}.log"))) handler.setFormatter( logging.Formatter( "%(levelname)s - %(asctime)s - %(filename)s - %(funcName)s - %(message)s" ) ) logger.setLevel(logLevel) logger.addHandler(handler) ================================================ FILE: eiseg/__main__.py ================================================ # Copyright (c) 2021 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. from run import main if __name__ == "__main__": main() ================================================ FILE: eiseg/app.py ================================================ # Copyright (c) 2021 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 logging import os import os.path as osp from functools import partial import json from distutils.util import strtobool import webbrowser from easydict import EasyDict as edict from qtpy import QtGui, QtCore, QtWidgets from qtpy.QtWidgets import QMainWindow, QMessageBox, QTableWidgetItem from qtpy.QtGui import QImage, QPixmap from qtpy.QtCore import Qt, QByteArray, QVariant, QCoreApplication, QThread, Signal import cv2 import numpy as np from eiseg import pjpath, __APPNAME__, logger from widget import ShortcutWidget, PolygonAnnotation from controller import InteractiveController from ui import Ui_EISeg import util from util import COCO from util import check_cn, normcase import plugin.remotesensing as rs from plugin.medical import med from plugin.remotesensing import Raster from plugin.n2grid import RSGrids, Grids, checkOpenGrid # TODO: 研究paddle子线程 class ModelThread(QThread): _signal = Signal(dict) def __init__(self, controller, param_path): super().__init__() self.controller = controller self.param_path = param_path def run(self): success, res = self.controller.setModel(self.param_path, False) self._signal.emit( {"success": success, "res": res, "param_path": self.param_path} ) class APP_EISeg(QMainWindow, Ui_EISeg): IDILE, ANNING, EDITING = 0, 1, 2 # IDILE:网络,权重,图像三者任一没有加载 # EDITING:多边形编辑,可以交互式,但是多边形内部不能点 # ANNING:交互式标注,只能交互式,不能编辑多边形,多边形不接hover # 宫格标注背景颜色 GRID_COLOR = { "idle": QtGui.QColor(255, 255, 255), "current": QtGui.QColor(192, 220, 243), "finised": QtGui.QColor(185, 185, 225), "overlying": QtGui.QColor(51, 52, 227), } def __init__(self, parent=None): super(APP_EISeg, self).__init__(parent) self.settings = QtCore.QSettings( osp.join(pjpath, "config/setting.ini"), QtCore.QSettings.IniFormat ) currentLang = self.settings.value("language") layoutdir = Qt.RightToLeft if currentLang == "Arabic" else Qt.LeftToRight self.setLayoutDirection(layoutdir) # 初始化界面 self.setupUi(self) # app变量 self._anning = False # self.status替代 self.isDirty = False # 是否需要保存 self.image = None # 可能先加载图片后加载模型,只用于暂存图片 self.predictor_params = { "brs_mode": "NoBRS", "with_flip": False, "zoom_in_params": { "skip_clicks": -1, "target_size": (400, 400), "expansion_ratio": 1.4, }, "predictor_params": { "net_clicks_limit": None, "max_size": 800, "with_mask": True, }, } self.controller = InteractiveController( predictor_params=self.predictor_params, prob_thresh=self.segThresh, ) # self.controller.labelList = util.LabelList() # 标签列表 self.save_status = { "gray_scale": True, "pseudo_color": True, "json": False, "coco": True, "cutout": True, } # 是否保存这几个格式 self.outputDir = None # 标签保存路径 self.labelPaths = [] # 所有outputdir中的标签文件路径 self.imagePaths = [] # 文件夹下所有待标注图片路径 self.currIdx = 0 # 文件夹标注当前图片下标 self.origExt = False # 是否使用图片本身拓展名,防止重名覆盖 if self.save_status["coco"]: self.coco = COCO() else: self.coco = None self.colorMap = util.colorMap if self.settings.value("cutout_background"): self.cutoutBackground = [ int(c) for c in self.settings.value("cutout_background") ] if len(self.cutoutBackground) == 3: self.cutoutBackground += tuple([255]) else: self.cutoutBackground = [0, 0, 128, 255] if self.settings.value("cross_color"): self.crossColor = [ int(c) for c in self.settings.value("cross_color") ] else: self.crossColor = [0, 0, 0, 127] self.scene.setPenColor(self.crossColor) # widget self.dockWidgets = { "model": self.ModelDock, "data": self.DataDock, "label": self.LabelDock, "seg": self.SegSettingDock, "rs": self.RSDock, "med": self.MedDock, "grid": self.GridDock, } # self.display_dockwidget = [True, True, True, True, False, False, False] self.dockStatus = self.settings.value( "dock_status", QVariant([]), type=list ) # 所有widget是否展示 if len(self.dockStatus) != len(self.dockWidgets): self.dockStatus = [True] * 4 + [False] * (len(self.dockWidgets) - 4) self.settings.setValue("dock_status", self.dockStatus) else: self.dockStatus = [strtobool(s) for s in self.dockStatus] self.layoutStatus = self.settings.value("layout_status", QByteArray()) # 界面元素位置 self.recentModels = self.settings.value( "recent_models", QVariant([]), type=list ) self.recentFiles = self.settings.value("recent_files", QVariant([]), type=list) self.config = util.parse_configs(osp.join(pjpath, "config/config.yaml")) # 支持的图像格式 rs_ext = [".tif", ".tiff"] img_ext = [] for fmt in QtGui.QImageReader.supportedImageFormats(): fmt = ".{}".format(fmt.data().decode()) if fmt not in rs_ext: img_ext.append(fmt) self.formats = [ img_ext, # 自然图像 [".dcm"], # 医学影像 rs_ext, # 遥感影像 ] # 遥感 self.raster = None self.grid = None self.rsRGB = [1, 1, 1] # 遥感索引 # 医疗参数 self.midx = 0 # 医疗切片索引 # 大图限制 self.thumbnail_min = 2000 # 初始化action self.initActions() # 更新近期记录 self.loadLayout() # 放前面 self.toggleWidget("all", warn=False) self.updateModelMenu() self.updateRecentFile() # 窗口 ## 快捷键 self.ShortcutWidget = ShortcutWidget(self.actions, pjpath) ## 画布 self.scene.clickRequest.connect(self.canvasClick) self.canvas.zoomRequest.connect(self.viewZoomed) self.canvas.mousePosChanged.connect(self.scene.onMouseChanged) self.annImage = QtWidgets.QGraphicsPixmapItem() self.scene.addItem(self.annImage) ## 按钮点击 self.btnSave.clicked.connect(self.exportLabel) # 保存 self.listFiles.itemDoubleClicked.connect(self.imageListClicked) # 标签列表点击 self.btnAddClass.clicked.connect(self.addLabel) self.btnParamsSelect.clicked.connect(self.changeParam) # 模型参数选择 self.cheWithMask.stateChanged.connect(self.chooseMode) # with_mask ## 滑动 self.sldOpacity.valueChanged.connect(self.maskOpacityChanged) self.sldClickRadius.valueChanged.connect(self.clickRadiusChanged) self.sldThresh.valueChanged.connect(self.threshChanged) self.sliderWw.valueChanged.connect(self.swwChanged) self.sliderWc.valueChanged.connect(self.swcChanged) self.textWw.returnPressed.connect(self.twwChanged) self.textWc.returnPressed.connect(self.twcChanged) ## 标签列表点击 self.labelListTable.cellDoubleClicked.connect(self.labelListDoubleClick) self.labelListTable.cellClicked.connect(self.labelListClicked) self.labelListTable.cellChanged.connect(self.labelListItemChanged) ## 功能区选择 # self.rsShow.currentIndexChanged.connect(self.rsShowModeChange) # 显示模型 for bandCombo in self.bandCombos: bandCombo.currentIndexChanged.connect(self.rsBandSet) # 设置波段 # self.btnInitGrid.clicked.connect(self.initGrid) # 打开宫格 self.btnFinishedGrid.clicked.connect(self.saveGridLabel) def initActions(self): tr = partial(QtCore.QCoreApplication.translate, "APP_EISeg") action = partial(util.newAction, self) start = dir() # 打开/加载/保存 open_image = action( tr("&打开图像"), self.openImage, "open_image", "OpenImage", tr("打开一张图像进行标注"), ) open_folder = action( tr("&打开文件夹"), self.openFolder, "open_folder", "OpenFolder", tr("打开一个文件夹下所有的图像进行标注"), ) change_output_dir = action( tr("&改变标签保存路径"), partial(self.changeOutputDir, None), "change_output_dir", "ChangeOutputDir", tr("改变标签保存的文件夹路径"), ) load_param = action( tr("&加载模型参数"), self.changeParam, "load_param", "Model", tr("加载一个模型参数"), ) save = action( tr("&保存"), self.exportLabel, "save", "Save", tr("保存图像标签"), ) save_as = action( tr("&另存为"), partial(self.exportLabel, saveAs=True), "save_as", "SaveAs", tr("在指定位置另存为标签"), ) auto_save = action( tr("&自动保存"), self.toggleAutoSave, "auto_save", "AutoSave", tr("翻页同时自动保存"), checkable=True, ) # auto_save.setChecked(self.config.get("auto_save", False)) # 标注 turn_prev = action( tr("&上一张"), partial(self.turnImg, -1), "turn_prev", "Prev", tr("翻到上一张图片"), ) turn_next = action( tr("&下一张"), partial(self.turnImg, 1), "turn_next", "Next", tr("翻到下一张图片"), ) finish_object = action( tr("&完成当前目标"), self.finishObject, "finish_object", "Ok", tr("完成当前目标的标注"), ) clear = action( tr("&清除所有标注"), self.clearAll, "clear", "Clear", tr("清除所有标注信息"), ) undo = action( tr("&撤销"), self.undoClick, "undo", "Undo", tr("撤销一次点击"), ) redo = action( tr("&重做"), self.redoClick, "redo", "Redo", tr("重做一次点击"), ) del_active_polygon = action( tr("&删除多边形"), self.delActivePolygon, "del_active_polygon", "DeletePolygon", tr("删除当前选中的多边形"), ) del_all_polygon = action( tr("&删除所有多边形"), self.delAllPolygon, "del_all_polygon", "DeleteAllPolygon", tr("删除所有的多边形"), ) largest_component = action( tr("&保留最大连通块"), self.toggleLargestCC, "largest_component", "SaveLargestCC", tr("保留最大的连通块"), checkable=True, ) origional_extension = action( tr("&标签和图像使用相同拓展名"), self.toggleOrigExt, "origional_extension", "Same", tr("标签和图像使用相同拓展名,用于图像中有文件名相同但拓展名不同的情况,防止标签覆盖"), checkable=True, ) save_pseudo = action( tr("&伪彩色保存"), partial(self.toggleSave, "pseudo_color"), "save_pseudo", "SavePseudoColor", tr("保存为伪彩色图像"), checkable=True, ) save_pseudo.setChecked(self.save_status["pseudo_color"]) save_grayscale = action( tr("&灰度保存"), partial(self.toggleSave, "gray_scale"), "save_grayscale", "SaveGrayScale", tr("保存为灰度图像,像素的灰度为对应类型的标签"), checkable=True, ) save_grayscale.setChecked(self.save_status["gray_scale"]) save_json = action( tr("&JSON保存"), partial(self.toggleSave, "json"), "save_json", "SaveJson", tr("保存为JSON格式"), checkable=True, ) save_json.setChecked(self.save_status["json"]) save_coco = action( tr("&COCO保存"), partial(self.toggleSave, "coco"), "save_coco", "SaveCOCO", tr("保存为COCO格式"), checkable=True, ) save_coco.setChecked(self.save_status["coco"]) # test func self.show_rs_poly = action( tr("&显示遥感多边形"), None, "show_rs_poly", "Show", tr("显示遥感大图的多边形结果"), checkable=True, ) self.show_rs_poly.setChecked(False) self.grid_message = action( tr("&启用宫格检测"), None, "grid_message", "Show", tr("针对每张图片启用宫格检测"), checkable=True, ) self.grid_message.setChecked(True) save_cutout = action( tr("&抠图保存"), partial(self.toggleSave, "cutout"), "save_cutout", "SaveCutout", tr("只保留前景,背景设置为背景色"), checkable=True, ) save_cutout.setChecked(self.save_status["cutout"]) set_cutout_background = action( tr("&设置抠图背景色"), self.setCutoutBackground, "set_cutout_background", self.cutoutBackground, tr("抠图后背景像素的颜色"), ) set_cross_color = action( tr("&设置十字丝颜色"), self.setCrossColor, "set_cross_color", self.crossColor, tr("十字丝的显示颜色"), ) close = action( tr("&关闭"), partial(self.saveImage, True), "close", "Close", tr("关闭当前图像"), ) quit = action( tr("&退出"), self.close, "quit", "Quit", tr("退出软件"), ) export_label_list = action( tr("&导出标签列表"), partial(self.exportLabelList, None), "export_label_list", "ExportLabel", tr("将标签列表导出成标签配置文件"), ) import_label_list = action( tr("&载入标签列表"), partial(self.importLabelList, None), "import_label_list", "ImportLabel", tr("从标签配置文件载入标签列表"), ) clear_label_list = action( tr("&清空标签列表"), self.clearLabelList, "clear_label_list", "ClearLabel", tr("清空所有的标签"), ) clear_recent = action( tr("&清除近期文件记录"), self.clearRecentFile, "clear_recent", "ClearRecent", tr("清除近期标注文件记录"), ) model_widget = action( tr("&模型选择"), partial(self.toggleWidget, 0), "model_widget", "Net", tr("隐藏/展示模型选择面板"), checkable=True, ) data_widget = action( tr("&数据列表"), partial(self.toggleWidget, 1), "data_widget", "Data", tr("隐藏/展示数据列表面板"), checkable=True, ) label_widget = action( tr("&标签列表"), partial(self.toggleWidget, 2), "label_widget", "Label", tr("隐藏/展示标签列表面板"), checkable=True, ) segmentation_widget = action( tr("&分割设置"), partial(self.toggleWidget, 3), "segmentation_widget", "Setting", tr("隐藏/展示分割设置面板"), checkable=True, ) rs_widget = action( tr("&遥感设置"), partial(self.toggleWidget, 4), "rs_widget", "RemoteSensing", tr("隐藏/展示遥感设置面板"), checkable=True, ) mi_widget = action( tr("&医疗设置"), partial(self.toggleWidget, 5), "mi_widget", "MedicalImaging", tr("隐藏/展示医疗设置面板"), checkable=True, ) grid_ann_widget = action( tr("&N2宫格标注"), partial(self.toggleWidget, 6), "grid_ann_widget", "N2", tr("隐藏/展示N^2宫格细粒度标注面板"), checkable=True, ) quick_start = action( tr("&快速入门"), self.quickStart, "quick_start", "Use", tr("主要功能使用介绍"), ) report_bug = action( tr("&反馈问题"), self.reportBug, "report_bug", "ReportBug", tr("通过Github Issue反馈使用过程中遇到的问题。我们会尽快进行修复"), ) edit_shortcuts = action( tr("&编辑快捷键"), self.editShortcut, "edit_shortcuts", "Shortcut", tr("编辑软件快捷键"), ) toggle_logging = action( tr("&调试日志"), self.toggleLogging, "toggle_logging", "Log", tr("用于观察软件执行过程和进行debug。我们不会自动收集任何日志,可能会希望您在反馈问题时间打开此功能,帮助我们定位问题。"), checkable=True, ) toggle_logging.setChecked(bool(self.settings.value("log", False))) use_qt_widget = action( tr("&使用QT文件窗口"), self.useQtWidget, "use_qt_widget", "Qt", tr("如果使用文件选择窗口时遇到问题可以选择使用Qt窗口"), checkable=True, ) # print( # "use_qt_widget", # self.settings.value("use_qt_widget", type=bool), # ) use_qt_widget.setChecked(self.settings.value("use_qt_widget", False, type=bool)) self.actions = util.struct() for name in dir(): if name not in start: self.actions.append(eval(name)) def newWidget(text, icon, showAction): widget = QtWidgets.QMenu(tr(text)) widget.setIcon(util.newIcon(icon)) widget.aboutToShow.connect(showAction) return widget recent_files = newWidget(self.tr("近期文件"), "Data", self.updateRecentFile) recent_params = newWidget(self.tr("近期模型及参数"), "Net", self.updateModelMenu) languages = newWidget(self.tr("语言"), "Language", self.updateLanguage) self.menus = util.struct( recent_files=recent_files, recent_params=recent_params, languages=languages, fileMenu=( open_image, open_folder, change_output_dir, load_param, clear_recent, recent_files, recent_params, None, save, save_as, auto_save, None, turn_next, turn_prev, close, None, quit, ), labelMenu=( export_label_list, import_label_list, clear_label_list, ), functionMenu=( largest_component, del_active_polygon, del_all_polygon, None, origional_extension, save_pseudo, save_grayscale, save_cutout, set_cutout_background, None, set_cross_color, None, save_json, save_coco, None, # test self.show_rs_poly, None, self.grid_message, ), showMenu=( model_widget, data_widget, label_widget, segmentation_widget, rs_widget, mi_widget, grid_ann_widget, ), helpMenu=( languages, use_qt_widget, quick_start, report_bug, edit_shortcuts, toggle_logging, ), toolBar=( finish_object, clear, undo, redo, turn_prev, turn_next, None, save_pseudo, save_grayscale, save_cutout, save_json, save_coco, origional_extension, None, largest_component, ), ) def menu(title, actions=None): menu = self.menuBar().addMenu(title) if actions: util.addActions(menu, actions) return menu menu(tr("文件"), self.menus.fileMenu) menu(tr("标注"), self.menus.labelMenu) menu(tr("功能"), self.menus.functionMenu) menu(tr("显示"), self.menus.showMenu) menu(tr("帮助"), self.menus.helpMenu) util.addActions(self.toolBar, self.menus.toolBar) def __setColor(self, action, setting_name): c = action color = QtWidgets.QColorDialog.getColor( QtGui.QColor(*c), self, options=QtWidgets.QColorDialog.ShowAlphaChannel, ) action = color.getRgb() self.settings.setValue( setting_name, [int(c) for c in action] ) return action def setCutoutBackground(self): self.cutoutBackground = self.__setColor(self.cutoutBackground, "cutout_background") self.actions.set_cutout_background.setIcon(util.newIcon(self.cutoutBackground)) def setCrossColor(self): self.crossColor = self.__setColor(self.crossColor, "cross_color") self.actions.set_cross_color.setIcon(util.newIcon(self.crossColor)) self.scene.setPenColor(self.crossColor) def editShortcut(self): self.ShortcutWidget.center() self.ShortcutWidget.show() # 多语言 def updateLanguage(self): self.menus.languages.clear() langs = os.listdir(osp.join(pjpath, "util/translate")) langs = [n.split(".")[0] for n in langs if n.endswith("qm")] langs.append("中文") for lang in langs: if lang == self.currLanguage: continue entry = util.newAction( self, lang, partial(self.changeLanguage, lang), None, lang if lang != "Arabic" else "Egypt", ) self.menus.languages.addAction(entry) def changeLanguage(self, lang): self.settings.setValue("language", lang) self.warn(self.tr("切换语言"), self.tr("切换语言需要重启软件才能生效")) # 近期图像 def updateRecentFile(self): menu = self.menus.recent_files menu.clear() recentFiles = self.settings.value("recent_files", QVariant([]), type=list) files = [f for f in recentFiles if osp.exists(f)] for i, f in enumerate(files): icon = util.newIcon("File") action = QtWidgets.QAction( icon, "&【%d】 %s" % (i + 1, QtCore.QFileInfo(f).fileName()), self ) action.triggered.connect(partial(self.openRecentImage, f)) menu.addAction(action) if len(files) == 0: menu.addAction(self.tr("无近期文件")) self.settings.setValue("recent_files", files) def addRecentFile(self, path): if not osp.exists(path): return paths = self.settings.value("recent_files", QVariant([]), type=list) if path not in paths: paths.append(path) if len(paths) > 15: del paths[0] self.settings.setValue("recent_files", paths) self.updateRecentFile() def clearRecentFile(self): self.settings.remove("recent_files") self.statusbar.showMessage(self.tr("已清除最近打开文件"), 10000) # 模型加载 def updateModelMenu(self): menu = self.menus.recent_params menu.clear() self.recentModels = [ m for m in self.recentModels if osp.exists(m["param_path"]) ] for idx, m in enumerate(self.recentModels): icon = util.newIcon("Model") action = QtWidgets.QAction( icon, f"{osp.basename(m['param_path'])}", self, ) action.triggered.connect(partial(self.setModelParam, m["param_path"])) menu.addAction(action) if len(self.recentModels) == 0: menu.addAction(self.tr("无近期模型记录")) self.settings.setValue("recent_params", self.recentModels) def setModelParam(self, paramPath): res = self.changeParam(paramPath) if res: return True return False def changeParam(self, param_path: str = None): if not param_path: filters = self.tr("Paddle静态模型权重文件(*.pdiparams)") start_path = ( "." if len(self.recentModels) == 0 else osp.dirname(self.recentModels[-1]["param_path"]) ) if self.settings.value("use_qt_widget", False, type=bool): options = QtWidgets.QFileDialog.DontUseNativeDialog else: options = QtWidgets.QFileDialog.ReadOnly param_path, _ = QtWidgets.QFileDialog.getOpenFileName( self, self.tr("选择模型参数") + " - " + __APPNAME__, start_path, filters, options=options, ) # QtWidgets.QFileDialog.DontUseNativeDialog if not param_path: return False # 中文路径打不开 if check_cn(param_path): self.warn(self.tr("参数路径存在中文"), self.tr("请修改参数路径为非中文路径!")) return False # success, res = self.controller.setModel(param_path) self.load_thread = ModelThread(self.controller, param_path) self.load_thread._signal.connect(self.__change_model_callback) self.load_thread.start() def __change_model_callback(self, signal_dict: dict): success = signal_dict["success"] res = signal_dict["res"] param_path = signal_dict["param_path"] if success: model_dict = {"param_path": param_path} if model_dict not in self.recentModels: self.recentModels.insert(0, model_dict) if len(self.recentModels) > 10: del self.recentModels[-1] else: # 如果存在移动位置,确保加载最近模型的正确 self.recentModels.remove(model_dict) self.recentModels.insert(0, model_dict) self.settings.setValue("recent_models", self.recentModels) self.statusbar.showMessage( osp.basename(param_path) + self.tr(" 模型加载成功"), 10000 ) return True else: self.warnException(res) return False def chooseMode(self): self.predictor_params["predictor_params"][ "with_mask" ] = self.cheWithMask.isChecked() self.controller.reset_predictor(predictor_params=self.predictor_params) if self.cheWithMask.isChecked(): self.statusbar.showMessage(self.tr("掩膜已启用"), 10000) else: self.statusbar.showMessage(self.tr("掩膜已关闭"), 10000) def loadRecentModelParam(self): if len(self.recentModels) == 0: self.statusbar.showMessage(self.tr("没有最近使用模型信息,请加载模型"), 10000) return m = self.recentModels[0] param_path = m["param_path"] self.setModelParam(param_path) # 标签列表 def importLabelList(self, filePath=None): if filePath is None: if self.settings.value("use_qt_widget", False, type=bool): options = QtWidgets.QFileDialog.DontUseNativeDialog else: options = QtWidgets.QFileDialog.ReadOnly filters = self.tr("标签配置文件") + " (*.txt)" filePath, _ = QtWidgets.QFileDialog.getOpenFileName( self, self.tr("选择标签配置文件路径") + " - " + __APPNAME__, ".", filters, options=options, ) filePath = normcase(filePath) if not osp.exists(filePath): return self.controller.importLabel(filePath) logger.info(f"Loaded label list: {self.controller.labelList.labelList}") self.refreshLabelList() def exportLabelList(self, savePath: str = None): if len(self.controller.labelList) == 0: self.warn(self.tr("没有需要保存的标签"), self.tr("请先添加标签之后再进行保存!")) return if savePath is None: filters = self.tr("标签配置文件") + "(*.txt)" dlg = QtWidgets.QFileDialog( self, self.tr("保存标签配置文件"), ".", filters, ) dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False) if self.settings.value("use_qt_widget", False, type=bool): options = QtWidgets.QFileDialog.DontUseNativeDialog else: options = QtWidgets.QFileDialog.DontUseCustomDirectoryIcons dlg.setDefaultSuffix("txt") dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) savePath, _ = dlg.getSaveFileName( self, self.tr("选择保存标签配置文件路径") + " - " + __APPNAME__, ".", filters, options=options, ) self.controller.exportLabel(savePath) def addLabel(self): c = self.colorMap.get_color() table = self.labelListTable idx = table.rowCount() table.insertRow(table.rowCount()) self.controller.addLabel(idx + 1, "", c) numberItem = QTableWidgetItem(str(idx + 1)) numberItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 0, numberItem) table.setItem(idx, 1, QTableWidgetItem()) colorItem = QTableWidgetItem() colorItem.setBackground(QtGui.QColor(c[0], c[1], c[2])) colorItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 2, colorItem) delItem = QTableWidgetItem() delItem.setIcon(util.newIcon("Clear")) delItem.setTextAlignment(Qt.AlignCenter) delItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 3, delItem) self.adjustTableSize() self.labelListClicked(self.labelListTable.rowCount() - 1, 0) def adjustTableSize(self): self.labelListTable.horizontalHeader().setDefaultSectionSize(25) self.labelListTable.horizontalHeader().setSectionResizeMode( 0, QtWidgets.QHeaderView.Fixed ) self.labelListTable.horizontalHeader().setSectionResizeMode( 3, QtWidgets.QHeaderView.Fixed ) self.labelListTable.horizontalHeader().setSectionResizeMode( 2, QtWidgets.QHeaderView.Fixed ) self.labelListTable.setColumnWidth(2, 50) def clearLabelList(self): if len(self.controller.labelList) == 0: return True res = self.warn( self.tr("清空标签列表?"), self.tr("请确认是否要清空标签列表"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Cancel: return False self.controller.labelList.clear() if self.controller: self.controller.label_list = [] self.controller.curr_label_number = 0 self.labelListTable.clear() self.labelListTable.setRowCount(0) return True def refreshLabelList(self): table = self.labelListTable table.clearContents() table.setRowCount(len(self.controller.labelList)) table.setColumnCount(4) for idx, lab in enumerate(self.controller.labelList): numberItem = QTableWidgetItem(str(lab.idx)) numberItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 0, numberItem) table.setItem(idx, 1, QTableWidgetItem(lab.name)) c = lab.color colorItem = QTableWidgetItem() colorItem.setBackground(QtGui.QColor(c[0], c[1], c[2])) colorItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 2, colorItem) delItem = QTableWidgetItem() delItem.setIcon(util.newIcon("Clear")) delItem.setTextAlignment(Qt.AlignCenter) delItem.setFlags(QtCore.Qt.ItemIsEnabled) table.setItem(idx, 3, delItem) self.adjustTableSize() cols = [0, 1, 3] for idx in cols: table.resizeColumnToContents(idx) self.adjustTableSize() def labelListDoubleClick(self, row, col): if col != 2: return table = self.labelListTable color = QtWidgets.QColorDialog.getColor() if color.getRgb() == (0, 0, 0, 255): return table.item(row, col).setBackground(color) self.controller.labelList[row].color = color.getRgb()[:3] if self.controller: self.controller.label_list = self.controller.labelList for p in self.scene.polygon_items: idlab = self.controller.labelList.getLabelById(p.labelIndex) if idlab is not None: color = idlab.color p.setColor(color, color) self.labelListClicked(row, 0) @property def currLabelIdx(self): return self.controller.curr_label_number - 1 def labelListClicked(self, row, col): table = self.labelListTable if col == 3: labelIdx = int(table.item(row, 0).text()) self.controller.labelList.remove(labelIdx) table.removeRow(row) if col == 0 or col == 1: for cl in range(2): for idx in range(len(self.controller.labelList)): table.item(idx, cl).setBackground(QtGui.QColor(255, 255, 255)) table.item(row, cl).setBackground(QtGui.QColor(48, 140, 198)) table.item(row, 0).setSelected(True) if self.controller: self.controller.setCurrLabelIdx(int(table.item(row, 0).text())) self.controller.label_list = self.controller.labelList def labelListItemChanged(self, row, col): self.colorMap.usedColors = self.controller.labelList.colors try: if col == 1: name = self.labelListTable.item(row, col).text() self.controller.labelList[row].name = name except: pass # 多边形标注 def createPoly(self, curr_polygon, color): if curr_polygon is None: return for points in curr_polygon: if len(points) < 3: continue poly = PolygonAnnotation( self.controller.labelList[self.currLabelIdx].idx, self.controller.image.shape, self.delPolygon, self.setDirty, color, color, self.opacity, ) poly.labelIndex = self.controller.labelList[self.currLabelIdx].idx self.scene.addItem(poly) self.scene.polygon_items.append(poly) for p in points: poly.addPointLast(QtCore.QPointF(p[0], p[1])) self.setDirty(True) def delActivePolygon(self): for idx, polygon in enumerate(self.scene.polygon_items): if polygon.hasFocus(): res = self.warn( self.tr("确认删除?"), self.tr("确认要删除当前选中多边形标注?"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Yes: self.delPolygon(polygon) def delPolygon(self, polygon): polygon.remove() if self.save_status["coco"]: if polygon.coco_id: self.coco.delAnnotation( polygon.coco_id, self.coco.imgNameToId[osp.basename(self.imagePath)], ) self.setDirty(True) def delAllPolygon(self): for p in self.scene.polygon_items[::-1]: # 删除所有多边形 self.delPolygon(p) def delActivePoint(self): for polygon in self.scene.polygon_items: polygon.removeFocusPoint() # 图片/标签 io def getMask(self): if not self.controller or self.controller.image is None: return s = self.controller.imgShape pesudo = np.zeros([s[0], s[1]]) # 覆盖顺序,从上往下 # TODO: 是标签数值大的会覆盖小的吗? # A: 是列表中上面的覆盖下面的,由于标签可以移动,不一定是大小按顺序覆盖 # RE: 我们做医学的时候覆盖比较多,感觉一般是数值大的标签覆盖数值小的标签。按照上面覆盖下面的话可能跟常见的情况正好是反过来的,感觉可能从下往上覆盖会比较好 len_lab = self.labelListTable.rowCount() for i in range(len_lab - 1, -1, -1): idx = int(self.labelListTable.item(len_lab - i - 1, 0).text()) for poly in self.scene.polygon_items: if poly.labelIndex == idx: pts = np.int32([np.array(poly.scnenePoints)]) cv2.fillPoly(pesudo, pts=pts, color=idx) return pesudo def openRecentImage(self, file_path): self.queueEvent(partial(self.loadImage, file_path)) self.listFiles.addItems([file_path.replace("\\", "/")]) self.currIdx = self.listFiles.count() - 1 self.listFiles.setCurrentRow(self.currIdx) # 移动位置 self.imagePaths.append(file_path) def openImage(self, filePath: str = None): # 在triggered.connect中使用不管默认filePath为什么返回值都为False if not isinstance(filePath, str) or filePath is False: prompts = ["图片", "医学影像", "遥感影像"] filters = "" for fmts, p in zip(self.formats, prompts): filters += f"{p} ({' '.join(['*' + f for f in fmts])}) ;; " filters = filters[:-3] recentPath = self.settings.value("recent_files", []) if len(recentPath) == 0: recentPath = "." else: recentPath = osp.dirname(recentPath[0]) if self.settings.value("use_qt_widget", False, type=bool): options = QtWidgets.QFileDialog.DontUseNativeDialog else: options = QtWidgets.QFileDialog.ReadOnly filePath, _ = QtWidgets.QFileDialog.getOpenFileName( self, self.tr("选择待标注图片") + " - " + __APPNAME__, recentPath, filters, options=options, ) if len(filePath) == 0: # 用户没选就直接关闭窗口 return filePath = normcase(filePath) if not self.loadImage(filePath): return False # 3. 添加记录 self.listFiles.addItems([filePath]) self.currIdx = self.listFiles.count() - 1 self.listFiles.setCurrentRow(self.currIdx) # 移动位置 self.imagePaths.append(filePath) return True def openFolder(self, inputDir: str = None): # 1. 如果没传文件夹,弹框让用户选 if not isinstance(inputDir, str): recentPath = self.settings.value("recent_files", []) if len(recentPath) == 0: recentPath = "." else: recentPath = osp.dirname(recentPath[-1]) options = ( QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks ) if self.settings.value("use_qt_widget", False, type=bool): options = options | QtWidgets.QFileDialog.DontUseNativeDialog inputDir = QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("选择待标注图片文件夹") + " - " + __APPNAME__, recentPath, options, ) if not osp.exists(inputDir): return # 2. 关闭当前图片,清空文件列表 self.saveImage(close=True) self.imagePaths = [] self.listFiles.clear() # 3. 扫描文件夹下所有图片 # 3.1 获取所有文件名 imagePaths = os.listdir(inputDir) exts = tuple(f for fmts in self.formats for f in fmts) imagePaths = [n for n in imagePaths if n.lower().endswith(exts)] imagePaths = [n for n in imagePaths if not n[0] == "."] imagePaths.sort() if len(imagePaths) == 0: return # 3.2 设置默认输出路径 if self.outputDir is None: # 没设置为文件夹下的 label 文件夹 self.outputDir = osp.join(inputDir, "label") if not osp.exists(self.outputDir): os.makedirs(self.outputDir) # 3.3 有重名图片,标签保留原来拓展名 names = [] for name in imagePaths: name = osp.splitext(name)[0] if name not in names: names.append(name) else: self.toggleOrigExt(True) break imagePaths = [osp.join(inputDir, n) for n in imagePaths] for p in imagePaths: p = normcase(p) self.imagePaths.append(p) self.listFiles.addItem(p) # 3.4 加载已有的标注 if self.outputDir is not None and osp.exists(self.outputDir): self.changeOutputDir(self.outputDir) if len(self.imagePaths) != 0: self.currIdx = 0 self.turnImg(0) self.inputDir = inputDir def loadImage(self, path): if self.controller.model is None: self.warn("未检测到模型", "请先加载模型参数") return # 1. 拒绝None和不存在的路径,关闭当前图像 if not path: return path = normcase(path) if not osp.exists(path): return self.saveImage(True) # 关闭当前图像 self.eximgsInit() # TODO: 将grid的部分整合到saveImage里 # 2. 判断图像类型,打开 # TODO: 加用户指定类型的功能 image = None # 直接if会报错,因为打开遥感图像后多波段不存在,现在把遥感图像的单独抽出来了 # 自然图像 if path.lower().endswith(tuple(self.formats[0])): image = cv2.imdecode(np.fromfile(path, dtype=np.uint8), 1) image = image[:, :, ::-1] # BGR转RGB if self.grid_message.isChecked(): if checkOpenGrid(image, self.thumbnail_min): if self.loadGrid(image, False): image, _ = self.grid.getGrid(0, 0) else: if self.dockWidgets["grid"].isVisible() is True: self.grid = Grids(image) self.initGrid() image, _ = self.grid.getGrid(0, 0) # 医学影像 if path.lower().endswith(tuple(self.formats[1])): if not self.dockStatus[5]: res = self.warn( self.tr("未启用医疗组件"), self.tr("加载医疗影像需启用医疗组件,是否立即启用?"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Cancel: return False self.toggleWidget(5) if not self.dockStatus[5]: return False image = med.dcm_reader(path) # TODO: 添加多层支持 if image.shape[-1] != 1: self.warn("医学影像打开错误", "暂不支持打开多层医学影像") return False maxValue = np.max(image) # 根据数据模态自适应窗宽窗位 minValue = np.min(image) if minValue == 0: ww = maxValue wc = int(maxValue / 2) else: ww = maxValue + int(abs(minValue)) wc = int((minValue + maxValue) / 2) self.sliderWw.setValue(int(ww)) self.textWw.setText(str(ww)) self.sliderWc.setValue(int(wc)) self.textWc.setText(str(wc)) self.controller.rawImage = self.image = image image = med.windowlize(image, self.ww, self.wc) # 遥感图像 if path.lower().endswith( tuple(self.formats[2]) ): # imghdr.what(path) == "tiff": if not self.dockStatus[4]: res = self.warn( self.tr("未打开遥感组件"), self.tr("打开遥感图像需启用遥感组件,是否立即启用?"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Cancel: return False self.toggleWidget(4) if not self.dockStatus[4]: return False self.raster = Raster(path) gi = self.raster.showGeoInfo() self.edtGeoinfo.setText(self.tr("● 波段数:") + gi[0] + "\n" + self.tr("● 数据类型:") + gi[1] + "\n" + self.tr("● 行数:") + gi[2] + "\n" + self.tr("● 列数:") + gi[3] + "\n" + "● EPSG:" + gi[4]) if max(self.rsRGB) > self.raster.geoinfo.count: self.rsRGB = [1, 1, 1] self.raster.setBand(self.rsRGB) if self.grid_message.isChecked(): if self.raster.checkOpenGrid(self.thumbnail_min): if self.loadGrid(self.raster): image, _ = self.raster.getGrid(0, 0) else: image, _ = self.raster.getArray() else: image, _ = self.raster.getArray() else: if self.dockWidgets["grid"].isVisible() is True: self.grid = RSGrids(self.raster) self.raster.open_grid = True self.initGrid() image, _ = self.raster.getGrid(0, 0) else: image, _ = self.raster.getArray() self.updateBandList() # self.updateSlideSld(True) else: self.edtGeoinfo.setText(self.tr("无")) # 如果没找到图片的reader if image is None: self.warn("打开图像失败", f"未找到{path}文件对应的读取程序") return self.image = image self.controller.setImage(image) self.updateImage(True) # 2. 加载标签 self.loadLabel(path) self.addRecentFile(path) self.imagePath = path return True def loadLabel(self, imgPath): if imgPath == "": return None # 1. 读取json格式标签 if self.save_status["json"]: def getName(path): return osp.splitext(osp.basename(path))[0] imgName = getName(imgPath) labelPath = None for path in self.labelPaths: if not path.endswith(".json"): continue if self.origExt: if getName(path) == osp.basename(imgPath): labelPath = path break else: if getName(path) == imgName: labelPath = path break if not labelPath: return labels = json.loads(open(labelPath, "r").read()) for label in labels: color = label["color"] labelIdx = label["labelIdx"] points = label["points"] poly = PolygonAnnotation( labelIdx, self.controller.image.shape, self.delPolygon, self.setDirty, color, color, self.opacity, ) self.scene.addItem(poly) self.scene.polygon_items.append(poly) for p in points: poly.addPointLast(QtCore.QPointF(p[0], p[1])) # 2. 读取coco格式标签 if self.save_status["coco"]: imgId = self.coco.imgNameToId.get(osp.basename(imgPath), None) if imgId is None: return anns = self.coco.imgToAnns[imgId] for ann in anns: xys = ann["segmentation"][0] points = [] for idx in range(0, len(xys), 2): points.append([xys[idx], xys[idx + 1]]) labelIdx = ann["category_id"] idlab = self.controller.labelList.getLabelById(labelIdx) if idlab is not None: color = idlab.color poly = PolygonAnnotation( ann["category_id"], self.controller.image.shape, self.delPolygon, self.setDirty, color, color, self.opacity, ann["id"], ) self.scene.addItem(poly) self.scene.polygon_items.append(poly) for p in points: poly.addPointLast(QtCore.QPointF(p[0], p[1])) def turnImg(self, delta, list_click=False): if (self.grid is None or self.grid.curr_idx is None) or list_click: # 1. 检查是否有图可翻,保存标签 self.currIdx += delta if self.currIdx >= len(self.imagePaths) or self.currIdx < 0: self.currIdx -= delta if delta == 1: self.statusbar.showMessage(self.tr(f"没有后一张图片")) else: self.statusbar.showMessage(self.tr(f"没有前一张图片")) self.saveImage(False) return else: self.saveImage(True) # 2. 打开新图 self.loadImage(self.imagePaths[self.currIdx]) self.listFiles.setCurrentRow(self.currIdx) else: self.turnGrid(delta) self.setDirty(False) def imageListClicked(self): if not self.controller: self.warn(self.tr("模型未加载"), self.tr("尚未加载模型,请先加载模型!")) self.changeParam() if not self.controller: return if self.controller.is_incomplete_mask: self.exportLabel() toRow = self.listFiles.currentRow() delta = toRow - self.currIdx self.turnImg(delta, True) def finishObject(self): if not self.controller or self.image is None: return current_mask, curr_polygon = self.controller.finishObject( building=self.boundaryRegular.isChecked() ) if curr_polygon is not None: self.updateImage() if current_mask is not None: # current_mask = current_mask.astype(np.uint8) * 255 # polygon = util.get_polygon(current_mask) color = self.controller.labelList[self.currLabelIdx].color self.createPoly(curr_polygon, color) # 状态改变 if self.status == self.EDITING: self.status = self.ANNING for p in self.scene.polygon_items: p.setAnning(isAnning=True) else: self.status = self.EDITING for p in self.scene.polygon_items: p.setAnning(isAnning=False) self.getMask() def completeLastMask(self): # 返回最后一个标签是否完成,false就是还有带点的 if not self.controller or self.controller.image is None: return True if not self.controller.is_incomplete_mask: return True res = self.warn( self.tr("完成最后一个目标?"), self.tr("是否完成最后一个目标的标注,不完成不会进行保存。"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Yes: self.finishObject() self.exportLabel() self.setDirty(False) return True return False def saveImage(self, close=False): if self.controller and self.controller.image is not None: # 1. 完成正在交互式标注的标签 self.completeLastMask() # 2. 进行保存 if self.isDirty: if self.actions.auto_save.isChecked(): self.exportLabel() else: res = self.warn( self.tr("保存标签?"), self.tr("标签尚未保存,是否保存标签"), QMessageBox.Yes | QMessageBox.Cancel, ) if res == QMessageBox.Yes: self.exportLabel() self.setDirty(False) if close: # 3. 清空多边形标注,删掉图片 for p in self.scene.polygon_items[::-1]: p.remove() self.scene.polygon_items = [] self.controller.resetLastObject() self.updateImage() self.controller.image = None if close: self.annImage.setPixmap(QPixmap()) def exportLabel(self, saveAs=False, savePath=None, lab_input=None): # 1. 需要处于标注状态 if not self.controller or self.controller.image is None: return # 2. 完成正在交互式标注的标签 self.completeLastMask() # 3. 确定保存路径 # 3.1 如果参数指定了保存路径直接存到savePath if not savePath: if not saveAs and self.outputDir is not None: # 3.2 指定了标签文件夹,而且不是另存为:根据标签文件夹和文件名出保存路径 name, ext = osp.splitext(osp.basename(self.imagePath)) if not self.origExt: ext = ".png" savePath = osp.join( self.outputDir, name + ext, ) else: # 3.3 没有指定标签存到哪,或者是另存为:弹框让用户选 savePath = self.chooseSavePath() if savePath is None or not osp.exists(osp.dirname(savePath)): return if savePath not in self.labelPaths: self.labelPaths.append(savePath) if lab_input is None: mask_output = self.getMask() s = self.controller.imgShape else: mask_output = lab_input s = lab_input.shape # BUG: 如果用了多边形标注从多边形生成mask # 4.1 保存灰度图 if self.save_status["gray_scale"]: if self.raster is not None: # FIXME: when big map saved, self.raster is None, # so adjust polygon can't saved in tif's mask. pathHead, _ = osp.splitext(savePath) # if self.rsSave.isChecked(): tifPath = pathHead + "_mask.tif" self.raster.saveMask(mask_output, tifPath) if self.shpSave.isChecked(): shpPath = pathHead + ".shp" # geocode_list = self.mask2poly(mask_output, False) print(rs.save_shp(shpPath, tifPath)) else: ext = osp.splitext(savePath)[1] cv2.imencode(ext, mask_output)[1].tofile(savePath) # self.labelPaths.append(savePath) # 4.2 保存伪彩色 if self.save_status["pseudo_color"]: if self.raster is None: pseudoPath, ext = osp.splitext(savePath) pseudoPath = pseudoPath + "_pseudo" + ext pseudo = np.zeros([s[0], s[1], 3]) # mask = self.controller.result_mask mask = mask_output # print(pseudo.shape, mask.shape) for lab in self.controller.labelList: pseudo[mask == lab.idx, :] = lab.color[::-1] cv2.imencode(ext, pseudo)[1].tofile(pseudoPath) # 4.3 保存前景抠图 if self.save_status["cutout"]: if self.raster is None: mattingPath, ext = osp.splitext(savePath) mattingPath = mattingPath + "_cutout" + ext img = np.ones([s[0], s[1], 4], dtype="uint8") * 255 img[:, :, :3] = self.controller.image.copy() img[mask_output == 0] = self.cutoutBackground img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) cv2.imencode(ext, img)[1].tofile(mattingPath) # 4.4 保存json if self.save_status["json"]: polygons = self.scene.polygon_items labels = [] for polygon in polygons: l = self.controller.labelList[polygon.labelIndex - 1] label = { "name": l.name, "labelIdx": l.idx, "color": l.color, "points": [], } for p in polygon.scnenePoints: label["points"].append(p) labels.append(label) if self.origExt: jsonPath = savePath + ".json" else: jsonPath = osp.splitext(savePath)[0] + ".json" open(jsonPath, "w", encoding="utf-8").write(json.dumps(labels)) self.labelPaths.append(jsonPath) # 4.5 保存coco if self.save_status["coco"]: if not self.coco.hasImage(osp.basename(self.imagePath)): imgId = self.coco.addImage(osp.basename(self.imagePath), s[1], s[0]) else: imgId = self.coco.imgNameToId[osp.basename(self.imagePath)] for polygon in self.scene.polygon_items: points = [] for p in polygon.scnenePoints: for val in p: points.append(val) if not polygon.coco_id: annId = self.coco.addAnnotation(imgId, polygon.labelIndex, points) polygon.coco_id = annId else: self.coco.updateAnnotation(polygon.coco_id, imgId, points) for lab in self.controller.labelList: if self.coco.hasCat(lab.idx): self.coco.updateCategory(lab.idx, lab.name, lab.color) else: self.coco.addCategory(lab.idx, lab.name, lab.color) saveDir = ( self.outputDir if self.outputDir is not None else osp.dirname(savePath) ) cocoPath = osp.join(saveDir, "annotations.json") open(cocoPath, "w", encoding="utf-8").write(json.dumps(self.coco.dataset)) self.setDirty(False) self.statusbar.showMessage(self.tr("标签成功保存至") + " " + savePath, 5000) def chooseSavePath(self): formats = [ "*.{}".format(fmt.data().decode()) for fmt in QtGui.QImageReader.supportedImageFormats() ] filters = "Label file (%s)" % " ".join(formats) dlg = QtWidgets.QFileDialog( self, self.tr("保存标签文件路径"), osp.dirname(self.imagePath), filters, ) dlg.setDefaultSuffix("png") dlg.setAcceptMode(QtWidgets.QFileDialog.AcceptSave) dlg.setOption(QtWidgets.QFileDialog.DontConfirmOverwrite, False) dlg.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, False) savePath, _ = dlg.getSaveFileName( self, self.tr("选择标签文件保存路径"), osp.splitext(osp.basename(self.imagePath))[0] + ".png", ) return savePath def eximgsInit(self): self.gridTable.setRowCount(0) self.gridTable.clearContents() # 清零 self.raster = None self.grid = None def setDirty(self, isDirty): self.isDirty = isDirty def changeOutputDir(self, outputDir=None): # 1. 弹框选择标签路径 if outputDir is None: options = ( QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks ) if self.settings.value("use_qt_widget", False, type=bool): options = options | QtWidgets.QFileDialog.DontUseNativeDialog outputDir = QtWidgets.QFileDialog.getExistingDirectory( self, self.tr("选择标签保存路径") + " - " + __APPNAME__, self.settings.value("output_dir", "."), options, ) if not osp.exists(outputDir): return False self.settings.setValue("output_dir", outputDir) self.outputDir = outputDir # 2. 加载标签 # 2.1 如果保存coco格式,加载coco标签 if self.save_status["coco"]: defaultPath = osp.join(self.outputDir, "annotations.json") if osp.exists(defaultPath): self.initCoco(defaultPath) # 2.2 如果保存json格式,获取所有json文件名 if self.save_status["json"]: labelPaths = os.listdir(outputDir) labelPaths = [n for n in labelPaths if n.endswith(".json")] labelPaths = [osp.join(outputDir, n) for n in labelPaths] self.labelPaths = labelPaths # 加载对应的标签列表 lab_auto_save = osp.join(self.outputDir, "autosave_label.txt") if osp.exists(lab_auto_save) == False: lab_auto_save = osp.join(self.outputDir, "label/autosave_label.txt") if osp.exists(lab_auto_save): try: self.importLabelList(lab_auto_save) except: pass return True def maskOpacityChanged(self): self.sldOpacity.textLab.setText(str(self.opacity)) if not self.controller or self.controller.image is None: return for polygon in self.scene.polygon_items: polygon.setOpacity(self.opacity) self.updateImage() def clickRadiusChanged(self): self.sldClickRadius.textLab.setText(str(self.clickRadius)) if not self.controller or self.controller.image is None: return self.updateImage() def threshChanged(self): self.sldThresh.textLab.setText(str(self.segThresh)) if not self.controller or self.controller.image is None: return self.controller.prob_thresh = self.segThresh self.updateImage() # def slideChanged(self): # self.sldMISlide.textLab.setText(str(self.slideMi)) # if not self.controller or self.controller.image is None: # return # self.midx = int(self.slideMi) - 1 # self.miSlideSet() # self.updateImage() def undoClick(self): if self.image is None: return if not self.controller: return self.controller.undoClick() self.updateImage() if not self.controller.is_incomplete_mask: self.setDirty(False) def clearAll(self): if not self.controller or self.controller.image is None: return self.controller.resetLastObject() self.updateImage() self.setDirty(False) def redoClick(self): if self.image is None: return if not self.controller: return self.controller.redoClick() self.updateImage() def canvasClick(self, x, y, isLeft): c = self.controller if c.image is None: return if not c.inImage(x, y): return if not c.modelSet: self.warn(self.tr("未选择模型", self.tr("尚未选择模型,请先在右上角选择模型"))) return if self.status == self.IDILE: return currLabel = self.controller.curr_label_number if not currLabel or currLabel == 0: self.warn(self.tr("未选择当前标签"), self.tr("请先在标签列表中单击点选标签")) return self.controller.addClick(x, y, isLeft) self.updateImage() self.status = self.ANNING def updateImage(self, reset_canvas=False): if not self.controller: return image = self.controller.get_visualization( alpha_blend=self.opacity, click_radius=self.clickRadius, ) height, width, _ = image.shape bytesPerLine = 3 * width image = QImage(image.data, width, height, bytesPerLine, QImage.Format_RGB888) if reset_canvas: self.resetZoom(width, height) self.annImage.setPixmap(QPixmap(image)) def viewZoomed(self, scale): self.scene.scale = scale self.scene.updatePolygonSize() # 界面缩放重置 def resetZoom(self, width, height): # 每次加载图像前设定下当前的显示框,解决图像缩小后不在中心的问题 self.scene.setSceneRect(0, 0, width, height) # 缩放清除 self.canvas.scale(1 / self.canvas.zoom_all, 1 / self.canvas.zoom_all) # 重置缩放 self.canvas.zoom_all = 1 # 最佳缩放 s_eps = 0.98 scr_cont = [ (self.scrollArea.width() * s_eps) / width, (self.scrollArea.height() * s_eps) / height, ] if scr_cont[0] * height > self.scrollArea.height(): self.canvas.zoom_all = scr_cont[1] else: self.canvas.zoom_all = scr_cont[0] self.canvas.scale(self.canvas.zoom_all, self.canvas.zoom_all) self.scene.scale = self.canvas.zoom_all def keyReleaseEvent(self, event): # print(event.key(), Qt.Key_Control) # 释放ctrl的时候刷新图像,对应自适应点大小在缩放后刷新 if not self.controller or self.controller.image is None: return if event.key() == Qt.Key_Control: self.updateImage() def queueEvent(self, function): QtCore.QTimer.singleShot(0, function) def toggleOrigExt(self, dst=None): if dst: self.origExt = dst else: self.origExt = not self.origExt self.actions.origional_extension.setChecked(self.origExt) def toggleAutoSave(self, save): if save and not self.outputDir: self.changeOutputDir(None) if save and not self.outputDir: save = False self.actions.auto_save.setChecked(save) self.settings.setValue("auto_save", save) def toggleSave(self, type): self.save_status[type] = not self.save_status[type] if type == "coco" and self.save_status["coco"]: self.initCoco() if type == "coco": self.save_status["json"] = not self.save_status["coco"] self.actions.save_json.setChecked(self.save_status["json"]) if type == "json": self.save_status["coco"] = not self.save_status["json"] self.actions.save_coco.setChecked(self.save_status["coco"]) def initCoco(self, coco_path: str = None): if not coco_path: if not self.outputDir or not osp.exists(self.outputDir): coco_path = None else: coco_path = osp.join(self.outputDir, "annotations.json") else: if not osp.exists(coco_path): coco_path = None self.coco = COCO(coco_path) if self.clearLabelList(): self.controller.labelList = util.LabelList(self.coco.dataset["categories"]) self.refreshLabelList() def toggleWidget(self, index=None, warn=True): # TODO: 输入从数字改成名字 # 1. 改变 if isinstance(index, int): self.dockStatus[index] = not self.dockStatus[index] # 2. 判断widget是否可以开启 # 2.1 遥感 if self.dockStatus[4] and not (rs.check_gdal() and rs.check_rasterio()): if warn: self.warn( self.tr("无法导入GDAL"), self.tr("使用遥感工具需要安装GDAL!"), QMessageBox.Yes, ) self.statusbar.showMessage(self.tr("打开遥感工具失败,请安装GDAL库")) self.dockStatus[4] = False # 2.2 医疗 if self.dockStatus[5] and not med.has_sitk(): if warn: self.warn( self.tr("无法导入SimpleITK"), self.tr("使用医疗工具需要安装SimpleITK!"), QMessageBox.Yes, ) self.statusbar.showMessage(self.tr("打开医疗工具失败,请安装SimpleITK")) self.dockStatus[5] = False widgets = list(self.dockWidgets.values()) for idx, s in enumerate(self.dockStatus): self.menus.showMenu[idx].setChecked(s) if s: widgets[idx].show() else: widgets[idx].hide() self.settings.setValue("dock_status", self.dockStatus) # self.display_dockwidget[index] = bool(self.display_dockwidget[index] - 1) # self.toggleDockWidgets() self.saveLayout() # def toggleDockWidgets(self, is_init=False): # if is_init == True: # if self.dockStatus != []: # if len(self.dockStatus) != len(self.menus.showMenu): # self.settings.remove("dock_status") # else: # self.display_dockwidget = [strtobool(w) for w in self.dockStatus] # for i in range(len(self.menus.showMenu)): # self.menus.showMenu[i].setChecked(bool(self.display_dockwidget[i])) # else: # self.settings.setValue("dock_status", self.display_dockwidget) # for t, w in zip(self.display_dockwidget, self.dockWidgets.values()): # if t == True: # w.show() # else: # w.hide() def rsBandSet(self, idx): if self.raster is None: return for i in range(len(self.bandCombos)): self.rsRGB[i] = self.bandCombos[i].currentIndex() + 1 # 从1开始 self.raster.setBand(self.rsRGB) if self.grid is not None: if isinstance(self.grid.curr_idx, (list, tuple)): row, col = self.grid.curr_idx image, _ = self.raster.getGrid(row, col) else: image, _ = self.raster.getArray() else: image, _ = self.raster.getArray() self.image = image self.controller.image = image self.updateImage() # def miSlideSet(self): # image = rs.slice_img(self.controller.rawImage, self.midx) # self.test_show(image) # def changeWorkerShow(self, index): # self.display_dockwidget[index] = bool(self.display_dockwidget[index] - 1) # self.toggleDockWidgets() def updateBandList(self, clean=False): if clean: for i in range(len(self.bandCombos)): try: # 避免打开jpg后再打开tif报错 self.bandCombos[i].currentIndexChanged.disconnect() except TypeError: pass self.bandCombos[i].clear() self.bandCombos[i].addItems(["band_1"]) return bands = self.raster.geoinfo.count for i in range(len(self.bandCombos)): try: # 避免打开jpg后再打开tif报错 self.bandCombos[i].currentIndexChanged.disconnect() except TypeError: pass self.bandCombos[i].clear() self.bandCombos[i].addItems([("band_" + str(j + 1)) for j in range(bands)]) try: self.bandCombos[i].setCurrentIndex(self.rsRGB[i] - 1) except IndexError: pass for bandCombo in self.bandCombos: bandCombo.currentIndexChanged.connect(self.rsBandSet) # 设置波段 # def updateSlideSld(self, clean=False): # if clean: # self.sldMISlide.setMaximum(1) # return # C = self.controller.rawImage.shape[-1] if len(self.controller.rawImage.shape) == 3 else 1 # self.sldMISlide.setMaximum(C) def toggleLargestCC(self, on): try: self.controller.filterLargestCC(on) except: pass # 宫格标注 def initGrid(self): self.delAllPolygon() grid_row_count, grid_col_count = self.grid.createGrids() self.gridTable.setRowCount(grid_row_count) self.gridTable.setColumnCount(grid_col_count) for r in range(grid_row_count): for c in range(grid_col_count): self.gridTable.setItem(r, c, QtWidgets.QTableWidgetItem()) self.gridTable.item(r, c).setBackground(self.GRID_COLOR["idle"]) self.gridTable.item(r, c).setFlags(Qt.ItemIsSelectable) # 无法高亮选择 # 初始显示第一个 self.grid.curr_idx = (0, 0) self.gridTable.item(0, 0).setBackground(self.GRID_COLOR["overlying"]) # 事件注册 self.gridTable.cellClicked.connect(self.changeGrid) def changeGrid(self, row, col): # 清除未保存的切换 # TODO: 这块应该通过dirty判断? if self.grid.curr_idx is not None: self.saveGrid() # 切换时自动保存上一块 last_r, last_c = self.grid.curr_idx if self.grid.mask_grids[last_r][last_c] is None: self.gridTable.item(last_r, last_c).setBackground( self.GRID_COLOR["idle"] ) else: self.gridTable.item(last_r, last_c).setBackground( self.GRID_COLOR["finised"] ) self.delAllPolygon() image, mask = self.grid.getGrid(row, col) self.controller.setImage(image) self.grid.curr_idx = (row, col) if mask is None: self.gridTable.item(row, col).setBackground(self.GRID_COLOR["current"]) else: self.gridTable.item(row, col).setBackground(self.GRID_COLOR["overlying"]) self.mask2poly(mask) # 刷新 self.updateImage(True) def mask2poly(self, mask, show=True): labs = np.unique(mask)[1:] colors = [] for i in range(len(labs)): idx = int(labs[i]) - 1 if idx < len(self.controller.labelList): c = self.controller.labelList[idx].color else: if self.currLabelIdx != -1: c = self.controller.labelList[self.currLabelIdx].color else: c = None colors.append(c) geocode_list = [] for idx, (l, c) in enumerate(zip(labs, colors)): if c is not None: curr_polygon = util.get_polygon( ((mask == l).astype(np.uint8) * 255), building=self.boundaryRegular.isChecked(), ) if show == True: self.createPoly(curr_polygon, c) for p in self.scene.polygon_items: p.setAnning(isAnning=False) else: for g in curr_polygon: points = [gi.tolist() for gi in g] geocode_list.append( { "name": self.controller.labelList[idx].name, "points": points, } ) return geocode_list def saveGrid(self): row, col = self.grid.curr_idx if self.grid.curr_idx is None: return self.gridTable.item(row, col).setBackground(self.GRID_COLOR["overlying"]) # if len(np.unique(self.grid.mask_grids[row][col])) == 1: self.grid.mask_grids[row][col] = np.array(self.getMask()) if self.cheSaveEvery.isChecked(): if self.outputDir is None: self.changeOutputDir() _, fullflname = osp.split(self.listFiles.currentItem().text()) fname, _ = os.path.splitext(fullflname) path = osp.join( self.outputDir, (fname + "_data_" + str(row) + "_" + str(col) + ".tif") ) im, tf = self.raster.getGrid(row, col) h, w = im.shape[:2] geoinfo = edict() geoinfo.xsize = w geoinfo.ysize = h geoinfo.dtype = self.raster.geoinfo.dtype geoinfo.crs = self.raster.geoinfo.crs geoinfo.geotf = tf self.raster.saveMask( self.grid.mask_grids[row][col], path.replace("data", "mask"), geoinfo ) # 保存mask self.raster.saveMask(im, path, geoinfo, 3) # 保存图像 def turnGrid(self, delta): # 切换下一个宫格 r, c = self.grid.curr_idx if self.grid.curr_idx is not None else (0, -1) c += delta if c >= self.grid.grid_count[1]: c = 0 r += 1 if r >= self.grid.grid_count[0]: r = 0 if c < 0: c = self.grid.grid_count[1] - 1 r -= 1 if r < 0: r = self.grid.grid_count[0] - 1 self.changeGrid(r, c) def closeGrid(self): self.grid = None self.gridTable.setRowCount(0) self.gridTable.clearContents() def saveGridLabel(self): if self.outputDir is not None: name, ext = osp.splitext(osp.basename(self.imagePath)) if not self.origExt: ext = ".png" save_path = osp.join(self.outputDir, name + ext) else: save_path = self.chooseSavePath() if save_path == "": return try: self.finishObject() self.saveGrid() # 先保存当前 except: pass self.delAllPolygon() # 清理 mask = self.grid.splicingList(save_path) if self.grid.__class__.__name__ == "RSGrids": self.image, is_big = self.raster.getArray() else: self.image = self.grid.detimg is_big = checkOpenGrid(self.image, self.thumbnail_min) if is_big is None: self.statusbar.showMessage(self.tr("图像过大,已显示缩略图")) self.controller.image = self.image self.controller._result_mask = mask self.exportLabel(savePath=save_path, lab_input=mask) # -- RS Show polygon demo -- if self.show_rs_poly.isChecked(): h, w = self.image.shape[:2] th_mask = cv2.resize(mask, dsize=(w, h), interpolation=cv2.INTER_NEAREST) indexs = np.unique(th_mask)[1:] for i in indexs: i_mask = np.zeros_like(th_mask, dtype="uint8") i_mask[th_mask == i] = 255 curr_polygon = util.get_polygon(i_mask) color = self.controller.labelList[i - 1].color self.createPoly(curr_polygon, color) for p in self.scene.polygon_items: p.setAnning(isAnning=False) # -- RS Show polygon demo -- # 刷新 grid_row_count = self.gridTable.rowCount() grid_col_count = self.gridTable.colorCount() for r in range(grid_row_count): for c in range(grid_col_count): try: self.gridTable.item(r, c).setBackground(self.GRID_COLOR["idle"]) except: pass self.raster = None self.closeGrid() self.updateBandList(True) self.controller.setImage(self.image) self.updateImage(True) self.setDirty(False) @property def opacity(self): return self.sldOpacity.value() / 100 @property def clickRadius(self): return self.sldClickRadius.value() @property def segThresh(self): return self.sldThresh.value() / 100 # @property # def slideMi(self): # return self.sldMISlide.value() def warnException(self, e): e = str(e) title = e.split("。")[0] self.warn(title, e) def warn(self, title, text, buttons=QMessageBox.Yes): msg = QMessageBox() # msg.setIcon(QMessageBox.Warning) msg.setWindowTitle(title) msg.setText(text) msg.setStandardButtons(buttons) return msg.exec_() @property def status(self): # TODO: 图片,模型 if not self.controller: return self.IDILE c = self.controller if c.model is None or c.image is None: return self.IDILE if self._anning: return self.ANNING return self.EDITING @status.setter def status(self, status): if status not in [self.ANNING, self.EDITING]: return if status == self.ANNING: self._anning = True else: self._anning = False def loadGrid(self, img, is_rs=True): res = self.warn(self.tr("图像过大"), self.tr("图像过大,将启用宫格功能!"), \ buttons=QMessageBox.Yes | QMessageBox.No) if res == QMessageBox.Yes: # 打开宫格功能 if self.dockWidgets["grid"].isVisible() is False: # TODO: 改成self.dockStatus self.menus.showMenu[-1].setChecked(True) # self.display_dockwidget[-1] = True self.dockWidgets["grid"].show() self.grid = RSGrids(img) if is_rs else Grids(img) self.initGrid() return True return False # 界面布局 def loadLayout(self): self.restoreState(self.layoutStatus) # TODO: 这里检查环境,判断是不是开医疗和遥感widget def saveLayout(self): # 保存界面 self.settings.setValue("layout_status", QByteArray(self.saveState())) self.settings.setValue( "save_status", [(k, self.save_status[k]) for k in self.save_status.keys()] ) # # 如果设置了保存路径,把标签也保存下 # if self.outputDir is not None and len(self.controller.labelList) != 0: # self.exportLabelList(osp.join(self.outputDir, "autosave_label.txt")) def closeEvent(self, event): self.saveImage() self.saveLayout() QCoreApplication.quit() # sys.exit(0) def reportBug(self): webbrowser.open("https://github.com/PaddleCV-SIG/EISeg/issues/new/choose") def quickStart(self): # self.saveImage(True) # self.canvas.setStyleSheet(self.note_style) webbrowser.open("https://github.com/PaddleCV-SIG/EISeg/tree/release/0.4.0") def toggleLogging(self, s): if s: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.CRITICAL) self.settings.setValue("log", s) def toBeImplemented(self): self.statusbar.showMessage(self.tr("功能尚在开发")) # 医疗 def wwChanged(self): if not self.controller or self.image is None: return try: # 那种jpg什么格式的医疗图像调整窗宽等会造成崩溃 self.textWw.selectAll() self.controller.image = med.windowlize( self.controller.rawImage, self.ww, self.wc ) self.updateImage() except: pass def wcChanged(self): if not self.controller or self.image is None: return try: self.textWc.selectAll() self.controller.image = med.windowlize( self.controller.rawImage, self.ww, self.wc ) self.updateImage() except: pass @property def ww(self): return int(self.textWw.text()) @property def wc(self): return int(self.textWc.text()) def twwChanged(self): if self.ww > self.sliderWw.maximum(): self.textWw.setText(str(self.sliderWw.maximum())) if self.ww < self.sliderWw.minimum(): self.textWw.setText(str(self.sliderWw.minimum())) self.sliderWw.setProperty("value", self.ww) self.wwChanged() def swwChanged(self): self.textWw.setText(str(self.sliderWw.value())) self.wwChanged() def twcChanged(self): if self.wc > self.sliderWc.maximum(): self.textWc.setText(str(self.sliderWc.maximum())) if self.wc < self.sliderWc.minimum(): self.textWc.setText(str(self.sliderWc.minimum())) self.sliderWc.setProperty("value", self.wc) self.wcChanged() def swcChanged(self): self.textWc.setText(str(self.sliderWc.value())) self.wcChanged() def useQtWidget(self, s): print("checked", s) self.settings.setValue("use_qt_widget", s) ================================================ FILE: eiseg/config/colormap.txt ================================================ 53,119,181 245,128,6 67,159,36 204,43,41 145,104,190 135,86,75 219,120,195 127,127,127 187,189,18 72,190,207 178,199,233 248,187,118 160,222,135 247,153,150 195,176,214 192,156,148 241,183,211 199,199,199 218,219,139 166,218,229 ================================================ FILE: eiseg/config/config.yaml ================================================ shortcut: about: Q auto_save: X change_output_dir: Shift+Z clear: Ctrl+Shift+Z clear_label: '' clear_recent: '' close: Ctrl+W data_worker: '' del_active_polygon: Backspace edit_shortcuts: E finish_object: Space grid_ann: '' label_worker: '' largest_component: '' load_label: '' load_param: Ctrl+M medical_worker: '' model_worker: '' open_folder: Shift+A open_image: Ctrl+A origional_extension: '' quick_start: '' quit: '' redo: Ctrl+Y remote_worker: '' save: '' save_as: '' save_coco: '' save_json: '' save_label: '' save_pseudo: '' set_worker: '' turn_next: F turn_prev: S undo: Ctrl+Z ================================================ FILE: eiseg/controller.py ================================================ # Copyright (c) 2021 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.path as osp import time import json import logging import cv2 import numpy as np from skimage.measure import label import paddle from eiseg import logger from inference import clicker from inference.predictor import get_predictor import util from util.vis import draw_with_blend_and_clicks from models import EISegModel from util import LabelList class InteractiveController: def __init__( self, predictor_params: dict = None, prob_thresh: float = 0.5, ): """初始化控制器. Parameters ---------- predictor_params : dict 推理器配置 prob_thresh : float 区分前景和背景结果的阈值 """ self.predictor_params = predictor_params self.prob_thresh = prob_thresh self.model = None self.image = None self.rawImage = None self.predictor = None self.clicker = clicker.Clicker() self.states = [] self.probs_history = [] self.polygons = [] # 用于redo self.undo_states = [] self.undo_probs_history = [] self.curr_label_number = 0 self._result_mask = None self.labelList = LabelList() self.lccFilter = False self.log = logging.getLogger(__name__) def filterLargestCC(self, do_filter: bool): """设置是否只保留推理结果中的最大联通块 Parameters ---------- do_filter : bool 是否只保存推理结果中的最大联通块 """ if not isinstance(do_filter, bool): return self.lccFilter = do_filter def setModel(self, param_path=None, use_gpu=None): """设置推理其模型. Parameters ---------- params_path : str 模型路径 use_gpu : bool None:检测,根据paddle版本判断 bool:按照指定是否开启GPU Returns ------- bool, str 是否成功设置模型, 失败原因 """ if param_path is not None: model_path = param_path.replace(".pdiparams", ".pdmodel") if not osp.exists(model_path): raise Exception(f"未在 {model_path} 找到模型文件") if use_gpu is None: if paddle.device.is_compiled_with_cuda(): # TODO: 可以使用GPU却返回False use_gpu = True else: use_gpu = False logger.info(f"User paddle compiled with gpu: use_gpu {use_gpu}") tic = time.time() try: self.model = EISegModel(model_path, param_path, use_gpu) self.reset_predictor() # 即刻生效 except KeyError as e: return False, str(e) logger.info(f"Load model {model_path} took {time.time() - tic}") return True, "模型设置成功" def setImage(self, image: np.array): """设置当前标注的图片 Parameters ---------- image : np.array 当前标注的图片 """ if self.model is not None: self.image = image self._result_mask = np.zeros(image.shape[:2], dtype=np.uint8) self.resetLastObject() # 标签操作 def setLabelList(self, labelList: json): """设置标签列表,会覆盖已有的标签列表 Parameters ---------- labelList : json 标签列表格式为 { { "idx" : int (like 0 or 1 or 2) "name" : str (like "car" or "airplan") "color" : list (like [255, 0, 0]) }, ... } Returns ------- type Description of returned object. """ self.labelList.clear() labels = json.loads(labelList) for lab in labels: self.labelList.add(lab["id"], lab["name"], lab["color"]) def addLabel(self, id: int, name: str, color: list): self.labelList.add(id, name, color) def delLabel(self, id: int): self.labelList.remove(id) def clearLabel(self): self.labelList.clear() def importLabel(self, path): self.labelList.importLabel(path) def exportLabel(self, path): self.labelList.exportLabel(path) # 点击操作 def addClick(self, x: int, y: int, is_positive: bool): """添加一个点并运行推理,保存历史用于undo Parameters ---------- x : int 点击的横坐标 y : int 点击的纵坐标 is_positive : bool 是否点的是正点 Returns ------- bool, str 点击是否添加成功, 失败原因 """ # 1. 确定可以点 if not self.inImage(x, y): return False, "点击越界" if not self.modelSet: return False, "未加载模型" if not self.imageSet: return False, "图像未设置" if len(self.states) == 0: # 保存一个空状态 self.states.append( { "clicker": self.clicker.get_state(), "predictor": self.predictor.get_states(), } ) # 2. 添加点击,跑推理 click = clicker.Click(is_positive=is_positive, coords=(y, x)) self.clicker.add_click(click) pred = self.predictor.get_prediction(self.clicker) # 3. 保存状态 self.states.append( { "clicker": self.clicker.get_state(), "predictor": self.predictor.get_states(), } ) if self.probs_history: self.probs_history.append((self.probs_history[-1][1], pred)) else: self.probs_history.append((np.zeros_like(pred), pred)) # 点击之后就不能接着之前的历史redo了 self.undo_states = [] self.undo_probs_history = [] return True, "点击添加成功" def undoClick(self): """ undo一步点击 """ if len(self.states) <= 1: # == 1就只剩下一个空状态了,不用再退 return self.undo_states.append(self.states.pop()) self.clicker.set_state(self.states[-1]["clicker"]) self.predictor.set_states(self.states[-1]["predictor"]) self.undo_probs_history.append(self.probs_history.pop()) if not self.probs_history: self.reset_init_mask() def redoClick(self): """ redo一步点击 """ if len(self.undo_states) == 0: # 如果还没撤销过 return if len(self.undo_probs_history) >= 1: next_state = self.undo_states.pop() self.states.append(next_state) self.clicker.set_state(next_state["clicker"]) self.predictor.set_states(next_state["predictor"]) self.probs_history.append(self.undo_probs_history.pop()) def finishObject(self, building=False): """ 结束当前物体标注,准备标下一个 """ object_prob = self.current_object_prob if object_prob is None: return None, None object_mask = object_prob > self.prob_thresh if self.lccFilter: object_mask = self.getLargestCC(object_mask) polygon = util.get_polygon((object_mask.astype(np.uint8) * 255), img_size=object_mask.shape, building=building) if polygon is not None: self._result_mask[object_mask] = self.curr_label_number self.resetLastObject() self.polygons.append([self.curr_label_number, polygon]) return object_mask, polygon # 多边形 def getPolygon(self): return self.polygon def setPolygon(self, polygon): self.polygon = polygon # mask def getMask(self): s = self.imgShape img = np.zeros([s[0], s[1]]) for poly in self.polygons: pts = np.int32([np.array(poly[1])]) cv2.fillPoly(img, pts=pts, color=poly[0]) return img def setCurrLabelIdx(self, number): if not isinstance(number, int): return False self.curr_label_number = number def resetLastObject(self, update_image=True): """ 重置控制器状态 Parameters update_image(bool): 是否更新图像 """ self.states = [] self.probs_history = [] self.undo_states = [] self.undo_probs_history = [] # self.current_object_prob = None self.clicker.reset_clicks() self.reset_predictor() self.reset_init_mask() def reset_predictor(self, predictor_params=None): """ 重置推理器,可以换推理配置 Parameters predictor_params(dict): 推理配置 """ if predictor_params is not None: self.predictor_params = predictor_params if self.model.model: self.predictor = get_predictor(self.model.model, **self.predictor_params) if self.image is not None: self.predictor.set_input_image(self.image) def reset_init_mask(self): self.clicker.click_indx_offset = 0 def getLargestCC(self, mask): mask = label(mask) if mask.max() == 0: return mask mask = mask == np.argmax(np.bincount(mask.flat)[1:]) + 1 return mask def get_visualization(self, alpha_blend: float, click_radius: int): if self.image is None: return None # 1. 正在标注的mask # results_mask_for_vis = self.result_mask # 加入之前标完的mask results_mask_for_vis = np.zeros_like(self.result_mask) results_mask_for_vis *= self.curr_label_number if self.probs_history: results_mask_for_vis[ self.current_object_prob > self.prob_thresh ] = self.curr_label_number if self.lccFilter: results_mask_for_vis = ( self.getLargestCC(results_mask_for_vis) * self.curr_label_number ) vis = draw_with_blend_and_clicks( self.image, mask=results_mask_for_vis, alpha=alpha_blend, clicks_list=self.clicker.clicks_list, radius=click_radius, palette=self.palette, ) return vis def inImage(self, x: int, y: int): s = self.image.shape if x < 0 or y < 0 or x >= s[1] or y >= s[0]: return False return True @property def result_mask(self): result_mask = self._result_mask.copy() return result_mask @property def palette(self): if self.labelList: colors = [ml.color for ml in self.labelList] colors.insert(0, [0, 0, 0]) else: colors = [[0, 0, 0]] return colors @property def current_object_prob(self): """ 获取当前推理标签 """ if self.probs_history: _, current_prob_additive = self.probs_history[-1] return current_prob_additive else: return None @property def is_incomplete_mask(self): """ Returns bool: 当前的物体是不是还没标完 """ return len(self.probs_history) > 0 @property def imgShape(self): return self.image.shape # [1::-1] @property def modelSet(self): return self.model is not None @property def modelName(self): return self.model.__name__ @property def imageSet(self): return self.image is not None ================================================ FILE: eiseg/exe.py ================================================ # Copyright (c) 2021 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.path as osp import sys sys.path.append(osp.dirname(osp.dirname(osp.realpath(__file__)))) from run import main if __name__ == "__main__": main() ================================================ FILE: eiseg/inference/__init__.py ================================================ ================================================ FILE: eiseg/inference/clicker.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import cv2 import numpy as np from copy import deepcopy class Clicker(object): def __init__( self, gt_mask=None, init_clicks=None, ignore_label=-1, click_indx_offset=0 ): self.click_indx_offset = click_indx_offset if gt_mask is not None: self.gt_mask = gt_mask == 1 self.not_ignore_mask = gt_mask != ignore_label else: self.gt_mask = None self.reset_clicks() if init_clicks is not None: for click in init_clicks: self.add_click(click) def make_next_click(self, pred_mask): assert self.gt_mask is not None click = self._get_next_click(pred_mask) self.add_click(click) def get_clicks(self, clicks_limit=None): return self.clicks_list[:clicks_limit] def _get_next_click(self, pred_mask, padding=True): fn_mask = np.logical_and( np.logical_and(self.gt_mask, np.logical_not(pred_mask)), self.not_ignore_mask, ) fp_mask = np.logical_and( np.logical_and(np.logical_not(self.gt_mask), pred_mask), self.not_ignore_mask, ) if padding: fn_mask = np.pad(fn_mask, ((1, 1), (1, 1)), "constant") fp_mask = np.pad(fp_mask, ((1, 1), (1, 1)), "constant") fn_mask_dt = cv2.distanceTransform(fn_mask.astype(np.uint8), cv2.DIST_L2, 0) fp_mask_dt = cv2.distanceTransform(fp_mask.astype(np.uint8), cv2.DIST_L2, 0) if padding: fn_mask_dt = fn_mask_dt[1:-1, 1:-1] fp_mask_dt = fp_mask_dt[1:-1, 1:-1] fn_mask_dt = fn_mask_dt * self.not_clicked_map fp_mask_dt = fp_mask_dt * self.not_clicked_map fn_max_dist = np.max(fn_mask_dt) fp_max_dist = np.max(fp_mask_dt) is_positive = fn_max_dist > fp_max_dist if is_positive: coords_y, coords_x = np.where(fn_mask_dt == fn_max_dist) # coords is [y, x] else: coords_y, coords_x = np.where(fp_mask_dt == fp_max_dist) # coords is [y, x] return Click(is_positive=is_positive, coords=(coords_y[0], coords_x[0])) def add_click(self, click): coords = click.coords click.indx = self.click_indx_offset + self.num_pos_clicks + self.num_neg_clicks if click.is_positive: self.num_pos_clicks += 1 else: self.num_neg_clicks += 1 self.clicks_list.append(click) if self.gt_mask is not None: self.not_clicked_map[coords[0], coords[1]] = False def _remove_last_click(self): click = self.clicks_list.pop() coords = click.coords if click.is_positive: self.num_pos_clicks -= 1 else: self.num_neg_clicks -= 1 if self.gt_mask is not None: self.not_clicked_map[coords[0], coords[1]] = True def reset_clicks(self): if self.gt_mask is not None: self.not_clicked_map = np.ones_like(self.gt_mask, dtype=np.bool) self.num_pos_clicks = 0 self.num_neg_clicks = 0 self.clicks_list = [] def get_state(self): return deepcopy(self.clicks_list) def set_state(self, state): self.reset_clicks() for click in state: self.add_click(click) def __len__(self): return len(self.clicks_list) class Click: def __init__(self, is_positive, coords, indx=None): self.is_positive = is_positive self.coords = coords self.indx = indx @property def coords_and_indx(self): return (*self.coords, self.indx) def copy(self, **kwargs): self_copy = deepcopy(self) for k, v in kwargs.items(): setattr(self_copy, k, v) return self_copy ================================================ FILE: eiseg/inference/predictor/__init__.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ from .base import BasePredictor from inference.transforms import ZoomIn def get_predictor( net, brs_mode, with_flip=False, zoom_in_params=dict(), predictor_params=None ): predictor_params_ = {"optimize_after_n_clicks": 1} if zoom_in_params is not None: zoom_in = ZoomIn(**zoom_in_params) else: zoom_in = None if brs_mode == "NoBRS": if predictor_params is not None: predictor_params_.update(predictor_params) predictor = BasePredictor( net, zoom_in=zoom_in, with_flip=with_flip, **predictor_params_ ) else: raise NotImplementedError("Just support NoBRS mode") return predictor ================================================ FILE: eiseg/inference/predictor/base.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import paddle import paddle.nn.functional as F import numpy as np from inference.transforms import AddHorizontalFlip, SigmoidForPred, LimitLongestSide from .ops import DistMaps, ScaleLayer, BatchImageNormalize class BasePredictor(object): def __init__( self, model, net_clicks_limit=None, with_flip=False, zoom_in=None, max_size=None, with_mask=True, **kwargs ): self.with_flip = with_flip self.net_clicks_limit = net_clicks_limit self.original_image = None self.zoom_in = zoom_in self.prev_prediction = None self.model_indx = 0 self.click_models = None self.net_state_dict = None self.with_prev_mask = with_mask self.net = model self.normalization = BatchImageNormalize( [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ) self.transforms = [zoom_in] if zoom_in is not None else [] if max_size is not None: self.transforms.append(LimitLongestSide(max_size=max_size)) self.transforms.append(SigmoidForPred()) if with_flip: self.transforms.append(AddHorizontalFlip()) self.dist_maps = DistMaps( norm_radius=5, spatial_scale=1.0, cpu_mode=False, use_disks=True ) def to_tensor(self, x): if isinstance(x, np.ndarray): if x.ndim == 2: x = x[:, :, None] img = paddle.to_tensor(x.transpose([2, 0, 1])).astype("float32") / 255 return img def set_input_image(self, image): image_nd = self.to_tensor(image) for transform in self.transforms: transform.reset() self.original_image = image_nd if len(self.original_image.shape) == 3: self.original_image = self.original_image.unsqueeze(0) self.prev_prediction = paddle.zeros_like(self.original_image[:, :1, :, :]) if not self.with_prev_mask: self.prev_edge = paddle.zeros_like(self.original_image[:, :1, :, :]) def get_prediction(self, clicker, prev_mask=None): clicks_list = clicker.get_clicks() input_image = self.original_image if prev_mask is None: if not self.with_prev_mask: prev_mask = self.prev_edge else: prev_mask = self.prev_prediction input_image = paddle.concat([input_image, prev_mask], axis=1) image_nd, clicks_lists, is_image_changed = self.apply_transforms( input_image, [clicks_list] ) pred_logits, pred_edges = self._get_prediction( image_nd, clicks_lists, is_image_changed ) pred_logits = paddle.to_tensor(pred_logits) prediction = F.interpolate( pred_logits, mode="bilinear", align_corners=True, size=image_nd.shape[2:] ) if pred_edges is not None: pred_edge = paddle.to_tensor(pred_edges) edge_prediction = F.interpolate( pred_edge, mode="bilinear", align_corners=True, size=image_nd.shape[2:] ) for t in reversed(self.transforms): if pred_edges is not None: edge_prediction = t.inv_transform(edge_prediction) self.prev_edge = edge_prediction prediction = t.inv_transform(prediction) if self.zoom_in is not None and self.zoom_in.check_possible_recalculation(): return self.get_prediction(clicker) self.prev_prediction = prediction return prediction.numpy()[0, 0] def prepare_input(self, image): prev_mask = None prev_mask = image[:, 3:, :, :] image = image[:, :3, :, :] image = self.normalization(image) return image, prev_mask def get_coord_features(self, image, prev_mask, points): coord_features = self.dist_maps(image, points) if prev_mask is not None: coord_features = paddle.concat((prev_mask, coord_features), axis=1) return coord_features def _get_prediction(self, image_nd, clicks_lists, is_image_changed): input_names = self.net.get_input_names() self.input_handle_1 = self.net.get_input_handle(input_names[0]) self.input_handle_2 = self.net.get_input_handle(input_names[1]) points_nd = self.get_points_nd(clicks_lists) image, prev_mask = self.prepare_input(image_nd) coord_features = self.get_coord_features(image, prev_mask, points_nd) image = image.numpy().astype("float32") coord_features = coord_features.numpy().astype("float32") self.input_handle_1.copy_from_cpu(image) self.input_handle_2.copy_from_cpu(coord_features) self.net.run() output_names = self.net.get_output_names() output_handle = self.net.get_output_handle(output_names[0]) output_data = output_handle.copy_to_cpu() if len(output_names) == 3: edge_handle = self.net.get_output_handle(output_names[2]) edge_data = edge_handle.copy_to_cpu() return output_data, edge_data else: return output_data, None def _get_transform_states(self): return [x.get_state() for x in self.transforms] def _set_transform_states(self, states): assert len(states) == len(self.transforms) for state, transform in zip(states, self.transforms): transform.set_state(state) def apply_transforms(self, image_nd, clicks_lists): is_image_changed = False for t in self.transforms: image_nd, clicks_lists = t.transform(image_nd, clicks_lists) is_image_changed |= t.image_changed return image_nd, clicks_lists, is_image_changed def get_points_nd(self, clicks_lists): total_clicks = [] num_pos_clicks = [ sum(x.is_positive for x in clicks_list) for clicks_list in clicks_lists ] num_neg_clicks = [ len(clicks_list) - num_pos for clicks_list, num_pos in zip(clicks_lists, num_pos_clicks) ] num_max_points = max(num_pos_clicks + num_neg_clicks) if self.net_clicks_limit is not None: num_max_points = min(self.net_clicks_limit, num_max_points) num_max_points = max(1, num_max_points) for clicks_list in clicks_lists: clicks_list = clicks_list[: self.net_clicks_limit] pos_clicks = [ click.coords_and_indx for click in clicks_list if click.is_positive ] pos_clicks = pos_clicks + (num_max_points - len(pos_clicks)) * [ (-1, -1, -1) ] neg_clicks = [ click.coords_and_indx for click in clicks_list if not click.is_positive ] neg_clicks = neg_clicks + (num_max_points - len(neg_clicks)) * [ (-1, -1, -1) ] total_clicks.append(pos_clicks + neg_clicks) return paddle.to_tensor(total_clicks) def get_states(self): return { "transform_states": self._get_transform_states(), "prev_prediction": self.prev_prediction, } def set_states(self, states): self._set_transform_states(states["transform_states"]) self.prev_prediction = states["prev_prediction"] def split_points_by_order(tpoints, groups): points = tpoints.numpy() num_groups = len(groups) bs = points.shape[0] num_points = points.shape[1] // 2 groups = [x if x > 0 else num_points for x in groups] group_points = [np.full((bs, 2 * x, 3), -1, dtype=np.float32) for x in groups] last_point_indx_group = np.zeros((bs, num_groups, 2), dtype=np.int) for group_indx, group_size in enumerate(groups): last_point_indx_group[:, group_indx, 1] = group_size for bindx in range(bs): for pindx in range(2 * num_points): point = points[bindx, pindx, :] group_id = int(point[2]) if group_id < 0: continue is_negative = int(pindx >= num_points) if group_id >= num_groups or ( group_id == 0 and is_negative ): # disable negative first click group_id = num_groups - 1 new_point_indx = last_point_indx_group[bindx, group_id, is_negative] last_point_indx_group[bindx, group_id, is_negative] += 1 group_points[group_id][bindx, new_point_indx, :] = point group_points = [paddle.to_tensor(x, dtype=tpoints.dtype) for x in group_points] return group_points ================================================ FILE: eiseg/inference/predictor/ops.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import paddle import paddle.nn as nn import numpy as np class DistMaps(nn.Layer): def __init__(self, norm_radius, spatial_scale=1.0, cpu_mode=True, use_disks=False): super(DistMaps, self).__init__() self.spatial_scale = spatial_scale self.norm_radius = norm_radius self.cpu_mode = cpu_mode self.use_disks = use_disks if self.cpu_mode: from util.cython import get_dist_maps self._get_dist_maps = get_dist_maps def get_coord_features(self, points, batchsize, rows, cols): if self.cpu_mode: coords = [] for i in range(batchsize): norm_delimeter = ( 1.0 if self.use_disks else self.spatial_scale * self.norm_radius ) coords.append( self._get_dist_maps( points[i].numpy().astype("float32"), rows, cols, norm_delimeter ) ) coords = paddle.to_tensor(np.stack(coords, axis=0)).astype("float32") else: num_points = points.shape[1] // 2 points = points.reshape([-1, points.shape[2]]) points, points_order = paddle.split(points, [2, 1], axis=1) invalid_points = paddle.max(points, axis=1, keepdim=False) < 0 row_array = paddle.arange(start=0, end=rows, step=1, dtype="float32") col_array = paddle.arange(start=0, end=cols, step=1, dtype="float32") coord_rows, coord_cols = paddle.meshgrid(row_array, col_array) coords = paddle.unsqueeze( paddle.stack([coord_rows, coord_cols], axis=0), axis=0 ).tile([points.shape[0], 1, 1, 1]) add_xy = (points * self.spatial_scale).reshape( [points.shape[0], points.shape[1], 1, 1] ) coords = coords - add_xy if not self.use_disks: coords = coords / (self.norm_radius * self.spatial_scale) coords = coords * coords coords[:, 0] += coords[:, 1] coords = coords[:, :1] invalid_points = invalid_points.numpy() coords[invalid_points, :, :, :] = 1e6 coords = coords.reshape([-1, num_points, 1, rows, cols]) coords = paddle.min(coords, axis=1) coords = coords.reshape([-1, 2, rows, cols]) if self.use_disks: coords = (coords <= (self.norm_radius * self.spatial_scale) ** 2).astype( "float32" ) else: coords = paddle.tanh(paddle.sqrt(coords) * 2) return coords def forward(self, x, coords): return self.get_coord_features(coords, x.shape[0], x.shape[2], x.shape[3]) class ScaleLayer(nn.Layer): def __init__(self, init_value=1.0, lr_mult=1): super().__init__() self.lr_mult = lr_mult self.scale = self.create_parameter( shape=[1], dtype="float32", default_initializer=nn.initializer.Constant(init_value / lr_mult), ) def forward(self, x): scale = paddle.abs(self.scale * self.lr_mult) return x * scale class BatchImageNormalize: def __init__(self, mean, std): self.mean = paddle.to_tensor( np.array(mean)[np.newaxis, :, np.newaxis, np.newaxis] ).astype("float32") self.std = paddle.to_tensor( np.array(std)[np.newaxis, :, np.newaxis, np.newaxis] ).astype("float32") def __call__(self, tensor): tensor = (tensor - self.mean) / self.std return tensor ================================================ FILE: eiseg/inference/transforms/__init__.py ================================================ # Copyright (c) 2021 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. from .base import SigmoidForPred from .flip import AddHorizontalFlip from .zoom_in import ZoomIn from .limit_longest_side import LimitLongestSide from .crops import Crops ================================================ FILE: eiseg/inference/transforms/base.py ================================================ import paddle.nn.functional as F class BaseTransform(object): def __init__(self): self.image_changed = False def transform(self, image_nd, clicks_lists): raise NotImplementedError def inv_transform(self, prob_map): raise NotImplementedError def reset(self): raise NotImplementedError def get_state(self): raise NotImplementedError def set_state(self, state): raise NotImplementedError class SigmoidForPred(BaseTransform): def transform(self, image_nd, clicks_lists): return image_nd, clicks_lists def inv_transform(self, prob_map): return F.sigmoid(prob_map) def reset(self): pass def get_state(self): return None def set_state(self, state): pass ================================================ FILE: eiseg/inference/transforms/crops.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import math import paddle import numpy as np from inference.clicker import Click from .base import BaseTransform class Crops(BaseTransform): def __init__(self, crop_size=(320, 480), min_overlap=0.2): super().__init__() self.crop_height, self.crop_width = crop_size self.min_overlap = min_overlap self.x_offsets = None self.y_offsets = None self._counts = None def transform(self, image_nd, clicks_lists): assert image_nd.shape[0] == 1 and len(clicks_lists) == 1 image_height, image_width = image_nd.shape[2:4] self._counts = None if image_height < self.crop_height or image_width < self.crop_width: return image_nd, clicks_lists self.x_offsets = get_offsets(image_width, self.crop_width, self.min_overlap) self.y_offsets = get_offsets(image_height, self.crop_height, self.min_overlap) self._counts = np.zeros((image_height, image_width)) image_crops = [] for dy in self.y_offsets: for dx in self.x_offsets: self._counts[dy : dy + self.crop_height, dx : dx + self.crop_width] += 1 image_crop = image_nd[ :, :, dy : dy + self.crop_height, dx : dx + self.crop_width ] image_crops.append(image_crop) image_crops = paddle.concat(image_crops, axis=0) self._counts = paddle.to_tensor(self._counts, dtype="float32") clicks_list = clicks_lists[0] clicks_lists = [] for dy in self.y_offsets: for dx in self.x_offsets: crop_clicks = [ x.copy(coords=(x.coords[0] - dy, x.coords[1] - dx)) for x in clicks_list ] clicks_lists.append(crop_clicks) return image_crops, clicks_lists def inv_transform(self, prob_map): if self._counts is None: return prob_map new_prob_map = paddle.zeros((1, 1, *self._counts.shape), dtype=prob_map.dtype) crop_indx = 0 for dy in self.y_offsets: for dx in self.x_offsets: new_prob_map[ 0, 0, dy : dy + self.crop_height, dx : dx + self.crop_width ] += prob_map[crop_indx, 0] crop_indx += 1 new_prob_map = paddle.divide(new_prob_map, self._counts) return new_prob_map def get_state(self): return self.x_offsets, self.y_offsets, self._counts def set_state(self, state): self.x_offsets, self.y_offsets, self._counts = state def reset(self): self.x_offsets = None self.y_offsets = None self._counts = None def get_offsets(length, crop_size, min_overlap_ratio=0.2): if length == crop_size: return [0] N = (length / crop_size - min_overlap_ratio) / (1 - min_overlap_ratio) N = math.ceil(N) overlap_ratio = (N - length / crop_size) / (N - 1) overlap_width = int(crop_size * overlap_ratio) offsets = [0] for i in range(1, N): new_offset = offsets[-1] + crop_size - overlap_width if new_offset + crop_size > length: new_offset = length - crop_size offsets.append(new_offset) return offsets ================================================ FILE: eiseg/inference/transforms/flip.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import paddle from inference.clicker import Click from .base import BaseTransform class AddHorizontalFlip(BaseTransform): def transform(self, image_nd, clicks_lists): assert len(image_nd.shape) == 4 image_nd = paddle.concat([image_nd, paddle.flip(image_nd, axis=[3])], axis=0) image_width = image_nd.shape[3] clicks_lists_flipped = [] for clicks_list in clicks_lists: clicks_list_flipped = [ click.copy(coords=(click.coords[0], image_width - click.coords[1] - 1)) for click in clicks_list ] clicks_lists_flipped.append(clicks_list_flipped) clicks_lists = clicks_lists + clicks_lists_flipped return image_nd, clicks_lists def inv_transform(self, prob_map): assert len(prob_map.shape) == 4 and prob_map.shape[0] % 2 == 0 num_maps = prob_map.shape[0] // 2 prob_map, prob_map_flipped = prob_map[:num_maps], prob_map[num_maps:] return 0.5 * (prob_map + paddle.flip(prob_map_flipped, axis=[3])) def get_state(self): return None def set_state(self, state): pass def reset(self): pass ================================================ FILE: eiseg/inference/transforms/limit_longest_side.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ from .zoom_in import ZoomIn, get_roi_image_nd class LimitLongestSide(ZoomIn): def __init__(self, max_size=800): super().__init__(target_size=max_size, skip_clicks=0) def transform(self, image_nd, clicks_lists): assert image_nd.shape[0] == 1 and len(clicks_lists) == 1 image_max_size = max(image_nd.shape[2:4]) self.image_changed = False if image_max_size <= self.target_size: return image_nd, clicks_lists self._input_image = image_nd self._object_roi = (0, image_nd.shape[2] - 1, 0, image_nd.shape[3] - 1) self._roi_image = get_roi_image_nd(image_nd, self._object_roi, self.target_size) self.image_changed = True tclicks_lists = [self._transform_clicks(clicks_lists[0])] return self._roi_image, tclicks_lists ================================================ FILE: eiseg/inference/transforms/zoom_in.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/saic-vul/ritm_interactive_segmentation Ths copyright of saic-vul/ritm_interactive_segmentation is as follows: MIT License [see LICENSE for details] """ import paddle import numpy as np from inference.clicker import Click from util.misc import get_bbox_iou, get_bbox_from_mask, expand_bbox, clamp_bbox from .base import BaseTransform class ZoomIn(BaseTransform): def __init__( self, target_size=700, skip_clicks=1, expansion_ratio=1.4, min_crop_size=480, recompute_thresh_iou=0.5, prob_thresh=0.50, ): super().__init__() self.target_size = target_size self.min_crop_size = min_crop_size self.skip_clicks = skip_clicks self.expansion_ratio = expansion_ratio self.recompute_thresh_iou = recompute_thresh_iou self.prob_thresh = prob_thresh self._input_image_shape = None self._prev_probs = None self._object_roi = None self._roi_image = None def transform(self, image_nd, clicks_lists): assert image_nd.shape[0] == 1 and len(clicks_lists) == 1 self.image_changed = False clicks_list = clicks_lists[0] if len(clicks_list) <= self.skip_clicks: return image_nd, clicks_lists self._input_image_shape = image_nd.shape current_object_roi = None if self._prev_probs is not None: current_pred_mask = (self._prev_probs > self.prob_thresh)[0, 0] if current_pred_mask.sum() > 0: current_object_roi = get_object_roi( current_pred_mask, clicks_list, self.expansion_ratio, self.min_crop_size, ) if current_object_roi is None: if self.skip_clicks >= 0: return image_nd, clicks_lists else: current_object_roi = 0, image_nd.shape[2] - 1, 0, image_nd.shape[3] - 1 update_object_roi = False if self._object_roi is None: update_object_roi = True elif not check_object_roi(self._object_roi, clicks_list): update_object_roi = True elif ( get_bbox_iou(current_object_roi, self._object_roi) < self.recompute_thresh_iou ): update_object_roi = True if update_object_roi: self._object_roi = current_object_roi self.image_changed = True self._roi_image = get_roi_image_nd(image_nd, self._object_roi, self.target_size) tclicks_lists = [self._transform_clicks(clicks_list)] return self._roi_image, tclicks_lists def inv_transform(self, prob_map): if self._object_roi is None: self._prev_probs = prob_map.numpy() return prob_map assert prob_map.shape[0] == 1 rmin, rmax, cmin, cmax = self._object_roi prob_map = paddle.nn.functional.interpolate( prob_map, size=(rmax - rmin + 1, cmax - cmin + 1), mode="bilinear", align_corners=True, ) if self._prev_probs is not None: new_prob_map = paddle.zeros( shape=self._prev_probs.shape, dtype=prob_map.dtype ) new_prob_map[:, :, rmin : rmax + 1, cmin : cmax + 1] = prob_map else: new_prob_map = prob_map self._prev_probs = new_prob_map.numpy() return new_prob_map def check_possible_recalculation(self): if ( self._prev_probs is None or self._object_roi is not None or self.skip_clicks > 0 ): return False pred_mask = (self._prev_probs > self.prob_thresh)[0, 0] if pred_mask.sum() > 0: possible_object_roi = get_object_roi( pred_mask, [], self.expansion_ratio, self.min_crop_size ) image_roi = ( 0, self._input_image_shape[2] - 1, 0, self._input_image_shape[3] - 1, ) if get_bbox_iou(possible_object_roi, image_roi) < 0.50: return True return False def get_state(self): roi_image = self._roi_image if self._roi_image is not None else None return ( self._input_image_shape, self._object_roi, self._prev_probs, roi_image, self.image_changed, ) def set_state(self, state): ( self._input_image_shape, self._object_roi, self._prev_probs, self._roi_image, self.image_changed, ) = state def reset(self): self._input_image_shape = None self._object_roi = None self._prev_probs = None self._roi_image = None self.image_changed = False def _transform_clicks(self, clicks_list): if self._object_roi is None: return clicks_list rmin, rmax, cmin, cmax = self._object_roi crop_height, crop_width = self._roi_image.shape[2:] transformed_clicks = [] for click in clicks_list: new_r = crop_height * (click.coords[0] - rmin) / (rmax - rmin + 1) new_c = crop_width * (click.coords[1] - cmin) / (cmax - cmin + 1) transformed_clicks.append(click.copy(coords=(new_r, new_c))) return transformed_clicks def get_object_roi(pred_mask, clicks_list, expansion_ratio, min_crop_size): pred_mask = pred_mask.copy() for click in clicks_list: if click.is_positive: pred_mask[int(click.coords[0]), int(click.coords[1])] = 1 bbox = get_bbox_from_mask(pred_mask) bbox = expand_bbox(bbox, expansion_ratio, min_crop_size) h, w = pred_mask.shape[0], pred_mask.shape[1] bbox = clamp_bbox(bbox, 0, h - 1, 0, w - 1) return bbox def get_roi_image_nd(image_nd, object_roi, target_size): rmin, rmax, cmin, cmax = object_roi height = rmax - rmin + 1 width = cmax - cmin + 1 if isinstance(target_size, tuple): new_height, new_width = target_size else: scale = target_size / max(height, width) new_height = int(round(height * scale)) new_width = int(round(width * scale)) with paddle.no_grad(): roi_image_nd = image_nd[:, :, rmin : rmax + 1, cmin : cmax + 1] roi_image_nd = paddle.nn.functional.interpolate( roi_image_nd, size=(new_height, new_width), mode="bilinear", align_corners=True, ) return roi_image_nd def check_object_roi(object_roi, clicks_list): for click in clicks_list: if click.is_positive: if click.coords[0] < object_roi[0] or click.coords[0] >= object_roi[1]: return False if click.coords[1] < object_roi[2] or click.coords[1] >= object_roi[3]: return False return True ================================================ FILE: eiseg/models.py ================================================ # Copyright (c) 2021 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.path as osp from abc import abstractmethod import paddle.inference as paddle_infer here = osp.dirname(osp.abspath(__file__)) class EISegModel: @abstractmethod def __init__(self, model_path, param_path, use_gpu=False): model_path, param_path = self.check_param(model_path, param_path) try: config = paddle_infer.Config(model_path, param_path) except: ValueError(" 模型和参数不匹配,请检查模型和参数是否加载错误") if not use_gpu: config.enable_mkldnn() # TODO: fluid要废弃了,研究判断方式 # if paddle.fluid.core.supports_bfloat16(): # config.enable_mkldnn_bfloat16() config.switch_ir_optim(True) config.set_cpu_math_library_num_threads(10) else: config.enable_use_gpu(500, 0) config.delete_pass("conv_elementwise_add_act_fuse_pass") config.delete_pass("conv_elementwise_add2_act_fuse_pass") config.delete_pass("conv_elementwise_add_fuse_pass") config.switch_ir_optim() config.enable_memory_optim() # use_tensoret = False # TODO: 目前Linux和windows下使用TensorRT报错 # if use_tensoret: # config.enable_tensorrt_engine( # workspace_size=1 << 30, # precision_mode=paddle_infer.PrecisionType.Float32, # max_batch_size=1, # min_subgraph_size=5, # use_static=False, # use_calib_mode=False, # ) self.model = paddle_infer.create_predictor(config) def check_param(self, model_path, param_path): if model_path is None or not osp.exists(model_path): raise Exception(f"模型路径{model_path}不存在。请指定正确的模型路径") if param_path is None or not osp.exists(param_path): raise Exception(f"权重路径{param_path}不存在。请指定正确的权重路径") return model_path, param_path ================================================ FILE: eiseg/plugin/__init__.py ================================================ # Copyright (c) 2021 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. ================================================ FILE: eiseg/plugin/medical/__init__.py ================================================ # Copyright (c) 2021 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. from .med import has_sitk, dcm_reader, windowlize ================================================ FILE: eiseg/plugin/medical/med.py ================================================ # Copyright (c) 2021 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 numpy as np import cv2 from eiseg import logger def has_sitk(): try: import SimpleITK return True except ImportError: return False if has_sitk(): import SimpleITK as sitk def dcm_reader(path): logger.debug(f"opening medical image {path}") reader = sitk.ImageSeriesReader() reader.SetFileNames([path]) image = reader.Execute() img = sitk.GetArrayFromImage(image) logger.debug(f"scan shape is {img.shape}") if len(img.shape) == 4: img = img[0] # WHC img = np.transpose(img, [1, 2, 0]) return img.astype(np.int32) def windowlize(scan, ww, wc): wl = wc - ww / 2 wh = wc + ww / 2 res = scan.copy() res = res.astype(np.float32) res = np.clip(res, wl, wh) res = (res - wl) / ww * 255 res = res.astype(np.uint8) # print("++", res.shape) # for idx in range(res.shape[-1]): # TODO: 支持3d或者改调用 res = cv2.cvtColor(res, cv2.COLOR_GRAY2BGR) return res # def open_nii(niiimg_path): # if IPT_SITK == True: # sitk_image = sitk.ReadImage(niiimg_path) # return _nii2arr(sitk_image) # else: # raise ImportError("can't import SimpleITK!") # # def _nii2arr(sitk_image): # if IPT_SITK == True: # img = sitk.GetArrayFromImage(sitk_image).transpose((1, 2, 0)) # return img # else: # raise ImportError("can't import SimpleITK!") # # # def slice_img(img, index): # if index == 0: # return sample_norm( # cv2.merge( # [ # np.uint16(img[:, :, index]), # np.uint16(img[:, :, index]), # np.uint16(img[:, :, index + 1]), # ] # ) # ) # elif index == img.shape[2] - 1: # return sample_norm( # cv2.merge( # [ # np.uint16(img[:, :, index - 1]), # np.uint16(img[:, :, index]), # np.uint16(img[:, :, index]), # ] # ) # ) # else: # return sample_norm( # cv2.merge( # [ # np.uint16(img[:, :, index - 1]), # np.uint16(img[:, :, index]), # np.uint16(img[:, :, index + 1]), # ] # ) # ) ================================================ FILE: eiseg/plugin/n2grid/__init__.py ================================================ # Copyright (c) 2021 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. from .rs_grid import RSGrids from .grid import Grids, checkOpenGrid ================================================ FILE: eiseg/plugin/n2grid/grid.py ================================================ # Copyright (c) 2021 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 math import numpy as np from PIL import Image def checkOpenGrid(img, thumbnail_min): H, W = img.shape[:2] if max(H, W) <= thumbnail_min: return False else: return True class Grids: def __init__(self, img, gridSize=(512, 512), overlap=(24, 24)): self.clear() self.detimg = img self.gridSize = np.array(gridSize) self.overlap = np.array(overlap) def clear(self): # 图像HWC格式 self.detimg = None # 宫格初始图像 self.grid_init = False # 是否初始化了宫格 # self.imagesGrid = [] # 图像宫格 self.mask_grids = [] # 标签宫格 self.grid_count = None # (row count, col count) self.curr_idx = None # (current row, current col) def createGrids(self): # 计算宫格横纵向格数 imgSize = np.array(self.detimg.shape[:2]) grid_count = np.ceil((imgSize + self.overlap) / self.gridSize) self.grid_count = grid_count = grid_count.astype("uint16") # ul = self.overlap - self.gridSize # for row in range(grid_count[0]): # ul[0] = ul[0] + self.gridSize[0] - self.overlap[0] # for col in range(grid_count[1]): # ul[1] = ul[1] + self.gridSize[1] - self.overlap[1] # lr = ul + self.gridSize # # print("ul, lr", ul, lr) # # 扩充 # det_tmp = self.detimg[ul[0]: lr[0], ul[1]: lr[1]] # tmp = np.zeros((self.gridSize[0], self.gridSize[1], self.detimg.shape[-1])) # tmp[:det_tmp.shape[0], :det_tmp.shape[1], :] = det_tmp # self.imagesGrid.append(tmp) # self.mask_grids = [[np.zeros(self.gridSize)] * grid_count[1]] * grid_count[0] # 不能用浅拷贝 self.mask_grids = [ [np.zeros(self.gridSize) for _ in range(grid_count[1])] for _ in range(grid_count[0]) ] # print(len(self.mask_grids), len(self.mask_grids[0])) self.grid_init = True return list(grid_count) def getGrid(self, row, col): gridIdx = np.array([row, col]) ul = gridIdx * (self.gridSize - self.overlap) lr = ul + self.gridSize # print("ul, lr", ul, lr) img = self.detimg[ul[0]:lr[0], ul[1]:lr[1]] mask = self.mask_grids[row][col] self.curr_idx = (row, col) return img, mask def splicingList(self, save_path): """ 将slide的out进行拼接,raw_size保证恢复到原状 """ imgs = self.mask_grids # print(len(imgs), len(imgs[0])) raw_size = self.detimg.shape[:2] # h, w = None, None # for i in range(len(imgs)): # for j in range(len(imgs[i])): # im = imgs[i][j] # if im is not None: # h, w = im.shape[:2] # break # if h is None and w is None: # return False h, w = self.gridSize row = math.ceil(raw_size[0] / h) col = math.ceil(raw_size[1] / w) # print('row, col:', row, col) result_1 = np.zeros((h * row, w * col), dtype=np.uint8) result_2 = result_1.copy() # k = 0 for i in range(row): for j in range(col): # print('h, w:', h, w) ih, iw = imgs[i][j].shape[:2] im = np.zeros(self.gridSize) im[:ih, :iw] = imgs[i][j] start_h = (i * h) if i == 0 else (i * (h - self.overlap[0])) end_h = start_h + h start_w = (j * w) if j == 0 else (j * (w - self.overlap[1])) end_w = start_w + w # print("se: ", start_h, end_h, start_w, end_w) # 单区自己,重叠取或 if (i + j) % 2 == 0: result_1[start_h:end_h, start_w:end_w] = im else: result_2[start_h:end_h, start_w:end_w] = im # k += 1 # print('r, c, k:', i_r, i_c, k) result = np.where(result_2 != 0, result_2, result_1) result = result[:raw_size[0], :raw_size[1]] Image.fromarray(result).save(save_path, "PNG") return result ================================================ FILE: eiseg/plugin/n2grid/rs_grid.py ================================================ # Copyright (c) 2021 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 numpy as np from typing import List, Tuple from eiseg.plugin.remotesensing.raster import Raster class RSGrids: def __init__(self, raset: Raster) -> None: """ 在EISeg中用于处理遥感栅格数据的宫格类. 参数: tif_path (str): GTiff数据的路径. show_band (Union[List[int], Tuple[int]], optional): 用于RGB合成显示的波段. 默认为 [1, 1, 1]. grid_size (Union[List[int], Tuple[int]], optional): 切片大小. 默认为 [512, 512]. overlap (Union[List[int], Tuple[int]], optional): 重叠区域的大小. 默认为 [24, 24]. """ super(RSGrids, self).__init__() self.raster = raset self.clear() def clear(self) -> None: self.mask_grids = [] # 标签宫格 self.grid_count = None # (row count, col count) self.curr_idx = None # (current row, current col) def createGrids(self) -> List[int]: img_size = (self.raster.geoinfo.ysize, self.raster.geoinfo.xsize) grid_count = np.ceil((img_size + self.raster.overlap) / self.raster.grid_size) self.grid_count = grid_count = grid_count.astype("uint16") self.mask_grids = [[np.zeros(self.raster.grid_size) \ for _ in range(grid_count[1])] for _ in range(grid_count[0])] return list(grid_count) def getGrid(self, row: int, col: int) -> Tuple[np.ndarray]: img, _ = self.raster.getGrid(row, col) mask = self.mask_grids[row][col] self.curr_idx = (row, col) return img, mask def splicingList(self, save_path: str) -> np.ndarray: mask = self.raster.saveMaskbyGrids(self.mask_grids, save_path, self.raster.geoinfo) return mask ================================================ FILE: eiseg/plugin/remotesensing/__init__.py ================================================ # Copyright (c) 2021 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. from .imgtools import * from .shape import * from .raster import * ================================================ FILE: eiseg/plugin/remotesensing/imgtools.py ================================================ # Copyright (c) 2021 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 numpy as np import cv2 from skimage import exposure # 2%线性拉伸 def two_percentLinear(image: np.ndarray, max_out: int=255, min_out: int=0) -> np.ndarray: b, g, r = cv2.split(image) def __gray_process(gray, maxout=max_out, minout=min_out): high_value = np.percentile(gray, 98) # 取得98%直方图处对应灰度 low_value = np.percentile(gray, 2) truncated_gray = np.clip(gray, a_min=low_value, a_max=high_value) processed_gray = ((truncated_gray - low_value) / (high_value - low_value)) * ( maxout - minout) return processed_gray r_p = __gray_process(r) g_p = __gray_process(g) b_p = __gray_process(b) result = cv2.merge((b_p, g_p, r_p)) return np.uint8(result) # 简单图像标准化 def sample_norm(image: np.ndarray) -> np.ndarray: stretches = [] if len(image.shape) == 3: for b in range(image.shape[-1]): stretched = exposure.equalize_hist(image[:, :, b]) stretched /= float(np.max(stretched)) stretches.append(stretched) stretched_img = np.stack(stretches, axis=2) else: # if len(image.shape) == 2 stretched_img = exposure.equalize_hist(image) return np.uint8(stretched_img * 255) # 计算缩略图 def get_thumbnail(image: np.ndarray, range: int=2000, max_size: int=1000) -> np.ndarray: h, w = image.shape[:2] if h >= range or w >= range: if h >= w: image = cv2.resize(image, (int(max_size / h * w), max_size)) else: image = cv2.resize(image, (max_size, int(max_size / w * h))) return image ================================================ FILE: eiseg/plugin/remotesensing/raster.py ================================================ # Copyright (c) 2021 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.path as osp import numpy as np import cv2 import math from typing import List, Dict, Tuple, Union from collections import defaultdict from easydict import EasyDict as edict from .imgtools import sample_norm, two_percentLinear, get_thumbnail def check_rasterio() -> bool: try: import rasterio return True except: return False IMPORT_STATE = False if check_rasterio(): import rasterio from rasterio.windows import Window IMPORT_STATE = True class Raster: def __init__(self, tif_path: str, show_band: Union[List[int], Tuple[int]]=[1, 1, 1], open_grid: bool=False, grid_size: Union[List[int], Tuple[int]]=[512, 512], overlap: Union[List[int], Tuple[int]]=[24, 24]) -> None: """ 在EISeg中用于处理遥感栅格数据的类. 参数: tif_path (str): GTiff数据的路径. show_band (Union[List[int], Tuple[int]], optional): 用于RGB合成显示的波段. 默认为 [1, 1, 1]. open_grid (bool, optional): 是否打开了宫格切片功能. 默认为 False. grid_size (Union[List[int], Tuple[int]], optional): 切片大小. 默认为 [512, 512]. overlap (Union[List[int], Tuple[int]], optional): 重叠区域的大小. 默认为 [24, 24]. """ super(Raster, self).__init__() if IMPORT_STATE is False: raise("Can't import rasterio!") if osp.exists(tif_path): self.src_data = rasterio.open(tif_path) self.geoinfo = self.__getRasterInfo() self.show_band = list(show_band) self.grid_size = np.array(grid_size) self.overlap = np.array(overlap) self.open_grid = open_grid else: raise("{0} not exists!".format(tif_path)) self.thumbnail_min = 2000 def __del__(self) -> None: self.src_data.close() def __getRasterInfo(self) -> Dict: meta = self.src_data.meta geoinfo = edict() geoinfo.count = meta["count"] geoinfo.dtype = meta["dtype"] geoinfo.xsize = meta["width"] geoinfo.ysize = meta["height"] geoinfo.geotf = meta["transform"] geoinfo.crs = meta["crs"] if geoinfo.crs is not None: geoinfo.crs_wkt = geoinfo.crs.wkt else: geoinfo.crs_wkt = None return geoinfo def checkOpenGrid(self, thumbnail_min: Union[int, None]) -> bool: if isinstance(thumbnail_min, int): self.thumbnail_min = thumbnail_min if max(self.geoinfo.xsize, self.geoinfo.ysize) <= self.thumbnail_min: self.open_grid = False else: self.open_grid = True return self.open_grid def setBand(self, bands: Union[List[int], Tuple[int]]) -> None: self.show_band = list(bands) # def __analysis_proj4(self) -> str: # proj4 = self.geoinfo.crs.wkt # TODO: 解析为proj4 # ap_dict = defaultdict(str) # dinf = proj4.split("+") # for df in dinf: # kv = df.strip().split("=") # if len(kv) == 2: # k, v = kv # ap_dict[k] = v # return str("● 投影:{0}\n● 基准:{1}\n● 单位:{2}".format( # ap_dict["proj"], ap_dict["datum"], ap_dict["units"]) # ) def showGeoInfo(self) -> str: # return str("● 波段数:{0}\n● 数据类型:{1}\n● 行数:{2}\n● 列数:{3}\n{4}".format( # self.geoinfo.count, self.geoinfo.dtype, self.geoinfo.xsize, # self.geoinfo.ysize, self.__analysis_proj4()) # ) if self.geoinfo.crs is not None: crs = str(self.geoinfo.crs.to_string().split(":")[-1]) else: crs = "None" return (str(self.geoinfo.count), str(self.geoinfo.dtype), str(self.geoinfo.xsize), str(self.geoinfo.ysize), crs) def getArray(self) -> Tuple[np.ndarray]: rgb = [] if not self.open_grid: for b in self.show_band: rgb.append(self.src_data.read(b)) geotf = self.geoinfo.geotf else: for b in self.show_band: rgb.append(get_thumbnail(self.src_data.read(b), self.thumbnail_min)) geotf = None ima = np.stack(rgb, axis=2) # cv2.merge(rgb) if self.geoinfo["dtype"] != "uint8": ima = sample_norm(ima) return two_percentLinear(ima), geotf def getGrid(self, row: int, col: int) -> Tuple[np.ndarray]: if self.open_grid is False: return self.getArray() grid_idx = np.array([row, col]) ul = grid_idx * (self.grid_size - self.overlap) lr = ul + self.grid_size # print("ul, lr", ul, lr) window = Window(ul[1], ul[0], (lr[1] - ul[1]), (lr[0] - ul[0])) rgb = [] for b in self.show_band: rgb.append(self.src_data.read(b, window=window)) win_tf = self.src_data.window_transform(window) ima = cv2.merge([np.uint16(c) for c in rgb]) if self.geoinfo["dtype"] == "uint32": ima = sample_norm(ima) return two_percentLinear(ima), win_tf def saveMask(self, img: np.array, save_path: str, geoinfo: Union[Dict, None]=None, count: int=1) -> None: if geoinfo is None: geoinfo = self.geoinfo new_meta = self.src_data.meta.copy() new_meta.update({ "driver": "GTiff", "width": geoinfo.xsize, "height": geoinfo.ysize, "count": count, "dtype": geoinfo.dtype, "crs": geoinfo.crs, "transform": geoinfo.geotf[:6], "nodata": 0 }) img = np.nan_to_num(img).astype("int16") with rasterio.open(save_path, "w", **new_meta) as tf: if count == 1: tf.write(img, indexes=1) else: for i in range(count): tf.write(img[:, :, i], indexes=(i + 1)) def saveMaskbyGrids(self, img_list: List[List[np.ndarray]], save_path: Union[str, None]=None, geoinfo: Union[Dict, None]=None) -> np.ndarray: if geoinfo is None: geoinfo = self.geoinfo raw_size = (geoinfo.ysize, geoinfo.xsize) h, w = self.grid_size row = math.ceil(raw_size[0] / h) col = math.ceil(raw_size[1] / w) # print("row, col:", row, col) result_1 = np.zeros((h * row, w * col), dtype=np.uint8) result_2 = result_1.copy() for i in range(row): for j in range(col): # print("h, w:", h, w) ih, iw = img_list[i][j].shape[:2] im = np.zeros(self.grid_size) im[:ih, :iw] = img_list[i][j] start_h = (i * h) if i == 0 else (i * (h - self.overlap[0])) end_h = start_h + h start_w = (j * w) if j == 0 else (j * (w - self.overlap[1])) end_w = start_w + w # print("se: ", start_h, end_h, start_w, end_w) # 单区自己,重叠取或 if (i + j) % 2 == 0: result_1[start_h: end_h, start_w: end_w] = im else: result_2[start_h: end_h, start_w: end_w] = im # print("r, c, k:", i_r, i_c, k) result = np.where(result_2 != 0, result_2, result_1) result = result[:raw_size[0], :raw_size[1]] if save_path is not None: self.saveMask(result, save_path, geoinfo) return result ================================================ FILE: eiseg/plugin/remotesensing/shape.py ================================================ # Copyright (c) 2021 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 os.path as osp def check_gdal() -> bool: try: import gdal except: try: from osgeo import gdal except ImportError: return False return True IMPORT_STATE = False if check_gdal(): try: import gdal import osr import ogr except: from osgeo import gdal, osr, ogr IMPORT_STATE = True # 保存shp文件 def save_shp(shp_path: str, tif_path: str, ignore_index :int=0) -> str: if IMPORT_STATE == True: ds = gdal.Open(tif_path) srcband = ds.GetRasterBand(1) maskband = srcband.GetMaskBand() gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8", "YES") gdal.SetConfigOption("SHAPE_ENCODING", "UTF-8") ogr.RegisterAll() drv = ogr.GetDriverByName("ESRI Shapefile") if osp.exists(shp_path): os.remove(shp_path) dst_ds = drv.CreateDataSource(shp_path) prosrs = osr.SpatialReference(wkt=ds.GetProjection()) dst_layer = dst_ds.CreateLayer( "segmentation", geom_type=ogr.wkbPolygon, srs=prosrs) dst_fieldname = "DN" fd = ogr.FieldDefn(dst_fieldname, ogr.OFTInteger) dst_layer.CreateField(fd) gdal.Polygonize(srcband, maskband, dst_layer, 0, []) lyr = dst_ds.GetLayer() lyr.SetAttributeFilter("DN = '{}'".format(str(ignore_index))) for holes in lyr: lyr.DeleteFeature(holes.GetFID()) dst_ds.Destroy() ds = None return "Dataset creation successfully!" else: raise ImportError("can't import gdal, osr, ogr!") ================================================ FILE: eiseg/run.py ================================================ # Copyright (c) 2021 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 sys import os import os.path as osp import logging from datetime import datetime from qtpy.QtWidgets import QApplication # 导入PyQt相关模块 from qtpy import QtCore from eiseg import pjpath from app import APP_EISeg # 导入带槽的界面 def main(): ## -- log -- settings = QtCore.QSettings( osp.join(pjpath, "config/setting.ini"), QtCore.QSettings.IniFormat ) # # logFolder = settings.value("logFolder") # logLevel = settings.value("logLevel") # logDays = settings.value("logDays") # # if logFolder is None or len(logFolder) == 0: # logFolder = osp.normcase(osp.join(pjpath, "log")) # if not osp.exists(logFolder): # os.makedirs(logFolder) # # if logLevel: # logLevel = eval(logLevel) # else: # logLevel = logging.DEBUG # if logDays: # logDays = int(logDays) # else: # logDays = 7 # # TODO: 删除大于logDays 的 log # # t = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") # logging.basicConfig( # level=logging.DEBUG, # filename=osp.normcase(osp.join(logFolder, f"eiseg-{t}.log")), # format="%(levelname)s - %(asctime)s - %(filename)s - %(funcName)s - %(message)s", # ) # logger = logging.getLogger("EISeg Logger") # handler = logging.FileHandler(osp.normcase(osp.join(logFolder, f"eiseg-{t}.log"))) # # handler.setFormatter( # logging.Formatter( # "%(levelname)s - %(asctime)s - %(filename)s - %(funcName)s - %(message)s" # ) # ) # logger.addHandler(handler) # logger.info("test info") # app = QApplication(sys.argv) lang = settings.value("language") if lang != "中文": trans = QtCore.QTranslator(app) trans.load(osp.join(pjpath, f"util/translate/{lang}")) app.installTranslator(trans) window = APP_EISeg() # 创建对象 window.currLanguage = lang window.showMaximized() # 全屏显示窗口 # 加载近期模型 QApplication.processEvents() window.loadRecentModelParam() sys.exit(app.exec()) ================================================ FILE: eiseg/ui.py ================================================ # Copyright (c) 2021 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.path as osp from functools import partial from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtGui import QIcon from qtpy.QtCore import Qt from eiseg import pjpath, __APPNAME__, __VERSION__, logger from eiseg.widget.create import creat_dock, create_button, create_slider, create_text from widget import AnnotationScene, AnnotationView from widget.create import * from widget.table import TableWidget # log = logging.getLogger(__name__ + ".ui") class Ui_EISeg(object): def __init__(self): super(Ui_EISeg, self).__init__() self.tr = partial(QtCore.QCoreApplication.translate, "APP_EISeg") def setupUi(self, MainWindow): ## -- 主窗体设置 -- MainWindow.setObjectName("MainWindow") MainWindow.setMinimumSize(QtCore.QSize(1200, 700)) # 1366x768的屏幕显示不全 MainWindow.setWindowTitle(__APPNAME__ + " " + __VERSION__) MainWindow.setWindowIcon(QIcon()) # TODO: 默认图标需要换一个吗,貌似不能不显示图标 CentralWidget = QtWidgets.QWidget(MainWindow) CentralWidget.setObjectName("CentralWidget") MainWindow.setCentralWidget(CentralWidget) ## ----- ## -- 工具栏 -- toolBar = QtWidgets.QToolBar(self) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(toolBar.sizePolicy().hasHeightForWidth()) toolBar.setSizePolicy(sizePolicy) toolBar.setMinimumSize(QtCore.QSize(0, 33)) toolBar.setMovable(True) toolBar.setAllowedAreas(QtCore.Qt.BottomToolBarArea | QtCore.Qt.TopToolBarArea) toolBar.setObjectName("toolBar") self.toolBar = toolBar MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) ## ----- ## -- 状态栏 -- self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") self.statusbar.setStyleSheet("QStatusBar::item {border: none;}") MainWindow.setStatusBar(self.statusbar) self.statusbar.addPermanentWidget( self.show_logo(osp.join(pjpath, "resource/Paddle.png")) ) ## ----- ## -- 图形区域 -- ImageRegion = QtWidgets.QHBoxLayout(CentralWidget) ImageRegion.setObjectName("ImageRegion") # 滑动区域 self.scrollArea = QtWidgets.QScrollArea(CentralWidget) self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") ImageRegion.addWidget(self.scrollArea) # 图形显示 self.scene = AnnotationScene() self.scene.addPixmap(QtGui.QPixmap()) self.canvas = AnnotationView(self.scene, self) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding ) self.canvas.setSizePolicy(sizePolicy) self.canvas.setAlignment(QtCore.Qt.AlignCenter) self.canvas.setAutoFillBackground(False) self.canvas.setStyleSheet("background-color: White") self.canvas.setObjectName("canvas") self.scrollArea.setWidget(self.canvas) ## ----- ## -- 工作区 -- p_create_dock = partial(self.creat_dock, MainWindow) p_create_button = partial(self.create_button, CentralWidget) # 模型加载 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) ModelRegion = QtWidgets.QVBoxLayout() ModelRegion.setObjectName("ModelRegion") # labShowSet = self.create_text(CentralWidget, "labShowSet", "模型选择") # ModelRegion.addWidget(labShowSet) # combo = QtWidgets.QComboBox(self) # # combo.addItems([self.tr(ModelsNick[m.__name__][0]) for m in MODELS]) # combo.addItems([self.tr(ModelsNick[m][0]) for m in ModelsNick.keys()]) # self.comboModelSelect = combo # ModelRegion.addWidget(self.comboModelSelect) # 网络参数 self.btnParamsSelect = p_create_button( "btnParamsLoad", self.tr("加载网络参数"), osp.join(pjpath, "resource/Model.png"), "Ctrl+D", ) ModelRegion.addWidget(self.btnParamsSelect) # 模型选择 self.cheWithMask = QtWidgets.QCheckBox(self) self.cheWithMask.setText(self.tr("使用掩膜")) self.cheWithMask.setChecked(True) ModelRegion.addWidget(self.cheWithMask) # with_mask horizontalLayout.addLayout(ModelRegion) self.ModelDock = p_create_dock("ModelDock", self.tr("模型选择"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.ModelDock) # 数据列表 # TODO: 数据列表加一个搜索功能 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) ListRegion = QtWidgets.QVBoxLayout() ListRegion.setObjectName("ListRegion") # labFiles = self.create_text(CentralWidget, "labFiles", "数据列表") # ListRegion.addWidget(labFiles) self.listFiles = QtWidgets.QListWidget(CentralWidget) self.listFiles.setObjectName("ListFiles") ListRegion.addWidget(self.listFiles) # ListRegion.addWidget(self.btnSave) horizontalLayout.addLayout(ListRegion) self.DataDock = p_create_dock("DataDock", self.tr("数据列表"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.DataDock) # 标签列表 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) LabelRegion = QtWidgets.QVBoxLayout() LabelRegion.setObjectName("LabelRegion") self.labelListTable = TableWidget( CentralWidget ) # QtWidgets.QTableWidget(CentralWidget) self.labelListTable.horizontalHeader().hide() # 铺满 self.labelListTable.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch ) self.labelListTable.verticalHeader().hide() self.labelListTable.setColumnWidth(0, 10) # self.labelListTable.setMinimumWidth() self.labelListTable.setObjectName("labelListTable") self.labelListTable.clearContents() self.labelListTable.setRowCount(0) self.labelListTable.setColumnCount(4) LabelRegion.addWidget(self.labelListTable) self.btnAddClass = p_create_button( "btnAddClass", self.tr("添加标签"), osp.join(pjpath, "resource/Label.png") ) LabelRegion.addWidget(self.btnAddClass) horizontalLayout.addLayout(LabelRegion) self.LabelDock = p_create_dock("LabelDock", self.tr("标签列表"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.LabelDock) ## 滑块设置 # 分割阈值 p_create_slider = partial(self.create_slider, CentralWidget) widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) ShowSetRegion = QtWidgets.QVBoxLayout() ShowSetRegion.setObjectName("ShowSetRegion") self.sldThresh, _, SegShowRegion = p_create_slider( "sldThresh", "labThresh", self.tr("分割阈值:") ) ShowSetRegion.addLayout(SegShowRegion) ShowSetRegion.addWidget(self.sldThresh) # 透明度 self.sldOpacity, _, MaskShowRegion = p_create_slider( "sldOpacity", "labOpacity", self.tr("标签透明度:"), 75 ) ShowSetRegion.addLayout(MaskShowRegion) ShowSetRegion.addWidget(self.sldOpacity) # 点大小 self.sldClickRadius, _, PointShowRegion = p_create_slider( "sldClickRadius", "labClickRadius", self.tr("点击可视化半径:"), 3, 10, 0, 1 ) ShowSetRegion.addLayout(PointShowRegion) ShowSetRegion.addWidget(self.sldClickRadius) # 保存 self.btnSave = p_create_button( "btnSave", self.tr("保存"), osp.join(pjpath, "resource/Save.png"), "Ctrl+S", ) ShowSetRegion.addWidget(self.btnSave) horizontalLayout.addLayout(ShowSetRegion) self.SegSettingDock = p_create_dock("SegSettingDock", self.tr("分割设置"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.SegSettingDock) ## 专业功能区工作区 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) bandRegion = QtWidgets.QVBoxLayout() bandRegion.setObjectName("bandRegion") bandSelection = create_text(CentralWidget, "bandSelection", self.tr("波段设置")) bandRegion.addWidget(bandSelection) text_list = ["R", "G", "B"] self.bandCombos = [] for txt in text_list: lab = create_text(CentralWidget, "band" + txt, txt) combo = QtWidgets.QComboBox() combo.addItems(["band_1"]) self.bandCombos.append(combo) hbandLayout = QtWidgets.QHBoxLayout() hbandLayout.setObjectName("hbandLayout") hbandLayout.addWidget(lab) hbandLayout.addWidget(combo) hbandLayout.setStretch(1, 4) bandRegion.addLayout(hbandLayout) resultSave = create_text(CentralWidget, "resultSave", self.tr("保存设置")) bandRegion.addWidget(resultSave) self.boundaryRegular = QtWidgets.QCheckBox(self.tr("建筑边界规范化")) self.boundaryRegular.setObjectName("boundaryRegular") bandRegion.addWidget(self.boundaryRegular) self.shpSave = QtWidgets.QCheckBox(self.tr("另存为shapefile")) self.shpSave.setObjectName("shpSave") bandRegion.addWidget(self.shpSave) horizontalLayout.addLayout(bandRegion) showGeoInfo = create_text(CentralWidget, "showGeoInfo", self.tr("地理信息")) bandRegion.addWidget(showGeoInfo) self.edtGeoinfo = QtWidgets.QTextEdit(self.tr("无")) self.edtGeoinfo.setObjectName("edtGeoinfo") self.edtGeoinfo.setReadOnly(True) bandRegion.addWidget(self.edtGeoinfo) self.RSDock = p_create_dock("RSDock", self.tr("遥感设置"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.RSDock) ## 医学影像设置 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) MIRegion = QtWidgets.QVBoxLayout() MIRegion.setObjectName("MIRegion") # mi_text = create_text(CentralWidget, "sliceSelection", self.tr("切片选择")) # MIRegion.addWidget(mi_text) # self.sldMISlide, _, slideRegion = p_create_slider( # "sldMISlide", "labMISlide", self.tr("切片选择:"), 1, 1, 1 # ) # self.sldMISlide.setMinimum(1) # MIRegion.addLayout(slideRegion) # MIRegion.addWidget(self.sldMISlide) self.sliderWw, self.textWw, WwRegion = p_create_slider( "sliderWw", "textWw", self.tr("窗宽:"), 200, 2048, -2048, 1, True ) MIRegion.addLayout(WwRegion) MIRegion.addWidget(self.sliderWw) self.sliderWc, self.textWc, WcRegion = p_create_slider( "sliderWc", "textWc", self.tr("窗位:"), 0, 2048, -2048, 1, True ) MIRegion.addLayout(WcRegion) MIRegion.addWidget(self.sliderWc) horizontalLayout.addLayout(MIRegion) self.MedDock = p_create_dock("MedDock", self.tr("医疗设置"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.MedDock) ## 宫格区域 widget = QtWidgets.QWidget() horizontalLayout = QtWidgets.QHBoxLayout(widget) GridRegion = QtWidgets.QVBoxLayout() GridRegion.setObjectName("GridRegion") # self.btnInitGrid = p_create_button( # "btnInitGrid", # self.tr("创建宫格"), # osp.join(pjpath, "resource/N2.png"), # "", # ) self.btnFinishedGrid = p_create_button( "btnFinishedGrid", self.tr("完成宫格"), osp.join(pjpath, "resource/Save.png"), "", ) hbandLayout = QtWidgets.QHBoxLayout() hbandLayout.setObjectName("hbandLayout") # hbandLayout.addWidget(self.btnInitGrid) hbandLayout.addWidget(self.btnFinishedGrid) GridRegion.addLayout(hbandLayout) # 创建宫格 self.cheSaveEvery = QtWidgets.QCheckBox(self) self.cheSaveEvery.setText(self.tr("保存每个宫格的标签")) self.cheSaveEvery.setChecked(False) GridRegion.addWidget(self.cheSaveEvery) self.gridTable = QtWidgets.QTableWidget(CentralWidget) self.gridTable.horizontalHeader().hide() self.gridTable.verticalHeader().hide() # 铺满 self.gridTable.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch ) self.gridTable.verticalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Stretch ) self.gridTable.setObjectName("gridTable") self.gridTable.clearContents() self.gridTable.setColumnCount(1) self.gridTable.setRowCount(1) GridRegion.addWidget(self.gridTable) horizontalLayout.addLayout(GridRegion) self.GridDock = p_create_dock("GridDock", self.tr("宫格切换"), widget) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.GridDock) ## ----- QtCore.QMetaObject.connectSlotsByName(MainWindow) # log.debug("Set up UI finished") ## 创建文本 def create_text(self, parent, text_name=None, text_text=None): return create_text(parent, text_name, text_text) ## 创建按钮 def create_button(self, parent, btn_name, btn_text, ico_path=None, curt=None): return create_button(parent, btn_name, btn_text, ico_path, curt) ## 创建dock def creat_dock(self, parent, name, text, layout): return creat_dock(parent, name, text, layout) ## 显示Logo def show_logo(self, logo_path): labLogo = QtWidgets.QLabel() sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum ) labLogo.setSizePolicy(sizePolicy) labLogo.setMaximumSize(QtCore.QSize(100, 33)) labLogo.setPixmap(QtGui.QPixmap(logo_path)) labLogo.setScaledContents(True) labLogo.setObjectName("labLogo") return labLogo ## 创建滑块区域 def create_slider( self, parent, sld_name, text_name, text, default_value=50, max_value=100, min_value=0, text_rate=0.01, edit=False ): return create_slider( parent, sld_name, text_name, text, default_value, max_value, min_value, text_rate, edit ) ================================================ FILE: eiseg/util/__init__.py ================================================ from .qt import newAction, addActions, struct, newIcon from .config import parse_configs, save_configs from .colormap import colorMap from .polygon import get_polygon, Instructions from .manager import MODELS from .language import TransUI from .coco.coco import COCO from .label import LabelList from .opath import check_cn, normcase ================================================ FILE: eiseg/util/coco/__init__.py ================================================ __author__ = 'tylin' ================================================ FILE: eiseg/util/coco/_mask.pyx ================================================ # distutils: language = c # distutils: sources = ../common/maskApi.c #************************************************************************** # Microsoft COCO Toolbox. version 2.0 # Data, paper, and tutorials available at: http://mscoco.org/ # Code written by Piotr Dollar and Tsung-Yi Lin, 2015. # Licensed under the Simplified BSD License [see coco/license.txt] #************************************************************************** __author__ = 'tsungyi' import sys PYTHON_VERSION = sys.version_info[0] # import both Python-level and C-level symbols of Numpy # the API uses Numpy to interface C and Python import numpy as np cimport numpy as np from libc.stdlib cimport malloc, free # intialized Numpy. must do. np.import_array() # import numpy C function # we use PyArray_ENABLEFLAGS to make Numpy ndarray responsible to memoery management cdef extern from "numpy/arrayobject.h": void PyArray_ENABLEFLAGS(np.ndarray arr, int flags) # Declare the prototype of the C functions in MaskApi.h cdef extern from "maskApi.h": ctypedef unsigned int uint ctypedef unsigned long siz ctypedef unsigned char byte ctypedef double* BB ctypedef struct RLE: siz h, siz w, siz m, uint* cnts, void rlesInit( RLE **R, siz n ) void rleEncode( RLE *R, const byte *M, siz h, siz w, siz n ) void rleDecode( const RLE *R, byte *mask, siz n ) void rleMerge( const RLE *R, RLE *M, siz n, int intersect ) void rleArea( const RLE *R, siz n, uint *a ) void rleIou( RLE *dt, RLE *gt, siz m, siz n, byte *iscrowd, double *o ) void bbIou( BB dt, BB gt, siz m, siz n, byte *iscrowd, double *o ) void rleToBbox( const RLE *R, BB bb, siz n ) void rleFrBbox( RLE *R, const BB bb, siz h, siz w, siz n ) void rleFrPoly( RLE *R, const double *xy, siz k, siz h, siz w ) char* rleToString( const RLE *R ) void rleFrString( RLE *R, char *s, siz h, siz w ) # python class to wrap RLE array in C # the class handles the memory allocation and deallocation cdef class RLEs: cdef RLE *_R cdef siz _n def __cinit__(self, siz n =0): rlesInit(&self._R, n) self._n = n # free the RLE array here def __dealloc__(self): if self._R is not NULL: for i in range(self._n): free(self._R[i].cnts) free(self._R) def __getattr__(self, key): if key == 'n': return self._n raise AttributeError(key) # python class to wrap Mask array in C # the class handles the memory allocation and deallocation cdef class Masks: cdef byte *_mask cdef siz _h cdef siz _w cdef siz _n def __cinit__(self, h, w, n): self._mask = malloc(h*w*n* sizeof(byte)) self._h = h self._w = w self._n = n # def __dealloc__(self): # the memory management of _mask has been passed to np.ndarray # it doesn't need to be freed here # called when passing into np.array() and return an np.ndarray in column-major order def __array__(self): cdef np.npy_intp shape[1] shape[0] = self._h*self._w*self._n # Create a 1D array, and reshape it to fortran/Matlab column-major array ndarray = np.PyArray_SimpleNewFromData(1, shape, np.NPY_UINT8, self._mask).reshape((self._h, self._w, self._n), order='F') # The _mask allocated by Masks is now handled by ndarray PyArray_ENABLEFLAGS(ndarray, np.NPY_OWNDATA) return ndarray # internal conversion from Python RLEs object to compressed RLE format def _toString(RLEs Rs): cdef siz n = Rs.n cdef bytes py_string cdef char* c_string objs = [] for i in range(n): c_string = rleToString( &Rs._R[i] ) py_string = c_string objs.append({ 'size': [Rs._R[i].h, Rs._R[i].w], 'counts': py_string }) free(c_string) return objs # internal conversion from compressed RLE format to Python RLEs object def _frString(rleObjs): cdef siz n = len(rleObjs) Rs = RLEs(n) cdef bytes py_string cdef char* c_string for i, obj in enumerate(rleObjs): if PYTHON_VERSION == 2: py_string = str(obj['counts']).encode('utf8') elif PYTHON_VERSION == 3: py_string = str.encode(obj['counts']) if type(obj['counts']) == str else obj['counts'] else: raise Exception('Python version must be 2 or 3') c_string = py_string rleFrString( &Rs._R[i], c_string, obj['size'][0], obj['size'][1] ) return Rs # encode mask to RLEs objects # list of RLE string can be generated by RLEs member function def encode(np.ndarray[np.uint8_t, ndim=3, mode='fortran'] mask): h, w, n = mask.shape[0], mask.shape[1], mask.shape[2] cdef RLEs Rs = RLEs(n) rleEncode(Rs._R,mask.data,h,w,n) objs = _toString(Rs) return objs # decode mask from compressed list of RLE string or RLEs object def decode(rleObjs): cdef RLEs Rs = _frString(rleObjs) h, w, n = Rs._R[0].h, Rs._R[0].w, Rs._n masks = Masks(h, w, n) rleDecode(Rs._R, masks._mask, n); return np.array(masks) def merge(rleObjs, intersect=0): cdef RLEs Rs = _frString(rleObjs) cdef RLEs R = RLEs(1) rleMerge(Rs._R, R._R, Rs._n, intersect) obj = _toString(R)[0] return obj def area(rleObjs): cdef RLEs Rs = _frString(rleObjs) cdef uint* _a = malloc(Rs._n* sizeof(uint)) rleArea(Rs._R, Rs._n, _a) cdef np.npy_intp shape[1] shape[0] = Rs._n a = np.array((Rs._n, ), dtype=np.uint8) a = np.PyArray_SimpleNewFromData(1, shape, np.NPY_UINT32, _a) PyArray_ENABLEFLAGS(a, np.NPY_OWNDATA) return a # iou computation. support function overload (RLEs-RLEs and bbox-bbox). def iou( dt, gt, pyiscrowd ): def _preproc(objs): if len(objs) == 0: return objs if type(objs) == np.ndarray: if len(objs.shape) == 1: objs = objs.reshape((objs[0], 1)) # check if it's Nx4 bbox if not len(objs.shape) == 2 or not objs.shape[1] == 4: raise Exception('numpy ndarray input is only for *bounding boxes* and should have Nx4 dimension') objs = objs.astype(np.double) elif type(objs) == list: # check if list is in box format and convert it to np.ndarray isbox = np.all(np.array([(len(obj)==4) and ((type(obj)==list) or (type(obj)==np.ndarray)) for obj in objs])) isrle = np.all(np.array([type(obj) == dict for obj in objs])) if isbox: objs = np.array(objs, dtype=np.double) if len(objs.shape) == 1: objs = objs.reshape((1,objs.shape[0])) elif isrle: objs = _frString(objs) else: raise Exception('list input can be bounding box (Nx4) or RLEs ([RLE])') else: raise Exception('unrecognized type. The following type: RLEs (rle), np.ndarray (box), and list (box) are supported.') return objs def _rleIou(RLEs dt, RLEs gt, np.ndarray[np.uint8_t, ndim=1] iscrowd, siz m, siz n, np.ndarray[np.double_t, ndim=1] _iou): rleIou( dt._R, gt._R, m, n, iscrowd.data, _iou.data ) def _bbIou(np.ndarray[np.double_t, ndim=2] dt, np.ndarray[np.double_t, ndim=2] gt, np.ndarray[np.uint8_t, ndim=1] iscrowd, siz m, siz n, np.ndarray[np.double_t, ndim=1] _iou): bbIou( dt.data, gt.data, m, n, iscrowd.data, _iou.data ) def _len(obj): cdef siz N = 0 if type(obj) == RLEs: N = obj.n elif len(obj)==0: pass elif type(obj) == np.ndarray: N = obj.shape[0] return N # convert iscrowd to numpy array cdef np.ndarray[np.uint8_t, ndim=1] iscrowd = np.array(pyiscrowd, dtype=np.uint8) # simple type checking cdef siz m, n dt = _preproc(dt) gt = _preproc(gt) m = _len(dt) n = _len(gt) if m == 0 or n == 0: return [] if not type(dt) == type(gt): raise Exception('The dt and gt should have the same data type, either RLEs, list or np.ndarray') # define local variables cdef double* _iou = 0 cdef np.npy_intp shape[1] # check type and assign iou function if type(dt) == RLEs: _iouFun = _rleIou elif type(dt) == np.ndarray: _iouFun = _bbIou else: raise Exception('input data type not allowed.') _iou = malloc(m*n* sizeof(double)) iou = np.zeros((m*n, ), dtype=np.double) shape[0] = m*n iou = np.PyArray_SimpleNewFromData(1, shape, np.NPY_DOUBLE, _iou) PyArray_ENABLEFLAGS(iou, np.NPY_OWNDATA) _iouFun(dt, gt, iscrowd, m, n, iou) return iou.reshape((m,n), order='F') def toBbox( rleObjs ): cdef RLEs Rs = _frString(rleObjs) cdef siz n = Rs.n cdef BB _bb = malloc(4*n* sizeof(double)) rleToBbox( Rs._R, _bb, n ) cdef np.npy_intp shape[1] shape[0] = 4*n bb = np.array((1,4*n), dtype=np.double) bb = np.PyArray_SimpleNewFromData(1, shape, np.NPY_DOUBLE, _bb).reshape((n, 4)) PyArray_ENABLEFLAGS(bb, np.NPY_OWNDATA) return bb def frBbox(np.ndarray[np.double_t, ndim=2] bb, siz h, siz w ): cdef siz n = bb.shape[0] Rs = RLEs(n) rleFrBbox( Rs._R, bb.data, h, w, n ) objs = _toString(Rs) return objs def frPoly( poly, siz h, siz w ): cdef np.ndarray[np.double_t, ndim=1] np_poly n = len(poly) Rs = RLEs(n) for i, p in enumerate(poly): np_poly = np.array(p, dtype=np.double, order='F') rleFrPoly( &Rs._R[i], np_poly.data, int(len(p)/2), h, w ) objs = _toString(Rs) return objs def frUncompressedRLE(ucRles, siz h, siz w): cdef np.ndarray[np.uint32_t, ndim=1] cnts cdef RLE R cdef uint *data n = len(ucRles) objs = [] for i in range(n): Rs = RLEs(1) cnts = np.array(ucRles[i]['counts'], dtype=np.uint32) # time for malloc can be saved here but it's fine data = malloc(len(cnts)* sizeof(uint)) for j in range(len(cnts)): data[j] = cnts[j] R = RLE(ucRles[i]['size'][0], ucRles[i]['size'][1], len(cnts), data) Rs._R[0] = R objs.append(_toString(Rs)[0]) return objs def frPyObjects(pyobj, h, w): # encode rle from a list of python objects if type(pyobj) == np.ndarray: objs = frBbox(pyobj, h, w) elif type(pyobj) == list and len(pyobj[0]) == 4: objs = frBbox(pyobj, h, w) elif type(pyobj) == list and len(pyobj[0]) > 4: objs = frPoly(pyobj, h, w) elif type(pyobj) == list and type(pyobj[0]) == dict \ and 'counts' in pyobj[0] and 'size' in pyobj[0]: objs = frUncompressedRLE(pyobj, h, w) # encode rle from single python object elif type(pyobj) == list and len(pyobj) == 4: objs = frBbox([pyobj], h, w)[0] elif type(pyobj) == list and len(pyobj) > 4: objs = frPoly([pyobj], h, w)[0] elif type(pyobj) == dict and 'counts' in pyobj and 'size' in pyobj: objs = frUncompressedRLE([pyobj], h, w)[0] else: raise Exception('input type is not supported.') return objs ================================================ FILE: eiseg/util/coco/coco.py ================================================ import json import time import matplotlib.pyplot as plt from matplotlib.collections import PatchCollection from matplotlib.patches import Polygon import numpy as np import copy import itertools import os import os.path as osp from collections import defaultdict import sys from datetime import datetime def _isArrayLike(obj): return hasattr(obj, "__iter__") and hasattr(obj, "__len__") class COCO: def __init__(self, annotation_file=None): """ Constructor of Microsoft COCO helper class for reading and visualizing annotations. :param annotation_file (str): location of annotation file :param image_folder (str): location to the folder that hosts images. :return: """ # dataset, anns, cats, imgs, imgToAnns, catToImgs, imgNameToId, maxAnnId, maxImgId self.dataset = { "categories": [], "images": [], "annotations": [], "info": "", "licenses": [], } # the complete json self.anns = dict() # anns[annId]={} self.cats = dict() # cats[catId] = {} self.imgs = dict() # imgs[imgId] = {} self.imgToAnns = defaultdict(list) # imgToAnns[imgId] = [ann] self.catToImgs = defaultdict(list) # catToImgs[catId] = [imgId] self.imgNameToId = defaultdict(list) # imgNameToId[name] = imgId self.maxAnnId = 0 self.maxImgId = 0 if annotation_file is not None and osp.exists(annotation_file): print("loading annotations into memory...") tic = time.time() dataset = json.load(open(annotation_file, "r")) assert ( type(dataset) == dict ), "annotation file format {} not supported".format(type(dataset)) print("Done (t={:0.2f}s)".format(time.time() - tic)) self.dataset = dataset self.createIndex() print( f"load coco with {len(self.dataset['images'])} images and {len(self.dataset['annotations'])} annotations." ) def hasImage(self, imageName): imgId = self.imgNameToId.get(imageName, None) return imgId is not None def hasCat(self, catIdx): res = self.cats.get(catIdx) return res is not None def createIndex(self): # create index print("creating index...") anns, cats, imgs = {}, {}, {} imgNameToId, imgToAnns, catToImgs, imgNameToId = [ defaultdict(list) for _ in range(4) ] if "annotations" in self.dataset: for ann in self.dataset["annotations"]: imgToAnns[ann["image_id"]].append(ann) anns[ann["id"]] = ann self.maxAnnId = max(self.maxAnnId, ann["id"]) if "images" in self.dataset: for img in self.dataset["images"]: imgs[img["id"]] = img imgNameToId[img["file_name"]] = img["id"] try: imgId = int(img["id"]) self.maxImgId = max(self.maxImgId, imgId) except: pass if "categories" in self.dataset: for cat in self.dataset["categories"]: cats[cat["id"]] = cat if "annotations" in self.dataset and "categories" in self.dataset: for ann in self.dataset["annotations"]: catToImgs[ann["category_id"]].append(ann["image_id"]) # TODO: read license print("index created!") self.anns = anns self.imgToAnns = imgToAnns self.catToImgs = catToImgs self.imgNameToId = imgNameToId self.imgs = imgs self.cats = cats def setInfo( self, year: int = "", version: str = "", description: str = "", contributor: str = "", url: str = "", date_created: datetime = "", ): self.dataset["info"] = { "year": year, "version": version, "description": description, "contributor": contributor, "url": url, "date_created": date_created, } def addCategory( self, id: int, name: str, color: list, supercategory: str = "", ): cat = { "id": id, "name": name, "color": color, "supercategory": supercategory, } self.cats[id] = cat self.dataset["categories"].append(cat) def updateCategory( self, id: int, name: str, color: list, supercategory: str = "", ): cat = { "id": id, "name": name, "color": color, "supercategory": supercategory, } self.cats[id] = cat for idx in range(len(self.dataset["categories"])): if self.dataset["categories"][idx]["id"] == id: self.dataset["categories"][idx] = cat def addImage( self, file_name: str, width: int, height: int, id: int = None, license: int = "", flickr_url: str = "", coco_url: str = "", date_captured: datetime = "", ): if self.hasImage(file_name): print(f"{file_name}图片已存在") return if not id: self.maxImgId += 1 id = self.maxImgId image = { "id": id, "width": width, "height": height, "file_name": file_name, "license": license, "flickr_url": flickr_url, "coco_url": coco_url, "date_captured": date_captured, } self.dataset["images"].append(image) self.imgs[id] = image self.imgNameToId[file_name] = id return id def getBB(self, segmentation): x = segmentation[::2] y = segmentation[1::2] maxx, minx, maxy, miny = max(x), min(x), max(y), min(y) return [minx, miny, maxx - minx, maxy - miny] def getArea(self, segmentation): x = segmentation[::2] y = segmentation[1::2] return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) def addAnnotation( self, image_id: int, category_id: int, segmentation: list, area: float = None, id: int = None, ): if id is not None and self.anns.get(id, None) is not None: print("标签已经存在") return if not id: self.maxAnnId += 1 id = self.maxAnnId ann = { "id": id, "iscrowd": 0, "image_id": image_id, "category_id": category_id, "segmentation": [segmentation], "area": self.getArea(segmentation), "bbox": self.getBB(segmentation), } self.dataset["annotations"].append(ann) self.anns[id] = ann self.imgToAnns[image_id].append(ann) self.catToImgs[category_id].append(image_id) return id def delAnnotation(self, annId, imgId): if "annotations" in self.dataset: for idx, ann in enumerate(self.dataset["annotations"]): if ann["id"] == annId: del self.dataset["annotations"][idx] if annId in self.anns.keys(): del self.anns[annId] for idx, ann in enumerate(self.imgToAnns[imgId]): if ann["id"] == annId: del self.imgToAnns[imgId][idx] def updateAnnotation(self, id, imgId, segmentation): self.anns[id]["segmentation"] = [segmentation] self.anns[id]["bbox"] = self.getBB(segmentation) self.anns[id]["area"] = self.getArea(segmentation) for rec in self.dataset["annotations"]: if rec["id"] == id: rec = self.anns[id] break for rec in self.dataset["annotations"]: if rec["id"] == id: # @todo TODO move into debug codes or controls print( "record point : ", rec["segmentation"][0][0], rec["segmentation"][0][1], ) break for rec in self.imgToAnns[imgId]: if rec["id"] == id: rec["segmentation"] = [segmentation] break def info(self): """ Print information about the annotation file. :return: """ for key, value in self.dataset["info"].items(): print("{}: {}".format(key, value)) def getAnnIds(self, imgIds=[], catIds=[], areaRng=[], iscrowd=None): """ Get ann ids that satisfy given filter conditions. default skips that filter :param imgIds (int array) : get anns for given imgs catIds (int array) : get anns for given cats areaRng (float array) : get anns for given area range (e.g. [0 inf]) iscrowd (boolean) : get anns for given crowd label (False or True) :return: ids (int array) : integer array of ann ids """ imgIds = imgIds if _isArrayLike(imgIds) else [imgIds] catIds = catIds if _isArrayLike(catIds) else [catIds] if len(imgIds) == len(catIds) == len(areaRng) == 0: anns = self.dataset["annotations"] else: if not len(imgIds) == 0: lists = [ self.imgToAnns[imgId] for imgId in imgIds if imgId in self.imgToAnns ] anns = list(itertools.chain.from_iterable(lists)) else: anns = self.dataset["annotations"] anns = ( anns if len(catIds) == 0 else [ann for ann in anns if ann["category_id"] in catIds] ) anns = ( anns if len(areaRng) == 0 else [ ann for ann in anns if ann["area"] > areaRng[0] and ann["area"] < areaRng[1] ] ) if not iscrowd == None: ids = [ann["id"] for ann in anns if ann["iscrowd"] == iscrowd] else: ids = [ann["id"] for ann in anns] return ids def getCatIds(self, catNms=[], supNms=[], catIds=[]): """ filtering parameters. default skips that filter. :param catNms (str array) : get cats for given cat names :param supNms (str array) : get cats for given supercategory names :param catIds (int array) : get cats for given cat ids :return: ids (int array) : integer array of cat ids """ catNms = catNms if _isArrayLike(catNms) else [catNms] supNms = supNms if _isArrayLike(supNms) else [supNms] catIds = catIds if _isArrayLike(catIds) else [catIds] if len(catNms) == len(supNms) == len(catIds) == 0: cats = self.dataset["categories"] else: cats = self.dataset["categories"] cats = ( cats if len(catNms) == 0 else [cat for cat in cats if cat["name"] in catNms] ) cats = ( cats if len(supNms) == 0 else [cat for cat in cats if cat["supercategory"] in supNms] ) cats = ( cats if len(catIds) == 0 else [cat for cat in cats if cat["id"] in catIds] ) ids = [cat["id"] for cat in cats] return ids def getImgIds(self, imgIds=[], catIds=[]): """ Get img ids that satisfy given filter conditions. :param imgIds (int array) : get imgs for given ids :param catIds (int array) : get imgs with all given cats :return: ids (int array) : integer array of img ids """ imgIds = imgIds if _isArrayLike(imgIds) else [imgIds] catIds = catIds if _isArrayLike(catIds) else [catIds] if len(imgIds) == len(catIds) == 0: ids = self.imgs.keys() else: ids = set(imgIds) for i, catId in enumerate(catIds): if i == 0 and len(ids) == 0: ids = set(self.catToImgs[catId]) else: ids &= set(self.catToImgs[catId]) return list(ids) def loadAnns(self, ids=[]): """ Load anns with the specified ids. :param ids (int array) : integer ids specifying anns :return: anns (object array) : loaded ann objects """ if _isArrayLike(ids): return [self.anns[id] for id in ids] elif type(ids) == int: return [self.anns[ids]] def loadCats(self, ids=[]): """ Load cats with the specified ids. :param ids (int array) : integer ids specifying cats :return: cats (object array) : loaded cat objects """ if _isArrayLike(ids): return [self.cats[id] for id in ids] elif type(ids) == int: return [self.cats[ids]] def loadImgs(self, ids=[]): """ Load anns with the specified ids. :param ids (int array) : integer ids specifying img :return: imgs (object array) : loaded img objects """ if _isArrayLike(ids): return [self.imgs[id] for id in ids] elif type(ids) == int: return [self.imgs[ids]] # def showAnns(self, anns, draw_bbox=False): # """ # Display the specified annotations. # :param anns (array of object): annotations to display # :return: None # """ # if len(anns) == 0: # return 0 # if "segmentation" in anns[0] or "keypoints" in anns[0]: # datasetType = "instances" # elif "caption" in anns[0]: # datasetType = "captions" # else: # raise Exception("datasetType not supported") # if datasetType == "instances": # ax = plt.gca() # ax.set_autoscale_on(False) # polygons = [] # color = [] # for ann in anns: # c = (np.random.random((1, 3)) * 0.6 + 0.4).tolist()[0] # if "segmentation" in ann: # if type(ann["segmentation"]) == list: # # polygon # for seg in ann["segmentation"]: # poly = np.array(seg).reshape((int(len(seg) / 2), 2)) # polygons.append(Polygon(poly)) # color.append(c) # else: # # mask # t = self.imgs[ann["image_id"]] # if type(ann["segmentation"]["counts"]) == list: # rle = maskUtils.frPyObjects( # [ann["segmentation"]], t["height"], t["width"] # ) # else: # rle = [ann["segmentation"]] # m = maskUtils.decode(rle) # img = np.ones((m.shape[0], m.shape[1], 3)) # if ann["iscrowd"] == 1: # color_mask = np.array([2.0, 166.0, 101.0]) / 255 # if ann["iscrowd"] == 0: # color_mask = np.random.random((1, 3)).tolist()[0] # for i in range(3): # img[:, :, i] = color_mask[i] # ax.imshow(np.dstack((img, m * 0.5))) # if "keypoints" in ann and type(ann["keypoints"]) == list: # # turn skeleton into zero-based index # sks = np.array(self.loadCats(ann["category_id"])[0]["skeleton"]) - 1 # kp = np.array(ann["keypoints"]) # x = kp[0::3] # y = kp[1::3] # v = kp[2::3] # for sk in sks: # if np.all(v[sk] > 0): # plt.plot(x[sk], y[sk], linewidth=3, color=c) # plt.plot( # x[v > 0], # y[v > 0], # "o", # markersize=8, # markerfacecolor=c, # markeredgecolor="k", # markeredgewidth=2, # ) # plt.plot( # x[v > 1], # y[v > 1], # "o", # markersize=8, # markerfacecolor=c, # markeredgecolor=c, # markeredgewidth=2, # ) # # if draw_bbox: # [bbox_x, bbox_y, bbox_w, bbox_h] = ann["bbox"] # poly = [ # [bbox_x, bbox_y], # [bbox_x, bbox_y + bbox_h], # [bbox_x + bbox_w, bbox_y + bbox_h], # [bbox_x + bbox_w, bbox_y], # ] # np_poly = np.array(poly).reshape((4, 2)) # polygons.append(Polygon(np_poly)) # color.append(c) # # p = PatchCollection(polygons, facecolor=color, linewidths=0, alpha=0.4) # ax.add_collection(p) # p = PatchCollection( # polygons, facecolor="none", edgecolors=color, linewidths=2 # ) # ax.add_collection(p) # elif datasetType == "captions": # for ann in anns: # print(ann["caption"]) # # def loadRes(self, resFile): # """ # Load result file and return a result api object. # :param resFile (str) : file name of result file # :return: res (obj) : result api object # """ # res = COCO() # res.dataset["images"] = [img for img in self.dataset["images"]] # # print("Loading and preparing results...") # tic = time.time() # if type(resFile) == str or (PYTHON_VERSION == 2 and type(resFile) == unicode): # anns = json.load(open(resFile)) # elif type(resFile) == np.ndarray: # anns = self.loadNumpyAnnotations(resFile) # else: # anns = resFile # assert type(anns) == list, "results in not an array of objects" # annsImgIds = [ann["image_id"] for ann in anns] # assert set(annsImgIds) == ( # set(annsImgIds) & set(self.getImgIds()) # ), "Results do not correspond to current coco set" # if "caption" in anns[0]: # imgIds = set([img["id"] for img in res.dataset["images"]]) & set( # [ann["image_id"] for ann in anns] # ) # res.dataset["images"] = [ # img for img in res.dataset["images"] if img["id"] in imgIds # ] # for id, ann in enumerate(anns): # ann["id"] = id + 1 # elif "bbox" in anns[0] and not anns[0]["bbox"] == []: # res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) # for id, ann in enumerate(anns): # bb = ann["bbox"] # x1, x2, y1, y2 = [bb[0], bb[0] + bb[2], bb[1], bb[1] + bb[3]] # if not "segmentation" in ann: # ann["segmentation"] = [[x1, y1, x1, y2, x2, y2, x2, y1]] # ann["area"] = bb[2] * bb[3] # ann["id"] = id + 1 # ann["iscrowd"] = 0 # elif "segmentation" in anns[0]: # res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) # for id, ann in enumerate(anns): # # now only support compressed RLE format as segmentation results # ann["area"] = maskUtils.area(ann["segmentation"]) # if not "bbox" in ann: # ann["bbox"] = maskUtils.toBbox(ann["segmentation"]) # ann["id"] = id + 1 # ann["iscrowd"] = 0 # elif "keypoints" in anns[0]: # res.dataset["categories"] = copy.deepcopy(self.dataset["categories"]) # for id, ann in enumerate(anns): # s = ann["keypoints"] # x = s[0::3] # y = s[1::3] # x0, x1, y0, y1 = np.min(x), np.max(x), np.min(y), np.max(y) # ann["area"] = (x1 - x0) * (y1 - y0) # ann["id"] = id + 1 # ann["bbox"] = [x0, y0, x1 - x0, y1 - y0] # print("DONE (t={:0.2f}s)".format(time.time() - tic)) # # res.dataset["annotations"] = anns # res.createIndex() # return res def download(self, tarDir=None, imgIds=[]): """ Download COCO images from mscoco.org server. :param tarDir (str): COCO results directory name imgIds (list): images to be downloaded :return: """ if tarDir is None: print("Please specify target directory") return -1 if len(imgIds) == 0: imgs = self.imgs.values() else: imgs = self.loadImgs(imgIds) N = len(imgs) if not os.path.exists(tarDir): os.makedirs(tarDir) for i, img in enumerate(imgs): tic = time.time() fname = os.path.join(tarDir, img["file_name"]) if not os.path.exists(fname): urlretrieve(img["coco_url"], fname) print( "downloaded {}/{} images (t={:0.1f}s)".format(i, N, time.time() - tic) ) def loadNumpyAnnotations(self, data): """ Convert result data from a numpy array [Nx7] where each row contains {imageID,x1,y1,w,h,score,class} :param data (numpy.ndarray) :return: annotations (python nested list) """ print("Converting ndarray to lists...") assert type(data) == np.ndarray print(data.shape) assert data.shape[1] == 7 N = data.shape[0] ann = [] for i in range(N): if i % 1000000 == 0: print("{}/{}".format(i, N)) ann += [ { "image_id": int(data[i, 0]), "bbox": [data[i, 1], data[i, 2], data[i, 3], data[i, 4]], "score": data[i, 5], "category_id": int(data[i, 6]), } ] return ann # def annToRLE(self, ann): # """ # Convert annotation which can be polygons, uncompressed RLE to RLE. # :return: binary mask (numpy 2D array) # """ # t = self.imgs[ann["image_id"]] # h, w = t["height"], t["width"] # segm = ann["segmentation"] # if type(segm) == list: # # polygon -- a single object might consist of multiple parts # # we merge all parts into one mask rle code # rles = maskUtils.frPyObjects(segm, h, w) # rle = maskUtils.merge(rles) # elif type(segm["counts"]) == list: # # uncompressed RLE # rle = maskUtils.frPyObjects(segm, h, w) # else: # # rle # rle = ann["segmentation"] # return rle # def annToMask(self, ann): # """ # Convert annotation which can be polygons, uncompressed RLE, or RLE to binary mask. # :return: binary mask (numpy 2D array) # """ # rle = self.annToRLE(ann) # m = maskUtils.decode(rle) # return m ================================================ FILE: eiseg/util/coco/cocoeval.py ================================================ __author__ = 'tsungyi' import numpy as np import datetime import time from collections import defaultdict from . import mask as maskUtils import copy class COCOeval: # Interface for evaluating detection on the Microsoft COCO dataset. # # The usage for CocoEval is as follows: # cocoGt=..., cocoDt=... # load dataset and results # E = CocoEval(cocoGt,cocoDt); # initialize CocoEval object # E.params.recThrs = ...; # set parameters as desired # E.evaluate(); # run per image evaluation # E.accumulate(); # accumulate per image results # E.summarize(); # display summary metrics of results # For example usage see evalDemo.m and http://mscoco.org/. # # The evaluation parameters are as follows (defaults in brackets): # imgIds - [all] N img ids to use for evaluation # catIds - [all] K cat ids to use for evaluation # iouThrs - [.5:.05:.95] T=10 IoU thresholds for evaluation # recThrs - [0:.01:1] R=101 recall thresholds for evaluation # areaRng - [...] A=4 object area ranges for evaluation # maxDets - [1 10 100] M=3 thresholds on max detections per image # iouType - ['segm'] set iouType to 'segm', 'bbox' or 'keypoints' # iouType replaced the now DEPRECATED useSegm parameter. # useCats - [1] if true use category labels for evaluation # Note: if useCats=0 category labels are ignored as in proposal scoring. # Note: multiple areaRngs [Ax2] and maxDets [Mx1] can be specified. # # evaluate(): evaluates detections on every image and every category and # concats the results into the "evalImgs" with fields: # dtIds - [1xD] id for each of the D detections (dt) # gtIds - [1xG] id for each of the G ground truths (gt) # dtMatches - [TxD] matching gt id at each IoU or 0 # gtMatches - [TxG] matching dt id at each IoU or 0 # dtScores - [1xD] confidence of each dt # gtIgnore - [1xG] ignore flag for each gt # dtIgnore - [TxD] ignore flag for each dt at each IoU # # accumulate(): accumulates the per-image, per-category evaluation # results in "evalImgs" into the dictionary "eval" with fields: # params - parameters used for evaluation # date - date evaluation was performed # counts - [T,R,K,A,M] parameter dimensions (see above) # precision - [TxRxKxAxM] precision for every evaluation setting # recall - [TxKxAxM] max recall for every evaluation setting # Note: precision and recall==-1 for settings with no gt objects. # # See also coco, mask, pycocoDemo, pycocoEvalDemo # # Microsoft COCO Toolbox. version 2.0 # Data, paper, and tutorials available at: http://mscoco.org/ # Code written by Piotr Dollar and Tsung-Yi Lin, 2015. # Licensed under the Simplified BSD License [see coco/license.txt] def __init__(self, cocoGt=None, cocoDt=None, iouType='segm'): ''' Initialize CocoEval using coco APIs for gt and dt :param cocoGt: coco object with ground truth annotations :param cocoDt: coco object with detection results :return: None ''' if not iouType: print('iouType not specified. use default iouType segm') self.cocoGt = cocoGt # ground truth COCO API self.cocoDt = cocoDt # detections COCO API self.evalImgs = defaultdict(list) # per-image per-category evaluation results [KxAxI] elements self.eval = {} # accumulated evaluation results self._gts = defaultdict(list) # gt for evaluation self._dts = defaultdict(list) # dt for evaluation self.params = Params(iouType=iouType) # parameters self._paramsEval = {} # parameters for evaluation self.stats = [] # result summarization self.ious = {} # ious between all gts and dts if not cocoGt is None: self.params.imgIds = sorted(cocoGt.getImgIds()) self.params.catIds = sorted(cocoGt.getCatIds()) def _prepare(self): ''' Prepare ._gts and ._dts for evaluation based on params :return: None ''' def _toMask(anns, coco): # modify ann['segmentation'] by reference for ann in anns: rle = coco.annToRLE(ann) ann['segmentation'] = rle p = self.params if p.useCats: gts=self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds)) dts=self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds, catIds=p.catIds)) else: gts=self.cocoGt.loadAnns(self.cocoGt.getAnnIds(imgIds=p.imgIds)) dts=self.cocoDt.loadAnns(self.cocoDt.getAnnIds(imgIds=p.imgIds)) # convert ground truth to mask if iouType == 'segm' if p.iouType == 'segm': _toMask(gts, self.cocoGt) _toMask(dts, self.cocoDt) # set ignore flag for gt in gts: gt['ignore'] = gt['ignore'] if 'ignore' in gt else 0 gt['ignore'] = 'iscrowd' in gt and gt['iscrowd'] if p.iouType == 'keypoints': gt['ignore'] = (gt['num_keypoints'] == 0) or gt['ignore'] self._gts = defaultdict(list) # gt for evaluation self._dts = defaultdict(list) # dt for evaluation for gt in gts: self._gts[gt['image_id'], gt['category_id']].append(gt) for dt in dts: self._dts[dt['image_id'], dt['category_id']].append(dt) self.evalImgs = defaultdict(list) # per-image per-category evaluation results self.eval = {} # accumulated evaluation results def evaluate(self): ''' Run per image evaluation on given images and store results (a list of dict) in self.evalImgs :return: None ''' tic = time.time() print('Running per image evaluation...') p = self.params # add backward compatibility if useSegm is specified in params if not p.useSegm is None: p.iouType = 'segm' if p.useSegm == 1 else 'bbox' print('useSegm (deprecated) is not None. Running {} evaluation'.format(p.iouType)) print('Evaluate annotation type *{}*'.format(p.iouType)) p.imgIds = list(np.unique(p.imgIds)) if p.useCats: p.catIds = list(np.unique(p.catIds)) p.maxDets = sorted(p.maxDets) self.params=p self._prepare() # loop through images, area range, max detection number catIds = p.catIds if p.useCats else [-1] if p.iouType == 'segm' or p.iouType == 'bbox': computeIoU = self.computeIoU elif p.iouType == 'keypoints': computeIoU = self.computeOks self.ious = {(imgId, catId): computeIoU(imgId, catId) \ for imgId in p.imgIds for catId in catIds} evaluateImg = self.evaluateImg maxDet = p.maxDets[-1] self.evalImgs = [evaluateImg(imgId, catId, areaRng, maxDet) for catId in catIds for areaRng in p.areaRng for imgId in p.imgIds ] self._paramsEval = copy.deepcopy(self.params) toc = time.time() print('DONE (t={:0.2f}s).'.format(toc-tic)) def computeIoU(self, imgId, catId): p = self.params if p.useCats: gt = self._gts[imgId,catId] dt = self._dts[imgId,catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId,cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId,cId]] if len(gt) == 0 and len(dt) ==0: return [] inds = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in inds] if len(dt) > p.maxDets[-1]: dt=dt[0:p.maxDets[-1]] if p.iouType == 'segm': g = [g['segmentation'] for g in gt] d = [d['segmentation'] for d in dt] elif p.iouType == 'bbox': g = [g['bbox'] for g in gt] d = [d['bbox'] for d in dt] else: raise Exception('unknown iouType for iou computation') # compute iou between each dt and gt region iscrowd = [int(o['iscrowd']) for o in gt] ious = maskUtils.iou(d,g,iscrowd) return ious def computeOks(self, imgId, catId): p = self.params # dimention here should be Nxm gts = self._gts[imgId, catId] dts = self._dts[imgId, catId] inds = np.argsort([-d['score'] for d in dts], kind='mergesort') dts = [dts[i] for i in inds] if len(dts) > p.maxDets[-1]: dts = dts[0:p.maxDets[-1]] # if len(gts) == 0 and len(dts) == 0: if len(gts) == 0 or len(dts) == 0: return [] ious = np.zeros((len(dts), len(gts))) sigmas = p.kpt_oks_sigmas vars = (sigmas * 2)**2 k = len(sigmas) # compute oks between each detection and ground truth object for j, gt in enumerate(gts): # create bounds for ignore regions(double the gt bbox) g = np.array(gt['keypoints']) xg = g[0::3]; yg = g[1::3]; vg = g[2::3] k1 = np.count_nonzero(vg > 0) bb = gt['bbox'] x0 = bb[0] - bb[2]; x1 = bb[0] + bb[2] * 2 y0 = bb[1] - bb[3]; y1 = bb[1] + bb[3] * 2 for i, dt in enumerate(dts): d = np.array(dt['keypoints']) xd = d[0::3]; yd = d[1::3] if k1>0: # measure the per-keypoint distance if keypoints visible dx = xd - xg dy = yd - yg else: # measure minimum distance to keypoints in (x0,y0) & (x1,y1) z = np.zeros((k)) dx = np.max((z, x0-xd),axis=0)+np.max((z, xd-x1),axis=0) dy = np.max((z, y0-yd),axis=0)+np.max((z, yd-y1),axis=0) e = (dx**2 + dy**2) / vars / (gt['area']+np.spacing(1)) / 2 if k1 > 0: e=e[vg > 0] ious[i, j] = np.sum(np.exp(-e)) / e.shape[0] return ious def evaluateImg(self, imgId, catId, aRng, maxDet): ''' perform evaluation for single category and image :return: dict (single image results) ''' p = self.params if p.useCats: gt = self._gts[imgId,catId] dt = self._dts[imgId,catId] else: gt = [_ for cId in p.catIds for _ in self._gts[imgId,cId]] dt = [_ for cId in p.catIds for _ in self._dts[imgId,cId]] if len(gt) == 0 and len(dt) ==0: return None for g in gt: if g['ignore'] or (g['area']aRng[1]): g['_ignore'] = 1 else: g['_ignore'] = 0 # sort dt highest score first, sort gt ignore last gtind = np.argsort([g['_ignore'] for g in gt], kind='mergesort') gt = [gt[i] for i in gtind] dtind = np.argsort([-d['score'] for d in dt], kind='mergesort') dt = [dt[i] for i in dtind[0:maxDet]] iscrowd = [int(o['iscrowd']) for o in gt] # load computed ious ious = self.ious[imgId, catId][:, gtind] if len(self.ious[imgId, catId]) > 0 else self.ious[imgId, catId] T = len(p.iouThrs) G = len(gt) D = len(dt) gtm = np.zeros((T,G)) dtm = np.zeros((T,D)) gtIg = np.array([g['_ignore'] for g in gt]) dtIg = np.zeros((T,D)) if not len(ious)==0: for tind, t in enumerate(p.iouThrs): for dind, d in enumerate(dt): # information about best match so far (m=-1 -> unmatched) iou = min([t,1-1e-10]) m = -1 for gind, g in enumerate(gt): # if this gt already matched, and not a crowd, continue if gtm[tind,gind]>0 and not iscrowd[gind]: continue # if dt matched to reg gt, and on ignore gt, stop if m>-1 and gtIg[m]==0 and gtIg[gind]==1: break # continue to next gt unless better match made if ious[dind,gind] < iou: continue # if match successful and best so far, store appropriately iou=ious[dind,gind] m=gind # if match made store id of match for both dt and gt if m ==-1: continue dtIg[tind,dind] = gtIg[m] dtm[tind,dind] = gt[m]['id'] gtm[tind,m] = d['id'] # set unmatched detections outside of area range to ignore a = np.array([d['area']aRng[1] for d in dt]).reshape((1, len(dt))) dtIg = np.logical_or(dtIg, np.logical_and(dtm==0, np.repeat(a,T,0))) # store results for given image and category return { 'image_id': imgId, 'category_id': catId, 'aRng': aRng, 'maxDet': maxDet, 'dtIds': [d['id'] for d in dt], 'gtIds': [g['id'] for g in gt], 'dtMatches': dtm, 'gtMatches': gtm, 'dtScores': [d['score'] for d in dt], 'gtIgnore': gtIg, 'dtIgnore': dtIg, } def accumulate(self, p = None): ''' Accumulate per image evaluation results and store the result in self.eval :param p: input params for evaluation :return: None ''' print('Accumulating evaluation results...') tic = time.time() if not self.evalImgs: print('Please run evaluate() first') # allows input customized parameters if p is None: p = self.params p.catIds = p.catIds if p.useCats == 1 else [-1] T = len(p.iouThrs) R = len(p.recThrs) K = len(p.catIds) if p.useCats else 1 A = len(p.areaRng) M = len(p.maxDets) precision = -np.ones((T,R,K,A,M)) # -1 for the precision of absent categories recall = -np.ones((T,K,A,M)) scores = -np.ones((T,R,K,A,M)) # create dictionary for future indexing _pe = self._paramsEval catIds = _pe.catIds if _pe.useCats else [-1] setK = set(catIds) setA = set(map(tuple, _pe.areaRng)) setM = set(_pe.maxDets) setI = set(_pe.imgIds) # get inds to evaluate k_list = [n for n, k in enumerate(p.catIds) if k in setK] m_list = [m for n, m in enumerate(p.maxDets) if m in setM] a_list = [n for n, a in enumerate(map(lambda x: tuple(x), p.areaRng)) if a in setA] i_list = [n for n, i in enumerate(p.imgIds) if i in setI] I0 = len(_pe.imgIds) A0 = len(_pe.areaRng) # retrieve E at each category, area range, and max number of detections for k, k0 in enumerate(k_list): Nk = k0*A0*I0 for a, a0 in enumerate(a_list): Na = a0*I0 for m, maxDet in enumerate(m_list): E = [self.evalImgs[Nk + Na + i] for i in i_list] E = [e for e in E if not e is None] if len(E) == 0: continue dtScores = np.concatenate([e['dtScores'][0:maxDet] for e in E]) # different sorting method generates slightly different results. # mergesort is used to be consistent as Matlab implementation. inds = np.argsort(-dtScores, kind='mergesort') dtScoresSorted = dtScores[inds] dtm = np.concatenate([e['dtMatches'][:,0:maxDet] for e in E], axis=1)[:,inds] dtIg = np.concatenate([e['dtIgnore'][:,0:maxDet] for e in E], axis=1)[:,inds] gtIg = np.concatenate([e['gtIgnore'] for e in E]) npig = np.count_nonzero(gtIg==0 ) if npig == 0: continue tps = np.logical_and( dtm, np.logical_not(dtIg) ) fps = np.logical_and(np.logical_not(dtm), np.logical_not(dtIg) ) tp_sum = np.cumsum(tps, axis=1).astype(dtype=np.float) fp_sum = np.cumsum(fps, axis=1).astype(dtype=np.float) for t, (tp, fp) in enumerate(zip(tp_sum, fp_sum)): tp = np.array(tp) fp = np.array(fp) nd = len(tp) rc = tp / npig pr = tp / (fp+tp+np.spacing(1)) q = np.zeros((R,)) ss = np.zeros((R,)) if nd: recall[t,k,a,m] = rc[-1] else: recall[t,k,a,m] = 0 # numpy is slow without cython optimization for accessing elements # use python array gets significant speed improvement pr = pr.tolist(); q = q.tolist() for i in range(nd-1, 0, -1): if pr[i] > pr[i-1]: pr[i-1] = pr[i] inds = np.searchsorted(rc, p.recThrs, side='left') try: for ri, pi in enumerate(inds): q[ri] = pr[pi] ss[ri] = dtScoresSorted[pi] except: pass precision[t,:,k,a,m] = np.array(q) scores[t,:,k,a,m] = np.array(ss) self.eval = { 'params': p, 'counts': [T, R, K, A, M], 'date': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'precision': precision, 'recall': recall, 'scores': scores, } toc = time.time() print('DONE (t={:0.2f}s).'.format( toc-tic)) def summarize(self): ''' Compute and display summary metrics for evaluation results. Note this functin can *only* be applied on the default parameter setting ''' def _summarize( ap=1, iouThr=None, areaRng='all', maxDets=100 ): p = self.params iStr = ' {:<18} {} @[ IoU={:<9} | area={:>6s} | maxDets={:>3d} ] = {:0.3f}' titleStr = 'Average Precision' if ap == 1 else 'Average Recall' typeStr = '(AP)' if ap==1 else '(AR)' iouStr = '{:0.2f}:{:0.2f}'.format(p.iouThrs[0], p.iouThrs[-1]) \ if iouThr is None else '{:0.2f}'.format(iouThr) aind = [i for i, aRng in enumerate(p.areaRngLbl) if aRng == areaRng] mind = [i for i, mDet in enumerate(p.maxDets) if mDet == maxDets] if ap == 1: # dimension of precision: [TxRxKxAxM] s = self.eval['precision'] # IoU if iouThr is not None: t = np.where(iouThr == p.iouThrs)[0] s = s[t] s = s[:,:,:,aind,mind] else: # dimension of recall: [TxKxAxM] s = self.eval['recall'] if iouThr is not None: t = np.where(iouThr == p.iouThrs)[0] s = s[t] s = s[:,:,aind,mind] if len(s[s>-1])==0: mean_s = -1 else: mean_s = np.mean(s[s>-1]) print(iStr.format(titleStr, typeStr, iouStr, areaRng, maxDets, mean_s)) return mean_s def _summarizeDets(): stats = np.zeros((12,)) stats[0] = _summarize(1) stats[1] = _summarize(1, iouThr=.5, maxDets=self.params.maxDets[2]) stats[2] = _summarize(1, iouThr=.75, maxDets=self.params.maxDets[2]) stats[3] = _summarize(1, areaRng='small', maxDets=self.params.maxDets[2]) stats[4] = _summarize(1, areaRng='medium', maxDets=self.params.maxDets[2]) stats[5] = _summarize(1, areaRng='large', maxDets=self.params.maxDets[2]) stats[6] = _summarize(0, maxDets=self.params.maxDets[0]) stats[7] = _summarize(0, maxDets=self.params.maxDets[1]) stats[8] = _summarize(0, maxDets=self.params.maxDets[2]) stats[9] = _summarize(0, areaRng='small', maxDets=self.params.maxDets[2]) stats[10] = _summarize(0, areaRng='medium', maxDets=self.params.maxDets[2]) stats[11] = _summarize(0, areaRng='large', maxDets=self.params.maxDets[2]) return stats def _summarizeKps(): stats = np.zeros((10,)) stats[0] = _summarize(1, maxDets=20) stats[1] = _summarize(1, maxDets=20, iouThr=.5) stats[2] = _summarize(1, maxDets=20, iouThr=.75) stats[3] = _summarize(1, maxDets=20, areaRng='medium') stats[4] = _summarize(1, maxDets=20, areaRng='large') stats[5] = _summarize(0, maxDets=20) stats[6] = _summarize(0, maxDets=20, iouThr=.5) stats[7] = _summarize(0, maxDets=20, iouThr=.75) stats[8] = _summarize(0, maxDets=20, areaRng='medium') stats[9] = _summarize(0, maxDets=20, areaRng='large') return stats if not self.eval: raise Exception('Please run accumulate() first') iouType = self.params.iouType if iouType == 'segm' or iouType == 'bbox': summarize = _summarizeDets elif iouType == 'keypoints': summarize = _summarizeKps self.stats = summarize() def __str__(self): self.summarize() class Params: ''' Params for coco evaluation api ''' def setDetParams(self): self.imgIds = [] self.catIds = [] # np.arange causes trouble. the data point on arange is slightly larger than the true value self.iouThrs = np.linspace(.5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True) self.recThrs = np.linspace(.0, 1.00, int(np.round((1.00 - .0) / .01)) + 1, endpoint=True) self.maxDets = [1, 10, 100] self.areaRng = [[0 ** 2, 1e5 ** 2], [0 ** 2, 32 ** 2], [32 ** 2, 96 ** 2], [96 ** 2, 1e5 ** 2]] self.areaRngLbl = ['all', 'small', 'medium', 'large'] self.useCats = 1 def setKpParams(self): self.imgIds = [] self.catIds = [] # np.arange causes trouble. the data point on arange is slightly larger than the true value self.iouThrs = np.linspace(.5, 0.95, int(np.round((0.95 - .5) / .05)) + 1, endpoint=True) self.recThrs = np.linspace(.0, 1.00, int(np.round((1.00 - .0) / .01)) + 1, endpoint=True) self.maxDets = [20] self.areaRng = [[0 ** 2, 1e5 ** 2], [32 ** 2, 96 ** 2], [96 ** 2, 1e5 ** 2]] self.areaRngLbl = ['all', 'medium', 'large'] self.useCats = 1 self.kpt_oks_sigmas = np.array([.26, .25, .25, .35, .35, .79, .79, .72, .72, .62,.62, 1.07, 1.07, .87, .87, .89, .89])/10.0 def __init__(self, iouType='segm'): if iouType == 'segm' or iouType == 'bbox': self.setDetParams() elif iouType == 'keypoints': self.setKpParams() else: raise Exception('iouType not supported') self.iouType = iouType # useSegm is deprecated self.useSegm = None ================================================ FILE: eiseg/util/coco/common/gason.cpp ================================================ // https://github.com/vivkin/gason - pulled January 10, 2016 #include "gason.h" #include #define JSON_ZONE_SIZE 4096 #define JSON_STACK_SIZE 32 const char *jsonStrError(int err) { switch (err) { #define XX(no, str) \ case JSON_##no: \ return str; JSON_ERRNO_MAP(XX) #undef XX default: return "unknown"; } } void *JsonAllocator::allocate(size_t size) { size = (size + 7) & ~7; if (head && head->used + size <= JSON_ZONE_SIZE) { char *p = (char *)head + head->used; head->used += size; return p; } size_t allocSize = sizeof(Zone) + size; Zone *zone = (Zone *)malloc(allocSize <= JSON_ZONE_SIZE ? JSON_ZONE_SIZE : allocSize); if (zone == nullptr) return nullptr; zone->used = allocSize; if (allocSize <= JSON_ZONE_SIZE || head == nullptr) { zone->next = head; head = zone; } else { zone->next = head->next; head->next = zone; } return (char *)zone + sizeof(Zone); } void JsonAllocator::deallocate() { while (head) { Zone *next = head->next; free(head); head = next; } } static inline bool isspace(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } static inline bool isdelim(char c) { return c == ',' || c == ':' || c == ']' || c == '}' || isspace(c) || !c; } static inline bool isdigit(char c) { return c >= '0' && c <= '9'; } static inline bool isxdigit(char c) { return (c >= '0' && c <= '9') || ((c & ~' ') >= 'A' && (c & ~' ') <= 'F'); } static inline int char2int(char c) { if (c <= '9') return c - '0'; return (c & ~' ') - 'A' + 10; } static double string2double(char *s, char **endptr) { char ch = *s; if (ch == '-') ++s; double result = 0; while (isdigit(*s)) result = (result * 10) + (*s++ - '0'); if (*s == '.') { ++s; double fraction = 1; while (isdigit(*s)) { fraction *= 0.1; result += (*s++ - '0') * fraction; } } if (*s == 'e' || *s == 'E') { ++s; double base = 10; if (*s == '+') ++s; else if (*s == '-') { ++s; base = 0.1; } unsigned int exponent = 0; while (isdigit(*s)) exponent = (exponent * 10) + (*s++ - '0'); double power = 1; for (; exponent; exponent >>= 1, base *= base) if (exponent & 1) power *= base; result *= power; } *endptr = s; return ch == '-' ? -result : result; } static inline JsonNode *insertAfter(JsonNode *tail, JsonNode *node) { if (!tail) return node->next = node; node->next = tail->next; tail->next = node; return node; } static inline JsonValue listToValue(JsonTag tag, JsonNode *tail) { if (tail) { auto head = tail->next; tail->next = nullptr; return JsonValue(tag, head); } return JsonValue(tag, nullptr); } int jsonParse(char *s, char **endptr, JsonValue *value, JsonAllocator &allocator) { JsonNode *tails[JSON_STACK_SIZE]; JsonTag tags[JSON_STACK_SIZE]; char *keys[JSON_STACK_SIZE]; JsonValue o; int pos = -1; bool separator = true; JsonNode *node; *endptr = s; while (*s) { while (isspace(*s)) { ++s; if (!*s) break; } *endptr = s++; switch (**endptr) { case '-': if (!isdigit(*s) && *s != '.') { *endptr = s; return JSON_BAD_NUMBER; } case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': o = JsonValue(string2double(*endptr, &s)); if (!isdelim(*s)) { *endptr = s; return JSON_BAD_NUMBER; } break; case '"': o = JsonValue(JSON_STRING, s); for (char *it = s; *s; ++it, ++s) { int c = *it = *s; if (c == '\\') { c = *++s; switch (c) { case '\\': case '"': case '/': *it = c; break; case 'b': *it = '\b'; break; case 'f': *it = '\f'; break; case 'n': *it = '\n'; break; case 'r': *it = '\r'; break; case 't': *it = '\t'; break; case 'u': c = 0; for (int i = 0; i < 4; ++i) { if (isxdigit(*++s)) { c = c * 16 + char2int(*s); } else { *endptr = s; return JSON_BAD_STRING; } } if (c < 0x80) { *it = c; } else if (c < 0x800) { *it++ = 0xC0 | (c >> 6); *it = 0x80 | (c & 0x3F); } else { *it++ = 0xE0 | (c >> 12); *it++ = 0x80 | ((c >> 6) & 0x3F); *it = 0x80 | (c & 0x3F); } break; default: *endptr = s; return JSON_BAD_STRING; } } else if ((unsigned int)c < ' ' || c == '\x7F') { *endptr = s; return JSON_BAD_STRING; } else if (c == '"') { *it = 0; ++s; break; } } if (!isdelim(*s)) { *endptr = s; return JSON_BAD_STRING; } break; case 't': if (!(s[0] == 'r' && s[1] == 'u' && s[2] == 'e' && isdelim(s[3]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_TRUE); s += 3; break; case 'f': if (!(s[0] == 'a' && s[1] == 'l' && s[2] == 's' && s[3] == 'e' && isdelim(s[4]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_FALSE); s += 4; break; case 'n': if (!(s[0] == 'u' && s[1] == 'l' && s[2] == 'l' && isdelim(s[3]))) return JSON_BAD_IDENTIFIER; o = JsonValue(JSON_NULL); s += 3; break; case ']': if (pos == -1) return JSON_STACK_UNDERFLOW; if (tags[pos] != JSON_ARRAY) return JSON_MISMATCH_BRACKET; o = listToValue(JSON_ARRAY, tails[pos--]); break; case '}': if (pos == -1) return JSON_STACK_UNDERFLOW; if (tags[pos] != JSON_OBJECT) return JSON_MISMATCH_BRACKET; if (keys[pos] != nullptr) return JSON_UNEXPECTED_CHARACTER; o = listToValue(JSON_OBJECT, tails[pos--]); break; case '[': if (++pos == JSON_STACK_SIZE) return JSON_STACK_OVERFLOW; tails[pos] = nullptr; tags[pos] = JSON_ARRAY; keys[pos] = nullptr; separator = true; continue; case '{': if (++pos == JSON_STACK_SIZE) return JSON_STACK_OVERFLOW; tails[pos] = nullptr; tags[pos] = JSON_OBJECT; keys[pos] = nullptr; separator = true; continue; case ':': if (separator || keys[pos] == nullptr) return JSON_UNEXPECTED_CHARACTER; separator = true; continue; case ',': if (separator || keys[pos] != nullptr) return JSON_UNEXPECTED_CHARACTER; separator = true; continue; case '\0': continue; default: return JSON_UNEXPECTED_CHARACTER; } separator = false; if (pos == -1) { *endptr = s; *value = o; return JSON_OK; } if (tags[pos] == JSON_OBJECT) { if (!keys[pos]) { if (o.getTag() != JSON_STRING) return JSON_UNQUOTED_KEY; keys[pos] = o.toString(); continue; } if ((node = (JsonNode *) allocator.allocate(sizeof(JsonNode))) == nullptr) return JSON_ALLOCATION_FAILURE; tails[pos] = insertAfter(tails[pos], node); tails[pos]->key = keys[pos]; keys[pos] = nullptr; } else { if ((node = (JsonNode *) allocator.allocate(sizeof(JsonNode) - sizeof(char *))) == nullptr) return JSON_ALLOCATION_FAILURE; tails[pos] = insertAfter(tails[pos], node); } tails[pos]->value = o; } return JSON_BREAKING_BAD; } ================================================ FILE: eiseg/util/coco/common/gason.h ================================================ // https://github.com/vivkin/gason - pulled January 10, 2016 #pragma once #include #include #include enum JsonTag { JSON_NUMBER = 0, JSON_STRING, JSON_ARRAY, JSON_OBJECT, JSON_TRUE, JSON_FALSE, JSON_NULL = 0xF }; struct JsonNode; #define JSON_VALUE_PAYLOAD_MASK 0x00007FFFFFFFFFFFULL #define JSON_VALUE_NAN_MASK 0x7FF8000000000000ULL #define JSON_VALUE_TAG_MASK 0xF #define JSON_VALUE_TAG_SHIFT 47 union JsonValue { uint64_t ival; double fval; JsonValue(double x) : fval(x) { } JsonValue(JsonTag tag = JSON_NULL, void *payload = nullptr) { assert((uintptr_t)payload <= JSON_VALUE_PAYLOAD_MASK); ival = JSON_VALUE_NAN_MASK | ((uint64_t)tag << JSON_VALUE_TAG_SHIFT) | (uintptr_t)payload; } bool isDouble() const { return (int64_t)ival <= (int64_t)JSON_VALUE_NAN_MASK; } JsonTag getTag() const { return isDouble() ? JSON_NUMBER : JsonTag((ival >> JSON_VALUE_TAG_SHIFT) & JSON_VALUE_TAG_MASK); } uint64_t getPayload() const { assert(!isDouble()); return ival & JSON_VALUE_PAYLOAD_MASK; } double toNumber() const { assert(getTag() == JSON_NUMBER); return fval; } char *toString() const { assert(getTag() == JSON_STRING); return (char *)getPayload(); } JsonNode *toNode() const { assert(getTag() == JSON_ARRAY || getTag() == JSON_OBJECT); return (JsonNode *)getPayload(); } }; struct JsonNode { JsonValue value; JsonNode *next; char *key; }; struct JsonIterator { JsonNode *p; void operator++() { p = p->next; } bool operator!=(const JsonIterator &x) const { return p != x.p; } JsonNode *operator*() const { return p; } JsonNode *operator->() const { return p; } }; inline JsonIterator begin(JsonValue o) { return JsonIterator{o.toNode()}; } inline JsonIterator end(JsonValue) { return JsonIterator{nullptr}; } #define JSON_ERRNO_MAP(XX) \ XX(OK, "ok") \ XX(BAD_NUMBER, "bad number") \ XX(BAD_STRING, "bad string") \ XX(BAD_IDENTIFIER, "bad identifier") \ XX(STACK_OVERFLOW, "stack overflow") \ XX(STACK_UNDERFLOW, "stack underflow") \ XX(MISMATCH_BRACKET, "mismatch bracket") \ XX(UNEXPECTED_CHARACTER, "unexpected character") \ XX(UNQUOTED_KEY, "unquoted key") \ XX(BREAKING_BAD, "breaking bad") \ XX(ALLOCATION_FAILURE, "allocation failure") enum JsonErrno { #define XX(no, str) JSON_##no, JSON_ERRNO_MAP(XX) #undef XX }; const char *jsonStrError(int err); class JsonAllocator { struct Zone { Zone *next; size_t used; } *head = nullptr; public: JsonAllocator() = default; JsonAllocator(const JsonAllocator &) = delete; JsonAllocator &operator=(const JsonAllocator &) = delete; JsonAllocator(JsonAllocator &&x) : head(x.head) { x.head = nullptr; } JsonAllocator &operator=(JsonAllocator &&x) { head = x.head; x.head = nullptr; return *this; } ~JsonAllocator() { deallocate(); } void *allocate(size_t size); void deallocate(); }; int jsonParse(char *str, char **endptr, JsonValue *value, JsonAllocator &allocator); ================================================ FILE: eiseg/util/coco/common/maskApi.c ================================================ /************************************************************************** * Microsoft COCO Toolbox. version 2.0 * Data, paper, and tutorials available at: http://mscoco.org/ * Code written by Piotr Dollar and Tsung-Yi Lin, 2015. * Licensed under the Simplified BSD License [see coco/license.txt] **************************************************************************/ #include "maskApi.h" #include #include uint umin( uint a, uint b ) { return (ab) ? a : b; } void rleInit( RLE *R, siz h, siz w, siz m, uint *cnts ) { R->h=h; R->w=w; R->m=m; R->cnts=(m==0)?0:malloc(sizeof(uint)*m); siz j; if(cnts) for(j=0; jcnts[j]=cnts[j]; } void rleFree( RLE *R ) { free(R->cnts); R->cnts=0; } void rlesInit( RLE **R, siz n ) { siz i; *R = (RLE*) malloc(sizeof(RLE)*n); for(i=0; i0 ) { c=umin(ca,cb); cc+=c; ct=0; ca-=c; if(!ca && a0) { crowd=iscrowd!=NULL && iscrowd[g]; if(dt[d].h!=gt[g].h || dt[d].w!=gt[g].w) { o[g*m+d]=-1; continue; } siz ka, kb, a, b; uint c, ca, cb, ct, i, u; int va, vb; ca=dt[d].cnts[0]; ka=dt[d].m; va=vb=0; cb=gt[g].cnts[0]; kb=gt[g].m; a=b=1; i=u=0; ct=1; while( ct>0 ) { c=umin(ca,cb); if(va||vb) { u+=c; if(va&&vb) i+=c; } ct=0; ca-=c; if(!ca && athr) keep[j]=0; } } } void bbIou( BB dt, BB gt, siz m, siz n, byte *iscrowd, double *o ) { double h, w, i, u, ga, da; siz g, d; int crowd; for( g=0; gthr) keep[j]=0; } } } void rleToBbox( const RLE *R, BB bb, siz n ) { siz i; for( i=0; id?1:c=dy && xs>xe) || (dxye); if(flip) { t=xs; xs=xe; xe=t; t=ys; ys=ye; ye=t; } s = dx>=dy ? (double)(ye-ys)/dx : (double)(xe-xs)/dy; if(dx>=dy) for( d=0; d<=dx; d++ ) { t=flip?dx-d:d; u[m]=t+xs; v[m]=(int)(ys+s*t+.5); m++; } else for( d=0; d<=dy; d++ ) { t=flip?dy-d:d; v[m]=t+ys; u[m]=(int)(xs+s*t+.5); m++; } } /* get points along y-boundary and downsample */ free(x); free(y); k=m; m=0; double xd, yd; x=malloc(sizeof(int)*k); y=malloc(sizeof(int)*k); for( j=1; jw-1 ) continue; yd=(double)(v[j]h) yd=h; yd=ceil(yd); x[m]=(int) xd; y[m]=(int) yd; m++; } /* compute rle encoding given y-boundary points */ k=m; a=malloc(sizeof(uint)*(k+1)); for( j=0; j0) b[m++]=a[j++]; else { j++; if(jm, p=0; long x; int more; char *s=malloc(sizeof(char)*m*6); for( i=0; icnts[i]; if(i>2) x-=(long) R->cnts[i-2]; more=1; while( more ) { char c=x & 0x1f; x >>= 5; more=(c & 0x10) ? x!=-1 : x!=0; if(more) c |= 0x20; c+=48; s[p++]=c; } } s[p]=0; return s; } void rleFrString( RLE *R, char *s, siz h, siz w ) { siz m=0, p=0, k; long x; int more; uint *cnts; while( s[m] ) m++; cnts=malloc(sizeof(uint)*m); m=0; while( s[p] ) { x=0; k=0; more=1; while( more ) { char c=s[p]-48; x |= (c & 0x1f) << 5*k; more = c & 0x20; p++; k++; if(!more && (c & 0x10)) x |= -1 << 5*k; } if(m>2) x+=(long) cnts[m-2]; cnts[m++]=(uint) x; } rleInit(R,h,w,m,cnts); free(cnts); } ================================================ FILE: eiseg/util/coco/common/maskApi.h ================================================ /************************************************************************** * Microsoft COCO Toolbox. version 2.0 * Data, paper, and tutorials available at: http://mscoco.org/ * Code written by Piotr Dollar and Tsung-Yi Lin, 2015. * Licensed under the Simplified BSD License [see coco/license.txt] **************************************************************************/ #pragma once typedef unsigned int uint; typedef unsigned long siz; typedef unsigned char byte; typedef double* BB; typedef struct { siz h, w, m; uint *cnts; } RLE; /* Initialize/destroy RLE. */ void rleInit( RLE *R, siz h, siz w, siz m, uint *cnts ); void rleFree( RLE *R ); /* Initialize/destroy RLE array. */ void rlesInit( RLE **R, siz n ); void rlesFree( RLE **R, siz n ); /* Encode binary masks using RLE. */ void rleEncode( RLE *R, const byte *mask, siz h, siz w, siz n ); /* Decode binary masks encoded via RLE. */ void rleDecode( const RLE *R, byte *mask, siz n ); /* Compute union or intersection of encoded masks. */ void rleMerge( const RLE *R, RLE *M, siz n, int intersect ); /* Compute area of encoded masks. */ void rleArea( const RLE *R, siz n, uint *a ); /* Compute intersection over union between masks. */ void rleIou( RLE *dt, RLE *gt, siz m, siz n, byte *iscrowd, double *o ); /* Compute non-maximum suppression between bounding masks */ void rleNms( RLE *dt, siz n, uint *keep, double thr ); /* Compute intersection over union between bounding boxes. */ void bbIou( BB dt, BB gt, siz m, siz n, byte *iscrowd, double *o ); /* Compute non-maximum suppression between bounding boxes */ void bbNms( BB dt, siz n, uint *keep, double thr ); /* Get bounding boxes surrounding encoded masks. */ void rleToBbox( const RLE *R, BB bb, siz n ); /* Convert bounding boxes to encoded masks. */ void rleFrBbox( RLE *R, const BB bb, siz h, siz w, siz n ); /* Convert polygon to encoded mask. */ void rleFrPoly( RLE *R, const double *xy, siz k, siz h, siz w ); /* Get compressed string representation of encoded mask. */ char* rleToString( const RLE *R ); /* Convert from compressed string representation of encoded mask. */ void rleFrString( RLE *R, char *s, siz h, siz w ); ================================================ FILE: eiseg/util/coco/mask.py ================================================ __author__ = 'tsungyi' import pycocotools._mask as _mask # Interface for manipulating masks stored in RLE format. # # RLE is a simple yet efficient format for storing binary masks. RLE # first divides a vector (or vectorized image) into a series of piecewise # constant regions and then for each piece simply stores the length of # that piece. For example, given M=[0 0 1 1 1 0 1] the RLE counts would # be [2 3 1 1], or for M=[1 1 1 1 1 1 0] the counts would be [0 6 1] # (note that the odd counts are always the numbers of zeros). Instead of # storing the counts directly, additional compression is achieved with a # variable bitrate representation based on a common scheme called LEB128. # # Compression is greatest given large piecewise constant regions. # Specifically, the size of the RLE is proportional to the number of # *boundaries* in M (or for an image the number of boundaries in the y # direction). Assuming fairly simple shapes, the RLE representation is # O(sqrt(n)) where n is number of pixels in the object. Hence space usage # is substantially lower, especially for large simple objects (large n). # # Many common operations on masks can be computed directly using the RLE # (without need for decoding). This includes computations such as area, # union, intersection, etc. All of these operations are linear in the # size of the RLE, in other words they are O(sqrt(n)) where n is the area # of the object. Computing these operations on the original mask is O(n). # Thus, using the RLE can result in substantial computational savings. # # The following API functions are defined: # encode - Encode binary masks using RLE. # decode - Decode binary masks encoded via RLE. # merge - Compute union or intersection of encoded masks. # iou - Compute intersection over union between masks. # area - Compute area of encoded masks. # toBbox - Get bounding boxes surrounding encoded masks. # frPyObjects - Convert polygon, bbox, and uncompressed RLE to encoded RLE mask. # # Usage: # Rs = encode( masks ) # masks = decode( Rs ) # R = merge( Rs, intersect=false ) # o = iou( dt, gt, iscrowd ) # a = area( Rs ) # bbs = toBbox( Rs ) # Rs = frPyObjects( [pyObjects], h, w ) # # In the API the following formats are used: # Rs - [dict] Run-length encoding of binary masks # R - dict Run-length encoding of binary mask # masks - [hxwxn] Binary mask(s) (must have type np.ndarray(dtype=uint8) in column-major order) # iscrowd - [nx1] list of np.ndarray. 1 indicates corresponding gt image has crowd region to ignore # bbs - [nx4] Bounding box(es) stored as [x y w h] # poly - Polygon stored as [[x1 y1 x2 y2...],[x1 y1 ...],...] (2D list) # dt,gt - May be either bounding boxes or encoded masks # Both poly and bbs are 0-indexed (bbox=[0 0 1 1] encloses first pixel). # # Finally, a note about the intersection over union (iou) computation. # The standard iou of a ground truth (gt) and detected (dt) object is # iou(gt,dt) = area(intersect(gt,dt)) / area(union(gt,dt)) # For "crowd" regions, we use a modified criteria. If a gt object is # marked as "iscrowd", we allow a dt to match any subregion of the gt. # Choosing gt' in the crowd gt that best matches the dt can be done using # gt'=intersect(dt,gt). Since by definition union(gt',dt)=dt, computing # iou(gt,dt,iscrowd) = iou(gt',dt) = area(intersect(gt,dt)) / area(dt) # For crowd gt regions we use this modified criteria above for the iou. # # To compile run "python setup.py build_ext --inplace" # Please do not contact us for help with compiling. # # Microsoft COCO Toolbox. version 2.0 # Data, paper, and tutorials available at: http://mscoco.org/ # Code written by Piotr Dollar and Tsung-Yi Lin, 2015. # Licensed under the Simplified BSD License [see coco/license.txt] iou = _mask.iou merge = _mask.merge frPyObjects = _mask.frPyObjects def encode(bimask): if len(bimask.shape) == 3: return _mask.encode(bimask) elif len(bimask.shape) == 2: h, w = bimask.shape return _mask.encode(bimask.reshape((h, w, 1), order='F'))[0] def decode(rleObjs): if type(rleObjs) == list: return _mask.decode(rleObjs) else: return _mask.decode([rleObjs])[:,:,0] def area(rleObjs): if type(rleObjs) == list: return _mask.area(rleObjs) else: return _mask.area([rleObjs])[0] def toBbox(rleObjs): if type(rleObjs) == list: return _mask.toBbox(rleObjs) else: return _mask.toBbox([rleObjs])[0] ================================================ FILE: eiseg/util/coco.py.bk ================================================ cocoDict = { "info": info, "images": [image], "annotations": [annotation], "categories": [ { "id": int, "name": str, "supercategory": str, } ], "licenses": [license], } license = { "id": int, "name": str, "url": str, } image = { "id": int, "width": int, "height": int, "file_name": str, "license": int, "flickr_url": str, "coco_url": str, "date_captured": datetime, } annotation = { "id": int, "image_id": int, "category_id": int, "segmentation": [polygon], "area": float, "bbox": [x, y, width, height], } info = { "year": int, "version": str, "description": str, "contributor": str, "url": str, "date_created": datetime, } import datetime class CoCoAnn: def __init__(self, cocoFile=None): self.dict = { "info": {}, "images": [], "annotations": [], "categories": [], "licenses": [], } self.annId = 0 def setInfo( self, year: int = "", version="", description="", contributor="", url="", date_created="", ): # if not year: # now = datetime.now() # year = now.strftime("%Y") # # TODO: datetime # if not date_created: # pass self.dict["info"] = { "year": year, "version": version, "description": description, "contributor": contributor, "url": url, "date_created": date_created, } def setCategories(self, categories): self.dict["categories"] = categories def addCategory(self, id, name, supercategory=""): cat = { "id": int, "name": str, "supercategory": str, } self.dict["categories"].append(cat) def setLicenses(self, licenses): self.licenses = licenses def addLicense(self, id, name, url): license = { "id": int, "name": str, "url": str, } self.dict["licenses"].append(license) def addImage( self, id, width, height, file_name, license="", flickr_url="", coco_url="", date_captured="", ): image = { "id": id, "width": width, "height": height, "file_name": file_name, "license": license, "flickr_url": flickr_url, "coco_url": coco_url, "date_captured": date_captured, } self.dict["images"].append(image) def addAnnotation( self, image_id, category_id, segmentation, bbox, area, id, ): { "id": int, "image_id": int, "category_id": int, "segmentation": [polygon], "area": float, "bbox": [x, y, width, height], } ================================================ FILE: eiseg/util/colormap.py ================================================ # Copyright (c) 2021 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.path as osp import random from eiseg import pjpath class ColorMap(object): def __init__(self, color_path, shuffle=False): self.colors = [] self.index = 0 self.usedColors = [] with open(color_path, "r") as f: colors = f.readlines() if shuffle: random.shuffle(colors) self.colors = [[int(x) for x in c.strip().split(",")] for c in colors] def get_color(self): color = self.colors[self.index] self.index = (self.index + 1) % len(self) return color def __len__(self): return len(self.colors) colorMap = ColorMap(osp.join(pjpath, "config/colormap.txt")) ================================================ FILE: eiseg/util/config.py ================================================ import yaml import os.path as osp import os from eiseg import pjpath def parse_configs(path): if not path or not osp.exists(path): return with open(path, "r", encoding="utf-8") as f: return yaml.load(f.read(), Loader=yaml.FullLoader) def save_configs(path=None, config=None, actions=None): if not path: path = osp.join(pjpath, "config/config.yaml") if not osp.exists(path): # os.makedirs(osp.basename(path)) # windows无法使用mknod f = open(path, "w+") f.close() if not config: config = {} if actions: config["shortcut"] = {} for action in actions: config["shortcut"][action.data()] = action.shortcut().toString() with open(path, "w", encoding="utf-8") as f: yaml.dump(config, f) class cfgData(object): def __init__(self, yaml_file): with open(yaml_file, "r", encoding="utf-8") as f: fig_data = f.read() self.dicts = yaml.load(fig_data) def get(self, key): if key in self.dicts.keys(): return self.dicts[key] else: raise ValueError("Not find this keyword.") if __name__ == "__main__": cfg = cfgData("EISeg/train/train_config.yaml") print(cfg.get("use_vdl")) ================================================ FILE: eiseg/util/exp_imports/default.py ================================================ import paddle from functools import partial from easydict import EasyDict as edict from albumentations import * from data.datasets import * from model.losses import * from data.transforms import * #from isegm.engine.trainer import ISTrainer from model.metrics import AdaptiveIoU from data.points_sampler import MultiPointSampler from model.initializer import XavierGluon from model.is_hrnet_model import HRNetModel from model.is_deeplab_model import DeeplabModel ================================================ FILE: eiseg/util/label.py ================================================ # Copyright (c) 2021 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 os.path as osp from . import colorMap class Label: def __init__(self, idx=None, name=None, color=None): self.idx = idx self.name = name self.color = color def __repr__(self): return f"{self.idx} {self.name} {self.color}" class LabelList(object): def __init__(self, labels: dict = None): self.labelList = [] if labels is not None: for lab in labels: color = lab.get("color", colorMap.get_color()) self.add(lab["id"], lab["name"], color) def add(self, idx, name, color): self.labelList.append(Label(idx, name, color)) def remove(self, index): for idx, lab in enumerate(self.labelList): if lab.idx == index: del self.labelList[idx] break # del self.labelList[index] def clear(self): self.labelList = [] def toint(self, seq): if isinstance(seq, list): for i in range(len(seq)): try: seq[i] = int(seq[i]) except ValueError: pass else: seq = int(seq) return seq def importLabel(self, path): if not osp.exists(path): return [] with open(path, "r", encoding="utf-8") as f: labels = f.readlines() labelList = [] for lab in labels: lab = lab.replace("\n", "").strip(" ").split(" ") if len(lab) != 5: # rm: and len(lab) != 2 print(f"{lab} 标签不合法") continue label = Label(self.toint(lab[0]), str(lab[1]), self.toint(lab[2:])) labelList.append(label) self.labelList = labelList def exportLabel(self, path): if not path or not osp.exists(osp.dirname(path)): print("label path don't exist") return with open(path, "w", encoding="utf-8") as f: for label in self.labelList: print(label.idx, end=" ", file=f) print(label.name, end=" ", file=f) for idx in range(3): print(label.color[idx], end=" ", file=f) print(file=f) def getLabelById(self, labelIdx): for lab in self.labelList: if lab.idx == labelIdx: return lab def __repr__(self): return str(self.labelList) def __getitem__(self, index): return self.labelList[index] def __len__(self): return len(self.labelList) @property def colors(self): cols = [] for lab in self.labelList: cols.append(lab.color) return cols ================================================ FILE: eiseg/util/language.py ================================================ # Copyright (c) 2021 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.path as osp import re from eiseg import pjpath from collections import defaultdict import json from urllib import parse import requests class TransUI(object): def __init__(self, is_trans=False): super().__init__() self.trans_dict = defaultdict(dict) with open(osp.join(pjpath, "config/zh_CN.EN"), "r", encoding="utf-8") as f: texts = f.readlines() for txt in texts: strs = txt.split("@") self.trans_dict[strs[0].strip()] = strs[1].strip() self.is_trans = is_trans self.youdao_url = "http://fanyi.youdao.com/translate?&doctype=json&type=AUTO&i=" def put(self, zh_CN): if self.is_trans == False: return zh_CN else: try: return str(self.trans_dict[zh_CN]) except: return zh_CN # 联网动态翻译 def tr(self, zh_CN): try: tr_url = self.youdao_url + parse.quote(zh_CN) response = requests.get(tr_url) js = json.loads(response.text) result_EN = js["translateResult"][0][0]["tgt"] return str(result_EN) except: return zh_CN ================================================ FILE: eiseg/util/manager.py ================================================ import inspect from collections.abc import Sequence class ComponentManager: def __init__(self, name=None): self._components_dict = dict() self._name = name def __len__(self): return len(self._components_dict) def __repr__(self): name_str = self._name if self._name else self.__class__.__name__ return "{}:{}".format(name_str, list(self._components_dict.keys())) def __getitem__(self, item): if isinstance(item, int): if item >= len(self): raise KeyError(f"指定的下标 {item} 在长度为 {len(self)} 的 {self} 中越界") return list(self._components_dict.values())[item] if item not in self._components_dict.keys(): raise KeyError(f"{self} 中不存在 {item}") return self._components_dict[item] def __iter__(self): for val in self._components_dict.values(): yield val def keys(self): return list(self._components_dict.keys()) def idx(self, item): for idx, val in enumerate(self.keys()): if val == item: return idx raise KeyError(f"{item} is not in {self}") @property def components_dict(self): return self._components_dict @property def name(self): return self._name def _add_single_component(self, component): # Currently only support class or function type if not (inspect.isclass(component) or inspect.isfunction(component)): raise TypeError( "Expect class/function type, but received {}".format(type(component)) ) # Obtain the internal name of the component component_name = component.__name__ # Check whether the component was added already if component_name in self._components_dict.keys(): raise KeyError("{} exists already!".format(component_name)) else: # Take the internal name of the component as its key self._components_dict[component_name] = component def add_component(self, components): # Check whether the type is a sequence if isinstance(components, Sequence): for component in components: self._add_single_component(component) else: component = components self._add_single_component(component) return components MODELS = ComponentManager("models") ACTIONS = ComponentManager("actions") ================================================ FILE: eiseg/util/misc.py ================================================ import paddle import numpy as np import pickle def get_dims_with_exclusion(dim, exclude=None): dims = list(range(dim)) if exclude is not None: dims.remove(exclude) return dims def save_checkpoint( net, checkpoints_path, epoch=None, prefix="", verbose=True, multi_gpu=False ): if epoch is None: checkpoint_name = "last_checkpoint.pdparams" else: checkpoint_name = f"{epoch:03d}.pdparams" if prefix: checkpoint_name = f"{prefix}_{checkpoint_name}" if not checkpoints_path.exists(): checkpoints_path.mkdir(parents=True) checkpoint_path = checkpoints_path / checkpoint_name net = net.module if multi_gpu else net # model_state = {'state_dict': net.state_dict(),'config': net.__dict__} paddle.save(net.state_dict(), checkpoint_path) def get_bbox_from_mask(mask): rows = np.any(mask, axis=1) cols = np.any(mask, axis=0) rmin, rmax = np.where(rows)[0][[0, -1]] cmin, cmax = np.where(cols)[0][[0, -1]] return rmin, rmax, cmin, cmax def expand_bbox(bbox, expand_ratio, min_crop_size=None): rmin, rmax, cmin, cmax = bbox rcenter = 0.5 * (rmin + rmax) ccenter = 0.5 * (cmin + cmax) height = expand_ratio * (rmax - rmin + 1) width = expand_ratio * (cmax - cmin + 1) if min_crop_size is not None: height = max(height, min_crop_size) width = max(width, min_crop_size) rmin = int(round(rcenter - 0.5 * height)) rmax = int(round(rcenter + 0.5 * height)) cmin = int(round(ccenter - 0.5 * width)) cmax = int(round(ccenter + 0.5 * width)) return rmin, rmax, cmin, cmax def clamp_bbox(bbox, rmin, rmax, cmin, cmax): return ( max(rmin, bbox[0]), min(rmax, bbox[1]), max(cmin, bbox[2]), min(cmax, bbox[3]), ) def get_bbox_iou(b1, b2): h_iou = get_segments_iou(b1[:2], b2[:2]) w_iou = get_segments_iou(b1[2:4], b2[2:4]) return h_iou * w_iou def get_segments_iou(s1, s2): a, b = s1 c, d = s2 intersection = max(0, min(b, d) - max(a, c) + 1) union = max(1e-6, max(b, d) - min(a, c) + 1) return intersection / union def get_labels_with_sizes(x): obj_sizes = np.bincount(x.flatten()) labels = np.nonzero(obj_sizes)[0].tolist() labels = [x for x in labels if x != 0] return labels, obj_sizes[labels].tolist() ================================================ FILE: eiseg/util/opath.py ================================================ import re # 检查中文 def check_cn(path): zh_model = re.compile(u'[\u4e00-\u9fa5]') return zh_model.search(path) # 替换斜杠 def normcase(path): return eval(repr(path).replace('\\\\', '/')) ================================================ FILE: eiseg/util/polygon.py ================================================ # Copyright (c) 2021 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. from enum import Enum import cv2 import numpy as np import math from .regularization import boundary_regularization class Instructions(Enum): No_Instruction = 0 Polygon_Instruction = 1 def get_polygon(label, sample="Dynamic", img_size=None, building=False): results = cv2.findContours( image=label, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_TC89_KCOS ) # 获取内外边界,用RETR_TREE更好表示 cv2_v = cv2.__version__.split(".")[0] contours = results[1] if cv2_v == "3" else results[0] # 边界 hierarchys = results[2] if cv2_v == "3" else results[1] # 隶属信息 if len(contours) != 0: # 可能出现没有边界的情况 polygons = [] relas = [] img_shape = label.shape for idx, (contour, hierarchy) in enumerate(zip(contours, hierarchys[0])): # print(hierarchy) # opencv实现边界简化 epsilon = ( 0.005 * cv2.arcLength(contour, True) if sample == "Dynamic" else sample ) if not isinstance(epsilon, float) and not isinstance(epsilon, int): epsilon = 0 # print("epsilon:", epsilon) if building is False: # -- Douglas-Peucker算法边界简化 contour = cv2.approxPolyDP(contour, epsilon / 10, True) else: # -- 建筑边界简化(https://github.com/niecongchong/RS-building-regularization) contour = boundary_regularization(contour, img_shape, epsilon) # -- 自定义(角度和距离)边界简化 out = approx_poly_DIY(contour) # 给出关系 rela = ( idx, # own hierarchy[-1] if hierarchy[-1] != -1 else None, ) # parent polygon = [] for p in out: polygon.append(p[0]) polygons.append(polygon) # 边界 relas.append(rela) # 关系 for i in range(len(relas)): if relas[i][1] != None: # 有父圈 for j in range(len(relas)): if relas[j][0] == relas[i][1]: # i的父圈就是j(i是j的子圈) if polygons[i] is not None and polygons[j] is not None: min_i, min_o = __find_min_point(polygons[i], polygons[j]) # 改变顺序 polygons[i] = __change_list(polygons[i], min_i) polygons[j] = __change_list(polygons[j], min_o) # 连接 if min_i != -1 and len(polygons[i]) > 0: polygons[j].extend(polygons[i]) # 连接内圈 polygons[i] = None polygons = list(filter(None, polygons)) # 清除加到外圈的内圈多边形 if img_size is not None: polygons = check_size_minmax(polygons, img_size) return polygons else: print("没有标签范围,无法生成边界") return None def __change_list(polygons, idx): if idx == -1: return polygons s_p = polygons[:idx] polygons = polygons[idx:] polygons.extend(s_p) polygons.append(polygons[0]) # 闭合圈 return polygons def __find_min_point(i_list, o_list): min_dis = 1e7 idx_i = -1 idx_o = -1 for i in range(len(i_list)): for o in range(len(o_list)): dis = math.sqrt( (i_list[i][0] - o_list[o][0]) ** 2 + (i_list[i][1] - o_list[o][1]) ** 2 ) if dis <= min_dis: min_dis = dis idx_i = i idx_o = o return idx_i, idx_o # 根据三点坐标计算夹角 def __cal_ang(p1, p2, p3): eps = 1e-12 a = math.sqrt((p2[0] - p3[0]) * (p2[0] - p3[0]) + (p2[1] - p3[1]) * (p2[1] - p3[1])) b = math.sqrt((p1[0] - p3[0]) * (p1[0] - p3[0]) + (p1[1] - p3[1]) * (p1[1] - p3[1])) c = math.sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1])) ang = math.degrees( math.acos((b ** 2 - a ** 2 - c ** 2) / (-2 * a * c + eps)) ) # p2对应 return ang # 计算两点距离 def __cal_dist(p1, p2): return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) # 边界点简化 def approx_poly_DIY(contour, min_dist=10, ang_err=5): # print(contour.shape) # N, 1, 2 cs = [contour[i][0] for i in range(contour.shape[0])] ## 1. 先删除两个相近点与前后两个点角度接近的点 i = 0 while i < len(cs): try: j = (i + 1) if (i != len(cs) - 1) else 0 if __cal_dist(cs[i], cs[j]) < min_dist: last = (i - 1) if (i != 0) else (len(cs) - 1) next = (j + 1) if (j != len(cs) - 1) else 0 ang_i = __cal_ang(cs[last], cs[i], cs[next]) ang_j = __cal_ang(cs[last], cs[j], cs[next]) # print(ang_i, ang_j) # 角度值为-180到+180 if abs(ang_i - ang_j) < ang_err: # 删除距离两点小的 dist_i = __cal_dist(cs[last], cs[i]) + __cal_dist(cs[i], cs[next]) dist_j = __cal_dist(cs[last], cs[j]) + __cal_dist(cs[j], cs[next]) if dist_j < dist_i: del cs[j] else: del cs[i] else: i += 1 else: i += 1 except: i += 1 ## 2. 再删除夹角接近180度的点 i = 0 while i < len(cs): try: last = (i - 1) if (i != 0) else (len(cs) - 1) next = (i + 1) if (i != len(cs) - 1) else 0 ang_i = __cal_ang(cs[last], cs[i], cs[next]) if abs(ang_i) > (180 - ang_err): del cs[i] else: i += 1 except: # i += 1 del cs[i] res = np.array(cs).reshape([-1, 1, 2]) return res def check_size_minmax(polygons, img_size): h_max, w_max = img_size for ps in polygons: for j in range(len(ps)): x, y = ps[j] if x < 0: x = 0 elif x > w_max: x = w_max if y < 0: y = 0 elif y > h_max: y = h_max ps[j] = np.array([x, y]) return polygons ================================================ FILE: eiseg/util/qt.py ================================================ from math import sqrt import os.path as osp import numpy as np from eiseg import pjpath from qtpy import QtCore from qtpy import QtGui from qtpy import QtWidgets from .config import parse_configs shortcuts = parse_configs(osp.join(pjpath, "config/config.yaml"))["shortcut"] here = osp.dirname(osp.abspath(__file__)) def newIcon(icon): if isinstance(icon, list) or isinstance(icon, tuple): pixmap = QtGui.QPixmap(100, 100) c = icon pixmap.fill(QtGui.QColor(c[0], c[1], c[2])) return QtGui.QIcon(pixmap) icons_dir = osp.join(here, "../resource") return QtGui.QIcon(osp.join(":/", icons_dir, f"{icon}.png")) def newButton(text, icon=None, slot=None): b = QtWidgets.QPushButton(text) if icon is not None: b.setIcon(newIcon(icon)) if slot is not None: b.clicked.connect(slot) return b def newAction( parent, text, slot=None, shortcutName=None, icon=None, tip=None, checkable=False, enabled=True, checked=False, ): """Create a new action and assign callbacks, shortcuts, etc.""" a = QtWidgets.QAction(text, parent) a.setData(shortcutName) # a = QtWidgets.QAction("", parent) if icon is not None: a.setIconText(text.replace(" ", "\n")) a.setIcon(newIcon(icon)) shortcut = shortcuts.get(shortcutName, None) if shortcut is not None: if isinstance(shortcut, (list, tuple)): a.setShortcuts(shortcut) else: a.setShortcut(shortcut) if tip is not None: a.setToolTip(tip) a.setStatusTip(tip) if slot is not None: a.triggered.connect(slot) if checkable: a.setCheckable(True) a.setEnabled(enabled) a.setChecked(checked) return a def addActions(widget, actions): for action in actions: if action is None: widget.addSeparator() elif isinstance(action, QtWidgets.QMenu): widget.addMenu(action) else: widget.addAction(action) def labelValidator(): return QtGui.QRegExpValidator(QtCore.QRegExp(r"^[^ \t].+"), None) class struct(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) def __len__(self): return len(self.__dict__) def append(self, action): if isinstance(action, QtWidgets.QAction): self.__dict__.update({action.data(): action}) def __iter__(self): return list(self.__dict__.values()).__iter__() def __getitem__(self, idx): return list(self.__dict__.values())[idx] def get(self, name): return self.__dict__[name] def fmtShortcut(text): mod, key = text.split("+", 1) return "%s+%s" % (mod, key) ================================================ FILE: eiseg/util/regularization/__init__.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ from .rs_regularization import boundary_regularization ================================================ FILE: eiseg/util/regularization/cal_line.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ import numpy as np # 线生成函数 def line(p1, p2): A = (p1[1] - p2[1]) B = (p2[0] - p1[0]) C = (p1[0] * p2[1] - p2[0] * p1[1]) return A, B, -C # 计算两条直线之间的交点 def intersection(L1, L2): D = L1[0] * L2[1] - L1[1] * L2[0] Dx = L1[2] * L2[1] - L1[1] * L2[2] Dy = L1[0] * L2[2] - L1[2] * L2[0] if D != 0: x = Dx / D y = Dy / D return x, y else: return False # 计算两个平行线之间的距离 def par_line_dist(L1, L2): A1, B1, C1 = L1 A2, B2, C2 = L2 new_A1 = 1 new_B1 = B1 / A1 new_C1 = C1 / A1 new_A2 = 1 new_B2 = B2 / A2 new_C2 = C2 / A2 dist = (np.abs(new_C1 - new_C2)) / (np.sqrt(new_A2**2 + new_B2**2)) return dist # 计算点在直线的投影位置 def point_in_line(m, n, x1, y1, x2, y2): x = (m * (x2 - x1) * (x2 - x1) + n * (y2 - y1) * (x2 - x1) + (x1 * y2 - x2 * y1) * (y2 - y1)) / ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) y = (m * (x2 - x1) * (y2 - y1) + n * (y2 - y1) * (y2 - y1) + (x2 * y1 - x1 * y2) * (x2 - x1)) / ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) return (x, y) ================================================ FILE: eiseg/util/regularization/cal_point.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ import numpy as np import math # 计算两点距离 def cal_dist(point_1, point_2): dist = np.sqrt(np.sum(np.power((point_1-point_2), 2))) return dist # 计算两条线的夹角 def cal_ang(point_1, point_2, point_3): def _cal_pp(p_1, p_2): return math.sqrt((p_1[0] - p_2[0]) * (p_1[0] - p_2[0]) + (p_1[1] - p_2[1]) * (p_1[1] - p_2[1])) a = _cal_pp(point_2, point_3) b = _cal_pp(point_1, point_3) c = _cal_pp(point_1, point_2) B = math.degrees(math.acos((b**2 - a**2 - c**2) / (-2 * a * c))) return B # 计算线条的方位角 def cal_azimuth(point_0, point_1): x1, y1 = point_0 x2, y2 = point_1 if x1 < x2: if y1 < y2: ang = math.atan((y2 - y1) / (x2 - x1)) ang = ang * 180 / math.pi return ang elif y1 > y2: ang = math.atan((y1 - y2) / (x2 - x1)) ang = ang * 180 / math.pi return 90 + (90 - ang) elif y1==y2: return 0 elif x1 > x2: if y1 < y2: ang = math.atan((y2-y1)/(x1-x2)) ang = ang*180/math.pi return 90+(90-ang) elif y1 > y2: ang = math.atan((y1-y2)/(x1-x2)) ang = ang * 180 / math.pi return ang elif y1==y2: return 0 elif x1==x2: return 90 ================================================ FILE: eiseg/util/regularization/rdp_alg.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ """ rdp ~~~ Pure Python implementation of the Ramer-Douglas-Peucker algorithm. :copyright: (c) 2014 Fabian Hirschmann :license: MIT, see LICENSE.txt for more details. """ import numpy as np def pldist(x0, x1, x2): """ Calculates the distance from the point ``x0`` to the line given by the points ``x1`` and ``x2``. :param x0: a point :type x0: a 2x1 numpy array :param x1: a point of the line :type x1: 2x1 numpy array :param x2: another point of the line :type x2: 2x1 numpy array """ x0, x1, x2 = x0[:2], x1[:2], x2[:2] # discard timestamp if x1[0] == x2[0]: return np.abs(x0[0] - x1[0]) return np.divide(np.linalg.norm(np.linalg.det([x2 - x1, x1 - x0])), np.linalg.norm(x2 - x1)) def _rdp(M, epsilon, dist): """ Simplifies a given array of points. :param M: an array :type M: Nx2 numpy array :param epsilon: epsilon in the rdp algorithm :type epsilon: float :param dist: distance function :type dist: function with signature ``f(x1, x2, x3)`` """ dmax = 0.0 index = -1 for i in range(1, M.shape[0]): d = dist(M[i], M[0], M[-1]) if d > dmax: index = i dmax = d if dmax > epsilon: r1 = _rdp(M[:index + 1], epsilon, dist) r2 = _rdp(M[index:], epsilon, dist) return np.vstack((r1[:-1], r2)) else: return np.vstack((M[0], M[-1])) def _rdp_nn(seq, epsilon, dist): """ Simplifies a given array of points. :param seq: a series of points :type seq: sequence of 2-tuples :param epsilon: epsilon in the rdp algorithm :type epsilon: float :param dist: distance function :type dist: function with signature ``f(x1, x2, x3)`` """ return _rdp(np.array(seq), epsilon, dist).tolist() def rdp(M, epsilon=0, dist=pldist): """ Simplifies a given array of points. :param M: a series of points :type M: either a Nx2 numpy array or sequence of 2-tuples :param epsilon: epsilon in the rdp algorithm :type epsilon: float :param dist: distance function :type dist: function with signature ``f(x1, x2, x3)`` """ if "numpy" in str(type(M)): return _rdp(M, epsilon, dist) return _rdp_nn(M, epsilon, dist) ================================================ FILE: eiseg/util/regularization/rotate_ang.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ import math # 顺时针旋转 def Nrotation_angle_get_coor_coordinates(point, center, angle): src_x, src_y = point center_x, center_y = center radian = math.radians(angle) dest_x = (src_x - center_x) * math.cos(radian) + \ (src_y - center_y) * math.sin(radian) + center_x dest_y = (src_y - center_y) * math.cos(radian) - \ (src_x - center_x) * math.sin(radian) + center_y # return (int(dest_x), int(dest_y)) return (dest_x, dest_y) # 逆时针旋转 def Srotation_angle_get_coor_coordinates(point, center, angle): src_x, src_y = point center_x, center_y = center radian = math.radians(angle) dest_x = (src_x - center_x) * math.cos(radian) - \ (src_y - center_y) * math.sin(radian) + center_x dest_y = (src_x - center_x) * math.sin(radian) + \ (src_y - center_y) * math.cos(radian) + center_y # return [int(dest_x), int(dest_y)] return (dest_x, dest_y) ================================================ FILE: eiseg/util/regularization/rs_regularization.py ================================================ # Copyright (c) 2021 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. """ This code is based on https://github.com/niecongchong/RS-building-regularization Ths copyright of niecongchong/RS-building-regularization is as follows: Apache License [see LICENSE for details] """ import cv2 import matplotlib.pyplot as plt import numpy as np from .rdp_alg import rdp from .cal_point import cal_ang, cal_dist, cal_azimuth from .rotate_ang import Nrotation_angle_get_coor_coordinates, Srotation_angle_get_coor_coordinates from .cal_line import line, intersection, par_line_dist, point_in_line def boundary_regularization(contours, img_shape, epsilon=6): h, w = img_shape[0:2] # 轮廓定位 contours = np.squeeze(contours) # 轮廓精简DP contours = rdp(contours, epsilon=epsilon) contours[:, 1] = h - contours[:, 1] # 轮廓规则化 dists = [] azis = [] azis_index = [] # 获取每条边的长度和方位角 for i in range(contours.shape[0]): cur_index = i next_index = i + 1 if i < contours.shape[0] - 1 else 0 prev_index = i - 1 cur_point = contours[cur_index] nest_point = contours[next_index] prev_point = contours[prev_index] dist = cal_dist(cur_point, nest_point) azi = cal_azimuth(cur_point, nest_point) dists.append(dist) azis.append(azi) azis_index.append([cur_index, next_index]) # 以最长的边的方向作为主方向 longest_edge_idex = np.argmax(dists) main_direction = azis[longest_edge_idex] # 方向纠正,绕中心点旋转到与主方向垂直或者平行 correct_points = [] para_vetr_idxs = [] # 0平行 1垂直 for i, (azi, (point_0_index, point_1_index)) in enumerate(zip(azis, azis_index)): if i == longest_edge_idex: correct_points.append([contours[point_0_index], contours[point_1_index]]) para_vetr_idxs.append(0) else: # 确定旋转角度 rotate_ang = main_direction - azi if np.abs(rotate_ang) < 180 / 4: rotate_ang = rotate_ang para_vetr_idxs.append(0) elif np.abs(rotate_ang) >= 90 - 180 / 4: rotate_ang = rotate_ang + 90 para_vetr_idxs.append(1) # 执行旋转任务 point_0 = contours[point_0_index] point_1 = contours[point_1_index] point_middle = (point_0 + point_1) / 2 if rotate_ang > 0: rotate_point_0 = Srotation_angle_get_coor_coordinates( point_0, point_middle, np.abs(rotate_ang)) rotate_point_1 = Srotation_angle_get_coor_coordinates( point_1, point_middle, np.abs(rotate_ang)) elif rotate_ang < 0: rotate_point_0 = Nrotation_angle_get_coor_coordinates( point_0, point_middle, np.abs(rotate_ang)) rotate_point_1 = Nrotation_angle_get_coor_coordinates( point_1, point_middle, np.abs(rotate_ang)) else: rotate_point_0 = point_0 rotate_point_1 = point_1 correct_points.append([rotate_point_0, rotate_point_1]) correct_points = np.array(correct_points) # 相邻边校正,垂直取交点,平行平移短边或者加线 final_points = [] final_points.append(correct_points[0][0]) for i in range(correct_points.shape[0] - 1): cur_index = i next_index = i + 1 if i < correct_points.shape[0] - 1 else 0 cur_edge_point_0 = correct_points[cur_index][0] cur_edge_point_1 = correct_points[cur_index][1] next_edge_point_0 = correct_points[next_index][0] next_edge_point_1 = correct_points[next_index][1] cur_para_vetr_idx = para_vetr_idxs[cur_index] next_para_vetr_idx = para_vetr_idxs[next_index] if cur_para_vetr_idx != next_para_vetr_idx: # 垂直取交点 L1 = line(cur_edge_point_0, cur_edge_point_1) L2 = line(next_edge_point_0, next_edge_point_1) point_intersection = intersection(L1, L2) final_points.append(point_intersection) elif cur_para_vetr_idx == next_para_vetr_idx: # 平行分两种,一种加短线,一种平移,取决于距离阈值 L1 = line(cur_edge_point_0, cur_edge_point_1) L2 = line(next_edge_point_0, next_edge_point_1) marg = par_line_dist(L1, L2) if marg < 3: # 平移 point_move = point_in_line(next_edge_point_0[0], next_edge_point_0[1], cur_edge_point_0[0], cur_edge_point_0[1], cur_edge_point_1[0], cur_edge_point_1[1]) final_points.append(point_move) # 更新平移之后的下一条边 correct_points[next_index][0] = point_move correct_points[next_index][1] = point_in_line( next_edge_point_1[0], next_edge_point_1[1], cur_edge_point_0[0], cur_edge_point_0[1], cur_edge_point_1[0], cur_edge_point_1[1]) else: # 加线 add_mid_point = (cur_edge_point_1 + next_edge_point_0) / 2 add_point_1 = point_in_line(add_mid_point[0], add_mid_point[1], cur_edge_point_0[0], cur_edge_point_0[1], cur_edge_point_1[0], cur_edge_point_1[1]) add_point_2 = point_in_line(add_mid_point[0], add_mid_point[1], next_edge_point_0[0], next_edge_point_0[1], next_edge_point_1[0], next_edge_point_1[1]) final_points.append(add_point_1) final_points.append(add_point_2) final_points.append(final_points[0]) final_points = np.array(final_points) final_points[:, 1] = h - final_points[:, 1] final_points = final_points[np.newaxis, :].transpose((1, 0, 2)) return final_points # def rs_build_re(mask): # # 中值滤波,去噪 # ori_img = cv2.medianBlur(mask, 5) # ori_img = cv2.cvtColor(ori_img, cv2.COLOR_BGR2GRAY) # ret, ori_img = cv2.threshold(ori_img, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # # 连通域分析 # num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(ori_img, connectivity=8) # # 遍历联通域 # for i in range(1, num_labels): # img = np.zeros_like(labels) # index = np.where(labels==i) # img[index] = 255 # img = np.array(img, dtype=np.uint8) # regularization_contour = boundary_regularization(img).astype(np.int32) # cv2.polylines(img=mask, pts=[regularization_contour], isClosed=True, color=(255, 0, 0), thickness=5) # single_out = np.zeros_like(mask) # cv2.polylines(img=single_out, pts=[regularization_contour], isClosed=True, color=(255, 0, 0), thickness=5) # cv2.imwrite('single_out_{}.jpg'.format(i), single_out) ================================================ FILE: eiseg/util/serialization.py ================================================ from functools import wraps from copy import deepcopy import inspect import paddle.nn as nn def serialize(init): parameters = list(inspect.signature(init).parameters) @wraps(init) def new_init(self, *args, **kwargs): params = deepcopy(kwargs) for pname, value in zip(parameters[1:], args): params[pname] = value config = {"class": get_classname(self.__class__), "params": dict()} specified_params = set(params.keys()) for pname, param in get_default_params(self.__class__).items(): if pname not in params: params[pname] = param.default for name, value in list(params.items()): param_type = "builtin" if inspect.isclass(value): param_type = "class" value = get_classname(value) config["params"][name] = { "type": param_type, "value": value, "specified": name in specified_params, } setattr(self, "_config", config) init(self, *args, **kwargs) return new_init def load_model(config, **kwargs): model_class = get_class_from_str(config["class"]) model_default_params = get_default_params(model_class) model_args = dict() for pname, param in config["params"].items(): value = param["value"] if param["type"] == "class": value = get_class_from_str(value) if pname not in model_default_params and not param["specified"]: continue assert pname in model_default_params if not param["specified"] and model_default_params[pname].default == value: continue model_args[pname] = value model_args.update(kwargs) return model_class(**model_args) def get_config_repr(config): config_str = f'Model: {config["class"]}\n' for pname, param in config["params"].items(): value = param["value"] if param["type"] == "class": value = value.split(".")[-1] param_str = f"{pname:<22} = {str(value):<12}" if not param["specified"]: param_str += " (default)" config_str += param_str + "\n" return config_str def get_default_params(some_class): params = dict() for mclass in some_class.mro(): if mclass is nn.Layer or mclass is object: continue mclass_params = inspect.signature(mclass.__init__).parameters for pname, param in mclass_params.items(): if param.default != param.empty and pname not in params: params[pname] = param return params def get_classname(cls): module = cls.__module__ name = cls.__qualname__ if module is not None and module != "__builtin__": name = module + "." + name return name def get_class_from_str(class_str): components = class_str.split(".") mod = __import__(".".join(components[:-1])) for comp in components[1:]: mod = getattr(mod, comp) return mod ================================================ FILE: eiseg/util/vis.py ================================================ from functools import lru_cache import cv2 import numpy as np def visualize_instances( imask, bg_color=255, boundaries_color=None, boundaries_width=1, boundaries_alpha=0.8 ): num_objects = imask.max() + 1 palette = get_palette(num_objects) if bg_color is not None: palette[0] = bg_color result = palette[imask].astype(np.uint8) if boundaries_color is not None: boundaries_mask = get_boundaries(imask, boundaries_width=boundaries_width) tresult = result.astype(np.float32) tresult[boundaries_mask] = boundaries_color tresult = tresult * boundaries_alpha + (1 - boundaries_alpha) * result result = tresult.astype(np.uint8) return result @lru_cache(maxsize=16) def get_palette(num_cls): return np.array([[0, 0, 0], [128, 0, 0], [0, 128, 0], [0, 0, 128]]) def visualize_mask(mask, num_cls): palette = get_palette(num_cls) mask[mask == -1] = 0 return palette[mask].astype(np.uint8) def visualize_proposals(proposals_info, point_color=(255, 0, 0), point_radius=1): proposal_map, colors, candidates = proposals_info proposal_map = draw_probmap(proposal_map) for x, y in candidates: proposal_map = cv2.circle(proposal_map, (y, x), point_radius, point_color, -1) return proposal_map def draw_probmap(x): return cv2.applyColorMap((x * 255).astype(np.uint8), cv2.COLORMAP_HOT) def draw_points(image, points, color, radius=3): image = image.copy() for p in points: image = cv2.circle(image, (int(p[1]), int(p[0])), radius, color, -1) return image def draw_instance_map(x, palette=None): num_colors = x.max() + 1 if palette is None: palette = get_palette(num_colors) return palette[x].astype(np.uint8) def blend_mask(image, mask, alpha=0.6): if mask.min() == -1: mask = mask.copy() + 1 imap = draw_instance_map(mask) result = (image * (1 - alpha) + alpha * imap).astype(np.uint8) return result def get_boundaries(instances_masks, boundaries_width=1): boundaries = np.zeros( (instances_masks.shape[0], instances_masks.shape[1]), dtype=np.bool ) for obj_id in np.unique(instances_masks.flatten()): if obj_id == 0: continue obj_mask = instances_masks == obj_id kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) inner_mask = cv2.erode( obj_mask.astype(np.uint8), kernel, iterations=boundaries_width ).astype(np.bool) obj_boundary = np.logical_xor(obj_mask, np.logical_and(inner_mask, obj_mask)) boundaries = np.logical_or(boundaries, obj_boundary) return boundaries def draw_with_blend_and_clicks( img, mask=None, alpha=0.6, clicks_list=None, pos_color=(0, 255, 0), neg_color=(255, 0, 0), radius=4, palette=None, ): result = img.copy() if mask is not None: if not palette: palette = get_palette(np.max(mask) + 1) palette = np.array(palette) rgb_mask = palette[mask.astype(np.uint8)] mask_region = (mask > 0).astype(np.uint8) result = ( result * (1 - mask_region[:, :, np.newaxis]) + (1 - alpha) * mask_region[:, :, np.newaxis] * result + alpha * rgb_mask ) result = result.astype(np.uint8) if clicks_list is not None and len(clicks_list) > 0: pos_points = [click.coords for click in clicks_list if click.is_positive] neg_points = [click.coords for click in clicks_list if not click.is_positive] result = draw_points(result, pos_points, pos_color, radius=radius) result = draw_points(result, neg_points, neg_color, radius=radius) return result ================================================ FILE: eiseg/widget/__init__.py ================================================ from .shortcut import ShortcutWidget from .loading import LoadingWidget from .line import LineItem from .grip import GripItem from .bbox import BBoxAnnotation from .polygon import PolygonAnnotation from .scene import AnnotationScene from .view import AnnotationView from .create import ( create_text, create_button, create_slider, DockWidget, creat_dock ) from .table import TableWidget ================================================ FILE: eiseg/widget/bbox.py ================================================ # Copyright (c) 2021 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. from qtpy import QtWidgets, QtGui, QtCore from qtpy.QtCore import Qt # Note, bbox annotation is more convenient than the default boundingBox generated by QGrpaphicItem class BBoxAnnotation(QtWidgets.QGraphicsPathItem): def __init__( self, labelIndex, polyline, borderColor=[0, 0, 255], cocoIndex=None, parent=None, ): super(BBoxAnnotation, self).__init__(parent) self.polyline = polyline self.corner_points = [] self.upper_right = QtCore.QPointF() self.bottom_left = QtCore.QPointF() self.w = -1.0 self.h = -1.0 self.parent = parent self.is_added = False if self.parent is not None: self.is_added = True self.labelIndex = labelIndex self.coco_id = cocoIndex self.bbox_hovering = True # set rendering attributes self.setZValue(10) # b = borderColor # self.borderColor = QtGui.QColor(b[0], b[1], b[2]) self.borderColor = QtGui.QColor(128, 128, 128) self.borderColor.setAlphaF(0.8) pen = QtGui.QPen(self.borderColor, 1.2) pen.setStyle(Qt.DashDotLine) self.setPen(pen) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, False) self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False) self.setAcceptHoverEvents(False) # self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) @property def scnenePoints(self): # return 4 corner points raise Exception("Not Implemented Yet!") def setAnning(self, isAnning=True): raise Exception("Not Implemented Yet!") def remove(self): raise Exception("Not Implemented Yet!") # ===== generate geometry info def create_corners(self): bbox_rect_geo = self.polyline.boundingRect() self.bottom_left = bbox_rect_geo.bottomLeft() self.upper_right = bbox_rect_geo.topRight() self.corner_points.clear() self.corner_points.extend( [ self.bottom_left, bbox_rect_geo.topLeft(), self.upper_right, bbox_rect_geo.bottomRight(), ] ) self.w = self.corner_points[3].x() - self.corner_points[1].x() self.h = self.corner_points[3].y() - self.corner_points[1].y() if self.corner_points[1].x() > 512 or self.corner_points[1].x() + self.w > 512: pass if self.corner_points[1].y() > 512 or self.corner_points[1].y() + self.h > 512: pass return self.corner_points def create_lines(self): pass # ===== graphic interface to update in scene tree def update(self): l = len(self.polyline.points) # print("up L:", l, " is_added:", self.is_added) if l < 3: if self.is_added: self.remove_from_scene() else: # 大于三个点就可以更新,小于三个点删除多边形 if self.is_added: self.add_to_scene() else: path_geo = QtGui.QPainterPath() self.create_corners() path_geo.moveTo(self.corner_points[0]) for i in range(4): path_geo.lineTo(self.corner_points[(i + 1) % 4]) self.setPath(QtGui.QPainterPath(path_geo)) pass pass pass def add_to_scene(self): # self.parentItem().scene().addItem(self) self.setParentItem(self.parent) self.is_added = True def remove_from_scene(self): # self.parentItem().scene().removeItem(self) self.setParentItem(None) self.is_added = False # ===== annotation info # @return : [x, y, w, h] def to_array(self): np_array = [ self._round(self.corner_points[1].x()), self._round(self.corner_points[1].y()), # topLeft self._round(self.w), self._round(self.h), ] return np_array def _round(self, number, ind=0): nint, ndec = str(number).split(".") res = float(nint + "." + ndec[:ind]) if res <= 0: res = 0.0 return res def __del__(self): self.corner_points.clear() ================================================ FILE: eiseg/widget/create.py ================================================ # Copyright (c) 2021 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. from qtpy.QtWidgets import QDockWidget from qtpy import QtCore, QtGui, QtWidgets from qtpy.QtCore import Qt ## 创建文本 def create_text(parent, text_name=None, text_text=None): text = QtWidgets.QLabel(parent) if text_name is not None: text.setObjectName(text_name) if text_text is not None: text.setText(text_text) return text ## 创建可编辑文本 def create_edit(parent, text_name=None, text_text=None): edit = QtWidgets.QLineEdit(parent) if text_name is not None: edit.setObjectName(text_name) if text_text is not None: edit.setText(text_text) edit.setValidator(QtGui.QIntValidator()) edit.setMaxLength(5) return edit ## 创建按钮 def create_button(parent, btn_name, btn_text, ico_path=None, curt=None): # 创建和设置按钮 sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed ) min_size = QtCore.QSize(0, 40) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) btn = QtWidgets.QPushButton(parent) sizePolicy.setHeightForWidth(btn.sizePolicy().hasHeightForWidth()) btn.setSizePolicy(sizePolicy) btn.setMinimumSize(min_size) btn.setObjectName(btn_name) if ico_path is not None: btn.setIcon(QtGui.QIcon(ico_path)) btn.setText(btn_text) if curt is not None: btn.setShortcut(curt) return btn ## 创建滑块区域 def create_slider( parent, sld_name, text_name, text, default_value=50, max_value=100, min_value=0, text_rate=0.01, edit=False ): Region = QtWidgets.QHBoxLayout() lab = create_text(parent, None, text) Region.addWidget(lab) if edit is False: labShow = create_text(parent, text_name, str(default_value * text_rate)) else: labShow = create_edit(parent, text_name, str(default_value * text_rate)) Region.addWidget(labShow) Region.setStretch(0, 1) Region.setStretch(1, 10) sld = QtWidgets.QSlider(parent) sld.setMaximum(max_value) # 好像只能整数的,这里是扩大了10倍,1 . 10 sld.setMinimum(min_value) sld.setProperty("value", default_value) sld.setOrientation(QtCore.Qt.Horizontal) sld.setObjectName(sld_name) sld.setStyleSheet( """ QSlider::sub-page:horizontal { background: #9999F1 } QSlider::handle:horizontal { background: #3334E3; width: 12px; border-radius: 4px; } """ ) sld.textLab = labShow return sld, labShow, Region class DockWidget(QDockWidget): def __init__(self, parent, name, text): super().__init__(parent=parent) self.setObjectName(name) self.setAllowedAreas(Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea) # 感觉不给关闭好点。可以在显示里面取消显示。有关闭的话显示里面的enable还能判断修改,累了 self.setFeatures( QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable ) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.setMinimumWidth(230) self.setWindowTitle(text) self.setStyleSheet("QDockWidget { background-color:rgb(204,204,248); }") self.topLevelChanged.connect(self.changeBackColor) def changeBackColor(self, isFloating): if isFloating: self.setStyleSheet("QDockWidget { background-color:rgb(255,255,255); }") else: self.setStyleSheet("QDockWidget { background-color:rgb(204,204,248); }") ## 创建dock def creat_dock(parent, name, text, widget): dock = DockWidget(parent, name, text) dock.setMinimumWidth(300) # Uniform size dock.setWidget(widget) return dock ================================================ FILE: eiseg/widget/grip.py ================================================ # Copyright (c) 2021 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. from PyQt5.QtCore import QPointF from qtpy import QtWidgets, QtGui, QtCore class GripItem(QtWidgets.QGraphicsPathItem): maxSize = 1.5 minSize = 0.8 def __init__(self, annotation_item, index, color, img_size): super(GripItem, self).__init__() self.m_annotation_item = annotation_item self.hovering = False self.m_index = index self.anning = True color.setAlphaF(1) self.color = color self.img_size = img_size self.updateSize() self.setPath(self.circle) self.setBrush(self.color) self.setPen(QtGui.QPen(self.color, 1)) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True) self.setAcceptHoverEvents(True) self.setZValue(12) self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) def setColor(self, color): self.setBrush(color) self.setPen(QtGui.QPen(color, 1)) self.color = color def setAnning(self, anning=True): self.anning = anning self.setEnabled(anning) # BUG: Scaling causes a crash @property def size(self): return GripItem.minSize # if not self.scene(): # return GripItem.minSize # else: # maxi, mini = GripItem.maxSize, GripItem.minSize # exp = 1 - mini / maxi # size = maxi * (1 - exp ** self.scene().scale) # if size > GripItem.maxSize: # size = GripItem.maxSize # if size < GripItem.minSize: # size = GripItem.minSize # return size def updateSize(self, s=2): size = self.size self.circle = QtGui.QPainterPath() self.circle.addEllipse(QtCore.QRectF(-size, -size, size * s, size * s)) self.square = QtGui.QPainterPath() self.square.addRect(QtCore.QRectF(-size, -size, size * s, size * s)) self.setPath(self.square if self.hovering else self.circle) def hoverEnterEvent(self, ev): self.setPath(self.square) self.setBrush(QtGui.QColor(0, 0, 0, 0)) self.m_annotation_item.item_hovering = True self.hovring = True super(GripItem, self).hoverEnterEvent(ev) def hoverLeaveEvent(self, ev): self.setPath(self.circle) self.setBrush(self.color) self.m_annotation_item.item_hovering = False self.hovring = False super(GripItem, self).hoverLeaveEvent(ev) def mouseReleaseEvent(self, ev): self.setSelected(False) super(GripItem, self).mouseReleaseEvent(ev) def itemChange(self, change, value): tmp_val = value if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled(): if value.x() > self.img_size[1]: x = self.img_size[1] elif value.x() < 0: x = 0 else: x = value.x() if value.y() > self.img_size[0]: y = self.img_size[0] elif value.y() < 0: y = 0 else: y = value.y() tmp_val = QPointF(x, y) self.m_annotation_item.movePoint(self.m_index, tmp_val) self.m_annotation_item.setDirty(True) return super(GripItem, self).itemChange(change, tmp_val) def shape(self): path = QtGui.QPainterPath() p = self.mapFromScene(self.pos()) x, y = p.x(), p.y() s = self.size path.addEllipse(p, s + GripItem.minSize, s + GripItem.minSize) return path def mouseDoubleClickEvent(self, ev): self.m_annotation_item.removeFocusPoint() ================================================ FILE: eiseg/widget/line.py ================================================ # Copyright (c) 2021 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. from qtpy import QtWidgets, QtGui, QtCore class LineItem(QtWidgets.QGraphicsLineItem): maxWidth = 1 minWidth = 0.5 def __init__(self, annotation_item, idx, color): super(LineItem, self).__init__() self.polygon_item = annotation_item self.idx = idx self.color = color self.anning = True self.setPen(QtGui.QPen(color, self.width)) self.setZValue(11) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True) # self.setFlag(QtWidgets.QGraphicsItem.ItemClipsToShape, True) self.setAcceptHoverEvents(True) self.setBoundingRegionGranularity(0.5) self.updateWidth() def setColor(self, color): self.setPen(QtGui.QPen(color, self.width)) self.color = color def setAnning(self, anning=True): self.anning = anning self.setEnabled(anning) self.updateWidth() # BUG: Scaling causes a crash @property def width(self): return LineItem.minWidth # if not self.scene(): # width = LineItem.minWidth # else: # maxi, mini = LineItem.maxWidth, LineItem.minWidth # exp = 1 - mini / maxi # width = maxi * (1 - exp ** self.scene().scale) # if width > LineItem.maxWidth: # width = LineItem.maxWidth # if width < LineItem.minWidth: # width = LineItem.minWidth # return width def updateWidth(self): self.setPen(QtGui.QPen(self.color, self.width)) def hoverEnterEvent(self, ev): self.boundingPolygon(True) print("hover in") if self.anning: self.polygon_item.line_hovering = True self.setPen(QtGui.QPen(self.color, self.width * 1.4)) super(LineItem, self).hoverEnterEvent(ev) def hoverLeaveEvent(self, ev): self.polygon_item.line_hovering = False self.setPen(QtGui.QPen(self.color, self.width)) super(LineItem, self).hoverLeaveEvent(ev) def mouseDoubleClickEvent(self, ev): print("anning", self.anning) if self.anning: self.setPen(QtGui.QPen(self.color, self.width)) self.polygon_item.addPointMiddle(self.idx, ev.pos()) super(LineItem, self).mouseDoubleClickEvent(ev) def shape(self): path = QtGui.QPainterPath() path.addPolygon(self.boundingPolygon(False)) return path # def shape(self): # path = QtGui.QPainterPath() # path.moveTo(self.line().p1()) # path.lineTo(self.line().p2()) # path.setPen(QtGui.QPen(self.color, self.width * 3)) # return path def boundingPolygon(self, debug): w = self.width * 1.5 w = min(w, 2) s, e = self.line().p1(), self.line().p2() dir = s - e dx, dy = -dir.y(), dir.x() norm = (dx ** 2 + dy ** 2) ** (1 / 2) if debug: print( self.width, w, s.x(), s.y(), e.x(), e.y(), dir.x(), dir.y(), dx, dy, norm, ) dx /= (norm + 1e-16) dy /= (norm + 1e-16) if debug: print("dir", dx, dy) p = QtCore.QPointF(dx * w, dy * w) poly = QtGui.QPolygonF([s - p, s + p, e + p, e - p]) return poly ================================================ FILE: eiseg/widget/loading.py ================================================ # Copyright (c) 2021 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.path as osp from qtpy.QtWidgets import QWidget, QLabel, QHBoxLayout from qtpy.QtGui import QIcon, QMovie from qtpy import QtCore from eiseg import pjpath class LoadingWidget(QWidget): def __init__(self): super().__init__() self.setWindowFlags(QtCore.Qt.FramelessWindowHint) layout = QHBoxLayout(self) self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) self.movie = QMovie(osp.join(pjpath, "resource", "loading.gif")) self.label.setMovie(self.movie) self.movie.start() ================================================ FILE: eiseg/widget/polygon.py ================================================ # Copyright (c) 2021 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. from qtpy import QtWidgets, QtGui, QtCore from . import GripItem, LineItem, BBoxAnnotation class PolygonAnnotation(QtWidgets.QGraphicsPolygonItem): def __init__( self, labelIndex, shape, delPolygon, setDirty, insideColor=[255, 0, 0], borderColor=[0, 255, 0], opacity=0.5, cocoIndex=None, parent=None, ): super(PolygonAnnotation, self).__init__(parent) self.points = [] self.m_items = [] self.m_lines = [] self.coco_id = cocoIndex self.height, self.width = shape[:2] self.delPolygon = delPolygon self.setDirty = setDirty self.labelIndex = labelIndex self.item_hovering = False self.polygon_hovering = False self.anning = False # 是否标注模式 self.line_hovering = False self.noMove = False self.last_focse = False # 之前是不是焦点在 self.setZValue(10) self.opacity = opacity i = insideColor self.insideColor = QtGui.QColor(i[0], i[1], i[2]) self.insideColor.setAlphaF(opacity) self.halfInsideColor = QtGui.QColor(i[0], i[1], i[2]) self.halfInsideColor.setAlphaF(opacity / 2) self.setBrush(self.halfInsideColor) b = borderColor self.borderColor = QtGui.QColor(b[0], b[1], b[2]) self.borderColor.setAlphaF(0.8) self.setPen(QtGui.QPen(self.borderColor)) self.setAcceptHoverEvents(True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True) self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) # persistent this bbox instance and update when needed self.bbox = BBoxAnnotation(labelIndex, self, cocoIndex, self) self.bbox.setParentItem(self) @property def scnenePoints(self): points = [] for p in self.points: p = self.mapToScene(p) points.append([p.x(), p.y()]) return points def setAnning(self, isAnning=True): if isAnning: self.setAcceptHoverEvents(False) self.last_focse = self.polygon_hovering self.polygon_hovering = False self.anning = True self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush)) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, False) # self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, False) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, False) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, False) self.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) for line in self.m_lines: line.setAnning(False) for grip in self.m_items: grip.setAnning(False) else: self.setAcceptHoverEvents(True) self.anning = False if self.last_focse: self.polygon_hovering = True self.setBrush(self.insideColor) else: self.setBrush(self.halfInsideColor) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True) # self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True) self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) for line in self.m_lines: line.setAnning(True) for grip in self.m_items: grip.setAnning(True) def addPointMiddle(self, lineIdx, point): gripItem = GripItem(self, lineIdx + 1, self.borderColor, (self.height, self.width)) gripItem.setEnabled(False) gripItem.setPos(point) self.scene().addItem(gripItem) gripItem.updateSize() gripItem.setEnabled(True) for grip in self.m_items[lineIdx + 1 :]: grip.m_index += 1 self.m_items.insert(lineIdx + 1, gripItem) self.points.insert(lineIdx + 1, self.mapFromScene(point)) self.setPolygon(QtGui.QPolygonF(self.points)) self.bbox.update() for line in self.m_lines[lineIdx + 1 :]: line.idx += 1 line = QtCore.QLineF(self.mapToScene(self.points[lineIdx]), point) self.m_lines[lineIdx].setLine(line) lineItem = LineItem(self, lineIdx + 1, self.borderColor) line = QtCore.QLineF( point, self.mapToScene(self.points[(lineIdx + 2) % len(self)]), ) lineItem.setLine(line) self.m_lines.insert(lineIdx + 1, lineItem) self.scene().addItem(lineItem) lineItem.updateWidth() def addPointLast(self, p): grip = GripItem(self, len(self), self.borderColor, (self.height, self.width)) self.scene().addItem(grip) self.m_items.append(grip) grip.updateSize() grip.setPos(p) if len(self) == 0: line = LineItem(self, len(self), self.borderColor) self.scene().addItem(line) self.m_lines.append(line) line.setLine(QtCore.QLineF(p, p)) else: self.m_lines[-1].setLine(QtCore.QLineF(self.points[-1], p)) line = LineItem(self, len(self), self.borderColor) self.scene().addItem(line) self.m_lines.append(line) line.setLine(QtCore.QLineF(p, self.points[0])) self.points.append(p) self.setPolygon(QtGui.QPolygonF(self.points)) self.bbox.update() def remove(self): for grip in self.m_items: self.scene().removeItem(grip) for line in self.m_lines: self.scene().removeItem(line) while len(self.m_items) != 0: self.m_items.pop() while len(self.m_lines) != 0: self.m_lines.pop() self.scene().polygon_items.remove(self) self.scene().removeItem(self) self.bbox.remove_from_scene() del self.bbox del self def removeFocusPoint(self): focusIdx = None for idx, item in enumerate(self.m_items): if item.hasFocus(): focusIdx = idx break if focusIdx is not None: if len(self) <= 3: self.delPolygon(self) # 调用app的删除多边形,为了同时删除coco标签 return del self.points[focusIdx] self.setPolygon(QtGui.QPolygonF(self.points)) self.bbox.update() self.scene().removeItem(self.m_items[focusIdx]) del self.m_items[focusIdx] for grip in self.m_items[focusIdx:]: grip.m_index -= 1 self.scene().removeItem(self.m_lines[focusIdx]) del self.m_lines[focusIdx] line = QtCore.QLineF( self.mapToScene(self.points[(focusIdx - 1) % len(self)]), self.mapToScene(self.points[focusIdx % len(self)]), ) # print((focusIdx - 1) % len(self), len(self.m_lines), len(self)) self.m_lines[(focusIdx - 1) % len(self)].setLine(line) for line in self.m_lines[focusIdx:]: line.idx -= 1 def removeLastPoint(self): # TODO: 创建的时候用到,需要删line if len(self.points) == 0: self.points.pop() self.setPolygon(QtGui.QPolygonF(self.points)) self.bbox.update() it = self.m_items.pop() self.scene().removeItem(it) del it def movePoint(self, i, p): # print("Move point", i, p) if 0 <= i < len(self.points): p = self.mapFromScene(p) self.points[i] = p self.setPolygon(QtGui.QPolygonF(self.points)) self.bbox.update() self.moveLine(i) def moveLine(self, i): # print("Moving line: ", i, self.noMove) if self.noMove: return points = self.points # line[i] line = QtCore.QLineF( self.mapToScene(points[i]), self.mapToScene(points[(i + 1) % len(self)]) ) self.m_lines[i].setLine(line) # line[i-1] line = QtCore.QLineF( self.mapToScene(points[(i - 1) % len(self)]), self.mapToScene(points[i]) ) # print((i - 1) % len(self), len(self.m_lines), len(self)) self.m_lines[(i - 1) % len(self)].setLine(line) def move_item(self, i, pos): if 0 <= i < len(self.m_items): item = self.m_items[i] item.setEnabled(False) item.setPos(pos) item.setEnabled(True) self.moveLine(i) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged: for i, point in enumerate(self.points): self.move_item(i, self.mapToScene(point)) return super(PolygonAnnotation, self).itemChange(change, value) def hoverEnterEvent(self, ev): self.polygon_hovering = True self.setBrush(self.insideColor) super(PolygonAnnotation, self).hoverEnterEvent(ev) def hoverLeaveEvent(self, ev): self.polygon_hovering = False if not self.hasFocus(): self.setBrush(self.halfInsideColor) super(PolygonAnnotation, self).hoverLeaveEvent(ev) def focusInEvent(self, ev): if not self.anning: self.setBrush(self.insideColor) def focusOutEvent(self, ev): if not self.polygon_hovering and not self.anning: self.setBrush(self.halfInsideColor) def setColor(self, insideColor, borderColor): i = insideColor self.insideColor = QtGui.QColor(i[0], i[1], i[2]) self.insideColor.setAlphaF(self.opacity) self.halfInsideColor = QtGui.QColor(i[0], i[1], i[2]) self.halfInsideColor.setAlphaF(self.opacity / 2) self.setBrush(self.halfInsideColor) b = borderColor self.borderColor = QtGui.QColor(b[0], b[1], b[2]) self.borderColor.setAlphaF(0.8) self.setPen(QtGui.QPen(self.borderColor)) for grip in self.m_items: grip.setColor(self.borderColor) for line in self.m_lines: line.setColor(self.borderColor) def __len__(self): return len(self.points) ================================================ FILE: eiseg/widget/scene.py ================================================ # Copyright (c) 2021 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. from PyQt5.QtCore import QPointF from qtpy import QtWidgets, QtCore from qtpy.QtCore import Qt from qtpy.QtGui import QPen, QColor class AnnotationScene(QtWidgets.QGraphicsScene): clickRequest = QtCore.Signal(int, int, bool) def __init__(self, parent=None): super(AnnotationScene, self).__init__(parent) self.creating = False self.polygon_items = [] # draw cross self.coords = None self.pen = QPen() self.pen.setWidth(1) self.pen.setColor(QColor(0, 0, 0, 127)) def setPenColor(self, color_list): R, G, B, A = color_list self.pen.setColor(QColor(R, G, B, A)) def updatePolygonSize(self): for poly in self.polygon_items: for grip in poly.m_items: grip.updateSize() for line in poly.m_lines: line.updateWidth() def setCreating(self, creating=True): self.creating = creating def mousePressEvent(self, ev): pos = ev.scenePos() if not self.creating and not self.hovering: if ev.buttons() in [Qt.LeftButton, Qt.RightButton]: self.clickRequest.emit( int(pos.x()), int(pos.y()), ev.buttons() == Qt.LeftButton ) elif self.creating: self.polygon_item.removeLastPoint() self.polygon_item.addPointLast(ev.scenePos()) # movable element self.polygon_item.addPointLast(ev.scenePos()) super(AnnotationScene, self).mousePressEvent(ev) def mouseMoveEvent(self, ev): if self.creating: self.polygon_item.movePoint( # self.polygon_item.number_of_points() - 1, ev.scenePos() len(self.polygon_item) - 1, ev.scenePos(), ) super(AnnotationScene, self).mouseMoveEvent(ev) def drawForeground(self, painter, rect): if self.coords is not None and self.coords != QPointF(-1, -1): painter.setClipRect(rect) painter.setPen(self.pen) painter.drawLine(int(self.coords.x()), int(rect.top()), int(self.coords.x()), int(rect.bottom())) painter.drawLine(int(rect.left()), int(self.coords.y()), int(rect.right()), int(self.coords.y())) def onMouseChanged(self, pointf): self.coords = pointf self.invalidate() @property def item_hovering(self): for poly in self.polygon_items: if poly.item_hovering: return True return False @property def polygon_hovering(self): for poly in self.polygon_items: if poly.polygon_hovering: return True return False @property def line_hovering(self): for poly in self.polygon_items: if poly.line_hovering: return True return False @property def hovering(self): return self.item_hovering or self.polygon_hovering or self.line_hovering ================================================ FILE: eiseg/widget/shortcut.py ================================================ # Copyright (c) 2021 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.path as osp import math from functools import partial from PyQt5.QtCore import QPoint from PyQt5.QtWidgets import QDesktopWidget from qtpy import QtCore, QtWidgets from qtpy.QtWidgets import ( QWidget, QLabel, QPushButton, QGridLayout, QKeySequenceEdit, QMessageBox, ) from qtpy.QtGui import QIcon from qtpy import QtCore from qtpy.QtCore import Qt from util import save_configs class RecordShortcutWidget(QKeySequenceEdit): def __init__(self, finishCallback, location): super().__init__() self.finishCallback = finishCallback # 隐藏界面 self.setWindowFlags(Qt.FramelessWindowHint) self.move(location) self.show() self.editingFinished.connect(lambda: finishCallback(self.keySequence())) def keyReleaseEvent(self, ev): self.finishCallback(self.keySequence()) class ShortcutWidget(QWidget): def __init__(self, actions, pjpath): super().__init__() self.tr = partial(QtCore.QCoreApplication.translate, "ShortcutWidget") self.setWindowTitle(self.tr("编辑快捷键")) self.setWindowIcon(QIcon(osp.join(pjpath, "resource/Shortcut.png"))) # self.setFixedSize(self.width(), self.height()) self.actions = actions self.recorder = None self.initUI() def initUI(self): grid = QGridLayout() self.setLayout(grid) actions = self.actions for idx, action in enumerate(actions): # 2列英文看不清 grid.addWidget(QLabel(action.iconText()[1:]), idx // 3, idx % 3 * 3) shortcut = action.shortcut().toString() if len(shortcut) == 0: shortcut = self.tr("-") button = QPushButton(shortcut) button.setFixedWidth(150) button.setFixedHeight(30) button.clicked.connect(partial(self.recordShortcut, action)) grid.addWidget( button, idx // 3, idx % 3 * 3 + 1, ) def refreshUi(self): actions = self.actions for idx, action in enumerate(actions): shortcut = action.shortcut().toString() if len(shortcut) == 0: shortcut = self.tr("-") self.layout().itemAtPosition( idx // 3, idx % 3 * 3 + 1, ).widget().setText(shortcut) def recordShortcut(self, action): # 打开快捷键设置的窗口时,如果之前的还在就先关闭 if self.recorder is not None: self.recorder.close() rect = self.geometry() x = rect.x() y = rect.y() + rect.height() self.recorder = RecordShortcutWidget(self.setShortcut, QPoint(x, y)) self.currentAction = action def setShortcut(self, key): self.recorder.close() for a in self.actions: if a.shortcut() == key: key = key.toString() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle(key + " " + self.tr("快捷键冲突")) msg.setText( key + " " + self.tr("快捷键已被") + " " + a.data() + " " + self.tr("使用,请设置其他快捷键或先修改") + " " + a.data() + " " + self.tr("的快捷键") ) msg.setStandardButtons(QMessageBox.Ok) msg.exec_() return key = "" if key.toString() == "Esc" else key # ESC不设置快捷键 self.currentAction.setShortcut(key) self.refreshUi() save_configs(None, None, self.actions) def center(self): qr = self.frameGeometry() cp = QDesktopWidget().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) # 快捷键设置跟随移动 def moveEvent(self, event): p = self.geometry() x = p.x() y = p.y() + p.height() if self.recorder is not None: self.recorder.move(x, y) def closeEvent(self, event): # 关闭时也退出快捷键设置 if self.recorder is not None: self.recorder.close() ================================================ FILE: eiseg/widget/table.py ================================================ # Copyright (c) 2021 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. from qtpy import QtWidgets from qtpy.QtCore import Qt class TableWidget(QtWidgets.QTableWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setDragEnabled(True) self.setAcceptDrops(True) self.viewport().setAcceptDrops(True) self.setDragDropOverwriteMode(False) self.setDropIndicatorShown(True) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) def dropEvent(self, event): if event.source() == self: rows = set([mi.row() for mi in self.selectedIndexes()]) targetRow = self.indexAt(event.pos()).row() rows.discard(targetRow) rows = sorted(rows) if not rows: return if targetRow == -1: targetRow = self.rowCount() for _ in range(len(rows)): self.insertRow(targetRow) rowMapping = dict() # Src row to target row. for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx colCount = self.columnCount() for srcRow, tgtRow in sorted(rowMapping.items()): for col in range(0, colCount): self.setItem(tgtRow, col, self.takeItem(srcRow, col)) for row in reversed(sorted(rowMapping.keys())): self.removeRow(row) event.accept() return def drop_on(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return self.rowCount() return index.row() + 1 if self.is_below(event.pos(), index) else index.row() def is_below(self, pos, index): rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: return False elif rect.bottom() - pos.y() < margin: return True # noinspection PyTypeChecker return ( rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y() ) ================================================ FILE: eiseg/widget/view.py ================================================ # Copyright (c) 2021 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. from qtpy import QtWidgets, QtCore, QtGui from qtpy.QtCore import Qt, QPointF class AnnotationView(QtWidgets.QGraphicsView): zoomRequest = QtCore.Signal(float) mousePosChanged = QtCore.Signal(QPointF) def __init__(self, *args): super(AnnotationView, self).__init__(*args) self.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) self.setMouseTracking(True) self.setTransformationAnchor(QtWidgets.QGraphicsView.NoAnchor) self.setResizeAnchor(QtWidgets.QGraphicsView.NoAnchor) self.point = QtCore.QPoint(0, 0) self.middle_click = False self.zoom_all = 1 # hint mouse self.setCursor(Qt.BlankCursor) def wheelEvent(self, ev): if ev.modifiers() & QtCore.Qt.ControlModifier: zoom = 1 + ev.angleDelta().y() / 2880 self.zoom_all *= zoom oldPos = self.mapToScene(ev.pos()) if self.zoom_all >= 0.02 and self.zoom_all <= 50: # 限制缩放的倍数 self.scale(zoom, zoom) newPos = self.mapToScene(ev.pos()) delta = newPos - oldPos self.translate(delta.x(), delta.y()) ev.ignore() self.zoomRequest.emit(self.zoom_all) else: super(AnnotationView, self).wheelEvent(ev) def mouseMoveEvent(self, ev): mouse_pos = QPointF(self.mapToScene(ev.pos())) self.mousePosChanged.emit(mouse_pos.toPoint()) if self.middle_click and ( self.horizontalScrollBar().isVisible() or self.verticalScrollBar().isVisible() ): # 放大到出现滚动条才允许拖动,避免出现抖动 self._endPos = ev.pos() / self.zoom_all - self._startPos / self.zoom_all # 这儿不写为先减后除,这样会造成速度不一致 self.point = self.point + self._endPos self._startPos = ev.pos() self.translate(self._endPos.x(), self._endPos.y()) super(AnnotationView, self).mouseMoveEvent(ev) def mousePressEvent(self, ev): if ev.buttons() == Qt.MiddleButton: self.middle_click = True self._startPos = ev.pos() super(AnnotationView, self).mousePressEvent(ev) def mouseReleaseEvent(self, ev): if ev.button() == Qt.MiddleButton: self.middle_click = False super(AnnotationView, self).mouseReleaseEvent(ev) def leaveEvent(self, ev): self.mousePosChanged.emit(QPointF(-1, -1)) return super(AnnotationView, self).leaveEvent(ev) ================================================ FILE: init.sh ================================================ #!/bin/bash ROOT=`cd "$(dirname ${BASH_SOURCE[0]})" && pwd` echo "ROOT : $ROOT" export PYTHONPATH=$PYTHONPATH:$ROOT/eiseg ================================================ FILE: requirements-med.txt ================================================ SimpleITK ================================================ FILE: requirements-rs.txt ================================================ GDAL>=3.3.0 rasterio>=1.2.4 ================================================ FILE: requirements.txt ================================================ pyqt5 qtpy opencv-python scipy paddleseg albumentations cython pyyaml wget requests easydict scikit-image ================================================ FILE: setup.py ================================================ import pathlib from setuptools import setup, find_packages, Extension import numpy as np from eiseg import __APPNAME__, __VERSION__ # from Cython.Build import cythonize HERE = pathlib.Path(__file__).parent README = (HERE / "README.md").read_text(encoding="utf-8") with open("requirements.txt") as fin: REQUIRED_PACKAGES = fin.read() ext_modules = [ Extension( "pycocotools._mask", sources=[ "./eiseg/util/coco/common/maskApi.c", "./eiseg/util/coco/pycocotools/_mask.pyx", ], include_dirs=[np.get_include(), "./eiseg/util/coco/common"], extra_compile_args=["-Wno-cpp", "-Wno-unused-function", "-std=c99"], ) ] setup( name=__APPNAME__, version=__VERSION__, description="交互式标注软件", long_description=README, long_description_content_type="text/markdown", url="https://github.com/PaddleCV-SIG/EISeg", author="PaddleCV-SIG", author_email="linhandev@qq.com", license="Apache Software License", classifiers=[ "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", ], packages=find_packages(exclude=("test",)), # packages=["EISeg"], include_package_data=True, install_requires=REQUIRED_PACKAGES, entry_points={ "console_scripts": [ "eiseg=eiseg.run:main", ] }, ) ================================================ FILE: tool/baidu_translate.py ================================================ # Copyright (c) 2021 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 json import random import hashlib from urllib import parse import http.client from tqdm import tqdm from collections import defaultdict from bs4 import BeautifulSoup as bs class BaiduTranslate: def __init__(self, fromLang, toLang): self.url = "/api/trans/vip/translate" self.appid = "20200311000396156" self.secretKey = "s6c3ZeYTI9lhrwQVugnM" self.fromLang = fromLang self.toLang = toLang self.salt = random.randint(32768, 65536) def trans(self, text): sign = self.appid + text + str(self.salt) + self.secretKey md = hashlib.md5() md.update(sign.encode(encoding="utf-8")) sign = md.hexdigest() myurl = ( self.url + "?appid=" + self.appid + "&q=" + parse.quote(text) + "&from=" + self.fromLang + "&to=" + self.toLang + "&salt=" + str(self.salt) + "&sign=" + sign ) try: httpClient = http.client.HTTPConnection("api.fanyi.baidu.com") httpClient.request("GET", myurl) response = httpClient.getresponse() html = response.read().decode("utf-8") html = json.loads(html) dst = html["trans_result"][0]["dst"] return True, dst except Exception as e: return False, e def read_ts(ts_path): xml = open(ts_path, "r", encoding="utf-8").read() xml = bs(xml, "xml") return xml pre_ts_path = "tool/ts/English.ts" # Russia ts_path = "tool/ts/out.ts" pre_xml = read_ts(pre_ts_path) xml = read_ts(ts_path) pre_messages = pre_xml.find_all("message") messages = xml.find_all("message") bd_trans = BaiduTranslate("auto", "en") # ru trans = bd_trans.trans translated = 0 failed = 0 for msg in messages: type = msg.translation.get("type", None) source = msg.source.string trans = msg.translation.string if type == "unfinished" and trans is None and source is not None: in_pre = False for pmsg in pre_messages: if pmsg.source.string == source: try: msg.translation.string = pmsg.translation.string translated += 1 print( f"{translated + failed} / {len(messages)}:{source} \t {msg.translation.string}" ) in_pre = True except: pass break if in_pre is False: res = bd_trans.trans(source) if res[0]: msg.translation.string = res[1] translated += 1 else: failed += 1 print( f"{translated + failed} / {len(messages)}:{source} \t {msg.translation.string}" ) for name in xml.find_all("name"): name.string = "APP_EISeg" print(f"Totally {len(messages)} , translated {translated}, failed {failed}") open(ts_path, "w", encoding="utf-8").write(str(xml)) ================================================ FILE: tool/pypi.sh ================================================ rm dist/* python setup.py sdist bdist_wheel twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose # https://upload.pypi.org/legacy/ conda create -n test python=3.9 conda activate test pip install --upgrade eiseg pip install paddlepaddle eiseg ================================================ FILE: tool/semantic2instance.py ================================================ # Copyright (c) 2021 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 os.path as osp import argparse from tqdm import tqdm import numpy as np import cv2 from PIL import Image def _savePalette(label, save_path): bin_colormap = np.random.randint(0, 255, (256, 3)) # 可视化的颜色 bin_colormap[0, :] = [0, 0, 0] bin_colormap = bin_colormap.astype(np.uint8) visualimg = Image.fromarray(label, "P") palette = bin_colormap # long palette of 768 items visualimg.putpalette(palette) visualimg.save(save_path, format='PNG') def _segMaskB2I(mask_path, save_path): img = np.asarray(Image.open(mask_path)) mask = np.zeros_like(img) results = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE) cv2_v = cv2.__version__.split(".")[0] contours = results[1] if cv2_v == "3" else results[0] # 边界 hierarchys = results[2] if cv2_v == "3" else results[1] # 隶属信息 areas = {} # 面积 for i in range(len(contours)): areas[i] = cv2.contourArea(contours[i]) sorted(areas.items(), key = lambda kv:(kv[1], kv[0]), reverse=True) # 面积升序 # 开始填充 color = 1 for idx in areas.keys(): contour = contours[idx] hierarchy = hierarchys[0][idx] # print(hierarchy) if hierarchy[-1] == -1: # 输入子轮廓 cv2.fillPoly(mask, [contour], color) color += 1 else: cv2.fillPoly(mask, [contour], 0) # 显示 # cv2.drawContours(mask, contours, -1, (125,125,125), 1) # cv2.imshow('src',mask) # cv2.waitKey() _savePalette(mask, save_path) parser = argparse.ArgumentParser(description='Label path and save path') parser.add_argument('--label_path', '-o', help='读取语义分割标签文件夹路径,必要参数', required=True) parser.add_argument('--save_path', '-d', help='实例分割标签保存文件夹路径,必要参数', required=True) args = parser.parse_args() if __name__ == "__main__": label_path = args.label_path save_path = args.save_path names = os.listdir(label_path) for name in tqdm(names): label = osp.join(label_path, name) saver = osp.join(save_path, name) _segMaskB2I(label, saver) ================================================ FILE: tool/translate.pro ================================================ CODECFORTR = UTF-8 SOURCES = ../eiseg/app.py ../eiseg/ui.py ../eiseg/widget/shortcut.py TRANSLATIONS = ./ts/out.ts ================================================ FILE: tool/translateUI.py ================================================ import os import os.path as osp import json import random import hashlib from urllib import parse import http.client from tqdm import tqdm from collections import defaultdict class BaiduTranslate: def __init__(self, fromLang, toLang): self.url = "/api/trans/vip/translate" self.appid = "20200311000396156" self.secretKey = "s6c3ZeYTI9lhrwQVugnM" self.fromLang = fromLang self.toLang = toLang self.salt = random.randint(32768, 65536) def BdTrans(self, text): sign = self.appid + text + str(self.salt) + self.secretKey md = hashlib.md5() md.update(sign.encode(encoding="utf-8")) sign = md.hexdigest() myurl = self.url + \ "?appid=" + self.appid + \ "&q=" + parse.quote(text) + \ "&from=" + self.fromLang + \ "&to=" + self.toLang + \ "&salt=" + str(self.salt) + \ "&sign=" + sign try: httpClient = http.client.HTTPConnection("api.fanyi.baidu.com") httpClient.request("GET", myurl) response = httpClient.getresponse() html = response.read().decode("utf-8") html = json.loads(html) dst = html["trans_result"][0]["dst"] return True, dst except Exception as e: return False , e # 获取所有可能带有ui的py文件 ui_files = [] widget_path = "eiseg/widget" widget_names = os.listdir(widget_path) for widget_name in widget_names: if widget_name != "__init__.py" and widget_name != "__pycache__": ui_files.append(osp.join(widget_path, widget_name)) ui_files.append("eiseg/ui.py") ui_files.append("eiseg/app.py") # 查找 chinese = [] keys = "trans.put(\"" for ui_file in ui_files: with open(ui_file, "r", encoding="utf-8") as f: codes = f.read() sp_codes = codes.split(keys) if len(sp_codes) == 1: continue else: sp_codes.pop(0) for sp_code in sp_codes: chinese.append(sp_code.split("\")")[0]) chinese = list(set(chinese)) # print(len(chinese)) # print(chinese) # 比对(以前有的不重新机翻) save_path = "eiseg/config/zh_CN.EN" now_words = defaultdict(dict) with open(save_path, "r", encoding="utf-8") as f: datas = f.readlines() for data in datas: words = data.strip().split("@") now_words[words[0]] = words[1] # 翻译 def firstCharUpper(s): return s[:1].upper() + s[1:] translate = [] baidu_trans = BaiduTranslate("zh", "en") for cn in tqdm(chinese): if cn not in now_words.keys(): en = baidu_trans.BdTrans(cn) tr = cn + "@" + firstCharUpper(en[-1]) # 首字母大写 else: tr = cn + "@" + now_words[cn] translate.append(tr) # 保存翻译内容 with open(save_path, "w", encoding="utf-8") as f: for language in translate: f.write(language + "\n") print("trans OK!") ================================================ FILE: tool/ts/Arabic.ts ================================================ APP_EISeg &打开图像 & فتح الصورة 打开一张图像进行标注 فتح صورة الشرح &打开文件夹 & فتح مجلد 打开一个文件夹下所有的图像进行标注 فتح مجلد تحت كل صورة الشرح &改变标签保存路径 & تغيير مسار حفظ التسمية 改变标签保存的文件夹路径 تغيير مسار المجلد حفظ التسمية &加载模型参数 & تحميل معالم النموذج 加载一个模型参数 تحميل نموذج المعلمة &保存 حفظ 保存图像标签 حفظ صورة العلامة &另存为 & حفظ باسم 在指定位置另存为标签 حفظ التسمية في الموقع المحدد &自动保存 & حفظ السيارات 翻页同时自动保存 حفظ الصفحة تلقائيا في نفس الوقت &上一张 و آخر واحد 翻到上一张图片 انتقل إلى الصورة السابقة &下一张 الصورة التالية 翻到下一张图片 انتقل إلى الصورة التالية &完成当前目标 & تحقيق الهدف الحالي 完成当前目标的标注 الانتهاء من وضع العلامات على الهدف الحالي &清除所有标注 & مسح جميع الشروح 清除所有标注信息 مسح جميع معلومات الشرح &撤销 إلغاء 撤销一次点击 إلغاء بنقرة واحدة &重做 و إعادة 重做一次点击 انقر فوق إعادة &删除多边形 & حذف المضلعات 删除当前选中的多边形 حذف المضلع المحدد حاليا &删除所有多边形 & حذف جميع المضلعات 删除所有的多边形 حذف جميع المضلعات &保留最大连通块 & الحفاظ على معظم كتل داليان تونغ 保留最大的连通块 الحفاظ على أقصى قدر من كتلة متصلة &标签和图像使用相同拓展名 & العلامات والصور باستخدام نفس التمديد 标签和图像使用相同拓展名,用于图像中有文件名相同但拓展名不同的情况,防止标签覆盖 التسمية والصورة استخدام نفس اسم التمديد ، في صورة لها نفس اسم الملف ولكن تمديد اسم مختلف ، ومنع التسمية من الكتابة . &伪彩色保存 & حفظ الألوان الزائفة 保存为伪彩色图像 حفظ الصورة الملونة الزائفة &灰度保存 & حفظ رمادي 保存为灰度图像,像素的灰度为对应类型的标签 حفظ الصورة الرمادية ، رمادي بكسل هو نوع من التسمية المقابلة &JSON保存 جسون حفظ 保存为JSON格式 حفظ جسون &COCO保存 حفظ كوكو 保存为COCO格式 حفظ كوكو &抠图保存 & حفظ حصيرة 只保留前景,背景设置为背景色 الحفاظ على الصدارة فقط ، تعيين الخلفية إلى لون الخلفية &设置抠图背景色 & تعيين لون الخلفية حصيرة 抠图后背景像素的颜色 لون الخلفية بكسل بعد حصيرة &关闭 إغلاق 关闭当前图像 إغلاق الصورة الحالية &退出 خروج 退出软件 الخروج من البرنامج &导出标签列表 & تصدير قائمة العلامات 将标签列表导出成标签配置文件 تصدير قائمة التسمية إلى تسمية ملف التكوين &载入标签列表 & تحميل قائمة العلامات 从标签配置文件载入标签列表 تحميل قائمة من العلامات العلامات من ملف التكوين &清空标签列表 & مسح قائمة التبويب 清空所有的标签 مسح جميع العلامات &清除近期文件记录 & مسح الملفات الأخيرة 清除近期标注文件记录 مسح سجل ملف الشرح الأخيرة &模型选择 & اختيار النموذج 隐藏/展示模型选择面板 إخفاء / عرض نموذج اختيار الفريق &数据列表 & قائمة البيانات 隐藏/展示数据列表面板 إخفاء / عرض لوحة قائمة البيانات &标签列表 & قائمة العلامات 隐藏/展示标签列表面板 إخفاء / عرض قائمة التبويب لوحة &分割设置 & إعدادات التقسيم 隐藏/展示分割设置面板 إخفاء / عرض تقسيم لوحة إعدادات &遥感设置 & إعدادات الاستشعار عن بعد 隐藏/展示遥感设置面板 إخفاء / عرض إعدادات الاستشعار عن بعد لوحة &医疗设置 & إعدادات طبية 隐藏/展示医疗设置面板 إخفاء / عرض لوحة الإعدادات الطبية &N2宫格标注 و N2 隐藏/展示N^2宫格细粒度标注面板 إخفاء / عرض ن ^ 2 غرامة الحبيبات لوحة الشرح &快速入门 & بداية سريعة 主要功能使用介绍 الوظيفة الرئيسية مقدمة &反馈问题 & أسئلة التغذية المرتدة 通过Github Issue反馈使用过程中遇到的问题。我们会尽快进行修复 المشاكل التي واجهتها في استخدام ردود الفعل من خلال github issue . . . . . . . ونحن سوف إصلاح ذلك في أقرب وقت ممكن . &编辑快捷键 & تحرير اختصارات 编辑软件快捷键 تحرير اختصارات البرامج &调试日志 & سجل التصحيح 用于观察软件执行过程和进行debug。我们不会自动收集任何日志,可能会希望您在反馈问题时间打开此功能,帮助我们定位问题。 تستخدم لمراقبة تنفيذ البرامج وإجراء التصحيحات . نحن لا تلقائيا جمع أي سجلات ، قد ترغب في فتح هذه الميزة في الوقت المناسب للحصول على التغذية المرتدة من الأسئلة لمساعدتنا في تحديد المشكلة . 文件 الوثائق 标注 الشرح 功能 وظيفة . 显示 عرض . 帮助 ساعد 近期文件 الوثائق الأخيرة 近期模型及参数 نموذج المعلمة الأخيرة 切换语言 تبديل اللغة 切换语言需要重启软件才能生效 لغة التبديل يتطلب إعادة تشغيل البرنامج قبل أن يصبح نافذا 无近期文件 لا توجد وثائق حديثة 已清除最近打开文件 تم مسح الملفات المفتوحة مؤخرا 无近期模型记录 لا يوجد سجل النموذج الحالي Paddle静态模型权重文件(*.pdiparams) بادل نموذج ثابت الوزن الملف (*.pdiparams) 选择模型参数 اختيار نموذج المعلمة 参数路径存在中文 المعلمة المسار موجود في الصينية 请修改参数路径为非中文路径! يرجى تعديل مسار المعلمة غير الصينية المسار ! 模型加载成功 تحميل النموذج بنجاح 掩膜已启用 قناع تمكين 掩膜已关闭 قناع مغلق 没有最近使用模型信息,请加载模型 لا يوجد نموذج المعلومات المستخدمة مؤخرا ، يرجى تحميل النموذج 标签配置文件 تسمية ملف التكوين 选择标签配置文件路径 حدد مسار ملف التسمية 没有需要保存的标签 لا تحتاج إلى حفظ العلامات 请先添加标签之后再进行保存! يرجى إضافة علامة قبل حفظ ! 保存标签配置文件 حفظ العلامة الشخصية 选择保存标签配置文件路径 حدد مسار حفظ العلامة الشخصية 清空标签列表? مسح قائمة العلامات ؟ 请确认是否要清空标签列表 تأكد من أن كنت تريد إفراغ قائمة العلامات 确认删除? تأكيد حذف ؟ 确认要删除当前选中多边形标注? هل أنت متأكد أنك تريد حذف المضلع المحدد حاليا الشرح ؟ 选择待标注图片 حدد الصورة إلى علامة 选择待标注图片文件夹 حدد مجلد الصور إلى علامة 未启用医疗组件 العنصر الطبي لا يمكن 加载医疗影像需启用医疗组件,是否立即启用? المكونات الطبية اللازمة لتحميل الصور الطبية . هل تريد تمكين الآن ؟ 未打开遥感组件 الاستشعار عن بعد عنصر لم تفتح 打开遥感图像需启用遥感组件,是否立即启用? فتح صور الاستشعار عن بعد يتطلب تمكين الاستشعار عن بعد عنصر . هل تريد تمكين الآن ؟ 图像过大 صورة كبيرة جدا 图像过大,将启用宫格功能! الصورة هي كبيرة جدا ، وسوف تمكين وظيفة الشبكة ! لا 模型未加载 نموذج لا تحميل 尚未加载模型,请先加载模型! لم يتم تحميل النموذج ، يرجى تحميل النموذج الأول ! 完成最后一个目标? تحقيق الهدف النهائي ؟ 是否完成最后一个目标的标注,不完成不会进行保存。 إذا كان الهدف النهائي هو الانتهاء من الشرح ، لا يتم حفظها . 保存标签? حفظ العلامات ؟ 标签尚未保存,是否保存标签 التسمية لم يتم حفظها ، حفظ التسمية 标签成功保存至 حفظ التسمية بنجاح 保存标签文件路径 حفظ مسار ملف التسمية 选择标签文件保存路径 حدد مسار حفظ ملف التسمية 选择标签保存路径 اختر علامة حفظ المسار 未选择模型 لا اختيار النموذج 尚未选择模型,请先在右上角选择模型 لم يتم اختيار النموذج ، حدد النموذج الأول في الزاوية اليمنى العليا 未选择当前标签 التسمية الحالية لم يتم اختياره 请先在标签列表中单击点选标签 انقر فوق علامة التبويب في قائمة 无法导入GDAL غير قادر على استيراد gdal 使用遥感工具需要安装GDAL! استخدام أدوات الاستشعار عن بعد تحتاج إلى تثبيت GDAL ! 打开遥感工具失败,请安装GDAL库 فشل في فتح أداة الاستشعار عن بعد ، يرجى تثبيت مكتبة جدال 无法导入SimpleITK لا يمكن استيراد simpleitk 使用医疗工具需要安装SimpleITK! استخدام الأدوات الطبية تحتاج إلى تثبيت simpleitk ! 打开医疗工具失败,请安装SimpleITK فشل في فتح الأداة الطبية ، الرجاء تثبيت simpleitk 图像过大,已显示缩略图 الصورة كبيرة جدا ، وقد تم عرض الصور المصغرة 功能尚在开发 وظيفة لا تزال قيد التطوير 编辑快捷键 تحرير اختصارات 快捷键冲突 اختصارات الصراع 快捷键已被 اختصارات تم 使用,请设置其他快捷键或先修改 استخدام مجموعة أخرى من مفاتيح الاختصار أو تعديل 的快捷键 مفاتيح الاختصار 加载网络参数 تحميل معلمات الشبكة 使用掩膜 استخدام قناع 模型选择 اختيار النموذج 数据列表 قائمة البيانات 添加标签 إضافة علامة 标签列表 قائمة العلامات 分割阈值: تجزئة العتبة : 标签透明度: علامة الشفافية : 点击可视化半径: انقر فوق دائرة نصف قطرها البصرية : 保存 حفظ . 分割设置 تقسيم إعدادات 波段设置 وضع الفرقة 保存设置 حفظ الإعدادات 使用建筑边界简化 تبسيط استخدام الحدود المعمارية 额外保存为ESRI Shapefile حفظ إضافية كما esri shapefile 地理信息 معلومات جغرافية 遥感设置 إعدادات الاستشعار عن بعد 医疗设置 الإعداد الطبي 完成宫格 الانتهاء من قصر شعرية 宫格切换 قصر التبديل ================================================ FILE: tool/ts/English.ts ================================================ APP_EISeg &打开图像 &Open Image 打开一张图像进行标注 Open an image for annotation &打开文件夹 &Open Dir 打开一个文件夹下所有的图像进行标注 Open all images in a folder for annotation &改变标签保存路径 &Change Output Dir 改变标签保存的文件夹路径 Change the folder where labels are saved &加载模型参数 &Load Model Parameters 加载一个模型参数 Load a model parameter &保存 &Save 保存图像标签 Save image label &另存为 &Save as 在指定位置另存为标签 Save as label at the specified location &自动保存 &Auto Save 翻页同时自动保存 Save automatically while turning image &上一张 &Prev Image 翻到上一张图片 Filp to previous image &下一张 &Next Image 翻到下一张图片 Flip to the next image &完成当前目标 &Finish Current Target 完成当前目标的标注 Finish labeling the current object &清除所有标注 &Clear All Labels 清除所有标注信息 Clear all labels in the image &撤销 &Undo 撤销一次点击 Undo one click &重做 &Redo 重做一次点击 Redo one click &删除多边形 &Delete Polygon 删除当前选中的多边形 Deletes the currently selected polygon &删除所有多边形 &Delete all polygons 删除所有的多边形 Delete all polygons &保留最大连通块 &Filter LCC 保留最大的连通块 Keep the largest connected component only &标签和图像使用相同拓展名 &Use same extension name 标签和图像使用相同拓展名,用于图像中有文件名相同但拓展名不同的情况,防止标签覆盖 The label and image use the same extension name, which is used to prevent the label from being overwritten when the file name in the image is the same but the extension name is different &伪彩色保存 &Pseudo Color Format 保存为伪彩色图像 Save label in pseudo color format &灰度保存 &Grayscale Format 保存为灰度图像,像素的灰度为对应类型的标签 Save label in grayscale format, the value of each pixel is the id for the label category of the pixel &JSON保存 &JSON Format 保存为JSON格式 Save polygon information in JSON format &COCO保存 &Coco Format 保存为COCO格式 Save polygon information in coco format &显示遥感多边形 &Display RS polygons 显示遥感大图多边形 Display RS large polygon &抠图保存 &Save Matting 只保留前景,背景设置为背景色 Only keep foreground pixels, set all background pixels to background color &设置抠图背景色 &Set matting background color 抠图后背景像素的颜色 The color to use for all background pixels &设置十字丝颜色 &Set cross wire color 十字丝的显示颜色 The color of crosshair &关闭 &Close 关闭当前图像 Close current image &退出 &Exit 退出软件 Exit software &导出标签列表 &Export label list 将标签列表导出成标签配置文件 Export label list to label profile &载入标签列表 &Load label list 从标签配置文件载入标签列表 Load label list from label profile &清空标签列表 &Clear Label List 清空所有的标签 Clear all labels &清除近期文件记录 &Clear recent file records 清除近期标注文件记录 Clear recent annotation file records &模型选择 &Model Selection 隐藏/展示模型选择面板 Hide / show model selection panel &数据列表 &Image List 隐藏/展示数据列表面板 Hide / show data list panel &标签列表 &Label List 隐藏/展示标签列表面板 Hide / show label list panel &分割设置 &Segmentation Setting 隐藏/展示分割设置面板 Hide / show split settings panel &遥感设置 &Remote sensing settings 隐藏/展示遥感设置面板 Hide / show remote sensing settings panel &医疗设置 &Medical settings 隐藏/展示医疗设置面板 Hide / show medical settings panel &N2宫格标注 &N2 grid label 隐藏/展示N^2宫格细粒度标注面板 Hide / show n ^ 2 grid fine-grained dimension panel &快速入门 &Quick start 主要功能使用介绍 Introduction to main functions &反馈问题 &Feedback questions 通过Github Issue反馈使用过程中遇到的问题。我们会尽快进行修复 Feed back the problems encountered during use through GitHub issue. We will repair it as soon as possible &编辑快捷键 &Edit Shortcuts 编辑软件快捷键 Edit software shortcuts &调试日志 &Debug log 用于观察软件执行过程和进行debug。我们不会自动收集任何日志,可能会希望您在反馈问题时间打开此功能,帮助我们定位问题。 It is used to observe the software execution process and debug. We don't automatically collect any logs. We may want you to turn on this function when you feed back the problem to help us locate the problem. &使用QT文件窗口 Use QT file window 如果使用文件选择窗口时遇到问题可以选择使用Qt窗口 If you encounter problems using the file selection window, you can choose to use the QT window. 语言 Language 文件 File 标注 Annotation 功能 Functions 显示 Display 帮助 Help 近期文件 Recent documents 近期模型及参数 Recent models and parameters 切换语言 Changing language 切换语言需要重启软件才能生效 Changing language only takes effect after restarting the app 无近期文件 No recent documents 已清除最近打开文件 Recently opened files have been cleared 无近期模型记录 No recent model parameters Paddle静态模型权重文件(*.pdiparams) Paddle static model weight files (*.pdiparams) 选择模型参数 Select model parameters 参数路径存在中文 Parameter path exists in Chinese 请修改参数路径为非中文路径! Please change the parameter path to non Chinese path! 模型加载成功 Model loaded successfully 掩膜已启用 Mask enabled 掩膜已关闭 Mask closed 没有最近使用模型信息,请加载模型 There is no recently used model information, please load the model 标签配置文件 Label profile 选择标签配置文件路径 Select label profile path 没有需要保存的标签 There are no labels to save 请先添加标签之后再进行保存! Please add a label before saving! 保存标签配置文件 Save label profile 选择保存标签配置文件路径 Select the path to save the label profile 清空标签列表? Clear label list? 请确认是否要清空标签列表 Please confirm you want to clear the label list 确认删除? Confirm deletion? 确认要删除当前选中多边形标注? Are you sure you want to delete the currently selected polygon dimension? 选择待标注图片 Select the image to be labeled 选择待标注图片文件夹 Select the image folder to label 未启用医疗组件 Medical components are not enabled 加载医疗影像需启用医疗组件,是否立即启用? Loading medical images requires enabling medical components. Do you want to enable them now? 未打开遥感组件 Remote sensing component not open 打开遥感图像需启用遥感组件,是否立即启用? The remote sensing component needs to be enabled to open the remote sensing image. Do you want to enable it now? ● 波段数: ● Number of bands: ● 数据类型: ● Data type: ● 行数: ● Number of rows: ● 列数: ● Number of columns: 图像过大 The image is too large 图像过大,将启用宫格功能! If the image is too large, the grid function will be enabled! Na 模型未加载 Model not loaded 尚未加载模型,请先加载模型! The model has not been loaded, please load the model first! 完成最后一个目标? Finish the last goal? 是否完成最后一个目标的标注,不完成不会进行保存。 Whether to complete the annotation of the last target. If not, it will not be saved. 保存标签? Save label? 标签尚未保存,是否保存标签 The label has not been saved. Do you want to save the label 标签成功保存至 Label successfully saved to 保存标签文件路径 Save label file path 选择标签文件保存路径 Select the path to save the label file 选择标签保存路径 Select the folder to save labels 未选择模型 Model not selected 尚未选择模型,请先在右上角选择模型 model not selected. Please select the model in the upper right corner first 未选择当前标签 The current label is not selected 请先在标签列表中单击点选标签 Please click the label in the label list first 无法导入GDAL Unable to import GDAL 使用遥感工具需要安装GDAL! GDAL needs to be installed to use remote sensing tools! 打开遥感工具失败,请安装GDAL库 Failed to open remote sensing tool. Please install GDAL library 无法导入SimpleITK Cannot import simpleitk 使用医疗工具需要安装SimpleITK! Simpleitk needs to be installed to use medical tools! 打开医疗工具失败,请安装SimpleITK Failed to open medical tool, please install simpleitk 图像过大,已显示缩略图 The image is too large and thumbnails are displayed 功能尚在开发 The function is still under development 编辑快捷键 Edit Keyboard Shortcuts 快捷键冲突 Shortcut key conflict 快捷键已被 shortcut has been used by 使用,请设置其他快捷键或先修改 . Please set another key sequence or modify the keyboard shotcut of 的快捷键 first! 加载网络参数 Load Model Parameter 使用掩膜 Use mask 模型选择 Model selection 数据列表 Image List 添加标签 Add Label 标签列表 Label List 分割阈值: Segmentation Threshold: 标签透明度: Label Transparency: 点击可视化半径: Click Visualization Radius: 保存 Save 分割设置 Segmentation Setting 波段设置 Band setting 保存设置 Save settings 建筑边界规范化 building boundary normalization 另存为shapefile Save extra as shapefile 地理信息 geographic information 遥感设置 Remote sensing settings 医疗设置 Medical settings 完成宫格 Complete the grid 保存每个宫格的标签 Save all of grids 宫格切换 Palace grid switching ================================================ FILE: tool/update_md5.py ================================================ # Copyright (c) 2021 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 hashlib from pathlib import Path models_dir = Path() ext = ".pdparams" for model_path in models_dir.glob("*/*" + ext): md5 = hashlib.md5(model_path.read_bytes()).hexdigest() md5_path = str(model_path)[: -len(ext)] + ".md5" Path(md5_path).write_text(md5)