From 0ab3a0f73db366d61d531e47ce906c0297653939 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Sat, 30 May 2020 23:41:58 +0100 Subject: [PATCH 01/69] BLD/STY: Set up cli for command line interface To run tiatoolbox through command line run `python -m tiatoolbox`. Added a dummy hello function to test command line interface works. This will be removed in next commit. Ran black on the project files which rearranged the style. --- docs/conf.py | 54 ++++++++++++++------------------ requirements_dev.txt | 3 +- setup.py | 56 ++++++++++++++++++---------------- tests/test_tiatoolbox.py | 6 ++-- tiatoolbox/__init__.py | 7 +++-- tiatoolbox/__main__.py | 7 +++++ tiatoolbox/cli.py | 29 ++++++++++++++---- tiatoolbox/utils/__init__.py | 1 + tiatoolbox/utils/misc_utils.py | 26 ++++++++++++++++ 9 files changed, 119 insertions(+), 70 deletions(-) create mode 100644 tiatoolbox/__main__.py create mode 100644 tiatoolbox/utils/__init__.py create mode 100644 tiatoolbox/utils/misc_utils.py diff --git a/docs/conf.py b/docs/conf.py index 93b7eafca..417225b0d 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,8 @@ # import os import sys -sys.path.insert(0, os.path.abspath('..')) + +sys.path.insert(0, os.path.abspath("..")) import tiatoolbox @@ -31,22 +32,22 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'TIA Toolbox' +project = "TIA Toolbox" copyright = "2020, TIA Lab" author = "TIA Lab" @@ -69,10 +70,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -83,7 +84,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the @@ -94,13 +95,13 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'tiatoolboxdoc' +htmlhelp_basename = "tiatoolboxdoc" # -- Options for LaTeX output ------------------------------------------ @@ -109,15 +110,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -127,9 +125,7 @@ # (source start file, target name, title, author, documentclass # [howto, manual, or own class]). latex_documents = [ - (master_doc, 'tiatoolbox.tex', - 'TIA Toolbox Documentation', - 'TIA Lab', 'manual'), + (master_doc, "tiatoolbox.tex", "TIA Toolbox Documentation", "TIA Lab", "manual"), ] @@ -137,11 +133,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'tiatoolbox', - 'TIA Toolbox Documentation', - [author], 1) -] +man_pages = [(master_doc, "tiatoolbox", "TIA Toolbox Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------- @@ -150,13 +142,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'tiatoolbox', - 'TIA Toolbox Documentation', - author, - 'tiatoolbox', - 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "tiatoolbox", + "TIA Toolbox Documentation", + author, + "tiatoolbox", + "One line description of project.", + "Miscellaneous", + ), ] - - - diff --git a/requirements_dev.txt b/requirements_dev.txt index 283f5d578..ce1ad5c0c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,4 +9,5 @@ Sphinx==1.8.5 twine==1.14.0 Click==7.0 pytest==4.6.5 -pytest-runner==5.1 \ No newline at end of file +pytest-runner==5.1 +opencv=3.3.1 diff --git a/setup.py b/setup.py index c386284c9..e722ca383 100644 --- a/setup.py +++ b/setup.py @@ -4,48 +4,50 @@ from setuptools import setup, find_packages -with open('README.rst') as readme_file: +with open("README.rst") as readme_file: readme = readme_file.read() -with open('HISTORY.rst') as history_file: +with open("HISTORY.rst") as history_file: history = history_file.read() -requirements = ['Click>=7.0', ] +requirements = [ + "Click>=7.0", +] -setup_requirements = ['pytest-runner', ] +setup_requirements = [ + "pytest-runner", +] -test_requirements = ['pytest>=3', ] +test_requirements = [ + "pytest>=3", +] setup( author="TIA Lab", - author_email='tialab@dcs.warwick.ac.uk', - python_requires='>=3.5', + author_email="tialab@dcs.warwick.ac.uk", + python_requires=">=3.5", classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", ], description="Computational pathology toolbox developed by TIA Lab.", - entry_points={ - 'console_scripts': [ - 'tiatoolbox=tiatoolbox.cli:main', - ], - }, + entry_points={"console_scripts": ["tiatoolbox=tiatoolbox.cli:main",],}, install_requires=requirements, - long_description=readme + '\n\n' + history, + long_description=readme + "\n\n" + history, include_package_data=True, - keywords='tiatoolbox', - name='tiatoolbox', - packages=find_packages(include=['tiatoolbox', 'tiatoolbox.*']), + keywords="tiatoolbox", + name="tiatoolbox", + packages=find_packages(include=["tiatoolbox", "tiatoolbox.*"]), setup_requires=setup_requirements, - test_suite='tests', + test_suite="tests", tests_require=test_requirements, - url='https://github.com/tialab/tiatoolbox', - version='0.1.0', + url="https://github.com/tialab/tiatoolbox", + version="0.1.0", zip_safe=False, ) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index c6654abcc..b884679e7 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -31,7 +31,7 @@ def test_command_line_interface(): runner = CliRunner() result = runner.invoke(cli.main) assert result.exit_code == 0 - assert 'tiatoolbox.cli.main' in result.output - help_result = runner.invoke(cli.main, ['--help']) + assert "tiatoolbox.cli.main" in result.output + help_result = runner.invoke(cli.main, ["--help"]) assert help_result.exit_code == 0 - assert '--help Show this message and exit.' in help_result.output + assert "--help Show this message and exit." in help_result.output diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index 204ba6d30..33d0bda7a 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -1,5 +1,8 @@ """Top-level package for TIA Toolbox.""" +from tiatoolbox import cli +from tiatoolbox import tiatoolbox + __author__ = """TIA Lab""" -__email__ = 'tialab@dcs.warwick.ac.uk' -__version__ = '0.1.0' +__email__ = "tialab@dcs.warwick.ac.uk" +__version__ = "0.1.0" diff --git a/tiatoolbox/__main__.py b/tiatoolbox/__main__.py new file mode 100644 index 000000000..cf1e36900 --- /dev/null +++ b/tiatoolbox/__main__.py @@ -0,0 +1,7 @@ +""" +__main__ file invoked with `python -m tiatoolbox` command +""" + +from tiatoolbox.cli import main + +main() diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 177e90e46..3b5416b89 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,16 +1,33 @@ """Console script for tiatoolbox.""" +from tiatoolbox.utils import misc_utils import sys import click -@click.command() -def main(args=None): - """Console script for tiatoolbox.""" - click.echo("Replace this message by putting your code into " - "tiatoolbox.cli.main") - click.echo("See click documentation at https://click.palletsprojects.com/") +@click.group(context_settings=dict(help_option_names=["-h", "--help"])) +def main(): + """ + Computational pathology toolbox designed by TIALAB. + """ return 0 +@main.command() +@click.option("--count", "-c", default=1, help="Number of times to print the input") +@click.option("--name", "-n", help="Print the output") +def hello(count, name): + """ + prints the command "count" times. + Args: + count: Number of times to print the input + name: Print the output + + Returns: + Prints the input + + """ + misc_utils.hello(count=count, name=name) + + if __name__ == "__main__": sys.exit(main()) # pragma: no cover diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py new file mode 100644 index 000000000..e9be2edb4 --- /dev/null +++ b/tiatoolbox/utils/__init__.py @@ -0,0 +1 @@ +from tiatoolbox.utils import misc_utils diff --git a/tiatoolbox/utils/misc_utils.py b/tiatoolbox/utils/misc_utils.py new file mode 100644 index 000000000..976da7326 --- /dev/null +++ b/tiatoolbox/utils/misc_utils.py @@ -0,0 +1,26 @@ +""" +This file contains miscellaneous small functions repeatedly required and used in the repo +""" +import cv2 + + +def cv2_imread(image_path): + """ + Read an image to a numpy array using OpenCV + + Args: + image_path: Input file path + + Returns: + img: image as numpy array + """ + img = cv2.imread(image_path) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + return img + + +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for x in range(count): + print("Hello %s!" % name) From 3a2465014edd8812e3376bc75f39299f04c93403 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Sun, 31 May 2020 01:27:50 +0100 Subject: [PATCH 02/69] BLD: Added slide information feature Added WSIReader class in wsireader.py and added slide_info.py to display or save WSI meta data. Updated cli for command line interface. Multiprocessing option added through slide_info.slide_info() --- tiatoolbox/__init__.py | 4 +- tiatoolbox/cli.py | 41 +++++++---- tiatoolbox/dataloader/__init__.py | 2 + tiatoolbox/dataloader/slide_info.py | 98 +++++++++++++++++++++++++ tiatoolbox/dataloader/wsireader.py | 106 ++++++++++++++++++++++++++++ tiatoolbox/utils/misc_utils.py | 46 ++++++++---- 6 files changed, 271 insertions(+), 26 deletions(-) create mode 100644 tiatoolbox/dataloader/__init__.py create mode 100644 tiatoolbox/dataloader/slide_info.py create mode 100644 tiatoolbox/dataloader/wsireader.py diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index 33d0bda7a..e73fcbb3a 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -1,6 +1,8 @@ """Top-level package for TIA Toolbox.""" -from tiatoolbox import cli from tiatoolbox import tiatoolbox +from tiatoolbox import cli +from tiatoolbox import dataloader +from tiatoolbox import utils __author__ = """TIA Lab""" diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 3b5416b89..b9b12c13c 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,5 +1,5 @@ """Console script for tiatoolbox.""" -from tiatoolbox.utils import misc_utils +from tiatoolbox import dataloader import sys import click @@ -13,20 +13,35 @@ def main(): @main.command() -@click.option("--count", "-c", default=1, help="Number of times to print the input") -@click.option("--name", "-n", help="Print the output") -def hello(count, name): +@click.option("--wsi_input", help="input path to WSI file or directory path") +@click.option( + "--output_dir", + help="Path to output directory to save the output, default=wsi_input/../meta", +) +@click.option( + "--file_types", + help="file types to capture from directory, default=('*.ndpi', '*.svs', '*.mrxs')", +) +@click.option( + "--mode", + help="'show' to display meta information only or 'save' to save the meta information, default=show", +) +@click.option( + "--num_cpu", + type=int, + help="num of cpus to use for multiprocessing, default=multiprocessing.cpu_count()", +) +def slide_info(wsi_input, output_dir, file_types, mode, num_cpu): """ - prints the command "count" times. - Args: - count: Number of times to print the input - name: Print the output - - Returns: - Prints the input - + Displays or saves WSI metadata """ - misc_utils.hello(count=count, name=name) + dataloader.slide_info.slide_info( + wsi_input=wsi_input, + output_dir=output_dir, + file_types=file_types, + mode=mode, + num_cpu=num_cpu, + ) if __name__ == "__main__": diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py new file mode 100644 index 000000000..01e880caa --- /dev/null +++ b/tiatoolbox/dataloader/__init__.py @@ -0,0 +1,2 @@ +from tiatoolbox.dataloader import slide_info +from tiatoolbox.dataloader import wsireader diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py new file mode 100644 index 000000000..009a0182f --- /dev/null +++ b/tiatoolbox/dataloader/slide_info.py @@ -0,0 +1,98 @@ +""" +This file contains code to output or save slide information using python multiprocessing +""" +from tiatoolbox.dataloader import wsireader +from tiatoolbox.utils import misc_utils as misc +import os +import multiprocessing +from multiprocessing import Pool +from functools import partial + + +def single_file_run(file_name, input_dir, output_dir=None, mode="show"): + """ + Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel + Args: + file_name: WSI file name + input_dir: Path to input directory + output_dir: Path to output directory to save the output + mode: "show" to display meta information only or "save" to save the meta information + + Returns: + displays or saves WSI meta information + + """ + print(file_name, flush=True) + _, file_type = os.path.splitext(file_name) + + if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": + wsi_reader = wsireader.WSIReader( + input_dir=input_dir, file_name=file_name, output_dir=output_dir + ) + if mode == "show": + print(wsi_reader.slide_info(save_mode=False)) + else: + _, name, _ = misc.split_path_name_ext(file_name) + wsi_reader.slide_info(output_dir=output_dir, output_name=name + ".yaml") + + +def slide_info( + wsi_input, + output_dir=None, + file_types=("*.ndpi", "*.svs", "*.mrxs"), + mode="show", + num_cpu=None, +): + """ + Displays or saves WSI metadata + Args: + wsi_input: input path to WSI file or directory path + output_dir: Path to output directory to save the output, default=wsi_input/../meta + file_types: file types to capture from directory, default=("*.ndpi", "*.svs", "*.mrxs") + mode: "show" to display meta information only or "save" to save the meta information, default=show + num_cpu: num of cpus to use for multiprocessing, default=multiprocessing.cpu_count() + + Returns: + displays or saves WSI meta information + + """ + + if output_dir is None: + if os.path.isfile(wsi_input): + dir_path, _ = os.path.split(wsi_input) + output_dir = os.path.join(dir_path, "..", "meta") + elif os.path.isdir(wsi_input): + output_dir = os.path.join(wsi_input, "..", "meta") + + if num_cpu is None: + num_cpu = multiprocessing.cpu_count() + + if file_types is None: + file_types = ("*.ndpi", "*.svs", "*.mrxs") + + if mode is None: + mode = "show" + + if not os.path.isdir(output_dir) and mode == "save": + os.makedirs(output_dir, exist_ok=True) + + if os.path.isdir(wsi_input): + files_all = misc.grab_files_from_dir( + input_path=wsi_input, file_types=file_types + ) + with Pool(num_cpu) as p: + p.map( + partial( + single_file_run, + output_dir=output_dir, + input_dir=wsi_input, + mode=mode, + ), + files_all, + ) + + if os.path.isfile(wsi_input): + input_dir, file_name = os.path.split(wsi_input) + single_file_run( + file_name=file_name, output_dir=output_dir, input_dir=input_dir, mode=mode + ) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py new file mode 100644 index 000000000..ba2b3154e --- /dev/null +++ b/tiatoolbox/dataloader/wsireader.py @@ -0,0 +1,106 @@ +""" +This file contains WSIReader class for WSI reading or extracting metadata information from WSIs +""" +import os +import numpy as np +import yaml +from PIL import Image + +# For Windows Platforms to add path to openslide binaries +if os.name == "nt": + os.environ["PATH"] = ( + "C:\\tools\\openslide\\openslide-win64-20171122\\bin" + ";" + os.environ["PATH"] + ) + +import openslide + + +class WSIReader: + def __init__( + self, + input_dir=os.getcwd(), + file_name=None, + output_dir=os.path.join(os.getcwd(), "output"), + tile_objective_value=20, + tile_read_size_w=5000, + tile_read_size_h=5000, + ): + """ + WSI Reader class to read WSI images + Args: + input_dir: input path to WSI directory + file_name: file name of the WSI + output_dir: output directory to save the output, default=os.getcwd()/output + tile_objective_value: objective value at which tile is generated, default=20 + tile_read_size_w: tile width, default=5000 + tile_read_size_h: tile height, default=5000 + """ + + self.input_dir = input_dir + self.file_name = os.path.basename(file_name) + if output_dir is not None: + self.output_dir = os.path.join(output_dir, self.file_name) + if not os.path.isdir(self.output_dir): + os.makedirs(self.output_dir, exist_ok=True) + + self.openslide_obj = openslide.OpenSlide( + filename=os.path.join(self.input_dir, self.file_name) + ) + self.tile_objective_value = np.int(tile_objective_value) + self.tile_read_size = np.array([tile_read_size_w, tile_read_size_h]) + self.objective_power = np.int( + self.openslide_obj.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER] + ) + self.level_count = self.openslide_obj.level_count + self.level_dimensions = self.openslide_obj.level_dimensions + self.level_downsamples = self.openslide_obj.level_downsamples + + def slide_info(self, save_mode=True, output_dir=None, output_name=None): + """ + WSI meta data reader + Args: + save_mode: save meta information as yaml file + output_dir: output directory to save the meta information + output_name: output file name + + Returns: + displays or saves WSI meta information + + """ + input_dir = self.input_dir + if output_dir is None: + output_dir = self.output_dir + if output_name is None: + output_name = "param.yaml" + if self.objective_power == 0: + self.objective_power = np.int( + self.openslide_obj.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER] + ) + objective_power = self.objective_power + slide_dimension = self.openslide_obj.level_dimensions[0] + tile_objective_value = self.tile_objective_value + rescale = np.int(objective_power / tile_objective_value) + filename = self.file_name + tile_read_size = self.tile_read_size + level_count = self.level_count + level_dimensions = self.level_dimensions + level_downsamples = self.level_downsamples + + param = { + "input_dir": input_dir, + "output_dir": output_dir, + "objective_power": objective_power, + "slide_dimension": slide_dimension, + "rescale": rescale, + "tile_objective_value": tile_objective_value, + "filename": filename, + "tile_read_size": tile_read_size.tolist(), + "level_count": level_count, + "level_dimensions": level_dimensions, + "level_downsamples": level_downsamples, + } + if save_mode: + with open(os.path.join(output_dir, output_name), "w") as yaml_file: + yaml.dump(param, yaml_file) + else: + return param diff --git a/tiatoolbox/utils/misc_utils.py b/tiatoolbox/utils/misc_utils.py index 976da7326..6dbf8486e 100644 --- a/tiatoolbox/utils/misc_utils.py +++ b/tiatoolbox/utils/misc_utils.py @@ -1,26 +1,48 @@ """ This file contains miscellaneous small functions repeatedly required and used in the repo """ -import cv2 +import os +import pathlib -def cv2_imread(image_path): +def split_path_name_ext(full_path): """ - Read an image to a numpy array using OpenCV + Split path of a file to directory path, file name and extension Args: - image_path: Input file path + full_path: Path to a file Returns: - img: image as numpy array + input_dir: directory path + file_name: name of the file without extension + ext: file extension """ - img = cv2.imread(image_path) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + input_dir, file_name = os.path.split(full_path) + file_name, ext = os.path.splitext(file_name) + return input_dir, file_name, ext - return img +def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): + """ + Grabs file paths specified by file extensions + + Args: + input_path: path to the directory where files need to be searched + file_types: file types (extensions) to be searched + + Returns: + list: file paths as a python list + """ + input_path = pathlib.Path(input_path) + + if type(file_types) == str: + if len(file_types.split(",")) > 1: + file_types = tuple(file_types.split(",")) + else: + file_types = (file_types,) + + files_grabbed = [] + for files in file_types: + files_grabbed.extend(input_path.glob(files)) -def hello(count, name): - """Simple program that greets NAME for a total of COUNT times.""" - for x in range(count): - print("Hello %s!" % name) + return list(files_grabbed) From 3477f5255b8028fdc2706060030647dfa52de700 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 12:46:01 +0100 Subject: [PATCH 03/69] BLD: Add multiproc decorator for toolbox Add multiproc.py which defines TIAMultiProcess. Add decorator package in the toolbox Add decorator to slide_info.py to allow multicore processing of slide_info --- tiatoolbox/cli.py | 29 +++++++--- tiatoolbox/dataloader/slide_info.py | 87 +++++++---------------------- tiatoolbox/dataloader/wsireader.py | 4 +- tiatoolbox/decorators/__init__.py | 1 + tiatoolbox/decorators/multiproc.py | 31 ++++++++++ 5 files changed, 74 insertions(+), 78 deletions(-) create mode 100644 tiatoolbox/decorators/__init__.py create mode 100644 tiatoolbox/decorators/multiproc.py diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index b9b12c13c..43352ef70 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,7 +1,9 @@ """Console script for tiatoolbox.""" from tiatoolbox import dataloader +from tiatoolbox.utils import misc_utils as misc import sys import click +import os @click.group(context_settings=dict(help_option_names=["-h", "--help"])) @@ -20,27 +22,40 @@ def main(): ) @click.option( "--file_types", - help="file types to capture from directory, default=('*.ndpi', '*.svs', '*.mrxs')", + help="file types to capture from directory, default='*.ndpi', '*.svs', '*.mrxs'", + default="*.ndpi, *.svs, *.mrxs" ) @click.option( "--mode", help="'show' to display meta information only or 'save' to save the meta information, default=show", ) @click.option( - "--num_cpu", + "--workers", type=int, - help="num of cpus to use for multiprocessing, default=multiprocessing.cpu_count()", + help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", ) -def slide_info(wsi_input, output_dir, file_types, mode, num_cpu): +def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ Displays or saves WSI metadata """ + file_types = tuple(file_types.split(', ')) + if os.path.isdir(wsi_input): + files_all = misc.grab_files_from_dir( + input_path=wsi_input, + file_types=file_types + ) + elif os.path.isfile(wsi_input): + files_all = [wsi_input, ] + else: + raise ValueError('wsi_input path is not valid') + + print(files_all) + dataloader.slide_info.slide_info( - wsi_input=wsi_input, + input_path=files_all, output_dir=output_dir, - file_types=file_types, mode=mode, - num_cpu=num_cpu, + workers=workers, ) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 009a0182f..8f42ea86c 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -3,18 +3,17 @@ """ from tiatoolbox.dataloader import wsireader from tiatoolbox.utils import misc_utils as misc +from tiatoolbox.decorators.multiproc import TIAMultiProcess + import os -import multiprocessing -from multiprocessing import Pool -from functools import partial -def single_file_run(file_name, input_dir, output_dir=None, mode="show"): +@TIAMultiProcess(iter_on='input_path') +def slide_info(input_path, output_dir=None, mode="show"): """ Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel Args: - file_name: WSI file name - input_dir: Path to input directory + input_path: Path to whole slide image output_dir: Path to output directory to save the output mode: "show" to display meta information only or "save" to save the meta information @@ -22,53 +21,11 @@ def single_file_run(file_name, input_dir, output_dir=None, mode="show"): displays or saves WSI meta information """ - print(file_name, flush=True) - _, file_type = os.path.splitext(file_name) - - if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": - wsi_reader = wsireader.WSIReader( - input_dir=input_dir, file_name=file_name, output_dir=output_dir - ) - if mode == "show": - print(wsi_reader.slide_info(save_mode=False)) - else: - _, name, _ = misc.split_path_name_ext(file_name) - wsi_reader.slide_info(output_dir=output_dir, output_name=name + ".yaml") - - -def slide_info( - wsi_input, - output_dir=None, - file_types=("*.ndpi", "*.svs", "*.mrxs"), - mode="show", - num_cpu=None, -): - """ - Displays or saves WSI metadata - Args: - wsi_input: input path to WSI file or directory path - output_dir: Path to output directory to save the output, default=wsi_input/../meta - file_types: file types to capture from directory, default=("*.ndpi", "*.svs", "*.mrxs") - mode: "show" to display meta information only or "save" to save the meta information, default=show - num_cpu: num of cpus to use for multiprocessing, default=multiprocessing.cpu_count() - Returns: - displays or saves WSI meta information - - """ + input_dir, file_name = os.path.split(input_path) if output_dir is None: - if os.path.isfile(wsi_input): - dir_path, _ = os.path.split(wsi_input) - output_dir = os.path.join(dir_path, "..", "meta") - elif os.path.isdir(wsi_input): - output_dir = os.path.join(wsi_input, "..", "meta") - - if num_cpu is None: - num_cpu = multiprocessing.cpu_count() - - if file_types is None: - file_types = ("*.ndpi", "*.svs", "*.mrxs") + output_dir = os.path.join(input_dir, "..", "meta") if mode is None: mode = "show" @@ -76,23 +33,17 @@ def slide_info( if not os.path.isdir(output_dir) and mode == "save": os.makedirs(output_dir, exist_ok=True) - if os.path.isdir(wsi_input): - files_all = misc.grab_files_from_dir( - input_path=wsi_input, file_types=file_types - ) - with Pool(num_cpu) as p: - p.map( - partial( - single_file_run, - output_dir=output_dir, - input_dir=wsi_input, - mode=mode, - ), - files_all, - ) + print(file_name, flush=True) + _, file_type = os.path.splitext(file_name) - if os.path.isfile(wsi_input): - input_dir, file_name = os.path.split(wsi_input) - single_file_run( - file_name=file_name, output_dir=output_dir, input_dir=input_dir, mode=mode + if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": + wsi_reader = wsireader.WSIReader( + input_dir=input_dir, file_name=file_name, output_dir=output_dir ) + if mode == "show": + info = wsi_reader.slide_info(save_mode=False) + print(info) + return info + else: + wsi_reader.slide_info(output_dir=output_dir, output_name=file_name + ".yaml") + return os.path.join(output_dir, file_name+".yaml") diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index ba2b3154e..d2aef5c9d 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -40,8 +40,6 @@ def __init__( self.file_name = os.path.basename(file_name) if output_dir is not None: self.output_dir = os.path.join(output_dir, self.file_name) - if not os.path.isdir(self.output_dir): - os.makedirs(self.output_dir, exist_ok=True) self.openslide_obj = openslide.OpenSlide( filename=os.path.join(self.input_dir, self.file_name) @@ -69,7 +67,7 @@ def slide_info(self, save_mode=True, output_dir=None, output_name=None): """ input_dir = self.input_dir if output_dir is None: - output_dir = self.output_dir + self.output_dir = output_dir if output_name is None: output_name = "param.yaml" if self.objective_power == 0: diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py new file mode 100644 index 000000000..9159393fc --- /dev/null +++ b/tiatoolbox/decorators/__init__.py @@ -0,0 +1 @@ +from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py new file mode 100644 index 000000000..b6e5f0012 --- /dev/null +++ b/tiatoolbox/decorators/multiproc.py @@ -0,0 +1,31 @@ +import multiprocessing +from pathos.multiprocessing import ProcessingPool as Pool +from functools import partial + + +class TIAMultiProcess: + def __init__(self, iter_on): + self.iter_on = iter_on + self.workers = multiprocessing.cpu_count() + + def __call__(self, func): + def func_wrap(*args, **kwargs): + iter_value = None + if 'workers' in kwargs: + self.workers = kwargs.pop('workers') + try: + iter_value = kwargs.pop(self.iter_on) + except ValueError: + print("Please specify iter_on in function decorator") + + with Pool(self.workers) as p: + results = p.map( + partial( + func, + **kwargs + ), + iter_value, + ) + + return results + return func_wrap From 6921300c58e434c72576b8643ae128f7097da01d Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 12:51:08 +0100 Subject: [PATCH 04/69] STY: Run black for style formatting Run black to fix issues with style formatting. --- tiatoolbox/cli.py | 18 ++++++++---------- tiatoolbox/dataloader/slide_info.py | 9 +++++---- tiatoolbox/decorators/multiproc.py | 13 ++++--------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 43352ef70..b5dffa3d6 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -23,7 +23,7 @@ def main(): @click.option( "--file_types", help="file types to capture from directory, default='*.ndpi', '*.svs', '*.mrxs'", - default="*.ndpi, *.svs, *.mrxs" + default="*.ndpi, *.svs, *.mrxs", ) @click.option( "--mode", @@ -38,24 +38,22 @@ def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ Displays or saves WSI metadata """ - file_types = tuple(file_types.split(', ')) + file_types = tuple(file_types.split(", ")) if os.path.isdir(wsi_input): files_all = misc.grab_files_from_dir( - input_path=wsi_input, - file_types=file_types + input_path=wsi_input, file_types=file_types ) elif os.path.isfile(wsi_input): - files_all = [wsi_input, ] + files_all = [ + wsi_input, + ] else: - raise ValueError('wsi_input path is not valid') + raise ValueError("wsi_input path is not valid") print(files_all) dataloader.slide_info.slide_info( - input_path=files_all, - output_dir=output_dir, - mode=mode, - workers=workers, + input_path=files_all, output_dir=output_dir, mode=mode, workers=workers, ) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 8f42ea86c..28d087b53 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -2,13 +2,12 @@ This file contains code to output or save slide information using python multiprocessing """ from tiatoolbox.dataloader import wsireader -from tiatoolbox.utils import misc_utils as misc from tiatoolbox.decorators.multiproc import TIAMultiProcess import os -@TIAMultiProcess(iter_on='input_path') +@TIAMultiProcess(iter_on="input_path") def slide_info(input_path, output_dir=None, mode="show"): """ Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel @@ -45,5 +44,7 @@ def slide_info(input_path, output_dir=None, mode="show"): print(info) return info else: - wsi_reader.slide_info(output_dir=output_dir, output_name=file_name + ".yaml") - return os.path.join(output_dir, file_name+".yaml") + wsi_reader.slide_info( + output_dir=output_dir, output_name=file_name + ".yaml" + ) + return os.path.join(output_dir, file_name + ".yaml") diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index b6e5f0012..ae14f752e 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -11,21 +11,16 @@ def __init__(self, iter_on): def __call__(self, func): def func_wrap(*args, **kwargs): iter_value = None - if 'workers' in kwargs: - self.workers = kwargs.pop('workers') + if "workers" in kwargs: + self.workers = kwargs.pop("workers") try: iter_value = kwargs.pop(self.iter_on) except ValueError: print("Please specify iter_on in function decorator") with Pool(self.workers) as p: - results = p.map( - partial( - func, - **kwargs - ), - iter_value, - ) + results = p.map(partial(func, **kwargs), iter_value,) return results + return func_wrap From c67d8b0725e9daae6a1d17045da5378dc750dd52 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 13:15:58 +0100 Subject: [PATCH 05/69] DOC: Add Docstring to multiproc.py Add docstring to multiproc.py and functions defined in it. --- tiatoolbox/decorators/__init__.py | 3 +++ tiatoolbox/decorators/multiproc.py | 31 ++++++++++++++++++++++++++++++ tiatoolbox/utils/__init__.py | 3 +++ 3 files changed, 37 insertions(+) diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py index 9159393fc..798c1e60f 100644 --- a/tiatoolbox/decorators/__init__.py +++ b/tiatoolbox/decorators/__init__.py @@ -1 +1,4 @@ +""" +Decorators init file +""" from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index ae14f752e..de3a5e3e7 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -1,15 +1,46 @@ +""" +This file defines multiprocessing decorators required by the tiatoolbox. +""" + import multiprocessing from pathos.multiprocessing import ProcessingPool as Pool from functools import partial class TIAMultiProcess: + """ + This class defines the multiprocessing decorator for the toolbox, requires a list iter_on as input on which + multiprocessing will run + """ + def __init__(self, iter_on): + """ + __init__ function for TIAMultiProcess decorator + Args: + iter_on: Variable on which iterations will be performed. + """ self.iter_on = iter_on self.workers = multiprocessing.cpu_count() def __call__(self, func): + """ + This is the function which will be called on a function on which decorator is applied + Args: + func: function to be run with multiprocessing + + Returns: + + """ def func_wrap(*args, **kwargs): + """ + Wrapping function for decorator call + Args: + *args: args inputs + **kwargs: kwargs inputs + + Returns: + + """ iter_value = None if "workers" in kwargs: self.workers = kwargs.pop("workers") diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py index e9be2edb4..80ad14b45 100644 --- a/tiatoolbox/utils/__init__.py +++ b/tiatoolbox/utils/__init__.py @@ -1 +1,4 @@ +""" +Utils init file +""" from tiatoolbox.utils import misc_utils From 6a6845ec1aa0890bd57621346c818b0889ff3e83 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 13:36:02 +0100 Subject: [PATCH 06/69] BUG: Update requirements_dev.txt to fix bugs with Travis Add opencv-python, pathos and openslide as dependencies. --- requirements_dev.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index ce1ad5c0c..812d892eb 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,4 +10,6 @@ twine==1.14.0 Click==7.0 pytest==4.6.5 pytest-runner==5.1 -opencv=3.3.1 +opencv-python==3.3.1 +pathos==0.2.5 +openslide-python==1.1.1 From f7b0c0c01dfc0d8ad792d8704c89c5db7d0743e6 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 13:43:31 +0100 Subject: [PATCH 07/69] BUG: Update requirements_dev.txt to fix bugs opencv-python with Travis Add opencv-python==3.3.1.11. --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 812d892eb..fb338bec2 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,6 +10,6 @@ twine==1.14.0 Click==7.0 pytest==4.6.5 pytest-runner==5.1 -opencv-python==3.3.1 +opencv-python==3.3.1.11 pathos==0.2.5 openslide-python==1.1.1 From 7381339c9bd89ce4d3b5dec8357a9b4f4188d9e8 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 15:42:18 +0100 Subject: [PATCH 08/69] BUG: Travis error fix setuptools Add setuptools 45.1.0 to make it compatible with openslide-python Add opencv-python to latest version 4.2.0.34 as 3.3.1.11 is not available for python 3.7 & 3.8 --- .travis.yml | 1 - requirements_dev.txt | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b9149ed9e..1e963157f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ python: - 3.8 - 3.7 - 3.6 - - 3.5 # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis diff --git a/requirements_dev.txt b/requirements_dev.txt index fb338bec2..6fb19feff 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,8 +8,10 @@ coverage==4.5.4 Sphinx==1.8.5 twine==1.14.0 Click==7.0 +setuptools==45.1.0 pytest==4.6.5 pytest-runner==5.1 -opencv-python==3.3.1.11 +opencv-python==4.2.0.34 pathos==0.2.5 openslide-python==1.1.1 + From eb4a99061ea290b5d64767ad7093911a770033ff Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 15:42:42 +0100 Subject: [PATCH 09/69] BUG: Travis error fix setuptools Add setuptools 45.1.0 to make it compatible with openslide-python Add opencv-python to latest version 4.2.0.34 as 3.3.1.11 is not available for python 3.7 & 3.8 --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e722ca383..6f0b5b900 100644 --- a/setup.py +++ b/setup.py @@ -25,13 +25,12 @@ setup( author="TIA Lab", author_email="tialab@dcs.warwick.ac.uk", - python_requires=">=3.5", + python_requires=">=3.6", classifiers=[ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From f06b65900bdf7c8ac41e6895a27e22816c32345c Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 16:33:49 +0100 Subject: [PATCH 10/69] BUG: Fix Travis error History.rst Update feature branch according to develop branch to fix the error. --- .travis.yml | 30 +++++++++++++++++++++--------- MANIFEST.in | 8 ++++---- requirements_dev.txt | 1 - setup.cfg | 2 +- setup.py | 2 +- tiatoolbox/__init__.py | 2 +- tox.ini | 3 +-- 7 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1e963157f..271ecf417 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,12 +17,24 @@ script: tox # following command to finish PyPI deployment setup: # $ travis encrypt --add deploy.password deploy: - provider: pypi - distributions: sdist bdist_wheel - user: tialab - password: - secure: PLEASE_REPLACE_ME - on: - tags: true - repo: tialab/tiatoolbox - python: 3.8 + - provider: pypi + server: https://test.pypi.org/legacy/ + distributions: sdist bdist_wheel + user: __token__ + password: + secure: a5EoBz4NAQWB0EmgQCKpDRtISy76uNt2dWYZURzTW5v7V2GHyP+drNDWW5HNN0qoLK8pYXBGS47DqJtNDD0+k4G1ogINQKTENWI/oAxXrI8mwAAEhp+2uhHBGSWUX0jlnfRD3VX3M4zSy0DEszGONrJMiJyfRAKNa+P1FPp7AFjRKh8keivCLXgL3Y27OfyBKf4wgy/EnlK2P/BkO9fhPLfDNdBAXikrOCTmG7b0MIRpSXdOqtgo0QtR8t2tQnekke14iH4oTlhEMq2+X875Fejg3n686mXvmTrenLNyZiO0PW4MXAsc5PEES3F2yyLVyk9tHhp54eBErhg60jr2q5GqkX+QBA9wooXcWoLdB+fKJbv4I4XtiMQ68B/1RLvQwWTUjMp520eFBc1dD3HiCEj0KuoplaxFWAgJoAAawC6/TrSpACgb1Cw1M5XcOb3dzUN4hduuvYp8emcixmc0mufrK1QU62u9320rMMF7HZJuLCg4GVHryWqBlc6u6kQrmnF+xU36ms0Deamjzb96TfA2W+DEG2N2dtxmbYEIaLyKZnpLhCV5FZDai4AK5nW9Hst6w/dDU7kYiHwM962ArEKApFuoF4mbNJe3EALR2xlNINZRaEqY+XPXtjODY7g/3bajsDfxEjKB9RTBxq9mL49ioxlXGRTvS7VhU+n6BE0= + on: + branch: develop + tags: true + repo: TIA-Lab/tiatoolbox + python: 3.8 + - provider: pypi + distributions: sdist bdist_wheel + user: __token__ + password: + secure: RibR6mwteL0ZT21Yd0yaWEbG2ArBZlCIA1W7QMlr0BXDbI03+99t90wVsOI6PZIQKaR/yUvU0unEz5dsBzvIAXcDbJADb2NHqn+OPZlIw+kjSU3wPUYxQZzUEemwaSuNMnVLjC1weOdazGyV4zt9kUiT/pIiZFSTFj7K5C6Tjta283UrAv4LBzOPZNJvdi5oqfuSWNmgWzTm2mTOBwnFxJTvMTBDMl8GRMhjb3zAD/4eQKpiSMItJoqpte924jcWWDfgTMGWzhP5KKm7WurkbT/j0R8Px6V/Zhfx82JUy7qMeJRu/5AdQaCd4tjkAPRhKx+z84Dm8EFhRkv1dVeQMSUMu91ejRIRLu4UmN8zplRGcpDjUZwv/j+62EswhN66cb80SczhuBYy0tmI6BOl4+h3oMivojHC6zcbgkaswwS5IGRCf7lY/grFvrEhJVX+c7aRi6/ZvsrO72XwO0OaoOxdwPW7sJZPkn+92DH/bMRe9sUb0PrxWvfHqYdtRGmf3i/NHcZli7Simrn5s6eS3AM8BGL2ZwUs9dmiP0pAj9FhfZNp5nyxQeoTcNgnZBamQk6liZMAW1DYGzLg/U11bsprt8pM5lzJR3G/zwCIgfBn37kg3JL9wkO5M/8eEJ8ADCkx/MeyUMzv2gm8R+jb5fFd8eTQhlpLuXZlxUZiKyA= + on: + branch: master + tags: true + repo: TIA-Lab/tiatoolbox + python: 3.8 diff --git a/MANIFEST.in b/MANIFEST.in index 54e006c98..965b2dda7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,8 @@ -include AUTHORS.md -include CONTRIBUTING.md -include HISTORY.md +include AUTHORS.rst +include CONTRIBUTING.rst +include HISTORY.rst include LICENSE -include README.md +include README.rst recursive-include tests * recursive-exclude * __pycache__ diff --git a/requirements_dev.txt b/requirements_dev.txt index 6fb19feff..60f5028ff 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -14,4 +14,3 @@ pytest-runner==5.1 opencv-python==4.2.0.34 pathos==0.2.5 openslide-python==1.1.1 - diff --git a/setup.cfg b/setup.cfg index f09fae40e..aefe58915 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.0 +current_version = 0.1.1 commit = True tag = True diff --git a/setup.py b/setup.py index 6f0b5b900..203e6f7ea 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/tialab/tiatoolbox", - version="0.1.0", + version="0.1.1", zip_safe=False, ) diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index e73fcbb3a..aab0996c6 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -7,4 +7,4 @@ __author__ = """TIA Lab""" __email__ = "tialab@dcs.warwick.ac.uk" -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/tox.ini b/tox.ini index 30040dbce..bfee79118 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,11 @@ [tox] -envlist = py35, py36, py37, py38, flake8 +envlist = py36, py37, py38, flake8 [travis] python = 3.8: py38 3.7: py37 3.6: py36 - 3.5: py35 [testenv:flake8] basepython = python From 1ba8c42209b61bd4f3c1a21a0fc9185e17efa83c Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 17:01:17 +0100 Subject: [PATCH 11/69] BUG: Fix Travis libopenslide.so.0: error Update .travis.yml to add openslide install before run --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 271ecf417..6897a40ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ python: - 3.7 - 3.6 +before_install: + - sudo apt-get -y install python3-openslide + # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis From 02747bb442e2e92fab339c16a9f9c70062a8b44f Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 1 Jun 2020 18:18:06 +0100 Subject: [PATCH 12/69] BUG: Update test_tiatoolbox.py to fix pytest errors Update test_tiatoolbox.py to fix pytest errors Add test for slide_info feature --- tests/test_tiatoolbox.py | 69 +++++++++++++++++++++++++++++++++------- tiatoolbox/cli.py | 2 +- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index b884679e7..9995197bf 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -1,37 +1,82 @@ #!/usr/bin/env python """Tests for `tiatoolbox` package.""" - import pytest -from click.testing import CliRunner - -from tiatoolbox import tiatoolbox +from tiatoolbox.dataloader.slide_info import slide_info +from tiatoolbox.utils import misc_utils as misc from tiatoolbox import cli +from click.testing import CliRunner +import requests +import os +import pathlib + @pytest.fixture -def response(): - """Sample pytest fixture. +def response_ndpi(): + """Sample pytest fixture for ndpi images. See more at: http://doc.pytest.org/en/latest/fixture.html """ - # import requests - # return requests.get('https://github.com/audreyr/cookiecutter-pypackage') + if not os.path.isfile("./CMU-1.ndpi"): + r = requests.get( + "http://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/CMU-1.ndpi" + ) + with open("./CMU-1.ndpi", "wb") as f: + f.write(r.content) + + +@pytest.fixture +def response_svs(): + """Sample pytest fixture for svs images. + + See more at: http://doc.pytest.org/en/latest/fixture.html + """ + if not os.path.isfile("./CMU-1.svs"): + r = requests.get( + "http://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1.svs" + ) + with open("./CMU-1.svs", "wb") as f: + f.write(r.content) -def test_content(response): +def test_content(response_ndpi, response_svs): """Sample pytest test function with the pytest fixture as an argument.""" # from bs4 import BeautifulSoup # assert 'GitHub' in BeautifulSoup(response.content).title.string + file_types = ("*.ndpi", "*.svs", "*.mrxs") + files_all = misc.grab_files_from_dir( + input_path=str(pathlib.Path(r".")), file_types=file_types, + ) + _ = slide_info(input_path=files_all, workers=2, mode="save") -def test_command_line_interface(): +def test_command_line_help_interface(): """Test the CLI.""" runner = CliRunner() result = runner.invoke(cli.main) assert result.exit_code == 0 - assert "tiatoolbox.cli.main" in result.output help_result = runner.invoke(cli.main, ["--help"]) assert help_result.exit_code == 0 - assert "--help Show this message and exit." in help_result.output + assert help_result.output == result.output + + +def test_command_line_slide_info(): + """Test the Slide infor CLI.""" + runner = CliRunner() + slide_info_result = runner.invoke( + cli.main, + [ + "slide-info", + "--wsi_input", + ".", + "--file_types", + '"*.ndpi, *.svs"', + "--mode", + "show", + "--workers", + "2", + ], + ) + assert slide_info_result.exit_code == 0 diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index b5dffa3d6..446a40e83 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -9,7 +9,7 @@ @click.group(context_settings=dict(help_option_names=["-h", "--help"])) def main(): """ - Computational pathology toolbox designed by TIALAB. + Computational pathology toolbox developed by TIALAB """ return 0 From 5f63b4a8e5de93dc8a60307535155350d0597ed4 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 15:26:46 +0100 Subject: [PATCH 13/69] BUG: FIX travis error for python3.8 Update pytest to 5.4.2 to fix the above issue. --- requirements_dev.txt | 2 +- tests/test_tiatoolbox.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 60f5028ff..7b715c551 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,7 +9,7 @@ Sphinx==1.8.5 twine==1.14.0 Click==7.0 setuptools==45.1.0 -pytest==4.6.5 +pytest==5.4.2 pytest-runner==5.1 opencv-python==4.2.0.34 pathos==0.2.5 diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 9995197bf..ea4e47cef 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -62,7 +62,7 @@ def test_command_line_help_interface(): assert help_result.output == result.output -def test_command_line_slide_info(): +def test_command_line_slide_info(response_ndpi, response_svs): """Test the Slide infor CLI.""" runner = CliRunner() slide_info_result = runner.invoke( From 34b9bd6dc729667afea38341ca36f8a29493a588 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 16:01:29 +0100 Subject: [PATCH 14/69] BUG: FIX travis error for python3.8 Update pytest-runner to 5.2 to fix the above issue. --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7b715c551..6ee8e13e0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,7 +10,7 @@ twine==1.14.0 Click==7.0 setuptools==45.1.0 pytest==5.4.2 -pytest-runner==5.1 +pytest-runner==5.2 opencv-python==4.2.0.34 pathos==0.2.5 openslide-python==1.1.1 From 6f3ffaadad2c527005435238b09ce646a0deb315 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 16:55:07 +0100 Subject: [PATCH 15/69] BUG: Fix Travis python 3.8 issue Added __exit__ to WSIReader in wsireader.py --- tiatoolbox/dataloader/wsireader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index d2aef5c9d..b2d1fbe63 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -53,6 +53,9 @@ def __init__( self.level_dimensions = self.openslide_obj.level_dimensions self.level_downsamples = self.openslide_obj.level_downsamples + def __exit__(self): + self.openslide_obj.close() + def slide_info(self, save_mode=True, output_dir=None, output_name=None): """ WSI meta data reader From 68d21775afd52cfcdc6e4467ef7df3c1c0fb00e3 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 17:05:44 +0100 Subject: [PATCH 16/69] BUG: Fix Travis python 3.8 issue Remove multiprocessing decorator from slide_info.py --- tiatoolbox/dataloader/slide_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 28d087b53..0fd3ef489 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -7,7 +7,7 @@ import os -@TIAMultiProcess(iter_on="input_path") +# @TIAMultiProcess(iter_on="input_path") def slide_info(input_path, output_dir=None, mode="show"): """ Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel From 6811a9872c9d11d20d855c230be8c67733c890f5 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 18:15:03 +0100 Subject: [PATCH 17/69] BUG: Fix Travis python 3.8 issue Remove multiprocessing decorator from slide_info.py --- tests/test_tiatoolbox.py | 8 +++-- tiatoolbox/cli.py | 13 +++---- tiatoolbox/dataloader/slide_info.py | 56 +++++++++++++++-------------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index ea4e47cef..1cf515f04 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -49,7 +49,9 @@ def test_content(response_ndpi, response_svs): files_all = misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, ) - _ = slide_info(input_path=files_all, workers=2, mode="save") + _ = slide_info(input_path=files_all, + # workers=2, + mode="save") def test_command_line_help_interface(): @@ -75,8 +77,8 @@ def test_command_line_slide_info(response_ndpi, response_svs): '"*.ndpi, *.svs"', "--mode", "show", - "--workers", - "2", + # "--workers", + # "2", ], ) assert slide_info_result.exit_code == 0 diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 446a40e83..368f04b4c 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -29,11 +29,11 @@ def main(): "--mode", help="'show' to display meta information only or 'save' to save the meta information, default=show", ) -@click.option( - "--workers", - type=int, - help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", -) +# @click.option( +# "--workers", +# type=int, +# help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", +# ) def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ Displays or saves WSI metadata @@ -53,7 +53,8 @@ def slide_info(wsi_input, output_dir, file_types, mode, workers=None): print(files_all) dataloader.slide_info.slide_info( - input_path=files_all, output_dir=output_dir, mode=mode, workers=workers, + input_path=files_all, output_dir=output_dir, mode=mode, + # workers=workers, ) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 0fd3ef489..6119c4c2b 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -21,30 +21,32 @@ def slide_info(input_path, output_dir=None, mode="show"): """ - input_dir, file_name = os.path.split(input_path) - - if output_dir is None: - output_dir = os.path.join(input_dir, "..", "meta") - - if mode is None: - mode = "show" - - if not os.path.isdir(output_dir) and mode == "save": - os.makedirs(output_dir, exist_ok=True) - - print(file_name, flush=True) - _, file_type = os.path.splitext(file_name) - - if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": - wsi_reader = wsireader.WSIReader( - input_dir=input_dir, file_name=file_name, output_dir=output_dir - ) - if mode == "show": - info = wsi_reader.slide_info(save_mode=False) - print(info) - return info - else: - wsi_reader.slide_info( - output_dir=output_dir, output_name=file_name + ".yaml" - ) - return os.path.join(output_dir, file_name + ".yaml") + if type(input_path) == list: + for input_str in input_path: + input_dir, file_name = os.path.split(input_str) + + if output_dir is None: + output_dir = os.path.join(input_dir, "..", "meta") + + if mode is None: + mode = "show" + + if not os.path.isdir(output_dir) and mode == "save": + os.makedirs(output_dir, exist_ok=True) + + print(file_name, flush=True) + _, file_type = os.path.splitext(file_name) + + if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": + wsi_reader = wsireader.WSIReader( + input_dir=input_dir, file_name=file_name, output_dir=output_dir + ) + if mode == "show": + info = wsi_reader.slide_info(save_mode=False) + print(info) + return info + else: + wsi_reader.slide_info( + output_dir=output_dir, output_name=file_name + ".yaml" + ) + return os.path.join(output_dir, file_name + ".yaml") From ab4393529bf62f8788f5a1190e0c2544231b7caa Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 19:12:17 +0100 Subject: [PATCH 18/69] BUG: Roll back multiprocessing changes for python 3.8 Add a watch for python 3.8 thread, for now python 3.6 and 3.7 test should run fine. --- tiatoolbox/cli.py | 13 +++---- tiatoolbox/dataloader/slide_info.py | 58 ++++++++++++++--------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 368f04b4c..446a40e83 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -29,11 +29,11 @@ def main(): "--mode", help="'show' to display meta information only or 'save' to save the meta information, default=show", ) -# @click.option( -# "--workers", -# type=int, -# help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", -# ) +@click.option( + "--workers", + type=int, + help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", +) def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ Displays or saves WSI metadata @@ -53,8 +53,7 @@ def slide_info(wsi_input, output_dir, file_types, mode, workers=None): print(files_all) dataloader.slide_info.slide_info( - input_path=files_all, output_dir=output_dir, mode=mode, - # workers=workers, + input_path=files_all, output_dir=output_dir, mode=mode, workers=workers, ) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 6119c4c2b..28d087b53 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -7,7 +7,7 @@ import os -# @TIAMultiProcess(iter_on="input_path") +@TIAMultiProcess(iter_on="input_path") def slide_info(input_path, output_dir=None, mode="show"): """ Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel @@ -21,32 +21,30 @@ def slide_info(input_path, output_dir=None, mode="show"): """ - if type(input_path) == list: - for input_str in input_path: - input_dir, file_name = os.path.split(input_str) - - if output_dir is None: - output_dir = os.path.join(input_dir, "..", "meta") - - if mode is None: - mode = "show" - - if not os.path.isdir(output_dir) and mode == "save": - os.makedirs(output_dir, exist_ok=True) - - print(file_name, flush=True) - _, file_type = os.path.splitext(file_name) - - if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": - wsi_reader = wsireader.WSIReader( - input_dir=input_dir, file_name=file_name, output_dir=output_dir - ) - if mode == "show": - info = wsi_reader.slide_info(save_mode=False) - print(info) - return info - else: - wsi_reader.slide_info( - output_dir=output_dir, output_name=file_name + ".yaml" - ) - return os.path.join(output_dir, file_name + ".yaml") + input_dir, file_name = os.path.split(input_path) + + if output_dir is None: + output_dir = os.path.join(input_dir, "..", "meta") + + if mode is None: + mode = "show" + + if not os.path.isdir(output_dir) and mode == "save": + os.makedirs(output_dir, exist_ok=True) + + print(file_name, flush=True) + _, file_type = os.path.splitext(file_name) + + if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": + wsi_reader = wsireader.WSIReader( + input_dir=input_dir, file_name=file_name, output_dir=output_dir + ) + if mode == "show": + info = wsi_reader.slide_info(save_mode=False) + print(info) + return info + else: + wsi_reader.slide_info( + output_dir=output_dir, output_name=file_name + ".yaml" + ) + return os.path.join(output_dir, file_name + ".yaml") From 045ef8395f99e750e3f43f974623a68cbc3d7bb5 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 19:51:53 +0100 Subject: [PATCH 19/69] BUG: Fix Travis error for multiprocessing Add p.close() in multiproc.py for multiprocessing pool to avoid error in python 3.8 --- tiatoolbox/decorators/multiproc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index de3a5e3e7..2075d8903 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -51,6 +51,7 @@ def func_wrap(*args, **kwargs): with Pool(self.workers) as p: results = p.map(partial(func, **kwargs), iter_value,) + p.close() return results From a24e4a503756b251bc268df7197546d5d35c9f6a Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 20:08:43 +0100 Subject: [PATCH 20/69] BUG: Fix Multiprocessing bug for python 3.8 restart pool after it's terminated otherwise it won't allow following processes to run. --- tests/test_tiatoolbox.py | 9 ++++----- tiatoolbox/decorators/multiproc.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 1cf515f04..32c8bea4b 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -49,9 +49,7 @@ def test_content(response_ndpi, response_svs): files_all = misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, ) - _ = slide_info(input_path=files_all, - # workers=2, - mode="save") + _ = slide_info(input_path=files_all, workers=2, mode="save") def test_command_line_help_interface(): @@ -77,8 +75,9 @@ def test_command_line_slide_info(response_ndpi, response_svs): '"*.ndpi, *.svs"', "--mode", "show", - # "--workers", - # "2", + "--workers", + "2", ], ) + assert slide_info_result.exit_code == 0 diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 2075d8903..ccda392f6 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -51,7 +51,8 @@ def func_wrap(*args, **kwargs): with Pool(self.workers) as p: results = p.map(partial(func, **kwargs), iter_value,) - p.close() + p.terminate() + p.restart() return results From e837fb71278d7656a6e37505a0daa063d4796b57 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 20:20:10 +0100 Subject: [PATCH 21/69] BUG: Fix Multiprocessing bug for python 3.8 clear pool after run in multiproc.py --- tiatoolbox/decorators/multiproc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index ccda392f6..281f65d04 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -51,8 +51,7 @@ def func_wrap(*args, **kwargs): with Pool(self.workers) as p: results = p.map(partial(func, **kwargs), iter_value,) - p.terminate() - p.restart() + p.clear() return results From abfa632d52ce0e81c5effa8b53f9399e479b3196 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 2 Jun 2020 23:21:21 +0100 Subject: [PATCH 22/69] DOC: Update docstring test_tiatoolbox.py Update docstring for pytest file --- tests/test_tiatoolbox.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 9be9dfe82..67421639e 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -15,9 +15,9 @@ @pytest.fixture def response_ndpi(): - """Sample pytest fixture for ndpi images. - - See more at: http://doc.pytest.org/en/latest/fixture.html + """ + Sample pytest fixture for ndpi images + Download ndpi image for pytest """ if not os.path.isfile("./CMU-1.ndpi"): r = requests.get( @@ -29,10 +29,10 @@ def response_ndpi(): @pytest.fixture def response_svs(): - """Sample pytest fixture for svs images. - - See more at: http://doc.pytest.org/en/latest/fixture.html - """ + """ + Sample pytest fixture for svs images + Download ndpi image for pytest + """ if not os.path.isfile("./CMU-1.svs"): r = requests.get( "http://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1.svs" @@ -41,8 +41,10 @@ def response_svs(): f.write(r.content) -def test_content(response_ndpi, response_svs): - """Sample pytest test function with the pytest fixture as an argument.""" +def test_slide_info(response_ndpi, response_svs): + """ + pytest for slide_info as a python function + """ # from bs4 import BeautifulSoup # assert 'GitHub' in BeautifulSoup(response.content).title.string file_types = ("*.ndpi", "*.svs", "*.mrxs") @@ -53,7 +55,9 @@ def test_content(response_ndpi, response_svs): def test_command_line_help_interface(): - """Test the CLI.""" + """ + Test the CLI help + """ runner = CliRunner() result = runner.invoke(cli.main) assert result.exit_code == 0 @@ -63,7 +67,9 @@ def test_command_line_help_interface(): def test_command_line_slide_info(response_ndpi, response_svs): - """Test the Slide infor CLI.""" + """ + Test the Slide information CLI. + """ runner = CliRunner() slide_info_result = runner.invoke( cli.main, @@ -80,4 +86,4 @@ def test_command_line_slide_info(response_ndpi, response_svs): ], ) - assert slide_info_result.exit_code == 0 \ No newline at end of file + assert slide_info_result.exit_code == 0 From eabf0910f540ea2f0003acf534aaaacad478599b Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 3 Jun 2020 01:55:02 +0100 Subject: [PATCH 23/69] DOC: Update README Update README.rst to add instructions to use the toolbox. --- README.rst | 51 ++++++++++++++++++++++++++-------- docs/tialab_logo.png | Bin 0 -> 55060 bytes docs/tiatoolbox_Structure.jpg | Bin 0 -> 50034 bytes 3 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 docs/tialab_logo.png create mode 100644 docs/tiatoolbox_Structure.jpg diff --git a/README.rst b/README.rst index e2c04b5a8..f9dfb2b4c 100644 --- a/README.rst +++ b/README.rst @@ -1,25 +1,52 @@ -=========== -TIA Toolbox -=========== +.. raw:: html +

