├── .circleci └── config.yml ├── .coveragerc ├── .gitignore ├── AUTHORS.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── about.rst ├── conf.py ├── contour.rst ├── index.rst ├── patch.rst ├── pyramid.rst ├── thyroid_diag.png └── wsi-thyroid-fs-slide.png ├── paper ├── paper.bib └── paper.md ├── pyslide ├── __init__.py ├── contour │ ├── __init__.py │ ├── _adjust.py │ ├── _check.py │ ├── _rela.py │ ├── _split.py │ └── setup.py ├── patch │ ├── __init__.py │ ├── _prop.py │ ├── _split.py │ └── setup.py ├── pyramid │ ├── __init__.py │ ├── _pyramid.py │ └── setup.py └── setup.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test ├── contour │ ├── test_adjust.py │ ├── test_check.py │ ├── test_cnt_split.py │ └── test_rela.py ├── data │ ├── Images │ │ ├── 3c32efd9.png │ │ └── CropBreastSlide.tif │ └── Slides │ │ └── CropBreastSlide.tiff ├── patch │ ├── test_prop.py │ └── test_split.py └── pyramid │ └── test_pyramid.py └── todo.md /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:3.6.1 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | - run: sudo apt-get install build-essential checkinstall 12 | - run: sudo apt-get install openslide-tools 13 | - run: sudo apt-get install imagemagick 14 | - run: sudo apt-get install libgeos-dev 15 | - run: 16 | name: install dependencies 17 | command: | 18 | python3 -m venv venv 19 | . venv/bin/activate 20 | pip install --upgrade pip 21 | pip install -r requirements.txt 22 | pip install scipy 23 | pip install matplotlib 24 | pip install -U pytest 25 | - run: 26 | name: run tests 27 | command: | 28 | . venv/bin/activate 29 | pytest test 30 | 31 | - store_artifacts: 32 | path: test-reports 33 | destination: test-reports 34 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = pyslide 4 | 5 | [report] 6 | exclude_lines = 7 | if self.debug: 8 | pragma: no cover 9 | raise NotImplementedError 10 | if __name__ == .__main__.: 11 | 12 | ignore_errors = True 13 | omit = 14 | test/* 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .vscode/ 3 | .pytest_cache/ 4 | __pycache__/ 5 | 6 | # File extensions 7 | .DS_Store 8 | *.py[cod] 9 | *$py.class 10 | *.log 11 | 12 | # Distribution / packaging 13 | build/ 14 | dist/ 15 | *.egg-info/ 16 | 17 | # coverage reports 18 | .coverage 19 | coverage.xml 20 | 21 | # Jupyter Notebook 22 | .ipynb_checkpoints 23 | venv 24 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | #### Main author 2 | 3 | - [Pingjun Chen](https://github.com/PingjunChen) 4 | 5 | #### Contributors (alphabetical last name) 6 | 7 | - [Sarthak Pati](https://github.com/sarthakpati) 8 | - [Ajinkya Kulkarni](https://github.com/ajinkya-kulkarni) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pingjun Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyslide - Python whole slide image analysis toolkit 2 | ============ 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/9fd878feda1d4780b8c101efda3422a4)](https://app.codacy.com/app/PingjunChen/pyslide?utm_source=github.com&utm_medium=referral&utm_content=PingjunChen/pyslide&utm_campaign=Badge_Grade_Dashboard) 4 | [![CircleCI](https://circleci.com/gh/PingjunChen/pyslide.svg?style=svg)](https://circleci.com/gh/PingjunChen/pyslide) 5 | [![Documentation Status](https://readthedocs.org/projects/pyslide/badge/?version=latest)](https://pyslide.readthedocs.io/en/latest/?badge=latest) 6 | ![](https://img.shields.io/github/license/PingjunChen/pyslide.svg) 7 | [![codecov](https://codecov.io/gh/PingjunChen/pyslide/branch/master/graph/badge.svg)](https://codecov.io/gh/PingjunChen/pyslide) 8 | [![Downloads](https://pepy.tech/badge/pyslide)](https://pepy.tech/project/pyslide) 9 | ![](https://img.shields.io/github/stars/PingjunChen/pyslide.svg) 10 | 11 | ![pyslide-banner](./docs/thyroid_diag.png) 12 | 13 | Please consider `star` this repo if you find [pyslide](https://github.com/PingjunChen/pyslide) to be helpful for your work. 14 | 15 | Installation 16 | ------------ 17 | To install pyslide, apt dependences before pip: 18 | ```alpha 19 | sudo apt-get install openslide-tools 20 | sudo apt-get install libgeos-dev 21 | pip install -r requirements.txt 22 | pip install pyslide==0.5.0 23 | ``` 24 | 25 | Usage 26 | ------------ 27 | 28 | Documentation 29 | ------------ 30 | Hosted in [https://pyslide.readthedocs.io](https://pyslide.readthedocs.io), powered by [readthedocs](https://readthedocs.org) and [Sphinx](http://www.sphinx-doc.org). 31 | 32 | License 33 | ------------ 34 | [pyslide](https://github.com/PingjunChen/pyslide) is free software made available under the MIT License. For details see the [LICENSE](LICENSE) file. 35 | 36 | Contributors 37 | ------------ 38 | See the [AUTHORS.md](AUTHORS.md) file for a complete list of contributors to the project. 39 | 40 | Contributing 41 | ------------ 42 | ``pyslide`` is an open source project and all whole slide image analysis related functions are very welcome to contribute. An easy way to get started is by suggesting a new enhancement on the [Issues](https://github.com/PingjunChen/pyslide/issues). If you have found a bug, then either report this through [Issues](https://github.com/PingjunChen/pyslide/issues), or even better, make a fork of the repository, fix the bug and then create a [Pull Request](https://github.com/PingjunChen/pyslide/pulls) to get the fix into the master branch. 43 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pyslide 8 | SOURCEDIR = source 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) -------------------------------------------------------------------------------- /docs/about.rst: -------------------------------------------------------------------------------- 1 | About pyslide 2 | ================ 3 | 4 | `pyslide `_ is written for whole slide pathology image automatic analysis. 5 | We would like to include as much as general utilities for whole slide image analysis. 6 | `Pull Request `_ and 7 | `Issue `_ are very welcome! 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyslide documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Oct 4 00:17:25 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | 20 | import os, sys 21 | sys.path.insert(0, os.path.abspath('.')) 22 | import sphinx_rtd_theme 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ['sphinx.ext.mathjax'] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # The suffix(es) of source filenames. 39 | # You can specify multiple suffix as a list of string: 40 | # 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The master toctree document. 45 | master_doc = 'index' 46 | 47 | # General information about the project. 48 | project = 'pyslide' 49 | copyright = '2018, Pingjun Chen' 50 | author = 'Pingjun Chen' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '0.4' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '0.4.1' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | # 64 | # This is also used if you do content translation via gettext catalogs. 65 | # Usually you set "language" from the command line for these cases. 66 | language = None 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | # This patterns also effect to html_static_path and html_extra_path 71 | exclude_patterns = [] 72 | 73 | # The name of the Pygments (syntax highlighting) style to use. 74 | pygments_style = 'sphinx' 75 | 76 | # If true, `todo` and `todoList` produce output, else they produce nothing. 77 | todo_include_todos = False 78 | 79 | 80 | # -- Options for HTML output ---------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | # html_theme = 'alabaster' 86 | html_theme = 'sphinx_rtd_theme' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | 102 | # -- Options for HTMLHelp output ------------------------------------------ 103 | 104 | # Output file base name for HTML help builder. 105 | htmlhelp_basename = 'pyslidedoc' 106 | 107 | 108 | # -- Options for LaTeX output --------------------------------------------- 109 | 110 | latex_elements = { 111 | # The paper size ('letterpaper' or 'a4paper'). 112 | # 113 | # 'papersize': 'letterpaper', 114 | 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | 119 | # Additional stuff for the LaTeX preamble. 120 | # 121 | # 'preamble': '', 122 | 123 | # Latex figure (float) alignment 124 | # 125 | # 'figure_align': 'htbp', 126 | } 127 | 128 | # Grouping the document tree into LaTeX files. List of tuples 129 | # (source start file, target name, title, 130 | # author, documentclass [howto, manual, or own class]). 131 | latex_documents = [ 132 | (master_doc, 'pyslide.tex', 'pyslide Documentation', 133 | 'Pingjun Chen', 'manual'), 134 | ] 135 | 136 | 137 | # -- Options for manual page output --------------------------------------- 138 | 139 | # One entry per manual page. List of tuples 140 | # (source start file, name, description, authors, manual section). 141 | man_pages = [ 142 | (master_doc, 'pyslide', 'pyslide Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ------------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'pyslide', 'pyslide Documentation', 154 | author, 'pyslide', 'One line description of project.', 155 | 'Miscellaneous'), 156 | ] 157 | 158 | 159 | 160 | # -- Options for Epub output ---------------------------------------------- 161 | 162 | # Bibliographic Dublin Core info. 163 | epub_title = project 164 | epub_author = author 165 | epub_publisher = author 166 | epub_copyright = copyright 167 | 168 | # The unique identifier of the text. This can be a ISBN number 169 | # or the project homepage. 170 | # 171 | # epub_identifier = '' 172 | 173 | # A unique identification for the text. 174 | # 175 | # epub_uid = '' 176 | 177 | # A list of files that should not be packed into the epub file. 178 | epub_exclude_files = ['search.html'] 179 | -------------------------------------------------------------------------------- /docs/contour.rst: -------------------------------------------------------------------------------- 1 | Contour 2 | ======== 3 | 4 | contour_valid 5 | -------- 6 | :: 7 | 8 | def contour_valid(cnt_arr): 9 | """ Check contour is valid or not. 10 | 11 | """ 12 | 13 | contour_to_poly_valid 14 | -------- 15 | :: 16 | 17 | 18 | def contour_to_poly_valid(cnt_arr): 19 | """ Convert contour to poly valid if not poly valid 20 | 21 | """ 22 | 23 | cnt_inside_wsi 24 | -------- 25 | :: 26 | 27 | def cnt_inside_wsi(cnt_arr, wsi_h, wsi_w): 28 | """ Determine contour is fully inside whole slide image or not. 29 | 30 | """ 31 | 32 | intersect_cnt_wsi 33 | -------- 34 | :: 35 | 36 | def intersect_cnt_wsi(cnt_arr, wsi_h, wsi_w): 37 | """ Cutting out the contour part inside the whole slide image. 38 | 39 | """ 40 | 41 | cnt_inside_ratio 42 | -------- 43 | :: 44 | 45 | def cnt_inside_ratio(cnt_arr1, cnt_arr2): 46 | """ Calculate the ratio between intersection part of cnt_arr1 and cnt_arr2 47 | to cnt_arr1. 48 | 49 | """ 50 | 51 | contour_patch_splitting_no_overlap 52 | -------- 53 | :: 54 | 55 | def contour_patch_splitting_no_overlap(cnt_arr, wsi_h, wsi_w, 56 | patch_size=299, inside_ratio=0.75): 57 | """ Splitting contour into patches with no overlapping between patches. 58 | 59 | """ 60 | 61 | contour_patch_splitting_self_overlap 62 | -------- 63 | :: 64 | 65 | def contour_patch_splitting_self_overlap(cnt_arr, patch_size=299, inside_ratio=0.75): 66 | """ Splitting contour into patches with both start and end meeting, 67 | with overlapping among patches. 68 | 69 | """ 70 | 71 | contour_patch_splitting_half_overlap 72 | -------- 73 | :: 74 | 75 | def contour_patch_splitting_half_overlap(cnt_arr, wsi_h, wsi_w, 76 | patch_size=448, inside_ratio=0.75): 77 | """ Splitting patches with half overlap between patches. 78 | 79 | """ 80 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pyslide's documentation! 2 | =================================== 3 | 4 | The documentation for `pyslide `_ is mainly organized by sub-modules. 5 | 6 | * :ref:`user-docs` 7 | * :ref:`about-docs` 8 | 9 | .. _user-docs: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: User Documentation 14 | 15 | contour 16 | patch 17 | pyramid 18 | 19 | .. _about-docs: 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | :caption: About pyslide 24 | 25 | about 26 | -------------------------------------------------------------------------------- /docs/patch.rst: -------------------------------------------------------------------------------- 1 | Patch 2 | ======== 3 | 4 | wsi_coor_splitting 5 | -------- 6 | :: 7 | 8 | def wsi_coor_splitting(wsi_h, wsi_w, length, overlap_flag=True): 9 | """ Spltting whole slide image to starting coordinates. 10 | 11 | """ 12 | 13 | 14 | wsi_stride_splitting 15 | -------- 16 | :: 17 | 18 | def wsi_stride_splitting(wsi_h, wsi_w, patch_len, stride_len): 19 | """ Spltting whole slide image to patches by stride. 20 | 21 | """ 22 | 23 | 24 | mean_patch_val 25 | -------- 26 | :: 27 | 28 | def mean_patch_val(img): 29 | """ Mean pixel value of the patch. 30 | 31 | """ 32 | 33 | std_patch_val 34 | -------- 35 | :: 36 | 37 | def std_patch_val(img): 38 | """ Standard deviation of pixel values in the patch. 39 | 40 | """ 41 | 42 | patch_bk_ratio 43 | -------- 44 | :: 45 | 46 | def patch_bk_ratio(img, bk_thresh=0.80): 47 | """ Calculate the ratio of background in the image 48 | 49 | """ 50 | -------------------------------------------------------------------------------- /docs/pyramid.rst: -------------------------------------------------------------------------------- 1 | Pyramid 2 | ======== 3 | 4 | create_pyramidal_img 5 | -------- 6 | :: 7 | 8 | def create_pyramidal_img(img_path, save_dir): 9 | """ Convert normal image to pyramidal image. 10 | 11 | """ 12 | 13 | load_wsi_head 14 | -------- 15 | :: 16 | 17 | def load_wsi_head(wsi_img_path): 18 | """ Load the header meta data of whole slide pyramidal image. 19 | 20 | """ 21 | 22 | load_wsi_level_img 23 | -------- 24 | :: 25 | 26 | def load_wsi_level_img(wsi_img_path, level=0): 27 | """ Load the image from specified level of the whole slide image. 28 | 29 | """ 30 | -------------------------------------------------------------------------------- /docs/thyroid_diag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PingjunChen/pyslide/94e5425a788b8460c49e7ba2daff22e808581442/docs/thyroid_diag.png -------------------------------------------------------------------------------- /docs/wsi-thyroid-fs-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PingjunChen/pyslide/94e5425a788b8460c49e7ba2daff22e808581442/docs/wsi-thyroid-fs-slide.png -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | 2 | @article{chen2019tissueloc, 3 | title={tissueloc: Whole slide digital pathology image tissue localization}, 4 | author={Chen, Pingjun and Yang, Lin}, 5 | journal={The Journal of Open Source Software}, 6 | volume={4}, 7 | pages={1148}, 8 | year={2019}, 9 | doi={10.21105/joss.01148} 10 | } 11 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'pyslide: Digital pathology whole slide image analysis toolbox' 3 | tags: 4 | - whole slide image 5 | - patch splitting 6 | authors: 7 | - name: Pingjun Chen 8 | orcid: 0000-0003-0528-1713 9 | affiliation: "1" 10 | - name: Sarthak Pati 11 | orcid: 0000-0003-2243-8487 12 | affiliation: "2" 13 | - name: Ajinkya Kulkarni 14 | orcid: 0000-0003-1423-3676 15 | affiliation: "3" 16 | affiliations: 17 | - name: The University of Texas MD Anderson Cancer Center 18 | index: 1 19 | - name: University of Pennsylvania 20 | index: 2 21 | - name: Max Planck Institute for Multidisciplinary Sciences 22 | index: 3 23 | date: 02 Apr 2023 24 | bibliography: paper.bib 25 | --- 26 | 27 | Summary 28 | ------------ 29 | With the rapid progression on storage and computing, current pathology image analysis 30 | gradually focus on whole slide image (WSI), which can provide fully automatic diagnosis 31 | to assist pathologists to be more productive and help improve accuracy. However, whole 32 | slide image analysis python package is very limited [@chen2019tissueloc]. 33 | 34 | Acknowledgement 35 | ------------ 36 | Development was supported by National Institutes of Health R01 AR065479-02. 37 | 38 | References 39 | ------------ 40 | -------------------------------------------------------------------------------- /pyslide/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | import pkg_resources 5 | 6 | __all__ = ["__version__", ] 7 | 8 | __version__ = pkg_resources.require("pyslide")[0].version 9 | 10 | from . import contour 11 | from . import patch 12 | from . import pyramid 13 | -------------------------------------------------------------------------------- /pyslide/contour/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ._adjust import * 4 | from ._check import * 5 | from ._rela import * 6 | from ._split import * 7 | -------------------------------------------------------------------------------- /pyslide/contour/_adjust.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from pycontour.poly_transform import np_arr_to_poly, poly_to_np_arr 5 | 6 | __all__ = ["contour_to_poly_valid", 7 | ] 8 | 9 | 10 | def contour_to_poly_valid(cnt_arr): 11 | """ Convert contour to poly valid if not poly valid 12 | 13 | Parameters 14 | ------- 15 | cnt_arr: np.array 16 | contour with standard numpy 2d array format 17 | 18 | Returns 19 | ------- 20 | cnt_valid_arr: np.array 21 | contour with standard numpy 2d array format 22 | 23 | """ 24 | 25 | poly = np_arr_to_poly(cnt_arr) 26 | if poly.is_valid == True: 27 | cnt_valid_arr = cnt_arr 28 | else: 29 | cnt_valid_arr = poly_to_np_arr(poly.convex_hull) 30 | 31 | return cnt_valid_arr 32 | -------------------------------------------------------------------------------- /pyslide/contour/_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pycontour.poly_transform import np_arr_to_poly 4 | 5 | 6 | __all__ = ["contour_valid", 7 | ] 8 | 9 | 10 | def contour_valid(cnt_arr): 11 | """ Check contour is valid or not. 12 | 13 | Parameters 14 | ------- 15 | cnt_arr: np.array 16 | contour with standard numpy 2d array format 17 | 18 | Returns 19 | ------- 20 | valid: boolean 21 | True if valid, else False 22 | 23 | """ 24 | 25 | poly = np_arr_to_poly(cnt_arr) 26 | valid = True if poly.is_valid else False 27 | 28 | return valid 29 | -------------------------------------------------------------------------------- /pyslide/contour/_rela.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from shapely import geometry 3 | 4 | from typing import Tuple 5 | 6 | 7 | __all__ = ["cnt_inside_wsi", "intersect_cnt_wsi", "cnt_inside_ratio"] 8 | 9 | 10 | def construct_polygon_from_points(point_list: np.ndarray) -> geometry.Polygon: 11 | """Constructs a Shapely polygon object from a numpy array of points.""" 12 | x, y = point_list[0], point_list[1] 13 | point_tuples = list(zip(y, x)) # Need to reverse order of x, y to match Shapely convention 14 | return geometry.Polygon(point_tuples) 15 | 16 | 17 | def cnt_inside_wsi(cnt_arr: np.ndarray, wsi_h: int, wsi_w: int) -> bool: 18 | """Determine if a contour is fully inside a whole slide image or not. 19 | 20 | Parameters 21 | ---------- 22 | cnt_arr : np.ndarray 23 | Contour with standard numpy 2d array format 24 | wsi_h : int 25 | Height of whole slide image 26 | wsi_w : int 27 | Width of whole slide image 28 | 29 | Returns 30 | ------- 31 | in_flag : bool 32 | True if contour is fully inside whole slide image, else False 33 | """ 34 | 35 | # Construct whole slide image polygon. In whole slide image, we need to avoid 36 | # contour on the maximum width and height line, thus subtract a small value. 37 | wsi_poly = geometry.box(0, 0, wsi_w - 0.001, wsi_h - 0.001) 38 | 39 | # Construct contour polygon 40 | cnt_poly = construct_polygon_from_points(cnt_arr) 41 | 42 | in_flag = wsi_poly.contains(cnt_poly) 43 | 44 | return in_flag 45 | 46 | 47 | def intersect_cnt_wsi(cnt_arr: np.ndarray, wsi_h: int, wsi_w: int) -> np.ndarray: 48 | """Cut out the contour part inside the whole slide image. 49 | 50 | Parameters 51 | ---------- 52 | cnt_arr : np.ndarray 53 | Contour with standard numpy 2d array format 54 | wsi_h : int 55 | Height of whole slide image 56 | wsi_w : int 57 | Width of whole slide image 58 | 59 | Returns 60 | ------- 61 | inter_cnt : np.ndarray 62 | Contour intersected with whole slide image 63 | """ 64 | 65 | if cnt_inside_wsi(cnt_arr, wsi_h, wsi_w): 66 | inter_cnt = cnt_arr.astype(np.uint32) 67 | else: 68 | # We remove the last line in both width and height of contour 69 | wsi_poly = geometry.box(0, 0, wsi_w - 1, wsi_h - 1) 70 | 71 | # Construct contour polygon 72 | cnt_poly = construct_polygon_from_points(cnt_arr) 73 | 74 | # Get the intersection part of two polygons 75 | inter_poly = wsi_poly.intersection(cnt_poly) 76 | 77 | x_coors, y_coors = inter_poly.exterior.coords.xy 78 | x_coors = x_coors[:-1].tolist() 79 | y_coors = y_coors[:-1].tolist() 80 | inter_cnt = np.zeros((2, len(x_coors)), dtype=np.uint32) 81 | for ind in np.arange(len(x_coors)): 82 | inter_cnt[0, ind] = y_coors[ind] 83 | inter_cnt[1, ind] = x_coors[ind] 84 | 85 | return inter_cnt 86 | 87 | 88 | def cnt_inside_ratio(cnt_arr1: np.ndarray, cnt_arr2: np.ndarray) -> float: 89 | """Calculate the ratio between intersection part of cnt_arr1 and cnt_arr2 to cnt_arr1. 90 | 91 | Parameters 92 | ---------- 93 | cnt_arr1 : np.ndarray 94 | Contour with standard numpy 2d array format 95 | cnt_arr2 : np.ndarray 96 | Contour with standard numpy 2d array format 97 | 98 | Returns 99 | ------- 100 | ratio : float 101 | Intersection ratio of cnt_arr1 102 | """ 103 | 104 | # Construct contour polygons 105 | cnt_poly1 = construct_polygon_from_points(cnt_arr1) 106 | cnt_poly2 = construct_polygon_from_points(cnt_arr2) 107 | 108 | # Check if the polygons intersect 109 | inter_flag = cnt_poly1.intersects(cnt_poly2) 110 | if not inter_flag: 111 | ratio = 0.0 112 | else: 113 | # Calculate the intersection area and ratio 114 | inter_poly = cnt_poly1.intersection(cnt_poly2) 115 | inter_area = inter_poly.area 116 | cnt1_area = cnt_poly1.area 117 | ratio = inter_area * 1.0 / cnt1_area 118 | 119 | return ratio 120 | -------------------------------------------------------------------------------- /pyslide/contour/_split.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | from ._rela import cnt_inside_ratio 5 | 6 | __all__ = ["contour_patch_splitting_no_overlap", 7 | "contour_patch_splitting_self_overlap", 8 | "contour_patch_splitting_half_overlap",] 9 | 10 | 11 | def contour_patch_splitting_no_overlap(cnt_arr, wsi_h, wsi_w, 12 | patch_size=299, inside_ratio=0.75): 13 | """ 14 | Split a contour into non-overlapping patches. 15 | 16 | Parameters 17 | ---------- 18 | cnt_arr : np.array 19 | The contour as a standard numpy 2d array. 20 | wsi_h : int 21 | The height of the whole slide image. 22 | wsi_w : int 23 | The width of the whole slide image. 24 | patch_size : int, optional 25 | The size of each patch. Default is 299. 26 | inside_ratio : float, optional 27 | The ratio of each patch that must be inside the contour. Default is 0.75. 28 | 29 | Returns 30 | ------- 31 | coors_arr : list 32 | List of starting coordinates of patches ([0]-h, [1]-w). 33 | """ 34 | cnt_min_h, cnt_min_w = np.min(cnt_arr[0, :]), np.min(cnt_arr[1, :]) 35 | cnt_max_h, cnt_max_w = np.max(cnt_arr[0, :]), np.max(cnt_arr[1, :]) 36 | if cnt_min_h < 0 or cnt_min_w < 0 or cnt_max_h > wsi_h or cnt_max_w > wsi_w: 37 | return [] 38 | 39 | # add border to top left 40 | start_h, start_w = None, None 41 | half_patch_size = int(np.floor(patch_size / 2.0)) 42 | quarter_patch_size = int(np.floor(patch_size / 4.0)) 43 | if cnt_min_h >= half_patch_size: 44 | start_h = cnt_min_h - half_patch_size 45 | elif cnt_min_h >= quarter_patch_size: 46 | start_h = cnt_min_h - quarter_patch_size 47 | else: 48 | start_h = cnt_min_h 49 | if cnt_min_w >= half_patch_size: 50 | start_w = cnt_min_w - half_patch_size 51 | elif cnt_min_w >= quarter_patch_size: 52 | start_w = cnt_min_w - quarter_patch_size 53 | else: 54 | start_w = cnt_min_w 55 | 56 | # make up the border to satisfy patch grids 57 | end_h = (1 + int(np.floor((cnt_max_h - start_h - 1.0) / patch_size))) * patch_size + start_h 58 | if end_h > wsi_h: 59 | end_h -= patch_size 60 | end_w = (1 + int(np.floor((cnt_max_w - start_w - 1.0) / patch_size))) * patch_size + start_w 61 | if end_w > wsi_w: 62 | end_w -= patch_size 63 | 64 | coors_arr = [] 65 | for cur_h in np.linspace(start_h, end_h-patch_size, num=int(np.floor((end_h-start_h)/patch_size))): 66 | for cur_w in np.linspace(start_w, end_w-patch_size, num=int(np.floor((end_w-start_w)/patch_size))): 67 | cur_patch_cnt = np.array([[cur_h, cur_h, cur_h+patch_size, cur_h+patch_size], 68 | [cur_w, cur_w+patch_size, cur_w+patch_size, cur_w]]) 69 | # inside ratio should satisfy conditions to be used 70 | if cnt_inside_ratio(cur_patch_cnt, cnt_arr) >= inside_ratio: 71 | coors_arr.append([cur_h, cur_w, patch_size, patch_size]) 72 | 73 | return coors_arr 74 | 75 | 76 | def contour_patch_splitting_self_overlap(cnt_arr, patch_size=299, inside_ratio=0.75): 77 | """ 78 | Split a contour into patches with self-overlap. 79 | 80 | Parameters 81 | ---------- 82 | cnt_arr : np.array 83 | The contour as a standard numpy 2d array. 84 | patch_size : int, optional 85 | The size of each patch. Default is 299. 86 | inside_ratio : float, optional 87 | The ratio of each patch that must be inside the contour. Default is 0.75. 88 | 89 | Returns 90 | ------- 91 | coors_arr : list 92 | List of starting coordinates of patches ([0]-h, [1]-w). 93 | """ 94 | cnt_min_h, cnt_min_w = np.min(cnt_arr[0, :]), np.min(cnt_arr[1, :]) 95 | cnt_max_h, cnt_max_w = np.max(cnt_arr[0, :]), np.max(cnt_arr[1, :]) 96 | 97 | cnt_h, cnt_w = cnt_max_h - cnt_min_h, cnt_max_w - cnt_min_w 98 | h_points = int(np.ceil(cnt_h * 1.0 / patch_size)) 99 | w_points = int(np.ceil(cnt_w * 1.0 / patch_size)) 100 | 101 | overlap_h_len = (h_points * patch_size - cnt_h) * 1.0 / (h_points - 1) 102 | extend_h_len = patch_size - overlap_h_len 103 | overlap_w_len = (w_points * patch_size - cnt_w) * 1.0 / (w_points - 1) 104 | extend_w_len = patch_size - overlap_w_len 105 | 106 | coors_arr = [] 107 | for h_ind in np.arange(h_points): 108 | for w_ind in np.arange(w_points): 109 | cur_h = int(np.floor(cnt_min_h + extend_h_len * h_ind)) 110 | cur_w = int(np.floor(cnt_min_w + extend_w_len * w_ind)) 111 | cur_patch_cnt = np.array([[cur_h, cur_h, cur_h+patch_size, cur_h+patch_size], 112 | [cur_w, cur_w+patch_size, cur_w+patch_size, cur_w]]) 113 | if cnt_inside_ratio(cur_patch_cnt, cnt_arr) >= inside_ratio: 114 | coors_arr.append([cur_h, cur_w, patch_size, patch_size]) 115 | return coors_arr 116 | 117 | 118 | def contour_patch_splitting_half_overlap(cnt_arr, wsi_h, wsi_w, 119 | patch_size=448, inside_ratio=0.75): 120 | """ 121 | Split a contour into patches with half-overlap. 122 | 123 | Parameters 124 | ---------- 125 | cnt_arr : np.array 126 | The contour as a standard numpy 2d array. 127 | wsi_h : int 128 | The height of the whole slide image. 129 | wsi_w : int 130 | The width of the whole slide image. 131 | patch_size : int, optional 132 | The size of each patch. Default is 448. 133 | inside_ratio : float, optional 134 | The ratio of each patch that must be inside the contour. Default is 0.75. 135 | 136 | Returns 137 | ------- 138 | coors_arr : list 139 | List of starting coordinates of patches ([0]-h, [1]-w). 140 | """ 141 | cnt_min_h, cnt_min_w = np.min(cnt_arr[0, :]), np.min(cnt_arr[1, :]) 142 | cnt_max_h, cnt_max_w = np.max(cnt_arr[0, :]), np.max(cnt_arr[1, :]) 143 | if cnt_min_h < 0 or cnt_min_w < 0 or cnt_max_h > wsi_h or cnt_max_w > wsi_w: 144 | return [] 145 | 146 | half_patch_size = int(np.floor(patch_size / 2.0)) 147 | quarter_patch_size = int(np.floor(patch_size / 4.0)) 148 | 149 | # add border to top left 150 | start_h = cnt_min_h if cnt_min_h < quarter_patch_size else cnt_min_h - half_patch_size 151 | start_w = cnt_min_w if cnt_min_w < quarter_patch_size else cnt_min_w - half_patch_size 152 | 153 | # make up the border to satisfy patch grids 154 | end_h = (1 + int(np.floor((cnt_max_h - start_h - 1.0) / half_patch_size))) * half_patch_size + start_h 155 | if end_h > wsi_h - patch_size: 156 | end_h -= patch_size 157 | end_w = (1 + int(np.floor((cnt_max_w - start_w - 1.0) / half_patch_size))) * half_patch_size + start_w 158 | if end_w > wsi_w - patch_size: 159 | end_w -= patch_size 160 | 161 | coors_arr = [] 162 | for cur_h in np.linspace(start_h, end_h, int((end_h - start_h) / (patch_size / 2)) + 1): 163 | for cur_w in np.linspace(start_w, end_w, int((end_w - start_w) / (patch_size / 2)) + 1): 164 | cur_patch_cnt = np.array([[cur_h, cur_h, cur_h + patch_size, cur_h + patch_size], 165 | [cur_w, cur_w + patch_size, cur_w + patch_size, cur_w]]) 166 | if cnt_inside_ratio(cur_patch_cnt, cnt_arr) >= inside_ratio: 167 | coors_arr.append([cur_h, cur_w, patch_size, patch_size]) 168 | 169 | return coors_arr 170 | -------------------------------------------------------------------------------- /pyslide/contour/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def configuration(parent_package='', top_path=None): 4 | from numpy.distutils.misc_util import Configuration 5 | 6 | config = Configuration('contour', parent_package, top_path) 7 | 8 | return config 9 | 10 | if __name__ == '__main__': 11 | from numpy.distutils.core import setup 12 | setup(maintainer='Pingjun Chen', 13 | maintainer_email='chenpingjun@gmx.com', 14 | description='Contour utilities in whole slide image', 15 | url='https://github.com/PingjunChen/pyslide', 16 | license='MIT', 17 | **(configuration(top_path='').todict()) 18 | ) 19 | -------------------------------------------------------------------------------- /pyslide/patch/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | 5 | from ._prop import * 6 | from ._split import * 7 | -------------------------------------------------------------------------------- /pyslide/patch/_prop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import numpy as np 6 | from skimage import color 7 | 8 | __all__ = ["mean_patch_val", 9 | "std_patch_val", 10 | "patch_bk_ratio"] 11 | 12 | 13 | def mean_patch_val(img): 14 | """Calculate the mean pixel value of the patch. 15 | 16 | Parameters 17 | ---------- 18 | img : np.ndarray 19 | Patch image. 20 | 21 | Returns 22 | ------- 23 | float 24 | Mean pixel value of the patch. 25 | """ 26 | return img.mean() 27 | 28 | 29 | def std_patch_val(img): 30 | """Calculate the standard deviation of pixel values in the patch. 31 | 32 | Parameters 33 | ---------- 34 | img : np.ndarray 35 | Patch image. 36 | 37 | Returns 38 | ------- 39 | float 40 | Standard deviation of pixel values in the patch. 41 | """ 42 | return img.std() 43 | 44 | 45 | def patch_bk_ratio(img, bk_thresh=0.80): 46 | """Calculate the ratio of background in the image. 47 | 48 | Parameters 49 | ---------- 50 | img : np.ndarray 51 | Patch image. 52 | bk_thresh : float, optional 53 | Background threshold value, by default 0.80. 54 | 55 | Returns 56 | ------- 57 | float 58 | Ratio of background in the patch. 59 | """ 60 | g_img = color.rgb2gray(img) 61 | bk_pixel_num = np.sum(g_img > bk_thresh) 62 | pixel_num = g_img.size 63 | background_ratio = bk_pixel_num / pixel_num 64 | return background_ratio 65 | -------------------------------------------------------------------------------- /pyslide/patch/_split.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | import numpy as np 5 | import itertools, uuid 6 | from skimage import io, transform 7 | import openslide 8 | 9 | 10 | __all__ = ["wsi_coor_splitting", 11 | "wsi_stride_splitting"] 12 | 13 | 14 | def wsi_coor_splitting(wsi_h, wsi_w, length, overlap_flag=True): 15 | """ Splitting whole slide image into starting coordinates. 16 | 17 | Parameters 18 | ---------- 19 | wsi_h : int 20 | Height of whole slide image. 21 | wsi_w : int 22 | Width of whole slide image. 23 | length : int 24 | Length of the patch image. 25 | overlap_flag : bool 26 | Patch with overlap or not. 27 | 28 | Returns 29 | ------- 30 | coors_arr : list 31 | List of starting coordinates of patches ([0]-h, [1]-w). 32 | 33 | """ 34 | 35 | coors_arr = [] 36 | 37 | # Function to split patch with overlap 38 | def split_patch_overlap(ttl_len, sub_len): 39 | p_sets = [] 40 | if ttl_len < sub_len: 41 | return p_sets 42 | if ttl_len == sub_len: 43 | p_sets.append(0) 44 | return p_sets 45 | 46 | p_num = int(np.ceil(ttl_len * 1.0 / sub_len)) 47 | overlap_len = (p_num * sub_len - ttl_len) * 1.0 / (p_num - 1) 48 | extend_len = sub_len - overlap_len 49 | for ind in np.arange(p_num): 50 | p_sets.append(int(round(extend_len * ind))) 51 | return p_sets 52 | 53 | # Function to split patch without overlap 54 | def split_patch_no_overlap(ttl_len, sub_len): 55 | p_sets = [] 56 | if ttl_len < sub_len: 57 | return p_sets 58 | if ttl_len == sub_len: 59 | p_sets.append(0) 60 | return p_sets 61 | 62 | p_num = int(np.floor(ttl_len * 1.0 / sub_len)) 63 | p_sets = [ele*sub_len for ele in np.arange(p_num)] 64 | return p_sets 65 | 66 | if overlap_flag == True: 67 | h_sets = split_patch_overlap(wsi_h, length) 68 | w_sets = split_patch_overlap(wsi_w, length) 69 | else: 70 | h_sets = split_patch_no_overlap(wsi_h, length) 71 | w_sets = split_patch_no_overlap(wsi_w, length) 72 | 73 | # Combine points in both w and h direction 74 | if len(w_sets) > 0 and len(h_sets) > 0: 75 | coors_arr = [(h, w) for h in h_sets for w in w_sets] 76 | 77 | return coors_arr 78 | 79 | 80 | def wsi_stride_splitting(wsi_h, wsi_w, patch_len, stride_len): 81 | """ Split whole slide image to patches with a certain stride. 82 | 83 | Parameters 84 | ------- 85 | wsi_h: int 86 | height of whole slide image 87 | wsi_w: int 88 | width of whole slide image 89 | patch_len: int 90 | length of the patch image 91 | stride_len: int 92 | length of the stride 93 | 94 | Returns 95 | ------- 96 | coors_arr: list 97 | list of starting coordinates of patches ([0]-h, [1]-w) 98 | 99 | Raises 100 | ------ 101 | AssertionError 102 | If patch length is greater than total length. 103 | 104 | """ 105 | 106 | coors_arr = [] 107 | 108 | def stride_split(ttl_len, patch_len, stride_len): 109 | p_sets = [] 110 | if patch_len > ttl_len: 111 | raise AssertionError("Patch length larger than total length.") 112 | elif patch_len == ttl_len: 113 | p_sets.append(0) 114 | else: 115 | stride_num = int(np.ceil((ttl_len - patch_len) * 1.0 / stride_len)) 116 | for ind in range(stride_num+1): 117 | cur_pos = int(((ttl_len - patch_len) * 1.0 / stride_num) * ind) 118 | p_sets.append(cur_pos) 119 | return p_sets 120 | 121 | h_sets = stride_split(wsi_h, patch_len, stride_len) 122 | w_sets = stride_split(wsi_w, patch_len, stride_len) 123 | 124 | # combine points in both w and h direction 125 | if len(w_sets) > 0 and len(h_sets) > 0: 126 | coors_arr = list(itertools.product(h_sets, w_sets)) 127 | 128 | return coors_arr 129 | -------------------------------------------------------------------------------- /pyslide/patch/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | 5 | 6 | def configuration(parent_package='', top_path=None): 7 | from numpy.distutils.misc_util import Configuration, get_numpy_include_dirs 8 | 9 | config = Configuration('patch', parent_package, top_path) 10 | 11 | return config 12 | 13 | 14 | if __name__ == '__main__': 15 | from numpy.distutils.core import setup 16 | setup(maintainer='Pingjun Chen', 17 | maintainer_email='chenpingjun@gmx.com', 18 | description='Patch utility in whole slide image analysis', 19 | url='https://github.com/PingjunChen/pyslide', 20 | license='MIT', 21 | **(configuration(top_path='').todict()) 22 | ) 23 | -------------------------------------------------------------------------------- /pyslide/pyramid/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ._pyramid import * 4 | -------------------------------------------------------------------------------- /pyslide/pyramid/_pyramid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | import numpy as np 5 | 6 | 7 | __all__ = ["create_pyramidal_img", 8 | "load_wsi_head", 9 | "load_wsi_level_img", 10 | ] 11 | 12 | def create_pyramidal_img(img_path, save_dir): 13 | """ Convert normal image to pyramidal image. 14 | 15 | Parameters 16 | ------- 17 | img_path: str 18 | Whole slide image path (absolute path is needed) 19 | save_dir: str 20 | Location of the saved the generated pyramidal image with extension tiff, 21 | (absolute path is needed) 22 | 23 | Returns 24 | ------- 25 | status: int 26 | The status of the pyramidal image generation (0 stands for success) 27 | 28 | Notes 29 | ------- 30 | ImageMagick need to be preinstalled to use this function. 31 | >>> sudo apt-get install imagemagick 32 | 33 | Examples 34 | -------- 35 | >>> img_path = os.path.join(PRJ_PATH, "test/data/Images/CropBreastSlide.tif") 36 | >>> save_dir = os.path.join(PRJ_PATH, "test/data/Slides") 37 | >>> status = pyramid.create_pyramidal_img(img_path, save_dir) 38 | >>> assert status == 0 39 | 40 | """ 41 | 42 | convert_cmd = "convert " + img_path 43 | convert_option = " -compress lzw -quality 90 -define tiff:tile-geometry=256x256 ptif:" 44 | img_name = os.path.basename(img_path) 45 | convert_dst = os.path.join(save_dir, os.path.splitext(img_name)[0] + ".tiff") 46 | status = os.system(convert_cmd + convert_option + convert_dst) 47 | 48 | return status 49 | 50 | 51 | def load_wsi_head(wsi_img_path): 52 | """ Load the header meta data of whole slide pyramidal image. 53 | 54 | Parameters 55 | ------- 56 | wsi_img_path: str 57 | The path to whole slide image 58 | 59 | Returns 60 | ------- 61 | wsi_head: slide metadata 62 | Meta information of whole slide image 63 | 64 | """ 65 | 66 | import openslide 67 | wsi_head = openslide.OpenSlide(wsi_img_path) 68 | 69 | return wsi_head 70 | 71 | 72 | def load_wsi_level_img(wsi_img_path, level=0): 73 | """ Load the image from specified level of the whole slide image. 74 | 75 | Parameters 76 | ------- 77 | wsi_img_path: str 78 | The path to whole slide image 79 | level: int 80 | Loading slide image level 81 | 82 | Returns 83 | ------- 84 | level_img: np.array 85 | Whole slide numpy image in given specified level 86 | 87 | """ 88 | 89 | 90 | wsi_head = load_wsi_head(wsi_img_path) 91 | if level < 0 or level >= wsi_head.level_count: 92 | raise AssertionError("level {} not availabel in {}".format( 93 | level, os.path.basename(wsi_img_path))) 94 | wsi_img = wsi_head.read_region((0, 0), level, wsi_head.level_dimensions[level]) 95 | wsi_img = np.array(wsi_img)[:,:,:3] 96 | 97 | return wsi_img 98 | -------------------------------------------------------------------------------- /pyslide/pyramid/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def configuration(parent_package='', top_path=None): 4 | from numpy.distutils.misc_util import Configuration 5 | 6 | config = Configuration('pyramid', parent_package, top_path) 7 | 8 | return config 9 | 10 | if __name__ == '__main__': 11 | from numpy.distutils.core import setup 12 | setup(maintainer='Pingjun Chen', 13 | maintainer_email='chenpingjun@gmx.com', 14 | description='File format utilities of whole slide image', 15 | url='https://github.com/PingjunChen/pyslide', 16 | license='MIT', 17 | **(configuration(top_path='').todict()) 18 | ) 19 | -------------------------------------------------------------------------------- /pyslide/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | 5 | 6 | def configuration(parent_package='', top_path=None): 7 | from numpy.distutils.misc_util import Configuration 8 | 9 | config = Configuration('pyslide', parent_package, top_path) 10 | config.add_subpackage('contour') 11 | config.add_subpackage('patch') 12 | config.add_subpackage('pyramid') 13 | 14 | return config 15 | 16 | 17 | if __name__ == "__main__": 18 | from numpy.distutils.core import setup 19 | 20 | config = configuration(top_path='').todict() 21 | setup(**config) 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydaily>=0.4.3 2 | pycontour>=1.4.0 3 | numpy>=1.20.0 4 | scikit-image>=0.18.0 5 | Pillow>=8.1.1 6 | shapely>=1.6.4 7 | openslide-python>=1.1.2 8 | setuptools>=50.0.0 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys, pdb 3 | from setuptools import setup, find_packages 4 | 5 | PKG_NAME = "pyslide" 6 | VERSION = "0.5.0" 7 | DESCRIPTION = "Python whole slide image analysis toolkit" 8 | HOMEPAGE = "https://github.com/PingjunChen/pyslide" 9 | LICENSE = "MIT" 10 | AUTHOR_NAME = "Pingjun Chen" 11 | AUTHOR_EMAIL = "pingjunchen@ieee.org" 12 | 13 | REQS = "" 14 | with open('requirements.txt') as f: 15 | REQS = f.read().splitlines() 16 | 17 | CLASSIFIERS = [ 18 | 'Development Status :: 1 - Planning', 19 | 'Intended Audience :: Developers', 20 | 'Intended Audience :: Healthcare Industry', 21 | 'Intended Audience :: Science/Research', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: 3.8', 26 | 'Topic :: Scientific/Engineering', 27 | ] 28 | 29 | args = dict( 30 | name=PKG_NAME, 31 | version=VERSION, 32 | description=DESCRIPTION, 33 | url=HOMEPAGE, 34 | license=LICENSE, 35 | author=AUTHOR_NAME, 36 | author_email=AUTHOR_EMAIL, 37 | packages=find_packages(), 38 | install_requires=REQS, 39 | classifiers= CLASSIFIERS, 40 | ) 41 | 42 | setup(**args) 43 | -------------------------------------------------------------------------------- /test/contour/test_adjust.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import numpy as np 5 | from os.path import dirname as opd 6 | from os.path import abspath as opa 7 | 8 | TEST_PATH = opa(opd(opd(__file__))) 9 | PRJ_PATH = opd(TEST_PATH) 10 | sys.path.insert(0, PRJ_PATH) 11 | 12 | from pyslide import contour 13 | 14 | def test_contour_to_poly_valid(): 15 | cnt_arr1 = np.array([(0, 2, 2, 0), (0, 0, 2, 2)]) 16 | valid_arr1 = contour.contour_to_poly_valid(cnt_arr1) 17 | assert np.array_equal(cnt_arr1, valid_arr1), "Contour arrays not equal." 18 | 19 | cnt_arr2 = np.array([(1, 3, 1, 3), (1, 3, 2, 2)]) 20 | valid_arr2 = contour.contour_to_poly_valid(cnt_arr2) 21 | assert not np.array_equal(cnt_arr2, valid_arr2), "Contour arrays should not be equal." 22 | -------------------------------------------------------------------------------- /test/contour/test_check.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import numpy as np 5 | from os.path import dirname as opd 6 | from os.path import abspath as opa 7 | 8 | TEST_PATH = opa(opd(opd(__file__))) 9 | PRJ_PATH = opd(TEST_PATH) 10 | sys.path.insert(0, PRJ_PATH) 11 | 12 | from pyslide import contour 13 | 14 | 15 | def test_contour_valid(): 16 | cnt_arr1 = np.array([(0, 2, 2, 0), (0, 0, 2, 2)]) 17 | assert contour.contour_valid(cnt_arr1), "Contour array 1 is not valid." 18 | 19 | cnt_arr2 = np.array([(1, 3, 1, 3), (1, 3, 2, 2)]) 20 | assert not contour.contour_valid(cnt_arr2), "Contour array 2 should not be valid." 21 | -------------------------------------------------------------------------------- /test/contour/test_cnt_split.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import numpy as np 5 | from os.path import dirname as opd 6 | from os.path import abspath as opa 7 | 8 | TEST_PATH = opa(opd(opd(__file__))) 9 | PRJ_PATH = opd(TEST_PATH) 10 | sys.path.insert(0, PRJ_PATH) 11 | 12 | from pyslide import contour 13 | 14 | def test_contour_patch_splitting_no_overlap(): 15 | wsi_h, wsi_w = 5000, 5000 16 | cnt_arr = np.array([[160, 160, 4800, 4800], 17 | [160, 4800, 4800, 160]]) 18 | coors_arr = contour.contour_patch_splitting_no_overlap(cnt_arr, wsi_h, wsi_w, 19 | patch_size=299, inside_ratio=0.75) 20 | assert len(coors_arr) > 0, "Coordinate array is empty." 21 | 22 | 23 | 24 | def test_contour_patch_splitting_self_overlap(): 25 | cnt_arr = np.array([[160, 160, 4800, 4800], 26 | [160, 4800, 4800, 160]]) 27 | coors_arr = contour.contour_patch_splitting_self_overlap(cnt_arr, patch_size=299, 28 | inside_ratio=0.75) 29 | assert len(coors_arr) > 0, "Coordinate array is empty." 30 | 31 | 32 | 33 | def test_contour_patch_splitting_half_overlap(): 34 | wsi_h, wsi_w = 4752, 4752 35 | cnt_arr = np.array([[160, 160, 4751, 4751], 36 | [160, 4751, 4751, 160]]) 37 | coors_arr = contour.contour_patch_splitting_half_overlap(cnt_arr, wsi_h, wsi_w, 38 | patch_size=448, inside_ratio=0.01) 39 | assert len(coors_arr) > 0, "Coordinate array is empty." 40 | 41 | -------------------------------------------------------------------------------- /test/contour/test_rela.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import numpy as np 5 | from os.path import dirname as opd 6 | from os.path import abspath as opa 7 | 8 | TEST_PATH = opa(opd(opd(__file__))) 9 | PRJ_PATH = opd(TEST_PATH) 10 | sys.path.insert(0, PRJ_PATH) 11 | 12 | from pyslide import contour 13 | 14 | def test_cnt_inside_wsi(): 15 | wsi_h, wsi_w = 4, 5 16 | cnt1 = np.array([[1, 0, 0, 1, 3.9, 3], 17 | [0, 0, 1, 3, 4.9, 1]]) 18 | in_flag = contour.cnt_inside_wsi(cnt1, wsi_h, wsi_w) 19 | 20 | assert in_flag == True, "The contour should be inside the WSI" 21 | 22 | 23 | def test_intersect_cnt_wsi(): 24 | wsi_h, wsi_w = 4, 5 25 | cnt1 = np.array([[1, 0, 0, 1, 3.9, 3], 26 | [0, 0, 1, 3, 5.1, 1]]) 27 | 28 | inter_cnt = contour.intersect_cnt_wsi(cnt1, wsi_h, wsi_w) 29 | assert np.max(inter_cnt[0, :]) < wsi_h, "The contour should not exceed the height of the WSI" 30 | assert np.max(inter_cnt[1, :]) < wsi_w, "The contour should not exceed the width of the WSI" 31 | 32 | 33 | def test_cnt_inside_ratio(): 34 | cnt1 = np.array([[1, 1, 3, 3], 35 | [1, 3, 3, 1]]) 36 | cnt2 = np.array([[2, 2, 9, 9], 37 | [2, 9, 9, 2]]) 38 | 39 | inside_ratio = contour.cnt_inside_ratio(cnt1, cnt2) 40 | assert 0.0 <= inside_ratio <= 1.0, "The inside ratio should be between 0 and 1" 41 | 42 | -------------------------------------------------------------------------------- /test/data/Images/3c32efd9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PingjunChen/pyslide/94e5425a788b8460c49e7ba2daff22e808581442/test/data/Images/3c32efd9.png -------------------------------------------------------------------------------- /test/data/Images/CropBreastSlide.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PingjunChen/pyslide/94e5425a788b8460c49e7ba2daff22e808581442/test/data/Images/CropBreastSlide.tif -------------------------------------------------------------------------------- /test/data/Slides/CropBreastSlide.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PingjunChen/pyslide/94e5425a788b8460c49e7ba2daff22e808581442/test/data/Slides/CropBreastSlide.tiff -------------------------------------------------------------------------------- /test/patch/test_prop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | from os.path import dirname as opd 5 | from os.path import abspath as opa 6 | 7 | from skimage import io 8 | 9 | TEST_PATH = opa(opd(opd(__file__))) 10 | PRJ_PATH = opd(TEST_PATH) 11 | sys.path.insert(0, PRJ_PATH) 12 | 13 | from pyslide import patch 14 | 15 | def test_patch_bk_ratio(): 16 | img_path = os.path.join(PRJ_PATH, "test/data/Images/3c32efd9.png") 17 | img = io.imread(img_path) 18 | 19 | bk_ratio = patch.patch_bk_ratio(img, bk_thresh=0.80) 20 | if bk_ratio > 1 or bk_ratio < 0: 21 | raise ValueError("Background ratio not in the expected range of 0 to 1.") 22 | -------------------------------------------------------------------------------- /test/patch/test_split.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from os.path import dirname as opd 5 | from os.path import abspath as opa 6 | 7 | TEST_PATH = opa(opd(opd(__file__))) 8 | PRJ_PATH = opd(TEST_PATH) 9 | sys.path.insert(0, PRJ_PATH) 10 | 11 | from pyslide import patch 12 | 13 | 14 | def test_wsi_coor_splitting(): 15 | coors_arr = patch.wsi_coor_splitting(wsi_h=1536, wsi_w=2048, length=224, overlap_flag=True) 16 | 17 | 18 | def test_wsi_stride_splitting(): 19 | coors_arr = patch.wsi_stride_splitting(wsi_h=234, wsi_w=240, patch_len=224, stride_len=8) 20 | -------------------------------------------------------------------------------- /test/pyramid/test_pyramid.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os, sys 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | 7 | from os.path import dirname as opd 8 | from os.path import abspath as opa 9 | 10 | TEST_PATH = opa(opd(opd(__file__))) 11 | PRJ_PATH = opd(TEST_PATH) 12 | sys.path.insert(0, PRJ_PATH) 13 | 14 | from pyslide import pyramid 15 | 16 | 17 | # def test_create_pyramidal_img(): 18 | # save_dir = os.path.join(PRJ_PATH, "test/data/Slides") 19 | # status = pyramid.create_pyramidal_img(img_path, save_dir) 20 | # if not os.path.exists(os.path.join(save_dir, "CropBreastSlide.tiff")): 21 | # raise AssertionError("Pyramidal creation error") 22 | # if status != 0: 23 | # raise AssertionError("Pyramidal creation error") 24 | 25 | 26 | def test_load_wsi_head(): 27 | wsi_img_path = os.path.join(PRJ_PATH, "test/data/Slides/CropBreastSlide.tiff") 28 | with open(wsi_img_path, 'r') as f: 29 | wsi_header = pyramid.load_wsi_head(f) 30 | 31 | 32 | print("WSI level dimension info:") 33 | level_num = wsi_header.level_count 34 | for ind in np.arange(level_num): 35 | print("level {:2d} size: {}".format(ind, wsi_header.level_dimensions[ind])) 36 | 37 | 38 | def test_load_wsi_level_img(): 39 | wsi_img_path = os.path.join(PRJ_PATH, "test/data/Slides/CropBreastSlide.tiff") 40 | wsi_level_img = pyramid.load_wsi_level_img(wsi_img_path, level=3) 41 | plt.imshow(wsi_level_img) 42 | # plt.show() 43 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ## Todo 2 | - [x] Docstring 3 | - [x] Testing 4 | - [x] Documentation 5 | - [x] Issues 6 | - [x] Pull Requests 7 | 8 | ### Functionality 9 | - Add more general tile splitting and fusing functionality 10 | --------------------------------------------------------------------------------