diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 78e59d8..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[bumpversion] -current_version = 0.4.2 -commit = True -tag = True - -[bumpversion:file:setup.py] - -[bumpversion:file:detectree/__init__.py] -search = __version__ = "{current_version}" -replace = __version__ = "{new_version}" diff --git a/.github/ISSUE_TEMPLATES/bug.md b/.github/ISSUE_TEMPLATES/bug.md index 7e60bfb..366d17a 100644 --- a/.github/ISSUE_TEMPLATES/bug.md +++ b/.github/ISSUE_TEMPLATES/bug.md @@ -1,6 +1,6 @@ -* detectree version: -* Python version: -* Operating system: +- detectree version: +- Python version: +- Operating system: ### Description diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..c047d23 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,11 @@ +# Config for Dependabot updates. See Documentation here: +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Update GitHub actions in workflows + - package-ecosystem: "github-actions" + directory: "/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fb89738..cc8d09c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,6 +4,6 @@ Before you proceed, review the contributing guidelines in the "Pull request guid In this pull request, please include: -* a reference to related issue(s) -* a description of the changes proposed in the pull request -* an example code snippet illustrating usage of the new functionality +- a reference to related issue(s) +- a description of the changes proposed in the pull request +- an example code snippet illustrating usage of the new functionality diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml deleted file mode 100644 index 0ea6ebd..0000000 --- a/.github/workflows/dev.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: dev - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - - name: checkout code repository - uses: actions/checkout@v3 - - - name: run pre-commit - uses: pre-commit/action@v3.0.0 - - tests: - - name: Python ${{ matrix.python-version }} ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] - include: - - environment-file: environment-dev.yml - miniforge-variant: Mambaforge - miniforge-version: 4.14.0-0 - - defaults: - run: - shell: bash -l {0} - - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 - - - name: setup conda environment with mambaforge - uses: conda-incubator/setup-miniconda@v2 - with: - use-mamba: true - activate-environment: detectree-dev - python-version: ${{ matrix.python-version }} - condarc-file: ${{ matrix.condarc-file }} - environment-file: ${{ matrix.environment-file }} - miniforge-variant: ${{ matrix.miniforge-variant }} - miniforge-version: ${{ matrix.miniforge-version }} - - - name: install detectree - run: | - pip install . - conda list - conda info --all - - - name: test docs - run: make -C ./docs html - - - name: test code - run: | - coverage run --source ./detectree --module pytest --verbose - coverage xml -i - coverage report -m - - - name: upload coverage report - uses: codecov/codecov-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9814f09 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,131 @@ +name: release + +on: + push: + tags: + - 'v*' + +jobs: + build_sdist: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: build source tarball + run: | + python -m build --sdist + twine check --strict dist/* + + - uses: actions/upload-artifact@v4 + with: + path: dist/* + + build_wheels: + name: wheel on ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # temporarily ignoring binary wheels for windows until pythran issues are fixed + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + # see https://github.com/pypa/cibuildwheel/issues/933 + # - uses: pypa/cibuildwheel@v2 + + # Used to host cibuildwheel + - uses: actions/setup-python@v4 + with: + python-version: "3.12" + + - name: install cibuildwheel + run: python -m pip install + + - name: build wheels + run: python -m cibuildwheel --output-dir wheelhouse + + - name: upload wheels + uses: actions/upload-artifact@v4 + with: + path: wheelhouse/*.whl + + publish_dev_build: + needs: [build_sdist, build_wheels] + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://pypi.org/p/detectree + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + + - name: publish to test pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + release: + needs: [publish_dev_build] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/detectree + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + # see https://github.com/softprops/action-gh-release/issues/236 + contents: write + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: generate change log + uses: heinrichreimer/github-changelog-generator-action@v2.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issues: true + issuesWoLabels: true + pullRequests: true + prWoLabels: true + unreleased: true + addSections: '{"documentation":{"prefix":"**Documentation:**","labels":["documentation"]}}' + sinceTag: v0.1.0 + output: RELEASE-CHANGELOG.md + + - uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + + - name: create github release + uses: softprops/action-gh-release@v1 + with: + body_path: ./RELEASE-CHANGELOG.md + files: dist/*.whl + draft: false + prerelease: false + + - name: publish to pypi + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/release_to_pypi.yml b/.github/workflows/release_to_pypi.yml deleted file mode 100644 index a0e5e9e..0000000 --- a/.github/workflows/release_to_pypi.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Publish detectree to PyPI / GitHub - -on: - push: - tags: - - "v*" - -jobs: - build-n-publish: - name: Build and publish detectree to PyPI - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@master - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.x" - - - name: Build a binary wheel and a source tarball - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - python setup.py sdist bdist_wheel - - - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{ secrets.PYPI_API_TOKEN }} - - - name: Create GitHub Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ github.ref }} - release_name: Version ${{ github.ref }} - draft: false - prerelease: false - - - name: Get Asset name - run: | - export PKG=$(ls dist/ | grep tar) - set -- $PKG - echo "name=$1" >> $GITHUB_ENV - - - name: Upload Release Asset (sdist) to GitHub - id: upload-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: dist/${{ env.name }} - asset_name: ${{ env.name }} - asset_content_type: application/zip diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6347686 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: tests + +on: + pull_request: + branches: + - "*" + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + include: + - os: macos-latest + python-version: "3.12" + - os: windows-latest + python-version: "3.12" + + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -l {0} + + steps: + - uses: actions/checkout@v4 + + - uses: mamba-org/setup-micromamba@v1 + with: + environment-name: test-env + create-args: >- + python=${{ matrix.python-version }} + pip + + - name: install dependencies + run: pip install tox tox-gh-actions + + - name: test with tox + run: tox + env: + CONDA_EXE: mamba + + - name: upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + + - name: list files + run: ls -l . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b486d59..890e113 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,38 +1,55 @@ -exclude: 'docs|node_modules|migrations|.tox' -default_stages: [commit] -fail_fast: true +ci: + autofix_commit_msg: "style(pre-commit.ci): auto fixes from pre-commit hooks" + autoupdate_commit_msg: "style(pre-commit.ci): pre-commit autoupdate" repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.18.2 + - repo: https://github.com/commitizen-tools/commitizen + rev: v3.15.0 hooks: - - id: check-github-workflows + - id: commitizen + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + additional_dependencies: [prettier, prettier-plugin-toml, prettier-plugin-ini] + types: ["ini", "toml", "yaml"] - - repo: https://github.com/psf/black - rev: 22.10.0 + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.17 # Use the ref you want to point at hooks: - - id: black + - id: mdformat + # Optionally add plugins + additional_dependencies: + - mdformat-gfm + - mdformat-black - - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 hooks: - - id: isort + - id: yamllint + args: ["-d relaxed"] + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.28.0 + hooks: + - id: check-github-workflows - - repo: https://gitlab.com/pycqa/flake8 - rev: 5.0.4 + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 hooks: - - id: flake8 - additional_dependencies: [flake8-isort] + - id: pyupgrade - - repo: https://github.com/pycqa/pydocstyle - rev: 6.1.1 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.2 hooks: - - id: pydocstyle - files: ^detectree + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/.readthedocs.yml b/.readthedocs.yml index a30cc50..9afa623 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,8 +1,11 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: mambaforge-4.10 + python: - install: - - requirements: requirements.txt - - requirements: requirements-dev.txt - - method: pip - path: . + install: + - method: pip + path: . diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e8ad6..b61b1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,38 +2,38 @@ ## 0.4.2 (24/10/2022) -* moved `_gaussain_kernel1d` from scipy to detectree codebase +- moved `_gaussain_kernel1d` from scipy to detectree codebase ## 0.4.1 (05/07/2021) -* added postprocess func args and kwargs to `LidarToCanopy` +- added postprocess func args and kwargs to `LidarToCanopy` ## 0.4.0 (03/07/2021) -* added pre-commit -* updated to laspy 2.0.0 (with optional laszip) -* updated bumpversion to double quotes (Python black) -* updated docs build -* tests and release to pypi and github with github actions -* added github issue templates, pull request template and updated contributing docs -* using pydocstyle and black -* added lidar to canopy module -* using keyword-only arguments +- added pre-commit +- updated to laspy 2.0.0 (with optional laszip) +- updated bumpversion to double quotes (Python black) +- updated docs build +- tests and release to pypi and github with github actions +- added github issue templates, pull request template and updated contributing docs +- using pydocstyle and black +- added lidar to canopy module +- using keyword-only arguments ## 0.3.1 (11/03/2020) -* drop `num_blocks` argument of `compute_image_descriptor` and `compute_image_descriptor_from_filepath` +- drop `num_blocks` argument of `compute_image_descriptor` and `compute_image_descriptor_from_filepath` ## 0.3.0 (02/03/2020) -* set default post-classification refinement parameter `refine_beta` to 50 (instead of 100) -* keyword arguments to `PixelFeaturesBuilder` and `PixelResponseBuilder` can be explicitly provided to the initialization of `ClassifierTrainer`, and are documented there -* raise a `ValueError` when a provided response is not a binary tree/non-tree image +- set default post-classification refinement parameter `refine_beta` to 50 (instead of 100) +- keyword arguments to `PixelFeaturesBuilder` and `PixelResponseBuilder` can be explicitly provided to the initialization of `ClassifierTrainer`, and are documented there +- raise a `ValueError` when a provided response is not a binary tree/non-tree image ## 0.2.0 (11/12/2019) -* correction (typo) `keep_emtpy_tiles` -> `keep_empty_tiles` in `split_into_tiles` +- correction (typo) `keep_emtpy_tiles` -> `keep_empty_tiles` in `split_into_tiles` ## 0.1.0 (14/11/2019) -* initial release +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03affdb..cd87c62 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,9 +10,9 @@ Report bugs at https://github.com/martibosch/detectree/issues. If you are reporting a bug, please include: -* Your operating system name and version. -* Any details about your local setup that might be helpful in troubleshooting. -* Detailed steps to reproduce the bug. +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. ### Fix bugs @@ -27,9 +27,9 @@ Look through the GitHub issues for features. Anything tagged with "enhancement" Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. -2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md. -3. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check https://travis-ci.org/martibosch/detectree/pull_requests and make sure that the tests pass for all supported Python versions. -4. Adhere to the following project standards: - * `black` code style with max line length of 79 - * `isort` sorted imports - * `numpy` style docstrings +1. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md. +1. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check https://travis-ci.org/martibosch/detectree/pull_requests and make sure that the tests pass for all supported Python versions. +1. Adhere to the following project standards: + - `black` code style with max line length of 79 + - `isort` sorted imports + - `numpy` style docstrings diff --git a/README.md b/README.md index b02ebc4..3de115a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![PyPI version fury.io](https://badge.fury.io/py/detectree.svg)](https://pypi.python.org/pypi/detectree/) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/detectree.svg)](https://anaconda.org/conda-forge/detectree) [![Documentation Status](https://readthedocs.org/projects/detectree/badge/?version=latest)](https://detectree.readthedocs.io/en/latest/?badge=latest) -[![Build Status](https://github.com/martibosch/detectree/workflows/tests/badge.svg?branch=main)](https://github.com/martibosch/detectree/actions?query=workflow%3A%22tests%22) +[![tests](https://github.com/martibosch/detectree/actions/workflows/tests.yml/badge.svg)](https://github.com/martibosch/detectree/blob/main/.github/workflows/tests.yml) [![codecov](https://codecov.io/gh/martibosch/detectree/branch/main/graph/badge.svg?token=ZTZK2LFR6T)](https://codecov.io/gh/martibosch/detectree) [![GitHub license](https://img.shields.io/github/license/martibosch/detectree.svg)](https://github.com/martibosch/detectree/blob/master/LICENSE) [![DOI](https://joss.theoj.org/papers/10.21105/joss.02172/status.svg)](https://doi.org/10.21105/joss.02172) @@ -11,7 +11,7 @@ ## Overview -DetecTree is a Pythonic library to classify tree/non-tree pixels from aerial imagery, following the methods of Yang et al. [1]. The target audience is researchers and practitioners in GIS that are interested in two-dimensional aspects of trees, such as their proportional abundance and spatial distribution throughout a region of study. These measurements can be used to assess important aspects of urban planning such as the provision of urban ecosystem services. The approach is of special relevance when LIDAR data is not available or it is too costly in monetary or computational terms. +DetecTree is a Pythonic library to classify tree/non-tree pixels from aerial imagery, following the methods of Yang et al. \[1\]. The target audience is researchers and practitioners in GIS that are interested in two-dimensional aspects of trees, such as their proportional abundance and spatial distribution throughout a region of study. These measurements can be used to assess important aspects of urban planning such as the provision of urban ecosystem services. The approach is of special relevance when LIDAR data is not available or it is too costly in monetary or computational terms. ```python import detectree as dtr @@ -48,7 +48,7 @@ A full example application of DetecTree to predict a tree canopy map for the Aus Bosch M. 2020. “DetecTree: Tree detection from aerial imagery in Python”. *Journal of Open Source Software, 5(50), 2172.* [doi.org/10.21105/joss.02172](https://doi.org/10.21105/joss.02172) -Note that DetecTree is based on the methods of Yang et al. [1], therefore it seems fair to reference their work too. An example citation in an academic paper might read as follows: +Note that DetecTree is based on the methods of Yang et al. \[1\], therefore it seems fair to reference their work too. An example citation in an academic paper might read as follows: > The classification of tree pixels has been performed with the Python library DetecTree (Bosch, 2020), which is based on the approach of Yang et al. (2009). @@ -58,7 +58,7 @@ Note that DetecTree is based on the methods of Yang et al. [1], therefore it see The easiest way to install `detectree` is with conda as in: -``` bash +```bash conda install -c conda-forge detectree ``` @@ -66,13 +66,13 @@ conda install -c conda-forge detectree You can install `detectree` with pip as in: -``` bash +```bash pip install detectree ``` -If you want to be able to read compressed LAZ files, you will need [the Python bindings for `laszip`](https://github.com/tmontaigu/laszip-python). Note that the latter require [`laszip`], which can be installed using conda (which is automatically handled when installing `detectree` with conda as shown above) or downloaded from [laszip.org](https://laszip.org/). Then, detectree and the Python bindings for `laszip` can be installed with pip as in: +If you want to be able to read compressed LAZ files, you will need [the Python bindings for `laszip`](https://github.com/tmontaigu/laszip-python). Note that the latter require \[`laszip`\], which can be installed using conda (which is automatically handled when installing `detectree` with conda as shown above) or downloaded from [laszip.org](https://laszip.org/). Then, detectree and the Python bindings for `laszip` can be installed with pip as in: -``` bash +```bash pip install detectree[laszip] ``` @@ -80,7 +80,7 @@ pip install detectree[laszip] To install a development version of detectree, you can first use conda to create an environment with all the dependencies - with the [`environment-dev.yml` file](https://github.com/martibosch/detectree/blob/main/environment-dev.yml) - and activate it as in: -``` bash +```bash conda env create -f environment-dev.yml conda activate detectree-dev ``` @@ -101,12 +101,12 @@ pre-commit install ## See also -* [lausanne-tree-canopy](https://github.com/martibosch/lausanne-tree-canopy): example computational workflow to get the tree canopy of Lausanne with DetecTree -* [A video of a talk about DetecTree](https://www.youtube.com/watch?v=USwF2KyxVjY) in the [Applied Machine Learning Days of EPFL (2020)](https://appliedmldays.org/) and [its respective slides](https://martibosch.github.io/detectree-amld-2020) +- [lausanne-tree-canopy](https://github.com/martibosch/lausanne-tree-canopy): example computational workflow to get the tree canopy of Lausanne with DetecTree +- [A video of a talk about DetecTree](https://www.youtube.com/watch?v=USwF2KyxVjY) in the [Applied Machine Learning Days of EPFL (2020)](https://appliedmldays.org/) and [its respective slides](https://martibosch.github.io/detectree-amld-2020) ## Acknowledgments -* With the support of the École Polytechnique Fédérale de Lausanne (EPFL) +- With the support of the École Polytechnique Fédérale de Lausanne (EPFL) ## References diff --git a/detectree/__init__.py b/detectree/__init__.py index 2b0b4b5..c17e3fb 100644 --- a/detectree/__init__.py +++ b/detectree/__init__.py @@ -1,7 +1,3 @@ """detectree init.""" -from .classifier import Classifier, ClassifierTrainer -from .lidar import LidarToCanopy, rasterize_lidar -from .train_test_split import TrainingSelector -from .utils import split_into_tiles __version__ = "0.4.2" diff --git a/detectree/classifier.py b/detectree/classifier.py index e7a9f85..51136de 100644 --- a/detectree/classifier.py +++ b/detectree/classifier.py @@ -16,7 +16,7 @@ MOORE_NEIGHBORHOOD_ARR = np.array([[0, 0, 0], [0, 0, 1], [1, 1, 1]]) -class ClassifierTrainer(object): +class ClassifierTrainer: """Train binary tree/non-tree classifier(s) of the pixel features.""" def __init__( @@ -88,7 +88,7 @@ def __init__( Keyword arguments that will be passed to `sklearn.ensemble.AdaBoostClassifier`. """ - super(ClassifierTrainer, self).__init__() + super().__init__() if num_estimators is None: num_estimators = settings.CLF_DEFAULT_NUM_ESTIMATORS @@ -251,7 +251,7 @@ def train_classifiers(self, split_df, response_img_dir): return clfs_dict -class Classifier(object): +class Classifier: """Use trained classifier(s) to predict tree pixels.""" def __init__( @@ -298,7 +298,7 @@ def __init__( Keyword arguments that will be passed to `detectree.PixelFeaturesBuilder`, which customize how the pixel features are built. """ - super(Classifier, self).__init__() + super().__init__() if tree_val is None: tree_val = settings.CLF_DEFAULT_TREE_VAL diff --git a/detectree/cli/main.py b/detectree/cli/main.py index 2e9dc43..1087bf5 100644 --- a/detectree/cli/main.py +++ b/detectree/cli/main.py @@ -17,7 +17,7 @@ def __init__(self, *args, **kwargs): self.save_other_options = kwargs.pop("save_other_options", True) nargs = kwargs.pop("nargs", -1) assert nargs == -1, "nargs, if set, must be -1 not {}".format(nargs) - super(_OptionEatAll, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._previous_parser_process = None self._eat_all_parser = None @@ -43,7 +43,7 @@ def parser_process(value, state): # call the actual process self._previous_parser_process(value, state) - retval = super(_OptionEatAll, self).add_to_parser(parser, ctx) + retval = super().add_to_parser(parser, ctx) for name in self.opts: our_parser = parser._long_opt.get(name) or parser._short_opt.get(name) if our_parser: diff --git a/detectree/image_descriptor.py b/detectree/image_descriptor.py index d614987..3f426c3 100644 --- a/detectree/image_descriptor.py +++ b/detectree/image_descriptor.py @@ -1,7 +1,7 @@ """Compute image descriptors.""" +import cv2 import numpy as np from PIL import Image -from scipy import ndimage as ndi from skimage import color from skimage.util import shape from sklearn import preprocessing @@ -60,7 +60,7 @@ def compute_image_descriptor(img_rgb, kernels, response_bins_per_axis, num_color ) for i, kernel in enumerate(kernels): - filter_response = ndi.convolve(img_gray, kernel) + filter_response = cv2.filter2D(img_gray, ddepth=-1, kernel=kernel) response_bins = shape.view_as_blocks(filter_response, block_shape) bin_sum = response_bins.sum(axis=(2, 3)).flatten() gist_descr[i * num_blocks : (i + 1) * num_blocks] = bin_sum diff --git a/detectree/lidar.py b/detectree/lidar.py index b68f766..12071ae 100644 --- a/detectree/lidar.py +++ b/detectree/lidar.py @@ -1,18 +1,17 @@ """Utilities to get canopy information from LiDAR data.""" - import laspy import numpy as np import pandas as pd import rasterio as rio +import shapely from rasterio import enums, features -from shapely import geometry from . import settings __all__ = ["rasterize_lidar", "LidarToCanopy"] -def rasterize_lidar(lidar_filepath, lidar_tree_values, ref_img_filepath): +def rasterize_lidar(lidar_filepath, lidar_tree_values, dst_shape, dst_transform): """Rasterize a LiDAR file. Transforms a LiDAR file into a raster aligned to `ref_img_filepath`, where each @@ -27,8 +26,10 @@ def rasterize_lidar(lidar_filepath, lidar_tree_values, ref_img_filepath): value will be passed to `laspy.file.File`. lidar_tree_values : int or list-like LiDAR point classes that correspond to trees. - ref_img_filepath : str, file object or pathlib.Path object - Reference raster image to which the LiDAR data will be rasterized. + dst_shape : tuple + Shape of the output raster. + dst_transform : Affine + Affine transformation of the output raster. Returns ------- @@ -42,23 +43,17 @@ def rasterize_lidar(lidar_filepath, lidar_tree_values, ref_img_filepath): cond = np.isin(c, lidar_tree_values) lidar_df = pd.DataFrame({"class_val": c[cond], "x": x[cond], "y": y[cond]}) - - with rio.open(ref_img_filepath) as src: - return features.rasterize( - shapes=[ - (geom, 1) - for geom, _ in zip( - [ - geometry.Point(x, y) - for x, y in zip(lidar_df["x"], lidar_df["y"]) - ], - lidar_df["class_val"], - ) - ], - out_shape=src.shape, - transform=src.transform, - merge_alg=enums.MergeAlg("ADD"), - ) + return features.rasterize( + shapes=[ + (geom, 1) + for geom in shapely.points( + *[lidar_df[coord].astype("float64").values for coord in ["x", "y"]] + ) + ], + out_shape=dst_shape, + transform=dst_transform, + merge_alg=enums.MergeAlg("ADD"), + ) class LidarToCanopy: @@ -111,7 +106,7 @@ def to_canopy_mask( output_filepath=None, postprocess_func=None, postprocess_func_args=None, - postprocess_func_kws=None + postprocess_func_kws=None, ): """ Transform a LiDAR file into a canopy mask. @@ -149,7 +144,11 @@ def to_canopy_mask( # iterations=self.num_opening_iterations), # iterations=self.num_dilation_iterations).astype( # self.output_dtype) * self.output_tree_val - lidar_arr = rasterize_lidar(lidar_filepath, lidar_tree_values, ref_img_filepath) + with rio.open(ref_img_filepath) as src: + meta = src.meta.copy() + lidar_arr = rasterize_lidar( + lidar_filepath, lidar_tree_values, meta["shape"], meta["transform"] + ) canopy_arr = lidar_arr >= self.tree_threshold if postprocess_func is not None: canopy_arr = postprocess_func( @@ -160,8 +159,6 @@ def to_canopy_mask( ).astype(self.output_dtype) if output_filepath is not None: - with rio.open(ref_img_filepath) as src: - meta = src.meta.copy() meta.update(dtype=self.output_dtype, count=1, nodata=self.output_nodata) with rio.open(output_filepath, "w", **meta) as dst: dst.write(canopy_arr, 1) diff --git a/detectree/pixel_features.py b/detectree/pixel_features.py index 586fbb2..225fa15 100644 --- a/detectree/pixel_features.py +++ b/detectree/pixel_features.py @@ -1,8 +1,8 @@ """Build pixel features.""" - import glob from os import path +import cv2 import dask import numpy as np from dask import diagnostics @@ -39,7 +39,7 @@ NUM_ILL_CHANNELS = 3 -class PixelFeaturesBuilder(object): +class PixelFeaturesBuilder: """Customize how pixel features are computed.""" def __init__( @@ -49,7 +49,7 @@ def __init__( num_orientations=None, neighborhood=None, min_neighborhood_range=None, - num_neighborhoods=None + num_neighborhoods=None, ): """ Initialize the pixel feature builder. @@ -180,7 +180,10 @@ def build_features_from_arr(self, img_rgb): # theta = orientation / num_orientations * np.pi theta = orientation * 180 / self.num_orientations oriented_kernel_arr = ndi.interpolation.rotate(base_kernel_arr, theta) - img_filtered = ndi.convolve(img_lab_l, oriented_kernel_arr) + # img_filtered = ndi.convolve(img_lab_l, oriented_kernel_arr) + img_filtered = cv2.filter2D( + img_lab_l, ddepth=-1, kernel=oriented_kernel_arr + ) img_filtered_vec = img_filtered.flatten() X[ :, self.num_color_features + i * self.num_orientations + j @@ -230,7 +233,7 @@ def build_features( img_dir=None, img_filename_pattern=None, method=None, - img_cluster=None + img_cluster=None, ): """ Build the pixel feature array for a list of images. @@ -256,6 +259,7 @@ def build_features( Method used in the train/test split img_cluster : int, optional The label of the cluster of images. Only used if `method` is 'cluster-II'. + Returns ------- X : numpy ndarray diff --git a/detectree/pixel_response.py b/detectree/pixel_response.py index c1b8275..523e654 100644 --- a/detectree/pixel_response.py +++ b/detectree/pixel_response.py @@ -14,7 +14,7 @@ class NonBinaryResponseError(Exception): pass -class PixelResponseBuilder(object): +class PixelResponseBuilder: """Customize how pixel responses (tree/non-tree labels) are computed.""" # It is really not necessary to use a class for this, but we do so for the sake of diff --git a/detectree/train_test_split.py b/detectree/train_test_split.py index 2bcea47..d0790fb 100644 --- a/detectree/train_test_split.py +++ b/detectree/train_test_split.py @@ -14,7 +14,7 @@ __all__ = ["TrainingSelector"] -class TrainingSelector(object): +class TrainingSelector: """Select the images/tiles to be used to train the classifier(s).""" def __init__( @@ -26,7 +26,7 @@ def __init__( gabor_frequencies=None, gabor_num_orientations=None, response_bins_per_axis=None, - num_color_bins=None + num_color_bins=None, ): """ Initialize the training selector. @@ -73,7 +73,7 @@ def __init__( the L*a*b color space. If no value is provided (default), the value will be taken from `seettings.GIST_DEFAULT_NUM_COLOR_BINS`. """ - super(TrainingSelector, self).__init__() + super().__init__() # get `None` keyword-arguments from settings if img_filename_pattern is None: @@ -158,7 +158,7 @@ def train_test_split( num_components=12, num_img_clusters=4, train_prop=0.01, - return_evr=False + return_evr=False, ): """ Select the image/tiles to be used for traning. diff --git a/detectree/utils.py b/detectree/utils.py index 3d616f1..717b7ec 100644 --- a/detectree/utils.py +++ b/detectree/utils.py @@ -286,7 +286,6 @@ def get_logger(*, level=None, name=None, filename=None): # if a logger with this name is not already set up if not getattr(logger, "handler_set", None): - # get today's date and construct a log filename todays_date = dt.datetime.today().strftime("%Y_%m_%d") log_filename = path.join( diff --git a/docs/src/advanced_topics.rst b/docs/src/advanced_topics.rst index 31e3158..a12cefe 100644 --- a/docs/src/advanced_topics.rst +++ b/docs/src/advanced_topics.rst @@ -2,7 +2,7 @@ Advanced Topics =============== -Most use cases of DetecTree only make use of the `TrainingSelector`, `ClassifierTrainer` and `Classifier` classes and their respective methods. Nevertheless, +Most use cases of DetecTree only make use of the `TrainingSelector`, `ClassifierTrainer` and `Classifier` classes and their respective methods. Nevertheless, See the `"background" example notebook `_ and the article of Yang et al. :cite:`yang2009tree` for more information. ---------------- @@ -21,8 +21,8 @@ Nevertheless, the way in which such image descriptor is computer can be customiz .. autofunction:: detectree.image_descriptor.compute_image_descriptor_from_filepath The GIST descriptor might also be directly computed from an array with the RGB representation of the image: - -.. autofunction:: detectree.image_descriptor.compute_image_descriptor + +.. autofunction:: detectree.image_descriptor.compute_image_descriptor On the other hand, in order to obtain a Gabor filter bank (e.g., for the `kernels` argument), the following function can be used: @@ -38,7 +38,7 @@ In order to perform a binary pixel-level classification of tree/non-tree pixels, :members: __init__, build_features The texture features are obtained by convolving the images with a filter bank, which is obtained by means of the following function: - + .. autofunction:: detectree.filters.get_texture_kernel The arguments of `Classifier.__init__` also serve to customize how the pixel response (i.e., tree/non-tree labels of each pixel) is computed, by forwarding them to the following class: diff --git a/docs/src/auto/references.el b/docs/src/auto/references.el index 2a2491b..f2821da 100644 --- a/docs/src/auto/references.el +++ b/docs/src/auto/references.el @@ -4,4 +4,3 @@ (LaTeX-add-bibitems "yang2009tree")) :bibtex) - diff --git a/docs/src/conf.py b/docs/src/conf.py index 960942f..75d491b 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # python_boilerplate documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. @@ -93,13 +92,18 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "default" +html_theme = "pydata_sphinx_theme" # 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 # documentation. # -# html_theme_options = {} +html_theme_options = { + "github_url": "https://github.com/martibosch/pylandstats", + "twitter_url": "https://twitter.com/mortybosch", + "pygment_light_style": "tango", +} + # 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, @@ -135,8 +139,8 @@ ( master_doc, "detectree.tex", - u"Detectree Documentation", - u"Martí Bosch", + "Detectree Documentation", + "Martí Bosch", "manual", ), ] @@ -145,9 +149,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, "detectree", u"Detectree Documentation", [author], 1) -] +man_pages = [(master_doc, "detectree", "Detectree Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------- @@ -158,7 +160,7 @@ ( master_doc, "detectree", - u"Detectree Documentation", + "Detectree Documentation", author, "detectree", "Tree detection from aerial imagery in Python", diff --git a/docs/src/index.rst b/docs/src/index.rst index 784b8de..9db17c9 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -6,11 +6,11 @@ DetecTree is a Pythonic library to classify tree/non-tree pixels from aerial ima .. toctree:: :maxdepth: 1 :caption: Reference Guide: - + train_test_split pixel_classification advanced_topics - + utils .. toctree:: @@ -18,11 +18,11 @@ DetecTree is a Pythonic library to classify tree/non-tree pixels from aerial ima :caption: Command-line interface (CLI): cli - + .. toctree:: :maxdepth: 1 :caption: Development: - + changelog contributing diff --git a/paper/paper.md b/paper/paper.md index d9af12d..d270b81 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -1,21 +1,24 @@ ---- +______________________________________________________________________ + title: 'DetecTree: Tree detection from aerial imagery in Python' tags: - - Python - - tree detection - - image segmentation - - remote sensing images - - GIS -authors: - - name: Martí Bosch - orcid: 0000-0001-8735-9144 - affiliation: 1 -affiliations: - - name: Urban and Regional Planning Community, École Polytechnique Fédérale de Lausanne, Switzerland - index: 1 -date: 4 March 2020 -bibliography: paper.bib ---- + +- Python +- tree detection +- image segmentation +- remote sensing images +- GIS + authors: +- name: Martí Bosch + orcid: 0000-0001-8735-9144 + affiliation: 1 + affiliations: +- name: Urban and Regional Planning Community, École Polytechnique Fédérale de Lausanne, Switzerland + index: 1 + date: 4 March 2020 + bibliography: paper.bib + +______________________________________________________________________ # Summary @@ -27,31 +30,28 @@ The aim of DetecTree is therefore to provide an open source library that perform DetecTree is based on the supervised learning approach of @yang2009tree, which requires an RGB aerial imagery dataset as the only input, and consists of the following steps: -* **Step 0**: split of the dataset into image tiles. Since aerial imagery datasets often already come as a mosaic of image tiles, this step might not be necessary. In any case, DetecTree provides a `split_into_tiles` function that can be used to divide a large image into a mosaic of tiles of a specified dimension. -* **Step 1**: selection of the tiles to be used for training a classifier. As a supervised learning task, the ground-truth maps must be provided for some subset of the dataset. Since this part is likely to involve manual work, it is crucial that the training set has as few tiles as possible. At the same time, to enhance the classifier's ability to detect trees in the diverse scenes of the dataset, the training set should contain as many of the diverse geographic features as possible. Thus, in order to optimize the representativity of the training set, the training tiles are selected according to their GIST descriptor [@oliva2001modeling], *i.e.*, a vector describing the key semantics of the tile's scene. More precisely, *k*-means clustering is applied to the GIST descriptors of all the tiles, with the number of clusters *k* set to the number of tiles of the training set (by default, one percent of the tiles is used). Then, for each cluster, the tile whose GIST descriptor is closest to the cluster's centroid is added to the training set. In DetecTree, this is done by the `train_test_split` method of the `TrainingSelector` class. -* **Step 2**: provision of the ground truth tree/non-tree masks for the training tiles. For each tile of the training set, the ground-truth tree/non-tree masks must be provided to get the pixel-level responses that will be used to train the classifier. To that end, an image editing software such as GIMP or Adobe Photoshop might be used. Alternatively, if LIDAR data for the training tiles is available, it might also be exploited to create the ground truth masks. -* **Step 3**: train a binary pixel-level classifier. For each pixel of the training tiles, a vector of 27 features is computed, where 6, 18 and 3 features capture characteristics of color, texture and entropy respectively. A binary AdaBoost classifier [@freund1995desicion] is then trained by mapping the feature vector of each pixel to its class in the ground truth masks (*i.e.*, tree or non-tree). -* **Step 4**: tree detection in the testing tiles. Given a trained classifier, the `classify_img` and `classify_imgs` methods of the `Classifier` class can respectively be used to classify the tree pixels of a single image tile or of multiple image tiles at scale. For each image tile, the pixel-level classification is refined by means of a graph cuts algorithm [@boykov2004experimental] to avoid sparse pixels classified as trees by enforcing consistency between adjacent tree pixels. An example of an image tile, its pre-refinement pixel-level classification and the final refined result is displayed below: +- **Step 0**: split of the dataset into image tiles. Since aerial imagery datasets often already come as a mosaic of image tiles, this step might not be necessary. In any case, DetecTree provides a `split_into_tiles` function that can be used to divide a large image into a mosaic of tiles of a specified dimension. +- **Step 1**: selection of the tiles to be used for training a classifier. As a supervised learning task, the ground-truth maps must be provided for some subset of the dataset. Since this part is likely to involve manual work, it is crucial that the training set has as few tiles as possible. At the same time, to enhance the classifier's ability to detect trees in the diverse scenes of the dataset, the training set should contain as many of the diverse geographic features as possible. Thus, in order to optimize the representativity of the training set, the training tiles are selected according to their GIST descriptor \[@oliva2001modeling\], *i.e.*, a vector describing the key semantics of the tile's scene. More precisely, *k*-means clustering is applied to the GIST descriptors of all the tiles, with the number of clusters *k* set to the number of tiles of the training set (by default, one percent of the tiles is used). Then, for each cluster, the tile whose GIST descriptor is closest to the cluster's centroid is added to the training set. In DetecTree, this is done by the `train_test_split` method of the `TrainingSelector` class. +- **Step 2**: provision of the ground truth tree/non-tree masks for the training tiles. For each tile of the training set, the ground-truth tree/non-tree masks must be provided to get the pixel-level responses that will be used to train the classifier. To that end, an image editing software such as GIMP or Adobe Photoshop might be used. Alternatively, if LIDAR data for the training tiles is available, it might also be exploited to create the ground truth masks. +- **Step 3**: train a binary pixel-level classifier. For each pixel of the training tiles, a vector of 27 features is computed, where 6, 18 and 3 features capture characteristics of color, texture and entropy respectively. A binary AdaBoost classifier \[@freund1995desicion\] is then trained by mapping the feature vector of each pixel to its class in the ground truth masks (*i.e.*, tree or non-tree). +- **Step 4**: tree detection in the testing tiles. Given a trained classifier, the `classify_img` and `classify_imgs` methods of the `Classifier` class can respectively be used to classify the tree pixels of a single image tile or of multiple image tiles at scale. For each image tile, the pixel-level classification is refined by means of a graph cuts algorithm \[@boykov2004experimental\] to avoid sparse pixels classified as trees by enforcing consistency between adjacent tree pixels. An example of an image tile, its pre-refinement pixel-level classification and the final refined result is displayed below: ![Example of an image tile (left), its pre-refinement pixel-level classification (center) and the final refined result (right).](figure.png) -Similar methods of tree classification from aerial imagery include the work of @jain2019efficient, who follow the train/test split method based on GIST descriptors as proposed by @yang2009tree but rely on the Mask R-CNN framework [@he2017mask] instead of the AdaBoost classifier. Another approach by @tianyang2018single employs a cascade neural network over texture and color features which detects single trees in a variety of forest images. Nonetheless, since the former approaches ultimately aim at single tree detection, the accuracy evaluation metrics that they provide are hard to compare with the pixel-level classification accuracy of DetecTree. The experiments performed by @yang2009tree in New York achieve a pixel classification accuracy of 91.7%, whereas the example applications of DetecTree in Zurich and Lausanne achieve accuracies of 85.98% and 91.75% respectively. +Similar methods of tree classification from aerial imagery include the work of @jain2019efficient, who follow the train/test split method based on GIST descriptors as proposed by @yang2009tree but rely on the Mask R-CNN framework \[@he2017mask\] instead of the AdaBoost classifier. Another approach by @tianyang2018single employs a cascade neural network over texture and color features which detects single trees in a variety of forest images. Nonetheless, since the former approaches ultimately aim at single tree detection, the accuracy evaluation metrics that they provide are hard to compare with the pixel-level classification accuracy of DetecTree. The experiments performed by @yang2009tree in New York achieve a pixel classification accuracy of 91.7%, whereas the example applications of DetecTree in Zurich and Lausanne achieve accuracies of 85.98% and 91.75% respectively. -The code of DetecTree is organized following an object-oriented approach, and relies on NumPy [@van2011numpy] to represent most data structures and perform operations upon them in a vectorized manner. The Scikit-learn library [@pedregosa2011scikit] is used to implement the AdaBoost pixel-level classifier as well as to perform the *k*-means clustering to select the training tiles. The computation of pixel-level features and GIST descriptors makes use of various features provided by the Scikit-image [@van2014scikit] and SciPy [@virtanen2020scipy] libraries. On the other hand, the classification refinement employs the graph cuts algorithm implementation provided by the library [PyMaxFlow](https://github.com/pmneila/PyMaxflow). Finally, when possible, DetecTree uses the Dask library [@rocklin2015dask] to perform various computations in parallel. +The code of DetecTree is organized following an object-oriented approach, and relies on NumPy \[@van2011numpy\] to represent most data structures and perform operations upon them in a vectorized manner. The Scikit-learn library \[@pedregosa2011scikit\] is used to implement the AdaBoost pixel-level classifier as well as to perform the *k*-means clustering to select the training tiles. The computation of pixel-level features and GIST descriptors makes use of various features provided by the Scikit-image \[@van2014scikit\] and SciPy \[@virtanen2020scipy\] libraries. On the other hand, the classification refinement employs the graph cuts algorithm implementation provided by the library [PyMaxFlow](https://github.com/pmneila/PyMaxflow). Finally, when possible, DetecTree uses the Dask library \[@rocklin2015dask\] to perform various computations in parallel. The features of DetecTree are implemented in a manner that enhances the flexibility of the library so that the user can integrate it into complex computational workflows, and also provide custom arguments for the technical aspects. Furthermore, the functionalities of DetecTree can be used through its Python API as well as through its command-line interface (CLI), which is implemented by means of the Click Python package. - # Availability -The source code of DetecTree is fully available at [a GitHub repository](https://github.com/martibosch/detectree). A dedicated Python package has been created and is hosted at the [Python Package Index (PyPI)](https://pypi.org/project/detectree/). The documentation site is hosted at [Read the Docs](https://detectree.readthedocs.io/), and an example repository with Jupyter notebooks of an example application to an openly-available orthophoto of Zurich is provided at a [dedicated GitHub repository](https://github.com/martibosch/detectree-example), which can be executed interactively online by means of the Binder web service [@jupyter2018binder]. An additional example use of DetecTree can be found at a [dedicated GitHub repository](https://github.com/martibosch/lausanne-tree-canopy) with the materials to obtain a tree canopy map for the urban agglomeration of Lausanne from the SWISSIMAGE 2016 orthophoto [@swisstopo2019swissimage]. +The source code of DetecTree is fully available at [a GitHub repository](https://github.com/martibosch/detectree). A dedicated Python package has been created and is hosted at the [Python Package Index (PyPI)](https://pypi.org/project/detectree/). The documentation site is hosted at [Read the Docs](https://detectree.readthedocs.io/), and an example repository with Jupyter notebooks of an example application to an openly-available orthophoto of Zurich is provided at a [dedicated GitHub repository](https://github.com/martibosch/detectree-example), which can be executed interactively online by means of the Binder web service \[@jupyter2018binder\]. An additional example use of DetecTree can be found at a [dedicated GitHub repository](https://github.com/martibosch/lausanne-tree-canopy) with the materials to obtain a tree canopy map for the urban agglomeration of Lausanne from the SWISSIMAGE 2016 orthophoto \[@swisstopo2019swissimage\]. Unit tests are run within the [Travis CI](https://travis-ci.org/martibosch/detectree) platform every time that new commits are pushed to the GitHub repository. Additionally, test coverage [is reported on Coveralls](https://coveralls.io/github/martibosch/detectree?branch=master). - # Acknowledgments This research has been supported by the École Polytechnique Fédérale de Lausanne. - # References diff --git a/pyproject.toml b/pyproject.toml index ed76fd6..0f1bdfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,92 @@ -[tool.black] +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "detectree" +version = "0.4.1" +description = "Tree detection from aerial imagery in Python." +readme = "README.md" +authors = [ + { name = "Martí Bosch", email = "marti.bosch@epfl.ch" }, +] +license = { text = "GPL-3.0" } +classifiers = [ + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.8" +dependencies = [ + "dask[delayed,distributed]", + "joblib", + "laspy >= 2.0.0", + "numpy >= 1.15", + "opencv-python >= 4.0.0", + "pandas >= 0.23", + "pymaxflow >= 1.0.0", + "rasterio >= 1.0.0", + "scikit-image", + "scikit-learn", + "scipy >= 1.0.0", + "tqdm", +] + +[project.urls] +Repository = "https://github.com/martibosch/detectree" + +[project.scripts] +detectree = "detectree.cli.main:cli" + +[project.optional-dependencies] +test = ["coverage[toml]", "pytest", "pytest-cov", "ruff"] +dev = ["build", "commitizen", "pre-commit", "pip", "toml", "tox", "twine"] +doc = ["m2r2", "pydata-sphinx-theme", "sphinx"] + +[tool.setuptools.packages.find] +include = ["detectree", "detectree.*"] + +[tool.ruff] line-length = 88 -[tool.isort] -known_first_party = "detectree" -default_section = "THIRDPARTY" -forced_separate = "test_detectree" -line_length = 88 -profile = "black" +[tool.ruff.lint] +select = ["D", "E", "F", "I"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.lint.isort] +known-first-party = ["detectree"] + +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["F403"] +"tests/test_detectree.py" = ["D"] +"docs/src/conf.py" = ["D"] + +[tool.coverage.run] +source = ["detectree"] + +[tool.coverage.report] +exclude_lines = [ + "if self.debug:", + "pragma: no cover", + "raise NotImplementedError", + "except ModuleNotFoundError", + "except ImportError", +] +ignore_errors = true +omit = ["tests/*", "docs/conf.py"] + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "v$version" +version_provider = "pep621" +version_files = [ + "detectree/__init__.py", + "pyproject.toml:version" +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index c4b5795..0000000 --- a/setup.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[bdist_wheel] -universal=1 - -[metadata] -description-file=README.md - -[flake8] -docstring-convention = numpy -max-line-length = 88 -exclude = ./build/* -ignore = E203,W503 -per-file-ignores = - detectree/__init__.py:F401 - tests/test_detectree.py:F401 diff --git a/setup.py b/setup.py index 6ab844f..0dcbea7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ See license in LICENSE file. """ -from io import open # compatible enconding parameter from os import path from setuptools import find_packages, setup diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..35fc191 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""detectree tests.""" diff --git a/tests/test_detectree.py b/tests/test_detectree.py index f13f3e5..983cba7 100644 --- a/tests/test_detectree.py +++ b/tests/test_detectree.py @@ -18,22 +18,7 @@ class TestImports(unittest.TestCase): def test_base_imports(self): - import glob - import itertools - from os import path - - import dask - import maxflow as mf - import numpy as np - import pandas as pd - import rasterio as rio - from dask import diagnostics - from rasterio import windows - from scipy import ndimage as ndi - from skimage import color, measure, morphology - from skimage.filters import gabor_kernel, rank - from skimage.util import shape - from sklearn import cluster, decomposition, ensemble, metrics, preprocessing + pass class TestTrainTestSplit(unittest.TestCase): @@ -429,7 +414,6 @@ def tearDown(self): shutil.rmtree(self.tmp_output_dir) def test_classifier_trainer(self): - # test that all the combinations of arguments of the `train_classifier` method # return an instance of `sklearn.ensemble.AdaBoostClassifier` option 1a: # `split_df` and `response_img_dir` with implicit method (note that we are using diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..552b521 --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +isolated_build = true +envlist = py38, py39, py310, py311, py312, lint + +[gh-actions] +python = + 3.12: py312 + 3.11: py311 + 3.10: py310 + 3.9: py39 + 3.8: py38 + +[testenv:lint] +whitelist_externals = + build + sphinx-build + twine +extras = + test + doc + dev +commands = + python -m build + sphinx-build docs docs/_build + twine check dist/* + +[testenv] +setenv = + PYTHONPATH = {toxinidir} +whitelist_externals = + pytest +extras = + test +commands = + pytest -s --cov=detectree --cov-append --cov-report=xml --cov-report term-missing tests