+ +

+tiatoolbox-private +================== +Computational Pathology Toolbox developed by TIA Lab +Please try +:: -Computational pathology toolbox developed by TIA Lab. + python -m tiatoolbox -h +Getting Started +=============== +First, install OpenSlide `here `__. For +Windows, extract the OpenSlide binaries at +*C:\\tools\\openslide\\openslide-win64-20171122*. Then, create and +activate the conda environment: -Features --------- +:: -* TODO + conda env create --name tiatoolbox --file requirements.conda.yml + conda activate tiatoolbox -Credits -------- +python tiatoolbox.py -h +======================= -This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. +:: + + usage: tiatoolbox.py [-h] [--version] [--verbose VERBOSE] + {read_region,generate_tiles,extract_patches,merge_patches,slide_info} + ... + + positional arguments: + {read_region,generate_tiles,extract_patches,merge_patches,slide_info} + read_region usage: python tiatoolbox.py read_region -h + generate_tiles usage: python tiatoolbox.py generate_tiles -h + extract_patches usage: python tiatoolbox.py extract_patches -h + merge_patches usage: python tiatoolbox.py merge_patches -h + slide_info usage: python tiatoolbox.py slide_info -h + + optional arguments: + -h, --help show this help message and exit + --version show program's version number and exit + --verbose VERBOSE -.. _Cookiecutter: https://github.com/audreyr/cookiecutter -.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage diff --git a/docs/tialab_logo.png b/docs/tialab_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..79fdc319c685650b70dabce64e9cfc5c52a2b2f0 GIT binary patch literal 55060 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

)@P$}1~)!*#aD}H`f z|5{mI(-7T(756HvtDM9s#yY2|9e&9Iws=)}gm%yPmLFklOoYBw4>?NJfw0@lIFNEU zkiD~uvCY`|RY)KJ#!2_@Tcx(GOJb$UgJjg;Zn(gJ(fBN z;tbKCAp;|iq5TC?>D)A2aUalIzUK6qKc0X4iC7R_m3g34I<)N}`(j~T;L*hHn@uF^K#?Sofy_CUkoyv>D4lfkLN*Dw8(i`8wwt zKD=aJU6k`hFc6i$x0Ocu&j?4$P3X*E5*sud{=tA{1Z(~(PAl_V*dv`e1ZCU_^VGXp zA|g84XIX7f<|`D!`yLNoyT6Yd(Rpui*a6*z&atIOlt0^5oG;!ov*E8w zE#vx)mOiZa=tM0`Ia+2C$oWf!-;uHMb1wJ}TIWr}EDQP>{9QahEE+t~l`^SvH-3k= zBtk|RgmV$z>p1P7ZVe!@;>dom&%c(?R>vzM46x75EzqrNh1)#-I_SE^-&8;Nm)dL! zO)0?$YVY%2!R@se^qIIGT?iOQ`yOh_xScf`!^rb%u$6ZEqua_)Vz!bPP)>b4E?6e5 zMICdh&gXSQ)fhO<(R=qIK`s|#vhr#gx(FJ@JL zfE&^45v#V@ZbH*k`qkHa4Td8t@{`?emRdh5gbHpbA|nzH8mj_ADmtvpA#fPnLM3WS)qwql+DzL0d7dR z8_q7pIA*BPHDz&w_AE{K1ofy{ThnLhdt@oI;h7wH&Sjdcz;Ed~fl6CeRR6_aQ=hQm ze1fs|9*?nVRvO9{E(8-$5|W|f|QfBu^`p3vq1J)+EG3G1ZkF&~5l z3wF!Q%~_pS=z26a-*Ls!<;*TbGh=;-PWQ`&ElB-qH|rcbMas8b?$y%CfZ~=Px~hy{ zgaS|VWSw3AlCsq}h{t1B+9TQr{Jn}Jo%~xKvPUuXJO&?BIy@cmlQ5Vs%Bg7leDAYP zrQOUYV*#ouJu#t(E=UGyo5?MdSHEILSUVF<#V5g(XA|{602U({>y{%fk*q{(^V;^M zZdfQ=nbr1V_XF$6N}wro8XndSpRBN!HU(s_M7gC6)uOJF&6lz|W0X7CcIm#M-ALHy zY3kU~ce=#Q=~RW9aN~y5xiRBbl!gZ%{b#sqRdh2bt6-T1@n=(7p8>J)6<&t}!Tjsk zNrsvF`__WZ2ZUY~#KSul@9(VgYZ=-4qK-P`fb2Gvu!0${Uh(1-V8`Tq$9{W*3`ULTIwS0}|kk5dpe%YNksmtO1k1L;t<0?FSDIdo-KYIvkE6%5Zq} zt_VObTqBPBp?9E?Z&(^|1^L#{T?0#l`?f?c8SR{%UjW=3h-d4`e4NGpiWqgxy#aCUG zxvTN3M7}#e<>{iyhGZkTGOGrt&IkCDcFR=6^*7cL+Y6_aL8!A{Y%KmeNrGUQC0ndJ z`%!wkx@UFt?W-VcU>j6iJ)4Rz9nF49e)!EC5Bp4ap6<(+2{1}ZK0H2{{Zt5f2!|T9 z8QrkaS!Gx#T?JHhw;mh2`uvI3B_6k$b%zs(`I}uqt)FL8XhCGS#<-C+^C@<(h*q7O zG*%n-33CmV`Ke`;V&*hizaWN{x6z@MtY@Vra~2uE1Ag|0NP+97u*K&%qPA}sXtHTr%pR1S74v}9;sK~O_zlq{6vB>wci}|u3FVjuWyqEpC+kC7OIe|1arhy zmh7Ai)UN(Z!y;m59Dr1yp2Ld>p}Ayu=^2pErk2>tT3tgWRwtLfsd7`O@r-&PoK>=$ z*6*y0uU?c*ldBAWL1UbYfV_wgTq$L!?x(l1c0MsMWGAZcCWT@8$mx?l$wN-Y1>BU( zK|bnP)~A9rWA`{c$Wp?r1#c6IwQJKDE{LxfJN6pHj(!hlD}?^@lL? z5T^#qQFbX1;yVs5mX0|shn|Tb&#Mofz$;|-6A1Y8P_(&zqCUd*!-j2oPTm%Ku~G$B zYIVtD;7IK_vQ_I9mzJACXuwe#(pyxF0(i$px6?shU5v+8=~``v0Pe*`XqJRkpjZ@` zLrRuLJuwq0<$)T5;EfA%AaYh+2kH?tQ+M6Jehdx)bLDQl@HCa2LO z1#0{6r5PCNoR7j(SnUM%V6CF`zPx13OlhOg-4Mf3an z@XFuA?_9UL63a}zD)U#i7EqAn54aF6LkFR&$|E z#STz4q1m=lx|dvgTnvY$;{t7%b1PgD$W!1RW6TihxTd^CO_jH(ht!&wbv16qnzK#}O84>E@?cjb#sitr(OGs`yQwxdIzQO$=WA`@_KW z$W3F#sElERv7Du+IiuCow%(LILPO&(v++5iW3~Xwf;XpH2>6K=#tbmZd|L!|Kuj^y z>xT7qeB&Pf>%r&Jr-SZeEo*Op;-pmf)s0h1MxZXP3b`g!vnI(mr*!{DA(YLp3)k(!Sv~@hlDyY#RDV~zV64d#D<8MP&TA7E5B#0WF`6|}! zWvuVhPFXB4IU72AtpW>9`W@WFU=hx7-13DismEV+Q3eT`le$Dy7Yy{B({$6^^LBqP z|A9^SbbO;G-&y`qs8wz=Q;OdN-Hcnve2|Bul&!6=kfS6W?WCSj37-A+#+_$@@fkvf z-Oj!)LZ#h=y`TwFH7{|ls-gDMEOC2_$Nwp@d3G>tkeyA$u1QCe5TtS7ae>@wH9gUd0Nf^C<8NU*MhfQq9W6LKqZV}bPgJimXK|xqB7J4 z4o*!qyXs)MPRZGPpHUsd9G9Re(5y;3nLYhjZCY@}8p2P~Ku~^-!#;U!ItZ*Rh4B%J zYF0%M@-Pfi+lES10!(Hx*k6RV*8JFID_tEOA9bLB{}i@IQ-|9x%mO?uE$x@~N<5<+ z%}%KKm+5CJ(W)&`TCI8hDYL)4j1BV#G9#6icB?KLE&4?=tW{48Uf2k6TDvPD|0=gqycDil4 zg`0WQh-YohU1kWUmE^uKtku@qz5eu}OxyV|&Va&(f9Am|y%(=%b;e@J2 zYaqPJ4Q^h_JZ4V7CYVCODXrlsd43blnBrIAe~2!Oq7Bc=Xk1Rz7p`fW|6SH~3nCZ1 zRX$nsm_|bY8zc`bn1nvj;K4J{ygCFVyUr{o?ZGDGUDP93(l>>Ql1!9{8D3_gSrS-} zrl@uFo;JLkPP*CE;QzhFI|iYmAya0!3$nop_!ng^N+WiKhH=VcJWcs9On&ddB&q|A zb&3yR?bE>>$4FTH5Q4;zNoHd0%_iWQvTIN0=V0l!RAlRY*39{Et^juzA(;9B2MnEF_UtAzIz9vE*w?aBal)4eun$+^Y%9k1PzZ$@Q56-h)+>I9Q zn8`n9|IATT3m!0sXR}62RJ$Yi)P#8ctc5*D4Bg)c>xUgNqD?@4jH1kxDnKaj#^7FE zuYJ#81$;MzuhPM9(C+&RzYEt}?WGWX_IU2Pa(BpZ;?Xuxv$Du|3z=di_gnwYTCoE} z)tjnib7o>?(^G$vykOIpmL`l&9@;rNt$!b@*D?Rh!BD^6+F=w{d1}H(%{IyNS^^Lo zOU8mJqEftShm0{hx%?)?{gfe7zLRKIl@dATPwiCgZ;|2Kx9aW6mN7hV)U4S&Uc4`V zS0u;f31?|T`{)cf|>zlFErBu~r))Xow z0!M1sz-EW?Ih~eS=#I{r;Eu)-N4*WZdKmLR+*4!*U!6v)O`ntf+Kc#3P8jQg zM;z)5cP`Ub5TTJzwm<7YMHtLY$7YOGylcd~D*`{(JoF}=^pXC7XkZGSXbw1F3ecv9 z-5vlq<*xBys$d@o)f86RzWkX7TCTgDOIH`K)qP0stx%Dty^Ohb1Z1bZm3@6iaFL_~$4LcL z)=4OSeEwU=$Ln|AnzI<42hd5Gg;E>>C|l^#(OlYf7Pw-?p|xU@YIAufQTFjXp+zH= zS14v)*D6kTUr^t0`^MO)k7G&yHY-bKF8$jBr7bBOYx~e~*ob?BLRXYiYKuKi~xn z;nBiWJcT5e%o#$Z1T~Cr4~#48$QRXpob-mJJYBXZo)-QX=nTHHQBmSs<9m0s1Nl>F zPGpu#tNIcb{MHXXmjZaxrpwE5sxFh}?46o}C}BTM*#9ceAAF{XWTrT-q>7MR?l6_m zGKZ8D$M}Cj>)2CWt&=O+#w{GWKY=(-sh{p};_To=!mWjyIF2gCXj3-FU!3E_)zlyy zb1Xr}a!{Iz(3U~F0c`+Ia@i)sgvvIxCZKK2X@TGMQ4mREB?6CrO%VL6VZ)sFF&Az^ zMpSg@%M+jJ%AdfwveMcLO8I4xkNc{xR6k1MQ2=OAl`vsCdn=+a<-|BM!%< zS=BwF)bA;cw2N}|c|h7>rXsRZ#|ibR;_pIcqd?HLvdbtp+?6UUPr`wgs^0s>WN5(5 zEb@EF^V6#J<5MgK*(TzLN$udKakIur_%AyEBCC0NBY z^9NB43WZ7^aUfJHM9x_M)+DXDUY{F*G?mS8Wc#M^p)53qm4Ezf>S7Gm)E2@^2(c^4 zFyUN*KJA(a-R|W#uu$Th$0QnyR$gD5#s*yAqLUx~$#)d+_ym=YW_+54*#xWJ4*r2q zf;~q7LcR;6bUrmW-5xc?AFXik;WIx&WO=b9|Gn=RN#82G;5(LNqyC6vQKw(l217q5EIg$-p@x(fMerGWk(2SI{`KI z-Bx3ZhLJP{p1&x0bn9J-_h}Xm+xA;!x@15 zMhwKGn^P>sw-zn0Yr3v0qtJ!W&%|%o%Ff19o&Obrj&>^w`R(wF^4><%A77PI7YTgL*g$s#SEQZh-R{JkT!~J0PeBYaajW>t1X3`U!+KF9Zx6v zAU#`N)-?a0KqLI3%R=w|<$FXAq}MbqrgrciPHGoZp_?#r9~uC({xe(UM? zXI@BY^!TtMkZ6+%-!DX9bcKN4*Z|{q7S0q3T63wEswiIeq z9b2%Ze;^7OK5-~o{}=)NdLce>BDUjPlFnN&y;^I*`RpM1zfA9oO{wWfzNR6*M0L= z0c=0yG4b`JmOSDH%EzY^o1;{S6BV=u`h)Jn{o2M_8XpwDRjDGPx-Tt{LCv|a)A)V= zNp{2h{cg#Qz5l)F7v;;@@YtY-Zi(naXxIP*o;N1 zvc^G7w?L%&>|Ik|^Xs`gx8FXueZ@aWbMrOX8<#HwK8olb{4^~XBU>Ag&qAZk^{=?= z&u=P%0q~i&XerwqwVM55|`eAJ&cV-J|YwvBk0q? z(ptE(Y+1IlkJg@w@z_7Y%-_v+0-rZ2%equ8;7@E(@~CDmx>x}H;+Y*I!EUQ2#SKeuGMS8r;P|2-+4BX^|K8@ZKC)g!^b$sXz6L`#V9^;_ z{6om?E3Qn|(`0BqQOin8`O@=7^RVScQ=evqc0V7jTyfg|k9@hz>soFX_`+2vg)XN} zwttVKg=rO~pdfw{_Dn`7SF>(_rqn#J`nsZ(Wt7*jKN4#6yJ^(Yp8)}jwdl8wxgC}G z5*_nq@ivzmN`Q*%3s%L@Zp4hDV&f!Q&D6bs^%^q2^&8B2EtL9uM!qJ4TUypIZou(Oj^wvU z6;`?=GOBMkbj;2^7V5F;`sp*{3L;43>Wq_v3Z=;Dobx~9*`YZLs&Vg=TgCKd9ldU+ zEVR!_Pi;GVg7@TeQlP4=X`Ml(ZdP~X38=dHJ$Ic%Z1Hr<%9d6dFmhoE%RxDcwW*XiY!-NTK)syQVgC1wPuj$uL*LLJFZvKccxh zV7nQ9w&s|!(mRTmn5zUC8Xs?=ep5F6(?t;MPj)6@V{YYZ89Tvo^=?5Qh)7@UF3;3f zO5Y;hS*PnX4b0doi`Ubkl)Y-|>5#X>Na8-d%EZq;6%%VS{0qv!YQaI=0OnMQbDGYP zG=}LoFS^a>D#GXBh;|~u#K8N4QDR+@#P__j_c|1gciR(Zfn-iv={h@ z3pyW(zQ=DBSxS+Z(J`F6a#}M|i)Z^iWf|W~YTl>DhvaIEfp6^;o@U9t&=0;$Bc5~E zm0gF&v@fxrn9=diaOY-RS;3wIp9*s(|6q@;pjBPhb^qk3_(z8p#(Ek3nfGxm$K@jo z-BKOuq*s5O(i6_kfyycA@w^Cn0)26=o}IDchO82a~IsK+`(Z z=M_*2?#ChGc z4oExC@uQ9Ve-J^$D4!@o1Q4&sx}|D+l4F%^4rv(q|&z{AU0r~sRmHiIg8f~ z^W;O&E0epn6@S_8$MAWH(P5x@2?Img%tYQQzl&j*%}LudNd06NSzw9^tH=S z8$3ujd!vq}!0%w7JVxNStQ0CBlPaW_8%x32-r_V5Hnz;>48Qul^8*Y?k{v{t6C)y1 z9~2cKPM1Y@5xcC8=Shu1cH$yFckSJ#>Gs_0iZcrS#65QUQV4Ieb()>AwD` z(KpO$9B#H9u~RoL*8G^0<$FRJ9Q^RDKdhzH*^)kl?X~ehi2YyYgqY}^{IugglLSc> z>xaZa2LH!TlQ-? zh3M!dG!u6MXv;KQ>^f^xgJg#~%yZE$we!w@OgxFIwkig6Iv^MZz-UG#6_y!P(pRE1 z-ZLPu=t^rJ;dDjAtt1DQm3LI~VBMXyjIclAs`!UqEzm;8*IU=Fb&{fVWN4Tp*haQh zl=q)PI^m@OMJc@VxcU~i8p$xXKescW^A9_{n#0_&2@1*1?LIT2e=Aet$VB`JG+dj3 z?1l=}h7j>8dD-ZjB`MP6i*8~CQUMq;$|ygBvgXl-cqBBIrM}0-7_n>L<98M$UhiFV z((tb~Zom&*KmbyYMgTtf3i4iQzrFU@a-u*72$0+632;nIYwZb!;K9cqbRw4~wyrBm zUC0I7Hux4V6=T=XxlytXst#AW{%JC~`L@!@J{>c`$ZoB;`b+V}ZPb|bHD<#+bL%LB z2Vimz6l%ZXFjcx8p!yZJiDe@xF+FHzYwma`PC|SYoc%ai%_3)21OgUUfD3|Q83X!3 zZN5Sbsm}#oL5OO@`l?>!KHX#AI6pQ)!>LDuy2vSWfCB--1{K?SEV0=#|RO z&a4(R{o(eG#Ze<6{$943mQa$XRQ#1~HiC`-ZnD3YOEZ%23|U5AxKNrDmyzdgO&U>Q z9Ki??BJVfr5bVtx&io=_eU?`|w_!hWQNISd^)C&)^(fGK1ShN0nx<~vMzWUI2$y_4 zQJmNQXW&KQly&&b`S}*v6@HQ(QlL=4zW+msuluio%$go#zZv=WTRm(0kBKt10#yo( zouxQXxxtgv#kZB`1x}5(;ghl*kIKeA^^WP|B_MGdNuZ%9&mQ6eQh^KPve2(p@MD&}wX_V!lmQ)z?8sWLjA zP`mtHSbk8Lu@U{fI^@b|zLhwU#^k7KdXh^}?T@f%@*Dv{`~bz_Q^KDRT@X@Xm2iQ( zG4AUB$=qdfL6Au(#h6D}(s(9Vr+-7bh|O(LMHe!SLR{SJ;kCJi@uFW`Hw)ttkN82F zu_r~wjV4|FVb@`F^At53I&A$~?DkaN_DoE{{X*e2He8l03zxSxK%lZ|tVxasM zE#<(mSt;jM?@iz;z;wS;@Pn7B!peocberH1)r)8vj9P>r{~)^)r578b_MTMKEfaDk zSBq~^)nyu`e__DdMlqh1K}(N&s&)UN*6}QJ1^It?ddsjjyI^e_cXtX!io3fN*P_LW zySoL4V#Pvnx8lX!y|{$pQe1+Q;LgXh_q)F%|L$Koa!+Q~%$(P?Y%Jxqx?^LxXh)P+ zXC}gp?VBtakhJ!qb4PK>qtNN_}^K;zE4izvdTutqbJ)*Q|!VNV&7Z>x1xVZ>O_jbZV9lq8|-*Jd>ME~;5O;J&t;ku#}i-8JKhH=lxUCNe=!ch#2%rjkd%k< z@$&85w*02BSezJ0>CRGb-;tf~B{waHj*&VN__cqSL@svS$Z(re6x)Sa>zXS^-ljXy zLCl;{PHfW+v(_^m^mv|ORhX}X$DZ8n<(I}zhyQZ^J+JpRY|56ri0kC_9}PbQd9!ZS zf+p?T0$j#%W~}rFr~7Jf5qP_=0qZPCKbh`_w&-Iba@7+$wgD`dxjSbFW}%Lp07f$& z^xktk?1e0;gOjuK7DI3v+)x>jo`-z)$LAf&_6(#GFC>ojM4|*UPYI7vu`@aNOG6MUv8`JO{7$fS1GChM;1)is^Y)E&0MQKU zO>VcB^#`pKh0nUyyKu2zw+n{S_}i0KLCsXe-(V~HZcTeJ#hcdUD3_g}`MAfBm~Wpx z7nYfqVx}!F&&Z#*7DlD%PkE75Lz4)JnkH`I#4BU9;&CsTkmjz2^7YQuC>kSK3^GB8 z7%K#ypLarm+6z|qD4udHymldm9I|{Y+hfG7-vqK;pH5o)Ob;R+#O$tM?oKXOD|R#A zvLyanPDmL0DbbPli6IrFxT#rd8ydRetz6}TR{Dcqc^rC7lS_C+a2~eF_rAJo?|te@ zYMaz!SbAA(vd#VYFiWXFwG-d+93zs`LJ%_t#cCUsa`Uz;q-5~^XCvQx1x{PJ2}${; z%*~|NCz{b`nN#4R-$!kY%-VVZW7TWAF2U>2y~B~w_(B-F#7-ZBTY#p%!0*E8a8&m^ zgZl}GZ-&G^^K=fYZ+LAn^U-Ldtm4Ye?U$?3#D|zeHTU1Eztq!U#Q6xSjBTaknp0zE zPQS4Vplkl7?!!|b|Cb%5yFvkTc!}gd)l#Rh@2iN47h-O*_$dBHL-vfK97u7|D*TEF z+O{e{qVg^Wn=YF_xl%kX^6>xbU6BT65-{HeZkGasNi-vB>-UT#diq9v8 z(%pn8FGyL|MNVDT2#;>aza9ON0BG~(D}E=>8=X!gOTlxqxoY@-LaUb#oxf;BEwHtR zz0!>CPL+xcGQ{4O6EA#RQIBt5Xa50TewJN}6-Wx(OF21{68IyQSvf8*T;OmR+AZD#wPD9EG zyMDxjOH&z!OMhv>IYP};YPv*H#09!Zhal?Hq8Zw3bzO1ae3SJU)*_|4ZQDBgr^p+fc(K^ z5i4$mf%0e%g~;RRQhOK^ju~rQnrYf#|$j7|Z{h0wsDv)opf@sxrTwLfDw*Ob>E@0n$08^ZF{HoyZszCQYyJR77bfs@w z-RmD}l}P>dr7oj_QCY+CN@tap(S~sT-v&?-$NiT6FL!7q46{WL0@S3GA$%XBsDWR^8Imy1JX^P#hqMj~N87Crh{Fm!C|6&uH zjEU0K%E^I>au#BCm651|4te<-Rcg;86HKH4aMLH+^h>lh$cHglno!Mmg9W zb*4vr7@|@7h@+GNe^*0i$E!hBKY+7VekPN>{#ikyF>v!W7{xp|#GT9Gn@2NW?ThRL z_PIQb^co@NVaKQ#@cW@GJPX#q!dEAiPQe4qOn3~*=G0$N7+_t8e-Jq(E~?O<#wf#; zs{RM&Z)myEy))h;S2O$eY|Y3}{REn{YKZ=g|3rHX6>NG*>ou$476B z3HU4JVx7Nwx=14vplK-Aefou6LM$-&oxQQfk_krUNx!BOMYet?pWWGF)sKblo8nJ} z6{+sE_i)W@i9DH!BiO%LGq=yksXZ?vC7ikr(FR!nP;V~$2Y|u5PK)!ilP&g9b}r%A zUO8w6ndWw7#qB6C4ehx)b8UI;3mj|p@^7h=xGLS~j{VKO6W;cd3-RQp&Mv5?bkb!Z zGP{HTswx%?&9*YKRbp+Fj2~=G*RHTOxoUb}P6)9an-EIOt+cB%3S>>tPi3@xG_n^0 z4@w+`!&==%4jh}5h=?lWLnu;Nt`&g>s#;5dgfO68woogqea8&qf(VyxSg+jdy}O`u zuF8jH_R8MUzYSwgZp-h8}wGHxi#MT<~tqrES!L(eu+{1Jv6xs7;*5 z#FWhnSAtn9GlKtXQXYZmz1W$?PAG>E*>6xMnbYw8V7B}03HwDwho-eD+B zp}*{_yy*%&g5n{06XM&$x5{?Q`oa1{cq9Jj*ZgnxyHaVmyXpvn-gc+7;}H3GVENtqYF=wMzg7$pxp;pFK>=nH5={e$RL5ca{i9xMK1Iv`7L7x%EEUx4eNJ>b-U$PsdJ7u^SEaES%>E8o^Q&cfGVvjg}e~c^#=x zk?EAm2~q2*0&V1z0Rz1PyY^zf>Yzk6zx{Rom{MWxx`ZB9c@bk(~r8tAN>pBUfZ zI~d+s=Y(Q?^ZxlT!x~Tg!Pbk3s{UW#4&s5*6|22se%&P1)|zL9SJzf-3q+_FF~gpM z@Tav{!Y{sdiBV=gyLwG)Yk>r`Yt%IYeMX)1<{_8vjc*sPgeSc~D;g0sJ+sZap$`3^ zS(vUSSljBZINqzIeglO7%BzmG$0~pS1i}rkM7F2L?5d+jzDYAB2PEFkXm`(xpt$)4 zl+W-rf*c#4g69*UGS(5rLdw-ngrDv6_#+p|LtK1ef^91&FR5$UXq2S@C4u@k{cuk0jVkYv@WYz?1KTb9-%#>VFk}mj^TzxXGlFD9lUg7EsNs{8)ZYNg-gThN0a(f+|NFM^peJH;|k4D-g_CR+3$3< z&5PQ#t)+yz2I`&TzB$8(`HP3l6<_2Idn#XlwN+Lts1F?@0#cNTs%~_?Ga2@b`vten zahq(|XL3+9p7u+AgK4@iODAs;YTdiixA0qwSiEhBt+u;DOR;MbniA-&A930v;ff2M zw;RLcu9)ye=_%3xg7PE9@@qh|c!XpbWuLQWz{$I?^x2uUFQgj_wU*KGpT72^-R~*b$cJC#V=Rj~UsvK$DyEe?a^_u2+ZzjY*6!)1QYQpI?Nyk!~vVerc z?ovMDYqvDGzl)l(L&n5~mQi?jxP{z}wHSC^< z$^6pu>%oJXx`O0pafLM>SBIb#y75p$ICda$TN*9KsNz=l~VzzSaFDkN7k2PcM0G)1fEdUVSF1ZC6q zTh={mxrm>IiJzQ8r>RiQgp%qP!m@yOLi@d)tQ>iU&T3`#!G>$QNd~ROcoCUQl>Vc6 z8vFG4MX`PMD(N()EGaY#GnG^2&oOMPh2WZ@EYo07$suI&uQ#7GQDg;EmtuO4=)_t|>`6FCss`woSfoqX# z2WQo7KU;5btcwzp{<|Og@uDC#=3bf(CWs+9$52iv#p6>ZY`FSBKI(x%^&_hNw-_&& zueu6h1u30Wa9e=S>%yBXJif* zs9!KkO;G5Fb}(Ol#q+y=HwuMuC%#Hg$49=@7T7cPVl+CXegC@R-cl$+ybs;Tpyxs#(>Reg1vsf5Y~a z1Qz*kxO&kWGRGT4Jfi!F096h_$iXPEqS}9Ob9pP&d1AiC>qANtFh|s|EX8qJ;ae9a zQIh`Bjop)8lY{H+&s(xkDUE$mq ziebp{dD*Ye#OzEam|{vA>;7@Ee@1daFR=BrWDHBq{$lv%PDK|JNRX z6IIDnvChnIlIyPRby#3^XQ_KC(z`KC*&Y4?^VGj01gYePIY(rBgxEpZqoSGL&$N@R z^GK}aa(qpCUG|-V_+0NaM3{E?zLNi`ESeTG7OLC~6Vj*c+h2Mhul3;fOFo0or7bn6 z@ee|sml+b1ZuoO~rpLh{M#l$Ak6uY>Q!kEooZcf|ahbziQ9ZkOze1d{PnNSQUls3^ zE;^e_PqVYy%KMKw^bURt86T5EfwT8J=+(E?p!#cUf2k2V#CzS&5}EN2b08TGJHAya zSe{zg(st=UqE5G5zD?I?1Meb=i9gj1+v%$RWjac%!@K{nbJ+@+j86&*!HyCHa@1!b zdasbZEvNOI8}YaKGr-|Z)p}zNsQJed1;?n)R*1-!gj^WrWQJb499NnlI`x zIi3O!=*XaDi9Sw@|EzASP3i`|)du8aVskT)u%b$?hZfnYCJ(P-PK=VurNTQ5X4-Q| zItl2lbr3u7p64T#xE*-+I*e_8*TOW=w(>j}{X8j@V)=`-u>1b>w%tmD2h6k8d zcG9Yfmj_JMdWW1hO_^GSgtKT0m|7~u&W2X+V>#+Nd`#?*x=+Khx3?Quy3!O0(IWtd z9~Rz4u}vEoSM_c|#6=^^3x_2q)h|GYdoxq>C6}KJ$9V-Ggas%+!e1SMhhRlt6FGr)6jyCCYFoYl-V&@S--5EYHwHYEv}B}<|w#r3hl+8RUfkqV9Ap*+a5`sui*Wpo;T?xw!}j@7JkN z>c2+xsZv$^5o(rUQTHW*yq3`P;Gxx;V=C@>#sua7JG{~M68>|cK4Raq7_o5%wL9|Q z_C?HA6BduMqDlv+{wNcNihVz>9sQ1UhRhjQhwAv9Rvgpbix;eI+P5POWpc8nJt_Kz zllxqpC*3dMXXVsOcI0K3{K6Ua1$=fDONQo{0?zc~gm?R6tYFUH3b9^exQuyE?^03Q@cpGZ{qPBEJyE@;3_%dsiM-D!_Ia9!i6u#8>_5W- zV$;|fBXr+<47~Q0tDT{;ha4yhA^hjwOo{g;N%-X`=CnRAiFswcR%_zEv3&E5HVoF| zRQ~g*w_W(RczyOrFr|6kY1{PsI2}2{_2?l8P`=3p=hhaMw;7%F`zSz8OB=+Oc^6HU z@6_XyM1Z%S>Ul1i_|UAI2()#xarq4}W4^SVvw)3aABgq#@kfUiG|5UDa2TT&vq_s^ z6T$2mqElHS^}fDaa$$RWYF>1mV$RFtm4DT6J|(6>Ktfozr}!{RL0-2JmDlq8B2wj= zu^z_R&v4zA!y|C*TeBh;#2cF_2iQP!=z5h>u41ogA3`AC71(mS&bc?sTL^nswCG6W zc~p9+-u)yclsw|6O3!Avavx;y{Bd@J5jnbtL)s-b-YpDh)QcD@3?d?+40RFppudX-RxCA!QXv*B>*n)Ns&{~BElw*+^n8sNrzGH?$L-k)V^37|16t`Y? zn*ZciLH`*>2i+f5eC>yw@>E(wt-}>oQ;E2?=xhMsgr<0+^w)?h$W;7h*HBxHk*S3@ zgN4p3mYN2BZF$P{KkL(Vk%o^nR}P~~`Ut~J(VxK;3R_kv{4=GUbT1xvj8VWnoTPzK z#NZWe>i!FnGMj7Rb9f8hB-rYEepW4T+soiH&)MTYY{x@32}@W4+>AV3>;zYs9VpXn zeO`VeC!`^1hqNvs#A*(5qbNnvQ@4;6bRk#S>rMC>`7PFZHxrOG*icAZ0oa2oBfdz* z5WUqOKkm1w2p54l3Uo;keT7XZ*GW8{$L#)7&&^GnZ}yQOho{4dOa^nWDfQVc;SS*> zwS!3aaRz4f4N;4W_;H&$`uOyuJM`e%71u*E;Rh2RsYFbJId@klxrMm=;<{awl%6SrdR_NhmR7DFzofn-v6MaesXv#doapeo`Sys5K!?hB2LA(_ zBSAfcf>hfF2o#&EPg+8=U!^@LlKzg#92MqJaatWG6IbwzcQ{Ded!dd$57r~mq7a;8 z$9ooEk)8-c!ZFW$|DYMIvF3`0m#Y~tH;-z*yPsMQS+`iOnSY7p_*2<7XlHq-)1;5@ z*C_YBqk}z1tAt#_m(^`X5xC%7DP$aE4nG-nE^IBG@UafAb9fKSEa~Gs0)xm{6F!nV z+tqY>ZCz>X56z~SK&zW&{duybY~Y|M_4;JRk8}c02|S?LOmqktmVpsgQ`?!!!+91* zQjiDS^6@vclY5u<$=ZJj&#>*_WHJ{%DJscPl|6DmLse3BIU*hC%}9$_!#w^{p4VwG zl%CvY=44k12g8`G9&W(8pGgGTp9dB z|Dfnj2%n!{?Vqb@msd^u1~c`+8QxIrq;;oo$>6XKM{#^Qk1wtY@4Y4$^!Xp2+czzU zR@;LLZV8HpWiiq_>~i0DEJSrm5JHq}Q6|00IP&fvGVSzVE8Uu3OPTxsEadx0cRusJ zxJ71NP5u97@K`z7?*xvYv$(DV^Zzr259OKsFKjZldxZY)iAVB(0Ou6=SbqQidBnR$ z{wFiQ;Q1#2{=X;A|B>qL4~8%0qR9XIndU@};ZphSvM{7j-E(ipR&iguUy=2y{aaVn zru4*qH8Wa{?+#p4t07|Z8)Wa&5u`hpaC9ucn|UOtC35w5SS4^K@Ww)XHD;KHvWfXd zDZSrJJcKVR zdzBwc9(H06WqFH&RBjBI*od2JMRRq9<*t8Od}Zj!JK;K(=er)CNHbAeh_avfqTQPn z(5!Jr@|#KbOEz-`1Gtd{SXX7pP*>0BNo_LvTD zf#m>q{Zaz18NbsalGcrW;M|4n@8YKq4%Y!?%mP|Ht3QnC zOPR^U=2jprc-6L|xZO%t2Q2SgS->nlTfAg%!Vlt-vqM8gQ1NxU+{52{qrwJ`_1NRo z^{iIq8=6NI`L7n)wKm0W(Z+vjk5<`wiEq4WZYGYVZv9Gc)uM9Lvv*zsAi<@tpft*b z6y59v(Uadl=|1rb3flUtd!-*Ss%ySxs`^m{1_s{Vw)U>`JFW6AB4f66cCKx_z22=s zAeln36t9sVu4U0v!~bZ*~IvwJdF?RjLSr~ttQ#`}V&b6Xsi*nRR;%PJquQf8}e zMeQ343;Q>qKp#B^hZDWtqRpSc{QEx)jwg%Ni~s(eh67|wzUzV@!(=W^OI-(i8n;zI z4UxMI|JuMa2a`r15Qxg5zY`xsIg!rMYVvkt()#{Q>cyu^so&oOAu1|Vrx!i&+CV7i zxeXH&v%NR{u$Abe&76*%{>$0AFF5zSl~U|v_Nh^9iDdq5gfjS_I3yH%dA3BE0iyh( zotc=3`?M?ZYGKgszP}u+blr6$dgD_Ft`V124EA79&8gGveOzdK#FW3O6p{md7JWOE zmpFc`*i5`EDt5Tp{o>-6?6lHgb=od_S1A@#D!!w8Jvo#j942Db+Y-OIIi*B5N&Ul-E5EdGYsJ2=^hDXbw6^BJpqAGJkBrrpFby_n zIr5?EXkGWtek!Nbcxoh-FL^z=P<~#zTG-4Pp4;4XDJm+m0NL_8JyYIJ!}m4(8h|f`^Bngh}Fw`BfFGufbJy5>(TP@+Gi19-P5%m*k z9x1x;`(3DmHFLCXreApt;=s=ujBcOSLfo3|($n5(D5&^cSgk3uJwn4YJI~ho zFUfEH#5kSU+hyh;`0XwjK5}Cip~HxM*EbKa6Fo2SJSfq{C@78m_;Os1rOzMC*T4siq}K5ijP zWPA|ucAWN8?xzWUoXeM5P&F7S5|NK`+YG$hAMzhdqRYJzo}z8jbn#zXb5DCo4}M5j zpPukU202=h2QxM12!6g>bLs8Ie{+O1td~*L#(Ix_{<7{l_k{^{wd-d1>ABh^&>@Bk zlWiCBENbu~)_(DPe4W0X?st%BqiOQAx}=$I>}RKOKeDEI`Vjm)5*%(VND1y;=$YGu>nW3CH?4BCM~?SGX;u`yl@l&bvyv&o8VykXG$fA=K&>N?@gg1-6R}Mq}Y-EnRvV3J^#6lAYO#L z7Gu1q1V8GHeQo)Ax8*b&j*1T#U)X*{MezxMb91D6DPG3fbyaEdGA@3V3G##;0+a+7 z(0FM@K5paJM8`NT*YyXzUgbwoM)X1=IKolpqZ#r@E{9K!;JT2BA(Rp$(!d{M_lx3v8U6<+X<7`*`CYP zT2EL$zw1M);CC@^S#Y|FFMD|`|4+dY9=ZmfVNx5<4G{c5@7D~DwL);)GA#ni07QX- zB%R=5R1mA}iFN>{?JbWAYivSj`?-A+{GmVeuj_d`IC4a*OcnF{cXbrk1R3`dyb%_I z?ss2`S`!z_r{xh9V*n0o3L-d_MfZL9H2Z|Yj<;Q77%eF$^?;vryBD|vYQMM>RV$RY zmHIBfRBf=N?|2W{-u{Q^3J!x)?Tdv~zR!1U5N({+BykPTB%u`XIme(bpN^svmOM=C za-xG?dQRDUJxyiAG3F=*C% z!t+b)!8%Ffa#`(-#e1*HDa;*hfp+EZBcZUE>Zak0z4t>Lk()P5CN3_)PitYC9^~6k z=a)T8=&Yt865BHuffxR_B^-fFqH@6R$Zdn5@JfG{cIsUxmL>`i_Sse?<=g$p?U-5+ zH^IpLuqn+RuuDUxDt7yu?Z@`Z?XLc(8ZGzz8=I(3-&fj+zap7(6i0iLiIp3)L55ES zquM~5yo*G6ileWk03U#nJhM>G1~A}ez!QT6Bj>LBBG4Noc88!QNu8*=OHCQ4Ddv;M zNZP?z|Y5rNP|IhUncQV!9?K5kn5?*&>bkcKgc)d zk!~gGfBjef>{l#1nlZQ6cl2Q&>cA{i@4~yCKy_PE5V%b;_>-55?}Wj?Hylw7TjW=g zn(oY;^rV2?%IRN(zj;37x)<{K1qhu&VK3{q3~yA{92~PbdT>h!-29$`3i{& zMWFb2gj(IjD){;U3g(|`MP4RB0Qe0t&N{w)2g4iTtg_Yw99NW`?j@_WbYAqV8Q=WY zjWCZZGY@ZHLVQ&<`K8TBgE!~Ns!scG<@#4SI0qQS+VtVlzw5h z!jA8ZFVKRV*tO(r;|OXaCRQdcC0`XnxBnY{HSw*c^PHP)02O{NwSp&fH=+LX`>i&y z5+_VGW0&DSahzwO{1R>va&rXof*$Qe_EWT^lD9AiGN38NOrGcO|KhIhP9p?krd5CX6A~6C%X0{cb z2lrpB5%|}#ghT)FL6K1j40KC$ro4Oy0u~C+%4t*vy=UqWp%{6-0w;&!pm z4;kVJOz^i{J3We3OXCc|gm6WS5lRf4tqSAwZH|P(9(4Ke*!0&{;4yG}jmMPk9y1tJ zo;^(^~IG(bEq*ov*SSf%{jfv-hP%PME_|zxa7j#`jxv!HkvVC~w zW^Iv(0bao%8&Bchjt?cqo9~jkY6r_adCyeEzaEly}SWEW7y`d{WcN&il7}&hhNVE%8j~ZY-<|&P2DA zQG}p5p$y=A4*iWAyOn`8IuGs}M3HXiHcoCpCyDbRx+n+$Xk*ck?kkCOO0y8D7OG?u zQ1^{m0R`}187T(JX;%-C)Vt!v#O{Qg1ehE8ue_yd*PJ`q7`67;UXE42HemPXKbQhw z6C^0HEh|Nq0;_*tq*SzDLaP`f&}Q>rugk)?`_UBm*^Nm-wyZkD$=k)n=0k9w%^=g% z+bAUQs-V=6emI1xiagp{WKla}FCKX*_eixCY_n)?Iw%&+cb-fJ(bH{8Q}g|S9~e4G zyV(d+$Ff8peFaC5<0+v^fr!7;GLV&*tNUoH@{H#MP%!T%L((xXq)v|Q9;8kb4`Rt! zyTarKloio*%6P8q0SD8RdlEPETGG8Zp-i%teZHG)LGNw>cvta|KgjP#=DOpox^^32fY-_@Q9R5ZNZ+Jb?utQeea!&n!jbZIn z!%^bb)SdS`v0_zY}4x?@MA|6IP&YG7Zha|SJ6447I z2AOXax@kuiCOj~61;wvtq__F6{E7jjx~1S;pS-}A6BF#%)E_9<)|Zdnl>SPTTQ>x9 z^=Bojkf>5WT@Kb5)4bT3oU5=wT9@Q5m@11nd440nh{y)6?Nk%Y0B)hdVTfm2CU6?8 zY9L`f{-oJ~{E5h3QPDU6Kcn%)6k^2)DnnWRGq2;H*d-qD1dP8HuEU zfd`e6+Z>U2g>vS84weii=NmSxSpA9ZGr~lk+FnqM1P-j=!}{?|%cRmK_+0!` z1k_#z(QoT{dwtrKMHf!krLjh{+KVpv!w6d{8p-Ze0YA_zMNM713$|*;V;3dXDw*Fl z8h6JIX#T)K7h6O1tdz>Mp&E50IN`25hK|<8WH^s#!==-N2mLB2yQ?oRrO1-6lW=o1d6`9%!9-coq-B{YR?9PR-$t|1 zAG#!74n31KRv!#w1BDggMWc>fU#OQ}|6PnZDB9_k!6a1*+086%u%|e5GgfOEw=QoI*!hhIGb-xt>G1#lb@S zLT*|2f8B0r?p`vDV^JYZAq+IHn!HRG$V@dU8z-j?iqV6jN%AAPMFV$2(6ZVikHeB7 z9VXvKCf~$vV9jnu?XT~mU#U0kGSR8heUoue(k!VT#+322h^@4LJY4Q1vcUs}CU%W} z);aD7T44I4-2bkuL0e=ngE=3Wu&FE~xNBSp<9m6pzLCr*&BOSGGs_zQmrI2o zu96p0n&j`c5`jPgX@u`wK%b+lASm}1H64kl$YXXhdN`dT`=DULE*S@imvjz>bw>EC zY3>w1|6XQ`wA3=-*)K@++rSir%HXzMGWv?%>kqzFU;dIZgujq$zkhH$k#a>Y<3nwN zx{wxW&(^RnKhVDmsF`c?{o?GGOKtl>7LNPv!l>Kx5%ECRwNR`)bSv<&s1*td_o2@s zs8VM#WPBIiNsR+2fI)evE1DZue$PKHsp$q0!VNNTdY>;R21PM_5A`@kZemjdlJaq) zSOOuJut>)52O`JM2~T4!-Fg6Gt&7Cc#-))ia+cCS=T$fFM}EZ&nkGOLL#zU7*HzwkS+i+mK7spoBW^t8+%g6wd&P z7UCpArZQUA|D6gU!e6sDSFE2fGt9`tx(oM^*)k*S1L)9`JJqDt&fjL84Ww?Zs`(v-(J%byoQXxJ18=a9284f3ZG$K+^p5-xdES9)lpt|J% z4bY<$yzd{yxHS-MI}|UlK|*mKd!H98I?2anGtR8kk|3Qqi_8uq=JK$d3McLidCj!> znjy-{H~^roj}|8*D{v)AU_|X=B4A>vRs@6tv>EyDDKo?rb`aLNJqT~)C3;f{P5+nb?Jyi64@my?AdgBQEA`^7} z;B_fV*;RDb-kLlli~=wwG^?kf_z>%TZ>3aKCy%K zllU3M$%I6(`vZ|Pj*q~NYn&HG?JZQ-Fmbo=@*7fZJ3#Q?l@v{!Kq>k1AW=k?%CuV7 zfwUA*G9LiP-eg^mH~s?w*p}-;WI)g}-LFo)X)zuoe*9tJwuz0VIZ@ zP;nKCUEdurQ2E)o9{i9#PlOI-oKlldLMfsr#gvs9=m={kRo^Rw_q&9rhmSSkFQroB z_8s8?0CeMJmId}G% z+F&rWIP@IpE5X=ZBmAva^k-Y-+%X>oh;ns4kC>O&Wel-4kXBP{do`vr7H)(7bK6D1 z)R#TowjbHt=5jf{%V;d&FeLTOBR)C2Z$$gIsRL=Z9Eq;qzssPEGPfkH0{I~xs!4R-~a z*o2+sVtYWVzqYVk4Ivsw;_!KBhc5xr<6%dhg4xMMO}OM;k?sV6TPl~K=WT@WcO^fU zkOhok*QOFl+Ms`Oh^2B7>3YP-_wQ4Mg~a7q#BwQ6JQ2jZ21(3WePiY@hqlKD^NHQV zDWQERD@i2D&M*uUBPY9=a~X9#=$^x0M7e4wx<1C19r|eIyzTnNiXr>p?Iv?_qoYJe z`WgTZw%v@yEJN|k+(*_D9GgJrO2xF8fg33o@>gCAniUZ2K0n9^lmSiVsW0|+82_3f zJl>+OQipaJ!OEZJ#~kF%~C`mtf&q-H^bo8ZC>E^Ba;p~%$X9p&>DGVIx24VUu~OMKwNb=q+o_oPswgo4PaCyV4`3_`vHyB zXjj^(Nhng;-@PjkKn+;Npgy1S6(43kigyXHfSfC>P;OAHx{uMNBSa_}1?AIR4?4(K z%&86=atpC>>4|dh``Y41yE1{@E~c-#mgNcPqrGQ?p?fT+5BIPQ?E-rk&ja&$J(M5g zvh%Lp^b2F`azhwD3S-hoj$#N>dU`gnXtO$tZ)%Tw{&yG^!ZOBU5K?_O?mL#bZ;1B% z2M^

