├── .gitattributes ├── .github └── workflows │ └── pythonapp.yml ├── .gitignore ├── LICENSE ├── README.md ├── SlideRunner ├── SlideRunner.py ├── Slides.sqlite ├── __init__.py ├── __main__.py ├── artwork │ ├── AboutScreen.png │ ├── AboutScreen.svg │ ├── ExactWelcomeScreen.png │ ├── SlideRunner.icns │ ├── SlideRunner.iconset │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2.png │ ├── SlideRunnerDMG.png │ ├── SliderRunnerDMG.svg │ ├── SplashScreen.png │ ├── SplashScreen.svg │ ├── annoInOverview.png │ ├── backArrow.png │ ├── blindedMode.png │ ├── drawCircle.png │ ├── icon.ico │ ├── icon.png │ ├── icon.svg │ ├── iconArrow.png │ ├── iconBlinded.png │ ├── iconCircle.png │ ├── iconOverlay.png │ ├── iconPolygon.png │ ├── iconQuestion.png │ ├── iconRect.png │ ├── iconWand.png │ ├── icon_flag.png │ ├── icon_nextView.png │ ├── icon_previousView.png │ ├── icon_screeningMode.png │ ├── iconsToolbar.svg │ └── welcomeExactScreen.svg ├── dataAccess │ ├── __init__.py │ ├── annotations.py │ ├── database.py │ └── exact.py ├── doc │ ├── DemoWorkflow_WorkingWithSlideRunnerDatabases.ipynb │ ├── MagicWandAnnotation.mov │ ├── SlideRunner_UML.pdf │ ├── SlideRunner_UML.png │ ├── annotations.png │ ├── annotations.svg │ ├── demo.sqlite │ ├── gui.png │ └── logoline.png ├── general │ ├── SlideRunnerPlugin.py │ ├── __init__.py │ ├── dependencies.py │ ├── pluginFinder.py │ └── types.py ├── gui │ ├── SlideRunner_ui.py │ ├── SlideRunner_ui.ui │ ├── __init__.py │ ├── annotation.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── about.py │ │ ├── dbmanager.py │ │ ├── exactDownloadDialog.py │ │ ├── exactLinkDialog.py │ │ ├── getCoordinates.py │ │ ├── question.py │ │ ├── settings.py │ │ └── welcomeExact.py │ ├── frameSlider.py │ ├── menu.py │ ├── mouseEvents.py │ ├── shortcuts.py │ ├── sidebar.py │ ├── splashScreen.py │ ├── style.py │ ├── toolbar.py │ ├── types.py │ └── zoomSlider.py ├── plugins │ ├── proc_HPF.py │ ├── proc_MitHeatmap.py │ ├── proc_ObjDetResults.py │ ├── proc_coregistration.py │ ├── proc_coregistration_qt.py │ ├── proc_countdown.py │ ├── proc_macenko.py │ ├── proc_otsu.py │ ├── proc_ppc.py │ ├── proc_secondaryDatabase.py │ ├── wsi_segmentation.py │ └── wsi_tumor_classification.py └── processing │ ├── __init__.py │ ├── screening.py │ └── thumbnail.py ├── build.bat ├── build.sh ├── databases ├── MITOS2012.sqlite ├── MITOS2012_test.sqlite ├── MITOS2014.sqlite ├── TUPAC.sqlite └── TUPAC_Mitosis_ROI.sqlite ├── main.py ├── main.spec ├── main_osx.spec ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test_database.py ├── test_exact.py └── test_screening.py /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/SlideRunner.app.zip filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install numpy 20 | pip install -r requirements.txt 21 | - name: Install openslide 22 | run: | 23 | sudo apt-get install libopenslide0 libopenslide-dev 24 | - name: Lint with flake8 25 | run: | 26 | pip install flake8 27 | # stop the build if there are Python syntax errors or undefined names 28 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 29 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 30 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 31 | - name: Set up EXACT for unit tests 32 | shell: 'script -q -e -c "bash {0}"' 33 | run: | 34 | git clone https://github.com/ChristianMarzahl/Exact.git 35 | cd Exact 36 | export DJANGO_SUPERUSER_USERNAME=testuser 37 | export DJANGO_SUPERUSER_PASSWORD=testpw 38 | cp env.dev env.prod 39 | cp env.dev.db env.prod.db 40 | cp exact/exact/settings.py.example exact/exact/settings.py 41 | docker-compose -f docker-compose.prod.yml up -d --build 42 | docker-compose -f docker-compose.prod.yml exec web python3 manage.py migrate --noinput 43 | docker-compose -f docker-compose.prod.yml exec web python3 manage.py createsuperuser --noinput 44 | docker-compose -f docker-compose.prod.yml exec web python3 manage.py collectstatic --no-input --clear 45 | - name: Test with pytest 46 | run: | 47 | pip install pytest 48 | cd tests 49 | pytest 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | __pycache__ 3 | !/.gitignore 4 | build 5 | dist 6 | *.egg-info 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logos](SlideRunner/doc/logoline.png) 2 | 3 | # SlideRunner 4 | 5 | [![DOI:10.1007/978-3-662-56537-7_81](https://zenodo.org/badge/DOI/10.1007/978-3-662-56537-7_81.svg)](https://doi.org/10.1007/978-3-662-56537-7_81) 6 | 7 | 8 | *SlideRunner* is a tool for massive cell annotations in whole slide images. 9 | 10 | It has been created in close cooperation between the [Pattern Recognition Lab](https://www5.cs.fau.de), Friedrich-Alexander-Universität Erlangen-Nürnberg and the [Institute of Veterenary Pathology](http://www.vetmed.fu-berlin.de/einrichtungen/institute/we12/index.html), Freie Universität Berlin. Development is continued now at Technische Hochschule Ingolstadt. 11 | 12 | If you use the software for research purposes, please cite our paper: 13 | 14 | > M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier (2018) SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. In: Bildverarbeitung für die Medizin 2018. Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. [link](https://www.springerprofessional.de/sliderunner/15478976) [arXiv:1802.02347](https://arxiv.org/abs/1802.02347) 15 | 16 | 17 | Please find the authors webpage at: https://imi.thi.de 18 | 19 | ## Version 2.0.0 20 | 21 | With so many new features, it is time to declare Version 2. While we initially wanted to declare the version on this years BVM, it has all been delayed a bit. But for a good reason: Following requests by our pathologists, SlideRunner now has support for a much broader range of image formats. 22 | 23 | List of new features: 24 | - New formats: 25 | - DICOM supprt (including DICOM WSI, through pydicom, new in 1.99beta4) 26 | - CellVizio MKT (Confocal Laser Endomicroscopy, new in 1.99beta4) 27 | - TIFF stacks (previously was only for 2D images, new in 1.99beta6) 28 | - NII files (thanks to nibabel package, new in 2.0.0) 29 | - Synchronization support with [EXACT](https://github.com/ChristianMarzahl/Exact) 30 | - Much enhanced polygon annotation support, including modifying and simplification 31 | - Support for z Stacks as well as time series 32 | - Support for non-clickable annotations (to aid some annotation tasks) (new in 2.0.0) 33 | - New plugins, e.g.: 34 | - WSI co-registration using the method of Jun Jiang [https://doi.org/10.1371/journal.pone.0220074](link) 35 | - Secondary Database visualization 36 | - and uncountable small fixes and improvements 37 | 38 | ## Download and Installation 39 | 40 | For Windows 10 and Mac OS X, we provide a binary file, that you can download here: 41 | 42 | | Operating System | Version | Download link| 43 | |---|---|---| 44 | | Windows 10 (google drive) | V. 2.2.0 | [link](https://drive.google.com/file/d/1wlE6kmBiALWHkdLuHJbXapU6nVKG1oRM/view?usp=sharing) | 45 | | Mac OS X (10.15) (google drive) | V. 2.0.0 | [link](https://drive.google.com/file/d/1xzAnnHaz4LnLGGjNMUCm4aIr-KkfWCAe/view?usp=sharing) | 46 | 47 | ## Updates 48 | 49 | Starting V. 1.31.0, SlideRunner has support for the DICOM WSI image format (thanks, pydicom team, for your support!). Use wsi2dcm to convert images into dicom format. 50 | 51 | Starting V. 1.25.0, SlideRunner features a *magic wand* tool. 52 | 53 | [![Watch the video](https://img.youtube.com/vi/X8NGDqVj3o0/hqdefault.jpg)](https://youtu.be/X8NGDqVj3o0) 54 | 55 | 56 | ## Installation - Source 57 | 58 | SlideRunner is written in Python 3, so you will need a Python 3 distribution like e.g. Anaconda (https://www.anaconda.com/download/) to run it. Further, you need to install 59 | OpenSlide (http://openslide.org/download/). 60 | 61 | ### Install using PIP 62 | 63 | After OpenSlide is installed, we provide a convenient installation by using pip. On Linux or Mac, simply run: 64 | 65 | >sudo pip install -U SlideRunner 66 | 67 | On windows, pip should install without sudo (untested): 68 | 69 | >pip install -U SlideRunner 70 | 71 | ### Installation from repository 72 | 73 | You need to clone this repository: 74 | 75 | >git clone https://github.com/maubreville/SlideRunner 76 | 77 | In order to use the automated installation process, you need to have setuptools installed. 78 | >pip install -U setuptools 79 | 80 | The installation procedure is then as easy as: 81 | >cd SlideRunner 82 | 83 | >python setup.py install 84 | 85 | To run, the following libraries and their dependencies will be installed: 86 | 87 | Library | version | link 88 | ------------------|-------------------|------------------- 89 | PyQT5 | >= 5.6.0 | https://pyqt.sourceforge.net/ 90 | numpy | >= 1.13 | https://www.numpy.org 91 | cv2 (OpenCV3) | >= 3.1.0 | https://opencv.org 92 | sqlite3 | >= 2.6.0 | https://www.sqlite.org 93 | openslide | >= 1.1.1 | https://www.openslide.org 94 | 95 | ## Screenshots 96 | 97 | ![SlideRunner Screenshot](SlideRunner/doc/gui.png) 98 | 99 | ## Usage 100 | 101 | SlideRunner features a number of annotation modes. 102 | 103 | ![Annotation modes](SlideRunner/doc/annotations.png) 104 | 105 | **View/Select** 106 | 107 | This mode is meant for browsing/screening the image, without the intention to add annotations. 108 | 109 | **Object center annotation** 110 | 111 | In this mode, annotations of object centers (e.g. cells) can be made with one click only. Select the class to annotate by clicking the button next to it in the class list or press 112 | the key corresponding to the number in the list of annotations (e.g. key **1** for the first class entry). Objects annotated in this mode are displayed with a circle having a diameter of 50 pixels in full resolution, and scaled appropriately when not viewed in full resolution. 113 | 114 | **Outline annotation (rectangular)** 115 | This mode provides a rectangular annotation tool, that can be used to annotate rectangular patches on the image. 116 | 117 | **Outline annotation (circle) (new in ver 1.8.0)** 118 | This mode provides a circle annotation tool, that can be used to annotate circular patches on the image. 119 | 120 | **Outline annotation (polygon)** 121 | This mode creates a polygon, i.e. a connected line of points corresponding to a single annotation object. This can be handy for cellular structures or tissue types. 122 | 123 | **Annotation of important position** 124 | Important positions are annotations shown with a cross in a circle. The size of this annotation does not change depending on the zooming level. An important position does not 125 | have a class attached to it. 126 | 127 | ## Key Shortcuts 128 | **General** 129 | 130 | | Key         | Function | 131 | |-----------|------------| 132 | | Ctrl + D | Open default database | 133 | | Ctrl + C | Open custom database | 134 | | Ctrl + V | Database overview | 135 | | Ctrl + O | Open image | 136 | | Ctrl + Q | Quit | 137 | 138 | **View** 139 | 140 | | Key | Function | 141 | |-----------|------------| 142 | | + | Zoom in | 143 | | - | Zoom out | 144 | | Ctrl + Alt + C | Create snapshot of current view | 145 | | X | Full plugin opacity | 146 | | V | Half plugin opacity | 147 | | Y | No plugin opacity | 148 | | N | Next frame in screening mode | 149 | 150 | **Annotation** 151 | 152 | | Key                  | Function | 153 | |-----------|------------| 154 | | S | Object center annotation | 155 | | R | Bounding box annotation | 156 | | P | Polygon annotation | 157 | | W | Magic wand annotation | 158 | | F | Important position annotation | 159 | | C | Spot annotation | 160 | | Del | Delete currently selected annotation | 161 | | Esc | Cancel polygon annotation | 162 | | Ctrl + R | Remove last polygon point | 163 | 164 | ## News 165 | 166 | New version 1.22.0 has a much improved plugin interface. Amongst the many improvements are annotation support from the plugin, as well as the ability to copy annotations from the plugin to the database, even annotation groups. 167 | 168 | 169 | ## Databases 170 | 171 | The MITOS2012 [http://ludo17.free.fr/mitos_2012/], MITOS-ATYPIA-2014 [https://mitos-atypia-14.grand-challenge.org/] and TUPAC16 [http://tupac.tue-image.nl] data sets are provided as SlideRunner database in the repository. Please download the original images (*.bmp, *.tif) in order to use them. 172 | 173 | ## Tools 174 | 175 | Mingrui Jiang has written a tool to extract patches around ROIS. Find it here: https://github.com/mingrui/SlideRunner-ROI-Patch-Maker 176 | 177 | ## Demo workflows 178 | 179 | To get started how to use SlideRunners databases, have a look into this [notebook](https://github.com/maubreville/SlideRunner/blob/master/SlideRunner/doc/DemoWorkflow_WorkingWithSlideRunnerDatabases.ipynb). 180 | 181 | ## Plugins 182 | 183 | There are currently three plug-ins available: 184 | 185 | - OTSU threshold: A simple OTSU threshold plugin. Positive pixels are shown in green color. 186 | - PPC: Positive Pixel Count, as in: Olson, Allen H. "Image analysis using the Aperio ScanScope." Technical manual. Aperio Technologies Inc (2006). 187 | - Macenko normalization: Image-based normalization using the method of Macenko et al. [link](https://ieeexplore.ieee.org/document/5193250/) 188 | 189 | ## Database structure 190 | 191 | The major entity of our database model is the annotation. Each annotation can have multiple coordinates, with their respective x and y coordinates, and the order they were drawn (for polygons). Further, each annotation has a multitude of labels that were given by one person each and are belonging to one class, respectively. 192 | 193 | ![DB Structure](SlideRunner/doc/SlideRunner_UML.png) 194 | 195 | 196 | ## Troubleshooting 197 | 198 | **Programm won't start, error message: Missing or nonfunctional package openslide: Couldn't locate OpenSlide dylib. Is OpenSlide installed?** 199 | 200 | Unfortunately, the OpenSlide binary libraries can't be fetched using pip. You need to install OpenSlide libraries (and dependencies). See http://openslide.org/download/ for more. 201 | 202 | -------------------------------------------------------------------------------- /SlideRunner/Slides.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/Slides.sqlite -------------------------------------------------------------------------------- /SlideRunner/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | 18 | __all__ = ['SlideRunner'] 19 | 20 | -------------------------------------------------------------------------------- /SlideRunner/__main__.py: -------------------------------------------------------------------------------- 1 | import rollbar 2 | import multiprocessing 3 | from SlideRunner_dataAccess.slide import SlideReader 4 | import PyQt5 5 | from PyQt5 import QtWidgets 6 | from SlideRunner.gui import splashScreen 7 | import sys 8 | 9 | version = '2.2.0' 10 | 11 | rollbar.init('98503f735c5648f5ae21b6c18e04926a') 12 | def main(): 13 | try: 14 | multiprocessing.freeze_support() 15 | multiprocessing.set_start_method('spawn') 16 | slideReaderThread = SlideReader() 17 | slideReaderThread.start() 18 | app = QtWidgets.QApplication(sys.argv) 19 | splash = splashScreen.splashScreen(app, version) 20 | import SlideRunner.general.pluginFinder 21 | pluginList = SlideRunner.general.pluginFinder.pluginList 22 | from SlideRunner import SlideRunner 23 | SlideRunner.main(slideReaderThread=slideReaderThread, app=app, splash=splash, 24 | version=version, pluginList=pluginList) 25 | 26 | except Exception as e: 27 | # catch-all 28 | import traceback 29 | print('Error: ',e) 30 | traceback.print_exc() 31 | # rollbar.report_exc_info() 32 | # equivalent to rollbar.report_exc_info(sys.exc_info()) 33 | print('An error has been reported to rollbar.') 34 | 35 | 36 | if __name__ == '__main__': 37 | main() 38 | 39 | -------------------------------------------------------------------------------- /SlideRunner/artwork/AboutScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/AboutScreen.png -------------------------------------------------------------------------------- /SlideRunner/artwork/ExactWelcomeScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/ExactWelcomeScreen.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.icns -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_128x128.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_128x128@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_128x128@2.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_16x16.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_16x16@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_16x16@2.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_256x256.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_256x256@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_256x256@2.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_32x32.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_32x32@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_32x32@2.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_512x512.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunner.iconset/icon_512x512@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunner.iconset/icon_512x512@2.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SlideRunnerDMG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SlideRunnerDMG.png -------------------------------------------------------------------------------- /SlideRunner/artwork/SplashScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/SplashScreen.png -------------------------------------------------------------------------------- /SlideRunner/artwork/annoInOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/annoInOverview.png -------------------------------------------------------------------------------- /SlideRunner/artwork/backArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/backArrow.png -------------------------------------------------------------------------------- /SlideRunner/artwork/blindedMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/blindedMode.png -------------------------------------------------------------------------------- /SlideRunner/artwork/drawCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/drawCircle.png -------------------------------------------------------------------------------- /SlideRunner/artwork/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon.ico -------------------------------------------------------------------------------- /SlideRunner/artwork/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconArrow.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconBlinded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconBlinded.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconCircle.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconOverlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconOverlay.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconPolygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconPolygon.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconQuestion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconQuestion.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconRect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconRect.png -------------------------------------------------------------------------------- /SlideRunner/artwork/iconWand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/iconWand.png -------------------------------------------------------------------------------- /SlideRunner/artwork/icon_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon_flag.png -------------------------------------------------------------------------------- /SlideRunner/artwork/icon_nextView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon_nextView.png -------------------------------------------------------------------------------- /SlideRunner/artwork/icon_previousView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon_previousView.png -------------------------------------------------------------------------------- /SlideRunner/artwork/icon_screeningMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/artwork/icon_screeningMode.png -------------------------------------------------------------------------------- /SlideRunner/dataAccess/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images 13 | Bildverarbeitung fuer die Medizin 2018, Springer Verlag, Berlin-Heidelberg 14 | 15 | """ 16 | 17 | __all__ = [] 18 | 19 | 20 | 21 | 22 | import SlideRunner_dataAccess.annotations as annotations 23 | import SlideRunner_dataAccess.database as database 24 | import SlideRunner_dataAccess.nifty as nifty 25 | import SlideRunner_dataAccess.slide as slide 26 | 27 | from SlideRunner_dataAccess.slide import os_fileformats 28 | from SlideRunner_dataAccess.nifty import nii_fileformats 29 | from SlideRunner_dataAccess.dicomimage import dic_fileformats 30 | fileformats = os_fileformats + nii_fileformats + dic_fileformats 31 | -------------------------------------------------------------------------------- /SlideRunner/dataAccess/annotations.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | warnings.warn('Deprecated package. Please use the SlideRunner_dataAccess package, and not the SlideRunner main package for data access.') 3 | 4 | from SlideRunner_dataAccess.annotations import * 5 | 6 | -------------------------------------------------------------------------------- /SlideRunner/dataAccess/database.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | warnings.warn('Deprecated package. Please use the SlideRunner_dataAccess package, and not the SlideRunner main package for data access.') 3 | 4 | from SlideRunner_dataAccess.database import * 5 | 6 | -------------------------------------------------------------------------------- /SlideRunner/doc/MagicWandAnnotation.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/MagicWandAnnotation.mov -------------------------------------------------------------------------------- /SlideRunner/doc/SlideRunner_UML.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/SlideRunner_UML.pdf -------------------------------------------------------------------------------- /SlideRunner/doc/SlideRunner_UML.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/SlideRunner_UML.png -------------------------------------------------------------------------------- /SlideRunner/doc/annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/annotations.png -------------------------------------------------------------------------------- /SlideRunner/doc/demo.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/demo.sqlite -------------------------------------------------------------------------------- /SlideRunner/doc/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/gui.png -------------------------------------------------------------------------------- /SlideRunner/doc/logoline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/SlideRunner/doc/logoline.png -------------------------------------------------------------------------------- /SlideRunner/general/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images 13 | Bildverarbeitung fuer die Medizin 2018, Springer Verlag, Berlin-Heidelberg 14 | 15 | """ 16 | 17 | __all__ = ['dependencies'] -------------------------------------------------------------------------------- /SlideRunner/general/dependencies.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images 13 | Bildverarbeitung fuer die Medizin 2018, Springer Verlag, Berlin-Heidelberg 14 | 15 | """ 16 | from PyQt5 import QtWidgets 17 | import re 18 | 19 | def numeric(text) -> str: 20 | """ 21 | converts a string including numbers to a string not including numbers any more 22 | """ 23 | return ''.join(re.findall('\d+', text )) 24 | 25 | def check_version(actual:str, target:str) -> bool: 26 | if (target is None): 27 | return True 28 | actual_tup = actual.split('.') 29 | target_tup = target.split('.') 30 | 31 | try: 32 | for idx in range(len(target_tup)): 33 | if int(numeric(target_tup[idx]))>int(numeric(actual_tup[idx])): 34 | return False 35 | if int(numeric(target_tup[idx]))= 5.6.0.' % QT_VERSION_STR) 58 | sys.exit(1) 59 | 60 | def check_all_dependencies(): 61 | 62 | 63 | libraries_with_versions = [ 64 | ('numpy', 'np', '1.13'), 65 | ('matplotlib', 'mp', '2.0.0'), 66 | ('cv2','cv2','3.1.0'), 67 | ('sqlite3','sqlite3', '2.6.0'), 68 | ('time','time',None), 69 | ('random','random',None), 70 | ('os','os',None), 71 | ('threading','threading',None), 72 | ('queue','queue', None), 73 | ('logging','logging','0.5'), 74 | ('signal','signal', None), 75 | ('matplotlib.path','path',None), 76 | ('rollbar','rollbar', '0.14'), 77 | ('time','time',None), 78 | ('shapely','shapely','1.5.9'), 79 | ('requests_toolbelt','requests_toolbelt','0.9.1'), 80 | ('functools','functools',None), 81 | ('openslide','openslide','1.1.1')] # (library_name, shorthand) 82 | 83 | for (name, short, version) in libraries_with_versions: 84 | try: 85 | lib = __import__(name) 86 | except: 87 | print('Missing or nonfunctional package %s: %s' % (name, sys.exc_info()[1])) 88 | sys.exit() 89 | else: 90 | globals()[short] = lib 91 | if ((hasattr(lib,'__version__') and not check_version(lib.__version__,version)) or 92 | (hasattr(lib,'version') and type(lib.version)==str and not check_version(lib.version,version))): 93 | 94 | reply = QtWidgets.QMessageBox.information(None, 'Error', 95 | 'Too old package %s: Version should be at least %s' % (name, version), QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) 96 | 97 | 98 | import sys 99 | import traceback 100 | check_qt_dependencies() 101 | 102 | check_all_dependencies() 103 | 104 | # The rest should now load fine 105 | 106 | # More PyQt5 imports 107 | from PyQt5.QtCore import QThread, QStringListModel, Qt, pyqtSignal 108 | from PyQt5.QtGui import QPixmap, QColor, QImage, QStandardItemModel, QStandardItem, QBrush, QIcon, QKeySequence 109 | #from PyQt5.QtWidgets import * 110 | from PyQt5.QtWidgets import QDialog, QWidget, QFileDialog, QMenu,QInputDialog, QAction, QPushButton, QItemDelegate, QTableWidgetItem, QCheckBox 111 | 112 | # internal imports 113 | from SlideRunner.gui.SlideRunner_ui import Ui_MainWindow 114 | from SlideRunner.gui.dialogs.about import aboutDialog 115 | from SlideRunner.gui.dialogs.welcomeExact import welcomeExactDialog 116 | from SlideRunner.gui.dialogs.question import YesNoAbortDialog 117 | from SlideRunner.gui.dialogs.settings import settingsDialog 118 | from SlideRunner.gui.dialogs.dbmanager import DatabaseManager 119 | from SlideRunner.gui.dialogs.exactLinkDialog import ExactLinkDialog 120 | from SlideRunner.dataAccess.exact import ExactManager, ExactProcessError 121 | from SlideRunner.gui.dialogs.exactDownloadDialog import ExactDownloadDialog 122 | from SlideRunner_dataAccess.slide import RotatableOpenSlide 123 | from SlideRunner.gui.dialogs.getCoordinates import getCoordinatesDialog 124 | from SlideRunner.gui import shortcuts, toolbar, mouseEvents, annotation 125 | from SlideRunner_dataAccess.database import Database, annotation, hex_to_rgb, rgb_to_hex 126 | from SlideRunner.processing import screening, thumbnail 127 | from SlideRunner.general import SlideRunnerPlugin 128 | from SlideRunner.gui.types import * 129 | from SlideRunner.gui.sidebar import * 130 | from SlideRunner.general.types import pluginEntry 131 | 132 | import matplotlib.cm 133 | partial = functools.partial 134 | path = path.path 135 | -------------------------------------------------------------------------------- /SlideRunner/general/pluginFinder.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.plugins 2 | import pkgutil 3 | import importlib 4 | import inspect 5 | 6 | from SlideRunner.general.types import pluginEntry 7 | 8 | def iter_namespace(ns_pkg): 9 | # Specifying the second argument (prefix) to iter_modules makes the 10 | # returned name an absolute name instead of a relative one. This allows 11 | # import_module to work without having to do additional modification to 12 | # the name. 13 | return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".") 14 | 15 | sliderunner_plugins={} 16 | 17 | for finder, name, ispkg in sorted(iter_namespace(SlideRunner.plugins)): 18 | try: 19 | mod = importlib.import_module(name) 20 | sliderunner_plugins[name] = mod 21 | except Exception as e: 22 | print('+++ Unable to active plugin: '+name,e) 23 | pass 24 | 25 | 26 | pluginList = list() 27 | shortNames=[] 28 | 29 | for plugin in sorted(sliderunner_plugins.keys()): 30 | newPlugin = pluginEntry() 31 | classes = inspect.getmembers(sliderunner_plugins[plugin], inspect.isclass) 32 | for classIdx in range(len(classes)): 33 | if (classes[classIdx][0] == 'Plugin'): 34 | newPlugin = classes[classIdx][1] 35 | if newPlugin.shortName in shortNames: 36 | print('++++ ERROR: Plugin has duplicate short name: ',newPlugin.shortName) 37 | continue 38 | pluginList.append(newPlugin) 39 | 40 | 41 | print('List of available plugins:') 42 | for entry in pluginList: 43 | print('%20s Version %s' % (entry.shortName, entry.version)) 44 | 45 | -------------------------------------------------------------------------------- /SlideRunner/general/types.py: -------------------------------------------------------------------------------- 1 | class pluginEntry: 2 | mainClass = None 3 | commonName = None 4 | plugin = None 5 | inQueue = None 6 | outQueue = None 7 | version = None 8 | receiverThread = None 9 | 10 | def __str__(self): 11 | return self.commonName -------------------------------------------------------------------------------- /SlideRunner/gui/SlideRunner_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'SlideRunner_ui.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.10 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | from SlideRunner.gui.zoomSlider import * 11 | from SlideRunner.gui.frameSlider import * 12 | 13 | class Ui_MainWindow(object): 14 | def setupUi(self, MainWindow): 15 | MainWindow.setObjectName("MainWindow") 16 | MainWindow.setWindowModality(QtCore.Qt.NonModal) 17 | MainWindow.resize(905, 821) 18 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 19 | sizePolicy.setHorizontalStretch(0) 20 | sizePolicy.setVerticalStretch(0) 21 | sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) 22 | MainWindow.setSizePolicy(sizePolicy) 23 | MainWindow.setFocusPolicy(QtCore.Qt.StrongFocus) 24 | MainWindow.setAcceptDrops(True) 25 | MainWindow.setDockOptions(QtWidgets.QMainWindow.AllowNestedDocks|QtWidgets.QMainWindow.AllowTabbedDocks|QtWidgets.QMainWindow.AnimatedDocks) 26 | MainWindow.setUnifiedTitleAndToolBarOnMac(True) 27 | self.centralwidget = QtWidgets.QWidget(MainWindow) 28 | self.centralwidget.setObjectName("centralwidget") 29 | self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) 30 | self.gridLayout.setObjectName("gridLayout") 31 | self.horizontalLayout = QtWidgets.QHBoxLayout() 32 | self.horizontalLayout.setObjectName("horizontalLayout") 33 | self.verticalLayout = QtWidgets.QVBoxLayout() 34 | self.verticalLayout.setObjectName("verticalLayout") 35 | self.MainImage = QtWidgets.QLabel(self.centralwidget) 36 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) 37 | sizePolicy.setHorizontalStretch(0) 38 | sizePolicy.setVerticalStretch(0) 39 | sizePolicy.setHeightForWidth(self.MainImage.sizePolicy().hasHeightForWidth()) 40 | self.MainImage.setSizePolicy(sizePolicy) 41 | self.MainImage.setMinimumSize(QtCore.QSize(400, 400)) 42 | self.MainImage.setLineWidth(0) 43 | self.MainImage.setAlignment(QtCore.Qt.AlignCenter) 44 | self.MainImage.setObjectName("MainImage") 45 | self.frameSlider = frameSlider() 46 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 47 | sizePolicy.setHorizontalStretch(0) 48 | sizePolicy.setVerticalStretch(0) 49 | sizePolicy.setHeightForWidth(self.frameSlider.sizePolicy().hasHeightForWidth()) 50 | self.frameSlider.setSizePolicy(sizePolicy) 51 | self.verticalLayout.addWidget(self.frameSlider) 52 | self.verticalLayout.addWidget(self.MainImage) 53 | self.horizontalScrollBar = QtWidgets.QScrollBar(self.centralwidget) 54 | self.horizontalScrollBar.setMaximum(999) 55 | self.horizontalScrollBar.setOrientation(QtCore.Qt.Horizontal) 56 | self.horizontalScrollBar.setObjectName("horizontalScrollBar") 57 | self.verticalLayout.addWidget(self.horizontalScrollBar) 58 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 59 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 60 | self.zoomSlider = zoomSlider() 61 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 62 | sizePolicy.setHorizontalStretch(0) 63 | sizePolicy.setVerticalStretch(0) 64 | sizePolicy.setHeightForWidth(self.zoomSlider.sizePolicy().hasHeightForWidth()) 65 | self.zoomSlider.setSizePolicy(sizePolicy) 66 | # self.zoomSlider.setHidden(True) 67 | self.verticalLayout.addWidget(self.zoomSlider) 68 | self.verticalLayout.addLayout(self.horizontalLayout_2) 69 | self.horizontalLayout.addLayout(self.verticalLayout) 70 | self.verticalScrollBar = QtWidgets.QScrollBar(self.centralwidget) 71 | self.verticalScrollBar.setOrientation(QtCore.Qt.Vertical) 72 | self.verticalScrollBar.setObjectName("verticalScrollBar") 73 | self.horizontalLayout.addWidget(self.verticalScrollBar) 74 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) 75 | self.horizontalLayout.addItem(spacerItem) 76 | self.sidebarLayout = QtWidgets.QVBoxLayout() 77 | self.sidebarLayout.setObjectName("sidebarLayout") 78 | self.OverviewLabel = QtWidgets.QLabel(self.centralwidget) 79 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) 80 | sizePolicy.setHorizontalStretch(0) 81 | sizePolicy.setVerticalStretch(0) 82 | sizePolicy.setHeightForWidth(self.OverviewLabel.sizePolicy().hasHeightForWidth()) 83 | self.OverviewLabel.setSizePolicy(sizePolicy) 84 | self.OverviewLabel.setMinimumSize(QtCore.QSize(200, 200)) 85 | self.OverviewLabel.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 86 | self.OverviewLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) 87 | self.OverviewLabel.setObjectName("OverviewLabel") 88 | self.sidebarLayout.addWidget(self.OverviewLabel) 89 | self.filenameLabel = QtWidgets.QLabel(self.centralwidget) 90 | self.filenameLabel.setObjectName("filenameLabel") 91 | self.sidebarLayout.addWidget(self.filenameLabel) 92 | self.databaseLabel = QtWidgets.QLabel(self.centralwidget) 93 | self.databaseLabel.setObjectName("databaseLabel") 94 | self.sidebarLayout.addWidget(self.databaseLabel) 95 | self.horizontalLayout.addLayout(self.sidebarLayout) 96 | self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1) 97 | MainWindow.setCentralWidget(self.centralwidget) 98 | self.menubar = QtWidgets.QMenuBar(MainWindow) 99 | self.menubar.setGeometry(QtCore.QRect(0, 0, 905, 22)) 100 | self.menubar.setObjectName("menubar") 101 | self.menuDatabase = QtWidgets.QMenu(self.menubar) 102 | self.menuDatabase.setObjectName("menuDatabase") 103 | self.menuFile = QtWidgets.QMenu(self.menubar) 104 | self.menuFile.setObjectName("menuFile") 105 | self.menuAnnotation = QtWidgets.QMenu(self.menubar) 106 | self.menuAnnotation.setObjectName("menuAnnotation") 107 | self.menuHelp = QtWidgets.QMenu(self.menubar) 108 | self.menuHelp.setObjectName("menuHelp") 109 | MainWindow.setMenuBar(self.menubar) 110 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 111 | self.statusbar.setObjectName("statusbar") 112 | MainWindow.setStatusBar(self.statusbar) 113 | self.action_Open = QtWidgets.QAction(MainWindow) 114 | self.action_Open.setObjectName("action_Open") 115 | self.actionOpen = QtWidgets.QAction(MainWindow) 116 | self.actionOpen.setObjectName("actionOpen") 117 | self.action_Close = QtWidgets.QAction(MainWindow) 118 | self.action_Close.setObjectName("action_Close") 119 | self.action_Quit = QtWidgets.QAction(MainWindow) 120 | self.action_Quit.setObjectName("action_Quit") 121 | self.actionOpen_custom = QtWidgets.QAction(MainWindow) 122 | self.actionOpen_custom.setObjectName("actionOpen_custom") 123 | self.actionMode = QtWidgets.QAction(MainWindow) 124 | self.actionMode.setObjectName("actionMode") 125 | self.actionAdd_annotator = QtWidgets.QAction(MainWindow) 126 | self.actionAdd_annotator.setEnabled(False) 127 | self.actionAdd_annotator.setObjectName("actionAdd_annotator") 128 | self.actionAdd_cell_class = QtWidgets.QAction(MainWindow) 129 | self.actionAdd_cell_class.setEnabled(False) 130 | self.actionAdd_cell_class.setObjectName("actionAdd_cell_class") 131 | self.actionCreate_new = QtWidgets.QAction(MainWindow) 132 | self.actionCreate_new.setObjectName("actionCreate_new") 133 | self.actionAbout = QtWidgets.QAction(MainWindow) 134 | self.actionAbout.setObjectName("actionAbout") 135 | self.menuDatabase.addAction(self.actionCreate_new) 136 | self.menuDatabase.addAction(self.action_Open) 137 | self.menuDatabase.addAction(self.actionOpen_custom) 138 | self.menuDatabase.addSeparator() 139 | self.menuDatabase.addAction(self.actionAdd_annotator) 140 | self.menuDatabase.addAction(self.actionAdd_cell_class) 141 | self.menuFile.addAction(self.actionOpen) 142 | self.menuFile.addAction(self.action_Close) 143 | self.menuFile.addSeparator() 144 | self.menuFile.addAction(self.action_Quit) 145 | self.menuHelp.addAction(self.actionAbout) 146 | self.menubar.addAction(self.menuDatabase.menuAction()) 147 | self.menubar.addAction(self.menuFile.menuAction()) 148 | self.menubar.addAction(self.menuAnnotation.menuAction()) 149 | self.menubar.addAction(self.menuHelp.menuAction()) 150 | 151 | self.retranslateUi(MainWindow) 152 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 153 | 154 | def retranslateUi(self, MainWindow): 155 | _translate = QtCore.QCoreApplication.translate 156 | MainWindow.setWindowTitle(_translate("MainWindow", "SlideRunner")) 157 | self.MainImage.setText(_translate("MainWindow", "No Image Open")) 158 | self.OverviewLabel.setText(_translate("MainWindow", "TextLabel")) 159 | self.filenameLabel.setText(_translate("MainWindow", "TextLabel")) 160 | self.databaseLabel.setText(_translate("MainWindow", "No database opened.")) 161 | self.menuDatabase.setTitle(_translate("MainWindow", "Database")) 162 | self.menuFile.setTitle(_translate("MainWindow", "Image")) 163 | self.menuAnnotation.setTitle(_translate("MainWindow", "Annotation")) 164 | self.menuHelp.setTitle(_translate("MainWindow", "Help")) 165 | self.action_Open.setText(_translate("MainWindow", "&Open default")) 166 | self.actionOpen.setText(_translate("MainWindow", "Open")) 167 | self.action_Close.setText(_translate("MainWindow", "&Close")) 168 | self.action_Quit.setText(_translate("MainWindow", "&Quit")) 169 | self.actionOpen_custom.setText(_translate("MainWindow", "Open custom")) 170 | self.actionMode.setText(_translate("MainWindow", "Mode")) 171 | self.actionAdd_annotator.setText(_translate("MainWindow", "Add annotator")) 172 | self.actionAdd_cell_class.setText(_translate("MainWindow", "Add annotation class")) 173 | self.actionCreate_new.setText(_translate("MainWindow", "Create new")) 174 | self.actionAbout.setText(_translate("MainWindow", "About")) 175 | 176 | -------------------------------------------------------------------------------- /SlideRunner/gui/SlideRunner_ui.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | Qt::NonModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 905 13 | 821 14 | 15 | 16 | 17 | 18 | 0 19 | 0 20 | 21 | 22 | 23 | Qt::StrongFocus 24 | 25 | 26 | true 27 | 28 | 29 | SlideRunner 30 | 31 | 32 | QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 0 48 | 0 49 | 50 | 51 | 52 | 53 | 400 54 | 400 55 | 56 | 57 | 58 | 0 59 | 60 | 61 | No Slide Open 62 | 63 | 64 | Qt::AlignCenter 65 | 66 | 67 | 68 | 69 | 70 | 71 | 999 72 | 73 | 74 | Qt::Horizontal 75 | 76 | 77 | 78 | 79 | 80 | 81 | -1 82 | 83 | 84 | 85 | 86 | 87 | 0 88 | 0 89 | 90 | 91 | 92 | Zoom Level 93 | 94 | 95 | Qt::Horizontal 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 0 104 | 0 105 | 106 | 107 | 108 | 109 | 0 110 | 10 111 | 112 | 113 | 114 | 0x 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Qt::Vertical 126 | 127 | 128 | 129 | 130 | 131 | 132 | Qt::Vertical 133 | 134 | 135 | 136 | 20 137 | 40 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 0 149 | 0 150 | 151 | 152 | 153 | 154 | 60 155 | 10 156 | 157 | 158 | 159 | Overview 160 | 161 | 162 | Qt::AlignCenter 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 0 171 | 0 172 | 173 | 174 | 175 | 176 | 200 177 | 200 178 | 179 | 180 | 181 | PointingHandCursor 182 | 183 | 184 | TextLabel 185 | 186 | 187 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 188 | 189 | 190 | 191 | 192 | 193 | 194 | TextLabel 195 | 196 | 197 | 198 | 199 | 200 | 201 | No database opened. 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 0 215 | 0 216 | 905 217 | 22 218 | 219 | 220 | 221 | 222 | Database 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | Slide 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | Annotation 243 | 244 | 245 | 246 | 247 | Help 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | &Open default 260 | 261 | 262 | 263 | 264 | Open 265 | 266 | 267 | 268 | 269 | &Close 270 | 271 | 272 | 273 | 274 | &Quit 275 | 276 | 277 | 278 | 279 | Open custom 280 | 281 | 282 | 283 | 284 | Mode 285 | 286 | 287 | 288 | 289 | false 290 | 291 | 292 | Add annotator 293 | 294 | 295 | 296 | 297 | false 298 | 299 | 300 | Add annotation class 301 | 302 | 303 | 304 | 305 | Create new 306 | 307 | 308 | 309 | 310 | About 311 | 312 | 313 | 314 | 315 | 316 | 317 | -------------------------------------------------------------------------------- /SlideRunner/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | 18 | __all__ = ['shortcuts','SlideRunner_ui', 'splashScreen', 19 | 'mouseEvents', 'annotation', 'menu', 'toolbar','types','style','dialogs'] -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | 18 | __all__ = ['about', 'settings', 'question', 'dbmanager'] -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/about.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | from PyQt5 import QtGui, QtWidgets, QtCore 18 | import sys 19 | from time import time,sleep 20 | from PyQt5.QtGui import QPixmap 21 | from PyQt5.QtWidgets import QDialog, QApplication, QMainWindow 22 | from PyQt5.QtWidgets import QSplashScreen 23 | from PyQt5.QtCore import Qt 24 | from functools import partial 25 | 26 | import os 27 | ARTWORK_DIR_NAME = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))+os.sep+'artwork'+os.sep 28 | 29 | 30 | def closeDlg(splash,e): 31 | splash.close() 32 | 33 | def aboutDialog(app,version): 34 | 35 | # Create and display the about screen 36 | splash_pix = QPixmap(ARTWORK_DIR_NAME+'AboutScreen.png') 37 | splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) 38 | 39 | splash.showMessage('Version %s\n'%version, alignment = Qt.AlignHCenter + Qt.AlignBottom, color=Qt.black) 40 | splash.setMask(splash_pix.mask()) 41 | splash.show() 42 | 43 | splash.mousePressEvent = partial(closeDlg, splash) 44 | 45 | start = time() 46 | while splash.isActiveWindow() & (time() - start < 10): 47 | sleep(0.001) 48 | app.processEvents() 49 | 50 | if (splash.isActiveWindow()): 51 | splash.close() 52 | 53 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/dbmanager.py: -------------------------------------------------------------------------------- 1 | from SlideRunner.general.dependencies import * 2 | from SlideRunner_dataAccess.database import Database 3 | from functools import partial 4 | from PyQt5 import QtCore, QtGui, QtWidgets 5 | from PyQt5.QtCore import pyqtSlot 6 | import sys 7 | import os 8 | from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QAction, QTableWidget,QTableWidgetItem,QVBoxLayout 9 | from PyQt5.QtGui import QIcon 10 | 11 | class DatabaseManager(QDialog): 12 | 13 | def __init__(self, DB:Database): 14 | super().__init__() 15 | self.title = 'Database overview (%s)' % DB.dbfilename 16 | self.left = 50 17 | self.top = 50 18 | self.width = 600 19 | self.height = 500 20 | self.DB = DB 21 | self.loadSlide = '' 22 | self.setModal(True) 23 | self.initUI() 24 | 25 | def initUI(self): 26 | self.setWindowTitle(self.title) 27 | self.setGeometry(self.left, self.top, self.width, self.height) 28 | 29 | self.createTable() 30 | 31 | self.layout = QtWidgets.QVBoxLayout() 32 | self.layout.addWidget(self.tableWidget) 33 | self.setLayout(self.layout) 34 | 35 | self.show() 36 | 37 | def updateTable(self): 38 | DB = self.DB 39 | fileToAnnos = {slide:count for slide,count in DB.execute('SELECT slide, COUNT(*) FROM Annotations group by slide').fetchall()} 40 | self.tableWidget.setRowCount(len(DB.listOfSlidesWithExact())) 41 | self.los = DB.listOfSlidesWithExact() 42 | for row,(idx,filename,exactid,directory) in enumerate(self.los): 43 | self.tableWidget.setItem(row,0, QTableWidgetItem(str(idx))) 44 | self.tableWidget.setItem(row,1, QTableWidgetItem(str(filename))) 45 | self.tableWidget.setItem(row,2, QTableWidgetItem(str(fileToAnnos[int(idx)]) if (int(idx)) in fileToAnnos else '0')) 46 | self.tableWidget.setItem(row,3, QTableWidgetItem(str(exactid))) 47 | 48 | btn = QPushButton('open') 49 | self.tableWidget.setCellWidget(row,4, btn) 50 | 51 | btn.clicked.connect(partial(self.loadFile, self.los[row])) 52 | 53 | 54 | def createTable(self): 55 | # Create table 56 | self.tableWidget = QTableWidget() 57 | self.tableWidget.setColumnCount(5) 58 | self.tableWidget.setHorizontalHeaderLabels(['ID','Filename','Annotations', 'EXACT Image ID', '']) 59 | self.updateTable() 60 | self.tableWidget.move(0,0) 61 | self.tableWidget.resizeColumnsToContents() 62 | self.tableWidget.viewport().installEventFilter(self) 63 | self.tableWidget.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers) 64 | 65 | def loadFile(self, uid): 66 | 67 | slidepath=(uid[-1]+os.sep+uid[1]) if os.path.exists((str(uid[-1])+os.sep+str(uid[1]))) else uid[1] 68 | 69 | if (os.path.exists(slidepath)): 70 | self.loadSlide = slidepath 71 | self.close() 72 | else: 73 | QtWidgets.QMessageBox.information(self,'Not found',f'The file {slidepath} could not be found.') 74 | 75 | 76 | def removeFile(self, uid): 77 | reply = QtWidgets.QMessageBox.question(self, 'Question', 78 | 'Do you really want to delete the file and all annotations from the database?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 79 | 80 | if reply == QtWidgets.QMessageBox.No: 81 | return 82 | self.DB.removeFileFromDatabase(uid[0]) 83 | self.DB.commit() 84 | self.updateTable() 85 | self.show() 86 | 87 | def removeExactLink(self, uids:list): 88 | if (len(uids)>1): 89 | reply = QtWidgets.QMessageBox.question(self, 'Question', 90 | 'Do you really want to remove the link to EXACT for these slides?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 91 | else: 92 | reply = QtWidgets.QMessageBox.question(self, 'Question', 93 | 'Do you really want to remove the link to EXACT for this slide?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 94 | 95 | if reply == QtWidgets.QMessageBox.No: 96 | return 97 | for uid in uids: 98 | self.DB.execute(f'UPDATE Slides SET exactImageID=Null where uid=={uid}') 99 | self.DB.commit() 100 | self.updateTable() 101 | self.show() 102 | 103 | def eventFilter(self, source, event): 104 | if(event.type() == QtCore.QEvent.MouseButtonPress and 105 | event.buttons() == QtCore.Qt.RightButton and 106 | source is self.tableWidget.viewport()): 107 | rows = np.unique([x.row() for x in self.tableWidget.selectedItems()]) 108 | rowuids = [self.los[x][0] for x in rows] 109 | item = self.tableWidget.itemAt(event.pos()) 110 | if item is not None: 111 | menu = QMenu(self) 112 | menu.addAction('Open '+self.los[item.row()][1], partial(self.loadFile, self.los[item.row()])) #(QAction('test')) 113 | menu.addAction('Remove '+self.los[item.row()][1], partial(self.removeFile, self.los[item.row()])) #(QAction('test')) 114 | if (len(rows)>1): 115 | menu.addAction('Remove EXACT link selected rows ', partial(self.removeExactLink, rowuids)) #(QAction('test')) 116 | else: 117 | menu.addAction('Remove EXACT link for '+self.los[item.row()][1], partial(self.removeExactLink, rowuids)) #(QAction('test')) 118 | menu.exec_(event.globalPos()) 119 | return super(DatabaseManager, self).eventFilter(source, event) 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/exactDownloadDialog.py: -------------------------------------------------------------------------------- 1 | from SlideRunner.general.dependencies import * 2 | from SlideRunner_dataAccess.database import Database 3 | from functools import partial 4 | from PyQt5 import QtCore, QtGui, QtWidgets 5 | from PyQt5.QtCore import pyqtSlot 6 | import sys 7 | from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QAction, QTableWidget,QTableWidgetItem,QVBoxLayout 8 | from PyQt5.QtGui import QIcon 9 | from SlideRunner.dataAccess.exact import * 10 | from functools import partial 11 | 12 | class ExactDownloadDialog(QDialog): 13 | 14 | def __init__(self, DB:Database, settingsObject): 15 | super().__init__() 16 | self.title = 'Download from EXACT (%s)' % settingsObject.value('exactHostname', 'https://exact.cs.fau.de').replace('//','//'+settingsObject.value('exactUsername', 'Demo')+'@') 17 | self.left = 50 18 | self.top = 50 19 | self.width = 900 20 | self.height = 500 21 | self.DB = DB 22 | self.setModal(True) 23 | hostname = settingsObject.value('exactHostname', 'https://exact.cs.fau.de') 24 | self.exm = ExactManager(settingsObject.value('exactUsername', 'Demo'), 25 | settingsObject.value('exactPassword', 'demodemo'), 26 | hostname) 27 | self.initUI() 28 | 29 | def initUI(self): 30 | self.setWindowTitle(self.title) 31 | self.setGeometry(self.left, self.top, self.width, self.height) 32 | 33 | self.createTable() 34 | 35 | self.layout = QtWidgets.QVBoxLayout() 36 | self.layout.addWidget(self.tableWidget) 37 | self.setLayout(self.layout) 38 | 39 | self.show() 40 | 41 | def updateTable(self): 42 | def image_and_product_to_id(image_id, product_id, imageset_id): 43 | return str(image_id)+'/'+str(product_id)+'/'+str(imageset_id) 44 | 45 | DB = self.DB 46 | fileToAnnos = {exactimageid:slide for slide,exactimageid in DB.execute('SELECT filename, exactImageID from Slides').fetchall()} 47 | self.loi = self.exm.APIs.image_sets_api.list_image_sets(pagination=False, expand='product_set,images', omit='images.annotations').results 48 | 49 | 50 | 51 | rowcount=0 52 | self.los = [] 53 | for imageset in self.loi: 54 | for row,image in enumerate(imageset.images): 55 | for product in imageset.product_set: 56 | self.los.append([image_and_product_to_id(image['id'], product['id'],imageset.id),imageset.name+'('+product['name']+')', str(image['name']),fileToAnnos[image_and_product_to_id(image['id'], product['id'],imageset.id)] if image_and_product_to_id(image['id'], product['id'],imageset.id) in fileToAnnos else '' ]) 57 | rowcount+=1 58 | self.tableWidget.setRowCount(rowcount) 59 | for row,(id,imset,im,linked) in enumerate(self.los): 60 | self.tableWidget.setItem(row,0, QTableWidgetItem(str(id))) 61 | self.tableWidget.setItem(row,1, QTableWidgetItem(str(imset))) 62 | self.tableWidget.setItem(row,2, QTableWidgetItem(str(im))) 63 | self.tableWidget.setItem(row,3, QTableWidgetItem(str(linked))) 64 | btn = QPushButton('download') 65 | self.tableWidget.setCellWidget(row,4, btn) 66 | btn.clicked.connect(partial(self.download, self.los[row])) 67 | 68 | def createTable(self): 69 | # Create table 70 | self.tableWidget = QTableWidget() 71 | self.tableWidget.setColumnCount(5) 72 | self.tableWidget.setHorizontalHeaderLabels(['ID','Imageset','Image','Assigned to']) 73 | self.updateTable() 74 | self.tableWidget.move(0,0) 75 | self.tableWidget.resizeColumnsToContents() 76 | self.tableWidget.viewport().installEventFilter(self) 77 | 78 | 79 | def download(self, los): 80 | print('Lets download ...') 81 | exactid, _, imname, linked = los 82 | if not (linked==''): 83 | reply = QtWidgets.QMessageBox.question(self, 'Question', 84 | f'This image exists already in the database. Check out new copy?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 85 | 86 | if reply == QtWidgets.QMessageBox.No: 87 | return 88 | 89 | savefolder = QtWidgets.QFileDialog.getExistingDirectory() 90 | if (savefolder==''): 91 | return 92 | 93 | filename = imname 94 | 95 | (image_id, product_id, imageset_id) = [int(x) for x in exactid.split('/')] 96 | 97 | reply = QtWidgets.QMessageBox.question(self, 'Question', 98 | f'Add image and annotations to database?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 99 | 100 | 101 | progress = QtWidgets.QProgressDialog("Downloading image ..", "Cancel", 0, 100, self) 102 | progress.setWindowModality(QtCore.Qt.WindowModal) 103 | progress.show() 104 | 105 | status, outfilename = self.exm.APIs.images_api.download_image(image_id, target_path=savefolder+os.sep+filename,original_image=True) 106 | # image_download_image(image_id=image_id, target_folder=savefolder, callback=progress.setValue) 107 | 108 | if reply == QtWidgets.QMessageBox.Yes: 109 | fpath,fname = os.path.split(outfilename) 110 | slideid = self.DB.insertNewSlide(fname,fpath) 111 | 112 | self.DB.execute(f'UPDATE Slides set exactImageID="{exactid}" where uid=={slideid}') 113 | progress.setWindowTitle('Synchronizing annotations') 114 | 115 | self.exm.sync(dataset_id=image_id, imageset_id=imageset_id, product_id=product_id, slideuid=slideid, filename=imname, database=self.DB, callback=progress.setValue) 116 | 117 | reply = QtWidgets.QMessageBox.information(self, 'Finished', 118 | 'Download finished', QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) 119 | 120 | progress.close() 121 | # self.DB.execute(f'UPDATE Slides set exactImageID={exactid} where uid=={self.DB.annotationsSlide}') 122 | self.updateTable() 123 | self.show() 124 | 125 | def eventFilter(self, source, event): 126 | if(event.type() == QtCore.QEvent.MouseButtonPress and 127 | event.buttons() == QtCore.Qt.RightButton and 128 | source is self.tableWidget.viewport()): 129 | item = self.tableWidget.itemAt(event.pos()) 130 | if item is not None: 131 | menu = QMenu(self) 132 | menu.addAction('Download this image', partial(self.download, self.los[item.row()])) #(QAction('test')) 133 | menu.exec_(event.globalPos()) 134 | return super(ExactDownloadDialog, self).eventFilter(source, event) 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/exactLinkDialog.py: -------------------------------------------------------------------------------- 1 | from SlideRunner.general.dependencies import * 2 | from SlideRunner_dataAccess.database import Database 3 | from functools import partial 4 | from PyQt5 import QtCore, QtGui, QtWidgets 5 | from PyQt5.QtCore import pyqtSlot 6 | import sys 7 | from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QAction, QTableWidget,QTableWidgetItem,QVBoxLayout 8 | from PyQt5.QtGui import QIcon 9 | from SlideRunner.dataAccess.exact import * 10 | 11 | 12 | class ExactLinkDialog(QDialog): 13 | 14 | def __init__(self, DB:Database, settingsObject, imageset): 15 | super().__init__() 16 | self.title = 'EXACT overview (%s)' % settingsObject.value('exactHostname', 'https://exact.cs.fau.de').replace('//','//'+settingsObject.value('exactUsername', 'Demo')+'@') 17 | self.left = 50 18 | self.top = 50 19 | self.width = 600 20 | self.height = 500 21 | self.DB = DB 22 | self.setModal(True) 23 | self.imageset = imageset 24 | hostname = settingsObject.value('exactHostname', 'https://exact.cs.fau.de') 25 | self.exm = ExactManager(settingsObject.value('exactUsername', 'Demo'), 26 | settingsObject.value('exactPassword', 'demodemo'), 27 | hostname) 28 | self.initUI() 29 | 30 | def initUI(self): 31 | self.setWindowTitle(self.title) 32 | self.setGeometry(self.left, self.top, self.width, self.height) 33 | 34 | self.createTable() 35 | 36 | self.layout = QtWidgets.QVBoxLayout() 37 | self.layout.addWidget(self.tableWidget) 38 | self.setLayout(self.layout) 39 | 40 | self.show() 41 | 42 | def updateTable(self): 43 | def image_and_product_to_id(image_id, product_id, imageset_id): 44 | return str(image_id)+'/'+str(product_id)+'/'+str(imageset_id) 45 | DB = self.DB 46 | fileToAnnos = {exactimageid:slide for slide,exactimageid in DB.execute('SELECT filename, exactImageID from Slides').fetchall()} 47 | 48 | print('Retrieving imageset: ',self.imageset) 49 | self.loi = self.exm.APIs.image_sets_api.retrieve_image_set(self.imageset, expand='images,product_set') 50 | rowcount=0 51 | self.los = [] 52 | 53 | for product in self.loi.product_set: 54 | for im in self.loi.images: 55 | self.los.append([image_and_product_to_id(im['id'], product['id'],self.imageset), str(im['name']),fileToAnnos[image_and_product_to_id(im['id'], product['id'],self.imageset)] if image_and_product_to_id(im['id'], product['id'],self.imageset) in fileToAnnos else '' ]) 56 | rowcount+=1 57 | 58 | self.tableWidget.setRowCount(rowcount) 59 | for row,(id,im,linked) in enumerate(self.los): 60 | self.tableWidget.setItem(row,0, QTableWidgetItem(str(id))) 61 | self.tableWidget.setItem(row,1, QTableWidgetItem(str(im))) 62 | self.tableWidget.setItem(row,2, QTableWidgetItem(str(linked))) 63 | 64 | def createTable(self): 65 | # Create table 66 | self.tableWidget = QTableWidget() 67 | self.tableWidget.setColumnCount(3) 68 | self.tableWidget.setHorizontalHeaderLabels(['ID','Image','Assigned to']) 69 | self.updateTable() 70 | self.tableWidget.move(0,0) 71 | self.tableWidget.resizeColumnsToContents() 72 | self.tableWidget.viewport().installEventFilter(self) 73 | 74 | 75 | def link(self, los): 76 | exactid, _, linked = los 77 | if not (linked==''): 78 | reply = QtWidgets.QMessageBox.question(self, 'Question', 79 | 'Do you really want to link this image to a new image?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) 80 | 81 | if reply == QtWidgets.QMessageBox.No: 82 | return 83 | (image_id, product_id, imageset_id) = [int(x) for x in exactid.split('/')] 84 | self.DB.execute(f'UPDATE Slides set exactImageID="{exactid}" where uid=={self.DB.annotationsSlide}') 85 | self.updateTable() 86 | self.show() 87 | 88 | def eventFilter(self, source, event): 89 | if(event.type() == QtCore.QEvent.MouseButtonPress and 90 | event.buttons() == QtCore.Qt.RightButton and 91 | source is self.tableWidget.viewport()): 92 | item = self.tableWidget.itemAt(event.pos()) 93 | if item is not None: 94 | menu = QMenu(self) 95 | menu.addAction('Link to this image '+item.text(), partial(self.link, self.los[item.row()])) #(QAction('test')) 96 | menu.exec_(event.globalPos()) 97 | return super(ExactLinkDialog, self).eventFilter(source, event) 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/getCoordinates.py: -------------------------------------------------------------------------------- 1 | from SlideRunner.general.dependencies import * 2 | from functools import partial 3 | import PyQt5.QtCore as QtCore 4 | 5 | def hitClose(ev, d: QDialog, elem:dict): 6 | if (elem['editx'].text().isdigit()) and (elem['edity'].text().isdigit()): 7 | d.close() 8 | 9 | def getCoordinatesDialog(self): 10 | d = QDialog() 11 | layout = QtWidgets.QGridLayout() 12 | d.setLayout(layout) 13 | elem = dict() 14 | image_dims=self.slide.level_dimensions[0] 15 | cx = int((self.relativeCoords[0]+0.5)*image_dims[0]) 16 | cy = int((self.relativeCoords[1]+0.5)*image_dims[1]) 17 | 18 | l1 = QtWidgets.QLabel("Please enter coordinates") 19 | layout.addWidget(l1, 0,0) 20 | l2 = QtWidgets.QLabel("x") 21 | layout.addWidget(l2, 1,0) 22 | editx = QtWidgets.QLineEdit() 23 | layout.addWidget(editx,1,1) 24 | editx.setText(str(cx)) 25 | 26 | l2 = QtWidgets.QLabel("y") 27 | layout.addWidget(l2, 1, 2) 28 | edity = QtWidgets.QLineEdit() 29 | layout.addWidget(edity,1,3) 30 | edity.setText(str(cy)) 31 | 32 | b1 = QPushButton("ok",d) 33 | layout.addWidget(b1, 1,5) 34 | elem['editx'] = editx 35 | elem['edity'] = edity 36 | 37 | b1.clicked.connect(partial(hitClose, d=d, elem=elem)) 38 | 39 | d.setWindowTitle("Enter Coordinates") 40 | d.setWindowModality(Qt.ApplicationModal) 41 | d.exec_() 42 | 43 | return int(editx.text()), int(edity.text()) 44 | 45 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/question.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QMessageBox, QAbstractButton 2 | 3 | def YesNoAbortDialog(title:str='Question', text:str='Answer?', textYes:str='Yes', textNo:str='No', textAbort:str=None): 4 | 5 | box = QMessageBox() 6 | box.setIcon(QMessageBox.Question) 7 | box.setWindowTitle(title) 8 | box.setText(text) 9 | 10 | box.setStandardButtons(QMessageBox.Yes|QMessageBox.No) 11 | if (textAbort is not None): 12 | box.setStandardButtons(QMessageBox.Yes|QMessageBox.No|QMessageBox.Abort) 13 | abortBtn = box.button(QMessageBox.Abort) 14 | abortBtn.setText(textAbort) 15 | else: 16 | abortBtn=None 17 | 18 | buttonY = box.button(QMessageBox.Yes) 19 | buttonY.setText(textYes) 20 | buttonN = box.button(QMessageBox.No) 21 | buttonN.setText(textNo) 22 | box.exec_() 23 | 24 | if (box.clickedButton()==buttonY): 25 | return QMessageBox.Yes 26 | elif (box.clickedButton()==buttonN): 27 | return QMessageBox.No 28 | elif (box.clickedButton()==abortBtn): 29 | return QMessageBox.Abort 30 | 31 | return None 32 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/settings.py: -------------------------------------------------------------------------------- 1 | from SlideRunner.general.dependencies import * 2 | from functools import partial 3 | import PyQt5.QtCore as QtCore 4 | cmaps = ['viridis', 'plasma', 'inferno', 'magma', 'cividis', 'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', 5 | 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', 6 | 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn', 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', 7 | 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', 8 | 'hot', 'afmhot', 'gist_heat', 'copper', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 9 | 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', 'twilight', 'twilight_shifted', 'hsv', 10 | 'Pastel1', 'Pastel2', 'Paired', 'Accent', 11 | 'Dark2', 'Set1', 'Set2', 'Set3', 12 | 'tab10', 'tab20', 'tab20b', 'tab20c', 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern', 13 | 'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', 14 | 'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'] 15 | 16 | GuidedScreeningThresholdOptions = ['OTSU','high','med','low','off'] 17 | 18 | def saveAndClose(ev, d: QDialog, elem:dict, settingsObject): 19 | cmap = cmaps[elem['combo_colorbar'].currentIndex()] 20 | settingsObject.setValue('OverlayColorMap', cmap) 21 | 22 | thres = GuidedScreeningThresholdOptions[elem['combo_guided'].currentIndex()] 23 | settingsObject.setValue('GuidedScreeningThreshold', thres) 24 | settingsObject.setValue('SpotCircleRadius', elem['radiusSlider'].value()) 25 | exactenabled = elem['exactsupport'].currentIndex() 26 | settingsObject.setValue('exactSupportEnabled', exactenabled) 27 | settingsObject.setValue('exactHostname',elem['exactHostname'].text() ) 28 | settingsObject.setValue('exactUsername',elem['exactUsername'].text() ) 29 | settingsObject.setValue('exactPassword',elem['exactPassword'].text() ) 30 | 31 | d.close() 32 | 33 | 34 | def chooseFile(ev, d: QDialog, elem:dict, settingsObject): 35 | 36 | ret,err = QFileDialog.getOpenFileName(None,"Please choose default database.", "",'*.sqlite') 37 | if (ret is not None): 38 | settingsObject.setValue('DefaultDatabase', ret) 39 | elem['dbfile'].setText(ret) 40 | 41 | 42 | def changePxSlider(ev, sliderObj:QtWidgets.QSlider, labelObj:QtWidgets.QLabel): 43 | labelObj.setText('%d px' % sliderObj.value()) 44 | 45 | def settingsDialog(settingsObject): 46 | d = QDialog() 47 | layout = QtWidgets.QGridLayout() 48 | d.setLayout(layout) 49 | elem = dict() 50 | 51 | l1 = QtWidgets.QLabel("Overlay color scheme") 52 | layout.addWidget(l1, 0,0) 53 | c1 = QtWidgets.QComboBox() 54 | ci = 0 55 | for key,item in enumerate(cmaps): 56 | c1.addItem(item) 57 | if (item == settingsObject.value('OverlayColorMap')): 58 | ci = key 59 | elem['combo_colorbar'] = c1 60 | c1.setCurrentIndex(ci) 61 | layout.addWidget(c1, 0,1) 62 | 63 | l2 = QtWidgets.QLabel("Radius of spot annotations") 64 | l3 = QtWidgets.QLabel("%d px" % int(settingsObject.value('SpotCircleRadius') )) 65 | layout.addWidget(l2, 1,0) 66 | layout.addWidget(l3, 1,2) 67 | newSlider = QtWidgets.QSlider() 68 | newSlider.setMinimum(1) 69 | newSlider.setMaximum(100) 70 | newSlider.setValue(int(settingsObject.value('SpotCircleRadius'))) 71 | newSlider.setOrientation(QtCore.Qt.Horizontal) 72 | layout.addWidget(newSlider, 1,1) 73 | elem['radiusSlider'] = newSlider 74 | 75 | newSlider.valueChanged.connect(partial(changePxSlider, sliderObj=newSlider, labelObj=l3)) 76 | 77 | l4 = QtWidgets.QLabel("Default database") 78 | l5 = QtWidgets.QLabel(settingsObject.value('DefaultDatabase')) 79 | layout.addWidget(l4, 2,0) 80 | layout.addWidget(l5, 2,1) 81 | b2 = QPushButton("choose",d) 82 | layout.addWidget(b2, 2,2) 83 | elem['dbfile'] = l5 84 | b2.clicked.connect(partial(chooseFile, d=d, elem=elem, settingsObject=settingsObject)) 85 | 86 | newSlider.valueChanged.connect(partial(changePxSlider, sliderObj=newSlider, labelObj=l3)) 87 | 88 | 89 | l6 = QtWidgets.QLabel("Guided Screening Threshold") 90 | layout.addWidget(l6, 3,0) 91 | c2 = QtWidgets.QComboBox() 92 | ci = 0 93 | 94 | for key,item in enumerate(GuidedScreeningThresholdOptions): 95 | c2.addItem(item) 96 | if (item == settingsObject.value('GuidedScreeningThreshold')): 97 | ci = key 98 | elem['combo_guided'] = c2 99 | c2.setCurrentIndex(ci) 100 | layout.addWidget(c2, 3,1) 101 | 102 | labelExactSupport = QtWidgets.QLabel('EXACT support:') 103 | c1 = QtWidgets.QComboBox() 104 | ci = 0 105 | for key,item in enumerate(['disabled','enabled']): 106 | c1.addItem(item) 107 | if (key == settingsObject.value('exactSupportEnabled')): 108 | ci = key 109 | elem['exactsupport'] = c1 110 | c1.setCurrentIndex(ci) 111 | 112 | layout.addWidget(labelExactSupport, 4, 0) 113 | layout.addWidget(c1, 4, 1) 114 | 115 | 116 | labelHostname = QtWidgets.QLabel('EXACT server:') 117 | editHostname = QtWidgets.QLineEdit(settingsObject.value('exactHostname', 'https://exact.cs.fau.de')) 118 | elem['exactHostname'] = editHostname 119 | 120 | layout.addWidget(labelHostname, 5, 0) 121 | layout.addWidget(editHostname, 5, 1) 122 | 123 | labelUsername = QtWidgets.QLabel('EXACT username:') 124 | editUsername = QtWidgets.QLineEdit(settingsObject.value('exactUsername', 'Demo')) 125 | layout.addWidget(labelUsername, 6, 0) 126 | layout.addWidget(editUsername, 6, 1) 127 | elem['exactUsername'] = editUsername 128 | 129 | labelPassword = QtWidgets.QLabel('EXACT password:') 130 | editPassword = QtWidgets.QLineEdit(settingsObject.value('exactPassword', 'demodemo')) 131 | editPassword.setEchoMode(QtWidgets.QLineEdit.PasswordEchoOnEdit) 132 | elem['exactPassword'] = editPassword 133 | 134 | layout.addWidget(labelPassword, 7, 0) 135 | layout.addWidget(editPassword, 7, 1) 136 | 137 | b1 = QPushButton("ok",d) 138 | layout.addWidget(b1, 8, 1) 139 | b1.clicked.connect(partial(saveAndClose, d=d, elem=elem, settingsObject=settingsObject)) 140 | 141 | 142 | 143 | # b1.move(50,50) 144 | d.setWindowTitle("Settings") 145 | d.setWindowModality(Qt.ApplicationModal) 146 | d.exec_() 147 | 148 | -------------------------------------------------------------------------------- /SlideRunner/gui/dialogs/welcomeExact.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | from PyQt5 import QtGui, QtWidgets, QtCore 18 | import sys 19 | from time import time,sleep 20 | from PyQt5.QtGui import QPixmap 21 | from PyQt5.QtWidgets import QDialog, QApplication, QMainWindow 22 | from PyQt5.QtWidgets import QSplashScreen 23 | from PyQt5.QtCore import Qt 24 | from functools import partial 25 | 26 | import os 27 | ARTWORK_DIR_NAME = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))+os.sep+'artwork'+os.sep 28 | from PyQt5.QtWidgets import QDialog, QWidget, QFileDialog, QMenu,QInputDialog, QAction, QPushButton, QItemDelegate, QTableWidgetItem, QCheckBox 29 | 30 | 31 | def closeDlg(splash,e): 32 | pass 33 | 34 | def enableAndClose(splash, settingsObject,mainWindow): 35 | settingsObject.setValue('exactSupportEnabled', 1) 36 | mainWindow.refreshMenu() 37 | splash.close() 38 | 39 | def disableAndClose(splash, settingsObject,mainWindow): 40 | settingsObject.setValue('exactSupportEnabled', 0) 41 | mainWindow.refreshMenu() 42 | splash.close() 43 | 44 | def welcomeExactDialog(app, settingsObject, mainwindow): 45 | 46 | # Create and display the about screen 47 | splash_pix = QPixmap(ARTWORK_DIR_NAME+'ExactWelcomeScreen.png') 48 | splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) 49 | 50 | btn = QPushButton('Enable EXACT', splash) 51 | btn.move(140, 320) 52 | btn.clicked.connect(partial(enableAndClose, splash, settingsObject,mainwindow)) 53 | 54 | btn = QPushButton('Disable EXACT',splash) 55 | btn.move(350, 320) 56 | btn.clicked.connect(partial(disableAndClose, splash, settingsObject,mainwindow)) 57 | # layout.addWidget(btn, 1,1) 58 | 59 | # splash.showMessage('Version %s\n'%version, alignment = Qt.AlignHCenter + Qt.AlignBottom, color=Qt.black) 60 | splash.setMask(splash_pix.mask()) 61 | splash.show() 62 | 63 | splash.mousePressEvent = partial(closeDlg, splash) 64 | 65 | start = time() 66 | while splash.isActiveWindow() & (time() - start < 10): 67 | sleep(0.001) 68 | app.processEvents() 69 | 70 | if (splash.isActiveWindow()): 71 | splash.close() 72 | 73 | -------------------------------------------------------------------------------- /SlideRunner/gui/frameSlider.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import numpy as np 3 | from PyQt5.QtCore import Qt 4 | 5 | 6 | class frameSlider(QtWidgets.QWidget): 7 | 8 | sliderPos = 0 9 | fps = 1.0 10 | numberOfFrames = 1 11 | valueChanged = QtCore.pyqtSignal() 12 | text = '1' 13 | 14 | def __init__(self): 15 | super(frameSlider, self).__init__() 16 | 17 | self.initUI() 18 | 19 | def setFPS(self, fps:float): 20 | self.fps = fps 21 | self.setSteps() 22 | self.repaint() 23 | 24 | def setNumberOfFrames(self, numberOfFrames): 25 | self.numberOfFrames = numberOfFrames 26 | self.setSteps() 27 | self.setHidden(numberOfFrames==1) 28 | self.repaint() 29 | 30 | def getMaxZoom(self) -> float: 31 | return self.maxZoom 32 | 33 | def setText(self, text:str): 34 | self.text = text 35 | self.repaint() 36 | 37 | def getValue(self) -> float: 38 | return self.sliderPos 39 | 40 | def setValue(self, value:float): 41 | self.sliderPos = min(max(int(value),0),self.numberOfFrames-1) 42 | if (self.fps != 1.0): 43 | self.text = '%.2f' % (self.sliderPos/self.fps) 44 | else: 45 | self.text = '%d' % (self.sliderPos+1) 46 | self.repaint() 47 | 48 | 49 | def setMinZoom(self, value : float): 50 | self.setSteps() 51 | self.repaint() 52 | 53 | def initUI(self): 54 | 55 | self.setMinimumSize(1, 40) 56 | self.value = 700 57 | self.num = list() 58 | self.setSteps() 59 | 60 | def mouseMoveEvent(self, event): 61 | self.mousePressEvent(event, dragging=True) 62 | 63 | def mouseReleaseEvent(self, event): 64 | self.mousePressEvent(event, dragging=True) 65 | self.valueChanged.emit() 66 | 67 | def mousePressEvent(self, event, dragging=False): 68 | if (self.numberOfFrames==1): 69 | return 70 | if ((event.button() == Qt.LeftButton)) or dragging: 71 | pntr_x = float(event.localPos().x()) 72 | w = self.size().width() - 40 73 | 74 | zoomPos = (pntr_x-20)/w 75 | self.sliderPos = 100*zoomPos 76 | 77 | # quantize according to frames 78 | self.sliderPos = np.round(self.sliderPos/100*(self.numberOfFrames-1)) 79 | 80 | self.sliderPos = np.clip(self.sliderPos,0,self.numberOfFrames-1) 81 | self.setValue(self.sliderPos) 82 | 83 | self.repaint() 84 | 85 | 86 | 87 | def setSteps(self): 88 | zoomList = np.linspace(0,100,10) 89 | self.steps = [] 90 | self.num = [] 91 | if (self.numberOfFrames==1): 92 | return 93 | for step in zoomList: 94 | if (self.fps!= 1.0): 95 | self.steps.append( np.round((self.numberOfFrames) / self.fps * step/100,2)) 96 | self.num.append(step) 97 | else: 98 | self.steps.append( 1+int(np.round(np.round((self.numberOfFrames-1) * step/100)))) 99 | self.num.append(step) 100 | 101 | 102 | 103 | def paintEvent(self, e): 104 | 105 | qp = QtGui.QPainter() 106 | qp.begin(self) 107 | self.drawWidget(qp) 108 | qp.end() 109 | 110 | 111 | 112 | def drawWidget(self, qp): 113 | 114 | if (self.numberOfFrames==1): 115 | return 116 | metrics = qp.fontMetrics() 117 | 118 | size = self.size() 119 | w = size.width() 120 | h = size.height() 121 | 122 | 123 | 124 | step = int(round(w / 10.0)) 125 | 126 | till = int(((w / 750.0) * self.value)) 127 | full = int(((w / 750.0) * 700)) 128 | 129 | w_net = w - 40 130 | 131 | qp.setBrush(QtGui.QColor(93, 93, 93)) 132 | qp.setPen(QtGui.QColor(39, 39, 39)) 133 | qp.drawRect(20-1, 20, w_net+2, 5) 134 | 135 | qp.setBrush(QtGui.QColor(70, 70, 70)) 136 | font = QtGui.QFont('Serif', 7, QtGui.QFont.Light) 137 | qp.setFont(font) 138 | 139 | for j in range(len(self.steps)): 140 | qp.setPen(QtGui.QColor(93, 93, 93)) 141 | qp.drawLine(self.num[j]*w_net/100+20, 25, self.num[j]*w_net/100+20, 32) 142 | labelstr = str(self.steps[j]) 143 | fw = metrics.width(labelstr) 144 | qp.setPen(QtGui.QColor(255, 255, 255)) 145 | qp.drawText(self.num[j]*w_net/100-fw/2+20, h, labelstr) 146 | 147 | 148 | font = QtGui.QFont('Serif', 12, QtGui.QFont.Light) 149 | qp.setFont(font) 150 | 151 | 152 | tw = metrics.width(self.text) 153 | qp.setPen(QtGui.QColor(38, 38, 38)) 154 | qp.setBrush(QtGui.QColor(71, 71, 71)) 155 | 156 | 157 | qp.drawRect(w_net*self.sliderPos/(self.numberOfFrames-1)*100*0.01+20-5, 13, 10, 20) 158 | qp.setPen(QtGui.QColor(99, 99, 99)) 159 | qp.drawRect(w_net*self.sliderPos/(self.numberOfFrames-1)*100*0.01+20-4, 14, 8, 20-2) 160 | 161 | qp.setBrush(QtGui.QColor(70, 70, 70)) 162 | qp.setPen(QtGui.QColor(255, 255, 255)) 163 | qp.drawText(self.sliderPos/(self.numberOfFrames-1)*100*w_net/100-tw/2+20, 10,self.text) 164 | 165 | 166 | -------------------------------------------------------------------------------- /SlideRunner/gui/shortcuts.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | File description: 17 | Definition of UI shortcuts 18 | 19 | """ 20 | 21 | 22 | from PyQt5.QtWidgets import QShortcut 23 | from PyQt5.QtGui import QKeySequence 24 | from functools import partial 25 | from SlideRunner.gui import annotation as GUIannotation 26 | 27 | 28 | def defineShortcuts(self): 29 | 30 | shortcuts=[] 31 | 32 | for k in range(8): 33 | shortcut = QShortcut(QKeySequence("Ctrl+%d" % (k+1)),self) 34 | shortcut.activated.connect(partial(self.toggleOneClass, k+1)) 35 | shortcuts.append(shortcut) 36 | 37 | for k in range(8): 38 | shortcut = QShortcut(QKeySequence("%d" % (k+1)),self) 39 | shortcut.activated.connect(partial(self.clickAnnoclass, k+1)) 40 | shortcuts.append(shortcut) 41 | 42 | for k in range(8): 43 | shortcut = QShortcut(QKeySequence("Shift+%d" % (k+1)),self) 44 | shortcut.activated.connect(partial(GUIannotation.addImageLabel, self, k+1)) 45 | shortcuts.append(shortcut) 46 | 47 | shortcut = QShortcut(QKeySequence('Shift+0'), self) 48 | shortcut.activated.connect(partial(GUIannotation.deleteImageLabel, self)) 49 | 50 | shortcut = QShortcut(QKeySequence('Ctrl+R'), self) 51 | shortcut.activated.connect(partial(self.removeLastPolygonPoint, self)) 52 | 53 | # Set keyboard shortcuts 54 | shortcutEsc = QShortcut(QKeySequence("Esc"), self) 55 | shortcutEsc.activated.connect(self.hitEscape) 56 | 57 | shortcutnextframe = QShortcut(QKeySequence("Shift++"), self) 58 | shortcutnextframe.activated.connect(self.nextFrame) 59 | 60 | shortcutprevframe = QShortcut(QKeySequence("Shift+-"), self) 61 | shortcutprevframe.activated.connect(self.previousFrame) 62 | 63 | self.ui.pluginNoOpacity.setShortcut('Y') 64 | self.ui.pluginHalfOpacity.setShortcut('V') 65 | self.ui.pluginFullOpacity.setShortcut('X') 66 | 67 | 68 | shortcuts.append(shortcutEsc) 69 | shortcutN = QShortcut(QKeySequence("N"), self) 70 | shortcutN.activated.connect(self.nextScreeningStep) 71 | shortcuts.append(shortcutN) 72 | 73 | shortcut = QShortcut(QKeySequence.Delete, self) 74 | shortcut.activated.connect(self.deleteCurrentSelection) 75 | shortcuts.append(shortcut) 76 | shortcut = QShortcut(QKeySequence.Backspace, self) 77 | shortcut.activated.connect(self.deleteCurrentSelection) 78 | shortcuts.append(shortcut) 79 | 80 | 81 | return shortcuts 82 | 83 | def defineMenuShortcuts(self): 84 | self.ui.action_Open.setShortcut("Ctrl+D") 85 | self.ui.action_Open.triggered.connect(self.openDatabase) 86 | self.ui.actionOpen.setShortcut("Ctrl+O") 87 | self.ui.actionOpen.triggered.connect(self.openSlideDialog) 88 | self.ui.actionOpen_custom.setShortcut("Ctrl+C") 89 | self.ui.zoomInAction.setShortcut("+") 90 | self.ui.zoomOutAction.setShortcut("-") 91 | self.ui.actionCreate_new.triggered.connect(self.createNewDatabase) 92 | self.ui.actionOpen_custom.triggered.connect(self.openCustomDB) 93 | self.ui.actionAdd_annotator.triggered.connect(self.addAnnotator) 94 | self.ui.actionAdd_cell_class.triggered.connect(self.addCellClass) 95 | self.ui.actionManageDatabase.setShortcut("Ctrl+V") 96 | if (self.settings.value('exactSupportEnabled', 0)): 97 | self.ui.databaseExactMenuSync.setShortcut("Ctrl+Shift+Y") 98 | self.ui.exactMenuSyncImage.setShortcut("Ctrl+Y") 99 | self.ui.action_Quit.setShortcut('Ctrl+Q') 100 | self.ui.action_Quit.triggered.connect(self.close) 101 | self.menuItemAnnotateOutline.setShortcut('P') 102 | self.ui.copySnapshot.setShortcut('Ctrl+Alt+C') 103 | self.menuItemAnnotateCenter.setShortcut('S') 104 | self.menuItemAnnotateCircle.setShortcut('C') 105 | self.menuItemAnnotateArea.setShortcut('R') 106 | self.menuItemAnnotateFlag.setShortcut('F') 107 | self.menuItemAnnotateWand.setShortcut('W') 108 | -------------------------------------------------------------------------------- /SlideRunner/gui/sidebar.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | from PyQt5.QtWidgets import QMenu 3 | from PyQt5.QtWidgets import QTableWidget 4 | from SlideRunner.gui.types import * 5 | from SlideRunner.gui import annotation as GUIannotation 6 | from functools import partial 7 | from PyQt5.QtCore import Qt 8 | 9 | class ClassSelectTableWidget(QTableWidget): 10 | def __init__(self, parent = None, parentObject = None): 11 | QTableWidget.__init__(self, parent) 12 | self.parentObject = parentObject 13 | 14 | def contextMenuEvent(self, event): 15 | modifiers = QtWidgets.QApplication.keyboardModifiers() 16 | columnId = self.columnAt(event.pos().x()) 17 | rowId = self.rowAt(event.pos().y()) 18 | if (modifiers == Qt.ShiftModifier): 19 | partial(GUIannotation.addImageLabel, self.parentObject, self.parentObject.classList[rowId].uid)() 20 | return 21 | 22 | menu = QMenu(self) 23 | if (columnId == 0): 24 | addAction = menu.addAction('Toggle all', self.parentObject.toggleAllClasses) 25 | 26 | elif (self.parentObject.db.isOpen()): 27 | addAction = menu.addAction('Add new class') 28 | addAction.triggered.connect(self.parentObject.addCellClass) 29 | if (rowId == 0): 30 | setSlideLabel = menu.addAction('Delete image label', partial(GUIannotation.deleteImageLabel, self.parentObject)) 31 | elif (rowId>0): 32 | if (self.parentObject.classList[rowId].itemID == ClassRowItemId.ITEM_DATABASE): 33 | changeColorAction = menu.addAction('Change color', partial(GUIannotation.changeClassColor, self.parentObject, self.parentObject.classList[rowId].color, self.parentObject.classList[rowId].uid)) 34 | renameAction = menu.addAction('Rename class',partial(GUIannotation.renameClass,self.parentObject, self.parentObject.classList[rowId].uid)) 35 | removeAction = menu.addAction('Remove class', partial(GUIannotation.deleteClass, self.parentObject, self.parentObject.classList[rowId].uid)) 36 | 37 | if (self.parentObject.classList[rowId].clickable == True): 38 | clickableAction = menu.addAction('Make class non-clickable', partial(GUIannotation.clickableClass, self.parentObject, self.parentObject.classList[rowId].uid, False)) 39 | else: 40 | clickableAction = menu.addAction('Make class clickable', partial(GUIannotation.clickableClass, self.parentObject, self.parentObject.classList[rowId].uid, True)) 41 | 42 | removeAllOfClassAction = menu.addAction('Remove all of this class from slide', partial(GUIannotation.deleteAllFromClassOnSlide,self.parentObject, self.parentObject.classList[rowId].uid)) 43 | setSlideLabel = menu.addAction('Assign to complete image', partial(GUIannotation.addImageLabel, self.parentObject, self.parentObject.classList[rowId].uid)) 44 | print('Self: ',self, self.parentObject) 45 | 46 | 47 | if (self.parentObject.activePlugin is not None) and (rowId > 0): 48 | if (self.parentObject.classList[rowId].itemID == ClassRowItemId.ITEM_PLUGIN): 49 | addmenu = menu.addMenu('Copy all items to database as:') 50 | menuitems = list() 51 | for clsname in self.parentObject.db.getAllClasses(): 52 | act=addmenu.addAction(clsname[0],partial(GUIannotation.copyAllAnnotations,self.parentObject,self.parentObject.classList[rowId].classID, clsname[1], event)) 53 | menuitems.append(act) 54 | 55 | action = menu.exec_(self.mapToGlobal(event.pos())) 56 | 57 | 58 | def addSidebar(self, parentObject): 59 | 60 | self.tabView = QtWidgets.QTabWidget() 61 | 62 | 63 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) 64 | sizePolicy.setHorizontalStretch(0) 65 | sizePolicy.setVerticalStretch(0) 66 | # sizePolicy.setHeightForWidth(self.tabView.sizePolicy().hasHeightForWidth()) 67 | self.tabView.setSizePolicy(sizePolicy) 68 | self.tabView.setMaximumSize(QtCore.QSize(16777215, 300)) 69 | 70 | self.tab1widget = QtWidgets.QWidget() 71 | self.tab1Layout = QtWidgets.QVBoxLayout() 72 | self.tab1widget.setLayout(self.tab1Layout) 73 | self.tab1Layout.setObjectName("tab1Layout") 74 | 75 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) 76 | sizePolicy.setHorizontalStretch(0) 77 | sizePolicy.setVerticalStretch(10) 78 | sizePolicy.setHeightForWidth(self.tab1widget.sizePolicy().hasHeightForWidth()) 79 | self.tab1widget.setSizePolicy(sizePolicy) 80 | self.tab1widget.setMinimumSize(QtCore.QSize(200, 100)) 81 | 82 | self.tab2widget = QtWidgets.QWidget() 83 | self.tab2Layout = QtWidgets.QVBoxLayout() 84 | self.tab2widget.setLayout(self.tab2Layout) 85 | self.tab2Layout.setObjectName("tab2Layout") 86 | 87 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) 88 | sizePolicy.setHorizontalStretch(0) 89 | sizePolicy.setVerticalStretch(1) 90 | sizePolicy.setHeightForWidth(self.tab2widget.sizePolicy().hasHeightForWidth()) 91 | self.tab2widget.setSizePolicy(sizePolicy) 92 | self.tab2widget.setMinimumSize(QtCore.QSize(200, 100)) 93 | 94 | 95 | 96 | self.annotationTypeTableView = ClassSelectTableWidget(self.centralwidget, parentObject) 97 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.MinimumExpanding) 98 | sizePolicy.setHorizontalStretch(0) 99 | sizePolicy.setVerticalStretch(1) 100 | sizePolicy.setHeightForWidth(self.annotationTypeTableView.sizePolicy().hasHeightForWidth()) 101 | self.annotationTypeTableView.setSizePolicy(sizePolicy) 102 | self.annotationTypeTableView.setMinimumSize(QtCore.QSize(200, 100)) 103 | # self.annotationTypeTableView.setMaximumSize(QtCore.QSize(16777215, 120)) 104 | self.annotationTypeTableView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) 105 | self.annotationTypeTableView.setObjectName("annotationTypeTableView") 106 | self.annotationTypeTableView.setColumnCount(0) 107 | self.annotationTypeTableView.setRowCount(0) 108 | self.annotationTypeTableView.setVisible(False) 109 | self.tab1Layout.addWidget(self.annotationTypeTableView) 110 | self.annotatorComboBox = QtWidgets.QComboBox(self.tab1widget) 111 | # self.annotatorComboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) 112 | self.annotatorComboBox.setObjectName("comboBox") 113 | self.annotatorComboBox.setVisible(False) 114 | self.tab1Layout.addWidget(self.annotatorComboBox) 115 | self.statisticView = QtWidgets.QTableView(self.tab2widget) 116 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) 117 | sizePolicy.setHorizontalStretch(0) 118 | sizePolicy.setVerticalStretch(1) 119 | sizePolicy.setHeightForWidth(self.statisticView.sizePolicy().hasHeightForWidth()) 120 | self.statisticView.setMinimumSize(QtCore.QSize(200, 10)) 121 | self.statisticView.setSizePolicy(sizePolicy) 122 | # self.statisticView.setMaximumSize(QtCore.QSize(16777215, 120)) 123 | self.statisticView.setObjectName("statisticView") 124 | self.statisticView.setVisible(True) 125 | self.tab2Layout.addWidget(self.statisticView) 126 | self.inspectorTableView = QtWidgets.QTableView(self.centralwidget) 127 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) 128 | sizePolicy.setHorizontalStretch(0) 129 | sizePolicy.setVerticalStretch(1) 130 | sizePolicy.setHeightForWidth(self.inspectorTableView.sizePolicy().hasHeightForWidth()) 131 | self.inspectorTableView.setSizePolicy(sizePolicy) 132 | self.inspectorTableView.setMinimumSize(QtCore.QSize(200, 10)) 133 | self.inspectorTableView.setObjectName("tableView") 134 | self.inspectorTableView.setVisible(False) 135 | self.tab1Layout.addWidget(self.inspectorTableView) 136 | self.statusLabel = QtWidgets.QLabel(self.centralwidget) 137 | self.statusLabel.setObjectName("statusLabel") 138 | self.progressBar = QtWidgets.QProgressBar(self.centralwidget) 139 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 140 | sizePolicy.setHorizontalStretch(0) 141 | sizePolicy.setVerticalStretch(0) 142 | sizePolicy.setHeightForWidth(self.progressBar.sizePolicy().hasHeightForWidth()) 143 | self.progressBar.setSizePolicy(sizePolicy) 144 | self.progressBar.setProperty("value", 24) 145 | self.progressBar.setObjectName("progressBar") 146 | 147 | self.opacitySlider = QtWidgets.QSlider(self.centralwidget) 148 | self.opacitySlider.setEnabled(False) 149 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 150 | sizePolicy.setHorizontalStretch(0) 151 | sizePolicy.setVerticalStretch(0) 152 | sizePolicy.setHeightForWidth(self.opacitySlider.sizePolicy().hasHeightForWidth()) 153 | self.opacitySlider.setSizePolicy(sizePolicy) 154 | self.opacitySlider.setProperty("value", 50) 155 | self.opacitySlider.setOrientation(QtCore.Qt.Horizontal) 156 | self.opacitySlider.setObjectName("opacitySlider") 157 | 158 | 159 | self.stretchlabel = QtWidgets.QLabel(self.centralwidget) 160 | self.stretchlabel.setObjectName("stretchlabel") 161 | self.stretchlabel.setText("") 162 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) 163 | sizePolicy.setHorizontalStretch(0) 164 | sizePolicy.setVerticalStretch(1) 165 | self.stretchlabel.setSizePolicy(sizePolicy) 166 | self.stretchlabel.setMinimumSize(QtCore.QSize(200, 0)) 167 | 168 | self.tab1Layout.addWidget(self.statusLabel) 169 | 170 | 171 | # self.tab3scrollarea.setWidget(self.tab3scrolllayout.widget()) 172 | self.tab3widget = QtWidgets.QWidget() 173 | self.tab3Layout = QtWidgets.QVBoxLayout() 174 | self.tab3widget.setLayout(self.tab3Layout) 175 | self.tab3Layout.setObjectName("tab3Layout") 176 | 177 | cb = QtWidgets.QComboBox() 178 | cb.setToolTip('Overlay selection') 179 | cb.addItems([]) 180 | cb.setHidden(True) 181 | self.overlaySelect = cb 182 | 183 | 184 | self.tab3Layout.addWidget(cb) 185 | 186 | self.opacityLabel = QtWidgets.QLabel(self.centralwidget) 187 | self.opacityLabel.setObjectName("opacityLabel") 188 | self.tab3Layout.addWidget(self.opacityLabel) 189 | self.tab3Layout.addWidget(self.opacitySlider) 190 | 191 | 192 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) 193 | sizePolicy.setHorizontalStretch(0) 194 | sizePolicy.setVerticalStretch(0) 195 | self.tab3widget.setSizePolicy(sizePolicy) 196 | 197 | 198 | self.tabView.addTab(self.tab1widget, "Annotation") 199 | self.tabView.addTab(self.tab2widget, "Statistics") 200 | self.tabView.addTab(self.tab3widget, "Plugin") 201 | self.sidebarLayout.addWidget(self.tabView) 202 | self.sidebarLayout.addWidget(self.stretchlabel) 203 | self.sidebarLayout.addWidget(self.progressBar) 204 | 205 | self.opacityLabel.setText( "Overlay opacity") 206 | self.opacityLabel.setStyleSheet("font-size:8px") 207 | self.opacitySlider.setToolTip( "Opacity") 208 | 209 | self.statusLabel.setText( "TextLabel") 210 | 211 | 212 | return self -------------------------------------------------------------------------------- /SlideRunner/gui/splashScreen.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | from PyQt5.QtGui import QPixmap 18 | from PyQt5.QtWidgets import QSplashScreen 19 | from PyQt5.QtCore import Qt 20 | 21 | import os 22 | 23 | ARTWORK_DIR_NAME = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))+os.sep+'artwork'+os.sep 24 | 25 | def splashScreen(app, version) -> QSplashScreen: 26 | 27 | 28 | # This first part is for loading the splash screen before anything else 29 | 30 | # Create and display the splash screen 31 | splash_pix = QPixmap(ARTWORK_DIR_NAME+'SplashScreen.png') 32 | splash = QSplashScreen(splash_pix, Qt.WindowStaysOnTopHint) 33 | splash.showMessage('Version %s\n'%version, alignment = Qt.AlignHCenter + Qt.AlignBottom, color=Qt.black) 34 | splash.setMask(splash_pix.mask()) 35 | splash.show() 36 | app.processEvents() 37 | 38 | return splash -------------------------------------------------------------------------------- /SlideRunner/gui/style.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | 17 | Style definitions 18 | """ 19 | 20 | from PyQt5.QtWidgets import QStyleFactory 21 | from PyQt5.QtGui import QPalette, QColor 22 | from PyQt5.QtCore import Qt 23 | 24 | from PyQt5 import QtGui, QtCore 25 | 26 | #from PyQt5.QtWidgets import QDialog, QStyleFactory, QWidget, QFileDialog, QMenu,QInputDialog, QAction, QPushButton, QItemDelegate, QTableWidgetItem, QCheckBox 27 | 28 | def setStyle(app): 29 | 30 | app.setStyle(QStyleFactory.create("Fusion")) 31 | 32 | darkPalette = QPalette() 33 | 34 | darkPalette.setColor(QPalette.Window, QColor.fromRgb(53,53,53)) 35 | darkPalette.setColor(QPalette.WindowText, Qt.white) 36 | darkPalette.setColor(QPalette.Base, QColor.fromRgb(25,25,25)) 37 | darkPalette.setColor(QPalette.AlternateBase, QColor.fromRgb(53,53,53)) 38 | darkPalette.setColor(QPalette.ToolTipBase, Qt.white) 39 | darkPalette.setColor(QPalette.ToolTipText, Qt.white) 40 | darkPalette.setColor(QPalette.Text, Qt.white) 41 | darkPalette.setColor(QPalette.Button, QColor.fromRgb(53,53,53)) 42 | darkPalette.setColor(QPalette.ButtonText, Qt.white) 43 | darkPalette.setColor(QPalette.BrightText, Qt.red) 44 | darkPalette.setColor(QPalette.Link, QColor.fromRgb(42, 130, 218)) 45 | 46 | darkPalette.setColor(QPalette.Highlight, QColor.fromRgb(42, 130, 218)) 47 | darkPalette.setColor(QPalette.HighlightedText, Qt.black) 48 | 49 | import os 50 | 51 | ARTWORK_DIR_NAME = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))+os.sep+'artwork'+os.sep 52 | 53 | import platform 54 | print('<%s>' % platform.system()) 55 | if (platform.system() == 'Darwin'): 56 | app_icon = QtGui.QIcon(ARTWORK_DIR_NAME+'SlideRunner.icns') 57 | else: 58 | app_icon = QtGui.QIcon(ARTWORK_DIR_NAME+'icon.png') 59 | 60 | app.setWindowIcon(app_icon) 61 | app.setApplicationName('SlideRunner') 62 | 63 | app.setPalette(darkPalette) 64 | 65 | app.setStyleSheet("""QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; } QTabWidget::pane { /* The tab widget frame */ 66 | border-top: 2px solid #C2C7CB; 67 | } 68 | QTabWidget::tab-bar { 69 | left: 5px; /* move to the right by 5px */ 70 | } 71 | 72 | QTabWidget { 73 | font-size:8px; 74 | } 75 | QTabBar::tab:selected, QTabBar::tab:hover { 76 | background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #ffa02f, stop: 1 #ff862f); 77 | } 78 | /* Style the tab using the tab sub-control. Note that it reads QTabBar _not_ QTabWidget */ 79 | QTabBar::tab { 80 | border: 2px solid #C4C4C3; 81 | border-bottom-color: #C2C7CB; /* same as the pane color */ 82 | border-top-left-radius: 4px; 83 | border-top-right-radius: 4px; 84 | font-size:8px; 85 | min-width: 8ex; 86 | padding: 2px; 87 | } 88 | QMenu::item:selected 89 | { 90 | background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #ffa02f, stop: 1 #ff862f); 91 | } 92 | QMenu::item:!selected 93 | { 94 | background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #343434, stop: 1 #343434); 95 | } 96 | QMenuBar::item:selected 97 | { 98 | background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #ffa02f, stop: 1 #ff862f); 99 | } 100 | QProgressBar::chunk 101 | { 102 | background-color: #d7801a; 103 | width: 2.15px; 104 | margin: 0.5px; 105 | } 106 | 107 | QTabBar::tab:!selected { 108 | margin-top: 2px; /* make non-selected tabs look smaller */ 109 | } 110 | """) 111 | -------------------------------------------------------------------------------- /SlideRunner/gui/toolbar.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | from functools import partial 18 | from PyQt5.QtWidgets import QAction 19 | from PyQt5.QtGui import QIcon 20 | from SlideRunner.gui.types import * 21 | """ 22 | Construct toolbar 23 | """ 24 | import os 25 | 26 | ARTWORK_DIR_NAME = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))+os.sep+'artwork'+os.sep 27 | 28 | 29 | def defineToolbar(self): 30 | self.ui.tb = self.addToolBar("Annotation") 31 | 32 | self.ui.iconView = QAction(QIcon(ARTWORK_DIR_NAME+"iconArrow.png"),"View",self) 33 | self.ui.iconCircle = QAction(QIcon(ARTWORK_DIR_NAME+"iconCircle.png"),"Annotate spots",self) 34 | self.ui.iconRect = QAction(QIcon(ARTWORK_DIR_NAME+"iconRect.png"),"Annotate areas",self) 35 | self.ui.iconDrawCircle = QAction(QIcon(ARTWORK_DIR_NAME+"drawCircle.png"),"Annotate circular area",self) 36 | self.ui.iconPolygon = QAction(QIcon(ARTWORK_DIR_NAME+"iconPolygon.png"),"Annotate polygon",self) 37 | self.ui.iconWand = QAction(QIcon(ARTWORK_DIR_NAME+"iconWand.png"),"Annotate with magic wand",self) 38 | self.ui.iconFlag = QAction(QIcon(ARTWORK_DIR_NAME+"icon_flag.png"),"Mark an important position",self) 39 | self.ui.iconBlinded = QAction(QIcon(ARTWORK_DIR_NAME+"iconBlinded.png"),"Blinded mode",self) 40 | self.ui.iconQuestion = QAction(QIcon(ARTWORK_DIR_NAME+"iconQuestion.png"),"Discovery mode",self) 41 | self.ui.iconScreening = QAction(QIcon(ARTWORK_DIR_NAME+"icon_screeningMode.png"),"Screening mode",self) 42 | self.ui.iconOverlay = QAction(QIcon(ARTWORK_DIR_NAME+"iconOverlay.png"),"Overlay screening map in overview",self) 43 | self.ui.iconAnnoTN = QAction(QIcon(ARTWORK_DIR_NAME+"annoInOverview.png"),"Overlay annotations in overview",self) 44 | self.ui.iconPreviousScreen = QAction(QIcon(ARTWORK_DIR_NAME+"icon_previousView.png"),"Previous view (screening)",self) 45 | self.ui.iconNextScreen = QAction(QIcon(ARTWORK_DIR_NAME+"icon_nextView.png"),"Next view (screening)",self) 46 | self.ui.iconBack = QAction(QIcon(ARTWORK_DIR_NAME+"backArrow.png"),"Back to last annotation",self) 47 | self.ui.iconRect.setCheckable(True) 48 | self.ui.iconView.setCheckable(True) 49 | self.ui.iconView.setChecked(True) 50 | self.ui.iconCircle.setCheckable(True) 51 | self.ui.iconDrawCircle.setCheckable(True) 52 | self.ui.iconPolygon.setCheckable(True) 53 | self.ui.iconBlinded.setCheckable(True) 54 | self.ui.iconWand.setCheckable(True) 55 | self.ui.iconFlag.setCheckable(True) 56 | self.ui.iconQuestion.setCheckable(True) 57 | self.ui.iconOverlay.setCheckable(True) 58 | self.ui.iconAnnoTN.setCheckable(True) 59 | self.ui.iconBack.setEnabled(False) 60 | self.ui.iconScreening.setCheckable(True) 61 | self.ui.iconBlinded.setEnabled(False) 62 | self.ui.iconNextScreen.setEnabled(False) 63 | self.ui.iconPreviousScreen.setEnabled(False) 64 | self.ui.iconQuestion.setEnabled(False) 65 | 66 | 67 | self.ui.tb.addAction(self.ui.iconView) 68 | self.ui.tb.addAction(self.ui.iconCircle) 69 | self.ui.tb.addAction(self.ui.iconRect) 70 | self.ui.tb.addAction(self.ui.iconDrawCircle) 71 | self.ui.tb.addAction(self.ui.iconPolygon) 72 | self.ui.tb.addAction(self.ui.iconWand) 73 | self.ui.tb.addAction(self.ui.iconFlag) 74 | self.ui.tb.addSeparator() 75 | self.ui.tb.addAction(self.ui.iconBlinded) 76 | self.ui.tb.addAction(self.ui.iconQuestion) 77 | self.ui.tb.addAction(self.ui.iconScreening) 78 | self.ui.tb.addSeparator() 79 | self.ui.tb.addAction(self.ui.iconOverlay) 80 | self.ui.tb.addAction(self.ui.iconAnnoTN) 81 | self.ui.tb.addSeparator() 82 | self.ui.tb.addAction(self.ui.iconPreviousScreen) 83 | self.ui.tb.addAction(self.ui.iconNextScreen) 84 | self.ui.tb.addAction(self.ui.iconBack) 85 | 86 | # Connect triggers for toolbar 87 | self.ui.iconView.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_VIEW)) 88 | self.ui.iconCircle.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_SPOT)) 89 | self.ui.iconRect.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_AREA)) 90 | self.ui.iconDrawCircle.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_CIRCLE)) 91 | self.ui.iconPolygon.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_POLYGON)) 92 | self.ui.iconWand.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_WAND)) 93 | self.ui.iconFlag.triggered.connect(partial(self.setUIMode, UIMainMode.MODE_ANNOTATE_FLAG)) 94 | self.ui.iconBlinded.triggered.connect(self.setBlindedMode) 95 | self.ui.iconQuestion.triggered.connect(self.setDiscoveryMode) 96 | self.ui.iconOverlay.triggered.connect(self.setOverlayHeatmap) 97 | self.ui.iconBack.triggered.connect(self.backToLastAnnotation) 98 | self.ui.iconAnnoTN.triggered.connect(self.setOverlayHeatmap) 99 | self.ui.iconScreening.triggered.connect(self.startStopScreening) 100 | self.ui.iconNextScreen.triggered.connect(self.nextScreeningStep) 101 | self.ui.iconPreviousScreen.triggered.connect(self.previousScreeningStep) 102 | -------------------------------------------------------------------------------- /SlideRunner/gui/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | 18 | class UIMainMode (enumerate): 19 | """ 20 | GUI main modes 21 | """ 22 | MODE_VIEW = 1 23 | MODE_ANNOTATE_SPOT = 2 24 | MODE_ANNOTATE_AREA = 3 25 | MODE_ANNOTATE_POLYGON = 4 26 | MODE_ANNOTATE_FLAG = 5 27 | MODE_ANNOTATE_CIRCLE = 6 28 | MODE_ANNOTATE_WAND = 7 29 | 30 | class ClassRowItemId (enumerate): 31 | ITEM_DATABASE = 0 32 | ITEM_PLUGIN = 1 33 | 34 | class ClassRowItem(object): 35 | def __init__(self, itemID: ClassRowItemId, classID, uid:int = None, color:str='#000000', clickable:bool=True): 36 | self.itemID = itemID 37 | self.uid = uid 38 | self.color = color 39 | self.classID = classID 40 | self.clickable = clickable 41 | 42 | 43 | class WandAnnotation(object): 44 | def __init__(self, xy:tuple=(None,None)): 45 | self.x, self.y = xy 46 | self.tolerance=2 47 | self.mask=None 48 | self.polygon=None 49 | 50 | def seed_point(self): 51 | return (self.x, self.y) -------------------------------------------------------------------------------- /SlideRunner/gui/zoomSlider.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | import numpy as np 3 | from PyQt5.QtCore import Qt 4 | 5 | 6 | class zoomSlider(QtWidgets.QWidget): 7 | 8 | maxZoom = 0.3 9 | minZoom = 80 10 | sliderPos = 0 11 | zoomLevel = 0 12 | valueChanged = QtCore.pyqtSignal() 13 | text = '8.0 x' 14 | 15 | def __init__(self): 16 | super(zoomSlider, self).__init__() 17 | 18 | self.initUI() 19 | 20 | def sliderToZoomValue(self, value): 21 | return np.power(2,value/100*(np.log2(0.5/ self.getMaxZoom())))*self.getMaxZoom() 22 | 23 | def zoomValueToSlider(self, value : float) -> float: 24 | maxZoom = self.getMaxZoom() 25 | retval = 100*np.log2(value/(maxZoom))/(np.log2(0.25/maxZoom)) 26 | return 100*np.log2(value/(maxZoom))/(np.log2(0.25/maxZoom)) 27 | 28 | def setMaxZoom(self, maxZoom : float): 29 | self.maxZoom = maxZoom 30 | self.setSteps() 31 | self.repaint() 32 | 33 | def getMaxZoom(self) -> float: 34 | return self.maxZoom 35 | 36 | def setText(self, text:str): 37 | self.text = text 38 | self.repaint() 39 | 40 | def getValue(self) -> float: 41 | return self.sliderPos 42 | 43 | def setValue(self, value:float): 44 | self.sliderPos = value 45 | self.zoomLevel = self.sliderToZoomValue(self.sliderPos) 46 | self.repaint() 47 | 48 | 49 | def setMinZoom(self, value : float): 50 | self.minZoom = value 51 | self.setSteps() 52 | self.repaint() 53 | 54 | def initUI(self): 55 | 56 | self.setMinimumSize(1, 40) 57 | self.value = 700 58 | self.num = list() 59 | self.setSteps() 60 | 61 | def mouseMoveEvent(self, event): 62 | self.mousePressEvent(event, dragging=True) 63 | 64 | def mouseReleaseEvent(self, event): 65 | self.mousePressEvent(event, dragging=True) 66 | self.valueChanged.emit() 67 | 68 | def mousePressEvent(self, event, dragging=False): 69 | if ((event.button() == Qt.LeftButton)) or dragging: 70 | pntr_x = float(event.localPos().x()) 71 | w = self.size().width() - 40 72 | 73 | zoomPos = (pntr_x-20)/w 74 | self.sliderPos = 100*zoomPos 75 | 76 | self.sliderPos = np.clip(self.sliderPos,0,100) 77 | self.zoomLevel = self.sliderToZoomValue(self.sliderPos) 78 | 79 | self.repaint() 80 | 81 | 82 | 83 | def setSteps(self): 84 | zoomList = 1 / np.float_power(2,np.asarray([-7,-6,-5,-4,-3,-2,-1,0])) 85 | self.steps = [] 86 | self.num = [] 87 | for step in zoomList: 88 | self.steps.append(self.minZoom / step / 2) 89 | self.num.append(self.zoomValueToSlider(step)) 90 | self.steps.append(self.minZoom) 91 | self.num.append(self.zoomValueToSlider(0.5)) 92 | 93 | 94 | 95 | def paintEvent(self, e): 96 | 97 | qp = QtGui.QPainter() 98 | qp.begin(self) 99 | self.drawWidget(qp) 100 | qp.end() 101 | 102 | 103 | 104 | def drawWidget(self, qp): 105 | 106 | 107 | metrics = qp.fontMetrics() 108 | 109 | size = self.size() 110 | w = size.width() 111 | h = size.height() 112 | 113 | step = int(round(w / 10.0)) 114 | 115 | till = int(((w / 750.0) * self.value)) 116 | full = int(((w / 750.0) * 700)) 117 | 118 | w_net = w - 40 119 | 120 | qp.setBrush(QtGui.QColor(93, 93, 93)) 121 | qp.setPen(QtGui.QColor(39, 39, 39)) 122 | qp.drawRect(20-1, 20, w_net+2, 5) 123 | 124 | qp.setBrush(QtGui.QColor(70, 70, 70)) 125 | font = QtGui.QFont('Serif', 7, QtGui.QFont.Light) 126 | qp.setFont(font) 127 | 128 | for j in range(len(self.steps)): 129 | qp.setPen(QtGui.QColor(93, 93, 93)) 130 | qp.drawLine(self.num[j]*w_net/100+20, 25, self.num[j]*w_net/100+20, 32) 131 | labelstr = str(self.steps[j])+' x' 132 | fw = metrics.width(labelstr) 133 | qp.setPen(QtGui.QColor(255, 255, 255)) 134 | qp.drawText(self.num[j]*w_net/100-fw/2+20, h, labelstr) 135 | 136 | 137 | font = QtGui.QFont('Serif', 12, QtGui.QFont.Light) 138 | qp.setFont(font) 139 | 140 | 141 | tw = metrics.width(self.text) 142 | qp.setPen(QtGui.QColor(38, 38, 38)) 143 | qp.setBrush(QtGui.QColor(71, 71, 71)) 144 | 145 | 146 | qp.drawRect(w_net*self.sliderPos*0.01+20-5, 13, 10, 20) 147 | qp.setPen(QtGui.QColor(99, 99, 99)) 148 | qp.drawRect(w_net*self.sliderPos*0.01+20-4, 14, 8, 20-2) 149 | 150 | qp.setBrush(QtGui.QColor(70, 70, 70)) 151 | qp.setPen(QtGui.QColor(255, 255, 255)) 152 | qp.drawText(self.sliderPos*w_net/100-tw/2+20, 10,self.text) 153 | 154 | 155 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_HPF.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | This plugin will annotate the size of a single high-power-field (0.237mm2) [1] 17 | in the digital slide. 18 | 19 | 1: Meuten et al., 2016: Mitotic Count and the Field of View Area, Vet. Path. 53(1):7-9 20 | 21 | 22 | """ 23 | 24 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 25 | from queue import Queue 26 | import threading 27 | import openslide 28 | import numpy as np 29 | from threading import Thread 30 | import SlideRunner_dataAccess.annotations as annotations 31 | 32 | 33 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 34 | version = 0.0 35 | shortName = 'High Power Field Visualization' 36 | inQueue = Queue() 37 | outQueue = Queue() 38 | description = 'Display size of 1 HPF' 39 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 40 | outputType = SlideRunnerPlugin.PluginOutputType.NO_OVERLAY 41 | modelInitialized=False 42 | updateTimer=0.1 43 | slideFilename = None 44 | annos = list() 45 | annotationLabels = {'HPF' : SlideRunnerPlugin.PluginAnnotationLabel(0,'HPF', [0,0,0,255]),} 46 | 47 | configurationList = list((SlideRunnerPlugin.PluginConfigurationEntry(uid=0, name='Re-center HPF', ctype=SlideRunnerPlugin.PluginConfigurationType.PUSHBUTTON), 48 | SlideRunnerPlugin.PluginConfigurationEntry(uid=1, name='Number of HPFs', initValue=0.5, minValue=0.5, maxValue=10.0), 49 | SlideRunnerPlugin.PluginConfigurationEntry(uid=2, name='Size of HPF (mm2)', initValue=0.237, minValue=0.20, maxValue=0.3),)) #0.237 50 | 51 | def __init__(self, statusQueue:Queue): 52 | self.statusQueue = statusQueue 53 | self.p = Thread(target=self.queueWorker, daemon=True) 54 | self.p.start() 55 | 56 | 57 | pass 58 | 59 | def getAnnotationLabels(self): 60 | # sending default annotation labels 61 | return list(self.annotationLabels.values()) 62 | 63 | def getAnnotationUpdatePolicy(): 64 | # This is important to tell SlideRunner that he needs to update for every change in position. 65 | return SlideRunnerPlugin.AnnotationUpdatePolicy.UPDATE_ON_SLIDE_CHANGE 66 | 67 | def queueWorker(self): 68 | 69 | quitSignal=False 70 | oldSlide = '' 71 | while not quitSignal: 72 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 73 | filename = job 74 | 75 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 76 | # signal to exit this thread 77 | quitSignal=True 78 | continue 79 | if (job.slideFilename != oldSlide) or (job.trigger is not None): 80 | self.processWholeSlide(job) 81 | oldSlide = job.slideFilename 82 | else: 83 | print('Trigger:',job.trigger) 84 | 85 | def getAnnotations(self): 86 | return self.annos 87 | 88 | def processWholeSlide(self, job : SlideRunnerPlugin.pluginJob): 89 | 90 | filename = job.slideFilename 91 | self.slide = openslide.open_slide(filename) 92 | 93 | # 1 HPF = 0.237 mm^2 94 | A = job.configuration[2] # mm^2 95 | W_hpf_microns = np.sqrt(A*4/3) * 1000 # in microns 96 | H_hpf_microns = np.sqrt(A*3/4) * 1000 # in microns 97 | 98 | micronsPerPixel = self.slide.properties[openslide.PROPERTY_NAME_MPP_X] 99 | 100 | W_hpf = int(W_hpf_microns / float(micronsPerPixel)) * np.sqrt(float(0.5*int(2*job.configuration[1]))) 101 | H_hpf = int(H_hpf_microns / float(micronsPerPixel)) * np.sqrt(float(0.5*int(2*job.configuration[1]))) 102 | 103 | center = (int((job.coordinates[0]+0.5*job.coordinates[2])), 104 | int((job.coordinates[1]+0.5*job.coordinates[3]))) 105 | 106 | self.annos = list() 107 | if (int(job.configuration[1])==1): 108 | myanno = annotations.rectangularAnnotation(0, center[0]-W_hpf/2, center[1]-H_hpf/2, center[0]+W_hpf/2, center[1]+H_hpf/2, 'High-Power Field', pluginAnnotationLabel=self.annotationLabels['HPF']) 109 | else: 110 | myanno = annotations.rectangularAnnotation(0, center[0]-W_hpf/2, center[1]-H_hpf/2, center[0]+W_hpf/2, center[1]+H_hpf/2, '%d High-Power Fields' % int(job.configuration[1]),pluginAnnotationLabel=self.annotationLabels['HPF']) 111 | self.annos.append(myanno) 112 | 113 | self.updateAnnotations() 114 | 115 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_ObjDetResults.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | This file: 17 | Visualize bounding boxes (object detection results) 18 | 19 | """ 20 | 21 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 22 | import queue 23 | from threading import Thread 24 | from queue import Queue 25 | import cv2 26 | import os 27 | import numpy as np 28 | import matplotlib.pyplot as plt 29 | import matplotlib.colors 30 | import pickle 31 | import SlideRunner_dataAccess.annotations as annotations 32 | import matplotlib.path as path 33 | 34 | 35 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 36 | version = 0.1 37 | shortName = 'Object Detection Results' 38 | inQueue = Queue() 39 | outQueue = Queue() 40 | initialOpacity=1.0 41 | updateTimer=0.1 42 | outputType = SlideRunnerPlugin.PluginOutputType.NO_OVERLAY 43 | description = 'Show unpickled object detection results' 44 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 45 | configurationList = list(( 46 | SlideRunnerPlugin.FilePickerConfigurationEntry(uid='file', name='Result file', mask='*.p;;*.txt'), 47 | SlideRunnerPlugin.PluginConfigurationEntry(uid='threshold', name='Detection threshold', initValue=0.75, minValue=0.0, maxValue=1.0), 48 | )) 49 | 50 | COLORS = [[0,128,0,255], 51 | [128,0,0,255], 52 | [0,0,128,255], 53 | [128,128,0,255], 54 | [0,128,128,255], 55 | [128,128,128,255]] 56 | 57 | def __init__(self, statusQueue:Queue): 58 | self.statusQueue = statusQueue 59 | self.annotationLabels = {'Detection' : SlideRunnerPlugin.PluginAnnotationLabel(0,'Detection', [0,180,0,255]),} 60 | self.p = Thread(target=self.queueWorker, daemon=True) 61 | self.p.start() 62 | 63 | 64 | 65 | pass 66 | 67 | def getAnnotationUpdatePolicy(): 68 | # This is important to tell SlideRunner that he needs to update for every change in position. 69 | return SlideRunnerPlugin.AnnotationUpdatePolicy.UPDATE_ON_SLIDE_CHANGE 70 | 71 | 72 | def queueWorker(self): 73 | debugModule= False 74 | quitSignal = False 75 | oldFilename = '' 76 | oldArchive = '' 77 | oldSlide = '' 78 | oldThres=-1 79 | while not quitSignal: 80 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 81 | print(job) 82 | print(job.configuration) 83 | 84 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 85 | # signal to exit this thread 86 | quitSignal=True 87 | continue 88 | 89 | if (job.configuration['file'] == oldArchive) and (job.configuration['threshold'] == oldThres) and (job.slideFilename == oldSlide): 90 | continue 91 | 92 | if not (os.path.exists(job.configuration['file'])): 93 | continue 94 | 95 | print('Performing label update') 96 | self.sendAnnotationLabelUpdate() 97 | 98 | oldArchive = job.configuration['file'] 99 | oldThres = job.configuration['threshold'] 100 | oldSlide = job.slideFilename 101 | [foo,self.ext] = os.path.splitext(oldArchive) 102 | self.ext = self.ext.upper() 103 | 104 | self.annos = list() 105 | 106 | if (self.ext=='.P'): # Pickled format - results for many slides 107 | self.resultsArchive = pickle.load(open(oldArchive,'rb')) 108 | 109 | pname,fname = os.path.split(job.slideFilename) 110 | fnamewithfolder = pname.split(os.sep)[-1] + os.sep + fname 111 | if (oldFilename is not fname): 112 | # process slide 113 | if (fname not in self.resultsArchive) and (fnamewithfolder not in self.resultsArchive): 114 | self.setMessage('Slide '+str(fname)+' not found in results file.') 115 | print('List is:',self.resultsArchive.keys()) 116 | continue 117 | 118 | if (fnamewithfolder in self.resultsArchive): 119 | fname=fnamewithfolder 120 | oldFilename=fname 121 | 122 | 123 | uniqueLabels = np.unique(np.array(self.resultsArchive[fname])[:,4]) 124 | 125 | self.annotationLabels = dict() 126 | for key,label in enumerate(uniqueLabels): 127 | self.annotationLabels[label] = SlideRunnerPlugin.PluginAnnotationLabel(key, 'Class %d' % label, self.COLORS[key % len(self.COLORS)]) 128 | 129 | for idx in range(len(self.resultsArchive[fname])): 130 | row = self.resultsArchive[fname][idx] 131 | # Jede Zeile: 132 | # x1, x2, y1, y2, ignorieren, probability, Text 133 | if (row[5]>job.configuration['threshold']): 134 | myanno = annotations.rectangularAnnotation(uid=idx, x1=row[0], x2=row[2], y1=row[1], y2=row[3], text='%.2f' % row[5], pluginAnnotationLabel=self.annotationLabels[row[4]]) 135 | self.annos.append(myanno) 136 | 137 | self.sendAnnotationLabelUpdate() 138 | 139 | 140 | elif (self.ext=='.TXT'): # Assume MS Coco format 141 | self.resultsArchive = np.loadtxt(oldArchive, dtype={'names': ('label', 'confidence', 'x','y','w','h'), 'formats': ('U30', 'f4', 'i4','i4','i4','i4')}, skiprows=0, delimiter=' ') 142 | uniqueLabels = np.unique(self.resultsArchive['label']) 143 | 144 | self.annotationLabels = dict() 145 | for key,label in enumerate(uniqueLabels): 146 | self.annotationLabels[label] = SlideRunnerPlugin.PluginAnnotationLabel(key, label, self.COLORS[key % len(self.COLORS)]) 147 | 148 | self.sendAnnotationLabelUpdate() 149 | 150 | for idx in range(len(self.resultsArchive)): 151 | row = self.resultsArchive[idx] 152 | if (row[5]>job.configuration['threshold']): 153 | myanno = annotations.rectangularAnnotation(uid=idx, x1=row['x'], y1=row['y'], x2=row['x']+row['w'], y2=row['y']+row['h'], text='%.2f' % row['confidence'], pluginAnnotationLabel=self.annotationLabels[row['label']]) 154 | self.annos.append(myanno) 155 | 156 | 157 | 158 | self.updateAnnotations() 159 | self.setProgressBar(-1) 160 | self.setMessage('found %d annotations.' % len(self.annos)) 161 | 162 | 163 | 164 | def getAnnotations(self): 165 | return self.annos 166 | 167 | 168 | def getAnnotationLabels(self): 169 | # sending default annotation labels 170 | return [self.annotationLabels[k] for k in self.annotationLabels.keys()] 171 | 172 | 173 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_coregistration_qt.py: -------------------------------------------------------------------------------- 1 | try: 2 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 3 | import queue 4 | from threading import Thread 5 | from queue import Queue 6 | import openslide 7 | import cv2 8 | import numpy as np 9 | import cv2, openslide 10 | import numpy as np 11 | import os, math 12 | from scipy import ndimage 13 | from scipy.stats import gaussian_kde 14 | import matplotlib.pyplot as plt 15 | import logging 16 | from pathlib import Path 17 | 18 | import qt_wsi_reg.registration_tree as registration 19 | 20 | except Exception: 21 | print('Unable to activate QT co-registration plugin') 22 | raise 23 | 24 | parameters = { 25 | # feature extractor parameters 26 | "point_extractor": "sift", #orb , sift 27 | "maxFeatures": 512, 28 | "crossCheck": False, 29 | "flann": False, 30 | "ratio": 0.6, 31 | "use_gray": False, 32 | 33 | # QTree parameter 34 | "homography": True, 35 | "filter_outliner": False, 36 | "debug": False, 37 | "target_depth": 1, 38 | "run_async": True, 39 | "num_workers": 2, 40 | "thumbnail_size": (1024, 1024) 41 | } 42 | 43 | 44 | 45 | 46 | 47 | 48 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 49 | version = 0.1 50 | shortName = 'Quad-Tree-based WSI Registration (Marzahl et al.)' 51 | inQueue = Queue() 52 | outQueue = Queue() 53 | updateTimer=0.5 54 | outputType = SlideRunnerPlugin.PluginOutputType.RGB_OVERLAY 55 | description = 'Apply Marzahl''s method for WSI co-registration' 56 | pluginType = SlideRunnerPlugin.PluginTypes.IMAGE_PLUGIN 57 | 58 | configurationList = list(( 59 | SlideRunnerPlugin.FilePickerConfigurationEntry(uid='file', name='Second WSI', mask='*.*'), 60 | SlideRunnerPlugin.PushbuttonPluginConfigurationEntry(uid='match',name='Match') 61 | )) 62 | 63 | 64 | def __init__(self, statusQueue:Queue): 65 | self.statusQueue = statusQueue 66 | self.p = Thread(target=self.queueWorker, daemon=True) 67 | self.p.start() 68 | 69 | pass 70 | 71 | def queueWorker(self): 72 | oldwsi=None 73 | quitSignal=False 74 | sl=None 75 | self.qt = None 76 | sl_main = None 77 | while not quitSignal: 78 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 79 | image = job.currentImage 80 | mainWSI = job.slideFilename 81 | 82 | if 'file' not in job.configuration: 83 | continue 84 | 85 | 86 | if (job.configuration['file'] != oldwsi) and (job.configuration['file']!='') and job.configuration['file']: 87 | sl = openslide.open_slide(job.configuration['file']) 88 | 89 | if (mainWSI): 90 | sl_main = openslide.open_slide(mainWSI) 91 | 92 | if (job.trigger is not None) and job.trigger.uid=='match': 93 | print('Trigger: ',job.trigger) 94 | self.setProgressBar(0) 95 | self.setMessage('Calculating registration') 96 | self.qt = registration.RegistrationQuadTree(source_slide_path=Path(mainWSI), target_slide_path=Path(job.configuration['file']), **parameters) 97 | box = np.array([0,0,0,0]) 98 | trans_box = self.qt.transform_boxes(np.array([box]))[0] 99 | self.setMessage('Registration done') 100 | self.setProgressBar(-1) 101 | 102 | if (sl) and (sl_main): 103 | self.setProgressBar(0) 104 | print('Reading from: ',job) 105 | 106 | zoomValue=job.coordinates[3]/job.currentImage.shape[0] 107 | print('Zoom value: ',zoomValue) 108 | 109 | print('Coordinates:',job.coordinates) 110 | if self.qt is None: 111 | self.setProgressBar(-1) 112 | continue 113 | coordinates=[job.coordinates] 114 | reg_coordinates = self.qt.transform_boxes([job.coordinates])[0] 115 | print('registered coordinates:', reg_coordinates) 116 | zoomValue=(reg_coordinates[2])/job.currentImage.shape[0] 117 | 118 | act_level = np.argmin(np.abs(np.asarray(sl.level_downsamples)-zoomValue)) 119 | closest_ds = sl_main.level_downsamples[np.argmin(np.abs(np.asarray(sl_main.level_downsamples)-zoomValue))] 120 | 121 | 122 | 123 | imgarea_w=job.coordinates[2:4] 124 | size_im = (int(imgarea_w[0]/closest_ds), int(imgarea_w[1]/closest_ds)) 125 | print('Image size: ',size_im) 126 | location = [int(x) for x in reg_coordinates[0:2]] 127 | print('Location (original):',job.coordinates[0:2]) 128 | print('Location (offset): ',location) 129 | img = sl.read_region(location=location, level=act_level, size=size_im) 130 | img = np.array(img.resize((job.currentImage.shape[1],job.currentImage.shape[0] ))) 131 | 132 | self.returnImage(img, job.procId) 133 | self.setMessage('Align done.') 134 | self.setProgressBar(-1) 135 | 136 | 137 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 138 | # signal to exit this thread 139 | quitSignal=True 140 | continue 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_countdown.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 2 | import queue 3 | from threading import Thread 4 | from queue import Queue 5 | import cv2 6 | import numpy as np 7 | 8 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 9 | version = 0.1 10 | shortName = 'Countdown' 11 | inQueue = Queue() 12 | outQueue = Queue() 13 | updateTimer=0.5 14 | description = 'Count database objects down to zero' 15 | outputType = SlideRunnerPlugin.PluginOutputType.NO_OVERLAY 16 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 17 | configurationList = list((SlideRunnerPlugin.PluginConfigurationEntry(uid=0, name='Count down from', initValue=300., minValue=0.0, maxValue=1200.0),)) 18 | 19 | def __init__(self, statusQueue:Queue): 20 | self.statusQueue = statusQueue 21 | self.p = Thread(target=self.queueWorker, daemon=True) 22 | self.p.start() 23 | self.total_number = 300 24 | self.last_count = None 25 | 26 | pass 27 | 28 | def queueWorker(self): 29 | quitSignal=False 30 | while not quitSignal: 31 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 32 | self.total_number = int(job.configuration[0]) 33 | 34 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 35 | # signal to exit this thread 36 | quitSignal=True 37 | continue 38 | 39 | database = job.openedDatabase 40 | 41 | annotations_count = len(database.annotations) 42 | if annotations_count != self.last_count: 43 | self.last_count = annotations_count 44 | if self.total_number - annotations_count == 0: 45 | self.setMessage('Done. Thanks for your help :)') 46 | self.showMessageBox('Done. Thanks for your help :)') 47 | elif self.total_number - annotations_count < 0: 48 | self.setMessage('You are ambitious, thats gread, thank you') 49 | else: 50 | self.setMessage('{0} anotations to go'.format(self.total_number - annotations_count)) 51 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_macenko.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 2 | import SlideRunner_dataAccess.annotations as annotations 3 | import queue 4 | from threading import Thread 5 | from queue import Queue 6 | import cv2 7 | import numpy as np 8 | 9 | 10 | import math 11 | import cv2 12 | import numpy as np 13 | 14 | 15 | ''' 16 | 17 | MacenkoNorm: Normalize a RGB image by mapping the appearance to that of a target 18 | 19 | Inputs: 20 | image - Original Image. 21 | 22 | Output: 23 | Inorm - Normalised RGB image. 24 | 25 | References: 26 | [1] M Macenko, M Niethammer, JS Marron, D Borland, JT Woosley, X Guan, C 27 | Schmitt, NE Thomas. "A method for normalizing histology slides for 28 | quantitative analysis". IEEE International Symposium on Biomedical 29 | Imaging: From Nano to Macro, 2009 vol.9, pp.1107-1110, 2009. 30 | 31 | Acknowledgements: 32 | This function is inspired by the Stain normalization toolbox by WARWICK 33 | ,which is available for download at: 34 | http://www2.warwick.ac.uk/fac/sci/dcs/research/tia/software/sntoolbox/ 35 | 36 | Info: provided by Max Krappmann 37 | ''' 38 | 39 | 40 | def quantile(x, q): 41 | n = len(x) 42 | y = np.sort(x) 43 | return (np.interp(q, np.linspace(1 / (2 * n), (2 * n - 1) / (2 * n), n), y)) 44 | 45 | def mypercentile(x, p): 46 | return (quantile(x, np.array(p) / 100)) 47 | 48 | 49 | # 50 | def normalize(I, Iselection = None, job=None): 51 | """ 52 | Macenko Normalization (includes, mypercentile, quantile): 53 | 54 | parameters: 55 | I: the image to normalize 56 | Iselection: subpart (N,3) of image used for normalization 57 | 58 | """ 59 | I0 = 240 60 | beta = 0.15 61 | alpha = 1 62 | # reference matrix for stain 63 | HRef = np.array(((0.5626, 0.2159), (0.7201, 0.8012), (0.4062, 0.5581))) 64 | maxCRef = np.array((1.9705, 1.0308)) 65 | 66 | h = I.shape[0] 67 | w = I.shape[1] 68 | 69 | I = np.float32(I.T) 70 | I = np.reshape(I, (h, w, 3)) 71 | I = I.flatten() 72 | I = np.reshape(I, (3, h * w)) 73 | 74 | I = I.T 75 | if (Iselection is None): 76 | Iselection = I 77 | 78 | print(Iselection.shape) 79 | OD = -np.log((Iselection + 1) / I0) 80 | ODHat = OD.copy() 81 | 82 | ODHat[ODHat <= beta] = 0 83 | ODHat = ODHat.reshape(-1, 3) 84 | ODflat = OD.reshape(-1, 3) 85 | ODcomplete = -np.log((I + 1) / I0) 86 | print('ODC shape',ODcomplete.shape) 87 | ODsmall = list() 88 | for i in ODHat: 89 | if np.count_nonzero(i) == 3: 90 | ODsmall.append(i) 91 | ODHat = np.asarray(ODsmall) 92 | ODHat = np.asarray(ODHat) 93 | 94 | # Compute CovarianceMatrix Unterscheidet sich von Matlab implementation 95 | Cov = np.asarray((np.cov(ODHat.T))) 96 | # Berechne ev von Cov 97 | # V ist nicht auf unitlength normiert 98 | ev, V = np.linalg.eigh(Cov) 99 | evSort = [0, 0, 0] 100 | VSort = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] 101 | 102 | for i in range(len(Cov)): 103 | evSort[2 - i] = ev[i] 104 | for k in Cov: 105 | for j in range(len(k)): 106 | VSort[j][i] = V[j][i] 107 | 108 | for i in range(len(Cov)): 109 | evSort[2 - i] = ev[i] 110 | 111 | ev = evSort 112 | ev = np.asarray(ev) 113 | idx = ev.argsort()[::-1] 114 | V = V[:, idx] 115 | # V mit den ersten 3 Hauptkompnenten unsicher ob wirklich die ersten 3 komponenten 116 | # 3x3 Matrix 117 | # Nimm spalten von V 118 | V = np.array([V[:, 1], V[:, 2]]) 119 | V = V.T 120 | 121 | # _________________________________________________________________ 122 | That = -np.dot(ODHat, V) 123 | # _________________________________________________________________ 124 | phi = np.arctan2(That.T[1], That.T[0]) 125 | # _________________________________________________________________ 126 | 127 | minPhi = mypercentile(phi, alpha) 128 | maxPhi = mypercentile(phi, (100 - alpha)) 129 | 130 | # _______________________________________________ 131 | 132 | RotMin = np.array([[math.cos(minPhi)], [math.sin(minPhi)]]) 133 | RotMax = np.array([[math.cos(maxPhi)], [math.sin(maxPhi)]]) 134 | # _________________________________________________________________ 135 | vMin = -np.dot(V, RotMin) 136 | vMax = -np.dot(V, RotMax) 137 | # _________________________________________________________________________________ 138 | if vMin[0] > vMax[0]: 139 | # if np.sum(vMin,0)*np.sum(vMin,0) > np.sum(vMax,0)*np.sum(vMax,0): 140 | HE = np.concatenate((vMin, vMax), axis=1) 141 | HE = np.array(HE) 142 | else: 143 | HE = np.concatenate((vMax, vMin), axis=1) 144 | HE = np.array(HE) 145 | 146 | Y = ODflat.T 147 | Ycompl = ODcomplete.T 148 | # _________________________________________________________________________________ 149 | C = np.asarray(np.linalg.lstsq(HE, Y)) 150 | C = C[0] 151 | print('C is :',C.shape) 152 | 153 | Ccompl = np.asarray(np.linalg.lstsq(HE, Ycompl)) 154 | Ccompl = Ccompl[0] 155 | # _________________________________________________________________________________ 156 | # Percentile von C ueber 2Dimensionen 157 | # 158 | C0 = C[0] 159 | C1 = C[1] 160 | C0 = np.asarray(C0) 161 | C1 = np.asarray(C1) 162 | C0 = C0.T 163 | C1 = C1.T 164 | maxC0 = mypercentile(C0, 99) 165 | maxC1 = mypercentile(C1, 99) 166 | maxC = np.asarray([maxC0, maxC1]) 167 | maxC = maxC.T 168 | # _________________________________________________________________________________ 169 | C_norm = list() 170 | 171 | C_norm.append([[(C[0] / maxC[0]) * maxCRef[0]], [(C[1] / maxC[1]) * maxCRef[1]]]) 172 | C_norm = np.reshape(np.asarray(C_norm), (C.shape)) 173 | print('CNorm: ',C_norm.shape) 174 | 175 | 176 | C_norm_compl = list() 177 | 178 | C_norm_compl.append([[(Ccompl[0] / maxC[0]) * maxCRef[0]], [(Ccompl[1] / maxC[1]) * maxCRef[1]]]) 179 | C_norm_compl = np.reshape(np.asarray(C_norm_compl), (Ccompl.shape)) 180 | print('CNormcompl: ',C_norm_compl.shape) 181 | 182 | if (job.configuration['mode']==1): 183 | C_norm_compl[0,:]=0 184 | print('CNormcompl: ',C_norm_compl.shape) 185 | elif (job.configuration['mode']==2): 186 | C_norm_compl[1,:]=0 187 | print('CNormcompl: ',C_norm_compl.shape) 188 | 189 | 190 | # _________________________________________________________________________________ 191 | exponent = np.dot(-HRef, C_norm_compl) 192 | Inorm = np.exp(exponent) 193 | Inorm = [i * I0 for i in Inorm] 194 | Inorm = np.int64(Inorm) 195 | Inorm = np.array(Inorm) 196 | print('Inorm: ',Inorm.shape) 197 | # _________________________________________________________________________________ 198 | # Form wieder auf Zeilen und Spalten umrechnen 199 | Inorm = np.reshape(Inorm.T, (w, h, 3)) 200 | Inorm = np.rot90(Inorm, 3) 201 | Inorm = cv2.flip(Inorm, 1) 202 | 203 | return Inorm 204 | 205 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 206 | version = 0.1 207 | shortName = 'Normalize (Macenko)' 208 | inQueue = Queue() 209 | outQueue = Queue() 210 | initialOpacity=1.0 211 | updateTimer=0.5 212 | outputType = SlideRunnerPlugin.PluginOutputType.RGB_IMAGE 213 | description = 'H&E Image normalization (Method by Macenko)' 214 | pluginType = SlideRunnerPlugin.PluginTypes.IMAGE_PLUGIN 215 | configurationList = list((SlideRunnerPlugin.ComboboxPluginConfigurationEntry(uid='mode', name='Mode', options=['show H&E', 'only E','only H'], selected_value=0),)) 216 | 217 | def __init__(self, statusQueue:Queue): 218 | self.statusQueue = statusQueue 219 | self.p = Thread(target=self.queueWorker, daemon=True) 220 | self.p.start() 221 | 222 | pass 223 | 224 | def queueWorker(self): 225 | quitSignal = False 226 | while not quitSignal: 227 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 228 | image = job.currentImage 229 | 230 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 231 | # signal to exit this thread 232 | quitSignal=True 233 | continue 234 | 235 | print('Macenko norm plugin: received 1 image from queue') 236 | self.setProgressBar(0) 237 | 238 | print(job) 239 | if (job.annotations is not None) and len(job.annotations)>0: 240 | if (job.annotations[0].annotationType == annotations.AnnotationType.AREA): 241 | print('Found an area annotation - great!') 242 | minC = job.annotations[0].minCoordinates() 243 | maxC = job.annotations[0].maxCoordinates() 244 | 245 | scaleX = (job.coordinates[2])/job.currentImage.shape[1] 246 | scaleY = (job.coordinates[3])/job.currentImage.shape[0] 247 | 248 | minC = np.array((max(0,int((minC.x-job.coordinates[0])/scaleX)), max(0,int((minC.y-job.coordinates[1])/scaleY)))) 249 | maxC = np.array((min(job.currentImage.shape[1],int((maxC.x-job.coordinates[0])/scaleX)), min(job.currentImage.shape[0],int((maxC.y-job.coordinates[1])/scaleY)))) 250 | 251 | rgb = np.copy(image[:,:,0:3]) 252 | rgb = normalize(rgb, np.reshape(rgb[minC[1]:maxC[1],minC[0]:maxC[0],:], (-1,3)), job=job) 253 | 254 | 255 | else: 256 | 257 | rgb = np.copy(image[:,:,0:3]) 258 | rgb = normalize(rgb, job=job) 259 | print('Stats: ',np.max(np.float32(rgb)), np.min(np.float32(rgb)), np.mean(np.float32(rgb)), np.float32(rgb).shape) 260 | 261 | self.returnImage(np.float32(rgb), job.procId) 262 | self.setMessage('Macenko normalization: done.') 263 | self.setProgressBar(-1) 264 | 265 | 266 | 267 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_otsu.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 2 | import queue 3 | from threading import Thread 4 | from queue import Queue 5 | import cv2 6 | import numpy as np 7 | 8 | 9 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 10 | version = 0.1 11 | shortName = 'OTSU threshold' 12 | inQueue = Queue() 13 | outQueue = Queue() 14 | updateTimer=0.5 15 | outputType = SlideRunnerPlugin.PluginOutputType.HEATMAP 16 | description = 'Apply simple OTSU threshold on the current image' 17 | pluginType = SlideRunnerPlugin.PluginTypes.IMAGE_PLUGIN 18 | 19 | def __init__(self, statusQueue:Queue): 20 | self.statusQueue = statusQueue 21 | self.p = Thread(target=self.queueWorker, daemon=True) 22 | self.p.start() 23 | 24 | pass 25 | 26 | def queueWorker(self): 27 | quitSignal=False 28 | while not quitSignal: 29 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 30 | image = job.currentImage 31 | 32 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 33 | # signal to exit this thread 34 | quitSignal=True 35 | continue 36 | print('OTSU plugin: received 1 image from queue') 37 | self.setProgressBar(0) 38 | 39 | rgb = np.copy(image[:,:,0:3]) 40 | 41 | gray = cv2.cvtColor(rgb,cv2.COLOR_RGB2GRAY) 42 | # OTSU thresholding 43 | ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) 44 | 45 | self.returnImage(np.float32(thresh/255.0), job.procId) 46 | self.setMessage('OTSU calculation done.') 47 | print('OTSU plugin: done') 48 | self.setProgressBar(-1) 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_ppc.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | This file: 17 | Positive Pixel Count Algorithm (Aperio) 18 | 19 | see: 20 | Olson, Allen H. "Image analysis using the Aperio ScanScope." Technical manual. Aperio Technologies Inc (2006). 21 | 22 | 23 | 24 | """ 25 | 26 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 27 | import queue 28 | from threading import Thread 29 | from queue import Queue 30 | import cv2 31 | import numpy as np 32 | import matplotlib.pyplot as plt 33 | 34 | 35 | 36 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 37 | version = 0.1 38 | shortName = 'Positive Pixel Count (Aperio)' 39 | inQueue = Queue() 40 | outQueue = Queue() 41 | initialOpacity=1.0 42 | outputType = SlideRunnerPlugin.PluginOutputType.RGB_IMAGE 43 | description = 'H&E Image normalization (Method by Macenko)' 44 | pluginType = SlideRunnerPlugin.PluginTypes.IMAGE_PLUGIN 45 | configurationList = list((SlideRunnerPlugin.PluginConfigurationEntry(uid=0, name='Hue value', initValue=0.04, minValue=0.0, maxValue=1.0), 46 | SlideRunnerPlugin.PluginConfigurationEntry(uid=1, name='Hue width', initValue=0.08, minValue=0.0, maxValue=1.0), 47 | SlideRunnerPlugin.PluginConfigurationEntry(uid=2, name='Saturation threshold', initValue=0.2, minValue=0.0, maxValue=1.0), 48 | SlideRunnerPlugin.PluginConfigurationEntry(uid=3, name='Weak Upper', initValue=220, minValue=0.0, maxValue=255.0), 49 | SlideRunnerPlugin.PluginConfigurationEntry(uid=4, name='Medium Upper = Weak Lower', initValue=175, minValue=0.0, maxValue=255.0), 50 | SlideRunnerPlugin.PluginConfigurationEntry(uid=5, name='Strong Upper = Medium Lower', initValue=100, minValue=0.0, maxValue=255.0), 51 | SlideRunnerPlugin.PluginConfigurationEntry(uid=6, name='Strong Lower', initValue=0, minValue=0.0, maxValue=255.0))) 52 | 53 | def __init__(self, statusQueue:Queue): 54 | self.statusQueue = statusQueue 55 | self.p = Thread(target=self.queueWorker, daemon=True) 56 | self.p.start() 57 | 58 | pass 59 | 60 | def queueWorker(self): 61 | debugModule= False 62 | quitSignal = False 63 | while not quitSignal: 64 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 65 | image = job.currentImage 66 | 67 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 68 | # signal to exit this thread 69 | quitSignal=True 70 | continue 71 | 72 | rgb = np.copy(image[:,:,0:3]) 73 | 74 | # Convert to HSV 75 | hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV) 76 | 77 | img_hsv = np.reshape(hsv, [-1,3]) 78 | 79 | if (debugModule): 80 | plt.clf() 81 | plt.hist(img_hsv[:,0]/255.0,255) 82 | plt.savefig('histo.pdf') 83 | 84 | HUE_VALUE = job.configuration[0] 85 | HUE_RANGE = job.configuration[1] 86 | SAT_THRESHOLD = job.configuration[2] 87 | hsv = np.float32(hsv)/255.0 88 | hue = hsv[:,:,0] 89 | sat = hsv[:,:,1] 90 | val = hsv[:,:,2]*255.0 91 | hue_masked = (hue > (HUE_VALUE-HUE_RANGE)) & (hue < (HUE_VALUE+HUE_RANGE) ) 92 | 93 | if (HUE_VALUE-HUE_RANGE) < 0.0: 94 | hue_masked += (hue > (HUE_VALUE-HUE_RANGE)+1.0) 95 | if (HUE_VALUE+HUE_RANGE) > 1.0: 96 | hue_masked += (hue < (HUE_VALUE-HUE_RANGE)-1.0) 97 | sat_masked = (hsv[:,:,1]>SAT_THRESHOLD) 98 | 99 | if (debugModule): 100 | plt.clf() 101 | plt.hist(val[sat_masked & hue_masked],255) 102 | plt.savefig('histo_val.pdf') 103 | 104 | strong = (val>job.configuration[6]) & (val<=job.configuration[5]) & sat_masked & hue_masked 105 | medium = (val>job.configuration[5]) & (val<=job.configuration[4]) & sat_masked & hue_masked 106 | weak = (val>job.configuration[4]) & (val<=job.configuration[3]) & sat_masked & hue_masked 107 | 108 | # strong: Red 109 | rgb[strong,0] = 255.0 110 | rgb[strong,1] = 0.0 111 | rgb[strong,2] = 0.0 112 | 113 | # medium: Orange 114 | rgb[medium,0] = 255.0 115 | rgb[medium,1] = 84.0 116 | rgb[medium,2] = 33.0 117 | 118 | # weak: Yellow 119 | rgb[weak,0] = 255.0 120 | rgb[weak,1] = 255.0 121 | rgb[weak,2] = 0.0 122 | 123 | img_hsv = np.reshape(hsv[hue_masked&sat_masked], [-1,3]) 124 | 125 | if (debugModule): 126 | plt.clf() 127 | plt.hist(img_hsv[:,0],255) 128 | plt.savefig('histo_limited.pdf') 129 | 130 | self.returnImage(np.float32(rgb)) 131 | self.statusQueue.put((1, 'PPC: Total: %d Weak: %d Medium: %d Strong: %d ' % (np.prod(rgb.shape[0:2]),np.sum(weak),np.sum(medium),np.sum(strong)) )) 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /SlideRunner/plugins/proc_secondaryDatabase.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | 16 | This file: 17 | Visualize secondary database 18 | 19 | """ 20 | 21 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 22 | import queue 23 | from threading import Thread 24 | from queue import Queue 25 | import cv2 26 | import os 27 | import json 28 | import numpy as np 29 | import matplotlib.pyplot as plt 30 | import matplotlib.colors 31 | import pickle 32 | import SlideRunner_dataAccess.annotations as annotations 33 | import matplotlib.path as path 34 | from SlideRunner_dataAccess.database import Database, hex_to_rgb 35 | 36 | 37 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 38 | version = 0.1 39 | shortName = 'Secondary database visualization' 40 | inQueue = Queue() 41 | outQueue = Queue() 42 | initialOpacity=1.0 43 | updateTimer=0.1 44 | outputType = SlideRunnerPlugin.PluginOutputType.NO_OVERLAY 45 | description = 'Visualize secondary SlideRunner database' 46 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 47 | configurationList = list(( 48 | SlideRunnerPlugin.FilePickerConfigurationEntry(uid='file', name='Database file', mask='*.sqlite;;*.json'), 49 | SlideRunnerPlugin.ComboboxPluginConfigurationEntry(uid='mode', name='Mode', options=['clickable','non-clickable'], selected_value=0), 50 | )) 51 | 52 | COLORS = [[0,128,0,255], 53 | [128,0,0,255], 54 | [0,0,128,255], 55 | [128,128,0,255], 56 | [0,128,128,255], 57 | [128,128,128,255]] 58 | 59 | def __init__(self, statusQueue:Queue): 60 | self.statusQueue = statusQueue 61 | self.annotationLabels = {} 62 | self.secondaryDB = Database() 63 | self.p = Thread(target=self.queueWorker, daemon=True) 64 | self.p.start() 65 | 66 | 67 | 68 | pass 69 | 70 | def getAnnotationUpdatePolicy(): 71 | # This is important to tell SlideRunner that he needs to update for every change in position. 72 | return SlideRunnerPlugin.AnnotationUpdatePolicy.UPDATE_ON_SLIDE_CHANGE 73 | 74 | 75 | def queueWorker(self): 76 | debugModule= False 77 | quitSignal = False 78 | oldFilename = '' 79 | oldArchive = '' 80 | oldSlide = '' 81 | oldThres=-1 82 | while not quitSignal: 83 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 84 | print(job) 85 | 86 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 87 | # signal to exit this thread 88 | quitSignal=True 89 | continue 90 | 91 | if (job.configuration['file'] == oldArchive) and (job.slideFilename == oldSlide): 92 | continue 93 | 94 | if not (os.path.exists(job.configuration['file'])): 95 | continue 96 | self.sendAnnotationLabelUpdate() 97 | 98 | oldArchive = job.configuration['file'] 99 | oldSlide = job.slideFilename 100 | 101 | if (oldArchive.split('.')[-1].upper()=='SQLITE'): 102 | self.secondaryDB.open(oldArchive) 103 | 104 | self.annos = list() 105 | self.annotationLabels = dict() 106 | 107 | for key, (label, annoId,col) in enumerate(self.secondaryDB.getAllClasses()): 108 | self.annotationLabels[annoId] = SlideRunnerPlugin.PluginAnnotationLabel(annoId,'%s' % label, [*hex_to_rgb(col), 255]) 109 | 110 | pname,fname = os.path.split(job.slideFilename) 111 | self.slideUID = self.secondaryDB.findSlideWithFilename(fname,pname) 112 | self.secondaryDB.loadIntoMemory(self.slideUID) 113 | self.annos = list() 114 | 115 | for annoId in self.secondaryDB.annotations.keys(): 116 | anno = self.secondaryDB.annotations[annoId] 117 | anno.pluginAnnotationLabel = self.annotationLabels[anno.agreedClass] 118 | anno.clickable=job.configuration['mode']==0 119 | self.annos.append(anno) 120 | elif (oldArchive.split('.')[-1].upper()=='JSON'): 121 | self.secondaryDB = json.load(open(oldArchive,'r')) 122 | 123 | fname_id = {x['file_name']:x['id'] for x in self.secondaryDB['images']} 124 | 125 | if (job.slideFilename not in fname_id): 126 | self.setMessage('Slide not found in database') 127 | continue 128 | 129 | slide_id = fname_id[job.slideFilename] 130 | 131 | id_category = {x['id']:x['name'] for x in self.secondaryDB['categories']} 132 | for key, (cat) in enumerate(self.secondaryDB['categories']): 133 | self.annotationLabels[int(cat['id'])] = SlideRunnerPlugin.PluginAnnotationLabel(int(cat['id']),'%s' % cat['name'], self.COLORS[key%len(self.COLORS)]) 134 | 135 | print('Added all labels') 136 | 137 | self.annos = list() 138 | for k, anno in enumerate(self.secondaryDB['annotations']): 139 | if (anno['image_id']==slide_id): 140 | bbox = anno['bbox'] 141 | myanno = annotations.rectangularAnnotation(uid=k, x1=bbox[0], y1=bbox[1], x2=bbox[2], y2=bbox[3], pluginAnnotationLabel=self.annotationLabels[int(anno['category_id'])]) 142 | self.annos.append(myanno) 143 | else: 144 | self.setMessage('Could not interpret database format: '+oldArchive.split('.')[-1].upper()) 145 | 146 | self.sendAnnotationLabelUpdate() 147 | 148 | self.updateAnnotations() 149 | self.setProgressBar(-1) 150 | self.setMessage('found %d annotations.' % len(self.annos)) 151 | 152 | 153 | 154 | def getAnnotations(self): 155 | return self.annos 156 | 157 | 158 | def getAnnotationLabels(self): 159 | # sending default annotation labels 160 | return [self.annotationLabels[k] for k in self.annotationLabels.keys()] 161 | 162 | 163 | -------------------------------------------------------------------------------- /SlideRunner/plugins/wsi_segmentation.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 2 | from threading import Thread 3 | from queue import Queue 4 | import cv2 5 | import os 6 | import numpy as np 7 | import h5py 8 | import openslide 9 | 10 | 11 | 12 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 13 | version = 0.1 14 | shortName = 'WSI Segmentation Overlay' 15 | inQueue = Queue() 16 | outQueue = Queue() 17 | initialOpacity = 0.6 18 | updateTimer = 0.1 19 | outputType = SlideRunnerPlugin.PluginOutputType.RGB_OVERLAY 20 | description = 'Visualize segmentation results' 21 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 22 | configurationList = list(( 23 | SlideRunnerPlugin.FilePickerConfigurationEntry(uid='file', name='Result file', mask='*.hdf5'), 24 | )) 25 | 26 | 27 | 28 | COLORS = [[255, 255, 255, 255], # BG 29 | [0, 0, 255, 255], # Dermis 30 | [0, 255, 255, 255], # Epidermis 31 | [255, 0, 0, 255], # Subcutis 32 | [255, 20, 147, 255], # Inflamm/Necrosis 33 | [255, 128, 0, 255]] # Tumor 34 | 35 | #COLORS = [[255, 255, 255, 255], # BG 36 | # [255, 128, 0, 255], # Tumor 37 | # [0, 255, 0, 255], # Necrosis 38 | # [0, 0, 255, 255]] # Tissue 39 | 40 | 41 | 42 | def __init__(self, statusQueue: Queue): 43 | self.statusQueue = statusQueue 44 | self.p = Thread(target=self.queueWorker, daemon=True) 45 | self.p.start() 46 | pass 47 | 48 | def getAnnotationUpdatePolicy(): 49 | # This is important to tell SlideRunner that he needs to update for every change in position. 50 | return SlideRunnerPlugin.AnnotationUpdatePolicy.UPDATE_ON_SCROLL_CHANGE 51 | 52 | def queueWorker(self): 53 | quitSignal = False 54 | oldArchive = '' 55 | oldSlide = '' 56 | oldCoordinates = [-1, -1, -1, -1] 57 | while not quitSignal: 58 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 59 | print(job) 60 | print(job.configuration) 61 | 62 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 63 | # signal to exit this thread 64 | quitSignal = True 65 | continue 66 | 67 | if (job.configuration['file'] == oldArchive) and (job.slideFilename == oldSlide) and np.all( 68 | job.coordinates == oldCoordinates): 69 | continue 70 | 71 | if not (os.path.exists(job.configuration['file'])): 72 | continue 73 | 74 | fileChanged = job.configuration['file'] != oldArchive 75 | slideChanged = job.slideFilename != oldSlide 76 | 77 | oldArchive = job.configuration['file'] 78 | oldSlide = job.slideFilename 79 | oldCoordinates = job.coordinates 80 | 81 | self.slideObj = openslide.open_slide(job.slideFilename) 82 | 83 | if (fileChanged): 84 | self.resultsArchive = h5py.File(oldArchive, "r") 85 | self.factor = 64 86 | if "openslide.bounds-x" in self.slideObj.properties: 87 | new_dimensions = ((int(self.slideObj.properties["openslide.bounds-width"]) + int( 88 | self.slideObj.properties["openslide.bounds-x"])), ( 89 | int(self.slideObj.properties["openslide.bounds-height"]) + int( 90 | self.slideObj.properties["openslide.bounds-y"]))) 91 | self.ds = int(np.round(self.factor / (new_dimensions[0] / self.resultsArchive["segmentation"].shape[1]))) 92 | else: 93 | self.ds = int(np.round(self.factor / (self.slideObj.dimensions[0] / self.resultsArchive["segmentation"].shape[1]))) 94 | self.downsampledMap = self.resultsArchive["segmentation"][::self.ds, ::self.ds] 95 | 96 | self.scaleX = ((job.coordinates[2]) / job.currentImage.shape[1]) 97 | self.scaleY = ((job.coordinates[3]) / job.currentImage.shape[0]) 98 | print('Opened results container.') 99 | print('Downsampled image: ', self.downsampledMap.shape) 100 | 101 | 102 | if slideChanged: 103 | self.slideObj = openslide.open_slide(job.slideFilename) 104 | 105 | 106 | print('returning overlay...') 107 | current_shape = np.array(job.currentImage).shape 108 | coords_overlay = np.int16(np.array(job.coordinates)/self.factor) 109 | image = self.downsampledMap[ 110 | max(coords_overlay[1], 0):min(coords_overlay[1] + coords_overlay[3], self.downsampledMap.shape[0]), 111 | max(coords_overlay[0], 0):min(coords_overlay[0] + coords_overlay[2], 112 | self.downsampledMap.shape[1])] 113 | image = np.asarray([self.COLORS[i] for i in np.int16(image.flatten())],dtype=np.uint8).\ 114 | reshape((image.shape[0], image.shape[1], -1)) 115 | #plt.imsave("overlay_3dhistech_4373_161c.png", image[:,:,:3]) 116 | image = cv2.copyMakeBorder(image,np.abs(min(0, coords_overlay[1])),0,np.abs(min(0, coords_overlay[0])),0,cv2.BORDER_CONSTANT,value=(0,0,0,255)) 117 | image = cv2.copyMakeBorder(image,0,max(0,coords_overlay[3]-image.shape[0]),0,max(0,coords_overlay[2]-image.shape[1]),cv2.BORDER_CONSTANT,value=(0,0,0,255)) 118 | image = cv2.resize(image, dsize=(current_shape[1], current_shape[0])) 119 | 120 | self.returnImage(np.float32(image[:,:,0:3])) 121 | -------------------------------------------------------------------------------- /SlideRunner/plugins/wsi_tumor_classification.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.general.SlideRunnerPlugin as SlideRunnerPlugin 2 | from threading import Thread 3 | from queue import Queue 4 | import cv2 5 | import os 6 | import numpy as np 7 | import h5py 8 | import openslide 9 | 10 | 11 | class Plugin(SlideRunnerPlugin.SlideRunnerPlugin): 12 | version = 0.1 13 | shortName = 'WSI Classification Overlay' 14 | inQueue = Queue() 15 | outQueue = Queue() 16 | initialOpacity = 0.6 17 | updateTimer = 0.1 18 | outputType = SlideRunnerPlugin.PluginOutputType.RGB_OVERLAY 19 | description = 'Visualize classification results' 20 | pluginType = SlideRunnerPlugin.PluginTypes.WHOLESLIDE_PLUGIN 21 | configurationList = list(( 22 | SlideRunnerPlugin.FilePickerConfigurationEntry(uid='file', name='Result file', mask='*.hdf5'), 23 | )) 24 | 25 | 26 | COLORS = [[255, 255, 255, 0], #[0, 0, 0, 125], # Non-Neoplastic 27 | [255, 128, 0, 255], # Melanoma 28 | [0, 96, 0, 255], # Plasmacytoma 29 | [0, 255, 0, 255], # Mast Cell Tumor 30 | [0, 0, 255, 255], # PNST 31 | [255, 0, 0, 255], # SCC 32 | [128, 0, 255, 255], # Trichoblastoma 33 | [0, 255, 255, 255], # Histiocytoma 34 | [255, 255, 255, 0]] # BG 35 | 36 | 37 | def __init__(self, statusQueue: Queue): 38 | self.statusQueue = statusQueue 39 | self.p = Thread(target=self.queueWorker, daemon=True) 40 | self.p.start() 41 | pass 42 | 43 | def getAnnotationUpdatePolicy(): 44 | # This is important to tell SlideRunner that he needs to update for every change in position. 45 | return SlideRunnerPlugin.AnnotationUpdatePolicy.UPDATE_ON_SCROLL_CHANGE 46 | 47 | def queueWorker(self): 48 | quitSignal = False 49 | oldArchive = '' 50 | oldSlide = '' 51 | oldCoordinates = [-1, -1, -1, -1] 52 | while not quitSignal: 53 | job = SlideRunnerPlugin.pluginJob(self.inQueue.get()) 54 | print(job) 55 | print(job.configuration) 56 | 57 | if (job.jobDescription == SlideRunnerPlugin.JobDescription.QUIT_PLUGIN_THREAD): 58 | # signal to exit this thread 59 | quitSignal = True 60 | continue 61 | 62 | if (job.configuration['file'] == oldArchive) and (job.slideFilename == oldSlide) and np.all( 63 | job.coordinates == oldCoordinates): 64 | continue 65 | 66 | if not (os.path.exists(job.configuration['file'])): 67 | continue 68 | 69 | fileChanged = job.configuration['file'] != oldArchive 70 | slideChanged = job.slideFilename != oldSlide 71 | 72 | oldArchive = job.configuration['file'] 73 | oldSlide = job.slideFilename 74 | oldCoordinates = job.coordinates 75 | 76 | self.slideObj = openslide.open_slide(job.slideFilename) 77 | #self.ds = 4 78 | 79 | if (fileChanged): 80 | self.resultsArchive = h5py.File(oldArchive, "r") 81 | self.downsampledMap = self.resultsArchive["classification"] 82 | self.factor = int(self.slideObj.dimensions[0] / self.downsampledMap.shape[1]) 83 | self.scaleX = ((job.coordinates[2]) / job.currentImage.shape[1]) 84 | self.scaleY = ((job.coordinates[3]) / job.currentImage.shape[0]) 85 | print('Opened results container.') 86 | print('Downsampled image: ', self.downsampledMap.shape) 87 | 88 | 89 | if slideChanged: 90 | self.slideObj = openslide.open_slide(job.slideFilename) 91 | 92 | 93 | print('returning overlay...') 94 | current_shape = np.array(job.currentImage).shape 95 | coords_overlay = np.int16(np.array(job.coordinates)/self.factor) 96 | image = self.downsampledMap[ 97 | max(coords_overlay[1], 0):min(coords_overlay[1] + coords_overlay[3], self.downsampledMap.shape[0]), 98 | max(coords_overlay[0], 0):min(coords_overlay[0] + coords_overlay[2], 99 | self.downsampledMap.shape[1])] 100 | image = np.asarray([self.COLORS[i] for i in np.int16(image.flatten())],dtype=np.uint8).\ 101 | reshape((image.shape[0], image.shape[1], -1)) 102 | image = cv2.copyMakeBorder(image,np.abs(min(0, coords_overlay[1])),0,np.abs(min(0, coords_overlay[0])),0,cv2.BORDER_CONSTANT,value=(0,0,0,255)) 103 | image = cv2.copyMakeBorder(image,0,max(0,coords_overlay[3]-image.shape[0]),0,max(0,coords_overlay[2]-image.shape[1]),cv2.BORDER_CONSTANT,value=(0,0,0,255)) 104 | image = cv2.resize(image, dsize=(current_shape[1], current_shape[0]), interpolation=cv2.INTER_NEAREST) 105 | image = np.float32(image[:, :, 0:3]) 106 | image[image == [255, 255, 255]] = np.array(job.currentImage)[:, :, :3][image == [255, 255, 255]] 107 | self.returnImage(image) 108 | -------------------------------------------------------------------------------- /SlideRunner/processing/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | 18 | __all__ = ['screening','thumbnail'] 19 | 20 | -------------------------------------------------------------------------------- /SlideRunner/processing/screening.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | import cv2 17 | import numpy as np 18 | 19 | class screeningMap(object): 20 | 21 | map = None 22 | mapWorkingCopy = None 23 | dims_screeningmap = None 24 | slideLevelDimensions = None 25 | thumbNailSize = None 26 | mapHeatmap = None 27 | mainImageSize = None 28 | 29 | # Reset screening - create new copy of screening map 30 | def reset(self,thresholding): 31 | gray = self.grayImage 32 | if (thresholding=='OTSU'): 33 | # OTSU thresholding 34 | ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) 35 | elif (thresholding=='high'): 36 | ret, thresh = cv2.threshold(gray,200,255,cv2.THRESH_BINARY_INV) 37 | elif (thresholding=='med'): 38 | ret, thresh = cv2.threshold(gray,127,255,cv2.THRESH_BINARY_INV) 39 | elif (thresholding=='low'): 40 | ret, thresh = cv2.threshold(gray,80,255,cv2.THRESH_BINARY_INV) 41 | else: # off 42 | thresh = np.ones_like(gray)*255 43 | 44 | # dilate 45 | dil = cv2.dilate(thresh, kernel = np.ones((7,7),np.uint8)) 46 | 47 | # erode 48 | er = cv2.erode(dil, kernel = np.ones((7,7),np.uint8)) 49 | 50 | self.map = er 51 | self.mapWorkingCopy = np.copy(er) 52 | 53 | 54 | def checkIsNew(self,coordinates,imgarea_w): 55 | check_x = np.int16(np.floor((coordinates[0])*self.map.shape[1])) 56 | check_y = np.int16(np.floor((coordinates[1])*self.map.shape[0])) 57 | self.w_screeningmap = np.int16(np.floor(0.9*imgarea_w[0]/self.slideLevelDimensions[0][0]*self.mapWorkingCopy.shape[1])) 58 | self.h_screeningmap = np.int16(np.floor(0.9*imgarea_w[1]/self.slideLevelDimensions[0][1]*self.mapWorkingCopy.shape[0])) 59 | # print('Checking ',check_x,check_y,self.w_screeningmap,self.h_screeningmap,imgarea_w,np.sum(self.mapWorkingCopy)) 60 | 61 | sumMap = np.sum(self.mapWorkingCopy[check_y:check_y+self.h_screeningmap, check_x:check_x+self.w_screeningmap]) 62 | if (sumMap>0): 63 | self.mapWorkingCopy[check_y:check_y+self.h_screeningmap, check_x:check_x+self.w_screeningmap]=0 64 | return True 65 | 66 | return False 67 | 68 | 69 | def __init__(self,overview, mainImageSize, slideLevelDimensions, thumbNailSize, thresholding:str): 70 | super(screeningMap, self).__init__() 71 | # Convert to grayscale 72 | gray = cv2.cvtColor(overview,cv2.COLOR_BGR2GRAY) 73 | self.grayImage = gray 74 | self.reset(thresholding) 75 | 76 | w_screeningmap = np.int16(np.floor(mainImageSize[0]/slideLevelDimensions[0][0]*self.map.shape[1])*0.9) 77 | h_screeningmap = np.int16(np.floor(mainImageSize[1]/slideLevelDimensions[0][1]*self.map.shape[0])*0.9) 78 | self.dims_screeningmap = (w_screeningmap, h_screeningmap) 79 | 80 | self.slideLevelDimensions = slideLevelDimensions 81 | 82 | self.mapHeatmap = np.zeros((thumbNailSize[1],thumbNailSize[0])) 83 | self.thumbNailSize=thumbNailSize 84 | self.mainImageSize=mainImageSize 85 | 86 | 87 | """ 88 | Display screening map as an overlay to the overview image 89 | """ 90 | 91 | def overlayHeatmap(self, numpyImage) -> np.ndarray: 92 | numpyImage[:,:,0] = np.uint8(np.clip((np.float32(numpyImage[:,:,0]) * (1-self.mapHeatmap[:,:]/255)),0,255)) 93 | numpyImage[:,:,1] = np.uint8(np.clip((np.float32(numpyImage[:,:,1]) * (1+self.mapHeatmap[:,:]/255)),0,255)) 94 | numpyImage[:,:,2] = np.uint8(np.clip((np.float32(numpyImage[:,:,2]) * (1-self.mapHeatmap[:,:]/255)),0,255)) 95 | resizedMap = cv2.resize(self.map, dsize=(numpyImage.shape[1], numpyImage.shape[0])) 96 | numpyImage[resizedMap<250,0] = 0 97 | return numpyImage 98 | 99 | """ 100 | Annotate currently viewed screen on screening map. 101 | """ 102 | 103 | 104 | def annotate(self, imgarea_p1, imgarea_w): 105 | 106 | x_screeningmap = np.int16(np.floor(imgarea_p1[0]/self.slideLevelDimensions[0][0]*self.mapWorkingCopy.shape[1])) 107 | y_screeningmap = np.int16(np.floor(imgarea_p1[1]/self.slideLevelDimensions[0][1]*self.mapWorkingCopy.shape[0])) 108 | 109 | self.w_screeningmap = np.int16(np.floor(0.9*imgarea_w[0]/self.slideLevelDimensions[0][0]*self.mapWorkingCopy.shape[1])) 110 | self.h_screeningmap = np.int16(np.floor(0.9*imgarea_w[1]/self.slideLevelDimensions[0][1]*self.mapWorkingCopy.shape[0])) 111 | 112 | 113 | #self.mapWorkingCopy[y_screeningmap:y_screeningmap+self.h_screeningmap,x_screeningmap:x_screeningmap+self.w_screeningmap] = 0 114 | 115 | image_dims=self.slideLevelDimensions[0] 116 | 117 | # annotate on heatmap 118 | overview_p1 = (int(imgarea_p1[0] / image_dims[0] * self.thumbNailSize[0]),int(imgarea_p1[1] / image_dims[1] * self.thumbNailSize[1])) 119 | overview_p2 = (int((imgarea_p1[0]+imgarea_w[0]) / image_dims[0] * self.thumbNailSize[0]),int((imgarea_w[1]+imgarea_p1[1]) / image_dims[1] * self.thumbNailSize[1])) 120 | 121 | col = int(20*np.square(12*self.mainImageSize[0]/imgarea_w[0])) 122 | if (col>255): 123 | col=255 124 | ohBackup = np.copy(self.mapHeatmap) 125 | cv2.rectangle(self.mapHeatmap, pt1=overview_p1, pt2=overview_p2, color=col, thickness=-1) 126 | 127 | self.mapHeatmap = np.maximum(self.mapHeatmap, ohBackup) 128 | 129 | -------------------------------------------------------------------------------- /SlideRunner/processing/thumbnail.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This is SlideRunner - An Open Source Annotation Tool 4 | for Digital Histology Slides. 5 | 6 | Marc Aubreville, Pattern Recognition Lab, 7 | Friedrich-Alexander University Erlangen-Nuremberg 8 | marc.aubreville@fau.de 9 | 10 | If you use this software in research, please citer our paper: 11 | M. Aubreville, C. Bertram, R. Klopfleisch and A. Maier: 12 | SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images. 13 | In: Bildverarbeitung für die Medizin 2018. 14 | Springer Vieweg, Berlin, Heidelberg, 2018. pp. 309-314. 15 | """ 16 | 17 | import cv2 18 | import numpy as np 19 | 20 | 21 | class thumbnail(): 22 | """ 23 | Thumbnail object for SlideRunner 24 | """ 25 | 26 | thumbnail = None 27 | workingCopy = None 28 | downsampled = None 29 | thumbnail_numpy = None 30 | size = None 31 | slide = None 32 | 33 | def __init__(self, sl ): 34 | thumbNail = sl.get_thumbnail(size=(200, 200)) 35 | self.thumbnail = thumbNail 36 | self.thumbnail_numpy = np.array(self.thumbnail, dtype=np.uint8) 37 | if (self.thumbnail_numpy.shape[0]>self.thumbnail_numpy.shape[1]): 38 | thumbnail_numpy_new = np.ones((200,200,3), np.uint8)*52 39 | thumbnail_numpy_new[0:self.thumbnail_numpy.shape[0],0:self.thumbnail_numpy.shape[1],:] = self.thumbnail_numpy 40 | self.thumbnail_numpy = thumbnail_numpy_new 41 | else: 42 | self.thumbnail_numpy = cv2.resize(self.thumbnail_numpy, dsize=(200,thumbNail.size[1])) 43 | self.downsamplingFactor = np.float32(sl.dimensions[1]/thumbNail.size[1]) 44 | self.size = self.thumbnail.size 45 | self.slide = sl 46 | self.shape = self.thumbnail_numpy.shape 47 | 48 | def getCopy(self): 49 | return np.copy(self.thumbnail_numpy) 50 | 51 | def annotateCurrentRegion(self, npimage, imgarea_p1, imgarea_w): 52 | 53 | image_dims = self.slide.level_dimensions[0] 54 | overview_p1 = (int(imgarea_p1[0] / image_dims[0] * self.thumbnail.size[0]),int(imgarea_p1[1] / image_dims[1] * self.thumbnail.size[1])) 55 | overview_p2 = (int((imgarea_p1[0]+imgarea_w[0]) / image_dims[0] * self.thumbnail.size[0]),int((imgarea_w[1]+imgarea_p1[1]) / image_dims[1] * self.thumbnail.size[1])) 56 | cv2.rectangle(npimage, pt1=overview_p1, pt2=overview_p2, color=[255,0,0,127],thickness=2) 57 | 58 | return npimage -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | pyinstaller main.spec -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | # Build script for Mac OS X 2 | # 3 | # make sure you have an old version of opencv2, else it does not work. 4 | # --> pip install --upgrade opencv-python==3.3.0.9 5 | 6 | 7 | pyinstaller --windowed --onefile main_osx.spec 8 | 9 | plutil -replace LSBackgroundOnly -bool false dist/SlideRunner.app/Contents/Info.plist 10 | 11 | hdiutil create -volname SlideRunner -srcfolder dist/SlideRunner.app -ov -format UDZO SlideRunner.dmg 12 | -------------------------------------------------------------------------------- /databases/MITOS2012.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/databases/MITOS2012.sqlite -------------------------------------------------------------------------------- /databases/MITOS2012_test.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/databases/MITOS2012_test.sqlite -------------------------------------------------------------------------------- /databases/MITOS2014.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/databases/MITOS2014.sqlite -------------------------------------------------------------------------------- /databases/TUPAC.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/databases/TUPAC.sqlite -------------------------------------------------------------------------------- /databases/TUPAC_Mitosis_ROI.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/databases/TUPAC_Mitosis_ROI.sqlite -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import SlideRunner.__main__ 2 | 3 | if __name__ == '__main__': 4 | SlideRunner.__main__.main() 5 | 6 | -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['main.py'], 9 | pathex=[], 10 | binaries=[('libopenslide-0.dll','.')], 11 | datas=[('SlideRunner/artwork/*.png','SlideRunner/artwork'), 12 | ('SlideRunner/plugins/*.*','SlideRunner/plugins')], 13 | hiddenimports=['h5py', 'skimage', 'sklearn', 'scipy.ndimage', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quadtree', 'sklearn.naive_bayes', 'qt_wsi_reg', 'qt_wsi_reg.registration_tree'], 14 | hookspath=[], 15 | hooksconfig={}, 16 | runtime_hooks=[], 17 | excludes=[], 18 | win_no_prefer_redirects=False, 19 | win_private_assemblies=False, 20 | cipher=block_cipher, 21 | noarchive=False, 22 | ) 23 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 24 | 25 | exe = EXE( 26 | pyz, 27 | a.scripts, 28 | a.binaries, 29 | a.zipfiles, 30 | a.datas, 31 | [], 32 | name='SlideRunner', 33 | debug=False, 34 | icon='SlideRunner/artwork/icon.ico', 35 | bootloader_ignore_signals=False, 36 | strip=False, 37 | upx=True, 38 | upx_exclude=[], 39 | runtime_tmpdir=None, 40 | console=True, 41 | disable_windowed_traceback=False, 42 | argv_emulation=False, 43 | target_arch=None, 44 | codesign_identity=None, 45 | entitlements_file=None, 46 | ) 47 | -------------------------------------------------------------------------------- /main_osx.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | 6 | a = Analysis(['main.py'], 7 | pathex=[], 8 | binaries=[], 9 | datas=[('SlideRunner/artwork/*.png','SlideRunner/artwork'), 10 | ('SlideRunner/plugins/*.*','SlideRunner/plugins')], 11 | hiddenimports=['openslide','cv2','opencv-python'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=[], 15 | win_no_prefer_redirects=False, 16 | win_private_assemblies=False, 17 | cipher=block_cipher, 18 | noarchive=False) 19 | pyz = PYZ(a.pure, a.zipped_data, 20 | cipher=block_cipher) 21 | exe = EXE(pyz, 22 | a.scripts, 23 | a.binaries, 24 | a.zipfiles, 25 | a.datas, 26 | [], 27 | name='SlideRunner', 28 | debug=False, 29 | icon='SlideRunner/artwork/icon.ico', 30 | bootloader_ignore_signals=False, 31 | strip=False, 32 | upx=True, 33 | runtime_tmpdir=None, 34 | console=True ) 35 | app = BUNDLE(exe, 36 | name='SlideRunner.app', 37 | icon='SlideRunner/artwork/SlideRunner.icns', 38 | bundle_identifier='SlideRunner') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | EXCAT_Sync==0.0.34 2 | h5py==3.7.0 3 | matplotlib==3.5.3 4 | numpy==1.22.0 5 | opencv_python==4.5.3.56 6 | openslide_wrapper==1.1.2 7 | Pillow==9.2.0 8 | PyQt5==5.15.7 9 | qt_wsi_registration==0.0.11 10 | requests==2.28.1 11 | requests_toolbelt==0.9.1 12 | rollbar==0.14.7 13 | scikit_image==0.16.2 14 | scikit_learn==1.1.2 15 | scipy==1.9.0 16 | setuptools==65.5.1 17 | Shapely==1.8.2 18 | SlideRunner_dataAccess==1.0.9 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # setup.py 4 | import os 5 | import sys 6 | 7 | version = '2.2.0' 8 | 9 | if sys.argv[-1] == 'publish': 10 | if (os.system("python setup.py test") == 0): 11 | if (os.system("python setup.py sdist upload") == 0): 12 | if (os.system("python setup.py bdist_wheel upload") == 0): 13 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 14 | os.system("git push") 15 | 16 | sys.exit() 17 | 18 | # Below this point is the rest of the setup() function 19 | 20 | setup(name='SlideRunner', 21 | version=version, 22 | description='SlideRunner - A Tool for Massive Cell Annotations in Whole Slide Images', 23 | url='http://github.com/maubreville/SlideRunner', 24 | author='Marc Aubreville', 25 | author_email='marc.aubreville@fau.de', 26 | license='GPL', 27 | packages=find_packages(), 28 | package_data={ 29 | 'SlideRunner': ['artwork/*.png', 'Slides.sqlite', 'plugins/*.py'], 30 | }, 31 | install_requires=[ 32 | 'openslide-python>=1.1.1', 'opencv-python>=3.1.0', 33 | 'SlideRunner_dataAccess>=1.0.7', 34 | 'matplotlib>=2.0.0', 'numpy>=1.13', 'matplotlib>=2.0.0', 'rollbar>=0.14', 'shapely>=1.6.4', 'pydicom>=1.4.1' 35 | ], 36 | setup_requires=['pytest-runner'], 37 | entry_points={ 38 | # 'console_scripts': [ 39 | # 'foo = my_package.some_module:main_func', 40 | # 'bar = other_module:some_func', 41 | # ], 42 | 'gui_scripts': [ 43 | 'sliderunner = SlideRunner.__main__:main', 44 | ] 45 | }, 46 | tests_require=['pytest'], 47 | zip_safe=False) 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeepMicroscopy/SlideRunner/532bfeb063a7e0e1f22d1816111e508b628436ec/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from SlideRunner_dataAccess.database import * 2 | import os 3 | 4 | def test_database(): 5 | DB = Database() 6 | assert(DB is not None) 7 | assert(DB.isOpen() == False) 8 | 9 | dbname = ':memory:' 10 | DB.create(dbname) 11 | 12 | assert(DB.isOpen() == True) 13 | assert(DB.getDBname() == dbname) 14 | 15 | DB.insertAnnotator('John Doe') 16 | DB.insertAnnotator('Jane Doe') 17 | pers = DB.getAllPersons() 18 | assert((pers[0][0]=='John Doe')) 19 | assert((pers[0][1]==1)) 20 | assert((pers[1][0]=='Jane Doe')) 21 | assert((pers[1][1]==2)) 22 | 23 | DB.insertClass('Cell') 24 | DB.insertClass('Crap') 25 | 26 | classes = DB.getAllClasses() 27 | assert((classes[0][0]=='Cell')) 28 | assert((classes[0][1]==1)) 29 | assert((classes[1][0]=='Crap')) 30 | assert((classes[1][1]==2)) 31 | assert((classes[1][2][0]=='#')) # some color 32 | 33 | DB.insertNewSlide('BLA.tiff','blub/BLA.tiff') 34 | DB.insertNewSlide('BLU.tiff','blub/BLU.tiff') 35 | 36 | assert(DB.findSlideWithFilename('BLA.tiff','blub/BLA.tiff')==1) 37 | assert(DB.findSlideWithFilename('BLA.tiff','blas/BLA.tiff')==1) 38 | 39 | assert(DB.findSlideWithFilename('BLU.tiff','blub/BLU.tiff')==2) 40 | 41 | # spotannos = DB.findSpotAnnotations(leftUpper=(0,0), rightLower=(10,10), slideUID=1, blinded = False, currentAnnotator=1) 42 | # assert(len(spotannos)==0) 43 | 44 | DB.insertNewSpotAnnotation(xpos_orig=5,ypos_orig=5, slideUID=1, classID=2, annotator=1, type = 1, description='abc', zLevel=0) 45 | DB.insertNewSpotAnnotation(xpos_orig=5,ypos_orig=15, slideUID=1, classID=2, annotator=1, type = 1, zLevel=0) 46 | DB.insertNewSpotAnnotation(xpos_orig=5,ypos_orig=8, slideUID=1, classID=2, annotator=2, type = 1, zLevel=0) 47 | DB.insertNewSpotAnnotation(xpos_orig=5,ypos_orig=4, slideUID=1, classID=1, annotator=2, type = 1, zLevel=0) 48 | DB.insertNewSpotAnnotation(xpos_orig=5,ypos_orig=4, slideUID=2, classID=2, annotator=2, type = 1, zLevel=0) 49 | 50 | # spotannos = DB.findSpotAnnotations(leftUpper=(0,0), rightLower=(10,10), slideUID=1, blinded = False, currentAnnotator=1) 51 | # assert(spotannos[0][0]==5) # x coordinate 52 | # assert(spotannos[0][1]==5) # y coordinate 53 | # assert(spotannos[0][2]==2) # agreed Class 54 | 55 | # assert(len(spotannos)==3) # entry 2 is not found 56 | 57 | # test blinded mode 58 | # spotannos = DB.findSpotAnnotations(leftUpper=(0,0), rightLower=(10,10), slideUID=1, blinded = True, currentAnnotator=2) 59 | # assert(spotannos[0][0]==5) # x coordinate 60 | # assert(spotannos[0][1]==5) # y coordinate 61 | # assert(spotannos[0][2]==0) # agreed Class is unknown, because blinded 62 | 63 | # spotannos = DB.findSpotAnnotations(leftUpper=(0,0), rightLower=(10,10), slideUID=1, blinded = True, currentAnnotator=255) 64 | # assert(spotannos[0][0]==5) # x coordinate 65 | # assert(spotannos[0][1]==5) # y coordinate 66 | # assert(spotannos[0][2]==0) # agreed Class is unknown, because blinded 67 | 68 | 69 | # spotannos = DB.findSpotAnnotations(leftUpper=(0,0), rightLower=(10,10), slideUID=5, blinded = True, currentAnnotator=2) 70 | # assert(len(spotannos)==0) 71 | 72 | # Test Circular annotations 73 | DB.insertNewAreaAnnotation(x1=10,y1=10,x2=20,y2=20, slideUID=1, classID=1, annotator=1, uuid='ABC', typeId=5, zLevel=0) # circle 74 | DB.insertNewAreaAnnotation(x1=15,y1=10,x2=20,y2=20, slideUID=1, classID=1, annotator=1, typeId=2, zLevel=0) # rectangle 75 | 76 | 77 | 78 | # test statistics 79 | 80 | stats = DB.countEntryPerClass(slideID=2) 81 | 82 | assert('Cell' in stats) 83 | assert('Crap' in stats) 84 | 85 | assert(stats['Cell']['count_slide']==0) 86 | assert(stats['Crap']['count_slide']==1) 87 | 88 | assert(stats['Cell']['count_total']==3) 89 | assert(stats['Crap']['count_total']==4) 90 | 91 | DB.loadIntoMemory(1) 92 | assert(DB.annotations[list(DB.annotations.keys())[-2]].guid=='ABC') 93 | 94 | assert(DB.annotations[list(DB.annotations.keys())[-2]].x1 == 15) # it's a circle object - thus it will have different coords 95 | assert(DB.annotations[list(DB.annotations.keys())[-1]].x2 == 20) 96 | 97 | # Manipulate agreed class 98 | assert(DB.annotations[1].agreedClass==2) 99 | DB.setAgreedClass(1,1) 100 | assert(DB.annotations[1].agreedClass==1) 101 | 102 | # check description field 103 | assert(DB.annotations[1].text=='abc') 104 | 105 | # Change class name 106 | DB.renameClass(1, 'Bla') 107 | 108 | 109 | pass 110 | 111 | 112 | -------------------------------------------------------------------------------- /tests/test_exact.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from SlideRunner.dataAccess.exact import * 4 | from SlideRunner_dataAccess.database import Database 5 | import os 6 | 7 | EXACT_UNITTEST_URL = 'http://localhost:1337/' 8 | 9 | 10 | from exact_sync.v1.api_client import ApiClient as client 11 | from exact_sync.v1.api.image_sets_api import ImageSetsApi # noqa: E501 12 | from exact_sync.v1.api.teams_api import TeamsApi 13 | from exact_sync.v1.rest import ApiException 14 | from exact_sync.v1.models import ImageSet, Team 15 | 16 | 17 | from exact_sync.v1.api.annotations_api import AnnotationsApi 18 | from exact_sync.v1.api.images_api import ImagesApi 19 | from exact_sync.v1.api.image_sets_api import ImageSetsApi 20 | from exact_sync.v1.api.annotation_types_api import AnnotationTypesApi 21 | from exact_sync.v1.api.products_api import ProductsApi 22 | from exact_sync.v1.api.teams_api import TeamsApi 23 | 24 | from exact_sync.v1.models import ImageSet, Team, Product, AnnotationType as ExactAnnotationType, Image, Annotation, AnnotationMediaFile 25 | from exact_sync.v1.rest import ApiException 26 | from exact_sync.v1.configuration import Configuration 27 | from exact_sync.v1.api_client import ApiClient 28 | 29 | from pathlib import Path 30 | 31 | configuration = Configuration() 32 | configuration.username = 'testuser' 33 | configuration.password = 'testpw' 34 | configuration.host = EXACT_UNITTEST_URL 35 | 36 | 37 | 38 | def test_setup(): 39 | 40 | client = ApiClient(configuration) 41 | apis = ExactAPIs(client) 42 | 43 | 44 | # annos = image_sets_api.list_image_sets(expand="product_set") 45 | # image_sets_api.retrieve_image_set(1, expand="product_set") 46 | 47 | # Delete all previous annotation types 48 | try: 49 | for at in apis.annotation_types_api.list_annotation_types().results: 50 | if not (at.deleted): 51 | apis.annotation_types_api.destroy_annotation_type(id=at.id) 52 | except: 53 | print('Unable to destroy annotation types') 54 | 55 | body = Team(name='TestTeam') 56 | apis.team_api.create_team(body=body) 57 | body = ImageSet(team=1, name='Test-ImageSet') 58 | apis.image_sets_api.create_image_set(body=body) 59 | 60 | 61 | def test_images(): 62 | 63 | client = ApiClient(configuration) 64 | apis = ExactAPIs(client) 65 | 66 | imageset = apis.image_sets_api.list_image_sets().results[0].id 67 | 68 | # Delete all images 69 | product_id=1 70 | 71 | allImagesInSet = apis.images_api.list_images(omit="annotations").results 72 | for img in allImagesInSet: 73 | apis.images_api.destroy_image(id=img.id) 74 | 75 | allImagesInSet = apis.images_api.list_images(omit="annotations").results 76 | assert(len(allImagesInSet)==0) # all images gone 77 | 78 | # generate dummy image 79 | dummy=np.random.randint(0,255, (200,200,3)) 80 | cv2.imwrite('dummy.png', dummy) 81 | 82 | apis.images_api.create_image(file_path='dummy.png', image_type=0, image_set=imageset).results 83 | 84 | allImagesInSet = apis.images_api.list_images(omit="annotations").results 85 | # select first image in imageset 86 | imageid = allImagesInSet[0].id 87 | 88 | assert(apis.images_api.retrieve_image(id=imageid).filename=='dummy.tiff') 89 | 90 | apis.images_api.download_image(id=imageid, target_path='./dummy-retrieved.png',original_image=True) 91 | retr = cv2.imread('dummy-retrieved.png') 92 | 93 | assert(np.all(retr==dummy)) 94 | 95 | apis.images_api.destroy_image(id=imageid) 96 | 97 | allImagesInSet = apis.images_api.list_images(omit="annotations").results 98 | 99 | 100 | assert(len(allImagesInSet)==0) # all images gone 101 | 102 | 103 | os.remove('dummy.png') 104 | os.remove('dummy-retrieved.png') 105 | 106 | def cleanup(): 107 | 108 | client = ApiClient(configuration) 109 | apis = ExactAPIs(client) 110 | 111 | imageset = apis.image_sets_api.list_image_sets().results[0].id 112 | 113 | # loop through dataset, delete all annotations and images 114 | allImagesInSet = apis.images_api.list_images(omit="annotations").results 115 | # select first image in imageset 116 | for image in allImagesInSet: 117 | annos = apis.annotations_api.list_annotations(id=image.id, pagination=False).results 118 | for anno in annos: 119 | apis.annotations_api.destroy_annotation(anno.id, keep_deleted_element=False) 120 | apis.images_api.destroy_image(id=image.id) 121 | 122 | product_id=1 123 | 124 | # Delete all previous annotation types 125 | annoTypes = apis.annotation_types_api.list_annotation_types(product_id).results 126 | for at in annoTypes: 127 | apis.annotation_types_api.destroy_annotation_type(at.id) 128 | 129 | assert(len(apis.annotation_types_api.list_annotation_types(product_id).results)==0) 130 | 131 | 132 | def test_pushannos(): 133 | imageset=1 134 | product_id=1 135 | 136 | client = ApiClient(configuration) 137 | apis = ExactAPIs(client) 138 | 139 | randstr = ''.join(['{:02x}'.format(x) for x in np.random.randint(0,255,6)]) 140 | imagename = f'dummy{randstr}.png' 141 | 142 | imageset = apis.image_sets_api.list_image_sets().results[0].id 143 | 144 | # generate dummy image 145 | dummy=np.random.randint(0,255, (200,200,3)) 146 | cv2.imwrite(imagename, dummy) 147 | 148 | exm = ExactManager(username=configuration.username, password=configuration.password, serverurl=configuration.host) 149 | iset = exm.upload_image_to_imageset(imageset_id=imageset, filename=imagename) 150 | # apis.images_api.create_image(file_path=imagename, image_type=0, image_set=imageset).results 151 | imageset_details = apis.image_sets_api.retrieve_image_set(id=imageset, expand='images') 152 | 153 | imageid=imageset_details.images[0]['id'] 154 | 155 | DB = Database().create(':memory:') 156 | 157 | # Add slide to database 158 | slideuid = DB.insertNewSlide(imagename,'') 159 | DB.insertClass('BB') 160 | DB.insertAnnotator('sliderunner_unittest') 161 | DB.insertAnnotator('otherexpert') # we will only send annotations of the marked expert 162 | DB.insertClass('POLY') 163 | 164 | coords = np.array([[100,200],[150,220],[180,250]]) 165 | 166 | DB.insertNewPolygonAnnotation(annoList=coords, slideUID=1, classID=2, annotator=1) 167 | 168 | coords = np.array([[150,250],[350,220],[0,250]]) 169 | DB.insertNewPolygonAnnotation(annoList=coords, slideUID=1, classID=2, annotator=1) 170 | 171 | coords = np.array([[150,255],[350,210],[50,250]]) 172 | DB.insertNewPolygonAnnotation(annoList=coords, slideUID=1, classID=2, annotator=2) 173 | 174 | DB.setExactPerson(1) 175 | #empty image 176 | annos = apis.annotations_api.list_annotations(id=imageid, pagination=False).results 177 | 178 | for anno in annos: 179 | apis.annotations_api.destroy_annotation(id=anno.id) 180 | 181 | # for anno in exm.retrieve_annotations(imageid): 182 | # print(anno) 183 | # All annotations have been removed 184 | assert(len(apis.annotations_api.list_annotations(id=imageid, pagination=False).results)==0) 185 | 186 | exm.sync(imageid, imageset_id=imageset, product_id=product_id, slideuid=slideuid, database=DB) 187 | 188 | # Only 2 annotations have been inserted 189 | assert(len(apis.annotations_api.list_annotations(imageid).results)==2) 190 | 191 | uuids = [x.unique_identifier for x in apis.annotations_api.list_annotations(imageid).results] 192 | # All were created with correct guid 193 | for dbanno in list(DB.annotations.keys())[:-1]: 194 | assert(DB.annotations[dbanno].guid in uuids) 195 | 196 | print('--- resync ---') 197 | 198 | # Sync again 199 | exm.sync(imageid, imageset_id=imageset, product_id=product_id, slideuid=slideuid, database=DB) 200 | 201 | # No change 202 | print('Length is now: ',len(apis.annotations_api.list_annotations(imageid).results)) 203 | assert(len(apis.annotations_api.list_annotations(imageid).results)==2) 204 | 205 | # All were created with correct guid 206 | uuids = [x.unique_identifier for x in apis.annotations_api.list_annotations(imageid).results] 207 | for dbanno in list(DB.annotations.keys())[:-1]: 208 | assert(DB.annotations[dbanno].guid in uuids) 209 | 210 | print('--- local update created ---') 211 | 212 | # Now let's create a local update - keep same exact_id (crucial!) 213 | DB.loadIntoMemory(1) 214 | DB.setAnnotationLabel(classId=1, person=1, annoIdx=1, entryId=DB.annotations[1].labels[0].uid, exact_id=DB.annotations[1].labels[0].exact_id) 215 | 216 | # Sync again 217 | exm.sync(imageid, imageset_id=imageset, product_id=product_id, slideuid=slideuid, database=DB) 218 | 219 | # check if remote has been updated 220 | # annos = apis.annotations_api.list_annotations(id=imageid, pagination=False).results 221 | annos = np.array(apis.annotations_api.list_annotations(imageid, expand='annotation_type').results) 222 | 223 | assert(len(annos)>0) 224 | 225 | for anno in annos: 226 | if (anno.id==DB.annotations[1].labels[0].exact_id): 227 | assert(anno.annotation_type['name']=='BB') 228 | annotype_id = anno.annotation_type['id'] 229 | 230 | # Now update remotely and see if changes are reflected 231 | newguid = str(uuid.uuid4()) 232 | vector = list_to_exactvector([[90,80],[20,30]]) 233 | vector['frame']=2 234 | lastModified=datetime.datetime.fromtimestamp(time.time()).strftime( "%Y-%m-%dT%H:%M:%S.%f") 235 | annotation = Annotation(annotation_type=annotype_id, vector=vector, image=imageid, unique_identifier=newguid, last_edit_time=lastModified, time=lastModified, description='abcdef') 236 | created = apis.annotations_api.create_annotation(body=annotation ) 237 | 238 | exm.sync(imageid, imageset_id=imageset, product_id=product_id, slideuid=slideuid, database=DB) 239 | DB.loadIntoMemory(1, zLevel=None) 240 | found=False 241 | for annoI in DB.annotations: 242 | anno=DB.annotations[annoI] 243 | if (anno.guid == newguid): 244 | found=True 245 | assert(anno.annotationType==AnnotationType.POLYGON) 246 | assert(anno.text=='abcdef') 247 | 248 | assert(anno.labels[0].exact_id==created.id) 249 | 250 | assert(found) 251 | 252 | # also check in stored database 253 | DB.loadIntoMemory(1) 254 | for annoI in DB.annotations: 255 | anno=DB.annotations[annoI] 256 | if (anno.guid == newguid): 257 | found=True 258 | assert(anno.annotationType==AnnotationType.POLYGON) 259 | assert(anno.labels[0].exact_id==created.id) 260 | 261 | 262 | annos = apis.annotations_api.list_annotations(id=imageid, pagination=False).results 263 | for anno in annos: 264 | apis.annotations_api.destroy_annotation(anno.id, keep_deleted_element=False) 265 | 266 | 267 | # All gone 268 | assert(len(apis.annotations_api.list_annotations(id=imageid, pagination=False).results)==0) 269 | 270 | # Now delete image 271 | apis.images_api.destroy_image(id=imageid) 272 | 273 | os.remove(imagename) 274 | exm.terminate() 275 | 276 | if __name__ == "__main__": 277 | test_setup() 278 | cleanup() 279 | test_images() 280 | test_pushannos() 281 | cleanup() 282 | -------------------------------------------------------------------------------- /tests/test_screening.py: -------------------------------------------------------------------------------- 1 | 2 | from SlideRunner.processing.screening import * 3 | import numpy as np 4 | def test_screening(): 5 | overview = np.zeros((100,100,3),np.uint8) 6 | overview[:,:,:] = 255 7 | overview[:,5:15,:] = 0 8 | overview[0,0,:] = 254 # removed by otsu 9 | overview[99,99,:] = 254 # removed by otsu 10 | 11 | overview[50:52,5:15,:] = 255 # removed by closing operator 12 | 13 | map = screeningMap(overview=overview,mainImageSize=(500,500), slideLevelDimensions=[[500,500],[100,100]], thumbNailSize=(20,20), thresholding='OTSU') 14 | 15 | # in mean, exactly 10 lines in the map 16 | assert(np.sum(map.mapWorkingCopy)/255/100) 17 | 18 | map.annotate(imgarea_p1=(200,200), imgarea_w=(250,250)) 19 | 20 | # rectangle with (in total) 121 pixels was created 21 | assert((np.sum(map.mapHeatmap)/255)==121.0) 22 | 23 | --------------------------------------------------------------------------------