├── src ├── __init__.py └── histolab │ ├── filters │ ├── __init__.py │ ├── util.py │ ├── compositions.py │ └── morphological_filters_functional.py │ ├── data │ ├── cmu_small_region.svs │ └── _registry.py │ ├── __init__.py │ ├── types.py │ ├── exceptions.py │ ├── scorer.py │ └── tile.py ├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── data │ │ ├── __init__.py │ │ └── test_data.py │ ├── filters │ │ ├── __init__.py │ │ ├── test_util.py │ │ └── test_morphological_filters.py │ ├── test_scorer.py │ ├── test_exceptions.py │ ├── test_compositions.py │ └── test_util.py ├── benchmarks │ ├── __init__.py │ └── test_benchmarks.py ├── integration │ ├── __init__.py │ ├── test_tile.py │ ├── test_slide.py │ ├── test_scorer.py │ ├── test_tiler.py │ └── test_morphological_filters.py ├── expectations │ ├── python-expr │ │ ├── np-to-pil-l.py │ │ ├── np-to-pil-la.py │ │ ├── apply-mask-image-exp4.py │ │ ├── 5-x-5-ones.py │ │ ├── 5-x-5-zeros.py │ │ ├── threshold-to-mask-140.py │ │ ├── threshold-to-mask-160.py │ │ ├── np-to-pil-rgba.py │ │ ├── apply-mask-image-exp1.py │ │ ├── apply-mask-image-exp2.py │ │ ├── apply-mask-image-exp3.py │ │ ├── 5-x-5-x-3-ones.py │ │ ├── threshold-to-mask-39.py │ │ ├── threshold-to-mask-200.py │ │ ├── 5-x-5-x-3-zeros.py │ │ ├── 5-x-5-x-4-ones.py │ │ ├── threshold-to-mask-178.py │ │ ├── threshold-to-mask-37.py │ │ └── 5-x-5-x-4-zeros.py │ ├── mask-arrays │ │ ├── polygon-to-mask-array-0325.npy │ │ ├── polygon-to-mask-array-1020.npy │ │ ├── polygon-to-mask-array-2143.npy │ │ ├── tcga-lung-rgb-pen-marks-mask.npy │ │ ├── diagnostic-slide-thumb-grays-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-grays-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-grays-mask.npy │ │ ├── ytma1-watershed-segmentation-region3.npy │ │ ├── ytma1-watershed-segmentation-region6.npy │ │ ├── ytma2-watershed-segmentation-region3.npy │ │ ├── ytma2-watershed-segmentation-region6.npy │ │ ├── diagnostic-slide-thumb-ycbcr-grays-mask.npy │ │ ├── diagnostic-slide-thumb-gs-canny-edges-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-pen-marks-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-otsu-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-red-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-pen-marks-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-yen-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-blue-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-green-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-gs-filter-entropy-mask.npy │ │ ├── diagnostic-slide-thumb-gs-otsu-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-gs-yen-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-yen-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-yen-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-rgba1-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba1-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba2-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba2-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba3-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba3-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-blue-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-pen-marks-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-red-filter-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-blue-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-red-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-blue-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-red-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba1-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba2-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba3-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-green-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-yen-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-green-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-rgb-green-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba1-green-ch-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba2-green-ch-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba3-green-ch-filter-mask.npy │ │ ├── diagnostic-slide-thumb-rgba4-green-ch-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-blue-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-green-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-otsu-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-red-pen-filter-mask.npy │ │ ├── diagnostic-slide-thumb-gs1-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-gs2-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-gs3-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-gs4-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-rgb2-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-ycbcr-hysteresis-threshold-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects-mask.npy │ │ ├── diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects2-mask.npy │ │ ├── diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects-mask.npy │ │ └── diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects2-mask.npy │ ├── pil-images-rgba │ │ ├── tcga-lung-eosin-channel.png │ │ ├── diagnostic-slide-thumb-inverted.png │ │ ├── tcga-lung-hematoxylin-channel.png │ │ ├── diagnostic-slide-thumb-rgba-to-hed.png │ │ ├── diagnostic-slide-thumb-blue-pen-filter.png │ │ ├── diagnostic-slide-thumb-red-pen-filter.png │ │ ├── diagnostic-slide-thumb-green-pen-filter.png │ │ ├── diagnostic-slide-thumb-stretch-contrast.png │ │ ├── diagnostic-slide-thumb-hysteresis-threshold.png │ │ ├── diagnostic-slide-thumb-kmeans-segmentation.png │ │ ├── diagnostic-slide-thumb-adaptive-equalization.png │ │ └── diagnostic-slide-thumb-histogram-equalization.png │ ├── pil-images-rgb │ │ ├── tcga-lung-rgb-eosin-channel.png │ │ ├── diagnostic-slide-thumb-rgb-to-hed.png │ │ ├── diagnostic-slide-thumb-rgb-to-hsv.png │ │ ├── diagnostic-slide-thumb-rgb-to-lab.png │ │ ├── tcga-lung-rgb-hematoxylin-channel.png │ │ ├── diagnostic-slide-thumb-hsv-inverted.png │ │ ├── diagnostic-slide-thumb-hsv-rgb-to-hed.png │ │ ├── diagnostic-slide-thumb-hsv-rgb-to-lab.png │ │ ├── diagnostic-slide-thumb-rgb-inverted.png │ │ ├── diagnostic-slide-thumb-ycbcr-inverted.png │ │ ├── diagnostic-slide-thumb-ycbcr-rgb-to-hed.png │ │ ├── diagnostic-slide-thumb-ycbcr-rgb-to-hsv.png │ │ ├── diagnostic-slide-thumb-ycbcr-rgb-to-lab.png │ │ ├── diagnostic-slide-thumb-hsv-blue-pen-filter.png │ │ ├── diagnostic-slide-thumb-hsv-rag-threshold.png │ │ ├── diagnostic-slide-thumb-hsv-red-pen-filter.png │ │ ├── diagnostic-slide-thumb-rgb-blue-pen-filter.png │ │ ├── diagnostic-slide-thumb-rgb-rag-threshold.png │ │ ├── diagnostic-slide-thumb-rgb-red-pen-filter.png │ │ ├── diagnostic-slide-thumb-ycbcr-rag-threshold.png │ │ ├── diagnostic-slide-thumb-hsv-green-pen-filter.png │ │ ├── diagnostic-slide-thumb-hsv-stretch-contrast.png │ │ ├── diagnostic-slide-thumb-rgb-green-pen-filter.png │ │ ├── diagnostic-slide-thumb-rgb-stretch-contrast.png │ │ ├── diagnostic-slide-thumb-ycbcr-blue-pen-filter.png │ │ ├── diagnostic-slide-thumb-ycbcr-red-pen-filter.png │ │ ├── diagnostic-slide-thumb-hsv-hysteresis-threshold.png │ │ ├── diagnostic-slide-thumb-hsv-kmeans-segmentation.png │ │ ├── diagnostic-slide-thumb-rgb-hysteresis-threshold.png │ │ ├── diagnostic-slide-thumb-rgb-kmeans-segmentation.png │ │ ├── diagnostic-slide-thumb-ycbcr-green-pen-filter.png │ │ ├── diagnostic-slide-thumb-ycbcr-stretch-contrast.png │ │ ├── diagnostic-slide-thumb-hsv-adaptive-equalization.png │ │ ├── diagnostic-slide-thumb-hsv-histogram-equalization.png │ │ ├── diagnostic-slide-thumb-rgb-adaptive-equalization.png │ │ ├── diagnostic-slide-thumb-rgb-histogram-equalization.png │ │ ├── diagnostic-slide-thumb-ycbcr-hysteresis-threshold.png │ │ ├── diagnostic-slide-thumb-ycbcr-kmeans-segmentation.png │ │ ├── diagnostic-slide-thumb-ycbcr-adaptive-equalization.png │ │ └── diagnostic-slide-thumb-ycbcr-histogram-equalization.png │ ├── svs-images │ │ └── small-region-svs-resampled-array.npy │ ├── pil-images-gs │ │ ├── diagnostic-slide-thumb-gs-inverted.png │ │ ├── diagnostic-slide-thumb-gs1-local-otsu.png │ │ ├── diagnostic-slide-thumb-gs2-local-otsu.png │ │ ├── diagnostic-slide-thumb-gs3-local-otsu.png │ │ ├── diagnostic-slide-thumb-gs4-local-otsu.png │ │ ├── diagnostic-slide-thumb-gs-rag-threshold.png │ │ ├── diagnostic-slide-thumb-gs-stretch-contrast.png │ │ ├── diagnostic-slide-thumb-gs-kmeans-segmentation.png │ │ ├── diagnostic-slide-thumb-gs-local-equalization.png │ │ ├── diagnostic-slide-thumb-gs-adaptive-equalization.png │ │ ├── diagnostic-slide-thumb-gs-histogram-equalization.png │ │ └── diagnostic-slide-thumb-gs-hysteresis-threshold.png │ └── tiles-location-images │ │ ├── cmu-1-small-region-tiles-location-grid.png │ │ ├── cmu-1-small-region-tiles-location-random.png │ │ └── cmu-1-small-region-tiles-location-scored.png ├── fixtures │ ├── tiles │ │ ├── no-tissue.png │ │ ├── no-tissue2.png │ │ ├── almost-white-1.png │ │ ├── almost-white-2.png │ │ ├── no-tissue-line.png │ │ ├── no-tissue-red-pen.png │ │ ├── no-tissue-green-pen.png │ │ ├── high-nuclei-score-level0.png │ │ ├── high-nuclei-score-level2.png │ │ ├── low-nuclei-score-level0.png │ │ ├── low-nuclei-score-level1.png │ │ ├── medium-nuclei-score-level0.png │ │ ├── medium-nuclei-score-level1.png │ │ ├── medium-nuclei-score-level2.png │ │ ├── medium-nuclei-score-level1-2.png │ │ ├── very-low-nuclei-score-level0.png │ │ ├── high-nuclei-score-red-pen-level1.png │ │ ├── tissue-level0-4302-10273-4814-10785.png │ │ ├── tissue-level0-7352-11762-7864-12274.png │ │ ├── tissue-level2-1784-6289-5880-10386.png │ │ ├── tissue-level2-3000-7666-7096-11763.png │ │ ├── tissue-level2-4640-4649-8736-8746.png │ │ ├── tissue-level2-4760-5241-8856-9338.png │ │ ├── medium-nuclei-score-green-pen-level1.png │ │ └── very-low-nuclei-score-red-pen-level1.png │ ├── arrays │ │ ├── np-to-pil-l.npy │ │ ├── np-to-pil-la.npy │ │ ├── np-to-pil-rgba.npy │ │ ├── apply-mask-image-f1.npy │ │ ├── apply-mask-image-f2.npy │ │ ├── apply-mask-image-f3.npy │ │ └── apply-mask-image-f4.npy │ ├── mask-arrays │ │ ├── ytma1.npy │ │ ├── ytma2.npy │ │ ├── apply-mask-image-f1.npy │ │ ├── apply-mask-image-f2.npy │ │ ├── apply-mask-image-f3.npy │ │ ├── apply-mask-image-f4.npy │ │ ├── diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy │ │ └── diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy │ ├── pil-images-rgba │ │ ├── tcga-lung.png │ │ └── diagnostic-slide-thumb.png │ ├── pil-images-rgb │ │ ├── tcga-lung-rgb.png │ │ ├── diagnostic-slide-thumb-hsv.png │ │ ├── diagnostic-slide-thumb-rgb.png │ │ └── diagnostic-slide-thumb-ycbcr.png │ ├── svs-images │ │ ├── cmu-1-small-region.svs │ │ └── broken.svs │ ├── pil-images-gs │ │ └── diagnostic-slide-thumb-gs.png │ └── __init__.py ├── util.py └── unitutil.py ├── docs ├── changelog.rst ├── logo.png ├── favicon.png ├── requirements.txt ├── logo_histolab_white_t.png ├── api │ ├── data.rst │ ├── slide.rst │ ├── tile.rst │ ├── scorer.rst │ ├── filters │ │ ├── image_filters.rst │ │ ├── morphological_filters.rst │ │ ├── image_filters_functional.rst │ │ └── morphological_filters_functional.rst │ ├── utils.rst │ ├── tiler.rst │ └── filters.rst ├── Makefile ├── make.bat ├── readme.rst ├── conf.py ├── index.rst └── contributing.rst ├── examples ├── examples_reqs.txt ├── GTEx │ └── README.md └── TCGA │ └── README.md ├── .ci-ignore ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── help-question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── benchmarks.yml │ └── tests.yml ├── requirements-dev.txt ├── AUTHORS ├── requirements.txt ├── pyproject.toml ├── .pre-commit-config.yaml ├── MANIFEST.in ├── setup.cfg ├── readthedocs.yml ├── .coveragerc ├── Makefile ├── CHANGELOG.rst ├── .flake8 ├── .gitignore ├── setup.py └── CONTRIBUTING.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/histolab/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst -------------------------------------------------------------------------------- /tests/expectations/python-expr/np-to-pil-l.py: -------------------------------------------------------------------------------- 1 | [[255, 0], [255, 255]] 2 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/docs/logo.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/docs/favicon.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | histolab 2 | sphinx 3 | sphinx-rtd-theme 4 | sphinxcontrib-katex 5 | IPython -------------------------------------------------------------------------------- /examples/examples_reqs.txt: -------------------------------------------------------------------------------- 1 | tqdm>=4.51.0 2 | requests>=2.24.0 3 | pandas>=1.1.4 4 | scikit-learn>=0.23.2 -------------------------------------------------------------------------------- /tests/expectations/python-expr/np-to-pil-la.py: -------------------------------------------------------------------------------- 1 | [[[229, 249], [190, 172]], [[179, 193], [158, 179]]] 2 | -------------------------------------------------------------------------------- /.ci-ignore: -------------------------------------------------------------------------------- 1 | .github 2 | docs 3 | setup.py 4 | .pre-commit-config.yaml 5 | readthedocs.yml 6 | tests 7 | examples 8 | -------------------------------------------------------------------------------- /docs/logo_histolab_white_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/docs/logo_histolab_white_t.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/no-tissue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/no-tissue.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/no-tissue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/no-tissue2.png -------------------------------------------------------------------------------- /tests/fixtures/arrays/np-to-pil-l.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/np-to-pil-l.npy -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/ytma1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/ytma1.npy -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/ytma2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/ytma2.npy -------------------------------------------------------------------------------- /src/histolab/data/cmu_small_region.svs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/src/histolab/data/cmu_small_region.svs -------------------------------------------------------------------------------- /tests/fixtures/arrays/np-to-pil-la.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/np-to-pil-la.npy -------------------------------------------------------------------------------- /tests/fixtures/arrays/np-to-pil-rgba.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/np-to-pil-rgba.npy -------------------------------------------------------------------------------- /tests/fixtures/tiles/almost-white-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/almost-white-1.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/almost-white-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/almost-white-2.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/no-tissue-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/no-tissue-line.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/no-tissue-red-pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/no-tissue-red-pen.png -------------------------------------------------------------------------------- /tests/fixtures/arrays/apply-mask-image-f1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/apply-mask-image-f1.npy -------------------------------------------------------------------------------- /tests/fixtures/arrays/apply-mask-image-f2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/apply-mask-image-f2.npy -------------------------------------------------------------------------------- /tests/fixtures/arrays/apply-mask-image-f3.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/apply-mask-image-f3.npy -------------------------------------------------------------------------------- /tests/fixtures/arrays/apply-mask-image-f4.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/arrays/apply-mask-image-f4.npy -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgba/tcga-lung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgba/tcga-lung.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/no-tissue-green-pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/no-tissue-green-pen.png -------------------------------------------------------------------------------- /src/histolab/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # Versioning convention 4 | # https://www.python.org/dev/peps/pep-0440/ 5 | __version__ = "0.1.1" 6 | -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgb/tcga-lung-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgb/tcga-lung-rgb.png -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/apply-mask-image-f1.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/apply-mask-image-f1.npy -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/apply-mask-image-f2.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/apply-mask-image-f2.npy -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/apply-mask-image-f3.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/apply-mask-image-f3.npy -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/apply-mask-image-f4.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/apply-mask-image-f4.npy -------------------------------------------------------------------------------- /tests/fixtures/svs-images/cmu-1-small-region.svs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/svs-images/cmu-1-small-region.svs -------------------------------------------------------------------------------- /tests/fixtures/tiles/high-nuclei-score-level0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/high-nuclei-score-level0.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/high-nuclei-score-level2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/high-nuclei-score-level2.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/low-nuclei-score-level0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/low-nuclei-score-level0.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/low-nuclei-score-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/low-nuclei-score-level1.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/medium-nuclei-score-level0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/medium-nuclei-score-level0.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/medium-nuclei-score-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/medium-nuclei-score-level1.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/medium-nuclei-score-level2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/medium-nuclei-score-level2.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/medium-nuclei-score-level1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/medium-nuclei-score-level1-2.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/very-low-nuclei-score-level0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/very-low-nuclei-score-level0.png -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgba/diagnostic-slide-thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgba/diagnostic-slide-thumb.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/high-nuclei-score-red-pen-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/high-nuclei-score-red-pen-level1.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pre-commit 3 | pyflakes 4 | pycodestyle 5 | toml 6 | twine 7 | sphinx 8 | sphinx-rtd-theme 9 | ipdb 10 | isort 11 | sphinxcontrib-katex -------------------------------------------------------------------------------- /tests/fixtures/pil-images-gs/diagnostic-slide-thumb-gs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-gs/diagnostic-slide-thumb-gs.png -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-hsv.png -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-rgb.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level0-4302-10273-4814-10785.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level0-4302-10273-4814-10785.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level0-7352-11762-7864-12274.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level0-7352-11762-7864-12274.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level2-1784-6289-5880-10386.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level2-1784-6289-5880-10386.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level2-3000-7666-7096-11763.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level2-3000-7666-7096-11763.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level2-4640-4649-8736-8746.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level2-4640-4649-8736-8746.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/tissue-level2-4760-5241-8856-9338.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/tissue-level2-4760-5241-8856-9338.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | List of main authors 2 | 3 | Alessia Marcolini 4 | Nicole Bussola 5 | 6 | Ernesto Arbitrio 7 | -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/polygon-to-mask-array-0325.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/polygon-to-mask-array-0325.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/polygon-to-mask-array-1020.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/polygon-to-mask-array-1020.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/polygon-to-mask-array-2143.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/polygon-to-mask-array-2143.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/tcga-lung-eosin-channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/tcga-lung-eosin-channel.png -------------------------------------------------------------------------------- /tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-ycbcr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/pil-images-rgb/diagnostic-slide-thumb-ycbcr.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/medium-nuclei-score-green-pen-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/medium-nuclei-score-green-pen-level1.png -------------------------------------------------------------------------------- /tests/fixtures/tiles/very-low-nuclei-score-red-pen-level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/tiles/very-low-nuclei-score-red-pen-level1.png -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/tcga-lung-rgb-pen-marks-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/tcga-lung-rgb-pen-marks-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/tcga-lung-rgb-eosin-channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/tcga-lung-rgb-eosin-channel.png -------------------------------------------------------------------------------- /tests/expectations/svs-images/small-region-svs-resampled-array.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/svs-images/small-region-svs-resampled-array.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-grays-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-grays-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-inverted.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/tcga-lung-hematoxylin-channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/tcga-lung-hematoxylin-channel.png -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-grays-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-grays-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-grays-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-grays-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/ytma1-watershed-segmentation-region3.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/ytma1-watershed-segmentation-region3.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/ytma1-watershed-segmentation-region6.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/ytma1-watershed-segmentation-region6.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/ytma2-watershed-segmentation-region3.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/ytma2-watershed-segmentation-region3.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/ytma2-watershed-segmentation-region6.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/ytma2-watershed-segmentation-region6.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-inverted.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-hed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-hed.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-hsv.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-to-lab.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/tcga-lung-rgb-hematoxylin-channel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/tcga-lung-rgb-hematoxylin-channel.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | Pillow 3 | scikit-image 4 | scipy 5 | openslide-python 6 | typing_extensions 7 | pytest 8 | pytest-xdist 9 | coverage 10 | pytest-cov 11 | coveralls 12 | pytest-benchmark -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-grays-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-grays-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs1-local-otsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs1-local-otsu.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs2-local-otsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs2-local-otsu.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs3-local-otsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs3-local-otsu.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs4-local-otsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs4-local-otsu.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-inverted.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rgb-to-hed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rgb-to-hed.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rgb-to-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rgb-to-lab.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-inverted.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-inverted.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-rgba-to-hed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-rgba-to-hed.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help/Question 3 | about: Describe here your question 4 | title: '' 5 | labels: help wanted, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-canny-edges-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-canny-edges-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-pen-marks-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-pen-marks-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-otsu-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-otsu-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-red-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-red-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-pen-marks-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-pen-marks-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-yen-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-yen-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-rag-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-rag-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-hed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-hed.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-hsv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-hsv.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-lab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rgb-to-lab.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-blue-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-blue-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-red-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-red-pen-filter.png -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-blue-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-blue-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-green-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-green-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-filter-entropy-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-filter-entropy-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-otsu-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-otsu-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-yen-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs-yen-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-yen-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-yen-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-yen-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-yen-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-blue-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-blue-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-pen-marks-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-pen-marks-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-red-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-red-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-stretch-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-stretch-contrast.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-blue-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-blue-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rag-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-rag-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-red-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-red-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-blue-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-blue-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-rag-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-rag-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-red-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-red-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rag-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-rag-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-green-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-green-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-stretch-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-stretch-contrast.png -------------------------------------------------------------------------------- /tests/expectations/python-expr/apply-mask-image-exp4.py: -------------------------------------------------------------------------------- 1 | [ 2 | [243, 123, 83, 251, 90], 3 | [135, 162, 48, 11, 227], 4 | [31, 151, 68, 192, 70], 5 | [0, 0, 0, 0, 0], 6 | [0, 0, 0, 0, 0], 7 | ] 8 | -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-blue-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-blue-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-red-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-red-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-blue-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-blue-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-red-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-red-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-green-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-green-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-yen-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-yen-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-kmeans-segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-kmeans-segmentation.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-local-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-local-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-green-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-green-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-stretch-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-stretch-contrast.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-green-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-green-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-stretch-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-stretch-contrast.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-blue-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-blue-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-red-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-red-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-hysteresis-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-hysteresis-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-kmeans-segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-kmeans-segmentation.png -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-green-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-green-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-green-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb-green-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-green-ch-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba1-green-ch-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-green-ch-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba2-green-ch-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-green-ch-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba3-green-ch-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba4-green-ch-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgba4-green-ch-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-blue-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-blue-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-green-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-green-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-otsu-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-otsu-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-red-pen-filter-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-red-pen-filter-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-adaptive-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-adaptive-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-histogram-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-histogram-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-hysteresis-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-gs/diagnostic-slide-thumb-gs-hysteresis-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-hysteresis-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-hysteresis-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-kmeans-segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-kmeans-segmentation.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-hysteresis-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-hysteresis-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-kmeans-segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-kmeans-segmentation.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-green-pen-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-green-pen-filter.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-stretch-contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-stretch-contrast.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-adaptive-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-adaptive-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgba/diagnostic-slide-thumb-histogram-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgba/diagnostic-slide-thumb-histogram-equalization.png -------------------------------------------------------------------------------- /tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-grid.png -------------------------------------------------------------------------------- /tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-random.png -------------------------------------------------------------------------------- /tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-scored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/tiles-location-images/cmu-1-small-region-tiles-location-scored.png -------------------------------------------------------------------------------- /tests/fixtures/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/fixtures/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /docs/api/data.rst: -------------------------------------------------------------------------------- 1 | Data 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: API Reference 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.data 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /docs/api/slide.rst: -------------------------------------------------------------------------------- 1 | Slide 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: API Reference 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.slide 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /docs/api/tile.rst: -------------------------------------------------------------------------------- 1 | Tile 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: API Reference 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.tile 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs1-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs1-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs2-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs2-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs3-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs3-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-gs4-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-gs4-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-adaptive-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-adaptive-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-histogram-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-hsv-histogram-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-adaptive-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-adaptive-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-histogram-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-rgb-histogram-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-hysteresis-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-hysteresis-threshold.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-kmeans-segmentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-kmeans-segmentation.png -------------------------------------------------------------------------------- /docs/api/scorer.rst: -------------------------------------------------------------------------------- 1 | Scorer 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: API Reference 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.scorer 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb2-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb2-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-hysteresis-threshold-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-ycbcr-hysteresis-threshold-mask.npy -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-adaptive-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-adaptive-equalization.png -------------------------------------------------------------------------------- /tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-histogram-equalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/pil-images-rgb/diagnostic-slide-thumb-ycbcr-histogram-equalization.png -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-ones.py: -------------------------------------------------------------------------------- 1 | [ 2 | [True, True, True, True, True], 3 | [True, True, True, True, True], 4 | [True, True, True, True, True], 5 | [True, True, True, True, True], 6 | [True, True, True, True, True], 7 | ] 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | include = '\.pyi?$' 4 | exclude = ''' 5 | /( 6 | \.git 7 | | \.hg 8 | | \.mypy_cache 9 | | \.tox 10 | | \.venv 11 | | _build 12 | | buck-out 13 | | build 14 | | dist 15 | )/ 16 | ''' -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects-mask.npy -------------------------------------------------------------------------------- /docs/api/filters/image_filters.rst: -------------------------------------------------------------------------------- 1 | Image Filters 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: Filters 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.filters.image_filters 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects2-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects2-mask.npy -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.8 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.7.9 9 | hooks: 10 | - id: flake8 -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects-mask.npy -------------------------------------------------------------------------------- /tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects2-mask.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leriomaggio/histolab/master/tests/expectations/mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-objects2-mask.npy -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-zeros.py: -------------------------------------------------------------------------------- 1 | [ 2 | [False, False, False, False, False], 3 | [False, False, False, False, False], 4 | [False, False, False, False, False], 5 | [False, False, False, False, False], 6 | [False, False, False, False, False], 7 | ] 8 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-140.py: -------------------------------------------------------------------------------- 1 | [ 2 | [False, False, False, True, True], 3 | [True, True, True, True, True], 4 | [False, False, False, False, False], 5 | [True, False, False, True, True], 6 | [False, True, False, True, False], 7 | ] 8 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | .. toctree:: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: API Reference 9 | :hidden: 10 | 11 | .. automodule:: src.histolab.util 12 | :members: np_to_pil, threshold_to_mask, polygon_to_mask_array, apply_mask_image -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-160.py: -------------------------------------------------------------------------------- 1 | [ 2 | [False, False, True, False, False], 3 | [False, False, False, False, False], 4 | [True, True, False, False, False], 5 | [False, False, False, False, True], 6 | [False, True, False, True, False], 7 | ] 8 | -------------------------------------------------------------------------------- /docs/api/filters/morphological_filters.rst: -------------------------------------------------------------------------------- 1 | Morphological Filters 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: Filters 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.filters.morphological_filters 9 | :members: 10 | 11 | .. toctree:: -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src 2 | graft tests 3 | 4 | include README.md 5 | include LICENSE.txt 6 | 7 | include .coveragerc 8 | include .travis.yml 9 | include *.md 10 | include *.txt 11 | include *.yaml 12 | include *.yml 13 | include Makefile 14 | 15 | recursive-exclude * __pycache__ *.py[cod] -------------------------------------------------------------------------------- /docs/api/filters/image_filters_functional.rst: -------------------------------------------------------------------------------- 1 | Image Filters Functional 2 | ==================================== 3 | 4 | 5 | .. toctree:: 6 | :caption: Filters 7 | :maxdepth: 2 8 | 9 | .. automodule:: src.histolab.filters.image_filters_functional 10 | :members: 11 | 12 | .. toctree:: -------------------------------------------------------------------------------- /docs/api/tiler.rst: -------------------------------------------------------------------------------- 1 | Tiler 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: API Reference 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.tiler 9 | :members: 10 | :inherited-members: locate_tiles 11 | :exclude-members: Tiler 12 | 13 | .. toctree:: -------------------------------------------------------------------------------- /docs/api/filters/morphological_filters_functional.rst: -------------------------------------------------------------------------------- 1 | Morphological Filters Functional 2 | ==================================== 3 | 4 | .. toctree:: 5 | :caption: Filters 6 | :maxdepth: 2 7 | 8 | .. automodule:: src.histolab.filters.morphological_filters_functional 9 | :members: 10 | 11 | 12 | .. toctree:: -------------------------------------------------------------------------------- /docs/api/filters.rst: -------------------------------------------------------------------------------- 1 | Filters 2 | ==================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: API Reference 7 | 8 | filters/image_filters 9 | filters/image_filters_functional 10 | filters/morphological_filters 11 | filters/morphological_filters_functional 12 | 13 | .. toctree:: -------------------------------------------------------------------------------- /tests/expectations/python-expr/np-to-pil-rgba.py: -------------------------------------------------------------------------------- 1 | [ 2 | [[87, 130, 227, 200], [67, 234, 45, 68], [244, 207, 19, 21], [216, 213, 220, 16]], 3 | [[16, 245, 236, 78], [240, 202, 186, 24], [201, 78, 14, 243], [43, 78, 74, 83]], 4 | [[16, 35, 219, 6], [97, 130, 251, 151], [184, 20, 109, 216], [200, 5, 179, 155]], 5 | [[1, 39, 195, 204], [90, 179, 247, 35], [129, 95, 194, 116], [228, 120, 156, 170]], 6 | ] 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | looponfailroots = tests 3 | python_classes = Test Describe 4 | python_files = test_*.py 5 | python_functions = test_ it_ they_ but_ and_it_ or_it 6 | testpaths = 7 | tests 8 | 9 | [flake8] 10 | max-line-length = 88 11 | 12 | [settings] 13 | multi_line_output=3 14 | include_trailing_comma=True 15 | force_grid_wrap=0 16 | use_parentheses=True 17 | line_length=88 18 | not_skip = __init__.py -------------------------------------------------------------------------------- /tests/expectations/python-expr/apply-mask-image-exp1.py: -------------------------------------------------------------------------------- 1 | [ 2 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 3 | [[73, 64, 74], [36, 44, 161], [135, 219, 69], [79, 139, 75], [148, 40, 155]], 4 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 5 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 6 | [[84, 55, 130], [106, 123, 248], [117, 155, 214], [16, 121, 122], [154, 146, 26]], 7 | ] 8 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/apply-mask-image-exp2.py: -------------------------------------------------------------------------------- 1 | [ 2 | [[94, 96, 86], [239, 168, 4], [60, 37, 62], [221, 219, 175], [10, 248, 11]], 3 | [[82, 206, 188], [17, 90, 219], [2, 17, 134], [62, 28, 4], [209, 124, 224]], 4 | [[104, 89, 239], [140, 137, 95], [60, 6, 105], [199, 99, 19], [97, 92, 234]], 5 | [[164, 160, 13], [4, 75, 214], [221, 106, 122], [216, 46, 242], [239, 77, 137]], 6 | [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]], 7 | ] 8 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - pdf 15 | 16 | # Optionally set the version of Python and requirements required to build your docs 17 | python: 18 | version: 3.7 19 | install: 20 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/apply-mask-image-exp3.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [23, 153, 214, 226], 4 | [82, 172, 200, 90], 5 | [179, 178, 109, 250], 6 | [178, 23, 132, 171], 7 | [159, 104, 98, 179], 8 | ], 9 | [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 10 | [ 11 | [172, 206, 47, 28], 12 | [185, 136, 220, 130], 13 | [183, 196, 147, 210], 14 | [156, 142, 209, 193], 15 | [135, 0, 153, 39], 16 | ], 17 | [ 18 | [45, 157, 13, 187], 19 | [98, 234, 128, 112], 20 | [11, 19, 126, 139], 21 | [215, 185, 148, 125], 22 | [66, 240, 220, 101], 23 | ], 24 | [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], 25 | ] 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. import '...' 16 | 2. instantiate '....' 17 | 3. call '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Software (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Python Version [e.g. 3.6] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | omit = 5 | */site-packages/* 6 | */distutils/* 7 | */examples/* 8 | 9 | [report] 10 | show_missing = true 11 | precision = 2 12 | # Regexes for lines to exclude from consideration 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | pragma: no cover 16 | 17 | # Don't complain about missing debug-only code: 18 | def __repr__ 19 | if self\.debug 20 | 21 | # Don't complain if tests don't hit defensive assertion code: 22 | raise AssertionError 23 | raise NotImplementedError 24 | 25 | # Don't complain if non-runnable code isn't run: 26 | if 0: 27 | if __name__ == .__main__.: 28 | 29 | ignore_errors = True 30 | 31 | [html] 32 | directory = coverage_html_report 33 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-x-3-ones.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [True, True, True], 4 | [True, True, True], 5 | [True, True, True], 6 | [True, True, True], 7 | [True, True, True], 8 | ], 9 | [ 10 | [True, True, True], 11 | [True, True, True], 12 | [True, True, True], 13 | [True, True, True], 14 | [True, True, True], 15 | ], 16 | [ 17 | [True, True, True], 18 | [True, True, True], 19 | [True, True, True], 20 | [True, True, True], 21 | [True, True, True], 22 | ], 23 | [ 24 | [True, True, True], 25 | [True, True, True], 26 | [True, True, True], 27 | [True, True, True], 28 | [True, True, True], 29 | ], 30 | [ 31 | [True, True, True], 32 | [True, True, True], 33 | [True, True, True], 34 | [True, True, True], 35 | [True, True, True], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-39.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [True, True, True], 4 | [True, True, True], 5 | [True, True, True], 6 | [True, True, True], 7 | [False, True, False], 8 | ], 9 | [ 10 | [True, True, True], 11 | [True, True, True], 12 | [True, True, False], 13 | [True, True, False], 14 | [True, True, True], 15 | ], 16 | [ 17 | [True, True, True], 18 | [False, True, True], 19 | [True, True, True], 20 | [True, True, True], 21 | [True, True, False], 22 | ], 23 | [ 24 | [True, True, False], 25 | [True, True, True], 26 | [True, True, False], 27 | [False, True, True], 28 | [False, True, True], 29 | ], 30 | [ 31 | [True, True, True], 32 | [False, False, True], 33 | [True, False, False], 34 | [True, True, True], 35 | [True, True, True], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-200.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [False, False, False], 4 | [False, True, True], 5 | [True, True, True], 6 | [True, True, True], 7 | [True, True, True], 8 | ], 9 | [ 10 | [True, False, False], 11 | [True, False, True], 12 | [True, True, True], 13 | [True, False, False], 14 | [True, True, False], 15 | ], 16 | [ 17 | [True, True, True], 18 | [True, False, True], 19 | [True, True, False], 20 | [False, True, True], 21 | [True, True, True], 22 | ], 23 | [ 24 | [True, True, True], 25 | [True, True, True], 26 | [True, False, False], 27 | [True, True, True], 28 | [True, True, True], 29 | ], 30 | [ 31 | [False, True, True], 32 | [True, False, True], 33 | [True, True, True], 34 | [True, True, True], 35 | [True, True, True], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/fixtures/svs-images/broken.svs: -------------------------------------------------------------------------------- 1 | IJrEr1vVVJCwCYMtcEJwkpHJUZFY0weMVEREPS2aJqq/DfyooekB0JMyPMLVgZR5UctIkL+Csf5Ws+4UMiSgwSQ7FKSx/AMNoZyFScLmF7Qj0tKYToCWca6Dul4r2PUUxJ0ILf5z/YzyLeiYvOIAC/ifziTtDsgniuIgMUVQ3HHn6qU4p5M2cPtsweFdmn8vjvsq3hB3fvzjCG1MSri3HBh+O3Ie3O+CxeS8RXBrmo5gzh3U8kECvS6ns1RJWU04i8FBs4r/BR3AI481Cv9nDWBoM3N0r9sLOXQfYv9Wx/a2zEdLmL4iEtc+8Cg+VBl/atnTMK1B1GVmkzu6g55zASASuzhWlsRowOtKsKL5P9hmtQB5F4g94R2RGO93vqd4LF8GhErcjBWbyKVROqbpA7eYwniHP7hZVpabnoDTMpeg1zLVaElTKdGGuJijy+o4wlGzk7a+UDWD6AEEiXM/QKgeV88LOG3W5q+uRHPcYYaXi0jlD9Ap13CVxgl67uQ4N8qmF/n7wOHrZWs0G2WqD/YCcpAj7xEEvOuX3gN596Sx49/zVEQRFUBJoqH69vdfeesiYrMXLyOPi7O8QrLjWg3EUalEuOH60GpSVbLuuCevu5jn5ZJoYtLUhRTMgczvjC7S2hDCwBKxIdSO5n/rh9OCgm32Cd5Lr4dBi0kp7SECVuCKeO/zNjuSdRN5R1n0EWNvZstTuHgJveZqU5sOCSDAaBzIzmc41yDRsS7UGuBujpkEhuGkhkjYNG7jj8yCXh9d1iAm4Zkqmw5eaDs7vppdNU7+0fgmL4JRLeI+bBcFZA+wrJghFobTC4lAlphl9bq7W9Ljm3rFeFXR5+vQtOh8R4gXkX+v6kyPUxaASiLhsGzOMp6G8YlTF28eGwjtH5hLycxhi4b44O27rWGJmACpSxu+cL4knLJErxos3YIIPx8kFBGMSLwESSr9ZsUGn73tIU+C8UJenyaMlTCP8thw0h65l6Epnxs2fArE -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-x-3-zeros.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [False, False, False], 4 | [False, False, False], 5 | [False, False, False], 6 | [False, False, False], 7 | [False, False, False], 8 | ], 9 | [ 10 | [False, False, False], 11 | [False, False, False], 12 | [False, False, False], 13 | [False, False, False], 14 | [False, False, False], 15 | ], 16 | [ 17 | [False, False, False], 18 | [False, False, False], 19 | [False, False, False], 20 | [False, False, False], 21 | [False, False, False], 22 | ], 23 | [ 24 | [False, False, False], 25 | [False, False, False], 26 | [False, False, False], 27 | [False, False, False], 28 | [False, False, False], 29 | ], 30 | [ 31 | [False, False, False], 32 | [False, False, False], 33 | [False, False, False], 34 | [False, False, False], 35 | [False, False, False], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/unit/filters/test_util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from histolab.filters.util import mask_difference 5 | 6 | from ...base import ( 7 | BASE_MASK, 8 | BASE_MASK2, 9 | BASE_MASK3, 10 | BASE_MASK4, 11 | COMPLEX_MASK, 12 | COMPLEX_MASK2, 13 | COMPLEX_MASK3, 14 | ) 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "array1, array2, expected_mask_difference", 19 | ( 20 | ( 21 | BASE_MASK, 22 | BASE_MASK2, 23 | np.array([[False, True, False, False], [False, False, False, False]]), 24 | ), 25 | (BASE_MASK3, BASE_MASK4, np.array([True, False, False, False])), 26 | (COMPLEX_MASK, COMPLEX_MASK2, COMPLEX_MASK3), 27 | ), 28 | ) 29 | def test_mask_difference(array1, array2, expected_mask_difference): 30 | mask_difference_ = mask_difference(array1, array2) 31 | 32 | assert type(mask_difference_) == np.ndarray 33 | assert mask_difference_.dtype == "bool" 34 | np.testing.assert_equal(mask_difference_, expected_mask_difference) 35 | -------------------------------------------------------------------------------- /src/histolab/types.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | from collections import namedtuple 20 | 21 | CoordinatePair = namedtuple("CoordinatePair", ("x_ul", "y_ul", "x_br", "y_br")) 22 | Region = namedtuple("Region", ("index", "area", "bbox", "center")) 23 | 24 | CP = CoordinatePair 25 | -------------------------------------------------------------------------------- /tests/integration/test_tile.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from histolab.tile import Tile 4 | from histolab.types import CP 5 | 6 | from ..fixtures import TILES 7 | 8 | 9 | class Describe_Tile: 10 | @pytest.mark.parametrize( 11 | "tile_img, expected_result", 12 | ( 13 | (TILES.ALMOST_WHITE_1, True), 14 | (TILES.ALMOST_WHITE_2, True), 15 | (TILES.TISSUE_LEVEL0_4302_10273_4814_10785, False), 16 | (TILES.TISSUE_LEVEL0_7352_11762_7864_12274, False), 17 | (TILES.TISSUE_LEVEL2_1784_6289_5880_10386, False), 18 | (TILES.TISSUE_LEVEL2_3000_7666_7096_11763, False), 19 | (TILES.TISSUE_LEVEL2_4640_4649_8736_8746, False), 20 | (TILES.TISSUE_LEVEL2_4760_5241_8856_9338, False), 21 | ), 22 | ) 23 | def it_knows_if_is_is_almost_white(self, tile_img, expected_result): 24 | coords = CP(0, 512, 0, 512) 25 | tile = Tile(tile_img, coords) 26 | 27 | is_almost_white = tile._is_almost_white 28 | 29 | assert is_almost_white == expected_result 30 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-x-4-ones.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [True, True, True, True], 4 | [True, True, True, True], 5 | [True, True, True, True], 6 | [True, True, True, True], 7 | [True, True, True, True], 8 | ], 9 | [ 10 | [True, True, True, True], 11 | [True, True, True, True], 12 | [True, True, True, True], 13 | [True, True, True, True], 14 | [True, True, True, True], 15 | ], 16 | [ 17 | [True, True, True, True], 18 | [True, True, True, True], 19 | [True, True, True, True], 20 | [True, True, True, True], 21 | [True, True, True, True], 22 | ], 23 | [ 24 | [True, True, True, True], 25 | [True, True, True, True], 26 | [True, True, True, True], 27 | [True, True, True, True], 28 | [True, True, True, True], 29 | ], 30 | [ 31 | [True, True, True, True], 32 | [True, True, True, True], 33 | [True, True, True, True], 34 | [True, True, True, True], 35 | [True, True, True, True], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /src/histolab/filters/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def mask_percent(mask: np.ndarray) -> float: 5 | """Compute mask percentage of pixels different from zero. 6 | 7 | Parameters 8 | ---------- 9 | mask : np.ndarray 10 | Input mask as Numpy array 11 | 12 | Returns 13 | ------- 14 | float 15 | Percentage of image masked 16 | """ 17 | 18 | mask_percentage = 100 - np.count_nonzero(mask) / mask.size * 100 19 | return mask_percentage 20 | 21 | 22 | def mask_difference(minuend: np.ndarray, subtrahend: np.ndarray) -> np.ndarray: 23 | """Return the element-wise difference between two binary masks. 24 | 25 | Parameters 26 | ---------- 27 | minuend : np.ndarray 28 | The mask from which another is subtracted 29 | subtrahend : np.ndarray 30 | The mask that is to be subtracted 31 | 32 | Returns 33 | ------- 34 | np.ndarray 35 | The element-wise difference between the two binary masks 36 | """ 37 | difference = minuend.astype("int8") - subtrahend.astype("int8") 38 | difference[difference == -1] = 0 39 | return difference.astype("bool") 40 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-178.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [False, False, True, True], 4 | [False, True, False, False], 5 | [False, True, True, True], 6 | [False, False, True, False], 7 | [True, False, True, False], 8 | ], 9 | [ 10 | [True, False, False, False], 11 | [False, False, True, False], 12 | [True, False, False, False], 13 | [False, False, True, False], 14 | [False, True, False, False], 15 | ], 16 | [ 17 | [False, False, True, True], 18 | [False, False, False, False], 19 | [False, True, False, False], 20 | [False, False, True, True], 21 | [True, False, True, False], 22 | ], 23 | [ 24 | [True, False, False, True], 25 | [True, True, True, False], 26 | [False, True, True, True], 27 | [True, False, True, True], 28 | [False, False, False, False], 29 | ], 30 | [ 31 | [False, False, True, True], 32 | [False, True, False, False], 33 | [True, False, False, True], 34 | [True, False, False, False], 35 | [False, False, False, True], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/threshold-to-mask-37.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [False, False, False, False], 4 | [False, False, False, False], 5 | [False, False, False, False], 6 | [False, False, True, False], 7 | [False, False, False, False], 8 | ], 9 | [ 10 | [True, False, True, False], 11 | [True, False, False, False], 12 | [False, False, False, True], 13 | [False, False, False, False], 14 | [False, False, True, False], 15 | ], 16 | [ 17 | [False, False, False, False], 18 | [False, False, False, False], 19 | [True, False, False, False], 20 | [False, False, False, False], 21 | [False, False, True, True], 22 | ], 23 | [ 24 | [False, False, False, False], 25 | [False, False, False, False], 26 | [True, True, True, False], 27 | [False, False, False, False], 28 | [False, False, False, False], 29 | ], 30 | [ 31 | [True, False, False, False], 32 | [True, False, False, False], 33 | [False, False, False, False], 34 | [True, False, False, False], 35 | [False, False, False, False], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /tests/expectations/python-expr/5-x-5-x-4-zeros.py: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [False, False, False, False], 4 | [False, False, False, False], 5 | [False, False, False, False], 6 | [False, False, False, False], 7 | [False, False, False, False], 8 | ], 9 | [ 10 | [False, False, False, False], 11 | [False, False, False, False], 12 | [False, False, False, False], 13 | [False, False, False, False], 14 | [False, False, False, False], 15 | ], 16 | [ 17 | [False, False, False, False], 18 | [False, False, False, False], 19 | [False, False, False, False], 20 | [False, False, False, False], 21 | [False, False, False, False], 22 | ], 23 | [ 24 | [False, False, False, False], 25 | [False, False, False, False], 26 | [False, False, False, False], 27 | [False, False, False, False], 28 | [False, False, False, False], 29 | ], 30 | [ 31 | [False, False, False, False], 32 | [False, False, False, False], 33 | [False, False, False, False], 34 | [False, False, False, False], 35 | [False, False, False, False], 36 | ], 37 | ] 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKE = make 2 | PYTHON = python 3 | SETUP = $(PYTHON) ./setup.py 4 | 5 | .PHONY: clean cleandocs coverage dist docs opendocs unit-coverage upload help 6 | 7 | help: 8 | @echo "Usage: \`make ' where is one or more of" 9 | @echo " clean delete intermediate work product and start fresh" 10 | @echo " cleandocs delete cached HTML documentation and start fresh" 11 | @echo " coverage report overall test coverage" 12 | @echo " docs build HTML documentation using Sphinx (incremental)" 13 | @echo " opendocs open local HTML documentation in browser" 14 | @echo " dist generate source and wheel distribution into dist/" 15 | @echo " unit-coverage report unit test coverage" 16 | @echo " upload upload distribution to PyPI" 17 | 18 | clean: 19 | find . -type f -name \*.pyc -exec rm {} \; 20 | find . -type f -name .DS_Store -exec rm {} \; 21 | rm -rf dist .coverage 22 | 23 | cleandocs: 24 | $(MAKE) -C docs clean 25 | 26 | coverage: 27 | pytest --cov-report term-missing --cov=src --cov=tests 28 | 29 | dist: 30 | rm -rf dist/ 31 | $(SETUP) sdist bdist_wheel 32 | 33 | docs: 34 | $(MAKE) -C docs html 35 | 36 | opendocs: 37 | open docs/_build/html/index.html 38 | 39 | unit-coverage: 40 | pytest --cov-report term-missing --cov=src tests/unit 41 | 42 | upload: 43 | rm -rf dist/ 44 | $(SETUP) sdist bdist_wheel 45 | twine upload dist/* 46 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Python Benchamrks 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | benchmark: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v1 12 | - name: Run Benchmarks 13 | run: | 14 | sudo apt install -y openslide-tools 15 | pip install numpy pytest pytest-benchmark requests 16 | pip install -e . 17 | pytest tests/benchmarks/test_benchmarks.py --benchmark-json output.json 18 | # Download previous benchmark result from cache (if exists) 19 | - name: Download previous benchmark data 20 | uses: actions/cache@v1 21 | with: 22 | path: ./cache 23 | key: ${{ runner.os }}-benchmark 24 | - name: Store benchmark result 25 | uses: rhysd/github-action-benchmark@v1 26 | with: 27 | name: Python Benchmark with pytest-benchmark 28 | tool: 'pytest' 29 | output-file-path: output.json 30 | # Personal access token to deploy GitHub Pages branch 31 | github-token: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 32 | # Push and deploy GitHub pages branch automatically 33 | auto-push: true 34 | fail-on-alert: true 35 | comment-on-alert: true 36 | alert-comment-cc-users: '@ernestoarbitrio' 37 | alert-threshold: '200%' -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """Utilities for histolab tests.""" 4 | 5 | import os 6 | 7 | import numpy as np 8 | from PIL import Image 9 | 10 | 11 | def load_expectation(expectation_file_name, type_=None): # pragma: no cover 12 | """Returns np.ndarray related to the *expectation_file_name*. 13 | 14 | Expectation file path is rooted at tests/expectations. 15 | """ 16 | thisdir = os.path.dirname(__file__) 17 | expectation_file_path = os.path.abspath( 18 | os.path.join(thisdir, "expectations", f"{expectation_file_name}.{type_}") 19 | ) 20 | if type_ == "npy": 21 | expectation_data = np.load(expectation_file_path) 22 | elif type_ == "png": 23 | expectation_data = Image.open(expectation_file_path) 24 | else: 25 | raise Exception("Type format not recognized") 26 | return expectation_data 27 | 28 | 29 | def load_python_expression(expression_file_name): # pragma: no cover 30 | """Return a Python object (list, dict) formed by parsing `expression_file_name`. 31 | 32 | Expectation file path is rooted at tests/expectations. 33 | """ 34 | thisdir = os.path.dirname(__file__) 35 | expression_file_path = os.path.abspath( 36 | os.path.join(thisdir, "expectations", "%s.py" % expression_file_name) 37 | ) 38 | with open(expression_file_path) as f: 39 | expression_bytes = f.read() 40 | return eval(expression_bytes) 41 | -------------------------------------------------------------------------------- /tests/benchmarks/test_benchmarks.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import pytest 4 | 5 | import numpy as np 6 | from histolab.filters.util import mask_difference, mask_percent 7 | from histolab.util import np_to_pil, apply_mask_image 8 | 9 | from ..fixtures import NPY 10 | from ..unitutil import PILIMG, NpArrayMock 11 | 12 | 13 | class TestDescribeBenchmarksFilterUtil: 14 | def test_mask_difference(self, benchmark): 15 | minuend = np.random.choice(a=[False, True], size=(5000, 4000)) 16 | subtrahend = np.random.choice(a=[False, True], size=(5000, 4000)) 17 | benchmark.pedantic(mask_difference, args=[minuend, subtrahend], rounds=100) 18 | 19 | def test_mask_percent(self, benchmark): 20 | mask_array = np.random.choice(a=[False, True], size=(10000, 4000)) 21 | benchmark.pedantic(mask_percent, args=[mask_array], iterations=100, rounds=50) 22 | 23 | 24 | class TestDescribeBenchmarksUtil: 25 | def test_apply_mask_image(self, benchmark): 26 | image = PILIMG.RGBA_COLOR_500X500_155_249_240 27 | mask = NpArrayMock.ONES_500X500X4_BOOL 28 | benchmark.pedantic( 29 | apply_mask_image, args=[image, mask], iterations=100, rounds=50 30 | ) 31 | 32 | @pytest.mark.parametrize( 33 | "image", (NPY.NP_TO_PIL_RGBA, NPY.NP_TO_PIL_L, NPY.NP_TO_PIL_LA) 34 | ) 35 | def test_np_to_pil(self, image, benchmark): 36 | benchmark.pedantic(np_to_pil, args=[image], iterations=500, rounds=250) 37 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.1.1 5 | ------ 6 | 7 | - Add RgbToLab image filter (`#147 `_) 8 | - Add Watershed segmentation filter (`#153 `_) 9 | - Support Python 3.8 on Linux and macOS (`#151 `_) 10 | 11 | v0.0.1 12 | ------ 13 | 14 | - Add Lambda filter (`#124 `_) 15 | - Add ScoreTiler and RandomScorer (`#129 `_) 16 | - Add NucleiScorer (`#132 `_) 17 | - Add Ovarian Tissue sample in data module (`#136 `_) 18 | - Fix tiles's save path (`#126 `_) 19 | - Fix critical memory issue when extracting biggest tissue box (`#128 `_) 20 | 21 | v0.0.5b 22 | ------- 23 | 24 | - Fix `issue #100 `_ 25 | - Fix `issue #108 `_ 26 | - `Grid Tiler `_ added 27 | 28 | 29 | v0.0.4b 30 | ------- 31 | 32 | - Fix kmeans segmentation image filter default parameters 33 | - Fix rag threshold image filter default parameters 34 | - Fix check tissue on `Tile` to discard almost white tiles 35 | 36 | 37 | .. toctree:: 38 | -------------------------------------------------------------------------------- /src/histolab/exceptions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | 20 | class HistolabException(Exception): 21 | """Histolab custom exception main class""" 22 | 23 | def __init__(self, *args) -> None: 24 | if args: 25 | self.message = args[0] 26 | else: 27 | self.message = None 28 | super(HistolabException, self).__init__() 29 | 30 | def __str__(self): 31 | if self.message: 32 | return self.message 33 | return "" 34 | 35 | 36 | class LevelError(HistolabException): 37 | """Raised when a requested level is not available""" 38 | 39 | 40 | class FilterCompositionError(HistolabException): 41 | """Raised when a filter composition for the class is not available""" 42 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # Recommended flake8 settings while editing zoom, we use Black for the final linting/say in how code is formatted 2 | # 3 | # pip install flake8 flake8-bugbear 4 | # 5 | # This will warn/error on things that black does not fix, on purpose. 6 | 7 | [flake8] 8 | # max line length is set to 88 in black, here it is set to 80 and we enable bugbear's B950 warning, which is: 9 | # 10 | # B950: Line too long. This is a pragmatic equivalent of pycodestyle’s E501: it 11 | # considers “max-line-length” but only triggers when the value has been 12 | # exceeded by more than 10%. You will no longer be forced to reformat code due 13 | # to the closing parenthesis being one character too far to satisfy the linter. 14 | # At the same time, if you do significantly violate the line length, you will 15 | # receive a message that states what the actual limit is. This is inspired by 16 | # Raymond Hettinger’s “Beyond PEP 8” talk and highway patrol not stopping you 17 | # if you drive < 5mph too fast. Disable E501 to avoid duplicate warnings. 18 | 19 | max-line-length = 88 20 | max-complexity = 12 21 | select = E,F,W,C,B,B9 22 | ignore = 23 | # E123 closing bracket does not match indentation of opening bracket’s line 24 | E123 25 | # E203 whitespace before ‘:’ (Not PEP8 compliant, Python Black) 26 | E203 27 | # E501 line too long (82 > 79 characters) (replaced by B950 from flake8-bugbear, https://github.com/PyCQA/flake8-bugbear) 28 | E501 29 | # W503 line break before binary operator (Not PEP8 compliant, Python Black) 30 | W503 31 | # C901 function too complex - since many of zz9 functions are too complex with a lot of if branching 32 | C901 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | **/__pycache__ 3 | coverage_html_report 4 | .benchmarks/ 5 | 6 | # IDE + OS 7 | .DS_Store 8 | .idea 9 | .project 10 | .vscode 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | *.ipynb 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | cover/ 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | .pybuilder/ 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 81 | __pypackages__/ 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | Pipfile 92 | 93 | # mkdocs documentation 94 | /site 95 | 96 | # mypy 97 | .mypy_cache/ 98 | .dmypy.json 99 | dmypy.json 100 | 101 | # Pyre type checker 102 | .pyre/ 103 | 104 | # pytype static type analyzer 105 | .pytype/ 106 | 107 | # Cython debug symbols 108 | cython_debug/ 109 | -------------------------------------------------------------------------------- /tests/integration/test_slide.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import os 4 | 5 | import ntpath 6 | import numpy as np 7 | import PIL 8 | import pytest 9 | 10 | from histolab.slide import Slide 11 | 12 | from ..fixtures import SVS 13 | from ..util import load_expectation 14 | 15 | 16 | class Describe_Slide: 17 | def it_knows_its_name(self): 18 | slide = Slide( 19 | SVS.CMU_1_SMALL_REGION, os.path.join(SVS.CMU_1_SMALL_REGION, "processed") 20 | ) 21 | 22 | name = slide.name 23 | 24 | assert name == ntpath.basename(SVS.CMU_1_SMALL_REGION).split(".")[0] 25 | 26 | def it_calculate_resampled_nparray_from_small_region_svs_image(self): 27 | slide = Slide( 28 | SVS.CMU_1_SMALL_REGION, os.path.join(SVS.CMU_1_SMALL_REGION, "processed") 29 | ) 30 | 31 | resampled_array = slide.resampled_array(scale_factor=32) 32 | 33 | expected_value = load_expectation( 34 | "svs-images/small-region-svs-resampled-array", type_="npy" 35 | ) 36 | np.testing.assert_almost_equal(resampled_array, expected_value) 37 | 38 | def it_knows_the_right_slide_dimension(self): 39 | slide = Slide( 40 | SVS.CMU_1_SMALL_REGION, os.path.join(SVS.CMU_1_SMALL_REGION, "processed") 41 | ) 42 | image = PIL.Image.open(SVS.CMU_1_SMALL_REGION) 43 | 44 | dimensions = slide.dimensions 45 | 46 | assert image.size == dimensions 47 | assert slide.dimensions == (2220, 2967) 48 | assert image.size == (2220, 2967) 49 | 50 | def it_raises_openslideerror_with_broken_wsi(self): 51 | slide = Slide(SVS.BROKEN, os.path.join(SVS.BROKEN, "processed")) 52 | 53 | with pytest.raises(PIL.UnidentifiedImageError) as err: 54 | slide._wsi 55 | 56 | assert isinstance(err.value, PIL.UnidentifiedImageError) 57 | assert ( 58 | str(err.value) == "Your wsi has something broken inside, a doctor is needed" 59 | ) 60 | -------------------------------------------------------------------------------- /tests/unit/test_scorer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import PIL 3 | from histolab import scorer 4 | from histolab.tile import Tile 5 | 6 | from ..base import COMPLEX_MASK 7 | from ..unitutil import PILIMG, function_mock, instance_mock, method_mock, property_mock 8 | 9 | 10 | class DescribeScorers: 11 | def it_can_construct_randomscorer(self, request): 12 | tile = instance_mock(request, Tile) 13 | random_scorer = scorer.RandomScorer() 14 | 15 | score = random_scorer(tile) 16 | 17 | assert isinstance(random_scorer, scorer.RandomScorer) 18 | assert type(score) == float 19 | 20 | def it_can_construct_nuclei_scorer(self, request): 21 | image = PILIMG.RGB_RANDOM_COLOR_10X10 22 | tissue_ratio_ = property_mock(request, Tile, "tissue_ratio") 23 | tissue_ratio_.return_value = 0.7 24 | apply_filters_ = method_mock(request, Tile, "apply_filters") 25 | apply_filters_.return_value = Tile(PIL.Image.fromarray(COMPLEX_MASK), None, 0) 26 | mask_difference_ = function_mock(request, "histolab.scorer.mask_difference") 27 | mask_difference_.return_value = np.zeros_like(COMPLEX_MASK).astype("bool") 28 | tile = Tile(image, None, 0) 29 | nuclei_scorer = scorer.NucleiScorer() 30 | 31 | score = nuclei_scorer(tile) 32 | 33 | tissue_ratio_.assert_called_once() 34 | assert len(apply_filters_.call_args_list) == 2 35 | # not possible to test filters compositions instances used in the call 36 | assert apply_filters_.call_args_list[0][0][0] == tile 37 | assert apply_filters_.call_args_list[1][0][0] == tile 38 | np.testing.assert_array_equal( 39 | mask_difference_.call_args_list[0][0][0], COMPLEX_MASK 40 | ) 41 | np.testing.assert_array_equal( 42 | mask_difference_.call_args_list[0][0][1], COMPLEX_MASK 43 | ) 44 | assert isinstance(nuclei_scorer, scorer.NucleiScorer) 45 | assert type(score) == np.float64 46 | assert score == 0 # to avoid float representation issues 47 | -------------------------------------------------------------------------------- /tests/integration/test_scorer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from histolab import scorer 4 | from histolab.tile import Tile 5 | 6 | from ..fixtures import TILES 7 | 8 | 9 | class Describe_Scorers: 10 | @pytest.mark.parametrize( 11 | "tile_img, expected_score", 12 | ( 13 | # level 0 14 | (TILES.VERY_LOW_NUCLEI_SCORE_LEVEL0, 4.95387907194613e-05), 15 | (TILES.LOW_NUCLEI_SCORE_LEVEL0, 0.011112025501054716), 16 | (TILES.MEDIUM_NUCLEI_SCORE_LEVEL0, 0.018651677436394662), 17 | (TILES.HIGH_NUCLEI_SCORE_LEVEL0, 0.39901978131493154), 18 | # level 1 19 | ( 20 | TILES.VERY_LOW_NUCLEI_SCORE_RED_PEN_LEVEL1, 21 | 0.0017590279896743531, 22 | ), # breast - red pen 23 | (TILES.LOW_NUCLEI_SCORE_LEVEL1, 0.019689596845556157), # breast - green pen 24 | (TILES.MEDIUM_NUCLEI_SCORE_LEVEL1, 0.009512701556682022), # aorta 25 | ( 26 | TILES.MEDIUM_NUCLEI_SCORE_LEVEL1_2, 27 | 0.1519627864167197, 28 | ), # breast - green pen 29 | ( 30 | TILES.MEDIUM_NUCLEI_SCORE_GREEN_PEN_LEVEL1, 31 | 0.35696740368342295, 32 | ), # breast - green pen 33 | ( 34 | TILES.HIGH_NUCLEI_SCORE_RED_PEN_LEVEL1, 35 | 0.19867406253648537, 36 | ), # breast - red pen 37 | # level 2 38 | (TILES.MEDIUM_NUCLEI_SCORE_LEVEL2, 0.17959041877765877), # prostate 39 | (TILES.HIGH_NUCLEI_SCORE_LEVEL2, 0.02165773137397028), # prostate 40 | # no tissue 41 | (TILES.NO_TISSUE, 6.677516254309149e-07), 42 | (TILES.NO_TISSUE2, 7.505964521573081e-06), 43 | (TILES.NO_TISSUE_LINE, 0.00028575083246431935), 44 | (TILES.NO_TISSUE_RED_PEN, 0.2051245888881937), 45 | (TILES.NO_TISSUE_GREEN_PEN, 0.28993597176882124), 46 | ), 47 | ) 48 | def it_knows_nuclei_score(self, tile_img, expected_score): 49 | tile = Tile(tile_img, None) 50 | nuclei_scorer = scorer.NucleiScorer() 51 | expected_warning_regex = ( 52 | r"Input image must be RGB. NOTE: the image will be converted to RGB before" 53 | r" HED conversion." 54 | ) 55 | 56 | with pytest.warns(UserWarning, match=expected_warning_regex): 57 | score = nuclei_scorer(tile) 58 | 59 | assert round(score, 5) == round(expected_score, 5) 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import os 4 | import re 5 | 6 | import setuptools 7 | 8 | 9 | def ascii_bytes_from(path, *paths): 10 | """ 11 | Return the ASCII characters in the file specified by *path* and *paths*. 12 | The file path is determined by concatenating *path* and any members of 13 | *paths* with a directory separator in between. 14 | """ 15 | file_path = os.path.join(path, *paths) 16 | with open(file_path) as f: 17 | ascii_bytes = f.read() 18 | return ascii_bytes 19 | 20 | 21 | # read required text from files 22 | thisdir = os.path.dirname(__file__) 23 | init_py = ascii_bytes_from(thisdir, "src", "histolab", "__init__.py") 24 | readme = ascii_bytes_from(thisdir, "README.md") 25 | # This allows users to check installed version with: 26 | # `python -c 'from histolab import __version__; print(__version__)'` 27 | version = re.search('__version__ = "([^"]+)"', init_py).group(1) 28 | 29 | install_requires = [ 30 | "numpy", 31 | "Pillow", 32 | "scikit-image", 33 | "scipy", 34 | "openslide-python", 35 | "typing_extensions", 36 | ] 37 | 38 | test_requires = [ 39 | "pytest", 40 | "pytest-xdist", 41 | "coverage", 42 | "pytest-cov", 43 | "coveralls", 44 | "pytest-benchmark", 45 | ] 46 | 47 | setuptools.setup( 48 | name="histolab", 49 | version=version, 50 | maintainer="Histolab Developers", 51 | maintainer_email="ernesto.arbitrio@gmail.com", 52 | author="E. Arbitrio, N. Bussola, A. Marcolini", 53 | description="Python library for Digital Pathology Image Processing", 54 | long_description=readme, 55 | long_description_content_type="text/markdown", 56 | url="https://github.com/histolab/histolab", 57 | download_url="https://pypi.python.org/pypi/histolab", 58 | install_requires=install_requires, 59 | tests_require=test_requires, 60 | extras_require={"testing": test_requires}, 61 | packages=setuptools.find_packages("src", exclude=["tests", "examples"]), 62 | classifiers=[ 63 | "Programming Language :: Python :: 3", 64 | "Programming Language :: Python :: 3.6", 65 | "Programming Language :: Python :: 3.7", 66 | "Programming Language :: Python :: 3.8", 67 | "License :: OSI Approved :: Apache Software License", 68 | "Operating System :: OS Independent", 69 | "Intended Audience :: Science/Research", 70 | ], 71 | package_dir={"": "src"}, 72 | include_package_data=True, 73 | entry_points={}, 74 | test_suite="pytest", 75 | zip_safe=True, 76 | python_requires=">=3.6", 77 | ) 78 | -------------------------------------------------------------------------------- /tests/integration/test_tiler.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import os 4 | 5 | import numpy as np 6 | 7 | from histolab.slide import Slide 8 | from histolab.tiler import GridTiler, RandomTiler, ScoreTiler 9 | from histolab.scorer import NucleiScorer 10 | 11 | from ..fixtures import SVS 12 | from ..util import load_expectation 13 | 14 | 15 | class DescribeRandomTiler: 16 | def it_locates_tiles_on_the_slide(self, tmpdir): 17 | slide = Slide(SVS.CMU_1_SMALL_REGION, os.path.join(tmpdir, "processed")) 18 | slide.save_scaled_image(10) 19 | random_tiles_extractor = RandomTiler( 20 | tile_size=(512, 512), n_tiles=2, level=0, seed=42, check_tissue=False 21 | ) 22 | expectation = load_expectation( 23 | "tiles-location-images/cmu-1-small-region-tiles-location-random", 24 | type_="png", 25 | ) 26 | tiles_location_img = random_tiles_extractor.locate_tiles(slide, scale_factor=10) 27 | 28 | np.testing.assert_array_almost_equal( 29 | np.asarray(tiles_location_img), expectation 30 | ) 31 | 32 | 33 | class DescribeGridTiler: 34 | def it_locates_tiles_on_the_slide(self, tmpdir): 35 | slide = Slide(SVS.CMU_1_SMALL_REGION, os.path.join(tmpdir, "processed")) 36 | grid_tiles_extractor = GridTiler( 37 | tile_size=(512, 512), 38 | level=0, 39 | check_tissue=False, 40 | ) 41 | expectation = load_expectation( 42 | "tiles-location-images/cmu-1-small-region-tiles-location-grid", type_="png" 43 | ) 44 | tiles_location_img = grid_tiles_extractor.locate_tiles(slide, scale_factor=10) 45 | 46 | np.testing.assert_array_almost_equal( 47 | np.asarray(tiles_location_img), expectation 48 | ) 49 | 50 | 51 | class DescribeScoreTiler: 52 | def it_locates_tiles_on_the_slide(self, tmpdir): 53 | slide = Slide(SVS.CMU_1_SMALL_REGION, os.path.join(tmpdir, "processed")) 54 | scored_tiles_extractor = ScoreTiler( 55 | scorer=NucleiScorer(), 56 | tile_size=(512, 512), 57 | n_tiles=100, 58 | level=0, 59 | check_tissue=False, 60 | ) 61 | expectation = load_expectation( 62 | "tiles-location-images/cmu-1-small-region-tiles-location-scored", 63 | type_="png", 64 | ) 65 | scored_location_img = scored_tiles_extractor.locate_tiles( 66 | slide, scale_factor=10 67 | ) 68 | 69 | np.testing.assert_array_almost_equal( 70 | np.asarray(scored_location_img), expectation 71 | ) 72 | -------------------------------------------------------------------------------- /tests/unit/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from histolab import exceptions as exp 4 | 5 | from ..unitutil import ANY, initializer_mock 6 | 7 | 8 | class DescribeExceptions: 9 | @pytest.mark.parametrize("arg", (["hello", "error"], None)) 10 | def it_can_construct_from_str_or_none_level_error(self, request, arg): 11 | _init = initializer_mock(request, exp.LevelError) 12 | 13 | level_error = exp.LevelError(arg) 14 | 15 | _init.assert_called_once_with(ANY, arg) 16 | assert isinstance(level_error, exp.LevelError) 17 | assert isinstance(level_error, Exception) 18 | 19 | def it_can_construct_from_list_level_error(self, request): 20 | _init = initializer_mock(request, exp.LevelError) 21 | args = ["hello", "error"] 22 | 23 | level_error = exp.LevelError(*args) 24 | 25 | _init.assert_called_once_with(ANY, *args) 26 | assert isinstance(level_error, exp.LevelError) 27 | assert isinstance(level_error, Exception) 28 | 29 | def it_knows_its_message_from_str_level_error(self): 30 | arg = "error" 31 | level_error = exp.LevelError(arg) 32 | 33 | message = level_error.message 34 | 35 | assert isinstance(message, str) 36 | assert message == arg 37 | 38 | def it_knows_its_message_from_list_level_error(self): 39 | args = ["hello", "error"] 40 | level_error = exp.LevelError(*args) 41 | 42 | message = level_error.message 43 | 44 | assert isinstance(message, str) 45 | assert message == list(args)[0] 46 | 47 | def it_knows_its_message_from_none_level_error(self): 48 | arg = None 49 | level_error = exp.LevelError(arg) 50 | 51 | message = level_error.message 52 | 53 | assert message is None 54 | 55 | def it_knows_its_message_from_empty_level_error(self): 56 | level_error = exp.LevelError() 57 | 58 | message = level_error.message 59 | 60 | assert message is None 61 | 62 | def it_knows_its_str_from_str_level_error(self): 63 | arg = "error" 64 | level_error = exp.LevelError(arg) 65 | 66 | s = str(level_error) 67 | 68 | assert s == level_error.message 69 | 70 | def it_knows_its_str_from_list_level_error(self): 71 | args = ["hello", "error"] 72 | level_error = exp.LevelError(*args) 73 | 74 | s = str(level_error) 75 | 76 | assert s == level_error.message 77 | 78 | def it_knows_its_str_from_none_level_error(self): 79 | arg = None 80 | level_error = exp.LevelError(arg) 81 | 82 | s = str(level_error) 83 | 84 | assert s == "" 85 | -------------------------------------------------------------------------------- /tests/integration/test_morphological_filters.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import histolab.filters.morphological_filters_functional as mof 5 | 6 | from ..fixtures import MASKNPY 7 | from ..util import load_expectation 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "mask_array, min_size, avoid_overmask, overmask_thresh, expected_array", 12 | ( 13 | ( 14 | MASKNPY.DIAGNOSTIC_SLIDE_THUMB_RGB1_HYSTERESIS_THRESHOLD_MASK, 15 | 3000, 16 | True, 17 | 95, 18 | "mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-" 19 | "objects-mask", 20 | ), 21 | ( 22 | MASKNPY.DIAGNOSTIC_SLIDE_THUMB_HSV_OTSU_THRESHOLD_MASK, 23 | 3000, 24 | True, 25 | 95, 26 | "mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects" 27 | "-mask", 28 | ), 29 | ( 30 | MASKNPY.DIAGNOSTIC_SLIDE_THUMB_HSV_OTSU_THRESHOLD_MASK, 31 | 3000, 32 | False, 33 | 95, 34 | "mask-arrays/diagnostic-slide-thumb-hsv-otsu-threshold-remove-small-objects" 35 | "2-mask", 36 | ), 37 | ( 38 | MASKNPY.DIAGNOSTIC_SLIDE_THUMB_RGB1_HYSTERESIS_THRESHOLD_MASK, 39 | 1200, 40 | True, 41 | 25, 42 | "mask-arrays/diagnostic-slide-thumb-rgb1-hysteresis-threshold-remove-small-" 43 | "objects2-mask", 44 | ), 45 | ), 46 | ) 47 | def test_remove_small_objects_filter( 48 | mask_array, min_size, avoid_overmask, overmask_thresh, expected_array 49 | ): 50 | expected_value = load_expectation(expected_array, type_="npy") 51 | mask_no_small_object = mof.remove_small_objects( 52 | mask_array, min_size, avoid_overmask, overmask_thresh 53 | ) 54 | 55 | np.testing.assert_array_equal(mask_no_small_object, expected_value) 56 | assert type(mask_no_small_object) == np.ndarray 57 | 58 | 59 | @pytest.mark.parametrize( 60 | "mask_array, region_shape, expected_array", 61 | ( 62 | (MASKNPY.YTMA1, 6, "mask-arrays/ytma1-watershed-segmentation-region6"), 63 | (MASKNPY.YTMA2, 6, "mask-arrays/ytma2-watershed-segmentation-region6"), 64 | (MASKNPY.YTMA1, 3, "mask-arrays/ytma1-watershed-segmentation-region3"), 65 | (MASKNPY.YTMA2, 3, "mask-arrays/ytma2-watershed-segmentation-region3"), 66 | ), 67 | ) 68 | def test_watershed_segmentation_filter(mask_array, region_shape, expected_array): 69 | expected_value = load_expectation(expected_array, type_="npy") 70 | 71 | mask_watershed = mof.watershed_segmentation(mask_array, region_shape) 72 | 73 | np.testing.assert_array_equal(mask_watershed, expected_value) 74 | assert type(mask_watershed) == np.ndarray 75 | -------------------------------------------------------------------------------- /src/histolab/data/_registry.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | # in legacy datasets we need to put our sample data within the data dir 4 | legacy_datasets = ["cmu_small_region.svs"] 5 | 6 | # Registry of datafiles that can be downloaded along with their SHA256 hashes 7 | # To generate the SHA256 hash, use the command 8 | # openssl sha256 filename 9 | registry = { 10 | "histolab/broken.svs": "b1325916876afa17ad5e02d2e7298ee883e758ed25369470d85bc0990e928e11", 11 | "histolab/kidney.png": "5c6dc1b9ae10a2865302d9c8eda360362ec47732cb3e9766c38ed90cb9f4c371", 12 | "data/cmu_small_region.svs": "ed92d5a9f2e86df67640d6f92ce3e231419ce127131697fbbce42ad5e002c8a7", 13 | "aperio/JP2K-33003-1.svs": "6205ccf75a8fa6c32df7c5c04b7377398971a490fb6b320d50d91f7ba6a0e6fd", 14 | "aperio/JP2K-33003-2.svs": "1a13cef86b55b51127cebd94a1f6069f7de494c98e3e708640d1ce7181d9e3fd", 15 | "tcga/breast/9c960533-2e58-4e54-97b2-8454dfb4b8c8": "03f542afa2d70224d594b2cca33b99977a5c0e41b1a8d03471ab3cf62ea3c4b3", 16 | "tcga/breast/da36d3aa-9b19-492a-af4f-cc028a926d96": "2172cca68a8b7722d281174a74c4f112d0f52fc71710d7d605f401731c783fc9", 17 | "tcga/breast/f8b4cee6-9149-45b4-ae53-82b0547e1e34": "55c694262c4d44b342e08eb3ef2082eeb9e9deeb3cb445e4776419bb9fa7dc21", 18 | "tcga/breast/31e248bf-ee24-4d18-bccb-47046fccb461": "95163831d9076bb5e5b21790933dee9535a3607ba35bd6ae425374a45ecb1ba6", 19 | "tcga/prostate/6b725022-f1d5-4672-8c6c-de8140345210": "305c80e28227b25fdd0cc24726da4cf038380b4326e25c6518ffe23051a25ac0", 20 | "tcga/ovarian/b777ec99-2811-4aa4-9568-13f68e380c86": "f8e5059a0c9f8c026cfb2613cddef6562f8cdbd5954580282e2afa41d2f86a8c", 21 | } 22 | 23 | APERIO_REPO_URL = "http://openslide.cs.cmu.edu/download/openslide-testdata/Aperio" 24 | TCGA_REPO_URL = "https://api.gdc.cancer.gov/data" 25 | 26 | registry_urls = { 27 | "histolab/broken.svs": "https://raw.githubusercontent.com/histolab/histolab/master/tests/fixtures/svs-images/broken.svs", 28 | "histolab/kidney.png": "https://user-images.githubusercontent.com/4196091/100275351-132cc880-2f60-11eb-8cc8-7a3bf3723260.png", 29 | "aperio/JP2K-33003-1.svs": f"{APERIO_REPO_URL}/JP2K-33003-1.svs", 30 | "aperio/JP2K-33003-2.svs": f"{APERIO_REPO_URL}/JP2K-33003-2.svs", 31 | "tcga/breast/9c960533-2e58-4e54-97b2-8454dfb4b8c8": f"{TCGA_REPO_URL}/9c960533-2e58-4e54-97b2-8454dfb4b8c8", 32 | "tcga/breast/da36d3aa-9b19-492a-af4f-cc028a926d96": f"{TCGA_REPO_URL}/da36d3aa-9b19-492a-af4f-cc028a926d96", 33 | "tcga/breast/f8b4cee6-9149-45b4-ae53-82b0547e1e34": f"{TCGA_REPO_URL}/f8b4cee6-9149-45b4-ae53-82b0547e1e34", 34 | "tcga/breast/31e248bf-ee24-4d18-bccb-47046fccb461": f"{TCGA_REPO_URL}/31e248bf-ee24-4d18-bccb-47046fccb461", 35 | "tcga/prostate/6b725022-f1d5-4672-8c6c-de8140345210": f"{TCGA_REPO_URL}/6b725022-f1d5-4672-8c6c-de8140345210", 36 | "tcga/ovarian/b777ec99-2811-4aa4-9568-13f68e380c86": f"{TCGA_REPO_URL}/b777ec99-2811-4aa4-9568-13f68e380c86", 37 | } 38 | 39 | legacy_registry = { 40 | ("data/" + filename): registry["data/" + filename] for filename in legacy_datasets 41 | } 42 | -------------------------------------------------------------------------------- /src/histolab/filters/compositions.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | import numpy as np 20 | 21 | from ..exceptions import FilterCompositionError 22 | from ..util import lazyproperty 23 | from . import image_filters as imf 24 | from . import morphological_filters as mof 25 | 26 | 27 | class FiltersComposition: 28 | """Provide appropriate filters compositions based on the ``cls_`` parameter. 29 | 30 | Arguments 31 | --------- 32 | cls_ : type, {Tile, Slide} 33 | The class to get the appropriate filters composition for 34 | """ 35 | 36 | def __new__(cls: type, cls_): 37 | if not cls_: 38 | raise FilterCompositionError("cls_ parameter cannot be None") 39 | FiltersSubCls = { 40 | "Tile": _TileFiltersComposition, 41 | "Slide": _SlideFiltersComposition, 42 | }.get(cls_.__name__) 43 | if FiltersSubCls: 44 | instance = super(FiltersComposition, FiltersSubCls).__new__(FiltersSubCls) 45 | return instance 46 | raise FilterCompositionError( 47 | f"Filters composition for the class {cls_.__name__} is not available" 48 | ) 49 | 50 | 51 | class _SlideFiltersComposition(FiltersComposition): 52 | @lazyproperty 53 | def tissue_mask_filters(self) -> imf.Compose: 54 | """Filters composition for slide's tissue estimation. 55 | 56 | Returns 57 | ------- 58 | imf.Compose 59 | Filters composition 60 | """ 61 | return imf.Compose( 62 | [ 63 | imf.RgbToGrayscale(), 64 | imf.OtsuThreshold(), 65 | mof.BinaryDilation(), 66 | mof.RemoveSmallHoles(), 67 | mof.RemoveSmallObjects(), 68 | ] 69 | ) 70 | 71 | 72 | class _TileFiltersComposition(FiltersComposition): 73 | @lazyproperty 74 | def tissue_mask_filters(self) -> imf.Compose: 75 | """Filters composition for tile's tissue estimation. 76 | 77 | Returns 78 | ------- 79 | imf.Compose 80 | Filters composition 81 | """ 82 | return imf.Compose( 83 | [ 84 | imf.RgbToGrayscale(), 85 | imf.OtsuThreshold(), 86 | mof.BinaryDilation(), 87 | mof.BinaryFillHoles(structure=np.ones((5, 5))), 88 | ] 89 | ) 90 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """SVS source files loader for testing purposes.""" 4 | 5 | import os 6 | 7 | import numpy as np 8 | from PIL import Image 9 | 10 | 11 | class LazyResponder: 12 | """Loads and caches fixtures files by name from fixture directory. 13 | Provides access to all the svs fixtures in a directory by 14 | a standardized mapping of the file name, e.g. ca-1-c.svs is available 15 | as the `.CA_1_C` attribute of the loader. 16 | 17 | The fixture directory is specified relative to this (fixture root) 18 | directory. 19 | """ 20 | 21 | def __init__(self, relpath): 22 | self._relpath = relpath 23 | self._cache = {} 24 | 25 | def __getattr__(self, fixture_name): 26 | if fixture_name not in self._cache: 27 | self._load_to_cache(fixture_name) 28 | return self._cache[fixture_name] 29 | 30 | @property 31 | def _dirpath(self): 32 | thisdir = os.path.dirname(os.path.abspath(__file__)) 33 | return os.path.abspath(os.path.join(thisdir, self._relpath)) 34 | 35 | 36 | class LazySVSResponseLoader(LazyResponder): 37 | """Specific class for SVS fixtures loader""" 38 | 39 | def _svs_path(self, fixture_name): 40 | return "%s/%s.svs" % (self._dirpath, fixture_name.replace("_", "-").lower()) 41 | 42 | def _load_svs(self, path): 43 | with open(path, "rb") as f: 44 | svs_image = f.name 45 | return svs_image 46 | 47 | def _load_to_cache(self, fixture_name): 48 | svs_path = self._svs_path(fixture_name) 49 | if not os.path.exists(svs_path): 50 | raise ValueError("no SVS fixture found at %s" % svs_path) 51 | self._cache[fixture_name] = self._load_svs(svs_path) 52 | 53 | 54 | class LazyPILResponseLoader(LazyResponder): 55 | """Specific class for PIL fixtures loader""" 56 | 57 | def _pil_path(self, fixture_name): 58 | return "%s/%s.png" % (self._dirpath, fixture_name.replace("_", "-").lower()) 59 | 60 | def _load_pil(self, path): 61 | return Image.open(path) 62 | 63 | def _load_to_cache(self, fixture_name): 64 | pil_path = self._pil_path(fixture_name) 65 | if not os.path.exists(pil_path): 66 | raise ValueError("no PIL fixture found at %s" % pil_path) 67 | self._cache[fixture_name] = self._load_pil(pil_path) 68 | 69 | 70 | class LazyNPYResponseLoader(LazyResponder): 71 | """Specific class for PIL fixtures loader""" 72 | 73 | def _npy_path(self, fixture_name): 74 | return "%s/%s.npy" % (self._dirpath, fixture_name.replace("_", "-").lower()) 75 | 76 | def _load_npy(self, path): 77 | return np.load(path) 78 | 79 | def _load_to_cache(self, fixture_name): 80 | npy_path = self._npy_path(fixture_name) 81 | if not os.path.exists(npy_path): 82 | raise ValueError("no NPY fixture found at %s" % npy_path) 83 | self._cache[fixture_name] = self._load_npy(npy_path) 84 | 85 | 86 | SVS = LazySVSResponseLoader("./svs-images") 87 | RGBA = LazyPILResponseLoader("./pil-images-rgba") 88 | RGB = LazyPILResponseLoader("./pil-images-rgb") 89 | GS = LazyPILResponseLoader("./pil-images-gs") 90 | MASKNPY = LazyNPYResponseLoader("./mask-arrays") 91 | NPY = LazyNPYResponseLoader("./arrays") 92 | TILES = LazyPILResponseLoader("./tiles") 93 | -------------------------------------------------------------------------------- /src/histolab/scorer.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | import operator 20 | from abc import abstractmethod 21 | 22 | import numpy as np 23 | 24 | from .filters import image_filters as imf 25 | from .filters import morphological_filters as mof 26 | from .filters.util import mask_difference 27 | from .tile import Tile 28 | 29 | try: 30 | from typing import Protocol, runtime_checkable 31 | except ImportError: 32 | from typing_extensions import Protocol, runtime_checkable 33 | 34 | 35 | @runtime_checkable 36 | class Scorer(Protocol): 37 | """General scorer object 38 | 39 | .. automethod:: __call__ 40 | """ 41 | 42 | @abstractmethod 43 | def __call__(self, tile: Tile) -> float: 44 | raise NotImplementedError 45 | 46 | 47 | class RandomScorer(Scorer): 48 | """Implement a Scorer that returns a random float score between 0 and 1. 49 | 50 | .. automethod:: __call__ 51 | """ 52 | 53 | def __call__(self, tile: Tile) -> float: 54 | """Return the random score associated with the tile. 55 | 56 | Parameters 57 | ---------- 58 | tile : Tile 59 | The tile to calculate the score from. 60 | 61 | Returns 62 | ------- 63 | float 64 | Random score ranging between 0 and 1. 65 | """ 66 | return np.random.random() 67 | 68 | 69 | class NucleiScorer(Scorer): 70 | r"""Implement a Scorer that estimates the presence of nuclei in an H&E-stained tile. 71 | 72 | A higher presence of nuclei is associated with a higher scorer, following this 73 | formula: 74 | 75 | .. math:: 76 | 77 | score = nuclei\_ratio \cdot tanh(tissue\_ratio) 78 | 79 | .. automethod:: __call__ 80 | """ 81 | 82 | def __call__(self, tile: Tile) -> float: 83 | """Return the nuclei score associated with the tile. 84 | 85 | Parameters 86 | ---------- 87 | tile : Tile 88 | The tile to calculate the score from. 89 | 90 | Returns 91 | ------- 92 | float 93 | Nuclei score 94 | """ 95 | 96 | filters_raw_nuclei = imf.Compose( 97 | [imf.HematoxylinChannel(), imf.YenThreshold(operator.gt)] 98 | ) 99 | filters_nuclei_cleaner = imf.Compose( 100 | [imf.HematoxylinChannel(), imf.YenThreshold(operator.gt), mof.WhiteTopHat()] 101 | ) 102 | 103 | mask_raw_nuclei = np.array(tile.apply_filters(filters_raw_nuclei).image) 104 | mask_nuclei_clean = np.array(tile.apply_filters(filters_nuclei_cleaner).image) 105 | 106 | mask_nuclei = mask_difference(mask_raw_nuclei, mask_nuclei_clean) 107 | nuclei_ratio = np.count_nonzero(mask_nuclei) / mask_nuclei.size 108 | 109 | return nuclei_ratio * np.tanh(tile.tissue_ratio) 110 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ==================================== 3 | 4 | .. image:: https://user-images.githubusercontent.com/4196091/84828232-048fcc00-b026-11ea-8caa-5c14bb8565bd.png 5 | :alt: Logo 6 | :align: center 7 | :width: 40em 8 | 9 | 10 | The aim of this project is to provide a tool for WSI processing in a reproducible environment to support clinical and 11 | scientific research. histolab is designed to handle WSIs, automatically detect the tissue, and retrieve informative 12 | tiles, and it can thus be integrated in a deep learning pipeline. 13 | 14 | Motivation 15 | ********** 16 | The histo-pathological analysis of tissue sections is the gold standard to assess the presence of many complex diseases, 17 | such as tumors, and understand their nature. 18 | 19 | In daily practice, pathologists usually perform microscopy examination of tissue slides considering a limited number of 20 | regions and the clinical evaulation relies on several factors such as nuclei morphology, cell distribution, and color 21 | (staining): this process is time consuming, could lead to information loss, and suffers from inter-observer variability. 22 | 23 | The advent of digital pathology is changing the way patholgists work and collaborate, and has opened the way to a new 24 | era in computational pathology. In particular, histopathology is expected to be at the center of the AI revolution in 25 | medicine [1], prevision supported by the increasing success of deep learning applications to digital pathology. 26 | 27 | Whole Slide Images (WSIs), namely the translation of tissue slides from glass to digital format, are a great source of 28 | information from both a medical and a computational point of view. WSIs can be coloured with different staining 29 | techniques (e.g. H&E or IHC), and are usually very large in size (up to several GB per slide). Because of WSIs typical 30 | pyramidal structure, images can be retrieved at different magnification factors, providing a further layer of 31 | information beyond color. 32 | 33 | However, processing WSIs is far from being trivial. First of all, WSIs can be stored in different proprietary formats, 34 | according to the scanner used to digitalize the slides, and a standard protocol is still missing. WSIs can also present 35 | artifacts, such as shadows, mold, or annotations (pen marks) that are not useful. Moreover, giving their dimensions, 36 | it is not possible to process a WSI all at once, or, for example, to feed a neural network: it is necessary to crop 37 | smaller regions of tissues (tiles), which in turns require a tissue detection step. 38 | 39 | Installation 40 | ************ 41 | 42 | The histolab package can be installed by using:: 43 | 44 | pip install histolab 45 | 46 | 47 | Prerequisites 48 | ************* 49 | histolab has only one system-wide dependency: `OpenSlide`. 50 | 51 | You can download and install it from https://openslide.org/download/ according to your operating system. 52 | 53 | 54 | Authors 55 | ******* 56 | * `Alessia Marcolini `_ 57 | * `Ernesto Arbitrio `_ 58 | * `Nicole Bussola `_ 59 | 60 | License 61 | ******* 62 | This project is licensed under `Apache License Version 2.0` - see the `LICENSE.txt `_ file for details. 63 | 64 | Acknowledgements 65 | **************** 66 | 67 | * https://github.com/deroneriksson 68 | 69 | References 70 | ********** 71 | [1] Colling, Richard, et al. "Artificial intelligence in digital pathology: A roadmap to routine use in clinical practice." The Journal of pathology 249.2 (2019) 72 | 73 | 74 | .. toctree:: 75 | 76 | 77 | .. toctree:: 78 | :caption: API Reference 79 | :maxdepth: 2 80 | :hidden: 81 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | 11 | jobs: 12 | 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [windows-latest, macOS-latest, ubuntu-latest] 18 | python-version: [3.6, 3.7] 19 | include: 20 | - os: macOS-latest 21 | python-version: 3.8 22 | - os: ubuntu-latest 23 | python-version: 3.8 24 | env: 25 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install openslide 33 | id: system-dependencies 34 | run: | 35 | if [ "$RUNNER_OS" == "Linux" ]; then 36 | sudo apt install openslide-tools 37 | elif [ "$RUNNER_OS" == "macOS" ]; then 38 | brew install openslide 39 | elif [ "$RUNNER_OS" == "Windows" ]; then 40 | choco install wget 41 | choco install 7zip-zstd 42 | wget https://github.com/openslide/openslide-winbuild/releases/download/v20171122/openslide-win64-20171122.zip -P /c/downloads 43 | 7z e /c/downloads/openslide-win64-20171122.zip -aoa 44 | export "PATH=C:/downloads/openslide-win64-20171122/bin:$PATH" 45 | else 46 | echo "$RUNNER_OS not supported" 47 | exit 1 48 | fi 49 | shell: bash 50 | - name: Get pip cache dir 51 | id: pip-cache 52 | run: | 53 | echo "::set-output name=dir::$(pip cache dir)" 54 | - name: pip cache 55 | uses: actions/cache@v2 56 | with: 57 | path: ${{ steps.pip-cache.outputs.dir }} 58 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 59 | restore-keys: | 60 | ${{ runner.os }}-pip- 61 | - name: Install dependencies 62 | id: python-dependencies 63 | run: | 64 | python -m pip install --upgrade pip 65 | python -m pip install -e .[testing] 66 | python -m pip install flake8 67 | python -m pip install pooch 68 | - name: Lint with flake8 69 | id: flake8 70 | run: | 71 | flake8 . --count --max-complexity=10 --max-line-length=88 --statistics 72 | - name: Test with pytest 73 | id: tests 74 | run: | 75 | python -m pytest -n auto --ignore=tests/benchmarks 76 | - name: Slack Notification 77 | uses: act10ns/slack@v1 78 | with: 79 | status: ${{ job.status }} 80 | steps: ${{ toJson(steps) }} 81 | if: failure() 82 | 83 | coveralls: 84 | needs: build 85 | runs-on: ubuntu-latest 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 89 | steps: 90 | - uses: actions/checkout@v2 91 | - name: Set up Python 3.7 92 | uses: actions/setup-python@v2 93 | with: 94 | python-version: 3.7 95 | - name: Install openslide 96 | run: sudo apt install openslide-tools 97 | - name: Install dependencies 98 | run: | 99 | python -m pip install --upgrade pip 100 | python -m pip install -e .[testing] 101 | python -m pip install flake8 102 | python -m pip install pooch 103 | - name: Test with pytest 104 | run: python -m pytest --ignore=tests/benchmarks --cov=histolab 105 | - name: Coveralls 106 | run: coveralls 107 | -------------------------------------------------------------------------------- /src/histolab/filters/morphological_filters_functional.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | import numpy as np 20 | import scipy.ndimage as sc_ndimage 21 | import skimage.feature as sk_feature 22 | import skimage.morphology as sk_morphology 23 | import skimage.segmentation as sk_segmentation 24 | 25 | from .util import mask_percent 26 | 27 | 28 | def remove_small_objects( 29 | np_mask: np.ndarray, 30 | min_size: int = 3000, 31 | avoid_overmask: bool = True, 32 | overmask_thresh: int = 95, 33 | ) -> np.ndarray: 34 | """Remove connected components which size is less than min_size. 35 | 36 | is True, this function can recursively call itself with progressively 37 | to avoid removing too many objects in the mask. 38 | 39 | Parameters 40 | ---------- 41 | np_img : np.ndarray (arbitrary shape, int or bool type) 42 | Input mask 43 | min_size : int, optional 44 | Minimum size of small object to remove. Default is 3000 45 | avoid_overmask : bool, optional (default is True) 46 | If True, avoid masking above the overmask_thresh percentage. 47 | overmask_thresh : int, optional (default is 95) 48 | If avoid_overmask is True, avoid masking above this threshold percentage value. 49 | 50 | Returns 51 | ------- 52 | np.ndarray 53 | Mask with small objects filtered out 54 | """ 55 | mask_no_small_object = sk_morphology.remove_small_objects(np_mask, min_size) 56 | if ( 57 | avoid_overmask 58 | and mask_percent(mask_no_small_object) >= overmask_thresh 59 | and min_size >= 1 60 | ): 61 | new_min_size = min_size // 2 62 | mask_no_small_object = remove_small_objects( 63 | np_mask, new_min_size, avoid_overmask, overmask_thresh 64 | ) 65 | return mask_no_small_object 66 | 67 | 68 | def watershed_segmentation(np_mask: np.ndarray, region_shape: int = 6) -> np.ndarray: 69 | """Segment and label an binary mask with Watershed segmentation [1]_ 70 | 71 | The watershed algorithm treats pixels values as a local topography (elevation). 72 | 73 | Parameters 74 | ---------- 75 | np_mask : np.ndarray 76 | Input mask 77 | region_shape : int, optional 78 | The local region within which to search for image peaks is defined as a squared 79 | area region_shape x region_shape. Default is 6. 80 | 81 | Returns 82 | ------- 83 | np.ndarray 84 | Labelled segmentation mask 85 | 86 | References 87 | -------- 88 | .. [1] Watershed segmentation. 89 | https://scikit-image.org/docs/dev/auto_examples/segmentation/plot_watershed.html 90 | """ 91 | distance = sc_ndimage.distance_transform_edt(np_mask) 92 | local_maxi = sk_feature.peak_local_max( 93 | distance, 94 | indices=False, 95 | footprint=np.ones((region_shape, region_shape)), 96 | labels=np_mask, 97 | ) 98 | markers = sc_ndimage.label(local_maxi)[0] 99 | labels = sk_segmentation.watershed(-distance, markers, mask=np_mask) 100 | return labels 101 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | import datetime 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import re 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath("..")) 20 | 21 | 22 | def ascii_bytes_from(path, *paths): 23 | """ 24 | Return the ASCII characters in the file specified by *path* and *paths*. 25 | The file path is determined by concatenating *path* and any members of 26 | *paths* with a directory separator in between. 27 | """ 28 | file_path = os.path.join(path, *paths) 29 | with open(file_path) as f: 30 | ascii_bytes = f.read() 31 | return ascii_bytes 32 | 33 | 34 | # read required text from files 35 | thisdir = os.path.dirname(__file__) 36 | init_py = ascii_bytes_from(thisdir, "..", "src", "histolab", "__init__.py") 37 | version = re.search('__version__ = "([^"]+)"', init_py).group(1) 38 | 39 | 40 | # -- Project information ----------------------------------------------------- 41 | 42 | project = "histolab" 43 | copyright = "2020, histolab" 44 | author = "histolab" 45 | 46 | # The full version, including alpha/beta/rc tags 47 | release = version 48 | 49 | 50 | # -- General configuration --------------------------------------------------- 51 | 52 | # Add any Sphinx extension module names here, as strings. They can be 53 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 54 | # ones. 55 | extensions = [ 56 | "sphinx.ext.autodoc", 57 | "sphinx.ext.coverage", 58 | "sphinx.ext.napoleon", 59 | "sphinx.ext.viewcode", 60 | "sphinx_rtd_theme", 61 | "sphinxcontrib.katex", 62 | ] 63 | 64 | # Add any paths that contain templates here, relative to this directory. 65 | templates_path = ["_templates"] 66 | 67 | # List of patterns, relative to source directory, that match files and 68 | # directories to ignore when looking for source files. 69 | # This pattern also affects html_static_path and html_extra_path. 70 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | html_theme = "sphinx_rtd_theme" 79 | html_logo = "logo_histolab_white_t.png" 80 | html_favicon = "favicon.png" 81 | html_theme_options = { 82 | "display_version": True, 83 | "prev_next_buttons_location": "bottom", 84 | "style_external_links": False, 85 | # Toc options 86 | "collapse_navigation": True, 87 | "sticky_navigation": True, 88 | "navigation_depth": 5, 89 | "includehidden": True, 90 | "titles_only": False, 91 | } 92 | html_context = { 93 | "display_github": True, 94 | "github_user": "histolab", 95 | "github_repo": "histolab", 96 | "github_version": "master/docs/", 97 | "author": "HistoLab Authors", 98 | "date": datetime.date.today().strftime("%d/%m/%y"), 99 | } 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ["_static"] 105 | add_module_names = False 106 | 107 | autodoc_mock_imports = ["openslide-python", "openslide"] 108 | master_doc = "index" 109 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://user-images.githubusercontent.com/4196091/84828232-048fcc00-b026-11ea-8caa-5c14bb8565bd.png 3 | :alt: Logo 4 | :align: center 5 | :width: 40em 6 | 7 | 8 | The aim of this project is to provide a tool for WSI processing in a reproducible environment to support clinical and 9 | scientific research. histolab is designed to handle WSIs, automatically detect the tissue, and retrieve informative 10 | tiles, and it can thus be integrated in a deep learning pipeline. 11 | 12 | Motivation 13 | ********** 14 | The histo-pathological analysis of tissue sections is the gold standard to assess the presence of many complex diseases, 15 | such as tumors, and understand their nature. 16 | 17 | In daily practice, pathologists usually perform microscopy examination of tissue slides considering a limited number of 18 | regions and the clinical evaulation relies on several factors such as nuclei morphology, cell distribution, and color 19 | (staining): this process is time consuming, could lead to information loss, and suffers from inter-observer variability. 20 | 21 | The advent of digital pathology is changing the way patholgists work and collaborate, and has opened the way to a new 22 | era in computational pathology. In particular, histopathology is expected to be at the center of the AI revolution in 23 | medicine [1], prevision supported by the increasing success of deep learning applications to digital pathology. 24 | 25 | Whole Slide Images (WSIs), namely the translation of tissue slides from glass to digital format, are a great source of 26 | information from both a medical and a computational point of view. WSIs can be coloured with different staining 27 | techniques (e.g. H&E or IHC), and are usually very large in size (up to several GB per slide). Because of WSIs typical 28 | pyramidal structure, images can be retrieved at different magnification factors, providing a further layer of 29 | information beyond color. 30 | 31 | However, processing WSIs is far from being trivial. First of all, WSIs can be stored in different proprietary formats, 32 | according to the scanner used to digitalize the slides, and a standard protocol is still missing. WSIs can also present 33 | artifacts, such as shadows, mold, or annotations (pen marks) that are not useful. Moreover, giving their dimensions, 34 | it is not possible to process a WSI all at once, or, for example, to feed a neural network: it is necessary to crop 35 | smaller regions of tissues (tiles), which in turns require a tissue detection step. 36 | 37 | Installation 38 | ************ 39 | 40 | The histolab package can be installed by using:: 41 | 42 | pip install histolab 43 | 44 | 45 | Prerequisites 46 | ************* 47 | histolab has only one system-wide dependency: `OpenSlide`. 48 | 49 | You can download and install it from https://openslide.org/download/ according to your operating system. 50 | 51 | Authors 52 | ******* 53 | * `Alessia Marcolini `_ 54 | * `Ernesto Arbitrio `_ 55 | * `Nicole Bussola `_ 56 | 57 | License 58 | ******* 59 | This project is licensed under `Apache License Version 2.0` - see the `LICENSE.txt `_ file for details. 60 | 61 | Acknowledgements 62 | **************** 63 | 64 | * https://github.com/deroneriksson 65 | 66 | References 67 | ********** 68 | [1] Colling, Richard, et al. "Artificial intelligence in digital pathology: A roadmap to routine use in clinical practice." The Journal of pathology 249.2 (2019) 69 | 70 | 71 | .. toctree:: 72 | :hidden: 73 | 74 | readme 75 | quickstart 76 | contributing 77 | changelog 78 | 79 | .. toctree:: 80 | :caption: API Reference 81 | :maxdepth: 2 82 | :hidden: 83 | 84 | api/slide 85 | api/filters 86 | api/tile 87 | api/tiler 88 | api/scorer 89 | api/data 90 | api/utils 91 | 92 | 93 | Indices and tables 94 | ================== 95 | 96 | * :ref:`genindex` 97 | * :ref:`modindex` 98 | * :ref:`search` 99 | -------------------------------------------------------------------------------- /examples/GTEx/README.md: -------------------------------------------------------------------------------- 1 | # Create a leakage-free dataset of tiles for GTEx 2 | 3 | `extract_tile_pw_gtex.py` is a Python script proposed as reference to retrieve a reproducible dataset of tiles using a collection of WSIs from the [GTEx](https://gtexportal.org/home/) public repository. In particular, it can be easily integrated in deep learning pipeline(s) for computational pathology. 4 | 5 | ## Prerequisites 6 | To run this script you will need the following packages, other than `histolab`: 7 | 8 | - `pandas` 9 | - `requests` 10 | - `tqdm` 11 | - `scikit-learn` 12 | 13 | Extra requirements are available in the `examples_reqs.txt` file located in the `examples` folder. 14 | All dependencies can be installed via command line, using `pip`: 15 | 16 | ```shell 17 | pip install -f examples_reqs.txt 18 | ``` 19 | 20 | Moreover, a CSV file of patient metadata (`metadata_csv`) is required; this file can be retrieved from the GTEx [data portal](https://gtexportal.org/home/histologyPage) and it is structured as follows: 21 | 22 | | Tissue Sample ID | Tissue | Subject ID | Sex | Age Bracket | Hardy Scale | Pathology Categories | Pathology Notes | 23 | |---------------------|-----------------------------------|---------------|-----------|----------------|----------------|-------------------------|-----------------------------------------------------------------| 24 | | GTEX-1117F-0126 | Skin - Sun Exposed (Lower leg) | GTEX-1117F | female | 60-69 | Slow death | | 6 pieces, minimal fat, squamous epithelium is ~50-70 microns | 25 | | GTEX-1117F-0226 | Adipose - Subcutaneous | GTEX-1117F | female | 60-69 | Slow death | | 2 pieces, ~15% vessel stroma, rep delineated | 26 | | ... | ... | ... | ... | ... | ... | ... | ... | 27 | 28 | ## Workflow 29 | 30 | The `extract_tile_pw_gtex.py` will perform the following steps: 31 | 32 | 1. the WSIs listed in the metadata file (`Tissue Sample ID` column) are downloaded from GTEx via the `download_wsi_gtex` function; slides are saved in the `wsi_dataset_dir` directory, which is specified as command-line argument. 33 | 2. a fixed number of tiles (100 by default) are randomly extracted from each WSI by the `extract_random_tiles` function. The directory where to store the tiles, along with several parameters that detail the extraction protocol (i.e. `n_tiles`, `seed`, `check_tissue`), can be defined as command-line arguments. 34 | 35 | **Note** `histolab` automatically saves the generated tiles in the 'tiles' subdirectory. 36 | 37 | 3. the `split_tiles_patient_wise` function sorts the tiles into the training and the test set (80-20 partition by default) adopting a *Patient-Wise* splitting protocol, namely ensuring that tiles belonging to the same subject are either in the training or the test set. 38 | 39 | ## Usage 40 | 41 | ``` 42 | usage: extract_tile_pw_gtex.py [-h] [--metadata_csv METADATA_CSV] 43 | [--wsi_dataset_dir WSI_DATASET_DIR] 44 | [--tile_dataset_dir TILE_DATASET_DIR] 45 | [--tile_size TILE_SIZE TILE_SIZE] 46 | [--n_tiles N_TILES] [--level LEVEL] 47 | [--seed SEED] [--check_tissue CHECK_TISSUE] 48 | 49 | Retrieve a leakage-free dataset of tiles using a collection of WSI. 50 | 51 | optional arguments: 52 | -h, --help show this help message and exit 53 | --metadata_csv METADATA_CSV 54 | CSV with WSI metadata. Default examples/GTEx/GTEx_AIDP2021.csv. 55 | --wsi_dataset_dir WSI_DATASET_DIR 56 | Path where to save the WSIs. Default WSI_GTEx. 57 | --tile_dataset_dir TILE_DATASET_DIR 58 | Path where to save the WSIs. Default tiles_GTEx. 59 | --tile_size TILE_SIZE TILE_SIZE 60 | width and height of the cropped tiles. Default (512, 512). 61 | --n_tiles N_TILES Maximum number of tiles to extract. Default 100. 62 | --level LEVEL Magnification level from which extract the tiles. Default 2. 63 | --seed SEED Seed for RandomState. Default 7. 64 | --check_tissue CHECK_TISSUE 65 | Whether to check if the tile has enough tissue to be saved. Default True. 66 | ``` -------------------------------------------------------------------------------- /tests/unit/test_compositions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | from histolab.exceptions import FilterCompositionError 5 | from histolab.filters.compositions import ( 6 | FiltersComposition, 7 | _SlideFiltersComposition, 8 | _TileFiltersComposition, 9 | ) 10 | from histolab.filters.image_filters import Compose 11 | from histolab.slide import Slide 12 | from histolab.tile import Tile 13 | 14 | from ..unitutil import class_mock, initializer_mock 15 | 16 | 17 | def it_knows_tissue_areas_mask_tile_filters_composition( 18 | RgbToGrayscale_, OtsuThreshold_, BinaryDilation_, BinaryFillHoles_ 19 | ): 20 | _enough_tissue_mask_filters_ = FiltersComposition(Tile).tissue_mask_filters 21 | 22 | RgbToGrayscale_.assert_called_once() 23 | OtsuThreshold_.assert_called_once() 24 | BinaryDilation_.assert_called_once() 25 | BinaryFillHoles_.assert_called_once() 26 | np.testing.assert_almost_equal( 27 | BinaryFillHoles_.call_args_list[0][1]["structure"], np.ones((5, 5)) 28 | ) 29 | assert _enough_tissue_mask_filters_.filters == [ 30 | RgbToGrayscale_(), 31 | OtsuThreshold_(), 32 | BinaryDilation_(), 33 | BinaryFillHoles_(), 34 | ] 35 | assert type(_enough_tissue_mask_filters_) == Compose 36 | 37 | 38 | def it_knows_tissue_areas_mask_slide_filters_composition( 39 | RgbToGrayscale_, 40 | OtsuThreshold_, 41 | BinaryDilation_, 42 | RemoveSmallHoles_, 43 | RemoveSmallObjects_, 44 | ): 45 | _enough_tissue_mask_filters_ = FiltersComposition(Slide).tissue_mask_filters 46 | 47 | RgbToGrayscale_.assert_called_once() 48 | OtsuThreshold_.assert_called_once() 49 | BinaryDilation_.assert_called_once() 50 | RemoveSmallHoles_.assert_called_once() 51 | RemoveSmallObjects_.assert_called_once() 52 | 53 | assert _enough_tissue_mask_filters_.filters == [ 54 | RgbToGrayscale_(), 55 | OtsuThreshold_(), 56 | BinaryDilation_(), 57 | RemoveSmallHoles_(), 58 | RemoveSmallObjects_(), 59 | ] 60 | 61 | assert type(_enough_tissue_mask_filters_) == Compose 62 | 63 | 64 | @pytest.mark.parametrize( 65 | "_cls, subclass", 66 | ((Tile, _TileFiltersComposition), (Slide, _SlideFiltersComposition)), 67 | ) 68 | def it_can_dispatch_subclass_according_class_type(request, _cls, subclass): 69 | _init_ = initializer_mock(request, FiltersComposition) 70 | 71 | filters_composition = FiltersComposition(_cls) 72 | 73 | _init_.assert_called_once_with(_cls) 74 | assert isinstance(filters_composition, subclass) 75 | 76 | 77 | def it_raises_filtercompositionerror_if_class_not_allowed(request): 78 | _init_ = initializer_mock(request, FiltersComposition) 79 | cls_ = Compose 80 | 81 | with pytest.raises(FilterCompositionError) as err: 82 | FiltersComposition(cls_) 83 | 84 | _init_.assert_not_called() 85 | assert isinstance(err.value, FilterCompositionError) 86 | assert ( 87 | str(err.value) == "Filters composition for the class Compose is not available" 88 | ) 89 | 90 | 91 | def it_raises_filtercompositionerror_if_class_is_none(request): 92 | _init_ = initializer_mock(request, FiltersComposition) 93 | cls_ = None 94 | 95 | with pytest.raises(FilterCompositionError) as err: 96 | FiltersComposition(cls_) 97 | 98 | _init_.assert_not_called() 99 | assert isinstance(err.value, FilterCompositionError) 100 | assert str(err.value) == "cls_ parameter cannot be None" 101 | 102 | 103 | # fixture components --------------------------------------------- 104 | 105 | 106 | @pytest.fixture 107 | def RgbToGrayscale_(request): 108 | return class_mock(request, "histolab.filters.image_filters.RgbToGrayscale") 109 | 110 | 111 | @pytest.fixture 112 | def OtsuThreshold_(request): 113 | return class_mock(request, "histolab.filters.image_filters.OtsuThreshold") 114 | 115 | 116 | @pytest.fixture 117 | def BinaryDilation_(request): 118 | return class_mock(request, "histolab.filters.morphological_filters.BinaryDilation") 119 | 120 | 121 | @pytest.fixture 122 | def BinaryFillHoles_(request): 123 | return class_mock(request, "histolab.filters.morphological_filters.BinaryFillHoles") 124 | 125 | 126 | @pytest.fixture 127 | def RemoveSmallHoles_(request): 128 | return class_mock( 129 | request, "histolab.filters.morphological_filters.RemoveSmallHoles" 130 | ) 131 | 132 | 133 | @pytest.fixture 134 | def RemoveSmallObjects_(request): 135 | return class_mock( 136 | request, "histolab.filters.morphological_filters.RemoveSmallObjects" 137 | ) 138 | -------------------------------------------------------------------------------- /tests/unitutil.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """Functions that make mocking with pytest easier and more readable.""" 4 | 5 | import os 6 | import sys 7 | 8 | import pytest 9 | import numpy as np 10 | from PIL import Image 11 | 12 | from unittest.mock import ANY, call # noqa # isort:skip 13 | from unittest.mock import create_autospec, patch, PropertyMock # isort:skip 14 | 15 | from histolab import data 16 | 17 | 18 | def dict_list_eq(l1, l2): 19 | sorted_l1 = sorted(sorted(d.items()) for d in l1) 20 | sorted_l2 = sorted(sorted(d.items()) for d in l2) 21 | return sorted_l1 == sorted_l2 22 | 23 | 24 | def class_mock(request, q_class_name, autospec=True, **kwargs): 25 | """Return mock patching class with qualified name *q_class_name*. 26 | 27 | The mock is autospec'ed based on the patched class unless the optional 28 | argument *autospec* is set to False. Any other keyword arguments are 29 | passed through to Mock(). Patch is reversed after calling test returns. 30 | """ 31 | _patch = patch(q_class_name, autospec=autospec, **kwargs) 32 | request.addfinalizer(_patch.stop) 33 | return _patch.start() 34 | 35 | 36 | def function_mock(request, q_function_name, autospec=True, **kwargs): 37 | """Return mock patching function with qualified name *q_function_name*. 38 | 39 | Patch is reversed after calling test returns. 40 | """ 41 | _patch = patch(q_function_name, autospec=autospec, **kwargs) 42 | request.addfinalizer(_patch.stop) 43 | return _patch.start() 44 | 45 | 46 | def initializer_mock(request, cls, autospec=True, **kwargs): 47 | """Return mock for __init__() method on *cls*. 48 | 49 | The patch is reversed after pytest uses it. 50 | """ 51 | _patch = patch.object( 52 | cls, "__init__", autospec=autospec, return_value=None, **kwargs 53 | ) 54 | request.addfinalizer(_patch.stop) 55 | return _patch.start() 56 | 57 | 58 | def instance_mock(request, cls, name=None, spec_set=True, **kwargs): 59 | """Return mock for instance of *cls* that draws its spec from the class. 60 | 61 | The mock will not allow new attributes to be set on the instance. If 62 | *name* is missing or |None|, the name of the returned |Mock| instance is 63 | set to *request.fixturename*. Additional keyword arguments are passed 64 | through to the Mock() call that creates the mock. 65 | """ 66 | name = name if name is not None else request.fixturename 67 | return create_autospec(cls, _name=name, spec_set=spec_set, instance=True, **kwargs) 68 | 69 | 70 | def method_mock(request, cls, method_name, autospec=True, **kwargs): 71 | """Return mock for method *method_name* on *cls*. 72 | 73 | The patch is reversed after pytest uses it. 74 | """ 75 | _patch = patch.object(cls, method_name, autospec=autospec, **kwargs) 76 | request.addfinalizer(_patch.stop) 77 | return _patch.start() 78 | 79 | 80 | def property_mock(request, cls, prop_name, **kwargs): 81 | """Return mock for property *prop_name* on class *cls*. 82 | 83 | The patch is reversed after pytest uses it. 84 | """ 85 | _patch = patch.object(cls, prop_name, new_callable=PropertyMock, **kwargs) 86 | request.addfinalizer(_patch.stop) 87 | return _patch.start() 88 | 89 | 90 | def fetch(data_filename): 91 | """Attempt to fetch data, but if unavailable, skip the tests.""" 92 | try: 93 | return data._fetch(data_filename) 94 | except (ConnectionError, ModuleNotFoundError): 95 | pytest.skip(f"Unable to download {data_filename}") 96 | 97 | 98 | def on_ci(): 99 | # GitHub Actions, Travis and AppVeyor have "CI" 100 | return "CI" in os.environ 101 | 102 | 103 | def is_win32(): 104 | return sys.platform.startswith("win32") 105 | 106 | 107 | class PILImageMock: 108 | RGBA_COLOR_500X500_155_249_240 = Image.new( 109 | "RGBA", size=(500, 500), color=(155, 249, 240) 110 | ) 111 | RGBA_COLOR_50X50_155_0_0 = Image.new("RGBA", size=(50, 50), color=(155, 0, 0)) 112 | 113 | RGB_RANDOM_COLOR_500X500 = Image.fromarray( 114 | (np.random.rand(500, 500, 3) * 255).astype("uint8") 115 | ).convert("RGB") 116 | 117 | RGB_RANDOM_COLOR_10X10 = Image.fromarray( 118 | (np.random.rand(10, 10, 3) * 255).astype("uint8") 119 | ).convert("RGB") 120 | 121 | GRAY_RANDOM_10X10 = Image.fromarray((np.random.rand(10, 10) * 255).astype("uint8")) 122 | 123 | 124 | class NpArrayMock: 125 | ONES_30X30_UINT8 = np.ones([30, 30], dtype="uint8") 126 | ONES_500X500X4_BOOL = np.ones([500, 500, 4], dtype="bool") 127 | ONES_500X500_BOOL = np.ones([500, 500], dtype="bool") 128 | RANDOM_500X500_BOOL = np.random.rand(500, 500) > 0.5 129 | 130 | 131 | PILIMG = PILImageMock 132 | -------------------------------------------------------------------------------- /examples/TCGA/README.md: -------------------------------------------------------------------------------- 1 | # Create a leakage-free dataset of tiles for TCGA 2 | 3 | `extract_tile_pw_tcga.py` is a Python script proposed as reference to retrieve a reproducible dataset of tiles using a collection of WSIs from the [TCGA](https://www.cancer.gov/about-nci/organization/ccg/research/structural-genomics/tcga) public repository. In particular, it can be easily integrated in deep learning pipeline(s) for computational pathology. 4 | 5 | ## Prerequisites 6 | To run this script you will need the following packages, other than `histolab`: 7 | 8 | - `pandas` 9 | - `tqdm` 10 | - `scikit-learn` 11 | 12 | Extra requirements are available in the `examples_reqs.txt` file located in the parent`examples` folder. 13 | All dependencies can be installed via command line, using `pip`: 14 | 15 | ```shell 16 | pip install -f examples_reqs.txt 17 | ``` 18 | 19 | A sample CSV file of patient clinical data is required by the script. 20 | This file can be retrieved from the TCGA [data portal](https://portal.gdc.cancer.gov/) and it is structured as follows: 21 | 22 | | case_id | case_submitter_id | project_id | age_at_index | ... | primary_diagnosis | ... | treatment_type | 23 | |---------------------|-----------------------------------|---------------|-----------|----------------|----------------|-------------------------|-----------------------------------------------------------------| 24 | | 6cd9baf5-bbe0-4c1e-a87f-c53b3af22890 | TCGA-A7-A13G | TCGA-BRCA | 79 | ... | Infiltrating duct carcinoma, NOS | ... | Pharmaceutical Therapy, NOS | 25 | | 928c48a0-68ee-4e28-ae83-9832e52850ca | TCGA-CH-5753 | TCGA-PRAD | 70 | ... | Adenocarcinoma, NOS | ... | Radiation Therapy, NOS | 26 | | ... | ... | ... | ... | ... | ... | ... | ... | 27 | 28 | An example file is available in the current folder (i.e. `clinical_csv_example.csv`), and used as default by the script. 29 | 30 | 31 | ## Workflow 32 | 33 | :warning: In order to run the script, the WSI collection is required to be downloaded upfront from the TCGA 34 | repository. 35 | The recommended way is to use the [gdc-client](https://gdc.cancer.gov/access-data/gdc-data-transfer-tool). 36 | 37 | The automatic download of WSIs via the [GDC wrapper](https://github.com/histolab/gdc-api-wrapper) of histolab will be available soon. 38 | 39 | The `extract_tile_pw_tcga.py` will perform the following steps: 40 | 41 | 1. a fixed number of tiles (100 by default) are randomly extracted from each WSI by the `extract_random_tiles` function. The directory where to store the tiles, along with several parameters that detail the extraction protocol (i.e. `n_tiles`, `seed`, `check_tissue`), can be defined as command-line arguments. 42 | **Note** `histolab` automatically saves the generated tiles in the 'tiles' subdirectory. 43 | 44 | 2. the `split_tiles_patient_wise` function sorts the tiles into the training and the test set (80-20 partition by default) adopting a *Patient-Wise* splitting protocol, namely ensuring that tiles belonging to the same subject are either in the training or the test set. 45 | 46 | ## Usage 47 | 48 | ``` 49 | usage: extract_tile_pw_tcga.py [-h] [--clinical_csv CLINICAL_CSV] 50 | [--wsi_dataset_dir WSI_DATASET_DIR] 51 | [--tile_dataset_dir TILE_DATASET_DIR] 52 | [--tile_size TILE_SIZE TILE_SIZE] 53 | [--n_tiles N_TILES] [--level LEVEL] 54 | [--seed SEED] [--check_tissue CHECK_TISSUE] 55 | 56 | Retrieve a leakage-free dataset of tiles using a collection of WSI. 57 | 58 | optional arguments: 59 | -h, --help show this help message and exit 60 | --clinical_csv CLINICAL_CSV 61 | CSV with clinical data. Default examples/TCGA/clinical_csv_example.csv. 62 | --wsi_dataset_dir WSI_DATASET_DIR 63 | Path where to save the WSIs. Default WSI_TCGA. 64 | --tile_dataset_dir TILE_DATASET_DIR 65 | Path where to save the WSIs. Default tiles_TCGA. 66 | --tile_size TILE_SIZE TILE_SIZE 67 | width and height of the cropped tiles. Default (512, 512). 68 | --n_tiles N_TILES Maximum number of tiles to extract. Default 100. 69 | --level LEVEL Magnification level from which extract the tiles. Default 2. 70 | --seed SEED Seed for RandomState. Default 7. 71 | --check_tissue CHECK_TISSUE 72 | Whether to check if the tile has enough tissue to be saved. Default True. 73 | ``` -------------------------------------------------------------------------------- /tests/unit/data/test_data.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import copy 4 | import os 5 | import sys 6 | from importlib import reload 7 | from unittest.mock import patch 8 | 9 | import openslide 10 | import PIL 11 | import pytest 12 | import numpy as np 13 | from requests.exceptions import HTTPError 14 | 15 | from histolab import __version__ as v 16 | from histolab.data import ( 17 | _fetch, 18 | _has_hash, 19 | _load_svs, 20 | _registry, 21 | cmu_small_region, 22 | data_dir, 23 | registry, 24 | ) 25 | 26 | from ...fixtures import SVS 27 | from ...unitutil import ANY, fetch, function_mock 28 | 29 | 30 | def test_data_dir(): 31 | # data_dir should be a directory that can be used as a standard directory 32 | data_directory = data_dir 33 | assert "cmu_small_region.svs" in os.listdir(data_directory) 34 | 35 | 36 | def test_download_file_from_internet(): 37 | # NOTE: This test will be skipped when internet connection is not available 38 | fetch("histolab/kidney.png") 39 | kidney_path = _fetch("histolab/kidney.png") 40 | kidney_image = np.array(PIL.Image.open(kidney_path)) 41 | 42 | assert kidney_image.shape == (537, 809, 4) 43 | 44 | 45 | def test_download_file_from_internet_but_it_is_broken(): 46 | # NOTE: This test will be skipped when internet connection is not available 47 | fetch("histolab/broken.svs") 48 | with pytest.raises(PIL.UnidentifiedImageError) as err: 49 | _load_svs("histolab/broken.svs") 50 | 51 | assert str(err.value) == "Your wsi has something broken inside, a doctor is needed" 52 | 53 | 54 | def test_cmu_small_region(): 55 | """ Test that "cmu_small_region" svs can be loaded. """ 56 | cmu_small_region_image, path = cmu_small_region() 57 | assert cmu_small_region_image.dimensions == (2220, 2967) 58 | 59 | 60 | @patch.dict(registry, {"data/cmu_small_region_broken.svs": "bar"}, clear=True) 61 | @patch.object(_registry, "legacy_datasets", ["data/cmu_small_region_broken.svs"]) 62 | def test_file_url_not_found(): 63 | data_filename = "data/cmu_small_region_broken.svs" 64 | with pytest.raises(HTTPError) as err: 65 | _fetch(data_filename) 66 | 67 | assert ( 68 | str(err.value) == f"404 Client Error: Not Found for url: " 69 | f"https://github.com/histolab/histolab/raw/{v}/histolab/{data_filename}" 70 | ) 71 | 72 | 73 | def test_load_svs(request): 74 | file = SVS.CMU_1_SMALL_REGION 75 | _fetch = function_mock(request, "histolab.data._fetch") 76 | _fetch.return_value = file 77 | 78 | svs, path = _load_svs(file) 79 | 80 | assert type(svs) == openslide.OpenSlide 81 | assert path == file 82 | 83 | 84 | @pytest.mark.parametrize( 85 | "file, hash, expected_value", 86 | ((SVS.CMU_1_SMALL_REGION, "1234abcd", True), ("/fake/file", "1234abcd", False)), 87 | ) 88 | def it_knows_its_hash(request, file, hash, expected_value): 89 | file = file 90 | file_hash_ = function_mock(request, "histolab.data.file_hash") 91 | file_hash_.return_value = hash 92 | 93 | has_hash = _has_hash(file, hash) 94 | 95 | assert has_hash is expected_value 96 | 97 | 98 | @patch.dict(registry, {"data/cmu_small_region.svs": "bar"}, clear=True) 99 | @patch("histolab.data.image_fetcher", None) 100 | def it_raises_error_on_fetch_if_image_fetcher_is_None(): 101 | with pytest.raises(ModuleNotFoundError) as err: 102 | _fetch("data/cmu_small_region.svs") 103 | 104 | assert ( 105 | str(err.value) 106 | == "The requested file is part of the histolab distribution, but requires the " 107 | "installation of an optional dependency, pooch. To install pooch, use your " 108 | "preferred python package manager. Follow installation instruction found at " 109 | "https://www.fatiando.org/pooch/latest/install.html" 110 | ) 111 | 112 | 113 | def test_pooch_missing(monkeypatch): 114 | from histolab import data 115 | 116 | fakesysmodules = copy.copy(sys.modules) 117 | fakesysmodules["pooch.utils"] = None 118 | monkeypatch.delitem(sys.modules, "pooch.utils") 119 | monkeypatch.setattr("sys.modules", fakesysmodules) 120 | file = SVS.CMU_1_SMALL_REGION 121 | reload(data) 122 | 123 | data.file_hash(file) 124 | 125 | assert data.file_hash.__module__ == "histolab.data" 126 | 127 | 128 | def test_file_hash_with_wrong_algorithm(monkeypatch): 129 | from histolab import data 130 | 131 | fakesysmodules = copy.copy(sys.modules) 132 | fakesysmodules["pooch.utils"] = None 133 | monkeypatch.delitem(sys.modules, "pooch.utils") 134 | monkeypatch.setattr("sys.modules", fakesysmodules) 135 | file = SVS.CMU_1_SMALL_REGION 136 | reload(data) 137 | 138 | with pytest.raises(ValueError) as err: 139 | data.file_hash(file, "fakesha") 140 | assert str(err.value) == "Algorithm 'fakesha' not available in hashlib" 141 | assert data.file_hash.__module__ == "histolab.data" 142 | 143 | 144 | def test_create_image_fetcher_without_pooch(monkeypatch): 145 | from histolab import data 146 | 147 | fakesysmodules = copy.copy(sys.modules) 148 | fakesysmodules["pooch"] = None 149 | monkeypatch.delitem(sys.modules, "pooch") 150 | monkeypatch.setattr("sys.modules", fakesysmodules) 151 | reload(data) 152 | 153 | create_image_fetcher = data._create_image_fetcher() 154 | 155 | assert create_image_fetcher == (None, ANY) 156 | -------------------------------------------------------------------------------- /tests/unit/test_util.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | """Unit test suite for histolab.util module.""" 4 | 5 | import operator 6 | 7 | import pytest 8 | 9 | import numpy as np 10 | from histolab.types import CP, Region 11 | from histolab.util import ( 12 | apply_mask_image, 13 | lazyproperty, 14 | np_to_pil, 15 | polygon_to_mask_array, 16 | region_coordinates, 17 | scale_coordinates, 18 | threshold_to_mask, 19 | ) 20 | from tests.base import ( 21 | IMAGE1_GRAY, 22 | IMAGE1_RGB, 23 | IMAGE1_RGBA, 24 | IMAGE2_GRAY, 25 | IMAGE2_RGB, 26 | IMAGE2_RGBA, 27 | IMAGE3_GRAY_BLACK, 28 | IMAGE3_RGB_BLACK, 29 | IMAGE3_RGBA_BLACK, 30 | IMAGE4_GRAY_WHITE, 31 | IMAGE4_RGB_WHITE, 32 | IMAGE4_RGBA_WHITE, 33 | ) 34 | 35 | from ..fixtures import MASKNPY, NPY 36 | from ..util import load_expectation, load_python_expression 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "ref_coords, ref_size, target_size, expected_value", 41 | ( 42 | (CP(0, 2, 4, 5), (10, 10), (5, 5), (0, 1, 2, 2)), 43 | (CP(90, 112, 124, 125), (100, 100), (95, 95), (85, 106, 117, 118)), 44 | ), 45 | ) 46 | def test_scale_coordinates(ref_coords, ref_size, target_size, expected_value): 47 | x_ul, y_ul, x_br, y_br = expected_value 48 | 49 | scaled_coords = scale_coordinates(ref_coords, ref_size, target_size) 50 | 51 | assert scaled_coords == CP(x_ul, y_ul, x_br, y_br) 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "img_array, expected_mode, expected_size, expected_type, expected_array", 56 | ( 57 | (NPY.NP_TO_PIL_L, "L", (2, 2), np.uint8, "python-expr/np-to-pil-l"), 58 | (NPY.NP_TO_PIL_LA, "LA", (2, 2), np.uint8, "python-expr/np-to-pil-la"), 59 | (NPY.NP_TO_PIL_RGBA, "RGBA", (4, 4), np.uint8, "python-expr/np-to-pil-rgba"), 60 | ), 61 | ) 62 | def test_util_np_to_pil( 63 | img_array, expected_mode, expected_size, expected_type, expected_array 64 | ): 65 | pil_img = np_to_pil(img_array) 66 | 67 | assert pil_img.mode == expected_mode 68 | assert pil_img.size == expected_size 69 | assert np.array(pil_img).dtype == expected_type 70 | np.testing.assert_array_almost_equal( 71 | np.array(pil_img), load_python_expression(expected_array) 72 | ) 73 | 74 | 75 | @pytest.mark.parametrize( 76 | "img, threshold, relate, expected_array", 77 | ( 78 | (IMAGE1_GRAY, 160, operator.gt, "python-expr/threshold-to-mask-160"), 79 | (IMAGE2_GRAY, 140, operator.lt, "python-expr/threshold-to-mask-140"), 80 | (IMAGE3_GRAY_BLACK, 0, operator.gt, "python-expr/5-x-5-zeros"), 81 | (IMAGE3_GRAY_BLACK, 0, operator.lt, "python-expr/5-x-5-zeros"), 82 | (IMAGE3_GRAY_BLACK, 1, operator.lt, "python-expr/5-x-5-ones"), 83 | (IMAGE4_GRAY_WHITE, 0, operator.gt, "python-expr/5-x-5-ones"), 84 | (IMAGE4_GRAY_WHITE, 1, operator.lt, "python-expr/5-x-5-zeros"), 85 | (IMAGE1_RGB, 200, operator.lt, "python-expr/threshold-to-mask-200"), 86 | (IMAGE2_RGB, 39, operator.gt, "python-expr/threshold-to-mask-39"), 87 | (IMAGE3_RGB_BLACK, 0, operator.gt, "python-expr/5-x-5-x-3-zeros"), 88 | (IMAGE3_RGB_BLACK, 0, operator.lt, "python-expr/5-x-5-x-3-zeros"), 89 | (IMAGE3_RGB_BLACK, 1, operator.lt, "python-expr/5-x-5-x-3-ones"), 90 | (IMAGE4_RGB_WHITE, 0, operator.gt, "python-expr/5-x-5-x-3-ones"), 91 | (IMAGE4_RGB_WHITE, 1, operator.lt, "python-expr/5-x-5-x-3-zeros"), 92 | (IMAGE1_RGBA, 178, operator.gt, "python-expr/threshold-to-mask-178"), 93 | (IMAGE2_RGBA, 37, operator.lt, "python-expr/threshold-to-mask-37"), 94 | (IMAGE3_RGBA_BLACK, 2, operator.gt, "python-expr/5-x-5-x-4-zeros"), 95 | (IMAGE3_RGBA_BLACK, 0, operator.lt, "python-expr/5-x-5-x-4-zeros"), 96 | (IMAGE3_RGBA_BLACK, 1, operator.lt, "python-expr/5-x-5-x-4-ones"), 97 | (IMAGE4_RGBA_WHITE, 0, operator.gt, "python-expr/5-x-5-x-4-ones"), 98 | (IMAGE4_RGBA_WHITE, 1, operator.lt, "python-expr/5-x-5-x-4-zeros"), 99 | ), 100 | ) 101 | def test_util_threshold_to_mask(img, threshold, relate, expected_array): 102 | mask = threshold_to_mask(np_to_pil(img), threshold, relate) 103 | 104 | np.testing.assert_array_equal(mask, load_python_expression(expected_array)) 105 | 106 | 107 | @pytest.mark.parametrize( 108 | "img, mask, expected_array", 109 | ( 110 | ( 111 | NPY.APPLY_MASK_IMAGE_F1, 112 | MASKNPY.APPLY_MASK_IMAGE_F1, 113 | "python-expr/apply-mask-image-exp1", 114 | ), 115 | ( 116 | NPY.APPLY_MASK_IMAGE_F2, 117 | MASKNPY.APPLY_MASK_IMAGE_F2, 118 | "python-expr/apply-mask-image-exp2", 119 | ), 120 | ( 121 | NPY.APPLY_MASK_IMAGE_F3, 122 | MASKNPY.APPLY_MASK_IMAGE_F3, 123 | "python-expr/apply-mask-image-exp3", 124 | ), 125 | ( 126 | NPY.APPLY_MASK_IMAGE_F4, 127 | MASKNPY.APPLY_MASK_IMAGE_F4, 128 | "python-expr/apply-mask-image-exp4", 129 | ), 130 | ), 131 | ) 132 | def test_apply_mask_image(img, mask, expected_array): 133 | masked_image = apply_mask_image(img, mask) 134 | 135 | np.testing.assert_array_almost_equal( 136 | np.array(masked_image), load_python_expression(expected_array) 137 | ) 138 | 139 | 140 | @pytest.mark.parametrize( 141 | "dims, vertices, expected_array", 142 | ( 143 | ((5, 5), CP(0, 3, 2, 5), "mask-arrays/polygon-to-mask-array-0325"), 144 | ((5, 6), CP(1, 0, 2, 0), "mask-arrays/polygon-to-mask-array-1020"), 145 | ((5, 5), CP(2, 1, 4, 3), "mask-arrays/polygon-to-mask-array-2143"), 146 | ), 147 | ) 148 | def test_util_polygon_to_mask_array(dims, vertices, expected_array): 149 | polygon_mask = polygon_to_mask_array(dims, vertices) 150 | 151 | np.testing.assert_array_almost_equal( 152 | polygon_mask, load_expectation(expected_array, type_="npy") 153 | ) 154 | 155 | 156 | def test_region_coordinates(): 157 | region = Region(index=0, area=14, bbox=(0, 1, 1, 2), center=(0.5, 0.5)) 158 | region_coords_ = region_coordinates(region) 159 | 160 | assert region_coords_ == CP(x_ul=1, y_ul=0, x_br=2, y_br=1) 161 | 162 | 163 | class DescribeLazyPropertyDecorator: 164 | """Tests @lazyproperty decorator class.""" 165 | 166 | def it_is_a_property_object_on_class_access(self, Obj): 167 | assert isinstance(Obj.fget, property) 168 | 169 | def and_it_adopts_the_docstring_of_the_decorated_method(self, Obj): 170 | assert Obj.fget.__doc__ == "Docstring of Obj.fget method definition." 171 | 172 | def it_only_calculates_value_on_first_call(self, obj): 173 | assert obj.fget == 1 174 | assert obj.fget == 1 175 | 176 | def it_raises_on_attempt_to_assign(self, obj): 177 | assert obj.fget == 1 178 | with pytest.raises(AttributeError): 179 | obj.fget = 42 180 | assert obj.fget == 1 181 | assert obj.fget == 1 182 | 183 | # fixture components --------------------------------------------- 184 | 185 | @pytest.fixture 186 | def Obj(self): 187 | class Obj: 188 | @lazyproperty 189 | def fget(self): 190 | """Docstring of Obj.fget method definition.""" 191 | if not hasattr(self, "_n"): 192 | self._n = 0 193 | self._n += 1 194 | return self._n 195 | 196 | return Obj 197 | 198 | @pytest.fixture 199 | def obj(self, Obj): 200 | return Obj() 201 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Contribution guidelines and standards 9 | Before sending your PR for review, make sure your changes are consistent with the guidelines and follow the coding 10 | style. 11 | 12 | ### General guidelines and philosophy for contribution 13 | - Include unit tests when you contribute new features, as they help to a) prove that your code works correctly, and b) 14 | guard against future breaking changes to lower the maintenance cost. 15 | - Bug fixes also generally require unit tests, because the presence of bugs usually indicates insufficient test 16 | coverage. 17 | - Keep API compatibility in mind when you change code in Histolab core. 18 | - Tests coverage cannot decrease from the current %. 19 | - Do not push integration tests without unit tests. 20 | 21 | ## Contribution Workflow 22 | 23 | Code contributions—bug fixes, new development, test improvement—all follow a GitHub-centered workflow. To participate 24 | in Histolab development, set up a GitHub account. Then: 25 | 26 | 1. Fork the repo https://github.com/histolab/histolab. Go to the project repo page and use the Fork button. This will 27 | create a copy of the repo, under your username. (For more details on how to fork a repository see 28 | [this guide](https://help.github.com/articles/fork-a-repo/).) 29 | 30 | 2. Clone down the forked repo to your local machine. 31 | 32 | `$ git clone git@github.com:your-user-name/project-name.git` 33 | 34 | 3. Create a new branch to hold your work. 35 | 36 | `$ git checkout -b new-branch-name` 37 | 38 | 4. Work on your code. Write and run tests. 39 | 40 | 5. Commit your changes. 41 | 42 | `$ git add .` 43 | 44 | `$ git commit -m "commit message here"` 45 | 46 | 6. Push your changes to your GitHub repo. 47 | 48 | `$ git push origin branch-name` 49 | 50 | 7. Open a Pull Request (PR). Go to the original project repo on GitHub. There will be a message about your recently 51 | pushed branch, asking if you would like to open a pull request. Follow the prompts, compare across repositories, 52 | and submit the PR. 53 | For more read [here](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) 54 | 55 | 8. Maintainers and other contributors will review your PR. Please participate in the conversation, 56 | and try to make any requested changes. Once the PR is approved, the code will be merged. 57 | 58 | Before working on your next contribution, make sure your local repository is up to date. 59 | 60 | 1. Set the upstream remote. (You only have to do this once per project, not every time.) 61 | 62 | `$ git remote add upstream git@github.com:histolab/histolab` 63 | 64 | 2. Switch to the local master branch. 65 | 66 | `$ git checkout master` 67 | 68 | 3. Pull down the changes from upstream. 69 | 70 | `$ git pull upstream master` 71 | 72 | 4. Push the changes to your GitHub account. (Optional, but a good practice.) 73 | 74 | `$ git push origin master` 75 | 76 | 5. Create a new branch if you are starting new work. 77 | 78 | `$ git checkout -b branch-name` 79 | 80 | Additional git and GitHub resources: 81 | 82 | - [Git documentation](https://git-scm.com/documentation) 83 | - [Git development workflow](https://docs.scipy.org/doc/numpy/dev/gitwash/development_workflow.html) 84 | - [Resolving merge conflicts](https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/) 85 | 86 | ## Create your local environment 87 | 88 | Before starting contributing to Histolab, test that your local environment is up and running. Here some steps: 89 | 90 | - Create a python 3.6 - 3.7 `virtualenv` 91 | - Activate the env and in the project root run: 92 | 93 | `pip install -e .[testing]` 94 | 95 | `pip install -r requirements-dev.txt` 96 | 97 | - Install the pre-commit hooks (Optional, but useful for code style compliance) 98 | 99 | `pre-commit install` <- *to be ran in the project root directory* 100 | 101 | - Run the tests 102 | 103 | `pytest` 104 | 105 | ## Code of Conduct 106 | 107 | ### Our Pledge 108 | 109 | In the interest of fostering an open and welcoming environment, we as 110 | contributors and maintainers pledge to making participation in our project a harassment-free experience for everyone, 111 | regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, 112 | personal appearance, race, religion, or sexual identity and orientation. 113 | 114 | ### Our Standards 115 | 116 | Examples of behavior that contributes to creating a positive environment 117 | include: 118 | 119 | - Using welcoming and inclusive language 120 | - Being respectful of differing viewpoints and experiences 121 | - Gracefully accepting constructive criticism 122 | - Focusing on what is best for the community 123 | - Showing empathy towards other community members 124 | 125 | Examples of unacceptable behavior by participants include: 126 | 127 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 128 | - Trolling, insulting/derogatory comments, and personal or political attacks 129 | - Public or private harassment 130 | - Publishing others' private information, such as a physical or electronic 131 | address, without explicit permission 132 | - Other conduct which could reasonably be considered inappropriate in a 133 | professional setting 134 | 135 | ### Our Responsibilities 136 | 137 | Project maintainers are responsible for clarifying the standards of acceptable 138 | behavior and are expected to take appropriate and fair corrective action in 139 | response to any instances of unacceptable behavior. 140 | 141 | Project maintainers have the right and responsibility to remove, edit, or 142 | reject comments, commits, code, wiki edits, issues, and other contributions 143 | that are not aligned to this Code of Conduct, or to ban temporarily or 144 | permanently any contributor for other behaviors that they deem inappropriate, 145 | threatening, offensive, or harmful. 146 | 147 | ### Scope 148 | 149 | This Code of Conduct applies both within project spaces and in public spaces 150 | when an individual is representing the project or its community. Examples of 151 | representing a project or community include using an official project e-mail 152 | address, posting via an official social media account, or acting as an appointed 153 | representative at an online or offline event. Representation of a project may be 154 | further defined and clarified by project maintainers. 155 | 156 | ### Enforcement 157 | 158 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 159 | reported by contacting one of the project mantainers/owners. All 160 | complaints will be reviewed and investigated and will result in a response that 161 | is deemed necessary and appropriate to the circumstances. The project team is 162 | obligated to maintain confidentiality with regard to the reporter of an incident. 163 | Further details of specific enforcement policies may be posted separately. 164 | 165 | Project maintainers who do not follow or enforce the Code of Conduct in good 166 | faith may face temporary or permanent repercussions as determined by other 167 | members of the project's leadership. 168 | 169 | ### Attribution 170 | 171 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 172 | available at [http://contributor-covenant.org/version/1/4][version] 173 | 174 | [homepage]: http://contributor-covenant.org 175 | [version]: http://contributor-covenant.org/version/1/4/ 176 | -------------------------------------------------------------------------------- /tests/unit/filters/test_morphological_filters.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import numpy as np 4 | import pytest 5 | import skimage.morphology 6 | 7 | from histolab.filters import morphological_filters as mof 8 | 9 | from ...base import IMAGE1_RGB, IMAGE2_RGBA 10 | from ...unitutil import NpArrayMock, function_mock 11 | 12 | 13 | class DescribeMorphologicalFilters: 14 | def it_calls_remove_small_objects_filter_functional(self, request): 15 | img_arr = IMAGE1_RGB 16 | F_remove_small_objects = function_mock( 17 | request, 18 | "histolab.filters.morphological_filters_functional.remove_small_objects", 19 | ) 20 | F_remove_small_objects.return_value = img_arr 21 | remove_small_objects = mof.RemoveSmallObjects() 22 | 23 | remove_small_objects(img_arr) 24 | 25 | F_remove_small_objects.assert_called_once_with(img_arr, 3000, True, 95) 26 | assert type(remove_small_objects(img_arr)) == np.ndarray 27 | 28 | def it_calls_remove_small_holes_filter_functional(self, request): 29 | img_arr = IMAGE2_RGBA 30 | F_remove_small_holes = function_mock( 31 | request, "skimage.morphology.remove_small_holes" 32 | ) 33 | F_remove_small_holes.return_value = img_arr 34 | remove_small_holes = mof.RemoveSmallHoles() 35 | 36 | remove_small_holes(img_arr) 37 | 38 | F_remove_small_holes.assert_called_once_with(img_arr, 3000) 39 | assert type(remove_small_holes(img_arr)) == np.ndarray 40 | 41 | def it_calls_binary_erosion_filter_functional(self, request): 42 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 43 | disk = skimage.morphology.disk(5) 44 | F_binary_erosion = function_mock( 45 | request, "scipy.ndimage.morphology.binary_erosion" 46 | ) 47 | F_binary_erosion.return_value = mask_arr 48 | 49 | binary_erosion = mof.BinaryErosion() 50 | 51 | binary_erosion(mask_arr) 52 | 53 | np.testing.assert_array_equal( 54 | F_binary_erosion.call_args_list[0][0][0], mask_arr 55 | ) 56 | np.testing.assert_array_equal(F_binary_erosion.call_args_list[0][0][1], disk) 57 | assert F_binary_erosion.call_args_list[0][0][2] == 1 58 | assert type(binary_erosion(mask_arr)) == np.ndarray 59 | 60 | def but_it_raises_exception_when_call_binary_erosion_non_mask_array(self, request): 61 | array = np.array([(1, 2, 3), (4, 5, 6)]) 62 | F_binary_erosion = function_mock( 63 | request, "scipy.ndimage.morphology.binary_erosion" 64 | ) 65 | F_binary_erosion.return_value = array 66 | 67 | with pytest.raises(ValueError) as err: 68 | binary_erosion = mof.BinaryErosion() 69 | binary_erosion(array) 70 | 71 | assert str(err.value) == "Mask must be binary" 72 | 73 | def it_calls_binary_dilation_filter_functional(self, request): 74 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 75 | disk = skimage.morphology.disk(5) 76 | F_binary_dilation = function_mock( 77 | request, "scipy.ndimage.morphology.binary_dilation" 78 | ) 79 | F_binary_dilation.return_value = mask_arr 80 | 81 | binary_dilation = mof.BinaryDilation() 82 | 83 | binary_dilation(mask_arr) 84 | 85 | np.testing.assert_array_equal( 86 | F_binary_dilation.call_args_list[0][0][0], mask_arr 87 | ) 88 | np.testing.assert_array_equal(F_binary_dilation.call_args_list[0][0][1], disk) 89 | assert F_binary_dilation.call_args_list[0][0][2] == 1 90 | assert type(binary_dilation(mask_arr)) == np.ndarray 91 | 92 | def but_it_raises_exception_when_call_binary_dilation_non_mask_array(self, request): 93 | array = np.array([(1, 2, 3), (4, 5, 6)]) 94 | F_binary_dilation = function_mock( 95 | request, "scipy.ndimage.morphology.binary_dilation" 96 | ) 97 | F_binary_dilation.return_value = array 98 | 99 | with pytest.raises(ValueError) as err: 100 | binary_dilation = mof.BinaryDilation() 101 | binary_dilation(array) 102 | 103 | assert str(err.value) == "Mask must be binary" 104 | 105 | def it_calls_binary_opening_filter_functional(self, request): 106 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 107 | disk = skimage.morphology.disk(3) 108 | F_binary_opening = function_mock( 109 | request, "scipy.ndimage.morphology.binary_opening" 110 | ) 111 | F_binary_opening.return_value = mask_arr 112 | 113 | binary_opening = mof.BinaryOpening() 114 | 115 | binary_opening(mask_arr) 116 | 117 | np.testing.assert_array_equal( 118 | F_binary_opening.call_args_list[0][0][0], mask_arr 119 | ) 120 | np.testing.assert_array_equal(F_binary_opening.call_args_list[0][0][1], disk) 121 | assert F_binary_opening.call_args_list[0][0][2] == 1 122 | assert type(binary_opening(mask_arr)) == np.ndarray 123 | 124 | def but_it_raises_exception_when_call_binary_opening_non_mask_array(self, request): 125 | array = np.array([(1, 2, 3), (4, 5, 6)]) 126 | F_binary_opening = function_mock( 127 | request, "scipy.ndimage.morphology.binary_opening" 128 | ) 129 | F_binary_opening.return_value = array 130 | 131 | with pytest.raises(ValueError) as err: 132 | binary_opening = mof.BinaryOpening() 133 | binary_opening(array) 134 | 135 | assert str(err.value) == "Mask must be binary" 136 | 137 | def it_calls_binary_closing_filter_functional(self, request): 138 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 139 | disk = skimage.morphology.disk(3) 140 | F_binary_closing = function_mock( 141 | request, "scipy.ndimage.morphology.binary_closing" 142 | ) 143 | F_binary_closing.return_value = mask_arr 144 | 145 | binary_closing = mof.BinaryClosing() 146 | 147 | binary_closing(mask_arr) 148 | 149 | np.testing.assert_array_equal( 150 | F_binary_closing.call_args_list[0][0][0], mask_arr 151 | ) 152 | np.testing.assert_array_equal(F_binary_closing.call_args_list[0][0][1], disk) 153 | assert F_binary_closing.call_args_list[0][0][2] == 1 154 | assert type(binary_closing(mask_arr)) == np.ndarray 155 | 156 | def but_it_raises_exception_when_call_binary_closing_non_mask_array(self, request): 157 | array = np.array([(1, 2, 3), (4, 5, 6)]) 158 | F_binary_closing = function_mock( 159 | request, "scipy.ndimage.morphology.binary_closing" 160 | ) 161 | F_binary_closing.return_value = array 162 | 163 | with pytest.raises(ValueError) as err: 164 | binary_closing = mof.BinaryClosing() 165 | binary_closing(array) 166 | 167 | assert str(err.value) == "Mask must be binary" 168 | 169 | def it_calls_white_top_hat_filter(self, request): 170 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 171 | disk = skimage.morphology.disk(5) 172 | _white_top_hat = function_mock(request, "skimage.morphology.white_tophat") 173 | _white_top_hat.return_value = mask_arr 174 | 175 | white_top_hat = mof.WhiteTopHat(disk) 176 | 177 | white_top_hat(mask_arr) 178 | 179 | np.testing.assert_array_equal(_white_top_hat.call_args_list[0][0][0], mask_arr) 180 | np.testing.assert_array_equal(_white_top_hat.call_args_list[0][0][1], disk) 181 | assert type(white_top_hat(mask_arr)) == np.ndarray 182 | 183 | def it_calls_watershed_segmentation_functional(self, request): 184 | mask_arr = NpArrayMock.ONES_500X500X4_BOOL 185 | F_watershed_segmentation = function_mock( 186 | request, 187 | "histolab.filters.morphological_filters_functional.watershed_segmentation", 188 | ) 189 | F_watershed_segmentation.return_value = mask_arr 190 | watershed_segmentation = mof.WatershedSegmentation() 191 | 192 | watershed_segmentation(mask_arr) 193 | 194 | F_watershed_segmentation.assert_called_once_with(mask_arr, 6) 195 | assert type(watershed_segmentation(mask_arr)) == np.ndarray 196 | -------------------------------------------------------------------------------- /src/histolab/tile.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # ------------------------------------------------------------------------ 4 | # Copyright 2020 All Histolab Contributors 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # ------------------------------------------------------------------------ 18 | 19 | import os 20 | from pathlib import Path 21 | from typing import Callable, Union 22 | 23 | import numpy as np 24 | import PIL 25 | 26 | from .filters import image_filters as imf 27 | from .filters.compositions import FiltersComposition 28 | from .types import CoordinatePair 29 | from .util import lazyproperty 30 | 31 | 32 | class Tile: 33 | """Provide Tile object representing a tile generated from a Slide object. 34 | 35 | Arguments 36 | --------- 37 | image : PIL.Image.Image 38 | Image describing the tile 39 | coords : CoordinatePair 40 | Level 0 Coordinates of the Slide from which the tile was extracted 41 | level : int, optional 42 | Level of tile extraction, by default 0 43 | """ 44 | 45 | def __init__(self, image: PIL.Image.Image, coords: CoordinatePair, level: int = 0): 46 | self._image = image 47 | self._coords = coords 48 | self._level = level 49 | 50 | def apply_filters( 51 | self, 52 | filters: Union[ 53 | Callable[[PIL.Image.Image], Union[PIL.Image.Image, np.ndarray]], imf.Compose 54 | ], 55 | ) -> "Tile": 56 | """Apply a filter or composition of filters on a tile. 57 | 58 | Parameters 59 | ---------- 60 | filters : Union[ Callable[[PIL.Image.Image], Union[PIL.Image.Image, np.ndarray]] 61 | , imf.Compose] 62 | Filter or composition of filters to be applied 63 | 64 | Returns 65 | ------- 66 | Tile 67 | Tile with the filters applied 68 | """ 69 | filtered_image = filters(self.image) 70 | if isinstance(filtered_image, np.ndarray): 71 | filtered_image = PIL.Image.fromarray(filtered_image) 72 | return Tile(filtered_image, self.coords, self.level) 73 | 74 | @lazyproperty 75 | def coords(self) -> CoordinatePair: 76 | return self._coords 77 | 78 | def has_enough_tissue( 79 | self, tissue_percent: float = 80.0, near_zero_var_threshold: float = 0.1 80 | ) -> bool: 81 | """Check if the tile has enough tissue. 82 | 83 | Parameters 84 | ---------- 85 | tissue_percent : float, optional 86 | Number between 0.0 and 100.0 representing the minimum required percentage of 87 | tissue over the total area of the image, default is 80.0 88 | near_zero_var_threshold : float, optional 89 | Minimum image variance after morphological operations (dilation, fill 90 | holes), default is 0.1 91 | 92 | Returns 93 | ------- 94 | enough_tissue : bool 95 | Whether the image has enough tissue, i.e. if the proportion of tissue 96 | over the total area of the image is more than ``tissue_percent`` and the 97 | image variance after morphological operations is more than 98 | ``near_zero_var_threshold``. 99 | """ 100 | 101 | if self._is_almost_white: 102 | return False 103 | 104 | if not self._has_only_some_tissue(near_zero_var_threshold): 105 | return False 106 | 107 | if not self._has_tissue_more_than_percent(tissue_percent): 108 | return False 109 | 110 | return True 111 | 112 | @lazyproperty 113 | def image(self) -> PIL.Image.Image: 114 | return self._image 115 | 116 | @lazyproperty 117 | def level(self) -> int: 118 | return self._level 119 | 120 | def save(self, path: Union[str, bytes, os.PathLike]) -> None: 121 | """Save tile at given path. 122 | 123 | The format to use is determined from the filename extension (to be compatible to 124 | PIL.Image formats). If no extension is provided, the image will be saved in png 125 | format. 126 | 127 | Parameters 128 | --------- 129 | path: str or pathlib.Path 130 | Path to which the tile is saved. 131 | 132 | """ 133 | ext = os.path.splitext(path)[1] 134 | if not ext: 135 | path = f"{path}.png" 136 | 137 | Path(path).parent.mkdir(parents=True, exist_ok=True) 138 | self._image.save(path) 139 | 140 | @lazyproperty 141 | def tissue_ratio(self) -> float: 142 | """Return the ratio of the tissue area over the total area of the tile. 143 | 144 | Returns 145 | ------- 146 | float 147 | The ratio of the tissue area over the total area of the tile 148 | """ 149 | filters = FiltersComposition(Tile).tissue_mask_filters 150 | tissue_mask = filters(self.image) 151 | tissue_ratio = np.count_nonzero(tissue_mask) / tissue_mask.size 152 | return tissue_ratio 153 | 154 | # ------- implementation helpers ------- 155 | 156 | def _has_only_some_tissue(self, near_zero_var_threshold: float = 0.1) -> np.bool_: 157 | """Check if the tile is composed by only some tissue. 158 | 159 | Parameters 160 | ---------- 161 | near_zero_var_threshold : float, optional 162 | Minimum image variance after morphological operations (dilation, fill holes) 163 | to consider the image to be composed by only some tissue, default is 0.1 164 | 165 | Returns 166 | ------- 167 | bool 168 | True if the image is composed by only some tissue. False if the tile is 169 | composed by all tissue or by no tissue at all. 170 | """ 171 | filters = FiltersComposition(Tile).tissue_mask_filters 172 | tissue_mask = filters(self._image) 173 | 174 | return np.var(tissue_mask) > near_zero_var_threshold 175 | 176 | def _has_tissue_more_than_percent(self, tissue_percent: float = 80.0) -> bool: 177 | """Check if tissue represent more than ``tissue_percent`` % of the image. 178 | 179 | Parameters 180 | ---------- 181 | tissue_percent : float, optional 182 | Number between 0.0 and 100.0 representing the minimum required percentage of 183 | tissue over the total area of the image, default is 80.0 184 | 185 | Returns 186 | ------- 187 | bool 188 | True if tissue represent more than ``tissue_percent`` % of the image, False 189 | otherwise. 190 | """ 191 | 192 | filters = FiltersComposition(Tile).tissue_mask_filters 193 | return np.mean(self._tissue_mask(filters)) * 100 > tissue_percent 194 | 195 | @lazyproperty 196 | def _is_almost_white(self) -> bool: 197 | """Check if the image is almost white. 198 | 199 | Returns 200 | ------- 201 | bool 202 | True if the image is almost white, False otherwise 203 | """ 204 | rgb2gray = imf.RgbToGrayscale() 205 | image_gray = rgb2gray(self._image) 206 | image_gray_arr = np.array(image_gray) 207 | image_gray_arr = image_gray_arr / 255 208 | 209 | return ( 210 | np.mean(image_gray_arr.ravel()) > 0.9 211 | and np.std(image_gray_arr.ravel()) < 0.09 212 | ) 213 | 214 | def _tissue_mask(self, filters: imf.Compose) -> np.ndarray: 215 | """Return the tissue mask of the tile image 216 | 217 | Parameters 218 | ---------- 219 | imf.Compose 220 | Filters composition 221 | 222 | Returns 223 | ------- 224 | np.ndarray 225 | Tissue mask array 226 | """ 227 | return filters(self._image) 228 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | When contributing to this repository, please first discuss the change you wish to make via issue, 5 | email, or any other method with the owners of this repository before making a change. 6 | 7 | Please note we have a code of conduct, please follow it in all your interactions with the project. 8 | 9 | Contribution guidelines and standards 10 | ------------------------------------- 11 | 12 | Before sending your PR for review, make sure your changes are consistent with the guidelines and follow the coding 13 | style. 14 | 15 | General guidelines and philosophy for contribution 16 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | 18 | 19 | * Include unit tests when you contribute new features, as they help to a) prove that your code works correctly, and b) 20 | guard against future breaking changes to lower the maintenance cost. 21 | * Bug fixes also generally require unit tests, because the presence of bugs usually indicates insufficient test 22 | coverage. 23 | * Keep API compatibility in mind when you change code in Histolab core. 24 | * Tests coverage cannot decrease from the current %. 25 | * Do not push integration tests without unit tests. 26 | 27 | Contribution Workflow 28 | --------------------- 29 | 30 | Code contributions—bug fixes, new development, test improvement—all follow a GitHub-centered workflow. To participate 31 | in Histolab development, set up a GitHub account. Then: 32 | 33 | 34 | #. 35 | Fork the repo https://github.com/histolab/histolab. Go to the project repo page and use the Fork button. This will 36 | create a copy of the repo, under your username. (For more details on how to fork a repository see 37 | `this guide `_.) 38 | 39 | #. 40 | Clone down the forked repo to your local machine. 41 | 42 | ``$ git clone git@github.com:your-user-name/project-name.git`` 43 | 44 | #. 45 | Create a new branch to hold your work. 46 | 47 | ``$ git checkout -b new-branch-name`` 48 | 49 | #. 50 | Work on your code. Write and run tests. 51 | 52 | #. 53 | Commit your changes. 54 | 55 | ``$ git add .`` 56 | 57 | ``$ git commit -m "commit message here"`` 58 | 59 | #. 60 | Push your changes to your GitHub repo. 61 | 62 | ``$ git push origin branch-name`` 63 | 64 | #. 65 | Open a Pull Request (PR). Go to the original project repo on GitHub. There will be a message about your recently 66 | pushed branch, asking if you would like to open a pull request. Follow the prompts, compare across repositories, 67 | and submit the PR. 68 | For more read `here `_ 69 | 70 | #. 71 | Maintainers and other contributors will review your PR. Please participate in the conversation, 72 | and try to make any requested changes. Once the PR is approved, the code will be merged. 73 | 74 | Before working on your next contribution, make sure your local repository is up to date. 75 | 76 | 77 | #. 78 | Set the upstream remote. (You only have to do this once per project, not every time.) 79 | 80 | ``$ git remote add upstream git@github.com:histolab/project-repo-name`` 81 | 82 | #. 83 | Switch to the local master branch. 84 | 85 | ``$ git checkout master`` 86 | 87 | #. 88 | Pull down the changes from upstream. 89 | 90 | ``$ git pull upstream master`` 91 | 92 | #. 93 | Push the changes to your GitHub account. (Optional, but a good practice.) 94 | 95 | ``$ git push origin master`` 96 | 97 | #. 98 | Create a new branch if you are starting new work. 99 | 100 | ``$ git checkout -b branch-name`` 101 | 102 | Additional git and GitHub resources: 103 | 104 | 105 | * `Git documentation `_ 106 | * `Git development workflow `_ 107 | * `Resolving merge conflicts `_ 108 | 109 | Create your local environment 110 | ----------------------------- 111 | 112 | Before starting contributing to Histolab, test that your local environment is up and running. Here some steps: 113 | 114 | 115 | * Create a python 3.6 - 3.7 ``virtualenv`` 116 | * 117 | Activate the env and in the project root run: 118 | 119 | ``pip install -e .[testing]`` 120 | 121 | ``pip install -r requirements-dev.txt`` 122 | 123 | * 124 | Install the pre-commit hooks (Optional, but useful for code style compliance) 125 | 126 | ``pre-commit install`` <- *to be ran in the project root directory* 127 | 128 | * 129 | Run the tests 130 | 131 | ``pytest tests/`` 132 | 133 | Code of Conduct 134 | --------------- 135 | 136 | Our Pledge 137 | ^^^^^^^^^^ 138 | 139 | In the interest of fostering an open and welcoming environment, we as 140 | contributors and maintainers pledge to making participation in our project a harassment-free experience for everyone, 141 | regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, 142 | personal appearance, race, religion, or sexual identity and orientation. 143 | 144 | Our Standards 145 | ^^^^^^^^^^^^^ 146 | 147 | Examples of behavior that contributes to creating a positive environment 148 | include: 149 | 150 | 151 | * Using welcoming and inclusive language 152 | * Being respectful of differing viewpoints and experiences 153 | * Gracefully accepting constructive criticism 154 | * Focusing on what is best for the community 155 | * Showing empathy towards other community members 156 | 157 | Examples of unacceptable behavior by participants include: 158 | 159 | 160 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 161 | * Trolling, insulting/derogatory comments, and personal or political attacks 162 | * Public or private harassment 163 | * Publishing others' private information, such as a physical or electronic 164 | address, without explicit permission 165 | * Other conduct which could reasonably be considered inappropriate in a 166 | professional setting 167 | 168 | Our Responsibilities 169 | ^^^^^^^^^^^^^^^^^^^^ 170 | 171 | Project maintainers are responsible for clarifying the standards of acceptable 172 | behavior and are expected to take appropriate and fair corrective action in 173 | response to any instances of unacceptable behavior. 174 | 175 | Project maintainers have the right and responsibility to remove, edit, or 176 | reject comments, commits, code, wiki edits, issues, and other contributions 177 | that are not aligned to this Code of Conduct, or to ban temporarily or 178 | permanently any contributor for other behaviors that they deem inappropriate, 179 | threatening, offensive, or harmful. 180 | 181 | Scope 182 | ^^^^^ 183 | 184 | This Code of Conduct applies both within project spaces and in public spaces 185 | when an individual is representing the project or its community. Examples of 186 | representing a project or community include using an official project e-mail 187 | address, posting via an official social media account, or acting as an appointed 188 | representative at an online or offline event. Representation of a project may be 189 | further defined and clarified by project maintainers. 190 | 191 | Enforcement 192 | ^^^^^^^^^^^ 193 | 194 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 195 | reported by contacting one of the project mantainers/owners. All 196 | complaints will be reviewed and investigated and will result in a response that 197 | is deemed necessary and appropriate to the circumstances. The project team is 198 | obligated to maintain confidentiality with regard to the reporter of an incident. 199 | Further details of specific enforcement policies may be posted separately. 200 | 201 | Project maintainers who do not follow or enforce the Code of Conduct in good 202 | faith may face temporary or permanent repercussions as determined by other 203 | members of the project's leadership. 204 | 205 | Attribution 206 | ^^^^^^^^^^^ 207 | 208 | This Code of Conduct is adapted from the `Contributor Covenant `_\ , version 1.4, 209 | available at `http://contributor-covenant.org/version/1/4 `_ 210 | 211 | .. toctree:: 212 | 213 | .. toctree:: --------------------------------------------------------------------------------