m_GD;|enTn}AxyzFP&6B6Noj-zBp`2$jf6AtzUP+`x&8x&o-q5;NgTT9uf zMG>C7v-mmN5?yQvv>eC(waYEo!S{o?2I=NM3o#1aW?3fXbWOSW;3amW3oR8K)B2|6 zs)$O(whV&yo+vP7IQqW?sCGCD> zzaB|t9G9Tb4%ID0W}16;0H|+Ky1E6RvAXN=Lx8*I;0^Y><@O6NN#ud~_u0S8{YRvx zaDk1R8lM}I)%Ub2C3&Hjf{*^?UX>bSdb|+;P9iQr+ig(qCk=TTqBVVYQ!~fKVEh^s zFdXePSV!-{q>V%xhP*u>g-XCoBz(st?_Ro>N)-nqs?w?#+rM=6okwE8r|&9(jz>UR z0VzUtT~$rO@1bb>@PdXR0>5pJ0)dqRG1jtxkM|(xIioUH75{=PY9jF!f!$+c3@3;^ zgJZ#FCK>^RQ)97>8T+)H$m+cO)rcDrDhhd3CKtW6`ky6pymVb&ON*Gk4gkg9rrGHguMkGC=7D=s8;<3R&9URrm$x~RV=|IpO#Ff)z`f(^ z=ch^=vFCMzV60zQS`h^o!Ub(urp>?G2DDCyX*0MdWxfRYB=il&6tg1{7LsmqWI}l0 z`%~>3utpI7Wxg4eK(MYl9S=Sqj&BxT2aH{=cY6aUD2l|zZGi>S3jB8cI{Z1Y)yW+| z%gaY4uC|!|HM`>*Vy)Yfyrs22_d7XsR|XUmUN!n$(qDEZ8{=II(9 z;(LZfQ_66Zz4>gNQFe+Nr?+e=xrBMXgko}3+Jw_B!Z6uy(NCK!L zoQ&=spZK3%!Rd6^_u0H0$;{n)Yvrv;>>OO~)2#vXvf!7cU`$z@&w!G#_C7}lS~ulL z=A%aG>nlYbAIojsVLy6gAQL@gI`vWvq}?I{$|(&x#fnS!BPBN8kV6pL!0){RNk}ki z9B6Nosl_SYH9b?vU(U!wIm-s)!7M{UH-e6}tpu*SoFOM?w;ekmp4?;(paFNnpc!zr zd)YEis&>82G>0c*ukI?f%vk3bkM9aas)t+tQizwMi7jGFTc;eAxxaw3AC(~oM?C*e zPxfSmpE;j1JN<3I0}us^z>{8XM3doqL|0jFNLerg2rA9#LstG+;`d*QOtoJX?6}Hq z4(G)Y6|*9DHKjlvy5NPO2Lnh+-~=`UJWx*RrEY9h#1D`19Z|QxFUfQ&XKqF0O?su` zrEi4qdxrVsFbEB7j?K{C869d&Pdw^vWp1+YO=G3k4>n;i)$}RrQ;O~@O|0oK2HfQp z%8KG5F>JqhOcQAOB;Vmr`%t(R-`Z@d?7pIe9U{_D}QqXVYZ{zL)IfWk>JG8a2Q~JitpeP79LP!vwxx ztxg67eh}=}%M{M_$B=WCqlTbz9R!|tUWj+@rx_^q2xD0d1k~=H!Xv_m{u$n`dbV1i z6(nS#;%=^I93aYAfdXyrh8R$=Y-0-X)MnDfuTl%4HYI|0?JFBu9L$KolJDCurz0w^ zz6YT0-sh}m=#|R8zdHzg3jihO{!mvS5i6-m=I0U*IT3Ls>^NRo8wSwQ`4YCnNi@R5 z@#A!q_T+7W0HqYbhoq%sw%XmDnAt0j>z)IJrdVHRlZD*cjdO93l?*XjW0g9OdJNV+ zcsv9XS6w|Iw;$|hs*@*zZwceE%!ZT?WcoiZ02?Zj%XV;HW43b#)1Fzcs!2aMZjXfs z_2t+@O|sqpVGyRwrEPXUshz%s&tiykgJyDc04}3cL=Zk%Nb7d(dMRp0DHW_&)1@r8 zoW+K{;`vEyT&K5Hj6w&rbRZbO0qQmk%K)R)0fI(Q3kPAMbo3T`;1ap@#!y6lAx**# zPh54Cp#m7)j{mH^DXTRRcg$@vvJ&WVC&8(C-76Ij=}`naJj2{V90* zPC*-P%Le2OF}rt8lv5m4o76$_`EKlwg5A6NR;$W7;le%rH|Dcn4Ee_vwoq@*NNP-* zY9kuFq;#-6TP07B=mlm-(RJ!ECOQ{Pol8i@uI=TM^g4IM0`wybP_E#zrc!pZ4gs;) z7w9$kD~gJWqk1Xo9c#jeHiQ9+*7+AG_^%jAo9b7*5XHMNTHvy0$ z-0wUWChI%<`E}km5!cKdgI>_+pROxp(*F>5vCXoUK`W?Hd)H(+EL*Mf!Cj{7-+z36 zOR(Leqw@OL#lPkiz=~qe`Mpr@5T-Qs^e1ooLsc=X>9siO4RauRDnjLvmy#zLCmL=) zo9%P6Sb&J%>mLDOD7FH*~Yd_o<0C+`4j&wsy7sd#<0|T-Z$Y6$h?ngTi8J zb-J^odDSDt^fHl%h4GgFqr44Cse*Au1&ZKffo?l?ntJR?)XTWmh+l?JOvxE zJf_RXe?V8VEVSez%9^SoH&MV89lzbh|K-xpNhom6|4wTK zETwvSM8P{2juR$a=^@?AN5k?={j`&=k&Z~K7K=q5Jy)%xVO4fvg^D?ZB8sQW$vUo& z$`1NB9l!7ihABOh>j-2$1O4M|Q;+ZJ`{{41#e+!{t=>_ z9-Cg4WvE0~^s<|9rF79GS-FM-}d9mdhMO;(DvGFHh+KSC_53+ta+?S%dlm4|b=Eej>yQ(CWhR+zi;2fi=5# zlMtsNs$OYWV6)su%{GHO(}RSDZ+7oeHlHDLy`vNj!|RyG!uKluiE+KY+J3)r_` zJOkO*qRz9`q^y6veZ8VIKSsud&sS%Y@WSJqkGI^rlF96z`Vx^Q;PH5tHxOq_r8OXM zGw@134`HYGH2qm$@8xHP31A5*iZc((DB)0mHXDr3O1p-C^(1CRaV6@S?ebCEd~(QU z7g=Yxw)AJD_J1M$v?UJjt^6cLt+~`eq=pFSts5QNO%1f54L>sv#8nO$J%6& ziwBb+<`(?K*Sf5_DIcdf)F3q@iWDM;{{H4EMKY8{xM0Yop0gjnJ8h;pm7HFCe}}pw z^^86bL2KAyXE=Ftr2Or*d~HoR%8V#p|Ij>){zSxyZz(LFv{h@2)QOlY2IXeMb2oNp zcgRyg$Y_sPYJ;kSe5#lIuUw>&kao?2XLqVSrcT1%g(9D!P=XHK>ER|?94nvPGJN;^ z<_!dV*qA6m||<^ILjL%jR)|6z+@@6*HjthE|Biqpk~0YU`! zmo4V9hYAp|vpn)hk*R-n`7%$84)c;@U_^8;OuE(*$|vk?i4^H)$=1Ul;_>%~ zPEfy!`}Kscmg7m$pp3t?P#uMt6DSFxPqb$+1}S}WB=n)a@n?e0aMI9cOO6nY>l?BS zION~L*OX|)N_n!_tfoYtUb#`ZLfpF~sLpd{Fka<-UP2B`@GH z11Zh(bd20LgHYqb$DxUo z59_C;aQ)CfE&TC{@gpo3KY6fkRHO8P=}m3~cQ;W6=_Y(bHYU-d`(3jC@smQm>1L(>l#H)X) z7Ja!_F(~G2xQSF_Y&ES)!a?YCXltkL`fq&mvo}0MLqt;IT`9uSLI~1Nt~Bc#9$WUG z*Y9Bl-p9q{Lb>IX-XQgpU=@|nE|_(EezZ(q1k)5oel+TC^CP6VpkBD$9tflJ2Pr7h zsyTQPPqILo6lB+YiL6!L>l-I@LX!ldWv6`MI2x2OG-X=#l#qV%+y}%;|MKYjx2yO7 z6ybXR;UXwIgBTLts)w7uXCtn3!kaPXO^O}N?iDTT0LU|Dg+RU|i7uziSw2MD zpp6wC_c#}b{@$vYh=a{Owty3V!|22_95`s4XRlEC$iOKe-8;8*e#syZiKh@%Yl6V- z^6LDmqvrgXRKSKseRq3GA0Rc4ilIsxNWZ+p1(5dVeeD_v#oad;3hGw`g~EI9hrBds zl*#tB^hnl|L`odaJULINcxO9%Hlu{~jZb(AZl7uv?nb~O-0(raylo0_jh>q3QKiy7 z_Id&$lzwIVUXMb?Dum2k>_C1ey*^e7;QNYtR<{R+CyDg$MgDmy!VWsC48ioAy4}0l zGk+s6kM@w22xP?!*>t`X7^MV!BZVd+deZKDeY)WnhT4AWWWJR6J6C#3{LmXo94ME) zZbiqr{=K6H`?AZe7MZ%=OK9^LiTVTQGVpYkq?TJt_L}M{&@!n1>)?@CeV(W1VMdti zUTIeUFvNa@>Rr9$dyC6tn0Srl*TijA1(#REw|`D#q{^?d#V3!Hq;DGh*bJELpVlff z;)Cj$+${6bhL&9uEP7!XR59&0c2U#q;j9vl-}*{fT?Kgi3n4B;rvx9?tJWS=rr@!o zEsTdYu>lK`w8L7Xs@?4P(np85g2-{&beWSOP=AE`p?`{^ljKEc1Z67-dA3~m4FxeTc?zWp%}1{p7n77J-L+>qB~Xj3LQ&(Mq;q@-`72bG92)rO@v)h=Eq#=%P020 zn#W!Zc=6&W1&KWi5UQ8*4(Ig9_CEv$P zV$1b5_K4vQp2`CG*Em1xL``3+6t|O|X=TxkEcBFO#czXIqxT<(r6+T{#+lQ`cu2`Kx}zpaYtrI7G33=*{`o~ z1f6@&rZj3S4UwnL_U$_z!|)c9S9csKF%1lKbh2{Ee*_d#O`MQVIGr}?cK(M8I^ziT zrrd5fJZX^PUNDWl_Y#)zZw`)6tvXcminr0(YpZ9O_PTEjp5D3D7<%V6&SRE!kY$e7 zkT%owqgXiwgR*DNKaQP_6am5r@ZCOtaCiJpoh$y1?d7oE0R9zrZ3x%! z+JRS_&wwqBmqZl?kMzQVo2l~|c+c&ze&zXJ%l(oU7ELJdB6d6rLeqjAiHzCK2U#nE za2VghViy`0Jx{?)cw%dd5ynwV^DCxjAfpfz+E^5;MhR`u?#?ieP}a>k;bQ^@#l$d1 z12t9XK4GcGh*P6 z?7hE{|LxuA?nCP~v>=BGUATADkOL%@6om`}J_5 z7;Ds2piqHr^Doqi;%hIV{=Z?T#KURKJ7q-Z0sOycEN(98=YyVp{#&Q6rcX_zg59KV z{*J_mvCj+?+9pLW7u~{DHcZ&?0@UasxVj}YU{NntrI9`gry-lyPmT=~zQ4h@H{?oJh0zka;0?|2DyU$i{lP`WAn+gZMPZX9Kq-kr9F zuH7?HZxD&&#Ham+Wfo^m0K^EjOuk+_BYVWNW~Dty3i-Q(Z` zW80s_uy%_ECNg4oncS7-6_V!C%pW@Zo8LAxDMGFOcKKZ7wiW`3cGg}P@QN!H%l?v@ zO+>|^D>xnl+}w3nZ49Bz?TJ^R5G0L%t%hbUDRUHB~s4%{S3I&RJ zd(YFnAgoy{+RKn5YjB+|%vufWGMh)f)m*H3+Xpbd#zaY3tgIn-Gg2PS+(zrj&+=Ko zPQlibiw;wbgTKtugr!swk#f4_8(|L1nfnt#H%VH0OJZ!O-#$)b_#>JV>QAv%X0!Ok z^}(PXas%J#oqrNdPV?kU*q(V`*gMa=s;<}R67olWdYWusLf1z^>*G0ilQ1X*bQo|H}d=56E(lqA$M9UQQ`tQN4b3SvvJy zo-cH_Lej>K<`1L&!VKyqoIv*EI3U#j|8WFv>YtlCTz;0sAzS7q)|*Wmme3cdQaqJN%@@RE3sa7+gZa zXj(<@@6f5ztT(Y5$TtsJJqmyo%T_MRr(G|5tlr#bqaMRj|ei+gK1NYd%J`#qdZveR=nOWYA;P$6N^^D}oVr-Kqn zG3))RKGgU3?)29yCwnE32=NSZZQ8{hmfz=(rWec^CKvWZA5W>Fu<6Mw%u?d}-SHY^ zLNZE8EDb+|Pp$R#2{(I#jC`FA`dZU1H-fwTy|>7%SXSa%1Ng<3fDsXoK|Vnr!XGl<#MU3P9b z!IoUh4>$0FWt*HHu(e0 zhYV_Y3PImt$FOPDwyjEfF#XW)tiGw^Km5PY|9$47l2@hds#5w|_5VkHhN0+`HOpYJ zDzd8oC#a82%dnuoKqve^Z_N`yO2M%S3Z=LFpCB0?>=+U!p4-^;e;@w;Gf03k57Q;f zA6xyO;D}^UgV>~G_I2a`%m}GSq&C35L`1-lWEIVR8o#OeR&Sz<c}gfKJ%?ar~Y-TOoZ4igrZx7=?`WZ;MnqSYmqgeMJ1mS6=l^XHkn_H7bSZ zUIwf#Z1s(NUv)agb3}zPC{?}LQze4JK-0&GYybB?~ zk$<1|OI&#z1f`7%Al3K)ya2<}S)3W={>?Zc@No3v$<4~KjH#`k)9u$cX&LSwzKWsz zBiDeRF^5WcF)lOyQz@s)iBmVfN!10Ql6ur&)$#8%XvKSE0w`F62|fRgwTLqv`$v9v zk~gP()~I+#7%Vf`L#5`QGZ%ZKhf*m1F|$+A9#EhQ<)@K-^JS)I(0V=6!=rLkwdPCB z$$1%uk`y+|9ugSE0E(C|_h-*8?QB>Z)j4D zj-<9`F|4(wOauwc+o$V&WwwSC88w`;)-z9@#xb78ViRh%h?Q-a6}c$!QBkcSm70%F zrG|#Oo=@?~@r?AqBAQKw!tq+^Pc34c89o^|KSbK?IUKpyemGicyeR`$w-2P}e0uQ! zalBccK51|iz=-BD<k@Fwf3c&2D|`W@XZ%uwE|0-bu4mm;XcBtTGKRa);z9UwOBXB_Djr> zGW+fH1GVR3J=r={0G@BYx#aEJ5)b2$O)&ygc=5UL5zae1a3I9Gk!HQ{DTWt8S#+x$ z-L&X;q2~%OoLnGbTgi;7Gb?e03hRO}03Qu`m$8@s3Tyvn{l9IvetW#C1|y{Dp%F&B zvS~W~>QvqYbgErEIbpEB=Z|j^3bCm+Fv=(&BRgZ)s>!bn3ca-5Tr!QmFtT5olaA`C zs+qL|4VqOI7@A3lRkt-%BAB+2spCu0CUz3uuO`7S>nWy=H8<&P)Y0=FWL+g*D#TY$ z@C=+MvGid7NYwz^-U<0U(@g8-f-){4?}8`^&Wf59*-shxWR5hcoT5ZeTqB&Eb}ZD0 zdD?XM zP^K0TbJ!)%)oOk;o!d83cN@{TRs56RjWHJd( zd>d>xgw{AbGVS%+hJiI(_U%g-$We{{GTEpE%hPai_5hW6_?6#g@Om1BM->4Y;6zr3 zT9_7vZ@uEFSad--iEPxaQc>OJKAF|I?WgM1y8U#5?daxg5y>?RV4VJ;oFEnQpQ6$|L0hx$>=*e_j} zZ(q3Nf;ji4N~XQ$F!)=V26J9#Dcr=Y@-%LhR;?X{r%vFOdjPR)-shn< zC4RfINM;5*4rg)oR%!ODxWSkwptJu>*v6b%Y5{mX)HJjUH?9n63ya#}Il$YUondml z4v1nUsP3KT?*$Qd^lk5{ujVkppZ)O=DB-%t>5wCQk0QwQpXSlQurX?5ijdDoC_Uk@ zK7CmR&23dTl{y;OidU(moYd`nu{j%)X08usdxk3;DaMsY{AVp%O7^k9+_5)On*NY! zen0LZY#y=z)NHEWxLM57N8(G!2dN4Qh8fly98S#82TZBuO5H1Q_n8E?-VJAZ^y3KE)HE8McP7ZaO zwxqOpN=p4u7q-Wk`E^?)hT}vU;?lpXclNNAZbs-|Pt&7R&Vu=-B5y7birWmDF(Uv! z#>WAZ4cQk7h94F;fMuU+w2Vwu^T>Erymr|Sbw^?%4vR(5s z5ks%FjT#zccb*^{>87Eh>j4*)F!Ny>9a@l!lPDvHqGI{jSvM^R<#|VDUz9@3-{t?~GtI=8aF~U;4ex6t+ zIt`TE^Ld?Zql5A)WuHVy1WFAJO}(`^_soY4POQDR>Wlit>p3b2yoD9^Zhd>j(s*D+ zm#?~xOIe_EC?H;<_WB_)jZ>3k>0L-uumeQk(Yz*jk!30V?XKC`5)GYnSTP)?oI<;b zKsmKR{m#p^Qn)5pgQ*H2npYWYw!;D_t}UcQ?;4cUS;HOhL;f%6YetIjLgYsyuDVI(|kg zK*mk$_zs`HieH81=!dmd;!1&Q=G?g!g_fwOhT8gGSLLOm*L(Chykxp$hw^NP8w+=BA&`!w^!e)4q9z0Nf?!qeH}zY zRqKVJPzBXyB%{zIB~wf{b~C{q7k=yR%ywz4HFdqi4;lsp zuj$#&znbZFgU|NUs9K{3KD0dE_=;XkW=gi9-#{$6pYf+lr8;Y^OjY*-FQB_KV^vb-aqLjNL6 zRvQ3DHQ&A4QNPY_@h$zXrY0kRJAH2ku9Q@|uav2UTaiT`BhQqX) zeBFrJKypQzUvifaZ&T#^Ns;-p6`Rskb*x+&*W@k;DLd!p%?u3Wo^B$Kja~AP=oGvU zOXa#NaOV1vuW=+JLqIU&A%Rtajdrn;+aSErB7&Wi$jWHWpUY?bWs|SZ!tlGK7f6C- z2Z0yy^mruek?TLP zdDX9_8@s2P-ylOaPWNAyu82{jo|ZIx4e`^OtJLnCczkdK3(pT!Wk$+qVB(gn;3`85 z+(xgoUl88_11z4uoiyHS z8S(d5AbikK;q5W%N;&;yfa2KCEat%8N&TI6iSmzhBm&OnSL$PCO>R4&Qf>_3rxh{T zX)cMdCo88j9`vT^oHzEYPzB6p;M96qvB?zNUs0ynsipTq0VHC-e*;C;AvhqyzaiX> zNvj%q)p&|yXph3mwfz22d8$iCZ{$O3O2mshVZ&H(T89Newt8zhe>uJ;F| zS&mKOHhluPhD2|PN&gCv@y7afUndp0Wb&8)z%r4SxzAee|3ytHr2$nuk++I2&=y~w zm$G0$K<4`=%}7Q9=>#5LSOhHBNF9I~a4N<|5uNY%I!gf^eKV>#*xv7iA`WyDSvv!ar=F|LVKyJo}JIxAhrrsQW(*FL;` zz^NhsRiPPpfLo!#BpFV-=|Hx;!~kya9<|->??HX;`w0Eooo@tcI;gCtb@di`Xq-V4 z1JjWs_6za>RKG00pwIYCy4aEu$f^Y4?vT>FMy)jzYIZ9&z$z8=31q<~-ek7z19dWUM@ELR4+&^(#y)J4_r_Dzh zFRY%~)8w0)o2hmYTE^rPr%RrcVF9S4TT(+)2P`o;d12iQ>rq{oNP~tL9{H7JJG>W0 z_}8@rS{7>q>q0slfxC0H!F`jK_99tRt;R~3R|U`4lFQR`P9N~G5n~h1)|x}?3*llK zwA}`ArrluSsU65%5%ayGDJ!Z}L?<6tsWDj2c%OWai)Gazx^~Sp1deOv{nG&J87Y9JQuEhB_UH2iOQXn;DAk!3L7p%T8k_nn~ zh)uS!+x+M4VKPI0OW8|=S~ub?Im&)HSBSRlY~JH0FgSx0T?6BK z=Sq+uy!tc5-0#9@+2#uisg<7AIWXh~n|Yzz(ox@mo%yew8)Yb^L*Q86UOBIgC5lxU z@&v^_j2SkCX+rCuj93w7Fz#r9z+C6l+Cs4&@x&dc`U)&*PI28nH1Ol;?HYJucSqcQ zwz|bwKflFIx#585?8T&^u?VzW^!K@RJRE1QL?6>&YFK_X4oBs8s5|Ju{`SM?L;V9!iE(nv+bUX;=?Dio67G*+YRJ}Bg6OM?!4W{c`KmyHmBdyWWJ87k9AoG~}IkHDoi zp)!}lF2@capWsh-eYw_uvRI5Fu#z+NYK&>{yMNA|0`AtNlTqXD96D7L^Gc(V+7xme z3ctNxxnHb7GZcNm5?&p;0!0%zbof*iq$o$V2L{#G!Of$ki+$~2csF75 z&psn{LcarLd>QK;_06_$T|#b`mRNqbmPyZjnD&Qpr8OPW400jdS}Vg^)k>G3zYf5u zD5V*C61mu=xxU}3)uPi_GEWs%(0FWD?b8u)crps#Ef$a?8+e$vFOQiwDs6tZV zU*}N`u)OH)MgZ?z7E4a;)pw$LZ~dKI#_EMU`MsJf>!$OD8`uR{22rUjNF^f6iE2*u zE<;(^fsXSj2HqcVPl4C(L+aLEKN_>|(C6*%;c09mr5ibkN>l3yW~M)!{U9R#DOV|c zAUf}tkzXZL>8{hDOUv``T>(b_kqS8^yd9JNSu1E?vU0KsE3QLOrNdii*02~&?}~Lo zAs1?5FHRkgcGRPsu4bmSgJlD+2k`ZlblUUi)=e5pcE3U8TvEYWJyZe| znCr#Y03KH&4B}a@Z@qI(KkqA%@KHLF*sP?4`Cm$h%Y@N>Pc3qlO^f*4zKz@6tP^AT zU-SKy?sHAlx;ngIo+&7EKXnZjtBZaBEqI{TfBEo^+QW)6_-L1FQszxJB^nF>U!z!C z$|GZjji~kc?|hzf*h|zYu|Z-ZE8`@pC97*VIlq! z;5xlM-ofBTR#)hKX@|3JAiF1y8IB6(5^>!Q*?0aD1K`3X%y&{(2>%hG`NM-2!2g_L zaz90CWT!So%;ZE*H<+Y~U2==~a~^;caqET7fP|F51u5-&u7cd*$TS4BsV)*G8{)gk zuy!9C{zhNby>+5gwa6nYi&ll&)Lc63kxq#dyPddxjBy@=ezdwoM7NA~NN2^^b$+B= zrpNI7f>I9L-TE%6Dl?YZgv!Tt@uj~~`>WYg#YF7P_`S7G{-2gp6MvX*?+ZCiFLvj5 z<5l$)Qa9L>T>m&q48%Ek`fJ!3f?9T01$iqfZN8)^3sZCvW= zcDI)_313fi32(yTY*#4~zddP|1_ZR&b=m8+sGS<_QIm}%{u5M~g;+LXw0>J_`&r7= z<)6%{P|?__o6d3k7GgJo|5kk*jQO3Kqor8lk{j>cJ@$YyEJT3rAS$>4;fmJL5gl_} zjc5n1zi`UJfNL_&YuRb7^=Iv(g35?4hwtOs^eCChxzbP7UnZmCmEiK)s`_3UqF_%#v6xHF?BvC2CvAOE?aePzs!5fbk@(GOpBz4)D4|JKYB1}c) zDGBJcFM*jdqpIa8*tQzNUTbcu!vB^r{zXW8PXV1>jygOEzCnEEwfZS}LvTBuM=kOs z$LsMmvu%-G?jq6k-wcG-his<0jwe)fZH7JVQ`uLSaX+?UZ4-&g5=CQGG8|3sO{2dF3DhT9h%94`}vVxGn z`0EPlkcyVHps)HSe_Mpkr6Xxpd`GBd$==1$=k6e2?59-LG>HzbHxG!bQe#}dB%#U? zspM-zrA=mNsXyyh0S+bY;tVr|;|M}K}4 zEOuG2vq4#f#1Lc22$_}?W|+^dGS%r_a^e6CnI;7cmHXL`dH`30*RTS3)>fa%&%OGyTh{6Am zWvLX2qpK6n+AtDX*C-^3d%6ZbWF*UL#c8Z1INyXz_;7m2=IO7hn=gVp~Yvb_HsCS)YK8_ zrv*fT>|zHt5>a2`j(R{`6gECCBd`K1p8LY8(M#B<_-dByLfzz|LO0WMXk-Qpdkryw z6ER9}4YImpshd~WsGQefN%cr@XNDpD%OI8Wny6$IfT!Ht!*=Dh*h{an$x1xZ;_cpU z#Ws^<%fcgI2T{+vAJ;a0-HPx44A>PGTK|>>W7z9EYc%Bj6+Ec^-Y^|~%Up`346!rT z;KRYN1K3xpZFcM4u!oMHAKyiO0MUJLY4SHLSo@1JXTuEt-VI+Fct8iHZf#h<0YJa* zl-yT9-JhD7ma%A?-~Q2$EOxj^j6rg%xZRgmsdzRG4owl5r1?6QY&>=Ga+Uc7qB;^- zDfxTFqUL@sjQ22rRl(XNVS_%Rj*&4j-lWD%SA@myXEk^;tD%>@((Yxw)BU_Xe)$!p zw)`^3EX)btf^y2bZUddP?m3YYcxkk>R(`Q*E`FEu&G3uoP5eh^jurxYGVj=|ShCgx=QB+u3f4aeOD#BzN~+|DV`}| z{!0;@AJ|czAeh4<68p9M_BKy7zs_<4GP_K3QcG?EG_xC_rzk8yQT3hm_j3{Qi9h+* zP+M`NScn%VtoLWHK&7pdS=kIrgI{ajK07snTnfnMa45CSA>MIqG!6RX^@BIN z*mF5nBI9bC2;z@1S<%FQ)pY;9auICb!Bq7`38hrkw{{!i7&gwzAm`U~;FPlG5hAc^ z(CcfJ*Wm-HO_tS!7k{p$ffs^{-A{#EayPv88-eGElS?!mhnR>02W2ZSoLEIqwtJ~0 z3H|*7Eya@uzLwRmZ3MQ9;m9H3i>nJaLvUmrOLHaJ)+bU6tB4o)r3uTBmG~PGZbHDL z%|&u_69JE*zF5Qe5E=LSnD4tyDfC{7ct(D5c}Ay0DVyxo!0PyujR7!-(af~T{FX1-Dr|xTQzbaReiMjSx;OwbbXP#}Y)|Y%9Rtj@aq#FOV10N6EqBg9COAWqzE(TiT#iFy&H!i#4y@)o-N%J!nPL?BMq6?_zXf`dT~ z&g-Hqq{9DzuT*KDi)^Dws)K&Rq^7@P=3TB~sg^m~N)bA0^UkoIGL(QH;z;g{zu#ATV(FpjX{~LI@nKQL4*x1V@+@e?M zS@~0(sLnc5Pc_Kz2}STf}gBD+IBk0g6R+TFq~&`DFWYOXWL!);ZR|Gc^%oWf07`mOL& z>B!D6sU3*-Mu!~LBY3u)ft#TGB|a3JL|UiM?crmjA2E@jgh|4PF5DkvyjR=hT+(^> z-e?CJ4*z|aVgEv5f556lXjrr8)A{AZybc9`IgC=bAx{s^d0=cb>Npf#Yc*A3T?Z{l zr)Nn^Vv!ea#L?Tm{i_C^z3AEZ{OUZcY{H%w{yq>+TFC@2X^EnW{!2l~CnmPtGl6s3 zx*iTcZ#uv_+1?N+s4$(7hR*#{L1p<^w$xQrp@=0q99()+)r?n zJ!0C)7d0ByPwHrB^fwQ_PiXNBNj3MlEJ=UG{JKyt*H&Fu7!(OKSDCQ1}w4vdcH)?UFm7 zE^ZK%EsN|36l@-UzYMcHi@M0FPQcixby4WUrLVIpt#aflOLsjT%X{U&|7WBHa>kS1 zwANr4g1B)OacuSn1^Nu(4TE-#v@JsbO&bSKK6d+sglCW3KTwc$`WWyc-CfH65dZnU z!Oy5xLkNL!xm|wpTg+VO*Aig~=vvsJ9#xpvIJVHH@7qBtmeir&ElMRx*?SVVo=h3H z*5YgHvGAE}qX#vZpz>+W+0-&w zxp2KOO(~@=;}}K%!LMuYy$0UGKBe~hfJH8$*2$&K4kJZ)8j*zs zG}N$leT$YY3M5T+cdAnt!61PmNn1`XmQ9Cg+lX)faNLr_(S2?5pN-=?&vU1N6}|MO zvfC%Durq>u9s2UAI5vphIr%F!y(-l~jkd>zdar*`uLlUK?EeZVMMipcuNxy@Y+z`n z0gqD?h3py2Q>gU{i^Z|1K!S^WOExc>PYJFcZmHRi8CMM_g?JtKR1?DqeDIofPR{hs z#knv-Uzk;wMDMNQstm}B$%oPF7yo5oXSD)1~uRIRPedq?6=^;j)lMXNlg$@-4#l*>r!EfEeQTOiI{JKnE0YY z6i)gjo?YLRTV(Cs%*#YxfI4WJE0vIR%>>?qi-Cg$`nNzYr7ZLjl#rr8Ku&l>PyB+@ z04apEco1#w{bX9NM2FbHA3H`*j^ar;I9p2+|9K<9UeLY!3UJ!4StH&7=*3;rXy6K< zHxyu#V;InSwJT+6mur9Ma1mqG?hHqnJndyt3E>KJcjKZ*?%+V}Q=^GXi;OD5F7Df2 zZbi!IkTJCo`; z_3QFqT}NgdF1TmwYA9;I*X_~H)XTjN0uD}j*|#`U@`IdSE`{?LiMR|}H_l%N)PSe& zxP;ucfXP;q5JmreB>wliws38*XUb2SU#3`5qJ5GAZ6Z$)-EUJ%Qf|z}j zz&PyHT9Juke$!u{6(et%6Fs#8Fln$r$)Q@w{FD*DQ@}x>j?PJ1&%z z^U4gx+z4X-ZT6Q=t#-q%(G{Du$4hs;X5(l@d^VtKFA-CmO<*Y{bFrOzVaKP96Caf4fL)^m>1EtP6m8++Xfyf*F5$(Pc(OK?Xt{e|nmME;J)I>YcX@3_)(tbbpH4E>&0G^pd`6JY9`p1&YIfiE;TXOBC|)S zd~3~5d4f*?m3{i=VPXtmU80^Pw+U-j{X))Y1O{~!XcmG2{y(RKQ1|yKGbWCUPoOVp z%MR&|u|sVz-5dxZ!MX202PoHL1d-(EV=hv%-)fS~;$_)(o4IUNs>2TLBpn#(~`huCmfNl5ji>-mm2P zARjm|k^c49l$xHR!!K{nEofn;5_b{OtC*l(x3~SP82@;IOTi53i#2A4%4X>gv-~Fe zKi_AuMKU$VdrSdek>Kg0x1iP{)AA^`Iu)%0j5eDVP$F`U^?x0hohL+tKSlfIYKY?y z2=A>IQG|D-_9g2Z3?qaW8F^HB3dMJBaa$)O%%?hBq|(7=j>b!{fD7D#%@tZi0cd!( zny*wQ$dMc@h@u2oFJX(dW4O5c$Nse9{7}Vxkq$_fO&%Z5&|HV7r~72|Sc?UzyP#m_ z3pt=+QI5Eoa1vsb>5(!@QNO!2tb<;oA)C#9_J((-Tr;!8uL^7_#39DTeOX<1s`cNR zY~c%H`EWk2Kcew_Z|S7i?$Fc4P^t|Z2M#3CvY#A3_l+9#m z$8%#TXgb%k=f1x8#Y(52zp`4+h#@kFUtP%So3dw5QSn1n@=P*oPJ#W&pQg2uX8Pa! zl2Cet&K^+mP#XLUR07LeHHht!x(&h#3}m%i#wZ*fFX zrWiLZ3Is`lYgrMuNnE7R%PNG$Cxu_XVaCZlM;rS!FQqhS^}>;o!jQXuV3lkK*T-xG z28oiJ>l9CN|M1Tg!nk}>)SP8h;WISc2CsqhKaPrG@gX3v!(=5y)w9j;{C$BPE}b^L zCu*~ClejXoV-CdqCr*C%4v7al-Pc9g5B13l9-uV5LHzrxK~Ggd3J${G3rCzaZ+d8; zw`a7IGj63OmByy-dMdOV*Q2^JUz_LqYlDq&GHFE&kni1 zZr1oXHtP2w%fh}Po{u90OOJ8*^Y=zh30gA5` z`4{RG3q|hveV>|P7ckVT`K@a%w^Mehgk^>$$X$^(<7pB9@r%(Z zWu3V(Rv0y{x8_Zv{hP!z4Sz0vS?GLaXSeUuq3miD{9gG}tB zN{ZSx=Ra?^SrJVyM%@r_pJuogUbrg;W2xPFAf1?+!jY z)^4x;!g*TBsJR|}Stslu1X7v6NeDgOw9xMVf6HvB7GHF1*=tMgS#2ZrjvhO>Ko|bk*Tj z7ChlQ{fC>ToIb{m`^OQs=$l(@kXE194;yGteRhp?DB5tw-_FpA^JrJ~B&kwbh_>{> zwf4X#=iAl=t+wrnKiivrewGc@`=cnO;L!*bR*zT915L@hy#H&@+N0O6w`Kpf&CdVf zF*f}*U zx{cBIiP`hIb9}_B5Wui-OJUe~r>wHu-uR@AsMptiM3Z?pueM32kJYCc(-o)dix*F0 z6mH3M)|k+8v${yM^ChURDFUFlLHNSZ!{3~*mdQI+h;5fBQmdVnbbwz_GT~3GSEPc(>Fxsg$sw;$v zl?&glU7qr|vq#v)Kb~p#UO87^w&-saD|z#+j|M;fKs*U%8dd}&YUlDN*4WzT_S>+T zT8%jOpiTe$Ftu6Al+297ED3?bobKfFM%kb3d23y&Pk>46Jon|*HdF7+4iqkIszP$6lti{3_w47?4MWKs$XumlRq`l#vU`wYKDjwA{SxTZR_{ibH80` z%l>Dp0)+i+RfUkU=r;NEkv3{Z zf7`!6??h{@!3u4Mx#fd9Z1_oog4fShg47pM=Sg%4Jj{dx8{2L9eH*m(BX1K#cWDe! zoA3o3?LxI_-A0@9pR2XMa+M7k(rE2kEXfyXkG*JOFktk`wT21|rTTnqMb(u5=Ab|% z=W}EzKT1*{NG)-j7?i=rf|dgn+Ny4qhFhcd=wt_)W}E%C|WM2`QvG4}9>mf0}9i`Kr8*KbzZ=$XT{)LuR$0D`p`0%7UW8sr*72m&TW!e_30 zkTH10c%xA zXzov5kU-UrPx>%4AJsbPvZ=w_h`zas5|89iKrr@)8c-NRrw+8~*Nn0G*KM=`Cu+m= z=h|(_{cG(sKAlD$yl=dzbbikGtQ_7k-<0g5)wl;9?XcfH`mAz9`IIA8Q*^~0yb8YT zG3Ao}HtE(edUc6@faaYI6+Lu^E)}iVjDRv>BX~UtQH21R{K^y>cc^T5RO}zs!N=GE z#%fSzoTK(~M^a2w%_hJ?D39vX%?$^p0|ceATH992!=Zhd68HJXw>kNs_J9cu_J&`b zWw&1OpzYB6wu7b&v>`JFNszbO@87*ZCKgMvFjeqrd9#K^F4Z=0dVhJJRkr1c?NUZ& zm;TG?)~6qvX!3%HG=_*7xIoM^_BgGQHibVQ)yL&0_Oapp2H5@wJM8`kpI2TjJQ^WS zN-KJ8Yn$!U2Jy6gu(rah)CYQ3FWh05e07?Q)Q)1zGSdG&N*WJzbATcmGGvIT^zGlr zw*KOP{puG_>KRXC1Ws27MWG$@{yU_0&ik|sf>~N^ND^Eihi@Ni6RL%GAP3ZkdT$8I z&3x@t@tu@6#v;Z>A_1Gb?3%`(W=wlO-Eq_>H7tNd<6e0#8 z;}Tw_Zl9*83iWAxXffBmM_TOG%O4ZAQGL`w3rO%jXUKti$*Td=Wd&=!$EF2)Z00+M z*s*6fku4C4aS+nsXh-+Ue-EQTS)m?AlH4~KlnA!4C_$2j1sYq{?64)*Z??K)^sU4B znnOLK-X@$VkBGctN@x0TBhBQ|dHOHfQoMWI#pCU9TWW1vwSoTFI$QDiZMNsF2dv*z zZHk~FP_6JS^HU5}L^1=bO&__gARM$;zM5X`h*lhH_-GJB0wP@pg92RIv}mEe%Fg@9 zY}@dt_Vs*VmkpdL(Wnn?6Z-Li$FV~KfW;=frdp|x&|1AJv-%c=3H1{G-Z=-XNds_{ zwpK(bg?BXb47?E5s;RQ+uN!aAUAHcL1c0w>%=`X&n|b*(>!Vk1RB6D0@NQ^?uBzIZ zH&y!vNPx(?uB@xDfsOhyM%rM|!^0AENQo|$&*^KN+Jrn)9tuS$M*za&8d{@|wKMA- z?m`mXV|EbZB~KH{oI43gMqhmK4_{ZcD~zq?^BJ=)eb)(*+M_-SOK5ai zsf|3y+df!3=#+~`Qo&-Ze^}~O+AGJ| zsAYrg;qS}y{L02a#d;~5{>Mp)w#ppaDMhnR*!Pu!mtaq|RomWW`)!Zr^v=Ad$<9*v z+4O2zpigCR@47aqh$~j#Rwxt=(^QC`=ugu}B?!avQHMbd3S-a!Qp66?dHQeoV8sBL z4cZdBMg~Oa2Jv4|ZYVi1$EM}mZPC{^TjMeM5dHiP>vvHf8*{3nR7xS#8w|xXK<*tn z4QF9HOyMx~wWIBctCw2Ej%ph)R0{PIYweW3o^JgnD^$)aG~g-z_5|Oy_rZ`+Kd4O= zBdTm*gSKD_?Lk@`s);G4K#LaTUai;s^gb#*cFM;m*hQb1WtDuCTzLY%UP1X-Fa^pA zwO~?38Qp7%l&bD&jLyAfscn_zI($jBZPS+x&wAI1R;zt$!;UHPPV*%MA|iY6xPdn1 z??%{DH>{Us-(Y;BV$OYw?3^jHBnB8jVss8ZJ~X)Y@7Sk-$4*7^a`ny0uijgSMBgxd-e6R?B}x{)*igfOtxz?11T+(w#rM?ky*lD0-LYY9O%${ri2}Syy!wsDhD9V{QHSG{qp^+=;lxqrH`qEUb2imq`N%fCDzwT@(2MVFs;@d& z;gR70IKJb!TlF=pK{)X55D79>$8(m3)<{|J({cYs1Zi|PSrW9_1qnf$YS#OMqD1wx zl+*0m9=XIy&;x}Lnpi+PL!pd+)PJq|5|DPy*`*q64Y3J*lU23@sa=Xxc1tIw(cP|p zgNGp+k!aDL$9GHp$bZ|2!r@!1X<%f?lHfOXiE7gek^Qye`}JQv*OtC+oz4065`B1G z@1<@=7E3TY2kEOc`qFOT!3l2~U?;wDnzqmzZ~wo&v)zrdh~oIPZkLu43bwR_1cOvf zh%wQ`5BkAGKbV;Kas}Ri7vXhy5#E7`H{b_98KpoB5F=<=Y?s%D1^oTaJne3`TerX> zCZ1=z+j*XuIdkSeXU>_KXI|P%Y&prW!PH}B4urA52-9c_xr4bLyc}JfFQ0z>Zh8B& z^>Y5Z%VmwZ0^OgS(@Of#1pU&!#nKvP89&67p8n!^`I!Bn-~8k>?+uYVdUm@Gao;CT zV;>{e+xF}M^1nd|w3y*;y&HEaaT+V-sY|(S-DYB4T+ug0qzFNNfBg0mIk;11FVO2H zQ~FE}f7U2B+{jcTyvu}m08YYJC z2^}$Lgmy2wmSC#PUGaP=n}dBp)$N_qR%=gvb#ml`Ryp_KE9KRXPL(t7t%NaluED6D z^jO|AKp;TlMp4M?pa_IO+Wn(~sc?Kpca1H^IJY;3gnI0?c4GA??}t!u7^DNW^e6$V zrZs-^>YcLjN0)b6Zfwmk8}mb!c`w^y#$>2aXFXbC3=#F{mHv6LV@;t#(`PyKQdwb0 zj_H+=_Fh|FE50pZw=+`--niH;_crd46V8H}qovL+O)V;a*cg)8BE1=mN$+m2{C?>= z&)bBA4vOyN>q}*N!68a%jaL{}XveNq2J2VY-FJP9-X=rN3WZ53NFa!X7+t}RVQ?Y3rp+uOm+Cnj}ys^mSFO+JimIzRPh--tuj`96# z_shoBZr#m>%^qft&N3v)$*J0milEv`y41#EnTHAG(rDYwCkn4c+?VGIB1n23QL*)p2O)s)V;YsMXj@dZC zhy$2g;mL5#%6J*88Qc3SJhd_UxZL7Q*1y;l$eqG8m2nNq&|vhyiXZak-nJgjeR%Ou zS%Oa|{^=bvg&8)zzILNLWWx-5tM+`CSo1x%FlSES-v(Ke)*$^Mca$69Hh#O!@s8cn zMh`q-?n7&Sx|~{FL1sWE3L9$R^O*YC;~@9-wM}40rU(#qWRPyu7wb}$1DT$mpD%}( zTJT4&7n)A@lkD^q*pFv(kb1fY5J-bM0LDR_Rv{~9R3#)-;g0tw1Cd<|=CG186$fLp8jlPTKa8hYhy=F#*UJBaGZm`w2zrs=+uXG1PeF z$!E;92&EpAk*JLBz@!1L`@r^j_nl9VZ`*W6q}2~uUC}-cdSJL`_92P_Wu@YM#CCO& zH~11u71pS{4AbBL9#lwI1oxB&&sBL;$zRpp~hCTC8eR`68K6aZZ$sg;%1g= z_+StJD7(0;(Vpzr7X}zaUmZYm5?e?{eq`OUvi>lfT$dH~Ig7Fg4B(&g1f9O;J}w(e z%EQbMPEmI(qe#yJK_@`&k)t^cKHTwqxBTvt%+BM$o3+8Q2@E47&xZ33+5fF04eHYt zeN$0FO{qabDTKOGZClg$#^3l9wksVG+=T`$RFx)<=m9`S`U<;#rV{D#&$CT^j??5Y z?z%%G)$l{98%_#QFLj~hRnV2D197&bxr_Xe6C&uRB68-YcadGm5KsT;O_}&Xu3!Lq z{C8WrQ0{1B4Ys`73y24KIqltLA6)&WBHn=nqJ)my*8UhD@J2lIxrI1c8+tURdFH5cmp9u#%ZH7#%tj-nNVi+lukR$>gQmvXB#_$Irb zCqo6b8b3n%6V&1^>Kcu*px1xA6EYo7hCmwBC&Md4Cj$Qu1gakH=QT8L|8`=oRN@nZ zG6}tS1nSF>7tcp>kO(9K|0M*TmjNBl8aJqS)2s{S_9FWK>5l93QfkYq?NCch{ zfi$Sk2w+A}1QLNnAQ4CeL?8`n%0VKK2qXfDKq4>!1k#{R0K_Db2qXfDKq8O`q(My? zNCXmrL?97J1SWt$8q^7Zm?RQ`L?97J1QLNXs3`-9Kq8O`Bm#-R1Q7TeXTj$e7b>hf P00000NkvXXu0mjfuV0gQ literal 0 HcmV?d00001 diff --git a/docs/tiatoolbox_Structure.jpg b/docs/tiatoolbox_Structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df588ed9d9d7d12350d88ad2484ba7dbafcdc97e GIT binary patch literal 50034 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 Date: Wed, 3 Jun 2020 01:57:12 +0100 Subject: [PATCH 24/69] DOC: Update README.rst Update README to correct details --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index f9dfb2b4c..92afc0ad9 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@

-tiatoolbox-private +tiatoolbox ================== Computational Pathology Toolbox developed by TIA Lab @@ -28,7 +28,7 @@ activate the conda environment: conda env create --name tiatoolbox --file requirements.conda.yml conda activate tiatoolbox -python tiatoolbox.py -h +python tiatoolbox -h ======================= :: From 5b1c768c1c11aa3a77ec3a139e7ebae15dd8ef19 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 3 Jun 2020 02:01:15 +0100 Subject: [PATCH 25/69] DEV: Add conda requirements file Add conda requirements.conda.yml for environment instructions. --- requirements.conda.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 requirements.conda.yml diff --git a/requirements.conda.yml b/requirements.conda.yml new file mode 100644 index 000000000..2493d0f03 --- /dev/null +++ b/requirements.conda.yml @@ -0,0 +1,29 @@ +name: tiatoolbox +channels: + - conda + - defaults +dependencies: + - python=3.6 + - setuptools==45.1.0 + - Click==7.0 + - cython=0.29.15 + - h5py=2.8.0 + - matplotlib-base=3.1.3 + - numpy=1.18.1 + - opencv=3.3.1 + - pandas=1.0.3 + - pillow=7.0.0 + - pip=20.0.2 + - pytorch=1.3.1 + - pyyaml=5.3.1 + - requests=2.23.0 + - scipy=1.4.1 + - scikit-image=0.16.2 + - scikit-learn=0.22.1 + - tensorflow-gpu==2.1.0 + - tqdm==4.46.0 + - colorama==0.4.3 + - pathos==0.2.5 + - pip: + - openslide-python==1.1.1 + From 3b78fc18240748f9ce665b223bdf1b74e78128e1 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 3 Jun 2020 12:22:25 +0100 Subject: [PATCH 26/69] TST: Add test for version information Add test to display version information using cli --- README.rst | 14 +++++--------- requirements.conda.yml | 11 ++--------- requirements_dev.txt | 2 +- tests/test_tiatoolbox.py | 10 ++++++++++ tiatoolbox/__init__.py | 1 - tiatoolbox/cli.py | 10 ++++++++++ 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 92afc0ad9..fcef7bdde 100644 --- a/README.rst +++ b/README.rst @@ -28,22 +28,18 @@ activate the conda environment: conda env create --name tiatoolbox --file requirements.conda.yml conda activate tiatoolbox -python tiatoolbox -h +python -m tiatoolbox -h ======================= :: - usage: tiatoolbox.py [-h] [--version] [--verbose VERBOSE] - {read_region,generate_tiles,extract_patches,merge_patches,slide_info} + usage: tiatoolbox [-h] [--version] [--verbose VERBOSE] + {slide_info} ... positional arguments: - {read_region,generate_tiles,extract_patches,merge_patches,slide_info} - read_region usage: python tiatoolbox.py read_region -h - generate_tiles usage: python tiatoolbox.py generate_tiles -h - extract_patches usage: python tiatoolbox.py extract_patches -h - merge_patches usage: python tiatoolbox.py merge_patches -h - slide_info usage: python tiatoolbox.py slide_info -h + {slide_info} + slide_info usage: python -m tiatoolbox slide_info -h optional arguments: -h, --help show this help message and exit diff --git a/requirements.conda.yml b/requirements.conda.yml index 2493d0f03..219efe7e0 100644 --- a/requirements.conda.yml +++ b/requirements.conda.yml @@ -1,6 +1,7 @@ name: tiatoolbox channels: - conda + - conda-forge - defaults dependencies: - python=3.6 @@ -10,19 +11,11 @@ dependencies: - h5py=2.8.0 - matplotlib-base=3.1.3 - numpy=1.18.1 - - opencv=3.3.1 - - pandas=1.0.3 + - opencv=4.2 - pillow=7.0.0 - pip=20.0.2 - - pytorch=1.3.1 - pyyaml=5.3.1 - requests=2.23.0 - - scipy=1.4.1 - - scikit-image=0.16.2 - - scikit-learn=0.22.1 - - tensorflow-gpu==2.1.0 - - tqdm==4.46.0 - - colorama==0.4.3 - pathos==0.2.5 - pip: - openslide-python==1.1.1 diff --git a/requirements_dev.txt b/requirements_dev.txt index 6ee8e13e0..2cc4b7c22 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ -pip==19.2.3 +pip==20.0.2 bump2version==0.5.11 wheel==0.33.6 watchdog==0.9.0 diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 67421639e..e915a07ea 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -6,6 +6,7 @@ from tiatoolbox.dataloader.slide_info import slide_info from tiatoolbox.utils import misc_utils as misc from tiatoolbox import cli +from tiatoolbox import __version__ from click.testing import CliRunner import requests @@ -66,6 +67,15 @@ def test_command_line_help_interface(): assert help_result.output == result.output +def test_command_line_version(): + """ + pytest for version check + """ + runner = CliRunner() + version_result = runner.invoke(cli.main, ["-V"]) + assert __version__ in version_result.output + + def test_command_line_slide_info(response_ndpi, response_svs): """ Test the Slide information CLI. diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index aab0996c6..71b786ce8 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -1,6 +1,5 @@ """Top-level package for TIA Toolbox.""" from tiatoolbox import tiatoolbox -from tiatoolbox import cli from tiatoolbox import dataloader from tiatoolbox import utils diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 446a40e83..0b20c089d 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,4 +1,5 @@ """Console script for tiatoolbox.""" +from tiatoolbox import __version__ from tiatoolbox import dataloader from tiatoolbox.utils import misc_utils as misc import sys @@ -6,7 +7,16 @@ import os +def version_msg(): + """Return the Cookiecutter version, location and Python powering it.""" + python_version = sys.version[:3] + location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + message = 'tiatoolbox %(version)s from {} (Python {})' + return message.format(location, python_version) + + @click.group(context_settings=dict(help_option_names=["-h", "--help"])) +@click.version_option(__version__, '--version', '-V', help="Version", message=version_msg()) def main(): """ Computational pathology toolbox developed by TIALAB From a70831d36b2518d5cfc65db6134ab433ba809ed7 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 21:57:55 +0100 Subject: [PATCH 27/69] DEP: Remove tiatoolbox_Structure.jpg Remove tiatoolbox_Structure.jpg --- docs/tiatoolbox_Structure.jpg | Bin 50034 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/tiatoolbox_Structure.jpg diff --git a/docs/tiatoolbox_Structure.jpg b/docs/tiatoolbox_Structure.jpg deleted file mode 100644 index df588ed9d9d7d12350d88ad2484ba7dbafcdc97e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50034 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 Date: Thu, 4 Jun 2020 22:07:37 +0100 Subject: [PATCH 28/69] DOC: Update to README.rst according to address PR comments Update README.rst to replace python -m tiatoolbox -h to tiatoolbox --help Updated name to TIA Toolbox, it was changed as copied from previous version --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index fcef7bdde..fb79ca277 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,9 @@

-tiatoolbox -================== +=========== +TIA Toolbox +=========== Computational Pathology Toolbox developed by TIA Lab @@ -28,7 +29,7 @@ activate the conda environment: conda env create --name tiatoolbox --file requirements.conda.yml conda activate tiatoolbox -python -m tiatoolbox -h +tiatoolbox --help ======================= :: From 60e7d69d6834814295c4f5d9fd40194ef814ee35 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 22:16:18 +0100 Subject: [PATCH 29/69] DEV: Update travis yml for openslide tools Replaced sudo apt-get -y install python3-openslide to apt install openslide-tools as suggested in PR review --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6897a40ec..e3de657de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 before_install: - - sudo apt-get -y install python3-openslide + - apt install openslide-tools # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis From 08ab34285f03b19d60d6e4a519808b8f14ecb889 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 22:24:22 +0100 Subject: [PATCH 30/69] DOC: Update cli docstring Update cli docstring as suggested in PR #1 review --- tiatoolbox/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 0b20c089d..16fe173c4 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -8,7 +8,7 @@ def version_msg(): - """Return the Cookiecutter version, location and Python powering it.""" + """Return a string with tiatoolbox package version and python version.""" python_version = sys.version[:3] location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = 'tiatoolbox %(version)s from {} (Python {})' @@ -19,7 +19,7 @@ def version_msg(): @click.version_option(__version__, '--version', '-V', help="Version", message=version_msg()) def main(): """ - Computational pathology toolbox developed by TIALAB + Computational pathology toolbox developed by TIA LAB """ return 0 From 573fa085e67ebd7a49cb3d2631bc0e1c38548927 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 22:25:38 +0100 Subject: [PATCH 31/69] BUG: Reverting back to python3-openslide for travis openslide-tools has failed built on travis reverting this back. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e3de657de..a24b50228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 before_install: - - apt install openslide-tools + - apt-get -y install python3-openslide # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis From f547d2d97f42560195ecdd731660097219148225 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 22:28:08 +0100 Subject: [PATCH 32/69] BUG: Reverting back to python3-openslide for travis openslide-tools has failed built on travis reverting this back. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a24b50228..6897a40ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 before_install: - - apt-get -y install python3-openslide + - sudo apt-get -y install python3-openslide # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis From b0ff26ea762e1c8813e22f08259d285daaaa085e Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 23:06:14 +0100 Subject: [PATCH 33/69] DEV: Update wsireader Remove os for paths and replaced it with pathlib as suggested in the PR Removed openslide path for Windows nt, this should be user's responsibility. --- README.rst | 4 +--- tiatoolbox/dataloader/wsireader.py | 24 +++++++++--------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index fb79ca277..8d1045f81 100644 --- a/README.rst +++ b/README.rst @@ -19,9 +19,7 @@ Please try Getting Started =============== -First, install OpenSlide `here `__. For -Windows, extract the OpenSlide binaries at -*C:\\tools\\openslide\\openslide-win64-20171122*. Then, create and +First, install OpenSlide `here `__. Then, create and activate the conda environment: :: diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index b2d1fbe63..97d25159a 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -1,26 +1,20 @@ """ -This file contains WSIReader class for WSI reading or extracting metadata information from WSIs +WSIReader for WSI reading or extracting metadata information from WSIs """ -import os +import pathlib import numpy as np import yaml from PIL import Image -# For Windows Platforms to add path to openslide binaries -if os.name == "nt": - os.environ["PATH"] = ( - "C:\\tools\\openslide\\openslide-win64-20171122\\bin" + ";" + os.environ["PATH"] - ) - import openslide class WSIReader: def __init__( self, - input_dir=os.getcwd(), + input_dir=pathlib.Path.cwd(), file_name=None, - output_dir=os.path.join(os.getcwd(), "output"), + output_dir=pathlib.Path(pathlib.Path.cwd(), "output"), tile_objective_value=20, tile_read_size_w=5000, tile_read_size_h=5000, @@ -36,13 +30,13 @@ def __init__( tile_read_size_h: tile height, default=5000 """ - self.input_dir = input_dir - self.file_name = os.path.basename(file_name) + self.input_dir = pathlib.Path(input_dir) + self.file_name = pathlib.Path(file_name).name if output_dir is not None: - self.output_dir = os.path.join(output_dir, self.file_name) + self.output_dir = pathlib.Path(output_dir, self.file_name) self.openslide_obj = openslide.OpenSlide( - filename=os.path.join(self.input_dir, self.file_name) + filename=str(pathlib.Path(self.input_dir, self.file_name)) ) self.tile_objective_value = np.int(tile_objective_value) self.tile_read_size = np.array([tile_read_size_w, tile_read_size_h]) @@ -101,7 +95,7 @@ def slide_info(self, save_mode=True, output_dir=None, output_name=None): "level_downsamples": level_downsamples, } if save_mode: - with open(os.path.join(output_dir, output_name), "w") as yaml_file: + with open(pathlib.Path(output_dir, output_name), "w") as yaml_file: yaml.dump(param, yaml_file) else: return param From 3d9f6e47b12c7d160b3a1a612a8b0a1088f02bea Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 23:34:37 +0100 Subject: [PATCH 34/69] TST: Modify tests to using setup/teardown functions Update pytest fixtures in test_tiatoolbox.py to use setup/teardown functions https://docs.pytest.org/en/latest/fixture.html#fixture-finalization-executing-teardown-code --- tests/test_tiatoolbox.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index e915a07ea..6d6766512 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -15,32 +15,48 @@ @pytest.fixture -def response_ndpi(): +def response_ndpi(request): """ Sample pytest fixture for ndpi images Download ndpi image for pytest """ - if not os.path.isfile("./CMU-1.ndpi"): + ndpi_file_path = pathlib.Path(__file__).parent.joinpath("CMU-1.ndpi") + if not pathlib.Path.is_file(ndpi_file_path): r = requests.get( "http://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/CMU-1.ndpi" ) - with open("./CMU-1.ndpi", "wb") as f: + with open(ndpi_file_path, "wb") as f: f.write(r.content) + def close_ndpi(): + if pathlib.Path.is_file(ndpi_file_path): + os.remove(str(ndpi_file_path)) + + request.addfinalizer(close_ndpi) + return response_ndpi + @pytest.fixture -def response_svs(): +def response_svs(request): """ Sample pytest fixture for svs images Download ndpi image for pytest """ - if not os.path.isfile("./CMU-1.svs"): + svs_file_path = pathlib.Path(__file__).parent.joinpath("CMU-1.svs") + if not pathlib.Path.is_file(svs_file_path): r = requests.get( - "http://openslide.cs.cmu.edu/download/openslide-testdata/Aperio/CMU-1.svs" + "http://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/CMU-1.ndpi" ) - with open("./CMU-1.svs", "wb") as f: + with open(svs_file_path, "wb") as f: f.write(r.content) + def close_ndpi(): + if pathlib.Path.is_file(svs_file_path): + os.remove(str(svs_file_path)) + + request.addfinalizer(close_ndpi) + return response_svs + def test_slide_info(response_ndpi, response_svs): """ From 2ed615ee3e9eb275490d74f7622eb412863531e9 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 4 Jun 2020 23:35:59 +0100 Subject: [PATCH 35/69] STY: Update the style using black Update the code by running black --- tiatoolbox/cli.py | 6 ++++-- tiatoolbox/decorators/multiproc.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 16fe173c4..d4206cc1c 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -11,12 +11,14 @@ def version_msg(): """Return a string with tiatoolbox package version and python version.""" python_version = sys.version[:3] location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - message = 'tiatoolbox %(version)s from {} (Python {})' + message = "tiatoolbox %(version)s from {} (Python {})" return message.format(location, python_version) @click.group(context_settings=dict(help_option_names=["-h", "--help"])) -@click.version_option(__version__, '--version', '-V', help="Version", message=version_msg()) +@click.version_option( + __version__, "--version", "-V", help="Version", message=version_msg() +) def main(): """ Computational pathology toolbox developed by TIA LAB diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 281f65d04..8c6dc69f8 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -31,6 +31,7 @@ def __call__(self, func): Returns: """ + def func_wrap(*args, **kwargs): """ Wrapping function for decorator call From 2661de6172000ec304ce84268e608dafde026ed8 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Fri, 5 Jun 2020 15:51:45 +0100 Subject: [PATCH 36/69] DEV: Update travis yml for openslide tools Replaced sudo apt-get -y install python3-openslide to sudo apt-get -y install openslide-tools as suggested in PR review --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6897a40ec..181c7d6e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - 3.6 before_install: - - sudo apt-get -y install python3-openslide + - sudo apt-get -y install openslide-tools # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis From e180dd6fff7db91e5c2b5bc4f39184f29f3d2bfd Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 17:14:39 +0100 Subject: [PATCH 37/69] DOC: Update instructions for pip Update instructions for pip install --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 8d1045f81..e3b670efb 100644 --- a/README.rst +++ b/README.rst @@ -22,6 +22,15 @@ Getting Started First, install OpenSlide `here `__. Then, create and activate the conda environment: +pip +---- + +:: + + pip install -r requirements_dev.txt + +conda +----- :: conda env create --name tiatoolbox --file requirements.conda.yml From 08471841d567acab2cdc71183e8c3077ec50fd72 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 17:33:52 +0100 Subject: [PATCH 38/69] STY: Format import on one line Format import on one line PEP 328 --- tiatoolbox/dataloader/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py index 01e880caa..2c57bcb0a 100644 --- a/tiatoolbox/dataloader/__init__.py +++ b/tiatoolbox/dataloader/__init__.py @@ -1,2 +1 @@ -from tiatoolbox.dataloader import slide_info -from tiatoolbox.dataloader import wsireader +from tiatoolbox.dataloader import slide_info, wsireader From a2e2a8161b37bc117a4e97adf5f38f3ea11cf45c Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 17:41:33 +0100 Subject: [PATCH 39/69] MAINT: Rename misc_utils.py to misc.py Rename to misc_utils.py to misc.py and import utils.misc instead of misc_utils --- tests/test_tiatoolbox.py | 4 ++-- tiatoolbox/cli.py | 4 ++-- tiatoolbox/utils/__init__.py | 2 +- tiatoolbox/utils/{misc_utils.py => misc.py} | 0 4 files changed, 5 insertions(+), 5 deletions(-) rename tiatoolbox/utils/{misc_utils.py => misc.py} (100%) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 6d6766512..1efc1fd8a 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -4,7 +4,7 @@ import pytest from tiatoolbox.dataloader.slide_info import slide_info -from tiatoolbox.utils import misc_utils as misc +from tiatoolbox import utils from tiatoolbox import cli from tiatoolbox import __version__ @@ -65,7 +65,7 @@ def test_slide_info(response_ndpi, response_svs): # from bs4 import BeautifulSoup # assert 'GitHub' in BeautifulSoup(response.content).title.string file_types = ("*.ndpi", "*.svs", "*.mrxs") - files_all = misc.grab_files_from_dir( + files_all = utils.misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, ) _ = slide_info(input_path=files_all, workers=2, mode="save") diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index d4206cc1c..7492cf062 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,7 +1,7 @@ """Console script for tiatoolbox.""" from tiatoolbox import __version__ from tiatoolbox import dataloader -from tiatoolbox.utils import misc_utils as misc +from tiatoolbox import utils import sys import click import os @@ -52,7 +52,7 @@ def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ file_types = tuple(file_types.split(", ")) if os.path.isdir(wsi_input): - files_all = misc.grab_files_from_dir( + files_all = utils.misc.grab_files_from_dir( input_path=wsi_input, file_types=file_types ) elif os.path.isfile(wsi_input): diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py index 80ad14b45..f17bd98a3 100644 --- a/tiatoolbox/utils/__init__.py +++ b/tiatoolbox/utils/__init__.py @@ -1,4 +1,4 @@ """ Utils init file """ -from tiatoolbox.utils import misc_utils +from tiatoolbox.utils import misc diff --git a/tiatoolbox/utils/misc_utils.py b/tiatoolbox/utils/misc.py similarity index 100% rename from tiatoolbox/utils/misc_utils.py rename to tiatoolbox/utils/misc.py From 66dac0f02e1fb7b3d349c1b5239f2583b4bde614 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 17:53:17 +0100 Subject: [PATCH 40/69] DOC: Update docstring to add file type Update docstring to add file type in utils.misc.py --- tiatoolbox/utils/misc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 6dbf8486e..d15105161 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -1,5 +1,5 @@ """ -This file contains miscellaneous small functions repeatedly required and used in the repo +Miscellaneous small functions repeatedly required and used in the repo """ import os import pathlib @@ -27,8 +27,8 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): Grabs file paths specified by file extensions Args: - input_path: path to the directory where files need to be searched - file_types: file types (extensions) to be searched + input_path (str, pathlib.Path): path to the directory where files need to be searched + file_types (str, tuple): file types (extensions) to be searched Returns: list: file paths as a python list From b49ec891c499a589134dd43e5786bd0a7d8d39ff Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 18:07:17 +0100 Subject: [PATCH 41/69] DOC: Update docstring to add file type Update docstring to add file type in utils.misc.py --- tiatoolbox/dataloader/wsireader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 97d25159a..bbde4987c 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -1,5 +1,6 @@ """ WSIReader for WSI reading or extracting metadata information from WSIs + """ import pathlib import numpy as np @@ -28,6 +29,7 @@ def __init__( tile_objective_value: objective value at which tile is generated, default=20 tile_read_size_w: tile width, default=5000 tile_read_size_h: tile height, default=5000 + """ self.input_dir = pathlib.Path(input_dir) From e80e860f7b3a4e0fd1a934290556b6f301a8c41b Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 18:07:50 +0100 Subject: [PATCH 42/69] DOC: Update README.rst to include TIA logo Update README.rst to include TIA logo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e3b670efb..5f6f25426 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ .. raw:: html

- +

=========== From b9b6dd5f9940591a52441381911555f9348ae566 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 18:17:53 +0100 Subject: [PATCH 43/69] DOC: Update misc.py docstring to ammend Returns Update utils.misc.split_path_name_ext docstring to ammend Returns statement compatible with Google style. --- tests/test_tiatoolbox.py | 2 -- tiatoolbox/utils/misc.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 1efc1fd8a..54ded3415 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -62,8 +62,6 @@ def test_slide_info(response_ndpi, response_svs): """ pytest for slide_info as a python function """ - # from bs4 import BeautifulSoup - # assert 'GitHub' in BeautifulSoup(response.content).title.string file_types = ("*.ndpi", "*.svs", "*.mrxs") files_all = utils.misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index d15105161..a44c911a2 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -13,9 +13,9 @@ def split_path_name_ext(full_path): full_path: Path to a file Returns: - input_dir: directory path - file_name: name of the file without extension - ext: file extension + tuple: Three sections of the input file path + (input directory path, file name, file extension) + """ input_dir, file_name = os.path.split(full_path) file_name, ext = os.path.splitext(file_name) From ff3073b1be351b2ea38649ea5d142823d551eb32 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 19:35:45 +0100 Subject: [PATCH 44/69] DEP: output only dictionary from slide_info output dictionary from slide info and allow the user to save yaml or in any other format --- tests/test_tiatoolbox.py | 5 ++++- tiatoolbox/dataloader/slide_info.py | 27 ++++++--------------------- tiatoolbox/dataloader/wsireader.py | 17 +++++------------ tiatoolbox/utils/misc.py | 6 ++++++ 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 54ded3415..43c6367fe 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -66,7 +66,10 @@ def test_slide_info(response_ndpi, response_svs): files_all = utils.misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, ) - _ = slide_info(input_path=files_all, workers=2, mode="save") + slide_params = slide_info(input_path=files_all, workers=2, mode="save") + + for slide_param in slide_params: + utils.misc.save_yaml(slide_param, slide_param['file_name'] + '.yaml') def test_command_line_help_interface(): diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 28d087b53..00b0204c9 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -8,14 +8,12 @@ @TIAMultiProcess(iter_on="input_path") -def slide_info(input_path, output_dir=None, mode="show"): +def slide_info(input_path, output_dir=None): """ Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel Args: input_path: Path to whole slide image output_dir: Path to output directory to save the output - mode: "show" to display meta information only or "save" to save the meta information - Returns: displays or saves WSI meta information @@ -23,15 +21,6 @@ def slide_info(input_path, output_dir=None, mode="show"): input_dir, file_name = os.path.split(input_path) - if output_dir is None: - output_dir = os.path.join(input_dir, "..", "meta") - - if mode is None: - mode = "show" - - if not os.path.isdir(output_dir) and mode == "save": - os.makedirs(output_dir, exist_ok=True) - print(file_name, flush=True) _, file_type = os.path.splitext(file_name) @@ -39,12 +28,8 @@ def slide_info(input_path, output_dir=None, mode="show"): wsi_reader = wsireader.WSIReader( input_dir=input_dir, file_name=file_name, output_dir=output_dir ) - if mode == "show": - info = wsi_reader.slide_info(save_mode=False) - print(info) - return info - else: - wsi_reader.slide_info( - output_dir=output_dir, output_name=file_name + ".yaml" - ) - return os.path.join(output_dir, file_name + ".yaml") + info = wsi_reader.slide_info() + return info + else: + print('File type not supported') + return None diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index bbde4987c..f3b728e64 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -4,7 +4,6 @@ """ import pathlib import numpy as np -import yaml from PIL import Image import openslide @@ -52,14 +51,11 @@ def __init__( def __exit__(self): self.openslide_obj.close() - def slide_info(self, save_mode=True, output_dir=None, output_name=None): + def slide_info(self, output_dir=None): """ WSI meta data reader Args: - save_mode: save meta information as yaml file output_dir: output directory to save the meta information - output_name: output file name - Returns: displays or saves WSI meta information @@ -67,8 +63,6 @@ def slide_info(self, save_mode=True, output_dir=None, output_name=None): input_dir = self.input_dir if output_dir is None: self.output_dir = output_dir - if output_name is None: - output_name = "param.yaml" if self.objective_power == 0: self.objective_power = np.int( self.openslide_obj.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER] @@ -82,6 +76,7 @@ def slide_info(self, save_mode=True, output_dir=None, output_name=None): level_count = self.level_count level_dimensions = self.level_dimensions level_downsamples = self.level_downsamples + file_name = self.file_name param = { "input_dir": input_dir, @@ -95,9 +90,7 @@ def slide_info(self, save_mode=True, output_dir=None, output_name=None): "level_count": level_count, "level_dimensions": level_dimensions, "level_downsamples": level_downsamples, + "file_name": file_name } - if save_mode: - with open(pathlib.Path(output_dir, output_name), "w") as yaml_file: - yaml.dump(param, yaml_file) - else: - return param + + return param diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index a44c911a2..3ad48c9d8 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -3,6 +3,7 @@ """ import os import pathlib +import yaml def split_path_name_ext(full_path): @@ -46,3 +47,8 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): files_grabbed.extend(input_path.glob(files)) return list(files_grabbed) + + +def save_yaml(input_dict, output_path="output.yaml"): + with open(pathlib.Path(output_path), "w") as yaml_file: + yaml.dump(input_dict, yaml_file) From ad355c4d5196edddfa34cecf3bcc3157ee216938 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 19:36:47 +0100 Subject: [PATCH 45/69] STY: Run black on python code Run black on python code for consistent formatting. --- tests/test_tiatoolbox.py | 2 +- tiatoolbox/dataloader/slide_info.py | 2 +- tiatoolbox/dataloader/wsireader.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 43c6367fe..82d7e6109 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -69,7 +69,7 @@ def test_slide_info(response_ndpi, response_svs): slide_params = slide_info(input_path=files_all, workers=2, mode="save") for slide_param in slide_params: - utils.misc.save_yaml(slide_param, slide_param['file_name'] + '.yaml') + utils.misc.save_yaml(slide_param, slide_param["file_name"] + ".yaml") def test_command_line_help_interface(): diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 00b0204c9..87c48fa90 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -31,5 +31,5 @@ def slide_info(input_path, output_dir=None): info = wsi_reader.slide_info() return info else: - print('File type not supported') + print("File type not supported") return None diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index f3b728e64..fbea6a375 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -90,7 +90,7 @@ def slide_info(self, output_dir=None): "level_count": level_count, "level_dimensions": level_dimensions, "level_downsamples": level_downsamples, - "file_name": file_name + "file_name": file_name, } return param From d7ea162bf62a1a8a903caaa1a3024f6c9aa7bf40 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 20:06:51 +0100 Subject: [PATCH 46/69] MAINT: Update multiproc to raise error instead of print Update multiproc to raise error instead of print --- tiatoolbox/decorators/multiproc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 8c6dc69f8..39df173f3 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -3,9 +3,10 @@ """ import multiprocessing -from pathos.multiprocessing import ProcessingPool as Pool from functools import partial +from pathos.multiprocessing import ProcessingPool as Pool + class TIAMultiProcess: """ @@ -48,7 +49,7 @@ def func_wrap(*args, **kwargs): try: iter_value = kwargs.pop(self.iter_on) except ValueError: - print("Please specify iter_on in function decorator") + raise ValueError("Please specify iter_on in multiprocessing decorator") with Pool(self.workers) as p: results = p.map(partial(func, **kwargs), iter_value,) From dc5d1448ba242c66be11d45c8de4a5a9bddf6cd3 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 20:18:08 +0100 Subject: [PATCH 47/69] DOC: Update slide_info docstring Update slide_info docstring --- tiatoolbox/dataloader/slide_info.py | 10 ++++++++++ tiatoolbox/dataloader/wsireader.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 87c48fa90..39b60f515 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -17,6 +17,16 @@ def slide_info(input_path, output_dir=None): Returns: displays or saves WSI meta information + Examples: + >>> from tiatoolbox import utils + >>> file_types = ("*.ndpi", "*.svs", "*.mrxs") + >>> files_all = utils.misc.grab_files_from_dir(input_path, file_types=file_types) + >>> slide_params = slide_info(input_path=files_all, workers=2) + + >>> for slide_param in slide_params: + >>> utils.misc.save_yaml(slide_param, slide_param["file_name"] + ".yaml") + >>> print(type(slide_param)) + """ input_dir, file_name = os.path.split(input_path) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index fbea6a375..bb7b67bcf 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -57,7 +57,7 @@ def slide_info(self, output_dir=None): Args: output_dir: output directory to save the meta information Returns: - displays or saves WSI meta information + param (dict): dictionary containing meta information """ input_dir = self.input_dir From d3af79a426df380f0595a34a323f708b08a856ef Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Mon, 15 Jun 2020 22:23:50 +0100 Subject: [PATCH 48/69] DOC: Update save_yaml doc in misc.py Update save_yaml docstring in misc.py --- tiatoolbox/utils/misc.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 3ad48c9d8..e941d8c67 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -50,5 +50,19 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): def save_yaml(input_dict, output_path="output.yaml"): + """ + Save dictionary as yaml + Args: + input_dict: A variable of type 'dict' + output_path: Path to save the output file + + Returns: + + Examples: + >>> input_dict = {'hello': 'Hello World!'} + >>> save_yaml(input_dict, './hello.yaml') + + + """ with open(pathlib.Path(output_path), "w") as yaml_file: yaml.dump(input_dict, yaml_file) From 322286c03ef28bef03baf85ff634adcfd30433b1 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 16 Jun 2020 17:25:37 +0100 Subject: [PATCH 49/69] DOC: Update docstring for sphinx Update docstring in the repo to make it readable and well structured for html and latexpdf --- README.rst | 2 +- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/usage.rst | 23 ++++++++++++++++ tiatoolbox/__init__.py | 3 +++ tiatoolbox/dataloader/__init__.py | 4 +++ tiatoolbox/dataloader/slide_info.py | 5 ++-- tiatoolbox/dataloader/wsireader.py | 41 +++++++++++++++++++---------- tiatoolbox/decorators/__init__.py | 2 +- tiatoolbox/decorators/multiproc.py | 17 +++++++++--- tiatoolbox/utils/__init__.py | 2 +- tiatoolbox/utils/misc.py | 27 +++++++++++++------ 12 files changed, 98 insertions(+), 32 deletions(-) diff --git a/README.rst b/README.rst index 5f6f25426..e4ede8a06 100644 --- a/README.rst +++ b/README.rst @@ -51,6 +51,6 @@ tiatoolbox --help optional arguments: -h, --help show this help message and exit - --version show program's version number and exit + --version show program`s version number and exit --verbose VERBOSE diff --git a/docs/conf.py b/docs/conf.py index 417225b0d..ce9d90bf5 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/index.rst b/docs/index.rst index 20666ba40..decd3e255 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ Welcome to TIA Toolbox's documentation! -====================================== +======================================= .. toctree:: :maxdepth: 2 diff --git a/docs/usage.rst b/docs/usage.rst index b707c0338..ac9abe63b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -5,3 +5,26 @@ Usage To use TIA Toolbox in a project:: import tiatoolbox + + +---------- +Dataloader +---------- +.. autoclass:: tiatoolbox.dataloader.wsireader.WSIReader + :members: __init__, slide_info + +.. automodule:: tiatoolbox.dataloader.slide_info + :members: slide_info + +---------- +Decorators +---------- +.. automodule:: tiatoolbox.decorators.multiproc + :members: TIAMultiProcess + +------ +Utils +------ +.. automodule:: tiatoolbox.utils +.. automodule:: tiatoolbox.utils.misc + :members: save_yaml, split_path_name_ext, grab_files_from_dir diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index 71b786ce8..2cb5fc0d7 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -7,3 +7,6 @@ __author__ = """TIA Lab""" __email__ = "tialab@dcs.warwick.ac.uk" __version__ = "0.1.1" + +if __name__ == '__main__': + pass diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py index 2c57bcb0a..80d95d249 100644 --- a/tiatoolbox/dataloader/__init__.py +++ b/tiatoolbox/dataloader/__init__.py @@ -1 +1,5 @@ +""" +Package to read whole slide images +""" + from tiatoolbox.dataloader import slide_info, wsireader diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 39b60f515..d2f7d9f38 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -1,5 +1,5 @@ """ -This file contains code to output or save slide information using python multiprocessing +Get Slide Meta Data information """ from tiatoolbox.dataloader import wsireader from tiatoolbox.decorators.multiproc import TIAMultiProcess @@ -10,6 +10,7 @@ @TIAMultiProcess(iter_on="input_path") def slide_info(input_path, output_dir=None): """ + slide_info() Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel Args: input_path: Path to whole slide image @@ -18,11 +19,11 @@ def slide_info(input_path, output_dir=None): displays or saves WSI meta information Examples: + >>> from tiatoolbox.dataloader.slide_info import slide_info >>> from tiatoolbox import utils >>> file_types = ("*.ndpi", "*.svs", "*.mrxs") >>> files_all = utils.misc.grab_files_from_dir(input_path, file_types=file_types) >>> slide_params = slide_info(input_path=files_all, workers=2) - >>> for slide_param in slide_params: >>> utils.misc.save_yaml(slide_param, slide_param["file_name"] + ".yaml") >>> print(type(slide_param)) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index bb7b67bcf..c1b5ec808 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -10,24 +10,40 @@ class WSIReader: + """ + WSI Reader class to read WSI images + + Attributes: + input_dir (pathlib.Path): input path to WSI directory + file_name (str): file name of the WSI + output_dir (pathlib.Path): output directory to save the output + openslide_obj (:obj:`openslide.OpenSlide`) + tile_objective_value (int): objective value at which tile is generated + tile_read_size (int): [tile width, tile height] + objective_power (int): objective value at which whole slide image is scanned + level_count (int): The number of pyramid levels in the slide + level_dimensions = A list of `(width, height)` tuples, one for each level of the slide + level_downsamples = A list of down sample factors for each level of the slide + + """ + def __init__( self, - input_dir=pathlib.Path.cwd(), + input_dir=".", file_name=None, - output_dir=pathlib.Path(pathlib.Path.cwd(), "output"), + output_dir="./output", tile_objective_value=20, tile_read_size_w=5000, tile_read_size_h=5000, ): """ - WSI Reader class to read WSI images Args: - input_dir: input path to WSI directory - file_name: file name of the WSI - output_dir: output directory to save the output, default=os.getcwd()/output - tile_objective_value: objective value at which tile is generated, default=20 - tile_read_size_w: tile width, default=5000 - tile_read_size_h: tile height, default=5000 + input_dir (str, pathlib.Path): input path to WSI directory + file_name (str): file name of the WSI + output_dir (str, pathlib.Path): output directory to save the output, default=os.getcwd()/output + tile_objective_value (int): objective value at which tile is generated, default=20 + tile_read_size_w (int): tile width, default=5000 + tile_read_size_h (int): tile height, default=5000 """ @@ -51,18 +67,16 @@ def __init__( def __exit__(self): self.openslide_obj.close() - def slide_info(self, output_dir=None): + def slide_info(self): """ WSI meta data reader Args: - output_dir: output directory to save the meta information + Returns: param (dict): dictionary containing meta information """ input_dir = self.input_dir - if output_dir is None: - self.output_dir = output_dir if self.objective_power == 0: self.objective_power = np.int( self.openslide_obj.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER] @@ -80,7 +94,6 @@ def slide_info(self, output_dir=None): param = { "input_dir": input_dir, - "output_dir": output_dir, "objective_power": objective_power, "slide_dimension": slide_dimension, "rescale": rescale, diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py index 798c1e60f..a4a18d2d6 100644 --- a/tiatoolbox/decorators/__init__.py +++ b/tiatoolbox/decorators/__init__.py @@ -1,4 +1,4 @@ """ -Decorators init file +Decorator class and functions for the toolbox """ from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 39df173f3..26e9bafb2 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -1,5 +1,5 @@ """ -This file defines multiprocessing decorators required by the tiatoolbox. +Multiprocessing decorators required by the tiatoolbox. """ import multiprocessing @@ -10,8 +10,18 @@ class TIAMultiProcess: """ - This class defines the multiprocessing decorator for the toolbox, requires a list iter_on as input on which + Multiprocessing class decorator for the toolbox, requires a list `iter_on` as input on which multiprocessing will run + + Examples: + >>> from tiatoolbox.decorators.multiproc import TIAMultiProcess + >>> import cv2 + >>> @TIAMultiProcess(iter_on="input_path") + >>> def read_images(input_path, output_dir=None): + >>> img = cv2.imread(input_path) + >>> return img + >>> imgs = read_images(input_path) + """ def __init__(self, iter_on): @@ -43,7 +53,6 @@ def func_wrap(*args, **kwargs): Returns: """ - iter_value = None if "workers" in kwargs: self.workers = kwargs.pop("workers") try: @@ -57,4 +66,6 @@ def func_wrap(*args, **kwargs): return results + func_wrap.__doc__ = func.__doc__ + return func_wrap diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py index f17bd98a3..e666c9c67 100644 --- a/tiatoolbox/utils/__init__.py +++ b/tiatoolbox/utils/__init__.py @@ -1,4 +1,4 @@ """ -Utils init file +Utils package for toolbox utilities """ from tiatoolbox.utils import misc diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index e941d8c67..92f9e527e 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -1,5 +1,5 @@ """ -Miscellaneous small functions repeatedly required and used in the repo +Miscellaneous small functions repeatedly used in tiatoolbox """ import os import pathlib @@ -11,28 +11,39 @@ def split_path_name_ext(full_path): Split path of a file to directory path, file name and extension Args: - full_path: Path to a file + full_path (str): Path to a file Returns: tuple: Three sections of the input file path (input directory path, file name, file extension) + Examples: + >>> from tiatoolbox import utils + >>> dir_path, file_name, extension = utils.misc.split_path_name_ext(full_path) + """ input_dir, file_name = os.path.split(full_path) file_name, ext = os.path.splitext(file_name) return input_dir, file_name, ext -def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): +def grab_files_from_dir(input_path, + file_types=("*.jpg", "*.png", "*.tif")): """ Grabs file paths specified by file extensions Args: - input_path (str, pathlib.Path): path to the directory where files need to be searched - file_types (str, tuple): file types (extensions) to be searched + input_path (str, pathlib.Path): Path to the directory where files need to be searched + file_types (str, tuple): File types (extensions) to be searched Returns: - list: file paths as a python list + list: File paths as a python list + + Examples: + >>> from tiatoolbox import utils + >>> file_types = ("*.ndpi", "*.svs", "*.mrxs") + >>> files_all = utils.misc.grab_files_from_dir(input_path, file_types=file_types,) + """ input_path = pathlib.Path(input_path) @@ -53,8 +64,8 @@ def save_yaml(input_dict, output_path="output.yaml"): """ Save dictionary as yaml Args: - input_dict: A variable of type 'dict' - output_path: Path to save the output file + input_dict (dict): A variable of type 'dict' + output_path (str, pathlib.Path): Path to save the output file Returns: From 8849f62933e2567b05a54e07fa0a8a2d347d6947 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Tue, 16 Jun 2020 17:26:21 +0100 Subject: [PATCH 50/69] STY: Update style using black Update style using black --- tiatoolbox/__init__.py | 2 +- tiatoolbox/utils/misc.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index 2cb5fc0d7..f4eee6be9 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -8,5 +8,5 @@ __email__ = "tialab@dcs.warwick.ac.uk" __version__ = "0.1.1" -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 92f9e527e..83b71060b 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -27,8 +27,7 @@ def split_path_name_ext(full_path): return input_dir, file_name, ext -def grab_files_from_dir(input_path, - file_types=("*.jpg", "*.png", "*.tif")): +def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): """ Grabs file paths specified by file extensions From 397051322b499b9f212ccfcb224276801ec357ee Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 13:17:36 +0100 Subject: [PATCH 51/69] DOC: Update usage.rst to reformat html and pdf Update usage.rst to reformat for html and pdf --- docs/usage.rst | 25 +++++++++++++++++++++++-- tiatoolbox/decorators/__init__.py | 2 +- tiatoolbox/decorators/multiproc.py | 6 ++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index ac9abe63b..2c7c1b5c9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -10,8 +10,19 @@ To use TIA Toolbox in a project:: ---------- Dataloader ---------- -.. autoclass:: tiatoolbox.dataloader.wsireader.WSIReader - :members: __init__, slide_info +.. automodule:: tiatoolbox.dataloader + +^^^^^^^^^^^^^^^^^^^^ +dataloader.wsireader +^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: tiatoolbox.dataloader.wsireader + :members: WSIReader + :special-members: __init__ + +^^^^^^^^^^^^^^^^^^^^^ +dataloader.slide_info +^^^^^^^^^^^^^^^^^^^^^ .. automodule:: tiatoolbox.dataloader.slide_info :members: slide_info @@ -19,12 +30,22 @@ Dataloader ---------- Decorators ---------- +.. automodule:: tiatoolbox.decorators + +^^^^^^^^^^^^^^^^^^^^ +decorators.multiproc +^^^^^^^^^^^^^^^^^^^^ .. automodule:: tiatoolbox.decorators.multiproc :members: TIAMultiProcess + :special-members: __init__, __call__ ------ Utils ------ .. automodule:: tiatoolbox.utils + +^^^^^^^^^^ +utils.misc +^^^^^^^^^^ .. automodule:: tiatoolbox.utils.misc :members: save_yaml, split_path_name_ext, grab_files_from_dir diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py index a4a18d2d6..80ac0c0eb 100644 --- a/tiatoolbox/decorators/__init__.py +++ b/tiatoolbox/decorators/__init__.py @@ -1,4 +1,4 @@ """ -Decorator class and functions for the toolbox +Package defines decorators for the toolbox """ from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 26e9bafb2..01fc80ec1 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -13,6 +13,10 @@ class TIAMultiProcess: Multiprocessing class decorator for the toolbox, requires a list `iter_on` as input on which multiprocessing will run + Attributes: + iter_on (str): Variable on which iterations will be performed. + workers (int): num of cpu cores to use for multiprocessing. + Examples: >>> from tiatoolbox.decorators.multiproc import TIAMultiProcess >>> import cv2 @@ -26,7 +30,6 @@ class TIAMultiProcess: def __init__(self, iter_on): """ - __init__ function for TIAMultiProcess decorator Args: iter_on: Variable on which iterations will be performed. """ @@ -35,7 +38,6 @@ def __init__(self, iter_on): def __call__(self, func): """ - This is the function which will be called on a function on which decorator is applied Args: func: function to be run with multiprocessing From 8b11fc81b1d7f1c900d0b21c326fda24b7fab631 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 15:18:57 +0100 Subject: [PATCH 52/69] DOC: Update docstring with space to make it consistent Update docstring with space to make it consistent --- tiatoolbox/__init__.py | 5 ++++- tiatoolbox/__main__.py | 1 + tiatoolbox/cli.py | 12 ++++++++++-- tiatoolbox/dataloader/__init__.py | 1 + tiatoolbox/dataloader/slide_info.py | 2 ++ tiatoolbox/decorators/__init__.py | 1 + tiatoolbox/decorators/multiproc.py | 1 + tiatoolbox/tiatoolbox.py | 5 ++++- tiatoolbox/utils/__init__.py | 1 + tiatoolbox/utils/misc.py | 1 + 10 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index f4eee6be9..e6e43b955 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -1,4 +1,7 @@ -"""Top-level package for TIA Toolbox.""" +""" +Top-level package for TIA Toolbox. + +""" from tiatoolbox import tiatoolbox from tiatoolbox import dataloader from tiatoolbox import utils diff --git a/tiatoolbox/__main__.py b/tiatoolbox/__main__.py index cf1e36900..7ca007bde 100644 --- a/tiatoolbox/__main__.py +++ b/tiatoolbox/__main__.py @@ -1,5 +1,6 @@ """ __main__ file invoked with `python -m tiatoolbox` command + """ from tiatoolbox.cli import main diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 7492cf062..984288d59 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,4 +1,7 @@ -"""Console script for tiatoolbox.""" +""" +Console script for tiatoolbox. + +""" from tiatoolbox import __version__ from tiatoolbox import dataloader from tiatoolbox import utils @@ -8,7 +11,10 @@ def version_msg(): - """Return a string with tiatoolbox package version and python version.""" + """ + Return a string with tiatoolbox package version and python version. + + """ python_version = sys.version[:3] location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = "tiatoolbox %(version)s from {} (Python {})" @@ -22,6 +28,7 @@ def version_msg(): def main(): """ Computational pathology toolbox developed by TIA LAB + """ return 0 @@ -49,6 +56,7 @@ def main(): def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """ Displays or saves WSI metadata + """ file_types = tuple(file_types.split(", ")) if os.path.isdir(wsi_input): diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py index 80d95d249..90d5aacb2 100644 --- a/tiatoolbox/dataloader/__init__.py +++ b/tiatoolbox/dataloader/__init__.py @@ -1,5 +1,6 @@ """ Package to read whole slide images + """ from tiatoolbox.dataloader import slide_info, wsireader diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index d2f7d9f38..165a88b27 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -1,5 +1,6 @@ """ Get Slide Meta Data information + """ from tiatoolbox.dataloader import wsireader from tiatoolbox.decorators.multiproc import TIAMultiProcess @@ -12,6 +13,7 @@ def slide_info(input_path, output_dir=None): """ slide_info() Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel + Args: input_path: Path to whole slide image output_dir: Path to output directory to save the output diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py index 80ac0c0eb..efea53d3b 100644 --- a/tiatoolbox/decorators/__init__.py +++ b/tiatoolbox/decorators/__init__.py @@ -1,4 +1,5 @@ """ Package defines decorators for the toolbox + """ from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 01fc80ec1..d036775da 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -1,5 +1,6 @@ """ Multiprocessing decorators required by the tiatoolbox. + """ import multiprocessing diff --git a/tiatoolbox/tiatoolbox.py b/tiatoolbox/tiatoolbox.py index dd0b80ede..f67686986 100644 --- a/tiatoolbox/tiatoolbox.py +++ b/tiatoolbox/tiatoolbox.py @@ -1 +1,4 @@ -"""Main module.""" +""" +Main module. + +""" diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py index e666c9c67..013e2be90 100644 --- a/tiatoolbox/utils/__init__.py +++ b/tiatoolbox/utils/__init__.py @@ -1,4 +1,5 @@ """ Utils package for toolbox utilities + """ from tiatoolbox.utils import misc diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 83b71060b..26c18a462 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -1,5 +1,6 @@ """ Miscellaneous small functions repeatedly used in tiatoolbox + """ import os import pathlib From 9cbb54bd38131dbfcbe2b4e54a0da5dd46edf77c Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 15:30:15 +0100 Subject: [PATCH 53/69] MAINT: Update location of logo Update location of the logo after restructuring tiatoolbox webpage. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e4ede8a06..822719eab 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ .. raw:: html

- +

=========== From 3fbd9425208cbf577afc9e376aa7074cb62514dd Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 15:58:16 +0100 Subject: [PATCH 54/69] DOC: Update docstring to fix One-line docstring error Update docstring to fix One-line docstring should fit on one line with quotes error --- tests/test_tiatoolbox.py | 16 ++++------------ tiatoolbox/__init__.py | 5 +---- tiatoolbox/__main__.py | 5 +---- tiatoolbox/cli.py | 20 ++++---------------- tiatoolbox/dataloader/__init__.py | 5 +---- tiatoolbox/dataloader/slide_info.py | 5 +---- tiatoolbox/dataloader/wsireader.py | 6 ++---- tiatoolbox/decorators/__init__.py | 5 +---- tiatoolbox/decorators/multiproc.py | 5 +---- tiatoolbox/tiatoolbox.py | 5 +---- tiatoolbox/utils/__init__.py | 5 +---- tiatoolbox/utils/misc.py | 5 +---- 12 files changed, 19 insertions(+), 68 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 82d7e6109..07848d977 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -59,9 +59,7 @@ def close_ndpi(): def test_slide_info(response_ndpi, response_svs): - """ - pytest for slide_info as a python function - """ + """pytest for slide_info as a python function""" file_types = ("*.ndpi", "*.svs", "*.mrxs") files_all = utils.misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, @@ -73,9 +71,7 @@ def test_slide_info(response_ndpi, response_svs): def test_command_line_help_interface(): - """ - Test the CLI help - """ + """Test the CLI help""" runner = CliRunner() result = runner.invoke(cli.main) assert result.exit_code == 0 @@ -85,18 +81,14 @@ def test_command_line_help_interface(): def test_command_line_version(): - """ - pytest for version check - """ + """pytest for version check""" runner = CliRunner() version_result = runner.invoke(cli.main, ["-V"]) assert __version__ in version_result.output def test_command_line_slide_info(response_ndpi, response_svs): - """ - Test the Slide information CLI. - """ + """Test the Slide information CLI.""" runner = CliRunner() slide_info_result = runner.invoke( cli.main, diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py index e6e43b955..f4eee6be9 100644 --- a/tiatoolbox/__init__.py +++ b/tiatoolbox/__init__.py @@ -1,7 +1,4 @@ -""" -Top-level package for TIA Toolbox. - -""" +"""Top-level package for TIA Toolbox.""" from tiatoolbox import tiatoolbox from tiatoolbox import dataloader from tiatoolbox import utils diff --git a/tiatoolbox/__main__.py b/tiatoolbox/__main__.py index 7ca007bde..8f4c3111c 100644 --- a/tiatoolbox/__main__.py +++ b/tiatoolbox/__main__.py @@ -1,7 +1,4 @@ -""" -__main__ file invoked with `python -m tiatoolbox` command - -""" +"""__main__ file invoked with `python -m tiatoolbox` command""" from tiatoolbox.cli import main diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 984288d59..5d0db6230 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -1,7 +1,4 @@ -""" -Console script for tiatoolbox. - -""" +"""Console script for tiatoolbox.""" from tiatoolbox import __version__ from tiatoolbox import dataloader from tiatoolbox import utils @@ -11,10 +8,7 @@ def version_msg(): - """ - Return a string with tiatoolbox package version and python version. - - """ + """Return a string with tiatoolbox package version and python version.""" python_version = sys.version[:3] location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) message = "tiatoolbox %(version)s from {} (Python {})" @@ -26,10 +20,7 @@ def version_msg(): __version__, "--version", "-V", help="Version", message=version_msg() ) def main(): - """ - Computational pathology toolbox developed by TIA LAB - - """ + """Computational pathology toolbox developed by TIA LAB""" return 0 @@ -54,10 +45,7 @@ def main(): help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", ) def slide_info(wsi_input, output_dir, file_types, mode, workers=None): - """ - Displays or saves WSI metadata - - """ + """Displays or saves WSI metadata""" file_types = tuple(file_types.split(", ")) if os.path.isdir(wsi_input): files_all = utils.misc.grab_files_from_dir( diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py index 90d5aacb2..36212b688 100644 --- a/tiatoolbox/dataloader/__init__.py +++ b/tiatoolbox/dataloader/__init__.py @@ -1,6 +1,3 @@ -""" -Package to read whole slide images - -""" +"""Package to read whole slide images""" from tiatoolbox.dataloader import slide_info, wsireader diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 165a88b27..712adef69 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -1,7 +1,4 @@ -""" -Get Slide Meta Data information - -""" +"""Get Slide Meta Data information""" from tiatoolbox.dataloader import wsireader from tiatoolbox.decorators.multiproc import TIAMultiProcess diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index c1b5ec808..4cbaad1e6 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -1,10 +1,8 @@ -""" -WSIReader for WSI reading or extracting metadata information from WSIs +"""WSIReader for WSI reading or extracting metadata information from WSIs""" -""" import pathlib import numpy as np -from PIL import Image +# from PIL import Image import openslide diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py index efea53d3b..d9608869c 100644 --- a/tiatoolbox/decorators/__init__.py +++ b/tiatoolbox/decorators/__init__.py @@ -1,5 +1,2 @@ -""" -Package defines decorators for the toolbox - -""" +"""Package defines decorators for the toolbox""" from tiatoolbox.decorators import multiproc diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index d036775da..a90b20902 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -1,7 +1,4 @@ -""" -Multiprocessing decorators required by the tiatoolbox. - -""" +"""Multiprocessing decorators required by the tiatoolbox.""" import multiprocessing from functools import partial diff --git a/tiatoolbox/tiatoolbox.py b/tiatoolbox/tiatoolbox.py index f67686986..dd0b80ede 100644 --- a/tiatoolbox/tiatoolbox.py +++ b/tiatoolbox/tiatoolbox.py @@ -1,4 +1 @@ -""" -Main module. - -""" +"""Main module.""" diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py index 013e2be90..1dabdcb08 100644 --- a/tiatoolbox/utils/__init__.py +++ b/tiatoolbox/utils/__init__.py @@ -1,5 +1,2 @@ -""" -Utils package for toolbox utilities - -""" +"""Utils package for toolbox utilities""" from tiatoolbox.utils import misc diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 26c18a462..719d80cae 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -1,7 +1,4 @@ -""" -Miscellaneous small functions repeatedly used in tiatoolbox - -""" +"""Miscellaneous small functions repeatedly used in tiatoolbox""" import os import pathlib import yaml From 90b7140e3d2ab5fc7fe09bfe606098e907b5d313 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:14:52 +0100 Subject: [PATCH 55/69] DOC: Update docstring to fix Doc line too long error Update docstring to fix Doc line too long error --- tiatoolbox/dataloader/slide_info.py | 11 +++++++---- tiatoolbox/dataloader/wsireader.py | 11 +++++++---- tiatoolbox/decorators/multiproc.py | 10 +++++----- tiatoolbox/utils/misc.py | 6 ++++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 712adef69..a48e9d832 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -9,7 +9,8 @@ def slide_info(input_path, output_dir=None): """ slide_info() - Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel + Single file run to output or save WSI meta data. Multiprocessing uses this function + to run slide_info in parallel Args: input_path: Path to whole slide image @@ -21,11 +22,13 @@ def slide_info(input_path, output_dir=None): >>> from tiatoolbox.dataloader.slide_info import slide_info >>> from tiatoolbox import utils >>> file_types = ("*.ndpi", "*.svs", "*.mrxs") - >>> files_all = utils.misc.grab_files_from_dir(input_path, file_types=file_types) + >>> files_all = utils.misc.grab_files_from_dir(input_path, + ... file_types=file_types) >>> slide_params = slide_info(input_path=files_all, workers=2) >>> for slide_param in slide_params: - >>> utils.misc.save_yaml(slide_param, slide_param["file_name"] + ".yaml") - >>> print(type(slide_param)) + ... utils.misc.save_yaml(slide_param, + ... slide_param["file_name"] + ".yaml") + ... print(slide_param) """ diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 4cbaad1e6..565f145cd 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -20,8 +20,9 @@ class WSIReader: tile_read_size (int): [tile width, tile height] objective_power (int): objective value at which whole slide image is scanned level_count (int): The number of pyramid levels in the slide - level_dimensions = A list of `(width, height)` tuples, one for each level of the slide - level_downsamples = A list of down sample factors for each level of the slide + level_dimensions (int): A list of `(width, height)` tuples, one for each level + of the slide + level_downsamples (int): A list of down sample factors for each level of the slide """ @@ -38,8 +39,10 @@ def __init__( Args: input_dir (str, pathlib.Path): input path to WSI directory file_name (str): file name of the WSI - output_dir (str, pathlib.Path): output directory to save the output, default=os.getcwd()/output - tile_objective_value (int): objective value at which tile is generated, default=20 + output_dir (str, pathlib.Path): output directory to save the output, + default=./output + tile_objective_value (int): objective value at which tile is generated, + default=20 tile_read_size_w (int): tile width, default=5000 tile_read_size_h (int): tile height, default=5000 diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index a90b20902..2a7a4c78e 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -8,8 +8,8 @@ class TIAMultiProcess: """ - Multiprocessing class decorator for the toolbox, requires a list `iter_on` as input on which - multiprocessing will run + Multiprocessing class decorator for the toolbox, requires a list `iter_on` + as input on which multiprocessing will run Attributes: iter_on (str): Variable on which iterations will be performed. @@ -19,9 +19,9 @@ class TIAMultiProcess: >>> from tiatoolbox.decorators.multiproc import TIAMultiProcess >>> import cv2 >>> @TIAMultiProcess(iter_on="input_path") - >>> def read_images(input_path, output_dir=None): - >>> img = cv2.imread(input_path) - >>> return img + ... def read_images(input_path, output_dir=None): + ... img = cv2.imread(input_path) + ... return img >>> imgs = read_images(input_path) """ diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index 719d80cae..a1f32f5f4 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -17,7 +17,8 @@ def split_path_name_ext(full_path): Examples: >>> from tiatoolbox import utils - >>> dir_path, file_name, extension = utils.misc.split_path_name_ext(full_path) + >>> dir_path, file_name, extension = + ... utils.misc.split_path_name_ext(full_path) """ input_dir, file_name = os.path.split(full_path) @@ -30,7 +31,8 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): Grabs file paths specified by file extensions Args: - input_path (str, pathlib.Path): Path to the directory where files need to be searched + input_path (str, pathlib.Path): Path to the directory where files + need to be searched file_types (str, tuple): File types (extensions) to be searched Returns: From 84187a8595309d75e548f91b9f4db1963bc4a3b7 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:28:11 +0100 Subject: [PATCH 56/69] MAINT: Modify if statement to remove unnecessary else error Modify if statement to remove unnecessary else error --- tiatoolbox/dataloader/slide_info.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index a48e9d832..8fd3d5ced 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -42,7 +42,8 @@ def slide_info(input_path, output_dir=None): input_dir=input_dir, file_name=file_name, output_dir=output_dir ) info = wsi_reader.slide_info() - return info else: print("File type not supported") - return None + info = None + + return info From 00b0c28f728ed49fea762b81c31840ad52848cbb Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:29:35 +0100 Subject: [PATCH 57/69] MAINT: Modify if statement to remove unnecessary else error Modify if statement to remove unnecessary else error --- tiatoolbox/dataloader/slide_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index 8fd3d5ced..ff30d1410 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -37,7 +37,7 @@ def slide_info(input_path, output_dir=None): print(file_name, flush=True) _, file_type = os.path.splitext(file_name) - if file_type == ".svs" or file_type == ".ndpi" or file_type == ".mrxs": + if file_type in (".svs", ".ndpi" , ".mrxs"): wsi_reader = wsireader.WSIReader( input_dir=input_dir, file_name=file_name, output_dir=output_dir ) From 8a459f05fe87a3d965ae71fb6415cbadc583fbc3 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:31:56 +0100 Subject: [PATCH 58/69] MAINT: Fix spaces after , Fix spaces after , --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 203e6f7ea..ba156103a 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "Programming Language :: Python :: 3.8", ], description="Computational pathology toolbox developed by TIA Lab.", - entry_points={"console_scripts": ["tiatoolbox=tiatoolbox.cli:main",],}, + entry_points={"console_scripts": ["tiatoolbox=tiatoolbox.cli:main", ], }, install_requires=requirements, long_description=readme + "\n\n" + history, include_package_data=True, From 5dfa11228cb212a42ab279ab1d7f79569f6e4162 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:35:40 +0100 Subject: [PATCH 59/69] DOC: Fix docstring line too long Fix docstring line too long --- tests/test_tiatoolbox.py | 6 ++++-- tiatoolbox/cli.py | 6 ++++-- tiatoolbox/utils/misc.py | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 07848d977..4f9358bb3 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -23,7 +23,8 @@ def response_ndpi(request): ndpi_file_path = pathlib.Path(__file__).parent.joinpath("CMU-1.ndpi") if not pathlib.Path.is_file(ndpi_file_path): r = requests.get( - "http://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/CMU-1.ndpi" + "http://openslide.cs.cmu.edu/download/openslide-testdata" + "/Hamamatsu/CMU-1.ndpi" ) with open(ndpi_file_path, "wb") as f: f.write(r.content) @@ -45,7 +46,8 @@ def response_svs(request): svs_file_path = pathlib.Path(__file__).parent.joinpath("CMU-1.svs") if not pathlib.Path.is_file(svs_file_path): r = requests.get( - "http://openslide.cs.cmu.edu/download/openslide-testdata/Hamamatsu/CMU-1.ndpi" + "http://openslide.cs.cmu.edu/download/openslide-testdata" + "/Hamamatsu/CMU-1.ndpi" ) with open(svs_file_path, "wb") as f: f.write(r.content) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 5d0db6230..7806573e3 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -37,12 +37,14 @@ def main(): ) @click.option( "--mode", - help="'show' to display meta information only or 'save' to save the meta information, default=show", + help="'show' to display meta information only or 'save' to save " + "the meta information, default=show", ) @click.option( "--workers", type=int, - help="num of cpu cores to use for multiprocessing, default=multiprocessing.cpu_count()", + help="num of cpu cores to use for multiprocessing, " + "default=multiprocessing.cpu_count()", ) def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """Displays or saves WSI metadata""" diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index a1f32f5f4..cd98706f6 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -41,7 +41,8 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): Examples: >>> from tiatoolbox import utils >>> file_types = ("*.ndpi", "*.svs", "*.mrxs") - >>> files_all = utils.misc.grab_files_from_dir(input_path, file_types=file_types,) + >>> files_all = utils.misc.grab_files_from_dir(input_path, + ... file_types=file_types,) """ input_path = pathlib.Path(input_path) From bd45034ec05cbc078d55edd0d3b9f6cca4fb01bb Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:37:01 +0100 Subject: [PATCH 60/69] DOC: Fix docstring line too long Fix docstring line too long --- tiatoolbox/dataloader/wsireader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 565f145cd..2b7cdc23f 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -22,7 +22,8 @@ class WSIReader: level_count (int): The number of pyramid levels in the slide level_dimensions (int): A list of `(width, height)` tuples, one for each level of the slide - level_downsamples (int): A list of down sample factors for each level of the slide + level_downsamples (int): A list of down sample factors for each level + of the slide """ From bc81b2b4c903b2c9feafcff5b64e00687ee2537e Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:39:16 +0100 Subject: [PATCH 61/69] DOC: Fix whitespace before comma Fix whitespace before comma --- tiatoolbox/dataloader/slide_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index ff30d1410..a2d36ac74 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -37,7 +37,7 @@ def slide_info(input_path, output_dir=None): print(file_name, flush=True) _, file_type = os.path.splitext(file_name) - if file_type in (".svs", ".ndpi" , ".mrxs"): + if file_type in (".svs", ".ndpi", ".mrxs"): wsi_reader = wsireader.WSIReader( input_dir=input_dir, file_name=file_name, output_dir=output_dir ) From 72f2b5daa2f39bc940acc02e8f5fb3e542a6a71c Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:40:26 +0100 Subject: [PATCH 62/69] STY: Update black styling Update black styling --- tiatoolbox/cli.py | 4 ++-- tiatoolbox/dataloader/wsireader.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tiatoolbox/cli.py b/tiatoolbox/cli.py index 7806573e3..fc532692d 100644 --- a/tiatoolbox/cli.py +++ b/tiatoolbox/cli.py @@ -38,13 +38,13 @@ def main(): @click.option( "--mode", help="'show' to display meta information only or 'save' to save " - "the meta information, default=show", + "the meta information, default=show", ) @click.option( "--workers", type=int, help="num of cpu cores to use for multiprocessing, " - "default=multiprocessing.cpu_count()", + "default=multiprocessing.cpu_count()", ) def slide_info(wsi_input, output_dir, file_types, mode, workers=None): """Displays or saves WSI metadata""" diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 2b7cdc23f..94aec5aef 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -2,6 +2,7 @@ import pathlib import numpy as np + # from PIL import Image import openslide From 2726d984d20252600c11dca74c33fe8ff4f6f82e Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:43:53 +0100 Subject: [PATCH 63/69] MAINT: Use `is` instead of `==` for test Use `is` instead of `==` for test --- tiatoolbox/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index cd98706f6..ddfcf5718 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -47,7 +47,7 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): """ input_path = pathlib.Path(input_path) - if type(file_types) == str: + if type(file_types) is str: if len(file_types.split(",")) > 1: file_types = tuple(file_types.split(",")) else: From 7e8dcf4f4a9a54f3dfbf1ac6702b1f1f29041173 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:47:19 +0100 Subject: [PATCH 64/69] MAINT: Unused arguments should start with _ Unused arguments should start with _ --- tests/test_tiatoolbox.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 4f9358bb3..60c6b3ece 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -15,7 +15,7 @@ @pytest.fixture -def response_ndpi(request): +def _response_ndpi(request): """ Sample pytest fixture for ndpi images Download ndpi image for pytest @@ -34,11 +34,11 @@ def close_ndpi(): os.remove(str(ndpi_file_path)) request.addfinalizer(close_ndpi) - return response_ndpi + return _response_ndpi @pytest.fixture -def response_svs(request): +def _response_svs(request): """ Sample pytest fixture for svs images Download ndpi image for pytest @@ -57,10 +57,10 @@ def close_ndpi(): os.remove(str(svs_file_path)) request.addfinalizer(close_ndpi) - return response_svs + return _response_svs -def test_slide_info(response_ndpi, response_svs): +def test_slide_info(_response_ndpi, _response_svs): """pytest for slide_info as a python function""" file_types = ("*.ndpi", "*.svs", "*.mrxs") files_all = utils.misc.grab_files_from_dir( @@ -89,7 +89,7 @@ def test_command_line_version(): assert __version__ in version_result.output -def test_command_line_slide_info(response_ndpi, response_svs): +def test_command_line_slide_info(_response_ndpi, _response_svs): """Test the Slide information CLI.""" runner = CliRunner() slide_info_result = runner.invoke( From 4614f7f4ac2a7e2427a6d96fb050701b6406874d Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 16:54:42 +0100 Subject: [PATCH 65/69] TST: Update test_tiatoolbox.py for slide_info Update test_tiatoolbox.py to accommodate changes in slide_info --- tests/test_tiatoolbox.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py index 60c6b3ece..9cf4d8d30 100644 --- a/tests/test_tiatoolbox.py +++ b/tests/test_tiatoolbox.py @@ -66,7 +66,7 @@ def test_slide_info(_response_ndpi, _response_svs): files_all = utils.misc.grab_files_from_dir( input_path=str(pathlib.Path(r".")), file_types=file_types, ) - slide_params = slide_info(input_path=files_all, workers=2, mode="save") + slide_params = slide_info(input_path=files_all, workers=2) for slide_param in slide_params: utils.misc.save_yaml(slide_param, slide_param["file_name"] + ".yaml") @@ -100,8 +100,6 @@ def test_command_line_slide_info(_response_ndpi, _response_svs): ".", "--file_types", '"*.ndpi, *.svs"', - "--mode", - "show", "--workers", "2", ], From eff04cf02f01b86af031c308e4c9afb514b83083 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Wed, 17 Jun 2020 17:07:18 +0100 Subject: [PATCH 66/69] MAINT: Remove __exit__ to fix errors with Deepsource Remove __exit__ to fix errors with Deepsource --- tiatoolbox/dataloader/wsireader.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 94aec5aef..04b0737a8 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -67,9 +67,6 @@ def __init__( self.level_dimensions = self.openslide_obj.level_dimensions self.level_downsamples = self.openslide_obj.level_downsamples - def __exit__(self): - self.openslide_obj.close() - def slide_info(self): """ WSI meta data reader From 8398bb46da109fd727583231afd530965ca82ce7 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 18 Jun 2020 13:37:16 +0100 Subject: [PATCH 67/69] DOC: Update docstrings Update docstrings to match Google Style and improve readability. --- tiatoolbox/dataloader/slide_info.py | 7 +++---- tiatoolbox/dataloader/wsireader.py | 8 +++----- tiatoolbox/decorators/multiproc.py | 6 ++---- tiatoolbox/utils/misc.py | 12 +++++------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/tiatoolbox/dataloader/slide_info.py b/tiatoolbox/dataloader/slide_info.py index a2d36ac74..dc7e7a867 100644 --- a/tiatoolbox/dataloader/slide_info.py +++ b/tiatoolbox/dataloader/slide_info.py @@ -7,16 +7,15 @@ @TIAMultiProcess(iter_on="input_path") def slide_info(input_path, output_dir=None): - """ - slide_info() - Single file run to output or save WSI meta data. Multiprocessing uses this function + """Single file run to output or save WSI meta data. Multiprocessing uses this function to run slide_info in parallel Args: input_path: Path to whole slide image output_dir: Path to output directory to save the output + workers: num of cpu cores to use for multiprocessing Returns: - displays or saves WSI meta information + list: list of dictionary Whole Slide meta information Examples: >>> from tiatoolbox.dataloader.slide_info import slide_info diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py index 04b0737a8..be4ccae27 100644 --- a/tiatoolbox/dataloader/wsireader.py +++ b/tiatoolbox/dataloader/wsireader.py @@ -9,8 +9,7 @@ class WSIReader: - """ - WSI Reader class to read WSI images + """WSI Reader class to read WSI images Attributes: input_dir (pathlib.Path): input path to WSI directory @@ -68,12 +67,11 @@ def __init__( self.level_downsamples = self.openslide_obj.level_downsamples def slide_info(self): - """ - WSI meta data reader + """WSI meta data reader Args: Returns: - param (dict): dictionary containing meta information + dict: dictionary containing meta information """ input_dir = self.input_dir diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py index 2a7a4c78e..6e6cf2fc3 100644 --- a/tiatoolbox/decorators/multiproc.py +++ b/tiatoolbox/decorators/multiproc.py @@ -7,8 +7,7 @@ class TIAMultiProcess: - """ - Multiprocessing class decorator for the toolbox, requires a list `iter_on` + """Multiprocessing class decorator for the toolbox, requires a list `iter_on` as input on which multiprocessing will run Attributes: @@ -44,8 +43,7 @@ def __call__(self, func): """ def func_wrap(*args, **kwargs): - """ - Wrapping function for decorator call + """Wrapping function for decorator call Args: *args: args inputs **kwargs: kwargs inputs diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py index ddfcf5718..c7ac0ce26 100644 --- a/tiatoolbox/utils/misc.py +++ b/tiatoolbox/utils/misc.py @@ -5,8 +5,7 @@ def split_path_name_ext(full_path): - """ - Split path of a file to directory path, file name and extension + """Split path of a file to directory path, file name and extension Args: full_path (str): Path to a file @@ -27,8 +26,7 @@ def split_path_name_ext(full_path): def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): - """ - Grabs file paths specified by file extensions + """Grabs file paths specified by file extensions Args: input_path (str, pathlib.Path): Path to the directory where files @@ -61,8 +59,7 @@ def grab_files_from_dir(input_path, file_types=("*.jpg", "*.png", "*.tif")): def save_yaml(input_dict, output_path="output.yaml"): - """ - Save dictionary as yaml + """Save dictionary as yaml Args: input_dict (dict): A variable of type 'dict' output_path (str, pathlib.Path): Path to save the output file @@ -70,8 +67,9 @@ def save_yaml(input_dict, output_path="output.yaml"): Returns: Examples: + >>> from tiatoolbox import utils >>> input_dict = {'hello': 'Hello World!'} - >>> save_yaml(input_dict, './hello.yaml') + >>> utils.misc.save_yaml(input_dict, './hello.yaml') """ From 8af253ae818e1178b40e511fc63843fbd5c4e82a Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 18 Jun 2020 15:23:34 +0100 Subject: [PATCH 68/69] BUG: Fix .travis.yml indentation Fix indented lines in the after_sucess section of the .travis.yml configuration file. --- .travis.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9578eb7a7..5cf1dea06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,7 @@ python: - 3.8 - 3.7 - 3.6 - -before_install: - - sudo apt-get -y install openslide-tools + - 3.5 # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis @@ -17,13 +15,13 @@ script: tox # Upload test coverage reports (codecov and deepsource) after_success: -# Upload coverage to codecov -- bash <(curl -s https://codecov.io/bash) -# Install deepsource CLI -- curl https://deepsource.io/cli | sh -- export DEEPSOURCE_DSN=https://sampledsn@deepsource.io -# Report coverage artifact to 'test-coverage' analyzer --./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml + # Upload coverage to codecov + - bash <(curl -s https://codecov.io/bash) + # Install deepsource CLI + - curl https://deepsource.io/cli | sh + - export DEEPSOURCE_DSN=https://sampledsn@deepsource.io + # Report coverage artifact to 'test-coverage' analyzer + - ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml # Assuming you have installed the travis-ci CLI tool, after you # create the Github repo and add it to Travis, run the From 1d99de165ed7dc57622afeddc40a61ef3878f417 Mon Sep 17 00:00:00 2001 From: shaneahmed Date: Thu, 18 Jun 2020 15:28:33 +0100 Subject: [PATCH 69/69] BUG: Fix .travis.yml openslide-tools Fix travis error add openslide-tools and remove python 3.5 --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5cf1dea06..986f5be63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ python: - 3.8 - 3.7 - 3.6 - - 3.5 + +before_install: + - sudo apt-get -y install openslide-tools # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis