diff --git a/.travis.yml b/.travis.yml
index 6cf99d8d2..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
@@ -15,13 +17,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
diff --git a/README.rst b/README.rst
index e2c04b5a8..822719eab 100644
--- a/README.rst
+++ b/README.rst
@@ -1,25 +1,56 @@
+.. raw:: html
+
+
+
+
+
===========
TIA Toolbox
===========
+Computational Pathology Toolbox developed by TIA Lab
+
+Please try
+
+::
+
+ python -m tiatoolbox -h
+
+Getting Started
+===============
+First, install OpenSlide `here `__. Then, create and
+activate the conda environment:
+pip
+----
+::
+ pip install -r requirements_dev.txt
-Computational pathology toolbox developed by TIA Lab.
+conda
+-----
+::
+ conda env create --name tiatoolbox --file requirements.conda.yml
+ conda activate tiatoolbox
+tiatoolbox --help
+=======================
-Features
---------
+::
-* TODO
+ usage: tiatoolbox [-h] [--version] [--verbose VERBOSE]
+ {slide_info}
+ ...
-Credits
--------
+ positional arguments:
+ {slide_info}
+ slide_info usage: python -m tiatoolbox slide_info -h
-This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
+ 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/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/tialab_logo.png b/docs/tialab_logo.png
new file mode 100644
index 000000000..79fdc319c
Binary files /dev/null and b/docs/tialab_logo.png differ
diff --git a/docs/usage.rst b/docs/usage.rst
index b707c0338..2c7c1b5c9 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -5,3 +5,47 @@ Usage
To use TIA Toolbox in a project::
import tiatoolbox
+
+
+----------
+Dataloader
+----------
+.. 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
+
+----------
+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/requirements.conda.yml b/requirements.conda.yml
new file mode 100644
index 000000000..219efe7e0
--- /dev/null
+++ b/requirements.conda.yml
@@ -0,0 +1,22 @@
+name: tiatoolbox
+channels:
+ - conda
+ - conda-forge
+ - 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=4.2
+ - pillow=7.0.0
+ - pip=20.0.2
+ - pyyaml=5.3.1
+ - requests=2.23.0
+ - pathos==0.2.5
+ - pip:
+ - openslide-python==1.1.1
+
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 75845e43c..eb180fe85 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
@@ -8,6 +8,10 @@ coverage==5.1
Sphinx==1.8.5
twine==1.14.0
Click==7.0
-pytest==4.6.5
-pytest-runner==5.1
+setuptools==45.1.0
+pytest==5.4.2
+pytest-runner==5.2
+opencv-python==4.2.0.34
+pathos==0.2.5
+openslide-python==1.1.1
pytest-cov==2.9.0
diff --git a/setup.py b/setup.py
index 93b60212b..ba156103a 100644
--- a/setup.py
+++ b/setup.py
@@ -25,19 +25,18 @@
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",
],
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,
diff --git a/tests/test_tiatoolbox.py b/tests/test_tiatoolbox.py
index b884679e7..9cf4d8d30 100644
--- a/tests/test_tiatoolbox.py
+++ b/tests/test_tiatoolbox.py
@@ -1,37 +1,108 @@
#!/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 import utils
from tiatoolbox import cli
+from tiatoolbox import __version__
+
+from click.testing import CliRunner
+import requests
+import os
+import pathlib
@pytest.fixture
-def response():
- """Sample pytest fixture.
+def _response_ndpi(request):
+ """
+ Sample pytest fixture for ndpi images
+ Download ndpi image for pytest
+ """
+ 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(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
- See more at: http://doc.pytest.org/en/latest/fixture.html
+
+@pytest.fixture
+def _response_svs(request):
"""
- # import requests
- # return requests.get('https://github.com/audreyr/cookiecutter-pypackage')
+ Sample pytest fixture for svs images
+ Download ndpi image for pytest
+ """
+ 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"
+ )
+ 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_content(response):
- """Sample pytest test function with the pytest fixture as an argument."""
- # from bs4 import BeautifulSoup
- # assert 'GitHub' in BeautifulSoup(response.content).title.string
+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(
+ input_path=str(pathlib.Path(r".")), 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")
-def test_command_line_interface():
- """Test the CLI."""
+
+def test_command_line_help_interface():
+ """Test the CLI help"""
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_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."""
+ runner = CliRunner()
+ slide_info_result = runner.invoke(
+ cli.main,
+ [
+ "slide-info",
+ "--wsi_input",
+ ".",
+ "--file_types",
+ '"*.ndpi, *.svs"',
+ "--workers",
+ "2",
+ ],
+ )
+
+ assert slide_info_result.exit_code == 0
diff --git a/tiatoolbox/__init__.py b/tiatoolbox/__init__.py
index 31d48b530..f4eee6be9 100644
--- a/tiatoolbox/__init__.py
+++ b/tiatoolbox/__init__.py
@@ -1,5 +1,12 @@
"""Top-level package for TIA Toolbox."""
+from tiatoolbox import tiatoolbox
+from tiatoolbox import dataloader
+from tiatoolbox import utils
+
__author__ = """TIA Lab"""
__email__ = "tialab@dcs.warwick.ac.uk"
__version__ = "0.1.1"
+
+if __name__ == "__main__":
+ pass
diff --git a/tiatoolbox/__main__.py b/tiatoolbox/__main__.py
new file mode 100644
index 000000000..8f4c3111c
--- /dev/null
+++ b/tiatoolbox/__main__.py
@@ -0,0 +1,5 @@
+"""__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 c0a8e6d67..fc532692d 100644
--- a/tiatoolbox/cli.py
+++ b/tiatoolbox/cli.py
@@ -1,15 +1,71 @@
"""Console script for tiatoolbox."""
+from tiatoolbox import __version__
+from tiatoolbox import dataloader
+from tiatoolbox import utils
import sys
import click
+import os
-@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/")
+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 {})"
+ 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 TIA LAB"""
return 0
+@main.command()
+@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'",
+ 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(
+ "--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"""
+ file_types = tuple(file_types.split(", "))
+ if os.path.isdir(wsi_input):
+ files_all = utils.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(
+ input_path=files_all, output_dir=output_dir, mode=mode, workers=workers,
+ )
+
+
if __name__ == "__main__":
sys.exit(main()) # pragma: no cover
diff --git a/tiatoolbox/dataloader/__init__.py b/tiatoolbox/dataloader/__init__.py
new file mode 100644
index 000000000..36212b688
--- /dev/null
+++ b/tiatoolbox/dataloader/__init__.py
@@ -0,0 +1,3 @@
+"""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
new file mode 100644
index 000000000..dc7e7a867
--- /dev/null
+++ b/tiatoolbox/dataloader/slide_info.py
@@ -0,0 +1,48 @@
+"""Get Slide Meta Data information"""
+from tiatoolbox.dataloader import wsireader
+from tiatoolbox.decorators.multiproc import TIAMultiProcess
+
+import os
+
+
+@TIAMultiProcess(iter_on="input_path")
+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
+ workers: num of cpu cores to use for multiprocessing
+ Returns:
+ list: list of dictionary Whole Slide 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(slide_param)
+
+ """
+
+ input_dir, file_name = os.path.split(input_path)
+
+ print(file_name, flush=True)
+ _, file_type = os.path.splitext(file_name)
+
+ if file_type in (".svs", ".ndpi", ".mrxs"):
+ wsi_reader = wsireader.WSIReader(
+ input_dir=input_dir, file_name=file_name, output_dir=output_dir
+ )
+ info = wsi_reader.slide_info()
+ else:
+ print("File type not supported")
+ info = None
+
+ return info
diff --git a/tiatoolbox/dataloader/wsireader.py b/tiatoolbox/dataloader/wsireader.py
new file mode 100644
index 000000000..be4ccae27
--- /dev/null
+++ b/tiatoolbox/dataloader/wsireader.py
@@ -0,0 +1,107 @@
+"""WSIReader for WSI reading or extracting metadata information from WSIs"""
+
+import pathlib
+import numpy as np
+
+# from PIL import Image
+
+import openslide
+
+
+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 (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
+
+ """
+
+ def __init__(
+ self,
+ input_dir=".",
+ file_name=None,
+ output_dir="./output",
+ tile_objective_value=20,
+ tile_read_size_w=5000,
+ tile_read_size_h=5000,
+ ):
+ """
+ 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=./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
+
+ """
+
+ self.input_dir = pathlib.Path(input_dir)
+ self.file_name = pathlib.Path(file_name).name
+ if output_dir is not None:
+ self.output_dir = pathlib.Path(output_dir, self.file_name)
+
+ self.openslide_obj = openslide.OpenSlide(
+ 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])
+ 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):
+ """WSI meta data reader
+ Args:
+
+ Returns:
+ dict: dictionary containing meta information
+
+ """
+ input_dir = self.input_dir
+ 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
+ file_name = self.file_name
+
+ param = {
+ "input_dir": input_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,
+ "file_name": file_name,
+ }
+
+ return param
diff --git a/tiatoolbox/decorators/__init__.py b/tiatoolbox/decorators/__init__.py
new file mode 100644
index 000000000..d9608869c
--- /dev/null
+++ b/tiatoolbox/decorators/__init__.py
@@ -0,0 +1,2 @@
+"""Package defines decorators for the toolbox"""
+from tiatoolbox.decorators import multiproc
diff --git a/tiatoolbox/decorators/multiproc.py b/tiatoolbox/decorators/multiproc.py
new file mode 100644
index 000000000..6e6cf2fc3
--- /dev/null
+++ b/tiatoolbox/decorators/multiproc.py
@@ -0,0 +1,69 @@
+"""Multiprocessing decorators required by the tiatoolbox."""
+
+import multiprocessing
+from functools import partial
+
+from pathos.multiprocessing import ProcessingPool as Pool
+
+
+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
+ >>> @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):
+ """
+ Args:
+ iter_on: Variable on which iterations will be performed.
+ """
+ self.iter_on = iter_on
+ self.workers = multiprocessing.cpu_count()
+
+ def __call__(self, func):
+ """
+ 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:
+
+ """
+ if "workers" in kwargs:
+ self.workers = kwargs.pop("workers")
+ try:
+ iter_value = kwargs.pop(self.iter_on)
+ except ValueError:
+ raise ValueError("Please specify iter_on in multiprocessing decorator")
+
+ with Pool(self.workers) as p:
+ results = p.map(partial(func, **kwargs), iter_value,)
+ p.clear()
+
+ return results
+
+ func_wrap.__doc__ = func.__doc__
+
+ return func_wrap
diff --git a/tiatoolbox/utils/__init__.py b/tiatoolbox/utils/__init__.py
new file mode 100644
index 000000000..1dabdcb08
--- /dev/null
+++ b/tiatoolbox/utils/__init__.py
@@ -0,0 +1,2 @@
+"""Utils package for toolbox utilities"""
+from tiatoolbox.utils import misc
diff --git a/tiatoolbox/utils/misc.py b/tiatoolbox/utils/misc.py
new file mode 100644
index 000000000..c7ac0ce26
--- /dev/null
+++ b/tiatoolbox/utils/misc.py
@@ -0,0 +1,77 @@
+"""Miscellaneous small functions repeatedly used in tiatoolbox"""
+import os
+import pathlib
+import yaml
+
+
+def split_path_name_ext(full_path):
+ """Split path of a file to directory path, file name and extension
+
+ Args:
+ 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")):
+ """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
+
+ Returns:
+ 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)
+
+ if type(file_types) is 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))
+
+ return list(files_grabbed)
+
+
+def save_yaml(input_dict, output_path="output.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
+
+ Returns:
+
+ Examples:
+ >>> from tiatoolbox import utils
+ >>> input_dict = {'hello': 'Hello World!'}
+ >>> utils.misc.save_yaml(input_dict, './hello.yaml')
+
+
+ """
+ with open(pathlib.Path(output_path), "w") as yaml_file:
+ yaml.dump(input_dict, yaml_file)
diff --git a/tox.ini b/tox.ini
index c0a377640..246ec48a7 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